fix(health): 修复 5 角色测试发现的 4 个共性问题
- 权限路由守卫:静默重定向改为显示 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:
@@ -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,15 +84,21 @@ function FrozenRoute() {
|
||||
return <Result status="info" title="功能暂未开放" subTitle="该功能正在优化中,敬请期待" />;
|
||||
}
|
||||
|
||||
function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
const isAuthenticated = useAuthStore((s) => s.isAuthenticated);
|
||||
const permissions = useAuthStore((s) => s.permissions);
|
||||
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>
|
||||
);
|
||||
}
|
||||
|
||||
if (!isAuthenticated) return <Navigate to="/login" replace />;
|
||||
|
||||
// 路由级权限检查:如果用户对某个模块完全没有权限,重定向到首页
|
||||
const path = window.location.hash.replace('#', '');
|
||||
const routePermissions: Record<string, string[]> = {
|
||||
const ROUTE_PERMISSIONS: Record<string, string[]> = {
|
||||
'/users': ['user.list', 'user.manage'],
|
||||
'/roles': ['role.list', 'role.manage'],
|
||||
'/organizations': ['organization.list', 'organization.manage'],
|
||||
@@ -110,6 +116,7 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
'/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'],
|
||||
@@ -124,12 +131,24 @@ function PrivateRoute({ children }: { children: React.ReactNode }) {
|
||||
'/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));
|
||||
'/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 = 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 />;
|
||||
}
|
||||
|
||||
// 冻结路由检查
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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 };
|
||||
}
|
||||
|
||||
|
||||
@@ -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);
|
||||
}
|
||||
|
||||
@@ -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 (
|
||||
|
||||
@@ -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<AlertResponse> {
|
||||
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<alerts::Model> {
|
||||
) -> HealthResult<AlertResponse> {
|
||||
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<alerts::Model> {
|
||||
) -> HealthResult<AlertResponse> {
|
||||
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<alerts::Model> {
|
||||
) -> HealthResult<AlertResponse> {
|
||||
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)]
|
||||
|
||||
@@ -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<SessionResp> {
|
||||
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(
|
||||
|
||||
Reference in New Issue
Block a user