From 92c1c3c17dd9488a781905b8d3d58affca111653 Mon Sep 17 00:00:00 2001 From: iven Date: Fri, 1 May 2026 09:17:18 +0800 Subject: [PATCH] =?UTF-8?q?feat(web):=20AI=20=E5=88=86=E6=9E=90=E8=AF=A6?= =?UTF-8?q?=E6=83=85=E5=A2=9E=E5=8A=A0=E5=BB=BA=E8=AE=AE=E9=9D=A2=E6=9D=BF?= =?UTF-8?q?=20=E2=80=94=20=E9=A3=8E=E9=99=A9=E7=AD=89=E7=BA=A7+=E5=BB=BA?= =?UTF-8?q?=E8=AE=AE=E5=88=97=E8=A1=A8+=E5=AE=A1=E6=89=B9=E6=93=8D?= =?UTF-8?q?=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 新增 suggestions API 层(list/approve/getComparison) - 展开分析详情时自动加载关联的 AI 建议列表 - 风险等级彩色标签(低/中/高) - 建议类型、原因、执行状态展示 - 待审批建议支持批准/拒绝操作 --- apps/web/src/api/ai/suggestions.ts | 34 +++++ apps/web/src/pages/health/AiAnalysisList.tsx | 145 ++++++++++++++++++- 2 files changed, 178 insertions(+), 1 deletion(-) create mode 100644 apps/web/src/api/ai/suggestions.ts diff --git a/apps/web/src/api/ai/suggestions.ts b/apps/web/src/api/ai/suggestions.ts new file mode 100644 index 0000000..bbfaacd --- /dev/null +++ b/apps/web/src/api/ai/suggestions.ts @@ -0,0 +1,34 @@ +import client from '../client'; + +export interface SuggestionItem { + id: string; + analysis_id: string; + suggestion_type: string; + risk_level: string; + params: Record | null; + status: string; + created_at: string; +} + +export interface ComparisonReport { + suggestion_id: string; + baseline: Record | null; + current: Record | null; + comparison_available: boolean; + message?: string; +} + +export const suggestionApi = { + list: async (params?: { analysis_id?: string; status?: string }) => { + const resp = await client.get('/ai/suggestions', { params }); + return resp.data.data as { data: SuggestionItem[]; total: number }; + }, + approve: async (id: string, action: 'approve' | 'reject') => { + const resp = await client.post(`/ai/suggestions/${id}/approve`, { action }); + return resp.data.data as { id: string; status: string }; + }, + getComparison: async (id: string) => { + const resp = await client.get(`/ai/suggestions/${id}/comparison`); + return resp.data.data as ComparisonReport; + }, +}; diff --git a/apps/web/src/pages/health/AiAnalysisList.tsx b/apps/web/src/pages/health/AiAnalysisList.tsx index f5ecc99..08879ea 100644 --- a/apps/web/src/pages/health/AiAnalysisList.tsx +++ b/apps/web/src/pages/health/AiAnalysisList.tsx @@ -1,10 +1,15 @@ import { useEffect, useState, useCallback, useMemo } from 'react'; -import { Table, Select, Tag, Space, message, Typography } from 'antd'; +import { Table, Select, Tag, Space, Button, message, Typography } from 'antd'; import { RobotOutlined, + CheckCircleOutlined, + CloseCircleOutlined, + ExclamationCircleOutlined, + WarningOutlined, } from '@ant-design/icons'; import { useThemeMode } from '../../hooks/useThemeMode'; import { analysisApi, type AnalysisItem } from '../../api/ai/analysis'; +import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions'; const { Text } = Typography; @@ -27,6 +32,27 @@ const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => ( label, })); +const RISK_CONFIG: Record = { + low: { color: 'green', text: '低风险', icon: }, + medium: { color: 'orange', text: '中风险', icon: }, + high: { color: 'red', text: '高风险', icon: }, +}; + +const SUGGESTION_TYPE_MAP: Record = { + followup: '随访建议', + appointment: '预约建议', + alert: '预警通知', +}; + +const SUGGESTION_STATUS_CONFIG: Record = { + pending: { color: 'orange', text: '待审批' }, + approved: { color: 'green', text: '已批准' }, + rejected: { color: 'red', text: '已拒绝' }, + executed: { color: 'blue', text: '已执行' }, + expired: { color: 'default', text: '已过期' }, + parse_failed: { color: 'red', text: '解析失败' }, +}; + // --------------------------------------------------------------------------- // 分析结果渲染(Markdown 风格) // --------------------------------------------------------------------------- @@ -105,6 +131,117 @@ function renderInlineStyles(text: string) { }); } +// --------------------------------------------------------------------------- +// AI 建议面板 +// --------------------------------------------------------------------------- + +function SuggestionPanel({ analysisId, isDark }: { analysisId: string; isDark: boolean }) { + const [suggestions, setSuggestions] = useState([]); + const [loading, setLoading] = useState(false); + const [actionLoading, setActionLoading] = useState(null); + + const fetchSuggestions = useCallback(async () => { + setLoading(true); + try { + const result = await suggestionApi.list({ analysis_id: analysisId }); + setSuggestions(result.data || []); + } catch { + // 静默处理 + } finally { + setLoading(false); + } + }, [analysisId]); + + useEffect(() => { + if (analysisId) fetchSuggestions(); + }, [analysisId, fetchSuggestions]); + + const handleAction = async (id: string, action: 'approve' | 'reject') => { + setActionLoading(id); + try { + await suggestionApi.approve(id, action); + message.success(action === 'approve' ? '已批准' : '已拒绝'); + fetchSuggestions(); + } catch { + message.error('操作失败'); + } finally { + setActionLoading(null); + } + }; + + if (loading) return
加载建议中...
; + if (suggestions.length === 0) return null; + + return ( +
+ + AI 建议列表 ({suggestions.length}) + + {suggestions.map((s) => { + const risk = RISK_CONFIG[s.risk_level] || { color: 'default', text: s.risk_level, icon: null }; + const status = SUGGESTION_STATUS_CONFIG[s.status] || { color: 'default', text: s.status }; + const typeLabel = SUGGESTION_TYPE_MAP[s.suggestion_type] || s.suggestion_type; + const isPending = s.status === 'pending'; + const params = s.params as Record | null; + const reason = params?.reason as string || params?.message as string || ''; + + return ( +
+ + + {risk.icon} {risk.text} + + {typeLabel} + {reason && {reason}} + {status.text} + + {isPending && ( + + + + + )} +
+ ); + })} +
+ ); +} + // --------------------------------------------------------------------------- // 主组件 // --------------------------------------------------------------------------- @@ -298,6 +435,12 @@ export default function AiAnalysisList() { 暂无结果内容 )} + + {/* AI 建议面板 */} + {detail.id && ( + + )} + ); }, }}