移动端适配

This commit is contained in:
Yeomai committed 2026-10-01 15:59:48 +08:00
1 parent fcb536a6fe
commit b248d237f2
9 files changed
+78 -19

No files matched your search

+64 -7
View File
@@ -41,6 +41,8 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
const [confirmOpen, setConfirmOpen] = useState(false);
const [switching, setSwitching] = useState(false);
const [error, setError] = useState('');
/** 小屏(手机)上的侧边栏抽屉开关。桌面端忽略它。 */
const [navOpen, setNavOpen] = useState(false);
const loadState = useCallback(() => {
setView(getViewEnv());
@@ -98,11 +100,31 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
return (
<div className="min-h-screen">
{/* 小屏顶栏:桌面端隐藏,点「菜单」拉出侧边栏 */}
<header className="sticky top-0 z-30 flex items-center gap-3 border-b border-gray-200 bg-white px-4 py-3 md:hidden">
<button
type="button"
onClick={() => setNavOpen(true)}
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm text-gray-700"
aria-label="打开菜单"
>
☰ 菜单
</button>
<span className="truncate text-sm font-semibold text-gray-900">叶雷讲数学 · 管理后台</span>
</header>
{/*
侧边栏:固定定位、高度恒等于视口。导航区自己滚动。
注意它只承载「导航」,底部数据源区不在它里面——见下面那个独立 fixed 区块。
小屏(<md)时它变成抽屉:默认滑出屏幕外,点顶栏「菜单」拉进来,
点遮罩或任一菜单项收回去。
*/}
<aside className="fixed left-0 top-0 z-30 h-screen w-60 border-r border-gray-200 bg-white">
<aside
className={`fixed left-0 top-0 z-50 h-screen w-60 border-r border-gray-200 bg-white transition-transform duration-200 md:translate-x-0 ${
navOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="flex h-full flex-col">
<div className="shrink-0 border-b border-gray-200 px-4 py-4">
<div className="text-sm font-semibold text-gray-900">叶雷讲数学</div>
@@ -111,11 +133,23 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
<nav className="min-h-0 flex-1 overflow-y-auto p-2">
{FIXED_NAV.map((item) => (
<NavItem key={item.href} href={item.href} label={item.label} active={pathname === item.href} />
<NavItem
key={item.href}
href={item.href}
label={item.label}
active={pathname === item.href}
onNavigate={() => setNavOpen(false)}
/>
))}
<div className="px-3 pt-4 pb-1 text-xs text-gray-400">内容管理</div>
{RESOURCE_CONFIGS.map((r) => (
<NavItem key={r.key} href={`/${r.key}`} label={r.title} active={pathname === `/${r.key}`} />
<NavItem
key={r.key}
href={`/${r.key}`}
label={r.title}
active={pathname === `/${r.key}`}
onNavigate={() => setNavOpen(false)}
/>
))}
</nav>
@@ -127,8 +161,14 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
{/*
数据源区:独立的 fixed 元素,钉在视口左下角。
它不在侧边栏里,也不参与任何 flex 布局,因此页面长短、导航多少、窗口高低都影响不到它。
小屏时它跟着侧边栏一起滑出/滑入(同样的 translate),所以抽屉关着时看不到它。
*/}
<div className="fixed bottom-0 left-0 z-40 w-60 border-t border-gray-200 bg-white p-2">
<div
className={`fixed bottom-0 left-0 z-50 w-60 border-t border-gray-200 bg-white p-2 transition-transform duration-200 md:translate-x-0 ${
navOpen ? 'translate-x-0' : '-translate-x-full'
}`}
>
<div className="px-3 pb-1 text-xs text-gray-400">查看的数据源</div>
<div className="flex gap-1 px-2">
{(['production', 'develop'] as DataSource[]).map((key) => {
@@ -191,7 +231,12 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
</button>
</div>
<main className="ml-60">
{/* 小屏抽屉打开时的遮罩,点它收回 */}
{navOpen && (
<div className="fixed inset-0 z-40 bg-black/40 md:hidden" onClick={() => setNavOpen(false)} />
)}
<main className="md:ml-60">
{miniEnv === 'develop' && (
<div className="border-b border-orange-300 bg-orange-100 px-6 py-2 text-sm font-medium text-orange-800">
⚠ 线上小程序当前读的是<span className="font-bold">开发库</span>
@@ -203,7 +248,7 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
你正在查看<span className="font-bold">开发库</span>的数据(仅影响你自己,小程序仍在读生产库)
</div>
)}
<div className="p-6">{children}</div>
<div className="p-4 md:p-6">{children}</div>
</main>
{confirmOpen && (
@@ -253,10 +298,22 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
);
}
function NavItem({ href, label, active }: { href: string; label: string; active: boolean }) {
function NavItem({
href,
label,
active,
onNavigate,
}: {
href: string;
label: string;
active: boolean;
/** 小屏:点完菜单就把抽屉收回去 */
onNavigate?: () => void;
}) {
return (
<Link
href={href}
onClick={onNavigate}
className={`block rounded-md px-3 py-2 text-sm transition ${
active ? 'bg-blue-50 font-medium text-blue-700' : 'text-gray-600 hover:bg-gray-100'
}`}
@@ -101,7 +101,7 @@ export default function NotifyLogsPage() {
<Empty />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<table className="w-full min-w-max text-sm">
<thead>
<tr className="border-b border-gray-200 text-left text-xs text-gray-500">
<th className="px-2 py-2">时间</th>
@@ -130,7 +130,7 @@ export default function OrdersPage() {
<Card
title={`共 ${total} 笔订单`}
action={
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Select
value={status}
onChange={setStatus}
@@ -155,7 +155,7 @@ export default function OrdersPage() {
<Empty text="暂无订单" />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<table className="w-full min-w-max text-sm">
<thead>
<tr className="border-b border-gray-200 text-left text-xs text-gray-500">
<th className="px-2 py-2">订单号</th>
+1 -1
View File
@@ -52,7 +52,7 @@ export default function DashboardPage() {
<div className="text-sm text-gray-400">加载中…</div>
) : (
<>
<div className="grid grid-cols-2 gap-4 lg:grid-cols-4">
<div className="grid grid-cols-1 gap-4 sm:grid-cols-2 lg:grid-cols-4">
<Metric label="用户总数" value={String(stats.users)} />
<Metric label="在售课程" value={String(stats.courses)} />
<Metric label="累计订单" value={String(stats.orders)} />
@@ -73,7 +73,7 @@ export default function RefundRequestsPage() {
<Empty />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<table className="w-full min-w-max text-sm">
<thead>
<tr className="border-b border-gray-200 text-left text-xs text-gray-500">
<th className="px-2 py-2">申请时间</th>
@@ -166,7 +166,7 @@ export default function UsersPage() {
title={`共 ${total} 位用户`}
action={
/* 筛选、排序、搜索排在一行;改任意一项都会从第 1 页重新拉 */
<div className="flex flex-nowrap items-center gap-2">
<div className="flex flex-wrap items-center gap-2">
<div className="w-32">
<Select
value={statusFilter}
@@ -217,7 +217,7 @@ export default function UsersPage() {
<Empty />
) : (
<div className="overflow-x-auto">
<table className="w-full text-sm">
<table className="w-full min-w-max text-sm">
<thead>
<tr className="border-b border-gray-200 text-left text-xs text-gray-500">
<th className="px-2 py-2">ID</th>