Files
hms/apps/web/src/routeConfig.ts
iven 8fbe1543cb fix(ai): ChatPage import/layout 修复 + 迁移表名列名修正 + 路由权限注册
- 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
2026-05-19 17:48:37 +08:00

318 lines
9.4 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. 子路由(如 /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,
);
}
}