Files
ems-core/frontend/src/pages/AIOperations/index.tsx
Du Wenbo 92ec910a13 ems-core v1.0.0: Standard EMS platform core
Shared backend + frontend for multi-customer EMS deployments.
- 12 enterprise modules: quota, cost, charging, maintenance, analysis, etc.
- 120+ API endpoints, 37 database tables
- Customer config mechanism (CUSTOMER env var + YAML config)
- Collectors: Modbus TCP, MQTT, HTTP API, Sungrow iSolarCloud
- Frontend: React 19 + Ant Design + ECharts + Three.js
- Infrastructure: Redis cache, rate limiting, aggregation engine

Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
2026-04-04 18:14:11 +08:00

860 lines
32 KiB
TypeScript

import { useEffect, useState } from 'react';
import {
Card, Row, Col, Statistic, Tag, Tabs, Button, Table, Space, Progress,
Drawer, Descriptions, Timeline, Badge, Select, message, Tooltip, Empty,
Modal, List, Calendar, Input,
} from 'antd';
import {
RobotOutlined, HeartOutlined, AlertOutlined, MedicineBoxOutlined,
ToolOutlined, BulbOutlined, SyncOutlined, ArrowUpOutlined,
ArrowDownOutlined, MinusOutlined, ThunderboltOutlined,
ExperimentOutlined, SafetyCertificateOutlined, EyeOutlined,
CheckCircleOutlined, CloseCircleOutlined, WarningOutlined,
InfoCircleOutlined, FireOutlined,
} from '@ant-design/icons';
import ReactECharts from 'echarts-for-react';
import dayjs from 'dayjs';
import {
getAiOpsDashboard, getAiOpsHealth, getAiOpsHealthHistory,
getAiOpsAnomalies, updateAnomalyStatus, triggerAnomalyScan,
getAiOpsDiagnostics, runDeviceDiagnostics,
getAiOpsPredictions, getAiOpsMaintenanceSchedule,
getAiOpsInsights, triggerInsights, triggerHealthCalc, triggerPredictions,
} from '../../services/api';
const severityColors: Record<string, string> = {
critical: 'red', warning: 'orange', info: 'blue',
};
const statusColors: Record<string, string> = {
healthy: 'green', warning: 'orange', critical: 'red',
detected: 'red', investigating: 'orange', resolved: 'green', false_positive: 'default',
generated: 'blue', reviewed: 'cyan', action_taken: 'green',
predicted: 'orange', scheduled: 'blue', completed: 'green', false_alarm: 'default',
};
const trendIcons: Record<string, React.ReactNode> = {
improving: <ArrowUpOutlined style={{ color: '#52c41a' }} />,
stable: <MinusOutlined style={{ color: '#1890ff' }} />,
degrading: <ArrowDownOutlined style={{ color: '#f5222d' }} />,
};
const anomalyTypeLabels: Record<string, string> = {
power_drop: '功率下降', efficiency_loss: '能效降低', abnormal_temperature: '温度异常',
communication_loss: '通讯中断', pattern_deviation: '模式偏移',
};
const urgencyColors: Record<string, string> = {
critical: 'red', high: 'orange', medium: 'blue', low: 'default',
};
const impactColors: Record<string, string> = {
high: 'red', medium: 'orange', low: 'blue',
};
const insightTypeLabels: Record<string, string> = {
efficiency_trend: '效率趋势', cost_anomaly: '费用异常',
performance_comparison: '性能对比', seasonal_pattern: '季节性规律',
};
// ── Tab: Health Overview ───────────────────────────────────────────
function HealthOverview() {
const [devices, setDevices] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [detailDevice, setDetailDevice] = useState<any>(null);
const [history, setHistory] = useState<any[]>([]);
const [historyLoading, setHistoryLoading] = useState(false);
useEffect(() => { loadHealth(); }, []);
const loadHealth = async () => {
setLoading(true);
try {
const data = await getAiOpsHealth();
setDevices(Array.isArray(data) ? data : []);
} catch { message.error('加载健康数据失败'); }
finally { setLoading(false); }
};
const loadHistory = async (deviceId: number) => {
setHistoryLoading(true);
try {
const data = await getAiOpsHealthHistory(deviceId, { days: 30 });
setHistory(Array.isArray(data) ? data : []);
} catch { /* ignore */ }
finally { setHistoryLoading(false); }
};
const showDetail = (device: any) => {
setDetailDevice(device);
loadHistory(device.device_id);
};
const handleRecalculate = async () => {
message.loading({ content: '正在计算健康评分...', key: 'calc' });
try {
await triggerHealthCalc();
message.success({ content: '健康评分计算完成', key: 'calc' });
loadHealth();
} catch { message.error({ content: '计算失败', key: 'calc' }); }
};
const getScoreColor = (score: number) => {
if (score >= 80) return '#52c41a';
if (score >= 60) return '#faad14';
return '#f5222d';
};
const gaugeOption = (score: number) => ({
series: [{
type: 'gauge',
startAngle: 200,
endAngle: -20,
min: 0,
max: 100,
itemStyle: { color: getScoreColor(score) },
progress: { show: true, width: 12 },
pointer: { show: false },
axisLine: { lineStyle: { width: 12, color: [[1, '#e6e6e6']] } },
axisTick: { show: false },
splitLine: { show: false },
axisLabel: { show: false },
detail: {
valueAnimation: true,
fontSize: 24,
fontWeight: 'bold',
offsetCenter: [0, '0%'],
formatter: '{value}',
color: getScoreColor(score),
},
data: [{ value: score }],
}],
});
const historyOption = history.length > 0 ? {
tooltip: { trigger: 'axis' },
xAxis: {
type: 'category',
data: history.map((h: any) => dayjs(h.timestamp).format('MM-DD HH:mm')),
},
yAxis: { type: 'value', min: 0, max: 100, name: '健康评分' },
series: [{
type: 'line',
data: history.map((h: any) => h.health_score),
smooth: true,
areaStyle: { opacity: 0.15 },
markLine: {
data: [
{ yAxis: 80, label: { formatter: '健康' }, lineStyle: { color: '#52c41a', type: 'dashed' } },
{ yAxis: 60, label: { formatter: '警告' }, lineStyle: { color: '#faad14', type: 'dashed' } },
],
},
}],
grid: { top: 30, right: 30, bottom: 30, left: 50 },
} : null;
const radarOption = detailDevice?.factors ? {
radar: {
indicator: [
{ name: '功率稳定', max: 100 },
{ name: '能效水平', max: 100 },
{ name: '告警频率', max: 100 },
{ name: '运行时间', max: 100 },
{ name: '温度状态', max: 100 },
],
},
series: [{
type: 'radar',
data: [{
value: [
detailDevice.factors.power_stability || 0,
detailDevice.factors.efficiency || 0,
detailDevice.factors.alarm_frequency || 0,
detailDevice.factors.uptime || 0,
detailDevice.factors.temperature || 0,
],
areaStyle: { opacity: 0.2 },
}],
}],
} : null;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 500 }}>
<HeartOutlined style={{ marginRight: 8 }} />
</span>
<Button icon={<SyncOutlined />} onClick={handleRecalculate}></Button>
</div>
<Row gutter={[16, 16]}>
{loading ? (
<Col span={24}><Card loading /></Col>
) : devices.length === 0 ? (
<Col span={24}><Card><Empty description="暂无健康评分数据,请点击「重新计算」" /></Card></Col>
) : devices.map((d: any) => (
<Col xs={24} sm={12} md={8} lg={6} key={d.device_id}>
<Card
hoverable
onClick={() => showDetail(d)}
styles={{ body: { padding: 16, textAlign: 'center' } }}
>
<div style={{ fontSize: 14, fontWeight: 500, marginBottom: 4 }}>{d.device_name}</div>
<Tag color="blue" style={{ marginBottom: 8 }}>{d.device_type}</Tag>
<div style={{ height: 130 }}>
<ReactECharts option={gaugeOption(d.health_score)} style={{ height: 130 }} />
</div>
<Space>
<Tag color={statusColors[d.status]}>{d.status === 'healthy' ? '健康' : d.status === 'warning' ? '警告' : '危险'}</Tag>
<span>{trendIcons[d.trend]} {d.trend === 'improving' ? '改善' : d.trend === 'degrading' ? '下降' : '稳定'}</span>
</Space>
</Card>
</Col>
))}
</Row>
<Drawer
title={detailDevice ? `${detailDevice.device_name} - 健康详情` : ''}
open={!!detailDevice}
onClose={() => setDetailDevice(null)}
width={720}
>
{detailDevice && (
<>
<Descriptions column={2} bordered size="small" style={{ marginBottom: 24 }}>
<Descriptions.Item label="设备">{detailDevice.device_name}</Descriptions.Item>
<Descriptions.Item label="类型">{detailDevice.device_type}</Descriptions.Item>
<Descriptions.Item label="健康评分">
<span style={{ fontSize: 24, fontWeight: 'bold', color: getScoreColor(detailDevice.health_score) }}>
{detailDevice.health_score}
</span>
</Descriptions.Item>
<Descriptions.Item label="状态">
<Tag color={statusColors[detailDevice.status]}>
{detailDevice.status === 'healthy' ? '健康' : detailDevice.status === 'warning' ? '警告' : '危险'}
</Tag>
</Descriptions.Item>
<Descriptions.Item label="趋势" span={2}>
{trendIcons[detailDevice.trend]} {detailDevice.trend === 'improving' ? '持续改善' : detailDevice.trend === 'degrading' ? '持续下降' : '保持稳定'}
</Descriptions.Item>
</Descriptions>
<Row gutter={16}>
<Col span={12}>
<Card title="因素分析" size="small">
{radarOption && <ReactECharts option={radarOption} style={{ height: 250 }} />}
</Card>
</Col>
<Col span={12}>
<Card title="评分历史" size="small" loading={historyLoading}>
{historyOption ? (
<ReactECharts option={historyOption} style={{ height: 250 }} />
) : (
<Empty description="暂无历史数据" />
)}
</Card>
</Col>
</Row>
</>
)}
</Drawer>
</div>
);
}
// ── Tab: Anomaly Center ────────────────────────────────────────────
function AnomalyCenter() {
const [anomalies, setAnomalies] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [filters, setFilters] = useState<Record<string, any>>({});
const [page, setPage] = useState(1);
useEffect(() => { loadAnomalies(); }, [page, filters]);
const loadAnomalies = async () => {
setLoading(true);
try {
const data = await getAiOpsAnomalies({ ...filters, page, page_size: 15 });
setAnomalies(data || { total: 0, items: [] });
} catch { message.error('加载异常数据失败'); }
finally { setLoading(false); }
};
const handleScan = async () => {
message.loading({ content: '正在扫描异常...', key: 'scan' });
try {
const result = await triggerAnomalyScan() as any;
message.success({ content: `扫描完成,发现 ${result?.anomalies_found || 0} 个异常`, key: 'scan' });
loadAnomalies();
} catch { message.error({ content: '扫描失败', key: 'scan' }); }
};
const handleStatusUpdate = async (id: number, status: string) => {
try {
await updateAnomalyStatus(id, { status });
message.success('状态已更新');
loadAnomalies();
} catch { message.error('更新失败'); }
};
const columns = [
{
title: '时间', dataIndex: 'detected_at', width: 160,
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
},
{ title: '设备', dataIndex: 'device_name', width: 120 },
{
title: '异常类型', dataIndex: 'anomaly_type', width: 100,
render: (v: string) => anomalyTypeLabels[v] || v,
},
{
title: '严重度', dataIndex: 'severity', width: 80,
render: (v: string) => <Tag color={severityColors[v]}>{v === 'critical' ? '严重' : v === 'warning' ? '警告' : '信息'}</Tag>,
},
{ title: '描述', dataIndex: 'description', ellipsis: true },
{
title: '偏差', dataIndex: 'deviation_percent', width: 80,
render: (v: number) => v != null ? `${v}%` : '-',
},
{
title: '状态', dataIndex: 'status', width: 100,
render: (v: string) => <Tag color={statusColors[v]}>{v === 'detected' ? '已检测' : v === 'investigating' ? '调查中' : v === 'resolved' ? '已解决' : '误报'}</Tag>,
},
{
title: '操作', width: 200,
render: (_: any, r: any) => r.status === 'detected' ? (
<Space size="small">
<Button size="small" onClick={() => handleStatusUpdate(r.id, 'investigating')}></Button>
<Button size="small" onClick={() => handleStatusUpdate(r.id, 'resolved')}></Button>
<Button size="small" onClick={() => handleStatusUpdate(r.id, 'false_positive')}></Button>
</Space>
) : r.status === 'investigating' ? (
<Space size="small">
<Button size="small" type="primary" onClick={() => handleStatusUpdate(r.id, 'resolved')}></Button>
<Button size="small" onClick={() => handleStatusUpdate(r.id, 'false_positive')}></Button>
</Space>
) : null,
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between', flexWrap: 'wrap', gap: 8 }}>
<Space wrap>
<Select
placeholder="严重度" allowClear style={{ width: 120 }}
onChange={(v) => setFilters((f) => ({ ...f, severity: v }))}
options={[
{ label: '严重', value: 'critical' },
{ label: '警告', value: 'warning' },
{ label: '信息', value: 'info' },
]}
/>
<Select
placeholder="状态" allowClear style={{ width: 120 }}
onChange={(v) => setFilters((f) => ({ ...f, status: v }))}
options={[
{ label: '已检测', value: 'detected' },
{ label: '调查中', value: 'investigating' },
{ label: '已解决', value: 'resolved' },
{ label: '误报', value: 'false_positive' },
]}
/>
</Space>
<Button type="primary" icon={<ExperimentOutlined />} onClick={handleScan}></Button>
</div>
<Table
columns={columns}
dataSource={anomalies.items}
rowKey="id"
loading={loading}
pagination={{
current: page,
pageSize: 15,
total: anomalies.total,
onChange: setPage,
showTotal: (t) => `${t}`,
}}
size="small"
/>
</div>
);
}
// ── Tab: Diagnostic Panel ──────────────────────────────────────────
function DiagnosticPanel() {
const [reports, setReports] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
const [detailReport, setDetailReport] = useState<any>(null);
const [page, setPage] = useState(1);
const [devices, setDevices] = useState<any[]>([]);
useEffect(() => { loadReports(); loadDeviceList(); }, [page]);
const loadReports = async () => {
setLoading(true);
try {
const data = await getAiOpsDiagnostics({ page, page_size: 15 });
setReports(data || { total: 0, items: [] });
} catch { message.error('加载诊断报告失败'); }
finally { setLoading(false); }
};
const loadDeviceList = async () => {
try {
const data = await getAiOpsHealth();
setDevices(Array.isArray(data) ? data : []);
} catch { /* ignore */ }
};
const handleRun = async (deviceId: number) => {
message.loading({ content: '正在运行诊断...', key: 'diag' });
try {
const result = await runDeviceDiagnostics(deviceId);
message.success({ content: '诊断完成', key: 'diag' });
setDetailReport(result);
loadReports();
} catch { message.error({ content: '诊断失败', key: 'diag' }); }
};
const columns = [
{
title: '时间', dataIndex: 'generated_at', width: 160,
render: (v: string) => dayjs(v).format('YYYY-MM-DD HH:mm'),
},
{ title: '设备', dataIndex: 'device_name', width: 120 },
{
title: '类型', dataIndex: 'report_type', width: 80,
render: (v: string) => v === 'routine' ? '常规' : v === 'triggered' ? '触发' : '综合',
},
{
title: '发现', dataIndex: 'findings', ellipsis: true,
render: (v: any[]) => v?.length ? v.map((f: any) => f.finding).join('; ') : '-',
},
{
title: '影响评估', dataIndex: 'estimated_impact',
render: (v: any) => v ? (
<span>
{v.energy_loss_kwh > 0 && <Tag> {v.energy_loss_kwh} kWh</Tag>}
{v.cost_impact_yuan > 0 && <Tag color="orange"> {v.cost_impact_yuan} </Tag>}
{v.energy_loss_kwh === 0 && v.cost_impact_yuan === 0 && <Tag color="green"></Tag>}
</span>
) : '-',
},
{
title: '状态', dataIndex: 'status', width: 80,
render: (v: string) => <Tag color={statusColors[v]}>{v === 'generated' ? '已生成' : v === 'reviewed' ? '已审阅' : '已处理'}</Tag>,
},
{
title: '操作', width: 80,
render: (_: any, r: any) => (
<Button size="small" icon={<EyeOutlined />} onClick={() => setDetailReport(r)}></Button>
),
},
];
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 500 }}>
<MedicineBoxOutlined style={{ marginRight: 8 }} />
</span>
<Select
placeholder="选择设备运行诊断" style={{ width: 300 }}
showSearch optionFilterProp="label"
onChange={(v) => handleRun(v)}
options={devices.map((d: any) => ({ label: `${d.device_name} (${d.device_type})`, value: d.device_id }))}
/>
</div>
<Table
columns={columns}
dataSource={reports.items}
rowKey="id"
loading={loading}
pagination={{ current: page, pageSize: 15, total: reports.total, onChange: setPage, showTotal: (t) => `${t}` }}
size="small"
/>
<Drawer
title="诊断报告详情"
open={!!detailReport}
onClose={() => setDetailReport(null)}
width={640}
>
{detailReport && (
<>
<Descriptions column={2} bordered size="small" style={{ marginBottom: 16 }}>
<Descriptions.Item label="设备">{detailReport.device_name}</Descriptions.Item>
<Descriptions.Item label="类型">{detailReport.report_type}</Descriptions.Item>
<Descriptions.Item label="时间" span={2}>{dayjs(detailReport.generated_at).format('YYYY-MM-DD HH:mm')}</Descriptions.Item>
</Descriptions>
<Card title="诊断发现" size="small" style={{ marginBottom: 16 }}>
<Timeline
items={(detailReport.findings || []).map((f: any) => ({
color: f.severity === 'warning' ? 'orange' : f.severity === 'critical' ? 'red' : 'blue',
children: (
<div>
<div style={{ fontWeight: 500 }}>{f.finding}</div>
<div style={{ color: '#888', fontSize: 12 }}>{f.detail}</div>
</div>
),
}))}
/>
</Card>
{detailReport.recommendations?.length > 0 && (
<Card title="建议措施" size="small" style={{ marginBottom: 16 }}>
<List
size="small"
dataSource={detailReport.recommendations}
renderItem={(r: any) => (
<List.Item>
<List.Item.Meta
avatar={<Tag color={r.priority === 'high' ? 'red' : r.priority === 'medium' ? 'orange' : 'blue'}>{r.priority === 'high' ? '高' : r.priority === 'medium' ? '中' : '低'}</Tag>}
title={r.action}
description={r.detail}
/>
</List.Item>
)}
/>
</Card>
)}
{detailReport.estimated_impact && (
<Card title="影响评估" size="small">
<Row gutter={16}>
<Col span={12}>
<Statistic title="预计电量损失" value={detailReport.estimated_impact.energy_loss_kwh} suffix="kWh" />
</Col>
<Col span={12}>
<Statistic title="预计费用影响" value={detailReport.estimated_impact.cost_impact_yuan} suffix="元" prefix="&yen;" />
</Col>
</Row>
</Card>
)}
</>
)}
</Drawer>
</div>
);
}
// ── Tab: Maintenance Predictor ─────────────────────────────────────
function MaintenancePredictor() {
const [predictions, setPredictions] = useState<any>({ total: 0, items: [] });
const [schedule, setSchedule] = useState<any[]>([]);
const [loading, setLoading] = useState(true);
const [page, setPage] = useState(1);
useEffect(() => { loadData(); }, [page]);
const loadData = async () => {
setLoading(true);
try {
const [pred, sched] = await Promise.all([
getAiOpsPredictions({ page, page_size: 15 }),
getAiOpsMaintenanceSchedule(),
]);
setPredictions(pred || { total: 0, items: [] });
setSchedule(Array.isArray(sched) ? sched : []);
} catch { message.error('加载预测数据失败'); }
finally { setLoading(false); }
};
const handleGenerate = async () => {
message.loading({ content: '正在生成维护预测...', key: 'pred' });
try {
const result = await triggerPredictions() as any;
message.success({ content: `生成 ${result?.generated || 0} 条预测`, key: 'pred' });
loadData();
} catch { message.error({ content: '生成失败', key: 'pred' }); }
};
const columns = [
{
title: '设备', dataIndex: 'device_name', width: 120,
},
{ title: '部件', dataIndex: 'component', width: 120 },
{ title: '故障模式', dataIndex: 'failure_mode', ellipsis: true },
{
title: '概率', dataIndex: 'probability', width: 80,
render: (v: number) => <Progress percent={Math.round(v * 100)} size="small" />,
},
{
title: '预计故障日期', dataIndex: 'predicted_failure_date', width: 120,
render: (v: string) => v ? dayjs(v).format('YYYY-MM-DD') : '-',
},
{
title: '紧急度', dataIndex: 'urgency', width: 80,
render: (v: string) => <Tag color={urgencyColors[v]}>{v === 'critical' ? '紧急' : v === 'high' ? '高' : v === 'medium' ? '中' : '低'}</Tag>,
},
{
title: '停机(h)', dataIndex: 'estimated_downtime_hours', width: 80,
},
{
title: '维修费(元)', dataIndex: 'estimated_repair_cost', width: 100,
render: (v: number) => v ? `${v.toLocaleString()}` : '-',
},
{
title: '状态', dataIndex: 'status', width: 80,
render: (v: string) => <Tag color={statusColors[v]}>{v === 'predicted' ? '预测' : v === 'scheduled' ? '已排期' : v === 'completed' ? '完成' : '误报'}</Tag>,
},
];
const calendarSchedule = schedule.reduce((acc: any, item: any) => {
if (item.predicted_failure_date) {
const key = dayjs(item.predicted_failure_date).format('YYYY-MM-DD');
if (!acc[key]) acc[key] = [];
acc[key].push(item);
}
return acc;
}, {} as Record<string, any[]>);
const dateCellRender = (value: dayjs.Dayjs) => {
const key = value.format('YYYY-MM-DD');
const items = calendarSchedule[key];
if (!items) return null;
return (
<ul style={{ listStyle: 'none', padding: 0, margin: 0 }}>
{items.slice(0, 2).map((item: any, i: number) => (
<li key={i}>
<Badge color={urgencyColors[item.urgency] || 'blue'} text={<span style={{ fontSize: 11 }}>{item.device_name}</span>} />
</li>
))}
{items.length > 2 && <li style={{ fontSize: 11, color: '#999' }}>+{items.length - 2} more</li>}
</ul>
);
};
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 500 }}>
<ToolOutlined style={{ marginRight: 8 }} />
</span>
<Button type="primary" icon={<ExperimentOutlined />} onClick={handleGenerate}></Button>
</div>
<Tabs
items={[
{
key: 'list',
label: '预测列表',
children: (
<Table
columns={columns}
dataSource={predictions.items}
rowKey="id"
loading={loading}
pagination={{ current: page, pageSize: 15, total: predictions.total, onChange: setPage, showTotal: (t) => `${t}` }}
size="small"
/>
),
},
{
key: 'calendar',
label: '维护日历',
children: (
<Card>
<Calendar cellRender={(value, info) => {
if (info.type === 'date') return dateCellRender(value);
return null;
}} />
</Card>
),
},
]}
/>
</div>
);
}
// ── Tab: Insights Board ────────────────────────────────────────────
function InsightsBoard() {
const [insights, setInsights] = useState<any>({ total: 0, items: [] });
const [loading, setLoading] = useState(true);
useEffect(() => { loadInsights(); }, []);
const loadInsights = async () => {
setLoading(true);
try {
const data = await getAiOpsInsights({ page_size: 50 });
setInsights(data || { total: 0, items: [] });
} catch { message.error('加载洞察数据失败'); }
finally { setLoading(false); }
};
const handleGenerate = async () => {
message.loading({ content: '正在生成运营洞察...', key: 'ins' });
try {
await triggerInsights();
message.success({ content: '洞察生成完成', key: 'ins' });
loadInsights();
} catch { message.error({ content: '生成失败', key: 'ins' }); }
};
const typeIcons: Record<string, React.ReactNode> = {
efficiency_trend: <ThunderboltOutlined />,
cost_anomaly: <FireOutlined />,
performance_comparison: <BarChartOutlined />,
seasonal_pattern: <SafetyCertificateOutlined />,
};
const BarChartOutlined = () => <span>{"#"}</span>;
return (
<div>
<div style={{ marginBottom: 16, display: 'flex', justifyContent: 'space-between' }}>
<span style={{ fontSize: 16, fontWeight: 500 }}>
<BulbOutlined style={{ marginRight: 8 }} />
</span>
<Button type="primary" icon={<BulbOutlined />} onClick={handleGenerate}></Button>
</div>
{loading ? <Card loading /> : insights.items?.length === 0 ? (
<Card><Empty description="暂无运营洞察,请点击「生成洞察」" /></Card>
) : (
<Row gutter={[16, 16]}>
{insights.items?.map((insight: any) => (
<Col xs={24} sm={12} lg={8} key={insight.id}>
<Card
hoverable
styles={{ body: { padding: 16 } }}
>
<div style={{ display: 'flex', justifyContent: 'space-between', marginBottom: 8 }}>
<Tag color={impactColors[insight.impact_level]}>
{insight.impact_level === 'high' ? '高影响' : insight.impact_level === 'medium' ? '中影响' : '低影响'}
</Tag>
<Tag>{insightTypeLabels[insight.insight_type] || insight.insight_type}</Tag>
</div>
<div style={{ fontSize: 15, fontWeight: 500, marginBottom: 8 }}>{insight.title}</div>
<div style={{ color: '#666', fontSize: 13, marginBottom: 12, minHeight: 40 }}>{insight.description}</div>
{insight.actionable && insight.recommended_action && (
<div style={{
background: '#f6ffed', border: '1px solid #b7eb8f', borderRadius: 4,
padding: '8px 12px', fontSize: 12,
}}>
<CheckCircleOutlined style={{ color: '#52c41a', marginRight: 4 }} />
{insight.recommended_action}
</div>
)}
<div style={{ marginTop: 8, fontSize: 11, color: '#999' }}>
{dayjs(insight.generated_at).format('YYYY-MM-DD HH:mm')}
{insight.valid_until && ` | 有效至 ${dayjs(insight.valid_until).format('MM-DD')}`}
</div>
</Card>
</Col>
))}
</Row>
)}
</div>
);
}
// ── Main Page ──────────────────────────────────────────────────────
export default function AIOperations() {
const [dashboard, setDashboard] = useState<any>(null);
const [loading, setLoading] = useState(true);
useEffect(() => { loadDashboard(); }, []);
const loadDashboard = async () => {
setLoading(true);
try {
const data = await getAiOpsDashboard();
setDashboard(data);
} catch { /* initial load may fail if no data */ }
finally { setLoading(false); }
};
const health = dashboard?.health || {};
const anomalyStats = dashboard?.anomalies?.stats || {};
return (
<div>
{/* Overview cards */}
<Row gutter={[16, 16]} style={{ marginBottom: 16 }}>
<Col xs={12} sm={6}>
<Card styles={{ body: { padding: 16 } }}>
<Statistic
title="平均健康评分"
value={health.avg_score || '--'}
suffix="/100"
prefix={<HeartOutlined style={{ color: '#52c41a' }} />}
loading={loading}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card styles={{ body: { padding: 16 } }}>
<Statistic
title="健康/警告/危险"
value={`${health.healthy || 0}/${health.warning || 0}/${health.critical || 0}`}
prefix={<SafetyCertificateOutlined style={{ color: '#1890ff' }} />}
loading={loading}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card styles={{ body: { padding: 16 } }}>
<Statistic
title="近7天异常"
value={anomalyStats.total || 0}
prefix={<AlertOutlined style={{ color: '#faad14' }} />}
loading={loading}
/>
</Card>
</Col>
<Col xs={12} sm={6}>
<Card styles={{ body: { padding: 16 } }}>
<Statistic
title="待处理预测"
value={dashboard?.predictions?.length || 0}
prefix={<ToolOutlined style={{ color: '#722ed1' }} />}
loading={loading}
/>
</Card>
</Col>
</Row>
{/* Tabs */}
<Card>
<Tabs
defaultActiveKey="health"
items={[
{
key: 'health',
label: <span><HeartOutlined /> </span>,
children: <HealthOverview />,
},
{
key: 'anomalies',
label: <span><AlertOutlined /> </span>,
children: <AnomalyCenter />,
},
{
key: 'diagnostics',
label: <span><MedicineBoxOutlined /> </span>,
children: <DiagnosticPanel />,
},
{
key: 'maintenance',
label: <span><ToolOutlined /> </span>,
children: <MaintenancePredictor />,
},
{
key: 'insights',
label: <span><BulbOutlined /> </span>,
children: <InsightsBoard />,
},
]}
/>
</Card>
</div>
);
}