- 修复 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 测试报告和五专家组评审报告
294 lines
11 KiB
TypeScript
294 lines
11 KiB
TypeScript
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>
|
||
);
|
||
}
|