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) => {
|
||||
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) {
|
||||
return new Promise((resolve, reject) => {
|
||||
failedQueue.push({ resolve, reject });
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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(() => {
|
||||
|
||||
@@ -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>
|
||||
),
|
||||
},
|
||||
{
|
||||
|
||||
@@ -426,6 +426,8 @@ async fn compute_avg_field(
|
||||
field: &str,
|
||||
) -> AppResult<Option<f64>> {
|
||||
const ALLOWED_FIELDS: &[&str] = &[
|
||||
"ultrafiltration_volume",
|
||||
"dialysis_duration",
|
||||
"uf_volume",
|
||||
"uf_rate",
|
||||
"blood_flow_rate",
|
||||
|
||||
Reference in New Issue
Block a user