feat(health): 健康数据统计 — 透析/化验/预约/体征上报率
- 新增 6 个统计端点: dialysis, lab-reports, appointments, vital-signs-report-rate, health-data(综合) - 透析统计: 类型分布/并发症率/平均超滤/平均时长 - 化验统计: 类型分布/异常项计数/审核状态 - 预约统计: 状态/类型分布/取消率 - 体征上报率: 月度上报率 + 近 7 天趋势 - Web 统计面板增加健康数据中心区块
This commit is contained in:
@@ -148,6 +148,62 @@ export interface OverviewStatistics {
|
||||
points: PointsStatistics;
|
||||
}
|
||||
|
||||
// --- Health Data Statistics Types ---
|
||||
|
||||
export interface NameValue {
|
||||
name: string;
|
||||
value: number;
|
||||
}
|
||||
|
||||
export interface DialysisStatistics {
|
||||
total_records: number;
|
||||
this_month: number;
|
||||
type_distribution: NameValue[];
|
||||
complication_rate: number;
|
||||
avg_ultrafiltration: number | null;
|
||||
avg_duration: number | null;
|
||||
pending_review: number;
|
||||
}
|
||||
|
||||
export interface LabReportStatistics {
|
||||
total_reports: number;
|
||||
this_month: number;
|
||||
type_distribution: NameValue[];
|
||||
abnormal_items: number;
|
||||
pending_review: number;
|
||||
reviewed: number;
|
||||
}
|
||||
|
||||
export interface AppointmentStatistics {
|
||||
total_appointments: number;
|
||||
this_month: number;
|
||||
status_distribution: NameValue[];
|
||||
type_distribution: NameValue[];
|
||||
cancel_rate: number;
|
||||
}
|
||||
|
||||
export interface DailyReportRate {
|
||||
date: string;
|
||||
reported: number;
|
||||
total: number;
|
||||
rate: number;
|
||||
}
|
||||
|
||||
export interface VitalSignsReportRate {
|
||||
total_patients: number;
|
||||
reported_patients: number;
|
||||
report_rate: number;
|
||||
total_records: number;
|
||||
daily_trend: DailyReportRate[];
|
||||
}
|
||||
|
||||
export interface HealthDataStats {
|
||||
dialysis: DialysisStatistics;
|
||||
lab_reports: LabReportStatistics;
|
||||
appointments: AppointmentStatistics;
|
||||
vital_signs_report_rate: VitalSignsReportRate;
|
||||
}
|
||||
|
||||
// --- API ---
|
||||
|
||||
export const pointsApi = {
|
||||
@@ -295,4 +351,12 @@ export const pointsApi = {
|
||||
}>('/health/admin/statistics/follow-ups');
|
||||
return data.data;
|
||||
},
|
||||
|
||||
getHealthDataStats: async (): Promise<HealthDataStats> => {
|
||||
const { data } = await client.get<{
|
||||
success: boolean;
|
||||
data: HealthDataStats;
|
||||
}>('/health/admin/statistics/health-data');
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -10,6 +10,7 @@ import {
|
||||
Button,
|
||||
Typography,
|
||||
Tooltip,
|
||||
Tag,
|
||||
} from 'antd';
|
||||
import {
|
||||
UserOutlined,
|
||||
@@ -33,6 +34,7 @@ import {
|
||||
type ConsultationStatistics,
|
||||
type FollowUpStatistics,
|
||||
type PointsStatistics,
|
||||
type HealthDataStats,
|
||||
} from '../../api/health/points';
|
||||
|
||||
const { Title: AntTitle, Text } = Typography;
|
||||
@@ -85,21 +87,24 @@ export default function StatisticsDashboard() {
|
||||
const [consultationStats, setConsultationStats] = useState<ConsultationStatistics | null>(null);
|
||||
const [followUpStats, setFollowUpStats] = useState<FollowUpStatistics | null>(null);
|
||||
const [pointsStats, setPointsStats] = useState<PointsStatistics | null>(null);
|
||||
const [healthDataStats, setHealthDataStats] = useState<HealthDataStats | null>(null);
|
||||
|
||||
const fetchAllStats = useCallback(async () => {
|
||||
setLoading(true);
|
||||
setError(null);
|
||||
try {
|
||||
const [patients, consultations, followUps, points] = await Promise.all([
|
||||
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);
|
||||
@@ -336,6 +341,159 @@ export default function StatisticsDashboard() {
|
||||
/>
|
||||
</Card>
|
||||
|
||||
{/* Section 2.5: Health Data Statistics */}
|
||||
<Card
|
||||
title={
|
||||
<span style={{ fontSize: 16, fontWeight: 600 }}>
|
||||
<MedicineBoxOutlined style={{ marginRight: 8, color: '#2563eb' }} />
|
||||
健康数据中心
|
||||
</span>
|
||||
}
|
||||
bordered={false}
|
||||
style={{ borderRadius: 12 }}
|
||||
>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* 透析统计 */}
|
||||
<Col xs={24} md={12}>
|
||||
<Card
|
||||
type="inner"
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>透析记录</span>}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<Statistic title="总记录" value={healthDataStats?.dialysis.total_records ?? 0} valueStyle={{ fontSize: 20 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="本月新增" value={healthDataStats?.dialysis.this_month ?? 0} valueStyle={{ fontSize: 20, color: '#2563eb' }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="待审核" value={healthDataStats?.dialysis.pending_review ?? 0} valueStyle={{ fontSize: 20, color: '#d97706' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[12, 12]} style={{ marginTop: 12 }}>
|
||||
<Col span={8}>
|
||||
<Statistic title="并发症率" value={healthDataStats?.dialysis.complication_rate ?? 0} suffix="%" precision={1} valueStyle={{ fontSize: 18 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="平均超滤(ml)" value={healthDataStats?.dialysis.avg_ultrafiltration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="平均时长(分)" value={healthDataStats?.dialysis.avg_duration ?? 0} precision={0} valueStyle={{ fontSize: 18 }} />
|
||||
</Col>
|
||||
</Row>
|
||||
{(healthDataStats?.dialysis.type_distribution ?? []).length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>类型分布: </Text>
|
||||
{healthDataStats!.dialysis.type_distribution.map((item) => (
|
||||
<Tag key={item.name} color="blue" style={{ marginTop: 4 }}>{item.name}: {item.value}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 化验报告 */}
|
||||
<Col xs={24} md={12}>
|
||||
<Card
|
||||
type="inner"
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>化验报告</span>}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<Statistic title="总报告" value={healthDataStats?.lab_reports.total_reports ?? 0} valueStyle={{ fontSize: 20 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="本月新增" value={healthDataStats?.lab_reports.this_month ?? 0} valueStyle={{ fontSize: 20, color: '#2563eb' }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="异常项" value={healthDataStats?.lab_reports.abnormal_items ?? 0} valueStyle={{ fontSize: 20, color: '#dc2626' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
<Row gutter={[12, 12]} style={{ marginTop: 12 }}>
|
||||
<Col span={12}>
|
||||
<Statistic title="待审核" value={healthDataStats?.lab_reports.pending_review ?? 0} valueStyle={{ fontSize: 18, color: '#d97706' }} />
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Statistic title="已审核" value={healthDataStats?.lab_reports.reviewed ?? 0} valueStyle={{ fontSize: 18, color: '#059669' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
{(healthDataStats?.lab_reports.type_distribution ?? []).length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>类型分布: </Text>
|
||||
{healthDataStats!.lab_reports.type_distribution.map((item) => (
|
||||
<Tag key={item.name} color="green" style={{ marginTop: 4 }}>{item.name}: {item.value}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 预约统计 */}
|
||||
<Col xs={24} md={12}>
|
||||
<Card
|
||||
type="inner"
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>预约统计</span>}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<Statistic title="总预约" value={healthDataStats?.appointments.total_appointments ?? 0} valueStyle={{ fontSize: 20 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="本月" value={healthDataStats?.appointments.this_month ?? 0} valueStyle={{ fontSize: 20, color: '#2563eb' }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="取消率" value={healthDataStats?.appointments.cancel_rate ?? 0} suffix="%" precision={1} valueStyle={{ fontSize: 20, color: '#dc2626' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
{(healthDataStats?.appointments.status_distribution ?? []).length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>状态: </Text>
|
||||
{healthDataStats!.appointments.status_distribution.map((item) => (
|
||||
<Tag key={item.name} color="purple" style={{ marginTop: 4 }}>{item.name}: {item.value}</Tag>
|
||||
))}
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 体征上报率 */}
|
||||
<Col xs={24} md={12}>
|
||||
<Card
|
||||
type="inner"
|
||||
title={<span style={{ fontSize: 14, fontWeight: 600 }}>体征上报率</span>}
|
||||
style={{ borderRadius: 8 }}
|
||||
>
|
||||
<Row gutter={[12, 12]}>
|
||||
<Col span={8}>
|
||||
<Statistic title="总患者" value={healthDataStats?.vital_signs_report_rate.total_patients ?? 0} valueStyle={{ fontSize: 20 }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="本月上报" value={healthDataStats?.vital_signs_report_rate.reported_patients ?? 0} valueStyle={{ fontSize: 20, color: '#059669' }} />
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Statistic title="上报率" value={healthDataStats?.vital_signs_report_rate.report_rate ?? 0} suffix="%" precision={1} valueStyle={{ fontSize: 20, color: '#7c3aed' }} />
|
||||
</Col>
|
||||
</Row>
|
||||
{(healthDataStats?.vital_signs_report_rate.daily_trend ?? []).length > 0 && (
|
||||
<div style={{ marginTop: 12 }}>
|
||||
<Text type="secondary" style={{ fontSize: 12 }}>近 7 天: </Text>
|
||||
<div style={{ display: 'flex', gap: 6, marginTop: 4, flexWrap: 'wrap' }}>
|
||||
{healthDataStats!.vital_signs_report_rate.daily_trend.map((d) => (
|
||||
<Tag key={d.date} color={d.rate >= 50 ? 'green' : d.rate >= 20 ? 'orange' : 'red'}>
|
||||
{d.date.slice(5)} {d.reported}/{d.total}
|
||||
</Tag>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
</Card>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
|
||||
{/* Section 3: Quick Links */}
|
||||
<Card
|
||||
title={
|
||||
|
||||
Reference in New Issue
Block a user