fix(web): Phase 3 前端 UX/i18n 修复 — 名称解析/确认对话框/日历切换/删除替换
- ConsultationList: 批量解析患者/医生名称替代截断 UUID - PointsOrderList: 使用 product_name + 批量解析患者/核销人名称 - AppointmentList: 破坏性状态变更添加 Modal.confirm + 取消原因收集 - CalendarView: 添加 onPanelChange 回调支持月份切换 - DoctorSchedule: 日历视图切换月份自动刷新数据 - PointsRuleList: 移除无效删除按钮,Switch 添加启用/停用文字 - PointsProductList: 删除按钮替换为上架/下架 Switch - PatientSelect: 性别显示中文化 (male→男, female→女) - VitalSignsChart: API 失败时显示 Alert 错误提示 - PointsOrder 类型: 添加 product_name 字段
This commit is contained in:
@@ -59,6 +59,7 @@ export interface PointsOrder {
|
|||||||
id: string;
|
id: string;
|
||||||
patient_id: string;
|
patient_id: string;
|
||||||
product_id: string;
|
product_id: string;
|
||||||
|
product_name: string | null;
|
||||||
points_cost: number;
|
points_cost: number;
|
||||||
status: string; // pending / verified / cancelled / expired
|
status: string; // pending / verified / cancelled / expired
|
||||||
qr_code: string;
|
qr_code: string;
|
||||||
|
|||||||
@@ -108,16 +108,60 @@ export default function AppointmentList() {
|
|||||||
}, [fetchData]);
|
}, [fetchData]);
|
||||||
|
|
||||||
// ---- 状态变更 ----
|
// ---- 状态变更 ----
|
||||||
const handleStatusChange = async (record: Appointment, newStatus: string) => {
|
const DESTRUCTIVE_STATUSES = new Set(['cancelled', 'no_show']);
|
||||||
try {
|
|
||||||
await appointmentApi.updateStatus(record.id, {
|
const handleStatusChange = (record: Appointment, newStatus: string) => {
|
||||||
status: newStatus,
|
const transition = STATUS_TRANSITIONS[record.status]?.find((t) => t.value === newStatus);
|
||||||
version: record.version,
|
if (!transition) return;
|
||||||
|
|
||||||
|
if (DESTRUCTIVE_STATUSES.has(newStatus)) {
|
||||||
|
let cancelReason = '';
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${transition.label}`,
|
||||||
|
content: newStatus === 'cancelled' ? (
|
||||||
|
<Input.TextArea
|
||||||
|
rows={3}
|
||||||
|
placeholder="请输入取消原因"
|
||||||
|
onChange={(e) => { cancelReason = e.target.value; }}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<span>确定将此预约标记为"{transition.label}"?</span>
|
||||||
|
),
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await appointmentApi.updateStatus(record.id, {
|
||||||
|
status: newStatus,
|
||||||
|
version: record.version,
|
||||||
|
...(newStatus === 'cancelled' && { cancel_reason: cancelReason }),
|
||||||
|
});
|
||||||
|
message.success('状态更新成功');
|
||||||
|
fetchData(page, pageSize);
|
||||||
|
} catch {
|
||||||
|
message.error('状态更新失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
Modal.confirm({
|
||||||
|
title: `确认${transition.label}`,
|
||||||
|
content: `确定将此预约状态变更为"${transition.label}"?`,
|
||||||
|
okText: '确认',
|
||||||
|
cancelText: '取消',
|
||||||
|
onOk: async () => {
|
||||||
|
try {
|
||||||
|
await appointmentApi.updateStatus(record.id, {
|
||||||
|
status: newStatus,
|
||||||
|
version: record.version,
|
||||||
|
});
|
||||||
|
message.success('状态更新成功');
|
||||||
|
fetchData(page, pageSize);
|
||||||
|
} catch {
|
||||||
|
message.error('状态更新失败');
|
||||||
|
}
|
||||||
|
},
|
||||||
});
|
});
|
||||||
message.success('状态更新成功');
|
|
||||||
fetchData(page, pageSize);
|
|
||||||
} catch {
|
|
||||||
message.error('状态更新失败');
|
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -13,6 +13,8 @@ import { PlusOutlined, CloseCircleOutlined } from '@ant-design/icons';
|
|||||||
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
|
||||||
import { useNavigate } from 'react-router-dom';
|
import { useNavigate } from 'react-router-dom';
|
||||||
import { consultationApi, type Session, type CreateSessionReq } from '../../api/health/consultations';
|
import { consultationApi, type Session, type CreateSessionReq } from '../../api/health/consultations';
|
||||||
|
import { patientApi } from '../../api/health/patients';
|
||||||
|
import { doctorApi } from '../../api/health/doctors';
|
||||||
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';
|
||||||
@@ -79,12 +81,48 @@ export default function ConsultationList() {
|
|||||||
const result = await consultationApi.listSessions(params);
|
const result = await consultationApi.listSessions(params);
|
||||||
setSessions(result.data);
|
setSessions(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
|
|
||||||
|
// 批量解析患者名称
|
||||||
|
const patientIds = [...new Set(result.data.map((s) => s.patient_id))];
|
||||||
|
const missingPatientIds = patientIds.filter((id) => !patientLabels[id]);
|
||||||
|
if (missingPatientIds.length > 0) {
|
||||||
|
const newLabels: Record<string, string> = {};
|
||||||
|
await Promise.all(
|
||||||
|
missingPatientIds.map(async (id) => {
|
||||||
|
try {
|
||||||
|
const detail = await patientApi.get(id);
|
||||||
|
newLabels[id] = detail.name;
|
||||||
|
} catch {
|
||||||
|
newLabels[id] = id.slice(0, 8);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setPatientLabels((prev) => ({ ...prev, ...newLabels }));
|
||||||
|
}
|
||||||
|
|
||||||
|
// 批量解析医生名称
|
||||||
|
const doctorIds = [...new Set(result.data.map((s) => s.doctor_id).filter(Boolean))] as string[];
|
||||||
|
const missingDoctorIds = doctorIds.filter((id) => !doctorLabels[id]);
|
||||||
|
if (missingDoctorIds.length > 0) {
|
||||||
|
const newLabels: Record<string, string> = {};
|
||||||
|
await Promise.all(
|
||||||
|
missingDoctorIds.map(async (id) => {
|
||||||
|
try {
|
||||||
|
const detail = await doctorApi.get(id);
|
||||||
|
newLabels[id] = detail.name;
|
||||||
|
} catch {
|
||||||
|
newLabels[id] = id.slice(0, 8);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setDoctorLabels((prev) => ({ ...prev, ...newLabels }));
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载咨询列表失败');
|
message.error('加载咨询列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, []);
|
}, [patientLabels, doctorLabels]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchSessions(query);
|
fetchSessions(query);
|
||||||
|
|||||||
@@ -94,17 +94,19 @@ export default function DoctorSchedule() {
|
|||||||
}, [page, pageSize, selectedDoctorId]);
|
}, [page, pageSize, selectedDoctorId]);
|
||||||
|
|
||||||
// ---- 日历数据获取 ----
|
// ---- 日历数据获取 ----
|
||||||
const fetchCalendar = useCallback(async () => {
|
const [calendarMonth, setCalendarMonth] = useState<Dayjs>(dayjs());
|
||||||
|
|
||||||
|
const fetchCalendar = useCallback(async (month?: Dayjs) => {
|
||||||
if (!selectedDoctorId) {
|
if (!selectedDoctorId) {
|
||||||
setCalendarData([]);
|
setCalendarData([]);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
const target = month ?? calendarMonth;
|
||||||
setCalendarLoading(true);
|
setCalendarLoading(true);
|
||||||
try {
|
try {
|
||||||
const now = dayjs();
|
|
||||||
const result = await appointmentApi.calendar({
|
const result = await appointmentApi.calendar({
|
||||||
start_date: now.startOf('month').format('YYYY-MM-DD'),
|
start_date: target.startOf('month').format('YYYY-MM-DD'),
|
||||||
end_date: now.endOf('month').format('YYYY-MM-DD'),
|
end_date: target.endOf('month').format('YYYY-MM-DD'),
|
||||||
doctor_id: selectedDoctorId,
|
doctor_id: selectedDoctorId,
|
||||||
});
|
});
|
||||||
setCalendarData(result);
|
setCalendarData(result);
|
||||||
@@ -113,7 +115,7 @@ export default function DoctorSchedule() {
|
|||||||
} finally {
|
} finally {
|
||||||
setCalendarLoading(false);
|
setCalendarLoading(false);
|
||||||
}
|
}
|
||||||
}, [selectedDoctorId]);
|
}, [selectedDoctorId, calendarMonth]);
|
||||||
|
|
||||||
// 切换医护或视图模式时加载数据
|
// 切换医护或视图模式时加载数据
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
@@ -339,7 +341,13 @@ export default function DoctorSchedule() {
|
|||||||
) : (
|
) : (
|
||||||
<Spin spinning={calendarLoading}>
|
<Spin spinning={calendarLoading}>
|
||||||
<div style={{ marginTop: 16 }}>
|
<div style={{ marginTop: 16 }}>
|
||||||
<CalendarView schedules={calendarScheduleMap} />
|
<CalendarView
|
||||||
|
schedules={calendarScheduleMap}
|
||||||
|
onPanelChange={(date) => {
|
||||||
|
setCalendarMonth(date);
|
||||||
|
fetchCalendar(date);
|
||||||
|
}}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</Spin>
|
</Spin>
|
||||||
)}
|
)}
|
||||||
|
|||||||
@@ -22,6 +22,7 @@ import {
|
|||||||
pointsApi,
|
pointsApi,
|
||||||
type PointsOrder,
|
type PointsOrder,
|
||||||
} from '../../api/health/points';
|
} from '../../api/health/points';
|
||||||
|
import { patientApi } from '../../api/health/patients';
|
||||||
|
|
||||||
/** 订单状态映射 */
|
/** 订单状态映射 */
|
||||||
const STATUS_MAP: Record<string, { text: string; color: string }> = {
|
const STATUS_MAP: Record<string, { text: string; color: string }> = {
|
||||||
@@ -54,6 +55,9 @@ export default function PointsOrderList() {
|
|||||||
const [verifyForm] = Form.useForm();
|
const [verifyForm] = Form.useForm();
|
||||||
const [verifying, setVerifying] = useState(false);
|
const [verifying, setVerifying] = useState(false);
|
||||||
|
|
||||||
|
// 名称缓存
|
||||||
|
const [nameCache, setNameCache] = useState<Record<string, string>>({});
|
||||||
|
|
||||||
// ---- 数据获取 ----
|
// ---- 数据获取 ----
|
||||||
const fetchData = useCallback(async (p = page, ps = pageSize) => {
|
const fetchData = useCallback(async (p = page, ps = pageSize) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -65,12 +69,30 @@ export default function PointsOrderList() {
|
|||||||
});
|
});
|
||||||
setData(result.data);
|
setData(result.data);
|
||||||
setTotal(result.total);
|
setTotal(result.total);
|
||||||
|
|
||||||
|
// 批量解析患者名称
|
||||||
|
const patientIds = [...new Set(result.data.map((o) => o.patient_id))];
|
||||||
|
const missingIds = patientIds.filter((id) => !nameCache[id]);
|
||||||
|
if (missingIds.length > 0) {
|
||||||
|
const newNames: Record<string, string> = {};
|
||||||
|
await Promise.all(
|
||||||
|
missingIds.map(async (id) => {
|
||||||
|
try {
|
||||||
|
const detail = await patientApi.get(id);
|
||||||
|
newNames[id] = detail.name;
|
||||||
|
} catch {
|
||||||
|
newNames[id] = id.slice(0, 8);
|
||||||
|
}
|
||||||
|
}),
|
||||||
|
);
|
||||||
|
setNameCache((prev) => ({ ...prev, ...newNames }));
|
||||||
|
}
|
||||||
} catch {
|
} catch {
|
||||||
message.error('加载订单列表失败');
|
message.error('加载订单列表失败');
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
}
|
}
|
||||||
}, [page, pageSize, statusFilter]);
|
}, [page, pageSize, statusFilter, nameCache]);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchData();
|
fetchData();
|
||||||
@@ -109,22 +131,19 @@ export default function PointsOrderList() {
|
|||||||
),
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '患者ID',
|
title: '患者',
|
||||||
dataIndex: 'patient_id',
|
dataIndex: 'patient_id',
|
||||||
key: 'patient_id',
|
key: 'patient_id',
|
||||||
width: 140,
|
width: 100,
|
||||||
render: (val: string) => (
|
render: (id: string) => nameCache[id] || id.slice(0, 8),
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{truncateId(val)}</span>
|
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '商品ID',
|
title: '商品',
|
||||||
dataIndex: 'product_id',
|
dataIndex: 'product_name',
|
||||||
key: 'product_id',
|
key: 'product_name',
|
||||||
width: 140,
|
width: 140,
|
||||||
render: (val: string) => (
|
render: (name: string | null, record: PointsOrder) =>
|
||||||
<span style={{ fontFamily: 'monospace', fontSize: 12 }}>{truncateId(val)}</span>
|
name || truncateId(record.product_id),
|
||||||
),
|
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '积分',
|
title: '积分',
|
||||||
@@ -161,8 +180,8 @@ export default function PointsOrderList() {
|
|||||||
title: '核销人',
|
title: '核销人',
|
||||||
dataIndex: 'verified_by',
|
dataIndex: 'verified_by',
|
||||||
key: 'verified_by',
|
key: 'verified_by',
|
||||||
width: 140,
|
width: 100,
|
||||||
render: (val: string | null) => val ? <Tag color="blue">{truncateId(val)}</Tag> : '-',
|
render: (val: string | null) => val ? <Tag color="blue">{nameCache[val] || val.slice(0, 8)}</Tag> : '-',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
title: '过期时间',
|
title: '过期时间',
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Tag,
|
Tag,
|
||||||
Badge,
|
Badge,
|
||||||
Popconfirm,
|
Switch,
|
||||||
message,
|
message,
|
||||||
Card,
|
Card,
|
||||||
Row,
|
Row,
|
||||||
@@ -19,7 +19,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
@@ -132,9 +131,24 @@ export default function PointsProductList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 删除 ----
|
// ---- 切换上下架 ----
|
||||||
const handleDelete = async (_id: string) => {
|
const handleToggleActive = async (record: PointsProduct) => {
|
||||||
message.info('当前版本暂不支持单独删除商品');
|
try {
|
||||||
|
const req: CreatePointsProductReq = {
|
||||||
|
name: record.name,
|
||||||
|
product_type: record.product_type,
|
||||||
|
points_cost: record.points_cost,
|
||||||
|
stock: record.stock,
|
||||||
|
description: record.description ?? undefined,
|
||||||
|
image_url: record.image_url ?? undefined,
|
||||||
|
sort_order: record.sort_order,
|
||||||
|
};
|
||||||
|
await pointsApi.createProduct(req);
|
||||||
|
message.success(record.is_active ? '已下架' : '已上架');
|
||||||
|
fetchData(page, pageSize);
|
||||||
|
} catch {
|
||||||
|
message.error('操作失败');
|
||||||
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 列定义 ----
|
// ---- 列定义 ----
|
||||||
@@ -207,16 +221,13 @@ export default function PointsProductList() {
|
|||||||
>
|
>
|
||||||
编辑
|
编辑
|
||||||
</Button>
|
</Button>
|
||||||
<Popconfirm
|
<Switch
|
||||||
title="确定删除该商品?"
|
size="small"
|
||||||
onConfirm={() => handleDelete(record.id)}
|
checked={record.is_active}
|
||||||
okText="确定"
|
checkedChildren="上架"
|
||||||
cancelText="取消"
|
unCheckedChildren="下架"
|
||||||
>
|
onChange={() => handleToggleActive(record)}
|
||||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
/>
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import {
|
|||||||
Select,
|
Select,
|
||||||
Tag,
|
Tag,
|
||||||
Badge,
|
Badge,
|
||||||
Popconfirm,
|
|
||||||
message,
|
message,
|
||||||
Card,
|
Card,
|
||||||
Row,
|
Row,
|
||||||
@@ -20,7 +19,6 @@ import {
|
|||||||
import {
|
import {
|
||||||
PlusOutlined,
|
PlusOutlined,
|
||||||
EditOutlined,
|
EditOutlined,
|
||||||
DeleteOutlined,
|
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import dayjs from 'dayjs';
|
import dayjs from 'dayjs';
|
||||||
import {
|
import {
|
||||||
@@ -145,11 +143,6 @@ export default function PointsRuleList() {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
// ---- 删除 ----
|
|
||||||
const handleDelete = async (_id: string) => {
|
|
||||||
message.info('当前版本通过重新创建规则覆盖,暂不支持单独删除');
|
|
||||||
};
|
|
||||||
|
|
||||||
// ---- 列定义 ----
|
// ---- 列定义 ----
|
||||||
const columns = [
|
const columns = [
|
||||||
{
|
{
|
||||||
@@ -235,18 +228,10 @@ export default function PointsRuleList() {
|
|||||||
<Switch
|
<Switch
|
||||||
size="small"
|
size="small"
|
||||||
checked={record.is_active}
|
checked={record.is_active}
|
||||||
|
checkedChildren="启用"
|
||||||
|
unCheckedChildren="停用"
|
||||||
onChange={() => handleToggleActive(record)}
|
onChange={() => handleToggleActive(record)}
|
||||||
/>
|
/>
|
||||||
<Popconfirm
|
|
||||||
title="确定删除该规则?"
|
|
||||||
onConfirm={() => handleDelete(record.id)}
|
|
||||||
okText="确定"
|
|
||||||
cancelText="取消"
|
|
||||||
>
|
|
||||||
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
|
|
||||||
删除
|
|
||||||
</Button>
|
|
||||||
</Popconfirm>
|
|
||||||
</Space>
|
</Space>
|
||||||
),
|
),
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -13,9 +13,10 @@ export interface ScheduleItem {
|
|||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
schedules: Record<string, ScheduleItem[]>;
|
schedules: Record<string, ScheduleItem[]>;
|
||||||
|
onPanelChange?: (date: Dayjs) => void;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function CalendarView({ schedules }: Props) {
|
export function CalendarView({ schedules, onPanelChange }: Props) {
|
||||||
const cellRender = (date: Dayjs) => {
|
const cellRender = (date: Dayjs) => {
|
||||||
const key = date.format('YYYY-MM-DD');
|
const key = date.format('YYYY-MM-DD');
|
||||||
const items = schedules[key];
|
const items = schedules[key];
|
||||||
@@ -38,5 +39,10 @@ export function CalendarView({ schedules }: Props) {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
return <Calendar cellRender={cellRender} />;
|
return (
|
||||||
|
<Calendar
|
||||||
|
cellRender={cellRender}
|
||||||
|
onPanelChange={(date) => onPanelChange?.(date)}
|
||||||
|
/>
|
||||||
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,6 +14,8 @@ export function PatientSelect({ value, onChange, placeholder }: Props) {
|
|||||||
>([]);
|
>([]);
|
||||||
const [fetching, setFetching] = useState(false);
|
const [fetching, setFetching] = useState(false);
|
||||||
|
|
||||||
|
const genderMap: Record<string, string> = { male: '男', female: '女' };
|
||||||
|
|
||||||
const handleSearch = useCallback(async (search: string) => {
|
const handleSearch = useCallback(async (search: string) => {
|
||||||
if (!search || search.length < 1) {
|
if (!search || search.length < 1) {
|
||||||
setOptions([]);
|
setOptions([]);
|
||||||
@@ -28,7 +30,7 @@ export function PatientSelect({ value, onChange, placeholder }: Props) {
|
|||||||
setOptions(
|
setOptions(
|
||||||
result.data.map((p) => ({
|
result.data.map((p) => ({
|
||||||
value: p.id,
|
value: p.id,
|
||||||
label: `${p.name}${p.gender ? ` (${p.gender})` : ''}`,
|
label: `${p.name}${p.gender ? ` (${genderMap[p.gender] || p.gender})` : ''}`,
|
||||||
})),
|
})),
|
||||||
);
|
);
|
||||||
} finally {
|
} finally {
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
import { useEffect, useState } from 'react';
|
import { useEffect, useState } from 'react';
|
||||||
import { Line } from '@ant-design/charts';
|
import { Line } from '@ant-design/charts';
|
||||||
import { Spin, Empty, Select, Space } from 'antd';
|
import { Spin, Empty, Select, Space, Alert } from 'antd';
|
||||||
import { healthDataApi } from '../../../api/health/healthData';
|
import { healthDataApi } from '../../../api/health/healthData';
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
@@ -20,17 +20,21 @@ export function VitalSignsChart({ patientId, indicator: initialIndicator }: Prop
|
|||||||
const [indicator, setIndicator] = useState(initialIndicator ?? 'systolic_bp_morning');
|
const [indicator, setIndicator] = useState(initialIndicator ?? 'systolic_bp_morning');
|
||||||
const [data, setData] = useState<{ date: string; value: number }[]>([]);
|
const [data, setData] = useState<{ date: string; value: number }[]>([]);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [error, setError] = useState(false);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
if (!patientId || !indicator) return;
|
if (!patientId || !indicator) return;
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
|
setError(false);
|
||||||
healthDataApi
|
healthDataApi
|
||||||
.getIndicatorTimeseries(patientId, indicator)
|
.getIndicatorTimeseries(patientId, indicator)
|
||||||
.then(setData)
|
.then(setData)
|
||||||
|
.catch(() => setError(true))
|
||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [patientId, indicator]);
|
}, [patientId, indicator]);
|
||||||
|
|
||||||
if (loading) return <Spin />;
|
if (loading) return <Spin />;
|
||||||
|
if (error) return <Alert type="error" message="加载数据失败,请稍后重试" />;
|
||||||
if (data.length === 0) return <Empty description="暂无数据" />;
|
if (data.length === 0) return <Empty description="暂无数据" />;
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
|
|||||||
Reference in New Issue
Block a user