移动端适配

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>
@@ -169,7 +169,7 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
<Card
title={`${config.title}(共 ${total} 条)`}
action={
<div className="flex gap-2">
<div className="flex flex-wrap gap-2">
<Input value={keyword} onChange={setKeyword} placeholder="搜索…" />
<Button variant="ghost" onClick={() => load(1, keyword)}>
搜索
@@ -192,7 +192,8 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
zoom: 0.8 —— 整张卡(文字/图标/内边距一起)等比缩放
用 zoom 而不是单独改字号,是因为长宽要一起缩;只改宽度会变成「更窄但一样高」。
*/
<div className="grid grid-cols-[repeat(auto-fill,minmax(290px,1fr))] gap-3">
/* min(100%,290px):窄屏时卡片能缩到屏幕宽度以内,不会被撑出横向滚动 */
<div className="grid grid-cols-[repeat(auto-fill,minmax(min(100%,290px),1fr))] gap-3">
{items.map((row) => {
const card = config.card!;
const coverPath = card.cover(row);
@@ -276,7 +277,7 @@ export function ResourceTable({ config }: { config: ResourceConfig }) {
</div>
) : (
<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>
+2 -1
View File
@@ -94,7 +94,8 @@ export function Card({ title, children, action }: { title?: string; children: Re
return (
<section className="rounded-lg border border-gray-200 bg-white p-5 shadow-sm">
{(title || action) && (
<header className="mb-4 flex items-center justify-between">
/* 小屏时标题和操作区换行,免得操作按钮被挤出屏幕 */
<header className="mb-4 flex flex-wrap items-center justify-between gap-2">
{title && <h2 className="text-base font-semibold text-gray-900">{title}</h2>}
{action}
</header>
+1 -1
View File
@@ -1 +1 @@
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated
{"fileNames":["./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es5.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.dom.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.core.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.collection.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.generator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.iterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.proxy.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.reflect.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2015.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.array.include.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2016.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.arraybuffer.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2017.typedarrays.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asyncgenerator.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.asynciterable.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2018.regexp.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.object.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.symbol.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2019.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.bigint.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.date.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.sharedmemory.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.symbol.wellknown.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2020.number.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.promise.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.string.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.weakref.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2021.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.array.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.error.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.intl.d.ts","./node_modules/.pnpm/typescript@5.9.3/node_modules/typescript/lib/lib.es2022.object.d.ts",".Line truncated