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(