P0 阻塞修复:
- 修复 PrivateRoute 权限旁路: p.startsWith('auth.') 匹配不到任何权限码,
改为基于实际权限码的路由级检查 (user.manage/role.manage/organization.manage)
- 修复 deviceReadings API 路径: /patients/{id}/device-readings/daily 改为
/vital-signs/daily?patient_id=, 消除 404
P1 重要修复:
- 补全事件注册表: 新增 auth(11) + config(8) + workflow(4) + plugin(2) = 25 条
- article_article_tag 联表新增 tenant_id + deleted_at + 审计列 (迁移 107)
- vital_signs_hourly 新增 deleted_at 支持软删除过滤 (迁移 108)
- 6 个页面添加权限守卫 (AlertDashboard/AlertRuleList/DeviceManage/
AiAnalysisList/AiUsageDashboard)
- DialysisModule 声明 auth 依赖
146 lines
4.6 KiB
TypeScript
146 lines
4.6 KiB
TypeScript
import { useEffect, useState } from 'react';
|
|
import { Card, Spin, Statistic, message, Empty, Result, Row, Col } from 'antd';
|
|
import {
|
|
ThunderboltOutlined,
|
|
ExperimentOutlined,
|
|
} from '@ant-design/icons';
|
|
import { useThemeMode } from '../../hooks/useThemeMode';
|
|
import { usePermission } from '../../hooks/usePermission';
|
|
import { usageApi, type UsageOverview, type TypeDistribution } from '../../api/ai/usage';
|
|
|
|
const ANALYSIS_TYPE_MAP: Record<string, string> = {
|
|
lab_report_interpretation: '化验单解读',
|
|
health_trend_analysis: '趋势分析',
|
|
personalized_checkup_plan: '体检方案',
|
|
report_summary_generation: '报告摘要',
|
|
};
|
|
|
|
const TYPE_COLORS: Record<string, string> = {
|
|
lab_report_interpretation: '#1890ff',
|
|
health_trend_analysis: '#52c41a',
|
|
personalized_checkup_plan: '#722ed1',
|
|
report_summary_generation: '#fa8c16',
|
|
};
|
|
|
|
export default function AiUsageDashboard() {
|
|
const { hasPermission } = usePermission('ai.usage.list');
|
|
if (!hasPermission) return <Result status="403" title="权限不足" subTitle="您没有查看 AI 用量的权限" />;
|
|
const [overview, setOverview] = useState<UsageOverview | null>(null);
|
|
const [types, setTypes] = useState<TypeDistribution[]>([]);
|
|
const [loading, setLoading] = useState(true);
|
|
const isDark = useThemeMode();
|
|
|
|
useEffect(() => {
|
|
const fetchData = async () => {
|
|
setLoading(true);
|
|
try {
|
|
const [ov, tp] = await Promise.all([
|
|
usageApi.overview(),
|
|
usageApi.byType(),
|
|
]);
|
|
setOverview(ov);
|
|
setTypes(tp);
|
|
} catch {
|
|
message.error('加载用量统计失败');
|
|
} finally {
|
|
setLoading(false);
|
|
}
|
|
};
|
|
fetchData();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div style={{ display: 'flex', justifyContent: 'center', padding: 80 }}>
|
|
<Spin size="large" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
const cardStyle = {
|
|
borderRadius: 12,
|
|
background: isDark ? '#111827' : '#FFFFFF',
|
|
border: `1px solid ${isDark ? '#0f172a' : '#f8fafc'}`,
|
|
};
|
|
|
|
const totalCount = types.reduce((sum, t) => sum + t.count, 0);
|
|
|
|
return (
|
|
<div>
|
|
<div className="erp-page-header">
|
|
<div>
|
|
<h4>AI 用量统计</h4>
|
|
<div className="erp-page-subtitle">查看 AI 分析服务的使用情况</div>
|
|
</div>
|
|
</div>
|
|
|
|
<Row gutter={16} style={{ marginBottom: 16 }}>
|
|
<Col span={8}>
|
|
<Card style={cardStyle}>
|
|
<Statistic
|
|
title="总分析次数"
|
|
value={overview?.total_count ?? 0}
|
|
prefix={<ThunderboltOutlined style={{ color: '#1890ff' }} />}
|
|
valueStyle={{ fontWeight: 600 }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Card style={cardStyle}>
|
|
<Statistic
|
|
title="分析类型数"
|
|
value={types.length}
|
|
prefix={<ExperimentOutlined style={{ color: '#52c41a' }} />}
|
|
valueStyle={{ fontWeight: 600 }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
<Col span={8}>
|
|
<Card style={cardStyle}>
|
|
<Statistic
|
|
title="本月分析"
|
|
value={totalCount}
|
|
prefix={<ThunderboltOutlined style={{ color: '#fa8c16' }} />}
|
|
valueStyle={{ fontWeight: 600 }}
|
|
/>
|
|
</Card>
|
|
</Col>
|
|
</Row>
|
|
|
|
<Card style={cardStyle} title="分析类型分布">
|
|
{types.length === 0 ? (
|
|
<Empty description="暂无分析数据" />
|
|
) : (
|
|
<Row gutter={[16, 16]}>
|
|
{types.map((t) => {
|
|
const pct = totalCount > 0 ? Math.round((t.count / totalCount) * 100) : 0;
|
|
const label = ANALYSIS_TYPE_MAP[t.analysis_type] || t.analysis_type;
|
|
const color = TYPE_COLORS[t.analysis_type] || '#1890ff';
|
|
return (
|
|
<Col span={6} key={t.analysis_type}>
|
|
<div
|
|
style={{
|
|
padding: 16,
|
|
borderRadius: 8,
|
|
background: isDark ? '#0f172a' : '#f8fafc',
|
|
textAlign: 'center',
|
|
}}
|
|
>
|
|
<div style={{ fontSize: 28, fontWeight: 700, color }}>{t.count}</div>
|
|
<div style={{ fontSize: 13, color: isDark ? '#94a3b8' : '#475569', marginTop: 4 }}>
|
|
{label}
|
|
</div>
|
|
<div style={{ fontSize: 12, color: isDark ? '#475569' : '#94a3b8', marginTop: 4 }}>
|
|
{pct}%
|
|
</div>
|
|
</div>
|
|
</Col>
|
|
);
|
|
})}
|
|
</Row>
|
|
)}
|
|
</Card>
|
|
</div>
|
|
);
|
|
}
|