From 1613e3cfe9bb0b347f50613490417db552d5085c Mon Sep 17 00:00:00 2001 From: iven Date: Thu, 7 May 2026 07:23:41 +0800 Subject: [PATCH] =?UTF-8?q?fix(health):=20=E4=BF=AE=E5=A4=8D=205=20?= =?UTF-8?q?=E8=A7=92=E8=89=B2=E6=B5=8B=E8=AF=95=E5=8F=91=E7=8E=B0=E7=9A=84?= =?UTF-8?q?=204=20=E4=B8=AA=E5=85=B1=E6=80=A7=E9=97=AE=E9=A2=98?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 权限路由守卫:静默重定向改为显示 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 空字符串处理 --- apps/web/src/App.tsx | 97 +++++++++++-------- apps/web/src/components/EntityName.tsx | 2 +- apps/web/src/hooks/usePaginatedData.ts | 12 +++ apps/web/src/pages/health/AlertDashboard.tsx | 6 +- .../health/components/AlertDetailPanel.tsx | 3 +- .../erp-health/src/service/alert_service.rs | 43 ++++++-- .../src/service/consultation_service.rs | 31 +++++- 7 files changed, 141 insertions(+), 53 deletions(-) diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 6fcf9e7..17dfb9b 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -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 ; } +function ForbiddenPage() { + const navigate = useNavigate(); + return ( +
+ navigate('/')} style={{ cursor: 'pointer', color: 'var(--ant-color-primary)', background: 'none', border: 'none', fontSize: 14 }}>返回首页} + /> +
+ ); +} + +const ROUTE_PERMISSIONS: Record = { + '/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 ; - // 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页 - const path = window.location.hash.replace('#', ''); - const routePermissions: Record = { - '/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 ; + if (!hasAccess) return ; } // 冻结路由检查 diff --git a/apps/web/src/components/EntityName.tsx b/apps/web/src/components/EntityName.tsx index 0e9afeb..d3c8562 100644 --- a/apps/web/src/components/EntityName.tsx +++ b/apps/web/src/components/EntityName.tsx @@ -7,7 +7,7 @@ interface EntityNameProps { } export function EntityName({ name, id, fallbackLabel = '未知' }: EntityNameProps) { - if (name) return {name}; + if (name !== undefined && name !== null && name !== '') return {name}; if (id) { return ( diff --git a/apps/web/src/hooks/usePaginatedData.ts b/apps/web/src/hooks/usePaginatedData.ts index 818d866..efec654 100644 --- a/apps/web/src/hooks/usePaginatedData.ts +++ b/apps/web/src/hooks/usePaginatedData.ts @@ -84,6 +84,18 @@ export function usePaginatedData( } }, [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 }; } diff --git a/apps/web/src/pages/health/AlertDashboard.tsx b/apps/web/src/pages/health/AlertDashboard.tsx index d8830a6..afb5410 100644 --- a/apps/web/src/pages/health/AlertDashboard.tsx +++ b/apps/web/src/pages/health/AlertDashboard.tsx @@ -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); } diff --git a/apps/web/src/pages/health/components/AlertDetailPanel.tsx b/apps/web/src/pages/health/components/AlertDetailPanel.tsx index 80d46a9..a995eb8 100644 --- a/apps/web/src/pages/health/components/AlertDetailPanel.tsx +++ b/apps/web/src/pages/health/components/AlertDetailPanel.tsx @@ -17,6 +17,7 @@ const SEVERITY_CONFIG: Record = { 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 ( diff --git a/crates/erp-health/src/service/alert_service.rs b/crates/erp-health/src/service/alert_service.rs index f915f41..4cb0e8f 100644 --- a/crates/erp-health/src/service/alert_service.rs +++ b/crates/erp-health/src/service/alert_service.rs @@ -88,6 +88,34 @@ pub async fn list_alerts( Ok((items, total)) } +/// 将 alerts::Model 转换为 AlertResponse,并查找关联的患者名称。 +async fn enrich_alert_response( + db: &DatabaseConnection, + tenant_id: Uuid, + model: alerts::Model, +) -> HealthResult { + let patient_name = patient::Entity::find_by_id(model.patient_id) + .filter(patient::Column::TenantId.eq(tenant_id)) + .one(db) + .await? + .map(|p| p.name); + Ok(AlertResponse { + id: model.id, + patient_id: model.patient_id, + patient_name, + rule_id: model.rule_id, + severity: model.severity, + title: model.title, + detail: model.detail, + status: model.status, + acknowledged_by: model.acknowledged_by, + acknowledged_at: model.acknowledged_at, + resolved_at: model.resolved_at, + created_at: model.created_at, + version: model.version, + }) +} + /// 查询指定医生负责的所有患者 ID 列表(通过 patient_doctor_relation 表)。 async fn get_patient_ids_for_doctor( db: &DatabaseConnection, @@ -110,7 +138,7 @@ pub async fn acknowledge_alert( alert_id: Uuid, user_id: Uuid, version: i32, -) -> HealthResult { +) -> HealthResult { let alert = alerts::Entity::find_by_id(alert_id) .filter(alerts::Column::TenantId.eq(tenant_id)) .filter(alerts::Column::DeletedAt.is_null()) @@ -128,7 +156,8 @@ pub async fn acknowledge_alert( active.updated_at = Set(Utc::now()); active.version = Set(version + 1); - Ok(active.update(&state.db).await?) + let updated = active.update(&state.db).await?; + enrich_alert_response(&state.db, tenant_id, updated).await } pub async fn dismiss_alert( @@ -137,7 +166,7 @@ pub async fn dismiss_alert( alert_id: Uuid, user_id: Uuid, version: i32, -) -> HealthResult { +) -> HealthResult { let alert = alerts::Entity::find_by_id(alert_id) .filter(alerts::Column::TenantId.eq(tenant_id)) .filter(alerts::Column::DeletedAt.is_null()) @@ -154,7 +183,8 @@ pub async fn dismiss_alert( active.updated_at = Set(Utc::now()); active.version = Set(version + 1); - Ok(active.update(&state.db).await?) + let updated = active.update(&state.db).await?; + enrich_alert_response(&state.db, tenant_id, updated).await } pub async fn resolve_alert( @@ -162,7 +192,7 @@ pub async fn resolve_alert( tenant_id: Uuid, alert_id: Uuid, version: i32, -) -> HealthResult { +) -> HealthResult { let alert = alerts::Entity::find_by_id(alert_id) .filter(alerts::Column::TenantId.eq(tenant_id)) .filter(alerts::Column::DeletedAt.is_null()) @@ -179,7 +209,8 @@ pub async fn resolve_alert( active.updated_at = Set(Utc::now()); active.version = Set(version + 1); - Ok(active.update(&state.db).await?) + let updated = active.update(&state.db).await?; + enrich_alert_response(&state.db, tenant_id, updated).await } #[cfg(test)] diff --git a/crates/erp-health/src/service/consultation_service.rs b/crates/erp-health/src/service/consultation_service.rs index b6c64d5..eb4c168 100644 --- a/crates/erp-health/src/service/consultation_service.rs +++ b/crates/erp-health/src/service/consultation_service.rs @@ -35,6 +35,31 @@ fn model_to_session_resp(m: consultation_session::Model) -> SessionResp { } } +/// 查询单个会话的 patient_name 和 doctor_name 并填充到 SessionResp。 +async fn enrich_session_resp( + db: &DatabaseConnection, + tenant_id: Uuid, + mut resp: SessionResp, +) -> HealthResult { + let patient_name = patient::Entity::find_by_id(resp.patient_id) + .filter(patient::Column::TenantId.eq(tenant_id)) + .one(db) + .await? + .map(|p| p.name); + resp.patient_name = patient_name; + + if let Some(did) = resp.doctor_id { + let doctor_name = doctor_profile::Entity::find_by_id(did) + .filter(doctor_profile::Column::TenantId.eq(tenant_id)) + .one(db) + .await? + .map(|d| d.name); + resp.doctor_name = doctor_name; + } + + Ok(resp) +} + pub async fn create_session( state: &HealthState, tenant_id: Uuid, @@ -92,7 +117,7 @@ pub async fn create_session( &state.db, ).await; - Ok(model_to_session_resp(m)) + enrich_session_resp(&state.db, tenant_id, model_to_session_resp(m)).await } /// 获取单个咨询会话 @@ -110,7 +135,7 @@ pub async fn get_session( .await? .ok_or(HealthError::ConsultationNotFound)?; - Ok(model_to_session_resp(model)) + enrich_session_resp(&state.db, tenant_id, model_to_session_resp(model)).await } pub async fn list_sessions( @@ -231,7 +256,7 @@ pub async fn close_session( &state.db, ).await; - Ok(model_to_session_resp(m)) + enrich_session_resp(&state.db, tenant_id, model_to_session_resp(m)).await } pub async fn export_sessions(