大修改
This commit is contained in:
1 parent
a1cbc9404a
commit
e81481623c
47 files changed
+1453
-285
No files matched your search
@@ -1,11 +1,15 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState } from 'react';
|
||||
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: '用户' },
|
||||
@@ -13,10 +17,41 @@ const FIXED_NAV = [
|
||||
{ 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<DataSource>('production');
|
||||
/** 线上小程序实际读的库 */
|
||||
const [miniEnv, setMiniEnv] = useState<DataSource>('production');
|
||||
const [developAvailable, setDevelopAvailable] = useState(false);
|
||||
const [confirmOpen, setConfirmOpen] = useState(false);
|
||||
const [switching, setSwitching] = useState(false);
|
||||
const [error, setError] = useState('');
|
||||
|
||||
const loadState = useCallback(() => {
|
||||
setView(getViewEnv());
|
||||
api
|
||||
.stats()
|
||||
.then((s) => {
|
||||
setMiniEnv(s.miniProgramEnv === 'develop' ? 'develop' : 'production');
|
||||
setDevelopAvailable(s.developAvailable);
|
||||
})
|
||||
.catch(() => {
|
||||
/* 取不到时按生产库显示,不阻塞页面 */
|
||||
});
|
||||
}, []);
|
||||
|
||||
useEffect(() => {
|
||||
if (!isLoggedIn()) {
|
||||
@@ -24,44 +59,194 @@ export default function AdminLayout({ children }: { children: React.ReactNode })
|
||||
return;
|
||||
}
|
||||
setReady(true);
|
||||
}, [router]);
|
||||
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 <div className="flex min-h-screen items-center justify-center text-sm text-gray-400">加载中…</div>;
|
||||
}
|
||||
|
||||
const nextMiniEnv: DataSource = miniEnv === 'production' ? 'develop' : 'production';
|
||||
|
||||
return (
|
||||
<div className="flex min-h-screen">
|
||||
<aside className="w-56 shrink-0 border-r border-gray-200 bg-white">
|
||||
<div className="border-b border-gray-200 px-4 py-4">
|
||||
<div className="text-sm font-semibold text-gray-900">叶雷讲数学</div>
|
||||
<div className="text-xs text-gray-400">管理后台</div>
|
||||
</div>
|
||||
<nav className="p-2">
|
||||
{FIXED_NAV.map((item) => (
|
||||
<NavItem key={item.href} href={item.href} label={item.label} active={pathname === item.href} />
|
||||
))}
|
||||
<div className="min-h-screen">
|
||||
{/*
|
||||
侧边栏:固定定位、高度恒等于视口。导航区自己滚动。
|
||||
注意它只承载「导航」,底部数据源区不在它里面——见下面那个独立 fixed 区块。
|
||||
*/}
|
||||
<aside className="fixed left-0 top-0 z-30 h-screen w-60 border-r border-gray-200 bg-white">
|
||||
<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>
|
||||
<div className="text-xs text-gray-400">管理后台</div>
|
||||
</div>
|
||||
|
||||
<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}`} />
|
||||
))}
|
||||
</nav>
|
||||
<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} />
|
||||
))}
|
||||
<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}`} />
|
||||
))}
|
||||
</nav>
|
||||
|
||||
<div className="border-t border-gray-200 p-2">
|
||||
<button
|
||||
onClick={() => {
|
||||
clearToken();
|
||||
router.replace('/login');
|
||||
}}
|
||||
className="w-full rounded-md px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
{/* 给下面那个 fixed 区块让位,避免遮住最后几个菜单 */}
|
||||
<div className="shrink-0" style={{ height: FOOTER_RESERVE_PX }} aria-hidden="true" />
|
||||
</div>
|
||||
</aside>
|
||||
|
||||
<main className="flex-1 p-6">{children}</main>
|
||||
{/*
|
||||
数据源区:独立的 fixed 元素,钉在视口左下角。
|
||||
它不在侧边栏里,也不参与任何 flex 布局,因此页面长短、导航多少、窗口高低都影响不到它。
|
||||
*/}
|
||||
<div className="fixed bottom-0 left-0 z-40 w-60 border-t border-gray-200 bg-white p-2">
|
||||
<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) => {
|
||||
const disabled = key === 'develop' && !developAvailable;
|
||||
return (
|
||||
<button
|
||||
key={key}
|
||||
onClick={() => changeViewEnv(key)}
|
||||
disabled={disabled}
|
||||
title={disabled ? '服务端未配置开发库' : '只影响你自己的查看与编辑'}
|
||||
className={`flex-1 rounded-md border px-2 py-1.5 text-xs transition ${
|
||||
viewEnv === key
|
||||
? key === 'develop'
|
||||
? 'border-orange-400 bg-orange-50 font-medium text-orange-700'
|
||||
: 'border-blue-400 bg-blue-50 font-medium text-blue-700'
|
||||
: 'border-gray-200 text-gray-500 hover:bg-gray-50'
|
||||
} ${disabled ? 'cursor-not-allowed opacity-40' : ''}`}
|
||||
>
|
||||
{ENV_LABEL[key]}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
<p className="px-3 pt-1 pb-2 text-[11px] leading-tight text-gray-400">
|
||||
只影响你自己的查看与编辑,不影响小程序
|
||||
</p>
|
||||
|
||||
<div className="border-t border-gray-100 px-3 pt-3 pb-1 text-xs text-gray-400">
|
||||
线上小程序数据源
|
||||
</div>
|
||||
<div className="px-2 pb-2">
|
||||
<div className="mb-1.5 flex items-center gap-1.5 px-1 text-sm">
|
||||
<span
|
||||
className={`inline-block h-2 w-2 rounded-full ${
|
||||
miniEnv === 'develop' ? 'bg-orange-500' : 'bg-green-500'
|
||||
}`}
|
||||
/>
|
||||
<span className={miniEnv === 'develop' ? 'font-medium text-orange-700' : 'text-gray-700'}>
|
||||
{ENV_LABEL[miniEnv]}
|
||||
</span>
|
||||
</div>
|
||||
<button
|
||||
onClick={() => setConfirmOpen(true)}
|
||||
disabled={!developAvailable || switching}
|
||||
title={developAvailable ? undefined : '服务端未配置开发库'}
|
||||
className="w-full rounded-md border border-gray-300 bg-white px-2 py-1.5 text-xs text-gray-700 transition hover:bg-gray-50 disabled:cursor-not-allowed disabled:opacity-40"
|
||||
>
|
||||
切换到{ENV_LABEL[nextMiniEnv]}
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<button
|
||||
onClick={() => {
|
||||
clearToken();
|
||||
router.replace('/login');
|
||||
}}
|
||||
className="w-full rounded-md px-3 py-2 text-left text-sm text-gray-600 hover:bg-gray-100"
|
||||
>
|
||||
退出登录
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<main className="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>
|
||||
——所有用户打开小程序看到的都是开发库数据,测完请及时切回生产库。
|
||||
</div>
|
||||
)}
|
||||
{miniEnv !== 'develop' && viewEnv === 'develop' && (
|
||||
<div className="border-b border-blue-200 bg-blue-50 px-6 py-2 text-sm text-blue-800">
|
||||
你正在查看<span className="font-bold">开发库</span>的数据(仅影响你自己,小程序仍在读生产库)
|
||||
</div>
|
||||
)}
|
||||
<div className="p-6">{children}</div>
|
||||
</main>
|
||||
|
||||
{confirmOpen && (
|
||||
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/40 p-4">
|
||||
<div className="w-full max-w-md rounded-lg bg-white p-5 shadow-xl">
|
||||
<h3 className="mb-3 text-base font-semibold text-gray-900">确认切换线上小程序数据源?</h3>
|
||||
<div className="space-y-2 text-sm text-gray-600">
|
||||
<p>
|
||||
即将把线上小程序从
|
||||
<span className="mx-1 font-medium">{ENV_LABEL[miniEnv]}</span>
|
||||
切换到
|
||||
<span className="mx-1 font-medium text-orange-700">{ENV_LABEL[nextMiniEnv]}</span>。
|
||||
</p>
|
||||
{nextMiniEnv === 'develop' ? (
|
||||
<p className="rounded-md bg-orange-50 px-3 py-2 text-orange-800">
|
||||
切换后,<span className="font-medium">所有用户</span>在微信里打开小程序看到的都是开发库数据
|
||||
(旧开发库:132 个用户、3 门课程,首页两个栏目为空)。
|
||||
他们会以为课程丢失,请务必在测试结束后切回生产库。
|
||||
</p>
|
||||
) : (
|
||||
<p className="rounded-md bg-green-50 px-3 py-2 text-green-800">
|
||||
将恢复为生产库,用户看到的即真实数据。
|
||||
</p>
|
||||
)}
|
||||
{error && <p className="rounded-md bg-red-50 px-3 py-2 text-red-700">{error}</p>}
|
||||
</div>
|
||||
<div className="mt-5 flex justify-end gap-2">
|
||||
<button
|
||||
onClick={() => setConfirmOpen(false)}
|
||||
disabled={switching}
|
||||
className="rounded-md border border-gray-300 bg-white px-3 py-1.5 text-sm text-gray-700 hover:bg-gray-50"
|
||||
>
|
||||
取消
|
||||
</button>
|
||||
<button
|
||||
onClick={applyMiniProgramEnv}
|
||||
disabled={switching}
|
||||
className="rounded-md bg-orange-600 px-3 py-1.5 text-sm font-medium text-white hover:bg-orange-700 disabled:opacity-50"
|
||||
>
|
||||
{switching ? '切换中…' : '确认切换'}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in new issue
Block a user