/** * 导航唯一注册表:菜单、页面标题、页面说明、顶栏面包屑都来自这里, * 避免同一个页面名在 Layout / 各页面标题里重复维护。 * 名称存的是 i18n key,渲染时由调用方用 t() 解析。 */ import type { ReactNode } from "react"; import type { MenuProps } from "antd"; import type { MsgKey, TFn } from "./i18n"; import { ApartmentOutlined, AppstoreOutlined, BookOutlined, DashboardOutlined, DatabaseOutlined, ExperimentOutlined, FieldTimeOutlined, FileTextOutlined, PlayCircleOutlined, ReadOutlined, RobotOutlined, SettingOutlined, TeamOutlined, ThunderboltOutlined, WarningOutlined, } from "@ant-design/icons"; export type ViewId = | "dashboard" | "knowledge" | "test" | "errors" | "records" | "daily" | "resources" | "reader" | "settings" | "users" | "catalog" | "courses" | "bank" | "llm" | "graph"; interface NavMeta { title: MsgKey; /** 顶栏标题下方的一句话说明 */ description: MsgKey; icon: ReactNode; } export const NAV_META: Record = { dashboard: { title: "nav.dashboard", description: "nav.dashboard.description", icon: , }, knowledge: { title: "nav.knowledge", description: "nav.knowledge.description", icon: , }, test: { title: "nav.test", description: "nav.test.description", icon: , }, errors: { title: "nav.errors", description: "nav.errors.description", icon: , }, records: { title: "nav.records", description: "nav.records.description", icon: , }, daily: { title: "nav.daily", description: "nav.daily.description", icon: , }, resources: { title: "nav.resources", description: "nav.resources.description", icon: , }, reader: { title: "nav.reader", description: "nav.reader.description", icon: , }, settings: { title: "nav.settings", description: "nav.settings.description", icon: , }, users: { title: "nav.users", description: "nav.users.description", icon: , }, catalog: { title: "nav.catalog", description: "nav.catalog.description", icon: , }, courses: { title: "nav.courses", description: "nav.courses.description", icon: , }, bank: { title: "nav.bank", description: "nav.bank.description", icon: , }, llm: { title: "nav.llm", description: "nav.llm.description", icon: , }, graph: { title: "nav.graph", description: "nav.graph.description", icon: , }, }; const LEARNING_SECTIONS: Array<{ label: MsgKey; items: ViewId[] }> = [ { label: "section.learning", items: ["dashboard", "daily", "knowledge", "test", "errors", "records"] }, { label: "section.resources", items: ["resources"] }, ]; const ADMIN_SECTIONS: Array<{ label: MsgKey; items: ViewId[] }> = [ { label: "section.content", items: ["catalog", "courses", "bank", "graph"] }, { label: "section.system", items: ["users", "llm"] }, ]; const SETTINGS_SECTION: MsgKey = "section.account"; /** 顶栏面包屑用的分区名(学习 / 内容与题库 / 系统管理 / 账号…)。 */ export function sectionForView(view: ViewId, role: string, t: TFn): string { if (view === "settings") return t(SETTINGS_SECTION); const hit = sectionsForRole(role).find((section) => section.items.includes(view), ); return hit ? t(hit.label) : ""; } /** antd Menu 的 items 类型。 */ export type MenuItems = NonNullable; function sectionsForRole(role: string) { return role === "admin" ? ADMIN_SECTIONS : LEARNING_SECTIONS; } function navItem(id: ViewId, t: TFn) { return { key: id, icon: NAV_META[id].icon, label: t(NAV_META[id].title) }; } /** * 侧边栏菜单:管理员只暴露管理功能,学生只暴露学习功能。 * 展开态按功能分组;收拢态摊平成一列图标(antd 收起时无法渲染分组标题), * 组与组之间用分隔线保持同样的分组语义,悬停图标即出现文字提示。 */ export function menuForRole(role: string, collapsed: boolean, t: TFn): MenuItems { const sections = sectionsForRole(role); if (collapsed) { const items: MenuItems = []; sections.forEach((section, index) => { if (index > 0) items.push({ type: "divider" as const }); section.items.forEach((id) => items.push(navItem(id, t))); }); return items; } return sections.map((section) => ({ type: "group" as const, label: t(section.label), children: section.items.map((id) => navItem(id, t)), })); } /** 当前角色可访问的视图,用于路由兜底。 */ export function isAllowedView(view: ViewId, role: string): boolean { // 阅读器不在菜单里,由「在线阅读」进入,两种角色都需要能打开 if (view === "settings" || view === "reader") return true; return sectionsForRole(role).some((section) => section.items.includes(view), ); } export function defaultViewForRole(role: string): ViewId { return role === "admin" ? "users" : "dashboard"; }