feat(ai): AI 健康管家 V2 基础设施 — 功能开关 + 角色沙箱准备 + 体征页 AI 趋势分析

- 迁移 000153: 新增 ai_feature_flags / ai_usage_daily / ai_suggestion_feedback 三张表,
  ai_tenant_configs 增加 billing_enabled 列, seed 12 个功能开关 + 2 个管理权限码
- 新增 FeatureFlagService: 5 分钟缓存 + DB 回退 + 即时更新
- VitalSignsTab 添加 AI 趋势分析按钮 (SSE 流式)
- 新增 3 个 Entity (ai_feature_flags / ai_usage_daily / ai_suggestion_feedback)
- AiState 扩展 feature_flags 字段
- 设计规格 + 讨论记录文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
iven
2026-05-18 22:55:40 +08:00
parent d623f8b2ff
commit bf37acc681
18 changed files with 2065 additions and 68 deletions

View File

@@ -1,6 +1,6 @@
import { useCallback, useState, useMemo } from 'react';
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message, Typography, Tooltip, Popconfirm, Space } from 'antd';
import { PlusOutlined, InfoCircleOutlined, EditOutlined, DeleteOutlined } from '@ant-design/icons';
import { Table, Button, Modal, Form, InputNumber, DatePicker, Input, message, Typography, Tooltip, Popconfirm, Space, Card } from 'antd';
import { PlusOutlined, InfoCircleOutlined, EditOutlined, DeleteOutlined, ThunderboltOutlined } from '@ant-design/icons';
import type { Dayjs } from 'dayjs';
import { dayjs } from '../../../utils/dayjs';
import { healthDataApi } from '../../../api/health/healthData';
@@ -9,6 +9,7 @@ import { VitalSignsChart } from './VitalSignsChart';
import { usePaginatedData } from '../../../hooks/usePaginatedData';
import { AuthButton } from '../../../components/AuthButton';
import { handleApiError } from '../../../api/client';
import { startAnalysis } from '../../../api/ai/analysisSse';
const { Text } = Typography;
@@ -20,6 +21,8 @@ export function VitalSignsTab({ patientId }: Props) {
const [modalOpen, setModalOpen] = useState(false);
const [editingRecord, setEditingRecord] = useState<VitalSigns | null>(null);
const [chartRefreshKey, setChartRefreshKey] = useState(0);
const [analyzingTrend, setAnalyzingTrend] = useState(false);
const [trendContent, setTrendContent] = useState('');
const [form] = Form.useForm();
const [submitting, setSubmitting] = useState(false);
@@ -32,6 +35,16 @@ export function VitalSignsTab({ patientId }: Props) {
const { data, total, page, loading, refresh } = usePaginatedData<VitalSigns>(fetcher, 10);
const handleTrendAnalysis = async () => {
setAnalyzingTrend(true);
setTrendContent('');
await startAnalysis('trends', { patient_id: patientId }, {
onChunk: (content) => setTrendContent(prev => prev + content),
onError: (msg) => { message.error(msg); setAnalyzingTrend(false); },
onDone: () => { message.success('AI 趋势分析完成'); setAnalyzingTrend(false); },
});
};
const handleOpenCreate = () => {
setEditingRecord(null);
form.resetFields();
@@ -211,9 +224,36 @@ export function VitalSignsTab({ patientId }: Props) {
<div>
{/* 趋势图 */}
<div style={{ marginBottom: 12 }}>
<div style={{ display: 'flex', justifyContent: 'flex-end', marginBottom: 8 }}>
<AuthButton
code="ai.analysis.manage"
icon={<ThunderboltOutlined />}
loading={analyzingTrend}
onClick={handleTrendAnalysis}
size="small"
>
AI
</AuthButton>
</div>
<VitalSignsChart patientId={patientId} refreshKey={chartRefreshKey} />
</div>
{/* AI 趋势分析结果 */}
{trendContent && (
<Card
title={<><ThunderboltOutlined /> AI </>}
size="small"
style={{ marginBottom: 12 }}
extra={
<Button size="small" onClick={() => setTrendContent('')}></Button>
}
>
<div style={{ whiteSpace: 'pre-wrap', lineHeight: 1.8 }}>
{trendContent}
</div>
</Card>
)}
{/* 最新记录摘要条 */}
{latest && (
<div style={{