feat: 添加管理端前端 (HMS 基座 React 管理面板)
Some checks failed
Main Merge / backend (push) Has been cancelled
Main Merge / frontend (push) Has been cancelled

- 从 HMS 基座复制 apps/web/ (React + Ant Design + Vite + TypeScript)
- 管理端自动代理 API 到 localhost:3000 (vite.config.ts)
- 更新 scripts/dev.sh 支持三端启动: backend/admin/app
- 登录验证通过, 用户管理/角色权限/审计日志等页面正常
- 添加 .gitignore 排除 node_modules/dist
This commit is contained in:
iven
2026-06-02 10:03:13 +08:00
parent 181bfb1f3e
commit 8111471e93
341 changed files with 72102 additions and 1059 deletions

319
apps/web/src/routeConfig.ts Normal file
View File

@@ -0,0 +1,319 @@
/**
* 路由权限配置 — 权限声明的单一真相源
*
* 规则:
* 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"],
frozen: true,
},
{
path: "/health/consents",
permissions: ["health.consent.list", "health.consent.manage"],
frozen: true,
},
// ===== 健康管理 — 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,
);
}
}