feat(web): 体征数据页面 UI/UX 优化 — 消除空白+信息密度提升
VitalSignsChart: - 空状态改为带图标的虚线提示区域(替代 Empty 组件大片空白) - 图表高度约束 180px,加载状态居中显示 - 添加最新值摘要显示 - 头部选择器与摘要信息并排布局 VitalSignsTab: - 添加最新记录摘要条(体征数据一览) - 表格上方显示记录总数 - 表头列名带 Tooltip 说明 - 录入按钮改为 small size,节省空间 - 表格添加 scroll.x 防止列溢出
This commit is contained in:
@@ -1,8 +1,11 @@
|
|||||||
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, Alert } from 'antd';
|
import { Spin, Select, Alert, Typography } from 'antd';
|
||||||
|
import { LineChartOutlined } from '@ant-design/icons';
|
||||||
import { healthDataApi } from '../../../api/health/healthData';
|
import { healthDataApi } from '../../../api/health/healthData';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
patientId: string;
|
patientId: string;
|
||||||
indicator?: string;
|
indicator?: string;
|
||||||
@@ -16,6 +19,14 @@ const INDICATORS = [
|
|||||||
{ value: 'blood_sugar', label: '血糖' },
|
{ value: 'blood_sugar', label: '血糖' },
|
||||||
];
|
];
|
||||||
|
|
||||||
|
const INDICATOR_UNITS: Record<string, string> = {
|
||||||
|
systolic_bp_morning: 'mmHg',
|
||||||
|
diastolic_bp_morning: 'mmHg',
|
||||||
|
heart_rate: 'bpm',
|
||||||
|
weight: 'kg',
|
||||||
|
blood_sugar: 'mmol/L',
|
||||||
|
};
|
||||||
|
|
||||||
export function VitalSignsChart({ patientId, indicator: initialIndicator }: Props) {
|
export function VitalSignsChart({ patientId, indicator: initialIndicator }: Props) {
|
||||||
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 }[]>([]);
|
||||||
@@ -33,30 +44,93 @@ export function VitalSignsChart({ patientId, indicator: initialIndicator }: Prop
|
|||||||
.finally(() => setLoading(false));
|
.finally(() => setLoading(false));
|
||||||
}, [patientId, indicator]);
|
}, [patientId, indicator]);
|
||||||
|
|
||||||
if (loading) return <Spin />;
|
const unit = INDICATOR_UNITS[indicator] ?? '';
|
||||||
if (error) return <Alert type="error" message="加载数据失败,请稍后重试" />;
|
|
||||||
if (data.length === 0) return <Empty description="暂无数据" />;
|
// 头部:选择器 + 最新值摘要
|
||||||
|
const latestValue = data.length > 0 ? data[data.length - 1].value : null;
|
||||||
|
|
||||||
|
const renderHeader = () => (
|
||||||
|
<div style={{
|
||||||
|
display: 'flex',
|
||||||
|
justifyContent: 'space-between',
|
||||||
|
alignItems: 'center',
|
||||||
|
marginBottom: 12,
|
||||||
|
}}>
|
||||||
|
<Select
|
||||||
|
value={indicator}
|
||||||
|
onChange={setIndicator}
|
||||||
|
options={INDICATORS}
|
||||||
|
style={{ width: 160 }}
|
||||||
|
size="small"
|
||||||
|
/>
|
||||||
|
{latestValue != null && (
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>
|
||||||
|
最新:{latestValue} {unit}
|
||||||
|
</Text>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
|
||||||
|
if (loading) {
|
||||||
|
return (
|
||||||
|
<div style={{ height: 180, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
|
||||||
|
<Spin size="small" />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (error) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderHeader()}
|
||||||
|
<Alert type="error" message="加载数据失败" showIcon style={{ borderRadius: 8 }} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (data.length === 0) {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
{renderHeader()}
|
||||||
|
<div style={{
|
||||||
|
height: 120,
|
||||||
|
display: 'flex',
|
||||||
|
flexDirection: 'column',
|
||||||
|
alignItems: 'center',
|
||||||
|
justifyContent: 'center',
|
||||||
|
background: 'var(--ant-color-bg-container, #fafafa)',
|
||||||
|
borderRadius: 8,
|
||||||
|
border: '1px dashed var(--ant-color-border, #d9d9d9)',
|
||||||
|
}}>
|
||||||
|
<LineChartOutlined style={{ fontSize: 24, color: '#bfbfbf', marginBottom: 4 }} />
|
||||||
|
<Text type="secondary" style={{ fontSize: 13 }}>暂无趋势数据</Text>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
const config = {
|
const config = {
|
||||||
data,
|
data,
|
||||||
xField: 'date',
|
xField: 'date',
|
||||||
yField: 'value',
|
yField: 'value',
|
||||||
smooth: true,
|
smooth: true,
|
||||||
point: { shapeField: 'circle', sizeField: 4 },
|
height: 180,
|
||||||
height: 220,
|
point: { shapeField: 'circle', sizeField: 3 },
|
||||||
|
axis: {
|
||||||
|
x: { labelAutoRotate: false },
|
||||||
|
y: { title: unit || undefined },
|
||||||
|
},
|
||||||
|
style: {
|
||||||
|
lineWidth: 2,
|
||||||
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Space direction="vertical" style={{ width: '100%' }}>
|
<div>
|
||||||
<Select
|
{renderHeader()}
|
||||||
value={indicator}
|
|
||||||
onChange={setIndicator}
|
|
||||||
options={INDICATORS}
|
|
||||||
style={{ width: 180 }}
|
|
||||||
/>
|
|
||||||
<div style={{ width: '100%' }}>
|
<div style={{ width: '100%' }}>
|
||||||
<Line {...config} />
|
<Line {...config} />
|
||||||
</div>
|
</div>
|
||||||
</Space>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,23 +1,69 @@
|
|||||||
import { useCallback, useState } from 'react';
|
import { useCallback, useState } from 'react';
|
||||||
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message } from 'antd';
|
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message, Typography, Tooltip } from 'antd';
|
||||||
import { PlusOutlined } from '@ant-design/icons';
|
import { PlusOutlined, InfoCircleOutlined } from '@ant-design/icons';
|
||||||
import type { Dayjs } from 'dayjs';
|
import type { Dayjs } from 'dayjs';
|
||||||
import { healthDataApi } from '../../../api/health/healthData';
|
import { healthDataApi } from '../../../api/health/healthData';
|
||||||
import type { VitalSigns } from '../../../api/health/healthData';
|
import type { VitalSigns } from '../../../api/health/healthData';
|
||||||
import { VitalSignsChart } from './VitalSignsChart';
|
import { VitalSignsChart } from './VitalSignsChart';
|
||||||
import { usePaginatedData } from '../../../hooks/usePaginatedData';
|
import { usePaginatedData } from '../../../hooks/usePaginatedData';
|
||||||
|
|
||||||
|
const { Text } = Typography;
|
||||||
|
|
||||||
interface Props {
|
interface Props {
|
||||||
patientId: string;
|
patientId: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
const columns = [
|
const columns = [
|
||||||
{ title: '记录日期', dataIndex: 'record_date', key: 'record_date', width: 120 },
|
{
|
||||||
{ title: '收缩压(晨)', dataIndex: 'systolic_bp_morning', key: 'systolic_bp_morning', width: 110, render: (v?: number) => (v != null ? `${v} mmHg` : '-') },
|
title: '记录日期',
|
||||||
{ title: '舒张压(晨)', dataIndex: 'diastolic_bp_morning', key: 'diastolic_bp_morning', width: 110, render: (v?: number) => (v != null ? `${v} mmHg` : '-') },
|
dataIndex: 'record_date',
|
||||||
{ title: '心率', dataIndex: 'heart_rate', key: 'heart_rate', width: 80, render: (v?: number) => (v != null ? `${v} bpm` : '-') },
|
key: 'record_date',
|
||||||
{ title: '体重', dataIndex: 'weight', key: 'weight', width: 80, render: (v?: number) => (v != null ? `${v} kg` : '-') },
|
width: 110,
|
||||||
{ title: '血糖', dataIndex: 'blood_sugar', key: 'blood_sugar', width: 80, render: (v?: number) => (v != null ? `${v} mmol/L` : '-') },
|
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) {
|
export function VitalSignsTab({ patientId }: Props) {
|
||||||
@@ -69,16 +115,64 @@ export function VitalSignsTab({ patientId }: Props) {
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
|
// 最近一次数据摘要
|
||||||
|
const latest = data.length > 0 ? data[0] : null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
|
{/* 趋势图 */}
|
||||||
<Button icon={<PlusOutlined />} onClick={() => { form.resetFields(); setModalOpen(true); }}>
|
|
||||||
录入体征
|
|
||||||
</Button>
|
|
||||||
</div>
|
|
||||||
<div style={{ marginBottom: 16 }}>
|
<div style={{ marginBottom: 16 }}>
|
||||||
<VitalSignsChart patientId={patientId} />
|
<VitalSignsChart patientId={patientId} />
|
||||||
</div>
|
</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
|
<Table
|
||||||
columns={columns}
|
columns={columns}
|
||||||
dataSource={data}
|
dataSource={data}
|
||||||
@@ -86,12 +180,17 @@ export function VitalSignsTab({ patientId }: Props) {
|
|||||||
loading={loading}
|
loading={loading}
|
||||||
size="small"
|
size="small"
|
||||||
pagination={{
|
pagination={{
|
||||||
current: page, total, pageSize: 10,
|
current: page,
|
||||||
|
total,
|
||||||
|
pageSize: 10,
|
||||||
onChange: (p) => refresh(p),
|
onChange: (p) => refresh(p),
|
||||||
showTotal: (t) => `共 ${t} 条`,
|
showTotal: (t) => `共 ${t} 条`,
|
||||||
|
size: 'small',
|
||||||
style: { margin: 0 },
|
style: { margin: 0 },
|
||||||
}}
|
}}
|
||||||
|
scroll={{ x: 600 }}
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<Modal
|
<Modal
|
||||||
title="录入体征数据"
|
title="录入体征数据"
|
||||||
open={modalOpen}
|
open={modalOpen}
|
||||||
|
|||||||
Reference in New Issue
Block a user