移动端适配
This commit is contained in:
1 parent
fcb536a6fe
commit
b248d237f2
9 files changed
+78
-19
No files matched your search
@@ -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>
|
||||
|
||||
@@ -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>
|
||||
|
||||
Reference in new issue
Block a user