Files
hms/apps/web/src/pages/health/components/VitalSignsTab.tsx
iven 6c60be0047
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
feat(web): 体征数据页面 UI/UX 优化 — 消除空白+信息密度提升
VitalSignsChart:
- 空状态改为带图标的虚线提示区域(替代 Empty 组件大片空白)
- 图表高度约束 180px,加载状态居中显示
- 添加最新值摘要显示
- 头部选择器与摘要信息并排布局

VitalSignsTab:
- 添加最新记录摘要条(体征数据一览)
- 表格上方显示记录总数
- 表头列名带 Tooltip 说明
- 录入按钮改为 small size,节省空间
- 表格添加 scroll.x 防止列溢出
2026-04-26 08:08:05 +08:00

235 lines
7.4 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { useCallback, useState } from 'react';
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message, Typography, Tooltip } from 'antd';
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
import { healthDataApi } from '../../../api/health/healthData';
import type { VitalSigns } from '../../../api/health/healthData';
import { VitalSignsChart } from './VitalSignsChart';
import { usePaginatedData } from '../../../hooks/usePaginatedData';
const { Text } = Typography;
interface Props {
patientId: string;
}
const columns = [
{
title: '记录日期',
dataIndex: 'record_date',
key: 'record_date',
width: 110,
fixed: 'left' as const,
},
{
title: () => (
<Tooltip title="晨间收缩压">
<span>()</span>
</Tooltip>
),
dataIndex: 'systolic_bp_morning',
key: 'systolic_bp_morning',
width: 110,
render: (v?: number) => (v != null ? `${v} mmHg` : '-'),
},
{
title: () => (
<Tooltip title="晨间舒张压">
<span>()</span>
</Tooltip>
),
dataIndex: 'diastolic_bp_morning',
key: 'diastolic_bp_morning',
width: 110,
render: (v?: number) => (v != null ? `${v} mmHg` : '-'),
},
{
title: '心率',
dataIndex: 'heart_rate',
key: 'heart_rate',
width: 80,
render: (v?: number) => (v != null ? `${v} bpm` : '-'),
},
{
title: '体重',
dataIndex: 'weight',
key: 'weight',
width: 80,
render: (v?: number) => (v != null ? `${v} kg` : '-'),
},
{
title: '血糖',
dataIndex: 'blood_sugar',
key: 'blood_sugar',
width: 90,
render: (v?: number) => (v != null ? `${v} mmol/L` : '-'),
},
];
export function VitalSignsTab({ patientId }: Props) {
const [modalOpen, setModalOpen] = useState(false);
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const fetcher = useCallback(
async (page: number, pageSize: number) => {
return healthDataApi.listVitalSigns(patientId, { page, page_size: pageSize });
},
[patientId],
);
const { data, total, page, loading, refresh } = usePaginatedData<VitalSigns>(fetcher, 10);
const handleCreate = async (values: {
record_date: Dayjs;
systolic_bp_morning?: number;
diastolic_bp_morning?: number;
heart_rate?: number;
weight?: number;
blood_sugar?: number;
water_intake_ml?: number;
urine_output_ml?: number;
notes?: string;
}) => {
setSubmitting(true);
try {
await healthDataApi.createVitalSigns(patientId, {
record_date: values.record_date.format('YYYY-MM-DD'),
systolic_bp_morning: values.systolic_bp_morning,
diastolic_bp_morning: values.diastolic_bp_morning,
heart_rate: values.heart_rate,
weight: values.weight,
blood_sugar: values.blood_sugar,
water_intake_ml: values.water_intake_ml,
urine_output_ml: values.urine_output_ml,
notes: values.notes,
});
message.success('体征数据录入成功');
setModalOpen(false);
form.resetFields();
refresh();
} catch {
message.error('录入失败');
} finally {
setSubmitting(false);
}
};
// 最近一次数据摘要
const latest = data.length > 0 ? data[0] : null;
return (
<div>
{/* 趋势图 */}
<div style={{ marginBottom: 16 }}>
<VitalSignsChart patientId={patientId} />
</div>
{/* 最新记录摘要条 */}
{latest && (
<div style={{
display: 'flex',
gap: 16,
marginBottom: 12,
padding: '8px 12px',
background: 'var(--ant-color-bg-container, #fafafa)',
borderRadius: 8,
border: '1px solid var(--ant-color-border-secondary, #f0f0f0)',
flexWrap: 'wrap',
}}>
<Text type="secondary" style={{ fontSize: 12, lineHeight: '24px' }}>
<InfoCircleOutlined style={{ marginRight: 4 }} />
{latest.record_date}
</Text>
{latest.systolic_bp_morning != null && (
<Text style={{ fontSize: 13 }}> {latest.systolic_bp_morning} mmHg</Text>
)}
{latest.diastolic_bp_morning != null && (
<Text style={{ fontSize: 13 }}> {latest.diastolic_bp_morning} mmHg</Text>
)}
{latest.heart_rate != null && (
<Text style={{ fontSize: 13 }}> {latest.heart_rate} bpm</Text>
)}
{latest.weight != null && (
<Text style={{ fontSize: 13 }}> {latest.weight} kg</Text>
)}
{latest.blood_sugar != null && (
<Text style={{ fontSize: 13 }}> {latest.blood_sugar} mmol/L</Text>
)}
</div>
)}
{/* 操作栏 + 表格 */}
<div style={{ marginBottom: 8, display: 'flex', justifyContent: 'space-between', alignItems: 'center' }}>
<Text type="secondary" style={{ fontSize: 13 }}>
{total}
</Text>
<Button
icon={<PlusOutlined />}
onClick={() => { form.resetFields(); setModalOpen(true); }}
size="small"
>
</Button>
</div>
<Table
columns={columns}
dataSource={data}
rowKey="id"
loading={loading}
size="small"
pagination={{
current: page,
total,
pageSize: 10,
onChange: (p) => refresh(p),
showTotal: (t) => `${t}`,
size: 'small',
style: { margin: 0 },
}}
scroll={{ x: 600 }}
/>
<Modal
title="录入体征数据"
open={modalOpen}
onCancel={() => setModalOpen(false)}
onOk={() => form.submit()}
confirmLoading={submitting}
destroyOnClose
width={600}
>
<Form form={form} layout="vertical" onFinish={handleCreate}>
<Form.Item name="record_date" label="记录日期" rules={[{ required: true, message: '请选择日期' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<div style={{ display: 'grid', gridTemplateColumns: '1fr 1fr', gap: 16 }}>
<Form.Item name="systolic_bp_morning" label="收缩压(晨) mmHg">
<InputNumber min={50} max={300} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="diastolic_bp_morning" label="舒张压(晨) mmHg">
<InputNumber min={30} max={200} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="heart_rate" label="心率 bpm">
<InputNumber min={30} max={250} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="weight" label="体重 kg">
<InputNumber min={1} max={500} step={0.1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="blood_sugar" label="血糖 mmol/L">
<InputNumber min={0} max={50} step={0.1} style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="water_intake_ml" label="饮水量 ml">
<InputNumber min={0} max={10000} style={{ width: '100%' }} />
</Form.Item>
</div>
<Form.Item name="notes" label="备注">
<Input.TextArea rows={2} />
</Form.Item>
</Form>
</Modal>
</div>
);
}