feat(web): AI 分析详情增加建议面板 — 风险等级+建议列表+审批操作
- 新增 suggestions API 层(list/approve/getComparison) - 展开分析详情时自动加载关联的 AI 建议列表 - 风险等级彩色标签(低/中/高) - 建议类型、原因、执行状态展示 - 待审批建议支持批准/拒绝操作
This commit is contained in:
34
apps/web/src/api/ai/suggestions.ts
Normal file
34
apps/web/src/api/ai/suggestions.ts
Normal 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;
|
||||||
|
},
|
||||||
|
};
|
||||||
@@ -1,10 +1,15 @@
|
|||||||
import { useEffect, useState, useCallback, useMemo } from 'react';
|
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 {
|
import {
|
||||||
RobotOutlined,
|
RobotOutlined,
|
||||||
|
CheckCircleOutlined,
|
||||||
|
CloseCircleOutlined,
|
||||||
|
ExclamationCircleOutlined,
|
||||||
|
WarningOutlined,
|
||||||
} from '@ant-design/icons';
|
} from '@ant-design/icons';
|
||||||
import { useThemeMode } from '../../hooks/useThemeMode';
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
||||||
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
|
import { analysisApi, type AnalysisItem } from '../../api/ai/analysis';
|
||||||
|
import { suggestionApi, type SuggestionItem } from '../../api/ai/suggestions';
|
||||||
|
|
||||||
const { Text } = Typography;
|
const { Text } = Typography;
|
||||||
|
|
||||||
@@ -27,6 +32,27 @@ const TYPE_OPTIONS = Object.entries(ANALYSIS_TYPE_MAP).map(([value, label]) => (
|
|||||||
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 风格)
|
// 分析结果渲染(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>
|
<Text type="secondary">暂无结果内容</Text>
|
||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
{/* AI 建议面板 */}
|
||||||
|
{detail.id && (
|
||||||
|
<SuggestionPanel analysisId={detail.id} isDark={isDark} />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
);
|
);
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
|
|||||||
Reference in New Issue
Block a user