feat(web): AI 分析详情增加建议面板 — 风险等级+建议列表+审批操作

- 新增 suggestions API 层(list/approve/getComparison)
- 展开分析详情时自动加载关联的 AI 建议列表
- 风险等级彩色标签(低/中/高)
- 建议类型、原因、执行状态展示
- 待审批建议支持批准/拒绝操作
This commit is contained in:
iven
2026-05-01 09:17:18 +08:00
parent 5d2402a1e7
commit 92c1c3c17d
2 changed files with 178 additions and 1 deletions

View File

@@ -0,0 +1,34 @@
import client from '../client';
export interface SuggestionItem {
id: string;
analysis_id: string;
suggestion_type: string;
risk_level: string;
params: Record<string, unknown> | null;
status: string;
created_at: string;
}
export interface ComparisonReport {
suggestion_id: string;
baseline: Record<string, unknown> | null;
current: Record<string, unknown> | 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;
},
};

View File

@@ -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<string, { color: string; text: string; icon: React.ReactNode }> = {
low: { color: 'green', text: '低风险', icon: <CheckCircleOutlined /> },
medium: { color: 'orange', text: '中风险', icon: <ExclamationCircleOutlined /> },
high: { color: 'red', text: '高风险', icon: <WarningOutlined /> },
};
const SUGGESTION_TYPE_MAP: Record<string, string> = {
followup: '随访建议',
appointment: '预约建议',
alert: '预警通知',
};
const SUGGESTION_STATUS_CONFIG: Record<string, { color: string; text: string }> = {
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<SuggestionItem[]>([]);
const [loading, setLoading] = useState(false);
const [actionLoading, setActionLoading] = useState<string | null>(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 <div style={{ padding: '8px 0' }}>...</div>;
if (suggestions.length === 0) return null;
return (
<div style={{
marginTop: 16,
padding: 12,
background: isDark ? '#0f172a' : '#f8fafc',
borderRadius: 8,
border: `1px solid ${isDark ? '#1e293b' : '#e2e8f0'}`,
}}>
<Text strong style={{ display: 'block', marginBottom: 8, fontSize: 13 }}>
AI ({suggestions.length})
</Text>
{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<string, unknown> | null;
const reason = params?.reason as string || params?.message as string || '';
return (
<div
key={s.id}
style={{
display: 'flex',
alignItems: 'center',
justifyContent: 'space-between',
padding: '8px 12px',
marginBottom: 6,
background: isDark ? '#111827' : '#fff',
borderRadius: 6,
border: `1px solid ${isDark ? '#1e293b' : '#f0f0f0'}`,
}}
>
<Space size={8}>
<Tag color={risk.color} style={{ margin: 0 }}>
{risk.icon} {risk.text}
</Tag>
<Tag style={{ margin: 0 }}>{typeLabel}</Tag>
{reason && <Text type="secondary" style={{ fontSize: 12, maxWidth: 300 }} ellipsis>{reason}</Text>}
<Tag color={status.color} style={{ margin: 0, fontSize: 11 }}>{status.text}</Tag>
</Space>
{isPending && (
<Space size={4}>
<Button
type="primary"
size="small"
icon={<CheckCircleOutlined />}
loading={actionLoading === s.id}
onClick={() => handleAction(s.id, 'approve')}
>
</Button>
<Button
danger
size="small"
icon={<CloseCircleOutlined />}
loading={actionLoading === s.id}
onClick={() => handleAction(s.id, 'reject')}
>
</Button>
</Space>
)}
</div>
);
})}
</div>
);
}
// ---------------------------------------------------------------------------
// 主组件
// ---------------------------------------------------------------------------
@@ -298,6 +435,12 @@ export default function AiAnalysisList() {
<Text type="secondary"></Text>
)}
</div>
{/* AI 建议面板 */}
{detail.id && (
<SuggestionPanel analysisId={detail.id} isDark={isDark} />
)}
</div>
);
},
}}