前端架构、路由与状态¶
前端是 React 19 + TypeScript + Vite 8 的 SPA,UI 基于 antd 6,状态用 Zustand,数据请求用 TanStack Query。
1. 启动链路¶
main.tsx
├── QueryClientProvider retry=1 / staleTime=15s / 关闭窗口聚焦重取
├── ThemeProvider antd ConfigProvider + 主题/色板/材质/水印
├── BrowserRouter
└── AppRoutes
├── /login Login(公开)
└── RequireAuth → AppLayout
└── 页面路由 → KeepAliveOutlet
RequireAuth 首次挂载时调用 useAuth.bootstrap():
- 没有 access token →
anon→ 跳转/login。 - 有 token → 请求
/auth/me,成功进入ready,失败清空令牌回登录页。 - 直接输入 URL 也会按
permissionForPath校验页面权限,无权限显示 403 占位。
2. 路由表¶
| 路径 | 页面 | 权限码 |
|---|---|---|
/login |
Login |
公开 |
/ |
HomeRedirect |
跳到第一个可访问菜单 |
/dashboard |
Dashboard |
dashboard:view |
/system/users |
Users |
system:user:list |
/system/roles |
Roles |
system:role:list |
/system/menus |
Menus |
system:menu:list |
/system/departments |
Departments |
system:dept:list |
/system/dicts |
Dicts |
system:dict:list |
/system/parameters |
Parameters |
system:param:list |
/system/files |
Files |
system:file:list |
/system/jobs |
Jobs |
system:job:list |
/system/online |
Online |
system:online:list |
/system/monitor |
Monitor |
system:monitor:view |
/system/logs/operations |
Logs(mode=operations) |
system:log:operation |
/system/logs/logins |
Logs(mode=logins) |
system:log:login |
/system/exports |
Exports |
system:export:list |
/profile |
Profile |
登录即可 |
* |
NotFound |
— |
前端路由是静态注册的:App.tsx 决定「路径 → 组件」,config/nav.ts 的 navItems 决定「路径 → 名称/图标/权限/分组」。后端菜单只决定展示哪些、顺序和显示名称,不会动态创建路由。
3. 导航生成¶
useNav() 把 /auth/me 的菜单树转换成导航:
type=1目录 → 一级分组(图标轨 + 上下文栏)。type=2且visible=1、status=1、有path→ 导航项。- 只保留
navItems里静态注册过的路径(navMeta),避免点进去 404。 - 再按
can(permission)过滤;super_admin直接通过。 - 结果为空(
/auth/me还没回来或菜单异常)时,回退到静态navItems,保证可用。
因此:
- 改菜单名称/图标/排序 → 后端改完刷新即可。
- 新增页面 →
App.tsx+navItems+ 后端菜单三处都要有。
4. 布局骨架¶
| 组件 | 作用 |
|---|---|
AppLayout |
氛围光、图标轨、上下文栏、命令栏、页签栏、内容滚动区 |
Ambient |
极光背景与漂浮光斑(纯色材质下关闭) |
Rail |
一级模块图标轨,选中态用 motion 滑动 |
ContextBar |
当前模块的二级导航;单页模块自动收起 |
CommandBar |
⌘K 命令入口、主题切换、通知中心、用户菜单 |
TabBar |
多标签页;右键刷新/关闭其他/关闭左侧/右侧/全部 |
KeepAliveOutlet |
页签保活:隐藏而非卸载,保留筛选/表单/滚动位置 |
LockScreen |
闲置锁屏,密码或 Passkey 解锁 |
NotificationCenter |
未读数角标、全部/未读、单条/全部已读 |
4.1 页签与保活¶
- 打开页面时自动创建页签,
key是 pathname。 KeepAliveOutlet用Map<path, {node, context}>缓存页面,关闭页签时释放,避免内存持续增长。- 每个页签有独立
refreshKey,右键「刷新」只重挂载当前页。 - 隐藏页通过冻结的
UNSAFE_LocationContext保留激活时的路由快照,不会读到后来切换的地址。 - 登录页与 404 不参与页签体系。
4.2 滚动静止与材质¶
- 向下滚动时页签栏自动收起,回滚/回到顶部展开。
- 内容滚到命令栏下方才开启实时磨砂,减少无意义的背景采样。
- 液态玻璃只在 Chromium 系浏览器挂载折射脚本,其他浏览器自动降级。
4.3 锁屏¶
useIdleLock按ui.lockIdleMinutes计时(0 关闭),监听pointermove/pointerdown/keydown/wheel/scroll。- 合成事件(
isTrusted=false)不重置计时,避免程序化滚动永久续命。 - 多标签页锁定状态通过
bindLockAcrossTabs联动。 - 解锁只恢复界面,不改变登录态;换人登录会重置锁屏状态。
5. 状态管理¶
| Store | 持久化 | 内容 |
|---|---|---|
useAuth |
localStorage x-admin.auth |
user / permissions / menus / status,登录、MFA、Passkey、登出、can() |
useTabs |
sessionStorage x-admin.tabs |
页签列表、当前页签、刷新计数 |
useUi |
localStorage x-admin.ui |
明暗主题、色板、材质、表格密度、水印、列设置、锁屏时长、二级导航偏好 |
useLock |
localStorage x-admin.lock |
锁屏状态 |
Token 单独存 localStorage:
这是前后端分离 SPA 的常见方案;若对 XSS 有更高要求,可改为 HttpOnly Cookie + CSRF。
useAuth 只持久化展示所需字段(user/permissions/menus),令牌由 tokenStore 管理;刷新页面时 bootstrap() 用令牌换最新用户信息。
6. 权限协作¶
const can = useAuth((state) => state.can)
// 按钮
{can('system:user:create') && <Button>新建用户</Button>}
// 路由守卫
<RequireAuth><AppLayout /></RequireAuth>
can()只影响 UI;后端Require才是最终授权。- 页面级权限来自
permissionForPath,与后端菜单无关;即使菜单被隐藏,直接输 URL 也会被拦。 super_admin不参与权限码判断,前后端都是直接放行。
7. 请求与错误¶
- 所有请求走
api/client.ts,自动拼接VITE_API_BASE ?? '/admin/v1'、注入 Bearer、401 单飞刷新。 - 查询用 TanStack Query,
queryKey约定['resource', page, pageSize, filters]。 - 写操作后用
queryClient.invalidateQueries({ queryKey: ['resource'] })刷新。 - 错误统一
toast.fail(error),展示后端message。 - 详见 API 客户端与状态。
8. 新增页面¶
pages/Announcements.tsx:列表 + 弹窗/抽屉,参考Parameters.tsx。App.tsx加路由。config/nav.ts加navItems(路径、权限码、图标、分组)。- 后端菜单/按钮权限入库(见 权限体系)。
- 按需接入
DataTable/FormModal/FileSelect。 - 加组件测试。
各页面现有能力见 页面与功能地图。
9. 移动端¶
useIsCompactViewport()判定:max-width: 900px,或触屏设备≤1024px。DataTable自动切换为MobileRecordList(首列作标题,其余列转标签值,操作列固定底部)。- 手机端筛选器默认收起,通过工具栏展开;输入框保持 16px 防 iOS 缩放。
- 抽屉/弹窗宽度不超过视口,过高时内部滚动。
- 多页签改为「当前页 + 数量」入口,底部弹出全部页签。
- 通知中心改为底部面板。
10. 常见问题¶
| 现象 | 排查 |
|---|---|
| 新页面菜单不出现 | navItems 没加、后端菜单路径不一致、角色未授权、visible/status 为 0 |
| 刷新后 404 | Nginx 没配 try_files ... /index.html |
| 登录后回到登录页 | /auth/me 失败(令牌过期/后端不可用),看 Network |
| 页签切换后筛选丢失 | 页面没在 KeepAliveOutlet 下,或用了 key 强制重挂载 |
| 手机布局不生效 | 断点判断用了 window.innerWidth 而非 useIsCompactViewport |
| 主题切换后部分颜色不变 | 写死了十六进制,没使用 var(--accent) 等 CSS 变量 |