Files
hms/apps/web/src/pages/health/AiUsageDashboard.tsx
iven 30a578ee00
Some checks failed
CI / rust-check (push) Has been cancelled
CI / rust-test (push) Has been cancelled
CI / frontend-build (push) Has been cancelled
CI / security-audit (push) Has been cancelled
fix(health): 客户试用前全局审计修复 — P0 权限旁路 + API 路径 + 事件注册
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 依赖
2026-05-04 11:02:25 +08:00

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>
);
}