Files
nj/apps/web/src/routeConfig.ts
iven 78018a9a64
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled
feat(app): 管理端 Web 基座→暖记品牌迁移 + 日记管理页面
Phase 1 — 品牌替换:
- BRAND_DEFAULTS 回退值改为暖记品牌 (themes.ts)
- 登录页/侧边栏/底部回退文字 → 暖记 (Login, MainLayout)
- index.html title/meta/favicon → 暖记
- localStorage key → nuanji-theme, 默认主题 → warm
- 4 套主题色适配暖记设计系统 (珊瑚 #E07A5F / 蓝 / 深色 / 鼠尾草绿)
- 品牌信息通过系统设置配置,不硬编码

Phase 2 — 清理 HMS 模块:
- 删除 health/ai 页面 (~55+2)、API (~30+9)、组件、stores、hooks
- 重写 Home.tsx 为暖记 Dashboard
- 重写 NotificationPanel/MediaPicker 移除 health 依赖
- 清理 routeConfig 移除所有 health/ai 路由权限

Phase 3 — 暖记管理页面:
- API 层: api/diary/{types,journals,classes,topics,comments,stickers}.ts
- 班级管理: 班级列表+创建+成员查看+班级码复制 (ClassList)
- 日记审核: 日记列表+筛选+详情+老师点评 (JournalList)
- 主题管理: 班级选择+主题卡片+创建+过期标记 (TopicList)
- 贴纸管理: 贴纸包卡片+贴纸详情网格 (StickerPackList)
- 路由注册: /diary/classes, /diary/journals, /diary/topics, /diary/stickers

验证: tsc 0 error, vite build ✓, vitest 226/226 pass
2026-06-02 12:16:44 +08:00

108 lines
4.2 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
/**
* 路由权限配置 — 权限声明的单一真相源
*
* 规则:
* 1. 每个受保护路由必须在此声明至少一个权限码TypeScript 强制非空数组)
* 2. 子路由(如 /diary/classes/:id通过前缀匹配自动继承父路由权限无需重复声明
* 3. 新增路由必须在此添加对应条目,否则 PrivateRoute 默认 403
* 4. 冻结路由标记 frozen: true自动归入 FROZEN_ROUTES
*
* 排列规则:精确路径优先于前缀路径(如 /plugins/admin 在 /plugins 之前)
*/
// 非空数组类型 — 确保每个路由至少有一个权限码
type Permissions = [string, ...string[]];
interface RoutePermissionEntry {
path: string;
permissions: Permissions;
frozen?: boolean;
}
const ENTRIES: RoutePermissionEntry[] = [
// ===== 基础模块 =====
{ path: "/users", permissions: ["user.list", "user.update"] },
{ path: "/roles", permissions: ["role.list", "role.update"] },
{
path: "/organizations",
permissions: ["organization.list", "organization.update"],
},
{ path: "/workflow", permissions: ["workflow.list", "workflow.read"] },
{ path: "/messages", permissions: ["message.list"] },
{ path: "/settings", permissions: ["setting.read", "setting.update"] },
// ===== 插件模块(精确路径优先于前缀通配) =====
{ path: "/plugins/admin", permissions: ["plugin.admin"] },
{ path: "/plugins/market", permissions: ["plugin.admin"] },
// 动态路由 catch-all: /plugins/:pluginId/:entityName 等
{ path: "/plugins", permissions: ["plugin.list", "plugin.admin"] },
// ===== 暖记日记模块 =====
{ path: "/diary/classes", permissions: ["diary.class.manage", "diary.journal.read"] },
{ path: "/diary/journals", permissions: ["diary.journal.read", "diary.journal.manage"] },
{ path: "/diary/topics", permissions: ["diary.topic.assign", "diary.journal.read"] },
{ path: "/diary/stickers", permissions: ["diary.journal.read"] },
];
/** 活跃路由的权限映射 — 自动从配置生成,供 PrivateRoute 使用 */
export const ROUTE_PERMISSIONS: Record<string, string[]> = Object.fromEntries(
ENTRIES.filter((e) => !e.frozen).map((e) => [e.path, [...e.permissions]]),
);
/** 冻结路由路径列表 — 自动从配置生成 */
export const FROZEN_ROUTES: string[] = ENTRIES.filter((e) => e.frozen).map(
(e) => e.path,
);
/** 开发模式下校验:检查是否有路由路径重复 */
if (import.meta.env.DEV) {
const paths = ENTRIES.map((e) => e.path);
const dupes = paths.filter((p, i) => paths.indexOf(p) !== i);
if (dupes.length > 0) {
console.error("[routeConfig] 检测到重复路径:", dupes);
}
}
/**
* Tab 级权限映射 — 详情页内 Tab 可见性的单一真相源
*
* key 格式: "{routePrefix}#{tabKey}"
* value: 权限码undefined 表示始终可见)
*
* 新增详情页 Tab 时必须在此声明,否则 Tab 默认不可见(安全默认)。
*/
const TAB_PERM_ENTRIES: Array<{ key: string; permission?: string }> = [
// ===== 暖记日记模块 Tab 权限 =====
{ key: "class#members", permission: "diary.class.manage" },
{ key: "class#topics", permission: "diary.topic.assign" },
{ key: "journal#detail", permission: "diary.journal.read" },
{ key: "journal#comments", permission: "diary.comment.write" },
];
export const TAB_PERMISSIONS: Record<string, string | undefined> = Object.fromEntries(
TAB_PERM_ENTRIES.map((e) => [e.key, e.permission]),
);
/**
* DEV 模式路由覆盖率校验。
* 在 App.tsx 挂载后调用,检查所有实际路由是否都有权限声明。
* 忽略带参数的子路由(如 /diary/classes/:id它们通过前缀匹配继承权限。
*/
export function validateRouteCoverage(registeredPaths: string[]): void {
if (!import.meta.env.DEV) return;
const allConfigPaths = ENTRIES.map((e) => e.path);
const uncovered = registeredPaths.filter((p) => {
if (p === "/" || p === "/login" || p === "/*") return false;
if (p.includes(":")) return false; // 子路由通过前缀继承
return !allConfigPaths.includes(p);
});
if (uncovered.length > 0) {
console.warn(
"[routeConfig] 以下路由未在 routeConfig.ts 中声明权限,将被 PrivateRoute 默认 403 拦截:",
uncovered,
);
}
}