Files
hms/apps/web/src/routeConfig.ts
iven d44c6167b1 fix: E2E 测试发现的 10 项 BUG 修复 — 全栈验证通过
P0 修复:
- 侧边栏路由不稳定: Content 区域添加 key={currentPath} 强制重渲染
- 轮播图缩略图不显示: BannerManage 导入 resolveMediaUrl + 反斜杠转正斜杠
- 超长名称导致 500: patient_handler 添加 name.len() > 255 校验
- 迁移 m20260515_000146: version 乐观锁 version+1 修复

P1 修复:
- 排班路由被冻结: routeConfig.ts 移除 /health/schedules 的 frozen 标记
- 轮播图 Switch 切换无效: 切换前先 GET 最新 version 避免乐观锁冲突
- thumbnail_url 反斜杠: media_service 存储时统一 replace('\', '/')

P2 修复:
- 预约类型 follow_up 未映射: APPOINTMENT_TYPE_MAP 补充 '随访'
- 日期选择器未汉化: DatePicker.RangePicker 添加中文 placeholder
- 轮播图 title 必填校验: banner_handler 添加空标题拒绝
- 文章分类重名: article_category_service 添加同名检查
2026-05-15 21:13:49 +08:00

304 lines
9.1 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/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"],
},
];
/** 活跃路由的权限映射 — 自动从配置生成,供 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,
);
}
}