fix(health): 修复 5 角色深度测试发现的 8 个问题
P0 修复: - 告警状态机新增 active 合法状态 + 转换规则 (active→acknowledged/dismissed) - 前端路由守卫改为默认拒绝,未注册路由返回 403 P1 修复: - 侧边栏菜单根据用户权限码过滤,非 admin 隐藏无权限菜单项 - Critical-alerts handler 增加详细错误日志 + div_ceil 安全防护 - 仪表盘统计 API 调用使用 silent 模式避免 500 触发全局 toast P2 修复: - 随访类型映射新增 visit → 上门 (前后端同步) - 随访 fallback 选项新增 visit 类型 排除的假 BUG (代码已正确): - 患者性别/血型: MCP fill() 不兼容 Select 组件,正常交互正确 - 随访筛选/对话框关闭: 代码逻辑验证正确 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -144,16 +144,23 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
||||
|
||||
const path = location.pathname;
|
||||
|
||||
// 冻结路由检查
|
||||
if (FROZEN_ROUTES.some((frozen) => path.startsWith(frozen))) {
|
||||
return <FrozenRoute />;
|
||||
}
|
||||
|
||||
// 首页/工作台始终放行
|
||||
if (path === '/' || path === '') return <>{children}</>;
|
||||
|
||||
const matchedPrefix = Object.keys(ROUTE_PERMISSIONS).find((prefix) => path.startsWith(prefix));
|
||||
if (matchedPrefix) {
|
||||
const required = ROUTE_PERMISSIONS[matchedPrefix];
|
||||
const hasAccess = required.some((r) => permissions.includes(r));
|
||||
if (!hasAccess) return <ForbiddenPage />;
|
||||
}
|
||||
|
||||
// 冻结路由检查
|
||||
if (FROZEN_ROUTES.some((frozen) => path.startsWith(frozen))) {
|
||||
return <FrozenRoute />;
|
||||
} else {
|
||||
// 未在 ROUTE_PERMISSIONS 中注册的路由,默认拒绝
|
||||
return <ForbiddenPage />;
|
||||
}
|
||||
|
||||
return <>{children}</>;
|
||||
|
||||
@@ -384,61 +384,61 @@ export const pointsApi = {
|
||||
},
|
||||
|
||||
// Points Statistics
|
||||
getStatistics: async () => {
|
||||
getStatistics: async (opts?: { silent?: boolean }) => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PointsStatistics;
|
||||
}>('/health/admin/points/statistics');
|
||||
}>('/health/admin/points/statistics', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
// --- Dashboard Statistics ---
|
||||
|
||||
getPatientStats: async (): Promise<PatientStatistics> => {
|
||||
getPatientStats: async (opts?: { silent?: boolean }): Promise<PatientStatistics> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PatientStatistics;
|
||||
}>('/health/admin/statistics/patients');
|
||||
}>('/health/admin/statistics/patients', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getConsultationStats: async (): Promise<ConsultationStatistics> => {
|
||||
getConsultationStats: async (opts?: { silent?: boolean }): Promise<ConsultationStatistics> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: ConsultationStatistics;
|
||||
}>('/health/admin/statistics/consultations');
|
||||
}>('/health/admin/statistics/consultations', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getFollowUpStats: async (): Promise<FollowUpStatistics> => {
|
||||
getFollowUpStats: async (opts?: { silent?: boolean }): Promise<FollowUpStatistics> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: FollowUpStatistics;
|
||||
}>('/health/admin/statistics/follow-ups');
|
||||
}>('/health/admin/statistics/follow-ups', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getHealthDataStats: async (): Promise<HealthDataStats> => {
|
||||
getHealthDataStats: async (opts?: { silent?: boolean }): Promise<HealthDataStats> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: HealthDataStats;
|
||||
}>('/health/admin/statistics/health-data');
|
||||
}>('/health/admin/statistics/health-data', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getDialysisStats: async (): Promise<DialysisStatistics> => {
|
||||
getDialysisStats: async (opts?: { silent?: boolean }): Promise<DialysisStatistics> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: DialysisStatistics;
|
||||
}>('/health/admin/statistics/dialysis');
|
||||
}>('/health/admin/statistics/dialysis', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getPersonalStats: async (): Promise<PersonalStats> => {
|
||||
getPersonalStats: async (opts?: { silent?: boolean }): Promise<PersonalStats> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: PersonalStats;
|
||||
}>('/health/admin/statistics/personal-stats');
|
||||
}>('/health/admin/statistics/personal-stats', { skipGlobalError: opts?.silent } as any);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -394,7 +394,27 @@ export default function MainLayout({ children }: { children: React.ReactNode })
|
||||
(async () => {
|
||||
try {
|
||||
const menus = await getMenusForUser();
|
||||
if (!cancelled) setDynamicMenus(menus);
|
||||
if (!cancelled) {
|
||||
// 根据用户权限过滤菜单:菜单项声明 permission 时,用户必须有对应权限
|
||||
const perms = useAuthStore.getState().permissions;
|
||||
const isAdmin = useAuthStore.getState().user?.roles?.some((r: string) => r === 'admin') ?? false;
|
||||
if (isAdmin) {
|
||||
setDynamicMenus(menus);
|
||||
} else {
|
||||
const filterByPerm = (items: MenuInfo[]): MenuInfo[] =>
|
||||
items
|
||||
.map((m) => ({
|
||||
...m,
|
||||
children: m.children ? filterByPerm(m.children) : undefined,
|
||||
}))
|
||||
.filter((m) => {
|
||||
if (!m.permission) return true;
|
||||
return perms.includes(m.permission);
|
||||
})
|
||||
.filter((m) => m.menu_type === 'directory' || !m.children || m.children.length > 0 || !m.permission || perms.includes(m.permission));
|
||||
setDynamicMenus(filterByPerm(menus));
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// fallback: 使用空数组,保留插件菜单
|
||||
}
|
||||
|
||||
@@ -39,6 +39,7 @@ const FOLLOW_UP_TYPE_FALLBACK = [
|
||||
{ value: 'phone', label: '电话' },
|
||||
{ value: 'outpatient', label: '门诊' },
|
||||
{ value: 'home_visit', label: '家访' },
|
||||
{ value: 'visit', label: '上门' },
|
||||
{ value: 'online', label: '线上' },
|
||||
{ value: 'wechat', label: '微信' },
|
||||
];
|
||||
@@ -47,6 +48,7 @@ const FOLLOW_UP_TYPE_MAP: Record<string, string> = {
|
||||
phone: '电话',
|
||||
outpatient: '门诊',
|
||||
home_visit: '家访',
|
||||
visit: '上门',
|
||||
online: '线上',
|
||||
wechat: '微信',
|
||||
};
|
||||
|
||||
@@ -53,12 +53,12 @@ export function useStatsData(): StatsData {
|
||||
};
|
||||
|
||||
await Promise.all([
|
||||
tryFetch(pointsApi.getPatientStats, setPatientStats, '患者'),
|
||||
tryFetch(pointsApi.getConsultationStats, setConsultationStats, '咨询'),
|
||||
tryFetch(pointsApi.getFollowUpStats, setFollowUpStats, '随访'),
|
||||
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
|
||||
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
|
||||
tryFetch(pointsApi.getDialysisStats, setDialysisStats, '透析'),
|
||||
tryFetch(() => pointsApi.getPatientStats({ silent: true }), setPatientStats, '患者'),
|
||||
tryFetch(() => pointsApi.getConsultationStats({ silent: true }), setConsultationStats, '咨询'),
|
||||
tryFetch(() => pointsApi.getFollowUpStats({ silent: true }), setFollowUpStats, '随访'),
|
||||
tryFetch(() => pointsApi.getStatistics({ silent: true }), setPointsStats, '积分'),
|
||||
tryFetch(() => pointsApi.getHealthDataStats({ silent: true }), setHealthDataStats, '健康数据'),
|
||||
tryFetch(() => pointsApi.getDialysisStats({ silent: true }), setDialysisStats, '透析'),
|
||||
tryFetch(
|
||||
async () => { const r = await doctorApi.list({ page: 1, page_size: 1 }); return r.total; },
|
||||
setDoctorCount,
|
||||
|
||||
Reference in New Issue
Block a user