'use client'; import { useCallback, useEffect, useState } from 'react'; import Link from 'next/link'; import { usePathname, useRouter } from 'next/navigation'; import { api } from '@/lib/api'; import { clearToken, isLoggedIn } from '@/lib/auth'; import { ENV_LABEL, getViewEnv, setViewEnv, type DataSourceEnv } from '@/lib/env'; import { RESOURCE_CONFIGS } from '@/lib/resources'; type DataSource = DataSourceEnv; const FIXED_NAV = [ { href: '/', label: '数据看板' }, { href: '/users', label: '用户' }, { href: '/orders', label: '订单' }, { href: '/refund-requests', label: '退款申请' }, { href: '/notify-logs', label: '通知日志' }, { href: '/settings', label: '系统配置' }, ]; /** * 底部「数据源」区域预留的高度(px)。 * * 这个区块用 position:fixed 钉在视口左下角,脱离文档流, * 所以要在这里留出等高的空白,免得它盖住最后几个菜单项。 * 数值按内容量给足余量,字体缩放时也不至于被遮住。 */ const FOOTER_RESERVE_PX = 290; export default function AdminLayout({ children }: { children: React.ReactNode }) { const router = useRouter(); const pathname = usePathname(); const [ready, setReady] = useState(false); /** 管理端「查看哪个库」——只影响自己 */ const [viewEnv, setView] = useState('production'); /** 线上小程序实际读的库 */ const [miniEnv, setMiniEnv] = useState('production'); const [developAvailable, setDevelopAvailable] = useState(false); const [confirmOpen, setConfirmOpen] = useState(false); const [switching, setSwitching] = useState(false); const [error, setError] = useState(''); /** 小屏(手机)上的侧边栏抽屉开关。桌面端忽略它。 */ const [navOpen, setNavOpen] = useState(false); const loadState = useCallback(() => { setView(getViewEnv()); api .stats() .then((s) => { setMiniEnv(s.miniProgramEnv === 'develop' ? 'develop' : 'production'); setDevelopAvailable(s.developAvailable); }) .catch(() => { /* 取不到时按生产库显示,不阻塞页面 */ }); }, []); useEffect(() => { if (!isLoggedIn()) { router.replace('/login'); return; } setReady(true); loadState(); }, [router, loadState]); /** 只切自己的查看视角,不动线上数据来源。 */ const changeViewEnv = (next: DataSource) => { if (next === viewEnv) return; setViewEnv(next); setView(next); window.location.reload(); }; /** 真正切换线上小程序的数据源,必须二次确认。 */ const applyMiniProgramEnv = () => { const next: DataSource = miniEnv === 'production' ? 'develop' : 'production'; setSwitching(true); setError(''); api .switchMiniProgramDataSource(next) .then(() => { setMiniEnv(next); setConfirmOpen(false); window.location.reload(); }) .catch((err: { message: string }) => { setError(err.message || '切换失败'); setSwitching(false); }); }; if (!ready) { return
加载中…
; } const nextMiniEnv: DataSource = miniEnv === 'production' ? 'develop' : 'production'; return (
{/* 小屏顶栏:桌面端隐藏,点「菜单」拉出侧边栏 */}
叶雷讲数学 · 管理后台
{/* 侧边栏:固定定位、高度恒等于视口。导航区自己滚动。 注意它只承载「导航」,底部数据源区不在它里面——见下面那个独立 fixed 区块。 小屏(
叶雷讲数学
管理后台
{/* 给下面那个 fixed 区块让位,避免遮住最后几个菜单 */} {/* 数据源区:独立的 fixed 元素,钉在视口左下角。 它不在侧边栏里,也不参与任何 flex 布局,因此页面长短、导航多少、窗口高低都影响不到它。 小屏时它跟着侧边栏一起滑出/滑入(同样的 translate),所以抽屉关着时看不到它。 */}
查看的数据源
{(['production', 'develop'] as DataSource[]).map((key) => { const disabled = key === 'develop' && !developAvailable; return ( ); })}

只影响你自己的查看与编辑,不影响小程序

线上小程序数据源
{ENV_LABEL[miniEnv]}
{/* 小屏抽屉打开时的遮罩,点它收回 */} {navOpen && (
setNavOpen(false)} /> )}
{miniEnv === 'develop' && (
⚠ 线上小程序当前读的是开发库 ——所有用户打开小程序看到的都是开发库数据,测完请及时切回生产库。
)} {miniEnv !== 'develop' && viewEnv === 'develop' && (
你正在查看开发库的数据(仅影响你自己,小程序仍在读生产库)
)}
{children}
{confirmOpen && (

确认切换线上小程序数据源?

即将把线上小程序从 {ENV_LABEL[miniEnv]} 切换到 {ENV_LABEL[nextMiniEnv]}。

{nextMiniEnv === 'develop' ? (

切换后,所有用户在微信里打开小程序看到的都是开发库数据 (旧开发库:132 个用户、3 门课程,首页两个栏目为空)。 他们会以为课程丢失,请务必在测试结束后切回生产库。

) : (

将恢复为生产库,用户看到的即真实数据。

)} {error &&

{error}

}
)}
); } function NavItem({ href, label, active, onNavigate, }: { href: string; label: string; active: boolean; /** 小屏:点完菜单就把抽屉收回去 */ onNavigate?: () => void; }) { return ( {label} ); }