fix(health): 修复 5 角色测试发现的 4 个共性问题
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled

- 权限路由守卫:静默重定向改为显示 403 页面,使用 useLocation 替代
  window.location.hash,补全缺失路由权限条目
- 随访状态筛选:usePaginatedData hook 添加 filters 变化监听自动刷新
- 告警操作:后端 acknowledge/dismiss/resolve 改返回 AlertResponse
  (含 patient_name),前端增加 active 状态兼容和错误反馈
- 咨询患者名:后端 create/get/close_session 增加 patient_name 和
  doctor_name enrichment,前端 EntityName 空字符串处理
This commit is contained in:
iven
2026-05-07 07:23:41 +08:00
parent 43f0ba7057
commit 1613e3cfe9
7 changed files with 141 additions and 53 deletions

View File

@@ -1,5 +1,5 @@
import { useEffect, lazy, Suspense, useMemo } from 'react';
import { HashRouter, Routes, Route, Navigate } from 'react-router-dom';
import { HashRouter, Routes, Route, Navigate, useLocation, useNavigate } from 'react-router-dom';
import { ConfigProvider, theme as antdTheme, Spin, Result } from 'antd';
import zhCN from 'antd/locale/zh_CN';
import MainLayout from './layouts/MainLayout';
@@ -84,52 +84,71 @@ function FrozenRoute() {
return <Result status="info" title="功能暂未开放" subTitle="该功能正在优化中,敬请期待" />;
}
function ForbiddenPage() {
const navigate = useNavigate();
return (
<div style={{ display: 'flex', justifyContent: 'center', alignItems: 'center', minHeight: '60vh' }}>
<Result
status="403"
title="权限不足"
subTitle="您没有访问此页面的权限,请联系管理员"
extra={<button onClick={() => navigate('/')} style={{ cursor: 'pointer', color: 'var(--ant-color-primary)', background: 'none', border: 'none', fontSize: 14 }}></button>}
/>
</div>
);
}
const ROUTE_PERMISSIONS: Record<string, string[]> = {
'/users': ['user.list', 'user.manage'],
'/roles': ['role.list', 'role.manage'],
'/organizations': ['organization.list', 'organization.manage'],
'/workflow': ['workflow.list', 'workflow.read'],
'/messages': ['message.list'],
'/settings': ['config.settings.list', 'config.settings.manage'],
'/plugins/admin': ['plugin.list', 'plugin.manage'],
'/health/patients': ['health.patient.list', 'health.patient.manage'],
'/health/doctors': ['health.doctor.list', 'health.doctor.manage'],
'/health/follow-up-tasks': ['health.follow-up.list', 'health.follow-up.manage'],
'/health/consultations': ['health.consultation.list', 'health.consultation.manage'],
'/health/action-inbox': ['health.action-inbox.list', 'health.action-inbox.manage'],
'/health/follow-up-templates': ['health.follow-up-templates.list', 'health.follow-up-templates.manage'],
'/health/diagnoses': ['health.health-data.list', 'health.health-data.manage'],
'/health/consents': ['health.consent.list', 'health.consent.manage'],
'/health/realtime-monitor': ['health.device-readings.list', 'health.device-readings.manage'],
'/health/alert-dashboard': ['health.alerts.list', 'health.alerts.manage'],
'/health/alerts': ['health.alerts.list', 'health.alerts.manage'],
'/health/devices': ['health.devices.list', 'health.devices.manage'],
'/health/ble-gateways': ['health.ble-gateways.list', 'health.ble-gateways.manage'],
'/health/critical-value-thresholds': ['health.critical-value-thresholds.list', 'health.critical-value-thresholds.manage'],
'/health/articles': ['health.articles.list', 'health.articles.manage'],
'/health/points-rules': ['health.points.list', 'health.points.manage'],
'/health/points-products': ['health.points.list', 'health.points.manage'],
'/health/points-orders': ['health.points.list', 'health.points.manage'],
'/health/offline-events': ['health.points.list', 'health.points.manage'],
'/health/ai-prompts': ['ai.prompt.list', 'ai.prompt.manage'],
'/health/ai-analysis': ['ai.analysis.list', 'ai.analysis.manage'],
'/health/ai-usage': ['ai.usage.list'],
'/health/oauth-clients': ['health.oauth.list', 'health.oauth.manage'],
'/health/statistics': ['health.health-data.list', 'health.dashboard.manage'],
'/health/tags': ['health.patient.list', 'health.patient.manage'],
'/health/daily-monitoring': ['health.device-readings.list', 'health.device-readings.manage'],
'/health/alert-rules': ['health.alert-rules.list', 'health.alert-rules.manage'],
'/health/medication-records': ['health.medication-records.manage'],
};
function PrivateRoute({ children }: { children: React.ReactNode }) {
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
const permissions = useAuthStore((s) => s.permissions);
const location = useLocation();
if (!isAuthenticated) return <Navigate to="/login" replace />;
// 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页
const path = window.location.hash.replace('#', '');
const routePermissions: Record<string, string[]> = {
'/users': ['user.list', 'user.manage'],
'/roles': ['role.list', 'role.manage'],
'/organizations': ['organization.list', 'organization.manage'],
'/workflow': ['workflow.list', 'workflow.read'],
'/messages': ['message.list'],
'/settings': ['config.settings.list', 'config.settings.manage'],
'/plugins/admin': ['plugin.list', 'plugin.manage'],
'/health/patients': ['health.patient.list', 'health.patient.manage'],
'/health/doctors': ['health.doctor.list', 'health.doctor.manage'],
'/health/follow-up-tasks': ['health.follow-up.list', 'health.follow-up.manage'],
'/health/consultations': ['health.consultation.list', 'health.consultation.manage'],
'/health/action-inbox': ['health.action-inbox.list', 'health.action-inbox.manage'],
'/health/follow-up-templates': ['health.follow-up-templates.list', 'health.follow-up-templates.manage'],
'/health/diagnoses': ['health.health-data.list', 'health.health-data.manage'],
'/health/consents': ['health.consent.list', 'health.consent.manage'],
'/health/realtime-monitor': ['health.device-readings.list', 'health.device-readings.manage'],
'/health/alert-dashboard': ['health.alerts.list', 'health.alerts.manage'],
'/health/devices': ['health.devices.list', 'health.devices.manage'],
'/health/ble-gateways': ['health.ble-gateways.list', 'health.ble-gateways.manage'],
'/health/critical-value-thresholds': ['health.critical-value-thresholds.list', 'health.critical-value-thresholds.manage'],
'/health/articles': ['health.articles.list', 'health.articles.manage'],
'/health/points-rules': ['health.points.list', 'health.points.manage'],
'/health/points-products': ['health.points.list', 'health.points.manage'],
'/health/points-orders': ['health.points.list', 'health.points.manage'],
'/health/offline-events': ['health.points.list', 'health.points.manage'],
'/health/ai-prompts': ['ai.prompt.list', 'ai.prompt.manage'],
'/health/ai-analysis': ['ai.analysis.list', 'ai.analysis.manage'],
'/health/ai-usage': ['ai.usage.list'],
'/health/oauth-clients': ['health.oauth.list', 'health.oauth.manage'],
'/health/statistics': ['health.health-data.list', 'health.dashboard.manage'],
'/health/tags': ['health.patient.list', 'health.patient.manage'],
};
const matchedPrefix = Object.keys(routePermissions).find((prefix) => path.startsWith(prefix));
const path = location.pathname;
const matchedPrefix = Object.keys(ROUTE_PERMISSIONS).find((prefix) => path.startsWith(prefix));
if (matchedPrefix) {
const required = routePermissions[matchedPrefix];
const required = ROUTE_PERMISSIONS[matchedPrefix];
const hasAccess = required.some((r) => permissions.includes(r));
if (!hasAccess) return <Navigate to="/" replace />;
if (!hasAccess) return <ForbiddenPage />;
}
// 冻结路由检查

View File

@@ -7,7 +7,7 @@ interface EntityNameProps {
}
export function EntityName({ name, id, fallbackLabel = '未知' }: EntityNameProps) {
if (name) return <span>{name}</span>;
if (name !== undefined && name !== null && name !== '') return <span>{name}</span>;
if (id) {
return (

View File

@@ -84,6 +84,18 @@ export function usePaginatedData<T, F = string>(
}
}, [shouldAutoFetch, refresh]);
// 筛选条件变化时自动刷新(解决 FollowUpTaskList 等组件直接调用 setFilters 不触发刷新的问题)
const isFirstRender = useRef(true);
useEffect(() => {
if (isFirstRender.current) {
isFirstRender.current = false;
return;
}
if (shouldAutoFetch) {
refresh(1);
}
}, [filters]);
return { ...state, searchText, setSearchText, filters, setFilters, refresh };
}

View File

@@ -106,7 +106,7 @@ export default function AlertDashboard() {
setAlerts((prev) => prev.map((a) => (a.id === id ? updated : a)));
setSelectedAlert((prev) => (prev?.id === id ? updated : prev));
} catch {
// 错误由 API client 统一处理
message.error('确认告警失败,请重试');
} finally {
setActionLoading(false);
}
@@ -119,7 +119,7 @@ export default function AlertDashboard() {
setAlerts((prev) => prev.map((a) => (a.id === id ? updated : a)));
setSelectedAlert((prev) => (prev?.id === id ? updated : prev));
} catch {
// 错误由 API client 统一处理
message.error('忽略告警失败,请重试');
} finally {
setActionLoading(false);
}
@@ -132,7 +132,7 @@ export default function AlertDashboard() {
setAlerts((prev) => prev.map((a) => (a.id === id ? updated : a)));
setSelectedAlert((prev) => (prev?.id === id ? updated : prev));
} catch {
// 错误由 API client 统一处理
message.error('恢复告警失败,请重试');
} finally {
setActionLoading(false);
}

View File

@@ -17,6 +17,7 @@ const SEVERITY_CONFIG: Record<string, { color: string; label: string; icon: Reac
const STATUS_CONFIG: Record<string, { color: string; label: string }> = {
pending: { color: 'orange', label: '待处理' },
active: { color: 'orange', label: '待处理' },
acknowledged: { color: 'blue', label: '已确认' },
resolved: { color: 'green', label: '已恢复' },
dismissed: { color: 'default', label: '已忽略' },
@@ -42,7 +43,7 @@ export function AlertDetailPanel({
}: AlertDetailPanelProps) {
const severity = SEVERITY_CONFIG[alert.severity] ?? SEVERITY_CONFIG.info;
const status = STATUS_CONFIG[alert.status] ?? STATUS_CONFIG.pending;
const isPending = alert.status === 'pending';
const isPending = alert.status === 'pending' || alert.status === 'active';
const isAcknowledged = alert.status === 'acknowledged';
return (