Files
hms/apps/web/src/pages/health/components/LabReportsTab.tsx
iven c38967a36e fix(mp): 修复小程序角色路由 + 前后端字段对齐 + E2E 测试报告
- 修复 stores/auth.ts 三种登录方式从错误路径提取 roles(resp.roles → resp.user.roles)
- 首页添加医护人员自动跳转医生端(useDidShow + isMedicalStaff)
- services/auth.ts credentialLogin 返回类型补全 roles 字段
- Web 前端 healthData.ts 字段对齐后端 DTO(indicators→items, content→overall_assessment)
- Web 前端 medicationReminders.ts 字段对齐(time_slots→reminder_times)
- 小程序 report.ts / reports 页面字段对齐后端(indicators→items, doctor_interpretation→doctor_notes)
- 小程序 patient.ts / followup.ts / alert.ts 补全缺失字段
- 后端 stats_handler.rs 权限码修正(health.patient.list→health.dashboard.manage)
- 新增 V1 E2E 测试报告和五专家组评审报告
2026-05-17 01:51:02 +08:00

294 lines
11 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, useMemo } from 'react';
import { Table, Tag, Button, Modal, Form, Input, DatePicker, message, Popconfirm, Space, Card } from 'antd';
import { PlusOutlined, EditOutlined, DeleteOutlined, AuditOutlined, ThunderboltOutlined, FileTextOutlined } from '@ant-design/icons';
import { dayjs } from '../../../utils/dayjs';
import type { Dayjs } from 'dayjs';
import { healthDataApi } from '../../../api/health/healthData';
import type { LabReport } from '../../../api/health/healthData';
import { usePaginatedData } from '../../../hooks/usePaginatedData';
import { AuthButton } from '../../../components/AuthButton';
import { handleApiError } from '../../../api/client';
import { startAnalysis } from '../../../api/ai/analysisSse';
interface Props {
patientId: string;
}
const STATUS_MAP: Record<string, { color: string; label: string }> = {
pending: { color: 'orange', label: '待审核' },
reviewed: { color: 'green', label: '已审核' },
};
export function LabReportsTab({ patientId }: Props) {
const [modalOpen, setModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<LabReport | null>(null);
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
const [reviewOpen, setReviewOpen] = useState(false);
const [reviewRecord, setReviewRecord] = useState<LabReport | null>(null);
const [reviewNotes, setReviewNotes] = useState('');
const [reviewSubmitting, setReviewSubmitting] = useState(false);
const [analyzingReportId, setAnalyzingReportId] = useState<string | null>(null);
const [analysisContent, setAnalysisContent] = useState('');
const [summaryReportId, setSummaryReportId] = useState<string | null>(null);
const [summaryContent, setSummaryContent] = useState('');
const handleAiAnalysis = async (reportId: string) => {
setAnalyzingReportId(reportId);
setAnalysisContent('');
await startAnalysis('lab-report', { report_id: reportId }, {
onChunk: (content) => setAnalysisContent(prev => prev + content),
onError: (msg) => { message.error(msg); setAnalyzingReportId(null); },
onDone: () => {
message.success('AI 分析完成');
setAnalyzingReportId(null);
},
});
};
const handleReportSummary = async (reportId: string) => {
setSummaryReportId(reportId);
setSummaryContent('');
await startAnalysis('report-summary', { report_id: reportId }, {
onChunk: (content) => setSummaryContent(prev => prev + content),
onError: (msg) => { message.error(msg); setSummaryReportId(null); },
onDone: () => {
message.success('报告摘要生成完成');
setSummaryReportId(null);
},
});
};
const fetcher = useCallback(
async (page: number, pageSize: number) => {
return healthDataApi.listLabReports(patientId, { page, page_size: pageSize });
},
[patientId],
);
const { data, total, page, loading, refresh } = usePaginatedData<LabReport>(fetcher, 10);
const openCreateModal = () => {
setEditingRecord(null);
form.resetFields();
setModalOpen(true);
};
const openEditModal = (record: LabReport) => {
setEditingRecord(record);
form.setFieldsValue({
report_date: dayjs(record.report_date),
report_type: record.report_type,
doctor_notes: record.doctor_notes,
});
setModalOpen(true);
};
const handleSubmit = async (values: {
report_date: Dayjs;
report_type: string;
doctor_notes?: string;
}) => {
setSubmitting(true);
try {
if (editingRecord) {
await healthDataApi.updateLabReport(patientId, editingRecord.id, {
report_date: values.report_date.format('YYYY-MM-DD'),
report_type: values.report_type,
doctor_notes: values.doctor_notes,
version: editingRecord.version,
});
message.success('化验报告更新成功');
} else {
await healthDataApi.createLabReport(patientId, {
report_date: values.report_date.format('YYYY-MM-DD'),
report_type: values.report_type,
doctor_notes: values.doctor_notes,
});
message.success('化验报告添加成功');
}
setModalOpen(false);
setEditingRecord(null);
form.resetFields();
refresh();
} catch (err) {
handleApiError(err, editingRecord ? '更新失败' : '添加失败');
} finally {
setSubmitting(false);
}
};
const handleDelete = async (record: LabReport) => {
try {
await healthDataApi.deleteLabReport(patientId, record.id);
message.success('化验报告删除成功');
refresh();
} catch (err) {
handleApiError(err, '删除失败');
}
};
const openReviewModal = (record: LabReport) => {
setReviewRecord(record);
setReviewNotes(record.doctor_notes || '');
setReviewOpen(true);
};
const handleReview = async () => {
if (!reviewRecord) return;
setReviewSubmitting(true);
try {
await healthDataApi.reviewLabReport(patientId, reviewRecord.id, {
version: reviewRecord.version,
doctor_notes: reviewNotes || undefined,
});
message.success('审核完成');
setReviewOpen(false);
setReviewRecord(null);
setReviewNotes('');
refresh();
} catch (err) {
handleApiError(err, '审核失败');
} finally {
setReviewSubmitting(false);
}
};
const columns = useMemo(() => [
{ title: '报告日期', dataIndex: 'report_date', key: 'report_date', width: 120 },
{ title: '报告类型', dataIndex: 'report_type', key: 'report_type', width: 120, render: (v: string) => <Tag>{v}</Tag> },
{
title: '状态', dataIndex: 'status', key: 'status', width: 90,
render: (v: string) => {
const m = STATUS_MAP[v];
return m ? <Tag color={m.color}>{m.label}</Tag> : <Tag>{v}</Tag>;
},
},
{ title: '医生解读', dataIndex: 'doctor_notes', key: 'doctor_notes', ellipsis: true },
{ title: '创建时间', dataIndex: 'created_at', key: 'created_at', width: 170, render: (v: string) => new Date(v).toLocaleString('zh-CN') },
{
title: '操作',
key: 'actions',
width: 180,
render: (_: unknown, record: LabReport) => (
<AuthButton code="health.health-data.manage">
<Space size={0}>
<AuthButton code="ai.analysis.manage">
<Button type="link" size="small" icon={<ThunderboltOutlined />} loading={analyzingReportId === record.id} onClick={(e) => { e.stopPropagation(); handleAiAnalysis(record.id); }}>
AI
</Button>
</AuthButton>
<AuthButton code="ai.analysis.manage">
<Button type="link" size="small" icon={<FileTextOutlined />} loading={summaryReportId === record.id} onClick={(e) => { e.stopPropagation(); handleReportSummary(record.id); }}>
</Button>
</AuthButton>
{record.status === 'pending' && (
<Button type="link" size="small" icon={<AuditOutlined />} onClick={() => openReviewModal(record)}>
</Button>
)}
<Button type="link" size="small" icon={<EditOutlined />} onClick={() => openEditModal(record)}>
</Button>
<Popconfirm title="确认删除该化验报告?" onConfirm={() => handleDelete(record)} okText="确认" cancelText="取消">
<Button type="link" size="small" danger icon={<DeleteOutlined />}>
</Button>
</Popconfirm>
</Space>
</AuthButton>
),
},
], [openEditModal, handleDelete]);
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'flex-end' }}>
<Button icon={<PlusOutlined />} onClick={openCreateModal}>
</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}`,
style: { margin: 0 },
}}
/>
{analysisContent && (
<Card title="AI 解读结果" style={{ marginTop: 16 }} size="small">
<div style={{ whiteSpace: 'pre-wrap' }}>{analysisContent}</div>
</Card>
)}
{summaryContent && (
<Card title="报告摘要" style={{ marginTop: 16 }} size="small">
<div style={{ whiteSpace: 'pre-wrap' }}>{summaryContent}</div>
</Card>
)}
<Modal
title={editingRecord ? '编辑化验报告' : '添加化验报告'}
open={modalOpen}
onCancel={() => { setModalOpen(false); setEditingRecord(null); }}
onOk={() => form.submit()}
confirmLoading={submitting}
destroyOnHidden
width={520}
>
<Form form={form} layout="vertical" onFinish={handleSubmit}>
<Form.Item name="report_date" label="报告日期" rules={[{ required: true, message: '请选择日期' }]}>
<DatePicker style={{ width: '100%' }} />
</Form.Item>
<Form.Item name="report_type" label="报告类型" rules={[{ required: true, message: '请选择类型' }]}>
<Input placeholder="如:血常规、生化全套" />
</Form.Item>
<Form.Item name="doctor_notes" label="医生解读">
<Input.TextArea rows={3} placeholder="检查结果解读备注" />
</Form.Item>
</Form>
</Modal>
<Modal
title="审核化验报告"
open={reviewOpen}
onCancel={() => { setReviewOpen(false); setReviewRecord(null); }}
onOk={handleReview}
confirmLoading={reviewSubmitting}
destroyOnHidden
width={480}
>
{reviewRecord && (
<div>
<p style={{ marginBottom: 8 }}>
<strong></strong>{reviewRecord.report_type}
</p>
<p style={{ marginBottom: 8 }}>
<strong></strong>{reviewRecord.report_date}
</p>
{reviewRecord.doctor_notes && (
<p style={{ marginBottom: 8 }}>
<strong></strong>{reviewRecord.doctor_notes}
</p>
)}
<div style={{ marginTop: 16 }}>
<label style={{ display: 'block', marginBottom: 4 }}></label>
<Input.TextArea
rows={3}
value={reviewNotes}
onChange={(e) => setReviewNotes(e.target.value)}
placeholder="填写审核意见(可选)"
/>
</div>
</div>
)}
</Modal>
</div>
);
}