feat(ai): Phase 2A-3 随访页 AI 辅助生成小结 — SSE 端点 + 前端集成

- AnalysisType 新增 FollowUpSummary 变体(as_str/prompt_name)
- HealthDataProvider 新增 get_follow_up_summary_data() + FollowUpSummaryDataDto
- erp-health 实现随访数据查询(task + records + PII 解密)
- 新增 /ai/analyze/follow-up-summary SSE 端点
- SanitizationService 新增 sanitize_follow_up_data()
- 前端 analysisSse.ts/AiAnalysisCard 支持 follow-up-summary 类型
- FollowUpTaskList 操作列新增「AI 小结」按钮
This commit is contained in:
iven
2026-05-19 00:54:15 +08:00
parent 205f6fb5a2
commit 2660f1afff
10 changed files with 223 additions and 13 deletions

View File

@@ -1,9 +1,10 @@
export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary';
export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary' | 'follow-up-summary';
interface AnalyzeBody {
report_id?: string;
patient_id?: string;
metrics?: string[];
source_id?: string;
}
const ENDPOINT_MAP: Record<AnalysisType, string> = {
@@ -11,6 +12,7 @@ const ENDPOINT_MAP: Record<AnalysisType, string> = {
'trends': '/ai/analyze/trends',
'checkup-plan': '/ai/analyze/checkup-plan',
'report-summary': '/ai/analyze/report-summary',
'follow-up-summary': '/ai/analyze/follow-up-summary',
};
export interface SseCallbacks {

View File

@@ -11,6 +11,7 @@ export interface AiAnalysisCardProps {
triggerLabel?: string;
permission?: string;
metrics?: string[];
taskId?: string;
}
type AnalysisState = 'idle' | 'loading' | 'success' | 'error';
@@ -21,6 +22,7 @@ export function AiAnalysisCard({
triggerLabel = 'AI 分析',
permission = 'ai.analysis.manage',
metrics,
taskId,
}: AiAnalysisCardProps) {
const [state, setState] = useState<AnalysisState>('idle');
const [content, setContent] = useState('');
@@ -38,6 +40,9 @@ export function AiAnalysisCard({
if (analysisType === 'trends' || analysisType === 'checkup-plan') {
body.patient_id = sourceRef;
}
if (analysisType === 'follow-up-summary') {
body.source_id = taskId || sourceRef;
}
if (metrics) {
body.metrics = metrics;
}
@@ -55,7 +60,7 @@ export function AiAnalysisCard({
setErrorMsg('分析请求失败');
setState('error');
}
}, [analysisType, sourceRef, metrics]);
}, [analysisType, sourceRef, metrics, taskId]);
const handleReset = useCallback(() => {
setState('idle');

View File

@@ -11,7 +11,7 @@ import {
Space,
Popconfirm,
} from 'antd';
import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined } from '@ant-design/icons';
import { PlusOutlined, EditOutlined, SwapOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
import type { ColumnsType, TablePaginationConfig } from 'antd/es/table';
import { dayjs } from '../../utils/dayjs';
import { followUpApi, type FollowUpTask, type CreateFollowUpTaskReq, type UpdateFollowUpTaskReq } from '../../api/health/followUp';
@@ -26,6 +26,7 @@ import { formatDate, formatDateTime } from '../../utils/format';
import { usePaginatedData } from '../../hooks/usePaginatedData';
import { useApiRequest } from '../../hooks/useApiRequest';
import { useDictionary } from '../../hooks/useDictionary';
import { AiAnalysisCard } from '../../components/ai/AiAnalysisCard';
const STATUS_OPTIONS = [
{ value: 'pending', label: '待处理' },
@@ -117,6 +118,9 @@ export default function FollowUpTaskList() {
const [assignForm] = Form.useForm<AssignFormValues>();
const [assignTask, setAssignTask] = useState<FollowUpTask | null>(null);
// AI summary state
const [aiSummaryTaskId, setAiSummaryTaskId] = useState<string | null>(null);
// --- Handlers ---
const handleTableChange = (pagination: TablePaginationConfig) => {
refresh(pagination.current ?? 1);
@@ -280,7 +284,7 @@ export default function FollowUpTaskList() {
{
title: '操作',
key: 'actions',
width: 220,
width: 280,
render: (_: unknown, record: FollowUpTask) => (
<AuthButton code="health.follow-up.manage">
<Space size={4}>
@@ -292,6 +296,14 @@ export default function FollowUpTaskList() {
>
</Button>
<Button
type="link"
size="small"
icon={<ThunderboltOutlined />}
onClick={() => setAiSummaryTaskId(aiSummaryTaskId === record.id ? null : record.id)}
>
AI
</Button>
<Button
type="link"
size="small"
@@ -394,6 +406,18 @@ export default function FollowUpTaskList() {
scroll={{ x: 980 }}
/>
{/* AI 随访小结 */}
{aiSummaryTaskId && (
<div style={{ marginTop: 16 }}>
<AiAnalysisCard
analysisType="follow-up-summary"
sourceRef={aiSummaryTaskId}
taskId={aiSummaryTaskId}
triggerLabel="AI 生成随访小结"
/>
</div>
)}
{/* Create Task Modal */}
<Modal
title="新建随访任务"