fix: QA 全量测试发现 5 个 bug 修复
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

- [P0] 登录失败无反馈: client.ts 响应拦截器跳过 /auth/login 的 401 处理,让错误传播到 Login 组件
- [P0] 统计仪表盘 400: 前端用独立 try/catch 替代 Promise.all 提高容错性;后端 stats_service 白名单补充 ultrafiltration_volume/dialysis_duration
- [P1] 随访负责人显示 UUID: 批量解析 assigned_to 用户名
- [P2] 消息中心时间未格式化: 添加 formatDateTime 函数
- [P2] 首页显示 login_failed: 过滤审计日志中的 login_failed 动作
This commit is contained in:
iven
2026-04-26 23:48:22 +08:00
parent 125d2479ea
commit ac919731a9
6 changed files with 58 additions and 22 deletions

View File

@@ -186,7 +186,7 @@ export default function Home() {
setActivitiesLoading(true);
try {
const result = await listAuditLogs({ page: 1, page_size: 5 });
if (!cancelled) setRecentActivities(result.data);
if (!cancelled) setRecentActivities(result.data.filter(a => a.action !== 'login_failed'));
} catch {
// 静默处理
} finally {

View File

@@ -16,6 +16,7 @@ import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import dayjs from 'dayjs';
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
import { patientApi } from '../../api/health/patients';
import { getUser } from '../../api/users';
import { StatusTag } from './components/StatusTag';
import { PatientSelect } from './components/PatientSelect';
import { DoctorSelect } from './components/DoctorSelect';
@@ -120,6 +121,21 @@ export default function FollowUpTaskList() {
if (Object.keys(newLabels).length > 0) {
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
}
// Batch resolve assignee names
const assigneeIds = [...new Set(result.data.map((t: FollowUpTask) => t.assigned_to).filter(Boolean))];
const newDoctorLabels: Record<string, string> = {};
await Promise.allSettled(
assigneeIds.map(async (id: string) => {
try {
const u = await getUser(id);
newDoctorLabels[id] = u.display_name || u.username;
} catch { /* skip */ }
}),
);
if (Object.keys(newDoctorLabels).length > 0) {
setDoctorLabels((prev) => ({ ...prev, ...newDoctorLabels }));
}
} catch {
message.error('加载随访任务失败');
} finally {

View File

@@ -92,25 +92,33 @@ export default function StatisticsDashboard() {
const fetchAllStats = useCallback(async () => {
setLoading(true);
setError(null);
try {
const [patients, consultations, followUps, points, healthData] = await Promise.all([
pointsApi.getPatientStats(),
pointsApi.getConsultationStats(),
pointsApi.getFollowUpStats(),
pointsApi.getStatistics(),
pointsApi.getHealthDataStats(),
]);
setPatientStats(patients);
setConsultationStats(consultations);
setFollowUpStats(followUps);
setPointsStats(points);
setHealthDataStats(healthData);
} catch (err: unknown) {
const message = err instanceof Error ? err.message : '加载统计数据失败';
setError(message);
} finally {
setLoading(false);
let hasAnyError = false;
const errors: string[] = [];
const tryFetch = async <T,>(fn: () => Promise<T>, setter: (v: T) => void, label: string) => {
try {
const data = await fn();
setter(data);
} catch {
hasAnyError = true;
errors.push(label);
}
};
await Promise.all([
tryFetch(pointsApi.getPatientStats, setPatientStats, '患者'),
tryFetch(pointsApi.getConsultationStats, setConsultationStats, '咨询'),
tryFetch(pointsApi.getFollowUpStats, setFollowUpStats, '随访'),
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
]);
if (hasAnyError && errors.length === 5) {
setError('加载统计数据失败');
}
setLoading(false);
}, []);
useEffect(() => {

View File

@@ -7,6 +7,16 @@ import { useThemeMode } from '../../hooks/useThemeMode';
const { Paragraph } = Typography;
function formatDateTime(value: string): string {
return new Date(value).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit',
});
}
interface Props {
queryFilter?: MessageQuery;
}
@@ -84,7 +94,7 @@ export default function NotificationList({ queryFilter }: Props) {
<div>
<Paragraph>{record.body}</Paragraph>
<div style={{ marginTop: 8, color: isDark ? '#475569' : '#94a3b8', fontSize: 12 }}>
{record.created_at}
{formatDateTime(record.created_at)}
</div>
</div>
),
@@ -170,7 +180,7 @@ export default function NotificationList({ queryFilter }: Props) {
key: 'created_at',
width: 180,
render: (v: string) => (
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{v}</span>
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{formatDateTime(v)}</span>
),
},
{