- ChatPage: 图标从 antd 移到 @ant-design/icons,Layout/Sider 改为 div 布局避免 Header 遮挡 - routeConfig: 注册 /ai/chat 路由权限 (ai.chat.session.list/manage) - 迁移 153: ai_tenant_configs → ai_tenant_config 表名修正 - 迁移 154: menus.name/is_external/status → title/visible/menu_type 列名修正 - 迁移 151/152: AI 配置菜单父级修复 + AI Provider 权限 seed
318 lines
9.4 KiB
TypeScript
318 lines
9.4 KiB
TypeScript
/**
|
||
* 路由权限配置 — 权限声明的单一真相源
|
||
*
|
||
* 规则:
|
||
* 1. 每个受保护路由必须在此声明至少一个权限码(TypeScript 强制非空数组)
|
||
* 2. 子路由(如 /health/patients/: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: "/health/patients",
|
||
permissions: ["health.patient.list", "health.patient.manage"],
|
||
},
|
||
{
|
||
path: "/health/tags",
|
||
permissions: ["health.patient.list", "health.patient.manage"],
|
||
},
|
||
{
|
||
path: "/health/doctors",
|
||
permissions: ["health.doctor.list", "health.doctor.manage"],
|
||
},
|
||
{
|
||
path: "/health/appointments",
|
||
permissions: ["health.appointment.list", "health.appointment.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — 随访与咨询 =====
|
||
{
|
||
path: "/health/follow-up-tasks",
|
||
permissions: ["health.follow-up.list", "health.follow-up.manage"],
|
||
},
|
||
{
|
||
path: "/health/follow-up-records",
|
||
permissions: ["health.follow-up.list", "health.follow-up.manage"],
|
||
},
|
||
{
|
||
path: "/health/follow-up-templates",
|
||
permissions: [
|
||
"health.follow-up-templates.list",
|
||
"health.follow-up-templates.manage",
|
||
],
|
||
},
|
||
{
|
||
path: "/health/consultations",
|
||
permissions: ["health.consultation.list", "health.consultation.manage"],
|
||
},
|
||
{
|
||
path: "/health/action-inbox",
|
||
permissions: ["health.action-inbox.list", "health.action-inbox.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — 告警与设备 =====
|
||
{
|
||
path: "/health/alerts",
|
||
permissions: ["health.alerts.list", "health.alerts.manage"],
|
||
},
|
||
{
|
||
path: "/health/alert-dashboard",
|
||
permissions: ["health.alerts.list", "health.alerts.manage"],
|
||
},
|
||
{
|
||
path: "/health/alert-rules",
|
||
permissions: ["health.alert-rules.list", "health.alert-rules.manage"],
|
||
},
|
||
{
|
||
path: "/health/devices",
|
||
permissions: ["health.devices.list", "health.devices.manage"],
|
||
},
|
||
{
|
||
path: "/health/realtime-monitor",
|
||
permissions: [
|
||
"health.device-readings.list",
|
||
"health.device-readings.manage",
|
||
],
|
||
},
|
||
{
|
||
path: "/health/ble-gateways",
|
||
permissions: ["health.ble-gateways.list", "health.ble-gateways.manage"],
|
||
},
|
||
{
|
||
path: "/health/critical-value-thresholds",
|
||
permissions: [
|
||
"health.critical-value-thresholds.list",
|
||
"health.critical-value-thresholds.manage",
|
||
],
|
||
},
|
||
{
|
||
path: "/health/daily-monitoring",
|
||
permissions: [
|
||
"health.daily-monitoring.list",
|
||
"health.daily-monitoring.manage",
|
||
],
|
||
},
|
||
|
||
// ===== 健康管理 — 诊断与知情同意 =====
|
||
{
|
||
path: "/health/diagnoses",
|
||
permissions: ["health.health-data.list", "health.health-data.manage"],
|
||
},
|
||
{
|
||
path: "/health/consents",
|
||
permissions: ["health.consent.list", "health.consent.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — AI 模块 =====
|
||
{
|
||
path: "/health/ai-prompts",
|
||
permissions: ["ai.prompt.list", "ai.prompt.manage"],
|
||
},
|
||
{
|
||
path: "/health/ai-analysis",
|
||
permissions: ["ai.analysis.list", "ai.analysis.manage"],
|
||
},
|
||
{ path: "/health/ai-usage", permissions: ["ai.usage.list"] },
|
||
{
|
||
path: "/health/ai-config",
|
||
permissions: ["ai.config.read", "ai.config.manage"],
|
||
},
|
||
{
|
||
path: "/health/ai-knowledge",
|
||
permissions: ["ai.knowledge.list", "ai.knowledge.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — 积分商城 =====
|
||
{
|
||
path: "/health/points-rules",
|
||
permissions: ["health.points.list", "health.points.manage"],
|
||
},
|
||
{
|
||
path: "/health/points-products",
|
||
permissions: ["health.points.list", "health.points.manage"],
|
||
},
|
||
{
|
||
path: "/health/points-orders",
|
||
permissions: ["health.points.list", "health.points.manage"],
|
||
},
|
||
{
|
||
path: "/health/offline-events",
|
||
permissions: ["health.points.list", "health.points.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — 内容管理 =====
|
||
{
|
||
path: "/health/articles",
|
||
permissions: ["health.articles.list", "health.articles.manage"],
|
||
},
|
||
{
|
||
path: "/health/article-categories",
|
||
permissions: ["health.articles.list", "health.articles.manage"],
|
||
},
|
||
{
|
||
path: "/health/article-tags",
|
||
permissions: ["health.articles.list", "health.articles.manage"],
|
||
},
|
||
{
|
||
path: "/health/banners",
|
||
permissions: ["health.banners.list", "health.banners.manage"],
|
||
},
|
||
{
|
||
path: "/health/media-library",
|
||
permissions: ["health.media.list", "health.media.manage"],
|
||
},
|
||
|
||
// ===== 健康管理 — 其他 =====
|
||
{
|
||
path: "/health/oauth-clients",
|
||
permissions: ["health.oauth.list", "health.oauth.manage"],
|
||
},
|
||
{
|
||
path: "/health/statistics",
|
||
permissions: ["health.health-data.list", "health.dashboard.manage"],
|
||
},
|
||
{
|
||
path: "/health/medication-records",
|
||
permissions: ["health.medication-records.list", "health.medication-records.manage"],
|
||
},
|
||
|
||
// ===== 冻结路由 =====
|
||
{
|
||
path: "/health/care-plans",
|
||
permissions: ["health.care-plan.list", "health.care-plan.manage"],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/shifts",
|
||
permissions: ["health.shifts.list", "health.shifts.manage"],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/family-proxy",
|
||
permissions: ["health.family-proxy.list", "health.family-proxy.manage"],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/medications",
|
||
permissions: [
|
||
"health.medication-records.list",
|
||
"health.medication-records.manage",
|
||
],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/dialysis",
|
||
permissions: ["health.dialysis.list", "health.dialysis.manage"],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/dialysis-prescriptions",
|
||
permissions: ["health.dialysis-prescription.list", "health.dialysis-prescription.manage"],
|
||
frozen: true,
|
||
},
|
||
{
|
||
path: "/health/schedules",
|
||
permissions: ["health.appointment.list", "health.appointment.manage"],
|
||
},
|
||
|
||
// ===== AI 聊天 =====
|
||
{
|
||
path: "/ai/chat",
|
||
permissions: ["ai.chat.session.list", "ai.chat.session.manage"],
|
||
},
|
||
];
|
||
|
||
/** 活跃路由的权限映射 — 自动从配置生成,供 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 }> = [
|
||
// 患者详情 /health/patients/:id
|
||
{ key: "patient#info", permission: undefined },
|
||
{ key: "patient#family", permission: "health.patient.manage" },
|
||
{ key: "patient#health", permission: "health.health-data.list" },
|
||
{ key: "patient#followup", permission: "health.follow-up.list" },
|
||
{ key: "patient#points", permission: "health.points.list" },
|
||
{ key: "patient#ai", permission: "ai.analysis.list" },
|
||
];
|
||
|
||
export const TAB_PERMISSIONS: Record<string, string | undefined> = Object.fromEntries(
|
||
TAB_PERM_ENTRIES.map((e) => [e.key, e.permission]),
|
||
);
|
||
|
||
/**
|
||
* DEV 模式路由覆盖率校验。
|
||
* 在 App.tsx 挂载后调用,检查所有实际路由是否都有权限声明。
|
||
* 忽略带参数的子路由(如 /health/patients/: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,
|
||
);
|
||
}
|
||
}
|