fix: QA 全量测试发现 5 个 bug 修复
- [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:
@@ -104,7 +104,7 @@ client.interceptors.response.use(
|
|||||||
},
|
},
|
||||||
async (error) => {
|
async (error) => {
|
||||||
const originalRequest = error.config;
|
const originalRequest = error.config;
|
||||||
if (error.response?.status === 401 && !originalRequest._retry) {
|
if (error.response?.status === 401 && !originalRequest._retry && !originalRequest.url?.includes('/auth/login')) {
|
||||||
if (isRefreshing) {
|
if (isRefreshing) {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
failedQueue.push({ resolve, reject });
|
failedQueue.push({ resolve, reject });
|
||||||
|
|||||||
@@ -186,7 +186,7 @@ export default function Home() {
|
|||||||
setActivitiesLoading(true);
|
setActivitiesLoading(true);
|
||||||
try {
|
try {
|
||||||
const result = await listAuditLogs({ page: 1, page_size: 5 });
|
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 {
|
} catch {
|
||||||
// 静默处理
|
// 静默处理
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
|||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
|
||||||
import { patientApi } from '../../api/health/patients';
|
import { patientApi } from '../../api/health/patients';
|
||||||
|
import { getUser } from '../../api/users';
|
||||||
import { StatusTag } from './components/StatusTag';
|
import { StatusTag } from './components/StatusTag';
|
||||||
import { PatientSelect } from './components/PatientSelect';
|
import { PatientSelect } from './components/PatientSelect';
|
||||||
import { DoctorSelect } from './components/DoctorSelect';
|
import { DoctorSelect } from './components/DoctorSelect';
|
||||||
@@ -120,6 +121,21 @@ export default function FollowUpTaskList() {
|
|||||||
if (Object.keys(newLabels).length > 0) {
|
if (Object.keys(newLabels).length > 0) {
|
||||||
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
|
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 {
|
} catch {
|
||||||
message.error('加载随访任务失败');
|
message.error('加载随访任务失败');
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -92,25 +92,33 @@ export default function StatisticsDashboard() {
|
|||||||
const fetchAllStats = useCallback(async () => {
|
const fetchAllStats = useCallback(async () => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
setError(null);
|
setError(null);
|
||||||
try {
|
|
||||||
const [patients, consultations, followUps, points, healthData] = await Promise.all([
|
let hasAnyError = false;
|
||||||
pointsApi.getPatientStats(),
|
const errors: string[] = [];
|
||||||
pointsApi.getConsultationStats(),
|
|
||||||
pointsApi.getFollowUpStats(),
|
const tryFetch = async <T,>(fn: () => Promise<T>, setter: (v: T) => void, label: string) => {
|
||||||
pointsApi.getStatistics(),
|
try {
|
||||||
pointsApi.getHealthDataStats(),
|
const data = await fn();
|
||||||
]);
|
setter(data);
|
||||||
setPatientStats(patients);
|
} catch {
|
||||||
setConsultationStats(consultations);
|
hasAnyError = true;
|
||||||
setFollowUpStats(followUps);
|
errors.push(label);
|
||||||
setPointsStats(points);
|
}
|
||||||
setHealthDataStats(healthData);
|
};
|
||||||
} catch (err: unknown) {
|
|
||||||
const message = err instanceof Error ? err.message : '加载统计数据失败';
|
await Promise.all([
|
||||||
setError(message);
|
tryFetch(pointsApi.getPatientStats, setPatientStats, '患者'),
|
||||||
} finally {
|
tryFetch(pointsApi.getConsultationStats, setConsultationStats, '咨询'),
|
||||||
setLoading(false);
|
tryFetch(pointsApi.getFollowUpStats, setFollowUpStats, '随访'),
|
||||||
|
tryFetch(pointsApi.getStatistics, setPointsStats, '积分'),
|
||||||
|
tryFetch(pointsApi.getHealthDataStats, setHealthDataStats, '健康数据'),
|
||||||
|
]);
|
||||||
|
|
||||||
|
if (hasAnyError && errors.length === 5) {
|
||||||
|
setError('加载统计数据失败');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setLoading(false);
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
|
|||||||
@@ -7,6 +7,16 @@ import { useThemeMode } from '../../hooks/useThemeMode';
|
|||||||
|
|
||||||
const { Paragraph } = Typography;
|
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 {
|
interface Props {
|
||||||
queryFilter?: MessageQuery;
|
queryFilter?: MessageQuery;
|
||||||
}
|
}
|
||||||
@@ -84,7 +94,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
|||||||
<div>
|
<div>
|
||||||
<Paragraph>{record.body}</Paragraph>
|
<Paragraph>{record.body}</Paragraph>
|
||||||
<div style={{ marginTop: 8, color: isDark ? '#475569' : '#94a3b8', fontSize: 12 }}>
|
<div style={{ marginTop: 8, color: isDark ? '#475569' : '#94a3b8', fontSize: 12 }}>
|
||||||
{record.created_at}
|
{formatDateTime(record.created_at)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
),
|
),
|
||||||
@@ -170,7 +180,7 @@ export default function NotificationList({ queryFilter }: Props) {
|
|||||||
key: 'created_at',
|
key: 'created_at',
|
||||||
width: 180,
|
width: 180,
|
||||||
render: (v: string) => (
|
render: (v: string) => (
|
||||||
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{v}</span>
|
<span style={{ color: isDark ? '#475569' : '#94a3b8', fontSize: 13 }}>{formatDateTime(v)}</span>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
|||||||
@@ -426,6 +426,8 @@ async fn compute_avg_field(
|
|||||||
field: &str,
|
field: &str,
|
||||||
) -> AppResult<Option<f64>> {
|
) -> AppResult<Option<f64>> {
|
||||||
const ALLOWED_FIELDS: &[&str] = &[
|
const ALLOWED_FIELDS: &[&str] = &[
|
||||||
|
"ultrafiltration_volume",
|
||||||
|
"dialysis_duration",
|
||||||
"uf_volume",
|
"uf_volume",
|
||||||
"uf_rate",
|
"uf_rate",
|
||||||
"blood_flow_rate",
|
"blood_flow_rate",
|
||||||
|
|||||||
Reference in New Issue
Block a user