diff --git a/apps/web/index.html b/apps/web/index.html index b35acdb..c648a88 100644 --- a/apps/web/index.html +++ b/apps/web/index.html @@ -3,14 +3,14 @@ - - + + - HMS 健康管理平台 + 暖记管理后台
diff --git a/apps/web/public/favicon.svg b/apps/web/public/favicon.svg index 6893eb1..3338a06 100644 --- a/apps/web/public/favicon.svg +++ b/apps/web/public/favicon.svg @@ -1 +1,12 @@ - \ No newline at end of file + + + + + + + + + + + + diff --git a/apps/web/src/App.tsx b/apps/web/src/App.tsx index 051f1fd..35f018f 100644 --- a/apps/web/src/App.tsx +++ b/apps/web/src/App.tsx @@ -26,54 +26,11 @@ const PluginGraphPage = lazy(() => import('./pages/PluginGraphPage').then((m) => const PluginDashboardPage = lazy(() => import('./pages/PluginDashboardPage').then((m) => ({ default: m.PluginDashboardPage }))); const PluginKanbanPage = lazy(() => import('./pages/PluginKanbanPage')); -// 健康管理模块 -const PatientList = lazy(() => import('./pages/health/PatientList')); -const PatientDetail = lazy(() => import('./pages/health/PatientDetail')); -const PatientTagManage = lazy(() => import('./pages/health/PatientTagManage')); -const DoctorList = lazy(() => import('./pages/health/DoctorList')); -const AppointmentList = lazy(() => import('./pages/health/AppointmentList')); -const DoctorSchedule = lazy(() => import('./pages/health/DoctorSchedule')); -const FollowUpTaskList = lazy(() => import('./pages/health/FollowUpTaskList')); -const FollowUpRecordList = lazy(() => import('./pages/health/FollowUpRecordList')); -const ConsultationList = lazy(() => import('./pages/health/ConsultationList')); -const ConsultationDetail = lazy(() => import('./pages/health/ConsultationDetail')); -const PointsRuleList = lazy(() => import('./pages/health/PointsRuleList')); -const PointsProductList = lazy(() => import('./pages/health/PointsProductList')); -const PointsOrderList = lazy(() => import('./pages/health/PointsOrderList')); -const OfflineEventList = lazy(() => import('./pages/health/OfflineEventList')); -const StatisticsDashboard = lazy(() => import('./pages/health/StatisticsDashboard')); -const AiPromptList = lazy(() => import('./pages/health/AiPromptList')); -const AiAnalysisList = lazy(() => import('./pages/health/AiAnalysisList')); -const AiUsageDashboard = lazy(() => import('./pages/health/AiUsageDashboard')); -const AiConfigPage = lazy(() => import('./pages/health/AiConfigPage')); -const KnowledgeV2Page = lazy(() => import('./pages/ai/KnowledgeV2Page')); -const AiChatPage = lazy(() => import('./pages/ai/ChatPage')); -const AlertList = lazy(() => import('./pages/health/AlertList')); -const AlertDashboard = lazy(() => import('./pages/health/AlertDashboard')); -const AlertRuleList = lazy(() => import('./pages/health/AlertRuleList')); -const DeviceManage = lazy(() => import('./pages/health/DeviceManage')); -const RealtimeMonitor = lazy(() => import('./pages/health/RealtimeMonitor')); -const OAuthClientList = lazy(() => import('./pages/health/OAuthClientList')); -const DialysisManageList = lazy(() => import('./pages/health/DialysisManageList')); -const ActionInbox = lazy(() => import('./pages/health/ActionInbox')); -const FollowUpTemplateList = lazy(() => import('./pages/health/FollowUpTemplateList')); -const CarePlanList = lazy(() => import('./pages/health/CarePlanList')); -const CarePlanDetail = lazy(() => import('./pages/health/CarePlanDetail')); -const ShiftList = lazy(() => import('./pages/health/ShiftList')); -const ShiftDetail = lazy(() => import('./pages/health/ShiftDetail')); -const MedicationRecordList = lazy(() => import('./pages/health/MedicationRecordList')); -const BleGatewayList = lazy(() => import('./pages/health/BleGatewayList')); -const BleGatewayDetail = lazy(() => import('./pages/health/BleGatewayDetail')); -const CriticalValueThresholdList = lazy(() => import('./pages/health/CriticalValueThresholdList')); -const FamilyProxyPage = lazy(() => import('./pages/health/FamilyProxyPage')); - -// 内容管理 -const ArticleManageList = lazy(() => import('./pages/health/ArticleManageList')); -const ArticleEditor = lazy(() => import('./pages/health/articleEditor/ArticleEditor')); -const ArticleCategoryManage = lazy(() => import('./pages/health/ArticleCategoryManage')); -const ArticleTagManage = lazy(() => import('./pages/health/ArticleTagManage')); -const BannerManage = lazy(() => import('./pages/health/BannerManage')); -const MediaLibrary = lazy(() => import('./pages/health/MediaLibrary')); +// 暖记日记模块 +const ClassList = lazy(() => import('./pages/diary/ClassList')); +const JournalList = lazy(() => import('./pages/diary/JournalList')); +const TopicList = lazy(() => import('./pages/diary/TopicList')); +const StickerPackList = lazy(() => import('./pages/diary/StickerPackList')); function FrozenRoute() { return ; @@ -126,7 +83,7 @@ function PrivateRoute({ children }: { children: React.ReactNode }) { const baseToken = { borderRadius: 10, - borderRadiusLG: 12, + borderRadiusLG: 16, borderRadiusSM: 6, fontFamily: "'Noto Sans SC', -apple-system, system-ui, 'Segoe UI', Roboto, 'PingFang SC', 'Microsoft YaHei', Helvetica, Arial, sans-serif", fontSize: 14, @@ -147,6 +104,28 @@ const baseComponents = { }; const themeConfigs: Record; components: Record> }> = { + warm: { + token: { + ...baseToken, + borderRadius: 16, + borderRadiusLG: 22, + borderRadiusSM: 10, + colorPrimary: '#E07A5F', + colorSuccess: '#81B29A', + colorWarning: '#F2CC8F', + colorError: '#E07A5F', + colorInfo: '#81B29A', + colorBgLayout: '#FFF8F0', + colorBgContainer: '#FFFFFF', + colorBgElevated: '#FFFFFF', + colorBorder: '#F0E8DF', + colorBorderSecondary: '#F5EDE5', + }, + components: { + ...baseComponents, + Table: { headerBg: '#FFF0E6', headerColor: '#8B7A6E', rowHoverBg: '#FFF8F0', fontSize: 14 }, + }, + }, blue: { token: { ...baseToken, @@ -166,56 +145,34 @@ const themeConfigs: Record; componen Table: { headerBg: '#f1f5f9', headerColor: '#475569', rowHoverBg: '#f1f5f9', fontSize: 14 }, }, }, - warm: { - token: { - ...baseToken, - borderRadius: 12, - borderRadiusLG: 14, - borderRadiusSM: 8, - colorPrimary: '#C4623A', - colorSuccess: '#5B7A5E', - colorWarning: '#C4873A', - colorError: '#B54A4A', - colorInfo: '#8B7A5E', - colorBgLayout: '#F5F0EB', - colorBgContainer: '#ffffff', - colorBgElevated: '#ffffff', - colorBorder: '#E8E2DC', - colorBorderSecondary: '#F0EBE5', - }, - components: { - ...baseComponents, - Table: { headerBg: '#EDE8E2', headerColor: '#7A756E', rowHoverBg: '#F5F0EB', fontSize: 14 }, - }, - }, dark: { token: { ...baseToken, - colorPrimary: '#60A5FA', - colorSuccess: '#34D399', - colorWarning: '#FBBF24', - colorError: '#F87171', - colorInfo: '#38BDF8', - colorBgLayout: '#0F172A', - colorBgContainer: '#1E293B', - colorBgElevated: '#334155', - colorBorder: '#334155', + colorPrimary: '#E8907A', + colorSuccess: '#8FBF9E', + colorWarning: '#D4B878', + colorError: '#E8907A', + colorInfo: '#8FBF9E', + colorBgLayout: '#1A1614', + colorBgContainer: '#2A2520', + colorBgElevated: '#352F2A', + colorBorder: '#3A3530', colorBorderSecondary: 'rgba(255,255,255,0.06)', boxShadow: 'none', boxShadowSecondary: '0 2px 8px rgba(0,0,0,0.3), 0 1px 3px rgba(0,0,0,0.2)', }, components: { ...baseComponents, - Table: { headerBg: '#1E293B', headerColor: '#94A3B8', rowHoverBg: '#1E293B', fontSize: 14 }, + Table: { headerBg: '#2A2520', headerColor: '#B0A89E', rowHoverBg: '#2A2520', fontSize: 14 }, }, }, emerald: { token: { ...baseToken, borderRadius: 10, - borderRadiusLG: 14, + borderRadiusLG: 16, borderRadiusSM: 8, - colorPrimary: '#5B7A5E', + colorPrimary: '#81B29A', colorSuccess: '#3D7A42', colorWarning: '#B8863A', colorError: '#A54A4A', @@ -250,27 +207,12 @@ export default function App() { validateRouteCoverage([ "/users", "/roles", "/organizations", "/workflow", "/messages", "/settings", "/plugins/admin", "/plugins/market", - "/health/statistics", "/health/patients", "/health/tags", "/health/doctors", - "/health/appointments", "/health/schedules", "/health/follow-up-tasks", - "/health/follow-up-records", "/health/consultations", - "/health/points-rules", "/health/points-products", "/health/points-orders", - "/health/offline-events", "/health/ai-prompts", "/health/ai-analysis", - "/health/ai-usage", "/health/ai-config", "/health/ai-knowledge", "/health/alerts", "/health/alert-dashboard", - "/ai/chat", - "/health/alert-rules", "/health/devices", "/health/realtime-monitor", - "/health/oauth-clients", "/health/dialysis", "/health/action-inbox", - "/health/follow-up-templates", "/health/care-plans", "/health/shifts", - "/health/medications", "/health/ble-gateways", - "/health/critical-value-thresholds", "/health/diagnoses", - "/health/family-proxy", "/health/consents", - "/health/articles", "/health/article-categories", "/health/article-tags", - "/health/banners", "/health/media-library", - "/health/medication-records", + "/diary/classes", "/diary/journals", "/diary/topics", "/diary/stickers", ]); }, []); const isDark = themeName === 'dark'; - const antTheme = useMemo(() => themeConfigs[themeName] ?? themeConfigs.blue, [themeName]); + const antTheme = useMemo(() => themeConfigs[themeName] ?? themeConfigs.warm, [themeName]); return ( <> @@ -308,54 +250,11 @@ export default function App() { } /> } /> } /> - {/* 健康管理 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - } /> - {/* 内容管理 */} - } /> - } /> - } /> - } /> - } /> - } /> - } /> + {/* 暖记日记模块 */} + } /> + } /> + } /> + } /> diff --git a/apps/web/src/api/ai/analysis.test.ts b/apps/web/src/api/ai/analysis.test.ts deleted file mode 100644 index f2ec95c..0000000 --- a/apps/web/src/api/ai/analysis.test.ts +++ /dev/null @@ -1,127 +0,0 @@ -/** - * AI 模块 API 契约测试(analysis + prompts + suggestions + usage) - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { analysisApi } from './analysis' -import { promptApi } from './prompts' -import { suggestionApi } from './suggestions' -import { usageApi } from './usage' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('analysisApi', () => { - const fakeRes = { data: { data: {} } } - - it('list 应调用 GET /ai/analysis/history 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await analysisApi.list({ patient_id: 'p-001', analysis_type: 'lab-report', page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/ai/analysis/history', { - params: { patient_id: 'p-001', analysis_type: 'lab-report', page: 1, page_size: 10 }, - }) - }) - - it('get 应调用 GET /ai/analysis/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await analysisApi.get('ana-001') - - expect(mockGet).toHaveBeenCalledWith('/ai/analysis/ana-001') - }) -}) - -describe('promptApi', () => { - const fakeRes = { data: { data: {} } } - - it('list 应调用 GET /ai/prompts 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await promptApi.list({ category: 'analysis', page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/ai/prompts', { - params: { category: 'analysis', page: 1, page_size: 10 }, - }) - }) - - it('create 应调用 POST /ai/prompts 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '化验解读', system_prompt: '你是专业医生', user_prompt_template: '解读: {report}', model_config: {}, category: 'analysis' } - await promptApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/ai/prompts', req) - }) - - it('activate 应调用 POST /ai/prompts/:id/activate', async () => { - mockPost.mockResolvedValue(fakeRes) - await promptApi.activate('prompt-001') - - expect(mockPost).toHaveBeenCalledWith('/ai/prompts/prompt-001/activate') - }) - - it('rollback 应调用 POST /ai/prompts/:id/rollback', async () => { - mockPost.mockResolvedValue(fakeRes) - await promptApi.rollback('prompt-001') - - expect(mockPost).toHaveBeenCalledWith('/ai/prompts/prompt-001/rollback') - }) -}) - -describe('suggestionApi', () => { - const fakeRes = { data: { data: {} } } - - it('list 应调用 GET /ai/suggestions 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await suggestionApi.list({ analysis_id: 'ana-001', status: 'pending' }) - - expect(mockGet).toHaveBeenCalledWith('/ai/suggestions', { - params: { analysis_id: 'ana-001', status: 'pending' }, - }) - }) - - it('approve 应调用 POST /ai/suggestions/:id/approve 并传递 action', async () => { - mockPost.mockResolvedValue(fakeRes) - await suggestionApi.approve('sug-001', 'approve') - - expect(mockPost).toHaveBeenCalledWith('/ai/suggestions/sug-001/approve', { action: 'approve' }) - }) - - it('getComparison 应调用 GET /ai/suggestions/:id/comparison', async () => { - mockGet.mockResolvedValue(fakeRes) - await suggestionApi.getComparison('sug-001') - - expect(mockGet).toHaveBeenCalledWith('/ai/suggestions/sug-001/comparison') - }) -}) - -describe('usageApi', () => { - const fakeRes = { data: { data: {} } } - - it('overview 应调用 GET /ai/usage/overview', async () => { - mockGet.mockResolvedValue(fakeRes) - await usageApi.overview() - - expect(mockGet).toHaveBeenCalledWith('/ai/usage/overview') - }) - - it('byType 应调用 GET /ai/usage/by-type', async () => { - mockGet.mockResolvedValue(fakeRes) - await usageApi.byType() - - expect(mockGet).toHaveBeenCalledWith('/ai/usage/by-type') - }) -}) diff --git a/apps/web/src/api/ai/analysis.ts b/apps/web/src/api/ai/analysis.ts deleted file mode 100644 index cdfbff9..0000000 --- a/apps/web/src/api/ai/analysis.ts +++ /dev/null @@ -1,47 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -export interface AnalysisItem { - id: string; - patient_id: string; - patient_name?: string; - analysis_type: string; - source_ref: string; - model_used: string; - status: string; - result_content: string | null; - result_metadata: Record | null; - error_message: string | null; - created_at: string; - updated_at: string; -} - -export interface HealthSummaryResponse { - patient_id: string; - risk_level: 'low' | 'medium' | 'high' | 'critical'; - active_insights_count: number; - recent_analyses_count: number; - latest_insight_title: string | null; - latest_analysis_type: string | null; - summary_items: Array<{ - category: string; - title: string; - severity: string | null; - created_at: string; - }>; -} - -export const analysisApi = { - list: async (params?: { patient_id?: string; analysis_type?: string; page?: number; page_size?: number }) => { - const resp = await client.get('/ai/analysis/history', { params }); - return resp.data.data as PaginatedResponse; - }, - get: async (id: string) => { - const resp = await client.get(`/ai/analysis/${id}`); - return resp.data.data as AnalysisItem; - }, - getHealthSummary: async (patientId: string) => { - const resp = await client.get('/ai/health-summary', { params: { patient_id: patientId } }); - return resp.data.data as HealthSummaryResponse; - }, -}; diff --git a/apps/web/src/api/ai/analysisSse.ts b/apps/web/src/api/ai/analysisSse.ts deleted file mode 100644 index 4dee7af..0000000 --- a/apps/web/src/api/ai/analysisSse.ts +++ /dev/null @@ -1,98 +0,0 @@ -export type AnalysisType = 'lab-report' | 'trends' | 'checkup-plan' | 'report-summary' | 'follow-up-summary'; - -interface AnalyzeBody { - report_id?: string; - patient_id?: string; - metrics?: string[]; - source_id?: string; -} - -const ENDPOINT_MAP: Record = { - 'lab-report': '/ai/analyze/lab-report', - 'trends': '/ai/analyze/trends', - 'checkup-plan': '/ai/analyze/checkup-plan', - 'report-summary': '/ai/analyze/report-summary', - 'follow-up-summary': '/ai/analyze/follow-up-summary', -}; - -export interface SseCallbacks { - onChunk: (content: string, index: number) => void; - onError: (message: string) => void; - onDone: (analysisId: string) => void; -} - -export async function startAnalysis( - type: AnalysisType, - body: AnalyzeBody, - callbacks: SseCallbacks, -): Promise { - const controller = new AbortController(); - const endpoint = ENDPOINT_MAP[type]; - - const token = localStorage.getItem('hms-token'); - const resp = await fetch(`/api/v1${endpoint}`, { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - Authorization: `Bearer ${token}`, - }, - body: JSON.stringify(body), - signal: controller.signal, - }); - - if (!resp.ok) { - const err = await resp.json().catch(() => ({ message: '分析请求失败' })); - callbacks.onError(err?.message || `HTTP ${resp.status}`); - return controller; - } - - const reader = resp.body?.getReader(); - if (!reader) { - callbacks.onError('无法读取响应流'); - return controller; - } - - const decoder = new TextDecoder(); - let chunkIndex = 0; - let buffer = ''; - - (async () => { - try { - while (true) { - const { done, value } = await reader.read(); - if (done) break; - - buffer += decoder.decode(value, { stream: true }); - const lines = buffer.split('\n'); - buffer = lines.pop() || ''; - - for (const line of lines) { - if (line.startsWith('data: ')) { - const data = line.slice(6); - if (data === '[DONE]') { - continue; - } - try { - const event = JSON.parse(data); - if (event.type === 'chunk' && event.content) { - callbacks.onChunk(event.content, chunkIndex++); - } else if (event.type === 'done' && event.analysis_id) { - callbacks.onDone(event.analysis_id); - } else if (event.type === 'error') { - callbacks.onError(event.message || '分析出错'); - } - } catch { - // 非 JSON 行,跳过 - } - } - } - } - } catch (err) { - if (!controller.signal.aborted) { - callbacks.onError(err instanceof Error ? err.message : '连接中断'); - } - } - })(); - - return controller; -} diff --git a/apps/web/src/api/ai/chat.ts b/apps/web/src/api/ai/chat.ts deleted file mode 100644 index b303e82..0000000 --- a/apps/web/src/api/ai/chat.ts +++ /dev/null @@ -1,118 +0,0 @@ -import client from '../client'; - -export interface ChatHistoryItem { - role: 'user' | 'assistant'; - content: string; -} - -export type DisplayHint = - | { - type: 'vital_card'; - indicator_type: string; - values: [string, number][]; - unit: string; - } - | { - type: 'lab_report_card'; - report_date: string; - abnormal_count: number; - } - | { - type: 'action_confirm'; - action_type: string; - summary: string; - confirm_payload: unknown; - } - | { - type: 'risk_alert'; - level: string; - message: string; - } - | { - type: 'trend_chart'; - metrics: string[]; - period: string; - summary: string; - } - | { - type: 'insight_card'; - title: string; - severity: string; - items: string[]; - } - | { - type: 'patient_profile'; - chronic_conditions: string[]; - medication_count: number; - } - | { type: 'text' }; - -export interface ChatResponse { - reply: string; - message_id: string; - iterations: number; - display_hints?: DisplayHint[]; -} - -export interface ChatSession { - id: string; - title: string | null; - patient_id: string | null; - status: string; - created_at: string; - updated_at: string; -} - -export const aiChatApi = { - sendMessage: async ( - message: string, - history: ChatHistoryItem[], - patientId?: string, - sessionId?: string - ): Promise => { - const resp = await client.post('/ai/chat', { - message, - history, - ...(patientId ? { patient_id: patientId } : {}), - ...(sessionId ? { session_id: sessionId } : {}), - }); - return resp.data.data as ChatResponse; - }, - - createSession: async ( - patientId?: string, - title?: string - ): Promise => { - const resp = await client.post('/ai/chat/sessions', { - ...(patientId ? { patient_id: patientId } : {}), - ...(title ? { title } : {}), - }); - return resp.data.data as ChatSession; - }, - - listSessions: async (): Promise => { - const resp = await client.get('/ai/chat/sessions'); - return resp.data.data as ChatSession[]; - }, - - renameSession: async ( - sessionId: string, - title: string - ): Promise => { - await client.put(`/ai/chat/sessions/${sessionId}/rename`, { title }); - }, - - closeSession: async (sessionId: string): Promise => { - await client.post(`/ai/chat/sessions/${sessionId}/close`); - }, - - getSessionMessages: async (sessionId: string): Promise> => { - const resp = await client.get(`/ai/chat/sessions/${sessionId}/messages`); - return resp.data.data; - }, -}; diff --git a/apps/web/src/api/ai/config.ts b/apps/web/src/api/ai/config.ts deleted file mode 100644 index e371755..0000000 --- a/apps/web/src/api/ai/config.ts +++ /dev/null @@ -1,45 +0,0 @@ -import client from '../client'; - -export interface AiAgentConfig { - model: string; - temperature: number; - max_tokens: number; - max_iterations: number; - system_prompt: string; -} - -export interface AiAnalysisDefaults { - model: string; - temperature: number; - max_tokens: number; -} - -export interface AiProviderConfig { - provider_type: string; - enabled: boolean; - base_url: string; - api_key: string; - model: string; -} - -export interface AiConfig { - agent: AiAgentConfig; - analysis_defaults: AiAnalysisDefaults; - default_provider: string; - providers: Record; -} - -export const aiConfigApi = { - get: async () => { - const resp = await client.get('/ai/config'); - return resp.data.data as AiConfig; - }, - getDefaults: async () => { - const resp = await client.get('/ai/config/defaults'); - return resp.data.data as AiConfig; - }, - update: async (config: AiConfig) => { - const resp = await client.put('/ai/config', { config }); - return resp.data.data as AiConfig; - }, -}; diff --git a/apps/web/src/api/ai/dialysis.ts b/apps/web/src/api/ai/dialysis.ts deleted file mode 100644 index 70268ab..0000000 --- a/apps/web/src/api/ai/dialysis.ts +++ /dev/null @@ -1,23 +0,0 @@ -import client from '../client'; - -export interface DialysisRiskRequest { - patient_id: string; - dialysis_session_id?: string; -} - -export interface DialysisRiskAssessment { - id: string; - patient_id: string; - risk_level: string; - risk_factors: string[]; - recommendations: string[]; - kdigo_stage?: string; - created_at: string; -} - -export const dialysisRiskApi = { - assess: async (data: DialysisRiskRequest) => { - const resp = await client.post('/ai/dialysis/risk-assessment', data); - return resp.data.data as DialysisRiskAssessment; - }, -}; diff --git a/apps/web/src/api/ai/knowledgeV2.ts b/apps/web/src/api/ai/knowledgeV2.ts deleted file mode 100644 index 3a1ba31..0000000 --- a/apps/web/src/api/ai/knowledgeV2.ts +++ /dev/null @@ -1,188 +0,0 @@ -import client from '../client'; - -// === Types === - -export interface KnowledgeBase { - id: string; - tenant_id: string; - name: string; - kb_type: string; - description: string | null; - icon: string | null; - chunk_strategy: Record; - intent_keywords: Record; - embedding_model: string | null; - is_enabled: boolean; - document_count: number; - chunk_count: number; - created_at: string; - updated_at: string; -} - -export interface KnowledgeDocument { - id: string; - tenant_id: string; - knowledge_base_id: string; - title: string; - doc_type: string; - source_type: string; - source_url: string | null; - file_name: string | null; - file_size: number | null; - file_mime_type: string | null; - content: string | null; - status: string; - chunk_count: number; - embedded_count: number; - error_message: string | null; - processing_started_at: string | null; - processing_completed_at: string | null; - created_at: string; - updated_at: string; -} - -export interface SearchHit { - chunk_id: string; - document_id: string; - chunk_index: number; - content: string; - doc_title: string; - similarity: number; - metadata: Record; -} - -export interface CreateKnowledgeBaseReq { - name: string; - kb_type: string; - description?: string; - icon?: string; - chunk_strategy?: Record; - intent_keywords?: Record; - embedding_model?: string; - is_enabled?: boolean; -} - -export interface UpdateKnowledgeBaseReq { - name?: string; - kb_type?: string; - description?: string; - icon?: string; - chunk_strategy?: Record; - intent_keywords?: Record; - embedding_model?: string; - is_enabled?: boolean; -} - -export interface CreateDocumentReq { - kb_id: string; - title: string; - doc_type?: string; - source_type?: string; - source_url?: string; - content?: string; -} - -// === API === - -export const knowledgeV2Api = { - // Knowledge Bases - listKnowledgeBases: async (params?: { - kb_type?: string; - is_enabled?: boolean; - page?: number; - page_size?: number; - }) => { - const resp = await client.get('/ai/knowledge-bases', { params }); - return resp.data.data as { - data: KnowledgeBase[]; - total: number; - page: number; - page_size: number; - }; - }, - - getKnowledgeBase: async (id: string) => { - const resp = await client.get(`/ai/knowledge-bases/${id}`); - return resp.data.data as KnowledgeBase; - }, - - createKnowledgeBase: async (data: CreateKnowledgeBaseReq) => { - const resp = await client.post('/ai/knowledge-bases', data); - return resp.data.data as { id: string }; - }, - - updateKnowledgeBase: async (id: string, data: UpdateKnowledgeBaseReq) => { - const resp = await client.put(`/ai/knowledge-bases/${id}`, data); - return resp.data.data as { id: string }; - }, - - deleteKnowledgeBase: async (id: string) => { - const resp = await client.delete(`/ai/knowledge-bases/${id}`); - return resp.data.data as { id: string }; - }, - - // Documents - listDocuments: async ( - kbId: string, - params?: { status?: string; page?: number; page_size?: number }, - ) => { - const resp = await client.get( - `/ai/knowledge-bases/${kbId}/documents`, - { params }, - ); - return resp.data.data as { - data: KnowledgeDocument[]; - total: number; - page: number; - page_size: number; - }; - }, - - getDocument: async (id: string) => { - const resp = await client.get(`/ai/documents/${id}`); - return resp.data.data as KnowledgeDocument; - }, - - createManualDocument: async (data: CreateDocumentReq) => { - const resp = await client.post('/ai/documents/manual', data); - return resp.data.data as { id: string }; - }, - - uploadDocument: async ( - kbId: string, - file: File, - title?: string, - ) => { - const formData = new FormData(); - formData.append('kb_id', kbId); - formData.append('file', file); - if (title) { - formData.append('title', title); - } - const resp = await client.post('/ai/documents/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - return resp.data.data as { id: string }; - }, - - deleteDocument: async (kbId: string, id: string) => { - const resp = await client.delete( - `/ai/knowledge-bases/${kbId}/documents/${id}`, - ); - return resp.data.data as { id: string }; - }, - - // Hit Test - hitTest: async (kbId: string, query: string, topK?: number) => { - const resp = await client.post('/ai/documents/hit-test', { - kb_id: kbId, - query, - top_k: topK, - }); - return resp.data.data as { - query: string; - total: number; - hits: SearchHit[]; - }; - }, -}; diff --git a/apps/web/src/api/ai/prompts.ts b/apps/web/src/api/ai/prompts.ts deleted file mode 100644 index a8fcc24..0000000 --- a/apps/web/src/api/ai/prompts.ts +++ /dev/null @@ -1,54 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -export interface PromptItem { - id: string; - name: string; - description: string; - system_prompt: string; - user_prompt_template: string; - model_config: Record; - version: number; - is_active: boolean; - category: string; - analysis_type: string; - tags: Record | null; - created_at: string; - updated_at: string; -} - -export interface CreatePromptReq { - name: string; - description?: string; - system_prompt: string; - user_prompt_template: string; - model_config: Record; - category: string; - analysis_type: string; -} - -export const promptApi = { - list: async (params?: { category?: string; analysis_type?: string; page?: number; page_size?: number }) => { - const resp = await client.get('/ai/prompts', { params }); - return resp.data.data as PaginatedResponse; - }, - create: async (data: CreatePromptReq) => { - const resp = await client.post('/ai/prompts', data); - return resp.data.data as PromptItem; - }, - activate: async (id: string) => { - const resp = await client.post(`/ai/prompts/${id}/activate`); - return resp.data.data as PromptItem; - }, - deactivate: async (id: string) => { - const resp = await client.post(`/ai/prompts/${id}/deactivate`); - return resp.data.data as PromptItem; - }, - rollback: async (id: string) => { - const resp = await client.post(`/ai/prompts/${id}/rollback`); - return resp.data.data as PromptItem; - }, - delete: async (id: string) => { - await client.delete(`/ai/prompts/${id}`); - }, -}; diff --git a/apps/web/src/api/ai/suggestions.ts b/apps/web/src/api/ai/suggestions.ts deleted file mode 100644 index 5e351fc..0000000 --- a/apps/web/src/api/ai/suggestions.ts +++ /dev/null @@ -1,38 +0,0 @@ -import client from '../client'; - -export interface SuggestionItem { - id: string; - analysis_id: string; - suggestion_type: string; - risk_level: string; - params: Record | null; - status: string; - created_at: string; -} - -export interface ComparisonReport { - suggestion_id: string; - baseline: Record | null; - current: Record | 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 }; - }, - execute: async (id: string) => { - const resp = await client.post(`/ai/suggestions/${id}/execute`); - 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; - }, -}; diff --git a/apps/web/src/api/ai/usage.ts b/apps/web/src/api/ai/usage.ts deleted file mode 100644 index 5b91f63..0000000 --- a/apps/web/src/api/ai/usage.ts +++ /dev/null @@ -1,107 +0,0 @@ -import client from '../client'; - -export interface UsageOverview { - total_count: number; -} - -export interface TypeDistribution { - analysis_type: string; - count: number; -} - -export interface ProviderInfo { - id: string; - name: string; - provider_type: string; - is_active: boolean; - model_name?: string; -} - -export interface ProviderHealth { - provider_id: string; - status: string; - latency_ms?: number; - last_checked_at?: string; -} - -export interface QuotaSummary { - provider_id: string; - quota_limit: number; - quota_used: number; - quota_remaining: number; - period: string; -} - -export interface BudgetStatus { - total_budget: number; - spent: number; - remaining: number; - period: string; -} - -export interface CostEstimate { - analysis_type: string; - estimated_cost: number; - currency: string; -} - -export interface DailyUsageRow { - date: string; - feature: string; - provider: string; - model: string; - total_calls: number; - total_input_tokens: number; - total_output_tokens: number; - total_cost_cents: number; -} - -export interface FeatureFlag { - feature: string; - is_enabled: boolean; -} - -export const usageApi = { - overview: async () => { - const resp = await client.get('/ai/usage/overview'); - return resp.data.data as UsageOverview; - }, - byType: async () => { - const resp = await client.get('/ai/usage/by-type'); - return resp.data.data as TypeDistribution[]; - }, - listProviders: async () => { - const resp = await client.get('/ai/providers'); - return resp.data.data as ProviderInfo[]; - }, - getProvidersHealth: async () => { - const resp = await client.get('/ai/providers/health'); - return resp.data.data as ProviderHealth[]; - }, - getQuotaSummary: async () => { - const resp = await client.get('/ai/quota/summary'); - return resp.data.data as QuotaSummary[]; - }, - getBudgetStatus: async () => { - const resp = await client.get('/ai/budget/status'); - return resp.data.data as BudgetStatus; - }, - getCostEstimate: async (params: { analysis_type: string }) => { - const resp = await client.get('/ai/cost/estimate', { params }); - return resp.data.data as CostEstimate; - }, - getDailyUsage: async (startDate: string, endDate: string) => { - const resp = await client.get('/ai/admin/daily-usage', { - params: { start_date: startDate, end_date: endDate }, - }); - return resp.data.data as DailyUsageRow[]; - }, - getFeatureFlags: async () => { - const resp = await client.get('/ai/admin/flags'); - return resp.data.data as FeatureFlag[]; - }, - updateFeatureFlag: async (feature: string, enabled: boolean) => { - const resp = await client.post('/ai/admin/flags', { feature, enabled }); - return resp.data.data as { feature: string; enabled: boolean }; - }, -}; diff --git a/apps/web/src/api/config-modules.test.ts b/apps/web/src/api/config-modules.test.ts index f051af7..f329835 100644 --- a/apps/web/src/api/config-modules.test.ts +++ b/apps/web/src/api/config-modules.test.ts @@ -153,7 +153,7 @@ describe('numberingRules API', () => { it('updateNumberingRule 应调用 PUT /config/numbering-rules/:id', async () => { mockPut.mockResolvedValue(fakeRes) - const req = { prefix: 'HMS', version: 1 } + const req = { prefix: 'NJ', version: 1 } await numberingApi.updateNumberingRule('nr-001', req) expect(mockPut).toHaveBeenCalledWith('/config/numbering-rules/nr-001', req) @@ -189,7 +189,7 @@ describe('themes API', () => { it('updateTheme 应调用 PUT /config/themes', async () => { mockPut.mockResolvedValue(fakeRes) - const theme = { primary_color: '#1890ff', brand_name: 'HMS' } + const theme = { primary_color: '#E07A5F', brand_name: 'Nuanji' } await themesApi.updateTheme(theme) expect(mockPut).toHaveBeenCalledWith('/config/themes', theme) diff --git a/apps/web/src/api/diary/classes.ts b/apps/web/src/api/diary/classes.ts new file mode 100644 index 0000000..99e9277 --- /dev/null +++ b/apps/web/src/api/diary/classes.ts @@ -0,0 +1,26 @@ +import client from '../client'; +import type { SchoolClass, CreateClassReq, ClassMember, PaginatedResponse } from './types'; + +export const classApi = { + list: (params?: { page?: number; page_size?: number }) => + client.get<{ success: boolean; data: PaginatedResponse }>('/diary/classes', { params }) + .then((r) => r.data.data), + + myClasses: () => + client.get<{ success: boolean; data: SchoolClass[] }>('/diary/classes/my') + .then((r) => r.data.data), + + get: (id: string) => + client.get<{ success: boolean; data: SchoolClass }>(`/diary/classes/${id}`) + .then((r) => r.data.data), + + create: (data: CreateClassReq) => + client.post('/diary/classes', data).then((r) => r.data.data), + + listMembers: (classId: string) => + client.get<{ success: boolean; data: ClassMember[] }>(`/diary/classes/${classId}/members`) + .then((r) => r.data.data), + + join: (classCode: string) => + client.post('/diary/classes/join', { class_code: classCode }).then((r) => r.data.data), +}; diff --git a/apps/web/src/api/diary/comments.ts b/apps/web/src/api/diary/comments.ts new file mode 100644 index 0000000..4938d3c --- /dev/null +++ b/apps/web/src/api/diary/comments.ts @@ -0,0 +1,14 @@ +import client from '../client'; +import type { Comment, CreateCommentReq } from './types'; + +export const commentApi = { + list: (journalId: string) => + client.get<{ success: boolean; data: Comment[] }>(`/diary/journals/${journalId}/comments`) + .then((r) => r.data.data), + + create: (journalId: string, data: CreateCommentReq) => + client.post(`/diary/journals/${journalId}/comments`, data).then((r) => r.data.data), + + delete: (commentId: string) => + client.delete(`/diary/comments/${commentId}`).then((r) => r.data), +}; diff --git a/apps/web/src/api/diary/journals.ts b/apps/web/src/api/diary/journals.ts new file mode 100644 index 0000000..7b787b4 --- /dev/null +++ b/apps/web/src/api/diary/journals.ts @@ -0,0 +1,27 @@ +import client from '../client'; +import type { JournalEntry, CreateJournalReq, UpdateJournalReq, PaginatedResponse } from './types'; + +export const journalApi = { + list: (params?: { + page?: number; + page_size?: number; + author_id?: string; + mood?: string; + date_from?: string; + date_to?: string; + class_id?: string; + }) => client.get<{ success: boolean; data: PaginatedResponse }>('/diary/journals', { params }) + .then((r) => r.data.data), + + get: (id: string) => client.get<{ success: boolean; data: JournalEntry }>(`/diary/journals/${id}`) + .then((r) => r.data.data), + + create: (data: CreateJournalReq) => client.post('/diary/journals', data) + .then((r) => r.data.data), + + update: (id: string, data: UpdateJournalReq) => client.put(`/diary/journals/${id}`, data) + .then((r) => r.data.data), + + delete: (id: string, version: number) => client.delete(`/diary/journals/${id}`, { data: { version } }) + .then((r) => r.data), +}; diff --git a/apps/web/src/api/diary/stickers.ts b/apps/web/src/api/diary/stickers.ts new file mode 100644 index 0000000..ce1e6cd --- /dev/null +++ b/apps/web/src/api/diary/stickers.ts @@ -0,0 +1,22 @@ +import client from '../client'; +import type { StickerPack, Sticker, Template } from './types'; + +export const stickerApi = { + listPacks: (params?: { category?: string }) => + client.get<{ success: boolean; data: StickerPack[] }>('/diary/sticker-packs', { params }) + .then((r) => r.data.data), + + listStickers: (packId: string) => + client.get<{ success: boolean; data: Sticker[] }>(`/diary/sticker-packs/${packId}/stickers`) + .then((r) => r.data.data), +}; + +export const templateApi = { + list: (params?: { category?: string }) => + client.get<{ success: boolean; data: Template[] }>('/diary/templates', { params }) + .then((r) => r.data.data), + + get: (id: string) => + client.get<{ success: boolean; data: Template }>(`/diary/templates/${id}`) + .then((r) => r.data.data), +}; diff --git a/apps/web/src/api/diary/topics.ts b/apps/web/src/api/diary/topics.ts new file mode 100644 index 0000000..bbe0ae2 --- /dev/null +++ b/apps/web/src/api/diary/topics.ts @@ -0,0 +1,11 @@ +import client from '../client'; +import type { TopicAssignment, CreateTopicReq } from './types'; + +export const topicApi = { + list: (classId: string) => + client.get<{ success: boolean; data: TopicAssignment[] }>(`/diary/classes/${classId}/topics`) + .then((r) => r.data.data), + + assign: (classId: string, data: CreateTopicReq) => + client.post(`/diary/classes/${classId}/topics`, data).then((r) => r.data.data), +}; diff --git a/apps/web/src/api/diary/types.ts b/apps/web/src/api/diary/types.ts new file mode 100644 index 0000000..5c19412 --- /dev/null +++ b/apps/web/src/api/diary/types.ts @@ -0,0 +1,128 @@ +export interface JournalEntry { + id: string; + author_id: string; + class_id?: string; + title: string; + date: string; + mood: string; + weather: string; + tags: string[]; + is_private: boolean; + shared_to_class: boolean; + assigned_topic_id?: string; + version: number; + created_at: string; + updated_at: string; +} + +export interface CreateJournalReq { + title: string; + date: string; + mood: string; + weather: string; + tags?: string[]; + is_private?: boolean; + class_id?: string; +} + +export interface UpdateJournalReq { + title?: string; + mood?: string; + weather?: string; + tags?: string[]; + is_private?: boolean; + shared_to_class?: boolean; + version: number; +} + +export interface SchoolClass { + id: string; + name: string; + school_name?: string; + teacher_id: string; + class_code: string; + member_count: number; + is_active: boolean; +} + +export interface CreateClassReq { + name: string; + school_name?: string; +} + +export interface ClassMember { + user_id: string; + role: string; + nickname?: string; + joined_at: string; +} + +export interface TopicAssignment { + id: string; + class_id: string; + teacher_id: string; + title: string; + description?: string; + due_date?: string; + is_active: boolean; +} + +export interface CreateTopicReq { + title: string; + description?: string; + due_date?: string; +} + +export interface Comment { + id: string; + journal_id: string; + author_id: string; + content: string; + created_at: string; +} + +export interface CreateCommentReq { + content: string; +} + +export interface StickerPack { + id: string; + name: string; + description?: string; + cover_image_url?: string; + sticker_count: number; + is_free: boolean; + category?: string; +} + +export interface Sticker { + id: string; + pack_id: string; + name: string; + image_url: string; + category?: string; +} + +export interface Template { + id: string; + name: string; + description?: string; + preview_url?: string; + template_data?: unknown; + category?: string; + is_free: boolean; +} + +export interface MoodStats { + mood_counts: Array<{ mood: string; count: number; percentage: number }>; + streak_days: number; + total_journals: number; + dominant_mood?: string; +} + +export interface PaginatedResponse { + data: T[]; + total: number; + page: number; + page_size: number; +} diff --git a/apps/web/src/api/health/actionInbox.ts b/apps/web/src/api/health/actionInbox.ts deleted file mode 100644 index 951ba63..0000000 --- a/apps/web/src/api/health/actionInbox.ts +++ /dev/null @@ -1,128 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -export type ActionType = 'ai_suggestion' | 'alert' | 'followup' | 'data_anomaly'; -export type ActionPriority = 'urgent' | 'high' | 'medium' | 'low'; -export type ActionStatus = 'pending' | 'in_progress' | 'completed' | 'dismissed'; - -export interface ActionItem { - id: string; - action_type: ActionType; - priority: ActionPriority; - status: ActionStatus; - title: string; - summary: string; - patient_id: string; - patient_name: string; - source_ref: string; - created_at: string; - updated_at: string; -} - -export interface ThreadEvent { - step: string; - label: string; - status: ActionStatus; - detail?: string; - timestamp?: string; - operator?: string; - link_to?: string; -} - -export interface ActionDefinition { - key: string; - label: string; - variant: 'primary' | 'danger' | 'default'; - api_endpoint?: string; -} - -export interface ThreadResponse { - action_item: ActionItem; - thread: ThreadEvent[]; - available_actions: ActionDefinition[]; -} - -export interface WorkbenchStats { - total_pending: number; - ai_suggestion_pending: number; - urgent_alerts: number; - followup_due: number; - completion_rate: number | null; -} - -export interface NursePatientSummary { - patient_id: string; - patient_name: string; - pending_actions: number; - highest_priority: ActionPriority; -} - -export interface TeamMemberOverview { - user_id: string; - name: string; - title: string; - pending_count: number; - completed_count: number; - overdue_count: number; - completion_rate: number; -} - -export interface TeamOverview { - members: TeamMemberOverview[]; - risk_distribution: { - high: number; - medium: number; - low: number; - }; - total_pending: number; - total_completed: number; -} - -export const actionInboxApi = { - list: async (params?: { - status?: string; - type?: string; - page?: number; - page_size?: number; - assigned_to_me?: boolean; - patient_id?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/action-inbox', { params }); - return data.data; - }, - - getThread: async (sourceRef: string) => { - const { data } = await client.get<{ - success: boolean; - data: ThreadResponse; - }>(`/health/action-inbox/${encodeURIComponent(sourceRef)}/thread`); - return data.data; - }, - - stats: async (params?: { assigned_to_me?: boolean }) => { - const { data } = await client.get<{ - success: boolean; - data: WorkbenchStats; - }>('/health/action-inbox/stats', { params }); - return data.data; - }, - - myPatients: async () => { - const { data } = await client.get<{ - success: boolean; - data: NursePatientSummary[]; - }>('/health/action-inbox/my-patients'); - return data.data; - }, - - team: async () => { - const { data } = await client.get<{ - success: boolean; - data: TeamOverview; - }>('/health/action-inbox/team'); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/alerts.test.ts b/apps/web/src/api/health/alerts.test.ts deleted file mode 100644 index 716bbfc..0000000 --- a/apps/web/src/api/health/alerts.test.ts +++ /dev/null @@ -1,100 +0,0 @@ -/** - * alerts API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { alertApi, alertRuleApi } from './alerts' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('alertApi', () => { - const fakeRes = { data: { data: {} } } - - it('list 应调用 GET /health/alerts 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await alertApi.list({ patient_id: 'p-001', status: 'active', page: 1, page_size: 20 }) - - expect(mockGet).toHaveBeenCalledWith('/health/alerts', { - params: { patient_id: 'p-001', status: 'active', page: 1, page_size: 20 }, - }) - }) - - it('acknowledge 应调用 PUT /health/alerts/:id/acknowledge 并传递 version', async () => { - mockPut.mockResolvedValue(fakeRes) - await alertApi.acknowledge('a-001', 2) - - expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/acknowledge', { version: 2 }) - }) - - it('dismiss 应调用 PUT /health/alerts/:id/dismiss', async () => { - mockPut.mockResolvedValue(fakeRes) - await alertApi.dismiss('a-001', 1) - - expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/dismiss', { version: 1 }) - }) - - it('resolve 应调用 PUT /health/alerts/:id/resolve', async () => { - mockPut.mockResolvedValue(fakeRes) - await alertApi.resolve('a-001', 3) - - expect(mockPut).toHaveBeenCalledWith('/health/alerts/a-001/resolve', { version: 3 }) - }) -}) - -describe('alertRuleApi', () => { - const fakeRes = { data: { data: {} } } - - it('list 应调用 GET /health/alert-rules 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await alertRuleApi.list({ device_type: 'blood_pressure', page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/health/alert-rules', { - params: { device_type: 'blood_pressure', page: 1, page_size: 10 }, - }) - }) - - it('create 应调用 POST /health/alert-rules 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { - name: '血压偏高告警', - device_type: 'blood_pressure', - condition_type: 'threshold', - condition_params: { field: 'systolic', operator: '>', value: 140 }, - severity: 'high', - } - await alertRuleApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/alert-rules', req) - }) - - it('update 应调用 PUT /health/alert-rules/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { severity: 'critical', version: 1 } - await alertRuleApi.update('rule-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/alert-rules/rule-001', req) - }) - - it('deactivate 应调用 PUT /health/alert-rules/:id/deactivate', async () => { - mockPut.mockResolvedValue(fakeRes) - await alertRuleApi.deactivate('rule-001', 2) - - expect(mockPut).toHaveBeenCalledWith('/health/alert-rules/rule-001/deactivate', { version: 2 }) - }) -}) diff --git a/apps/web/src/api/health/alerts.ts b/apps/web/src/api/health/alerts.ts deleted file mode 100644 index f1f6f1c..0000000 --- a/apps/web/src/api/health/alerts.ts +++ /dev/null @@ -1,118 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface Alert { - id: string; - patient_id: string; - patient_name?: string; - rule_id: string; - severity: string; - title: string; - detail?: Record; - status: string; - acknowledged_by?: string; - acknowledged_by_name?: string; - acknowledged_at?: string; - resolved_at?: string; - created_at: string; - version: number; -} - -export interface AlertRule { - id: string; - name: string; - description?: string; - device_type: string; - condition_type: string; - condition_params: Record; - severity: string; - is_active: boolean; - apply_tags?: Record; - notify_roles: unknown[]; - cooldown_minutes: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateAlertRuleReq { - name: string; - description?: string; - device_type: string; - condition_type: string; - condition_params: Record; - severity?: string; - apply_tags?: Record; - notify_roles?: unknown[]; - cooldown_minutes?: number; -} - -export interface UpdateAlertRuleReq { - name?: string; - description?: string; - condition_params?: Record; - severity?: string; - apply_tags?: Record; - notify_roles?: unknown[]; - cooldown_minutes?: number; - version: number; -} - -// --- API --- -export const alertApi = { - list: (params?: { patient_id?: string; doctor_id?: string; status?: string; page?: number; page_size?: number }) => - client.get('/health/alerts', { params }).then((r) => r.data.data as PaginatedResponse), - - acknowledge: (id: string, version: number) => - client.put(`/health/alerts/${id}/acknowledge`, { version }).then((r) => r.data.data as Alert), - - dismiss: (id: string, version: number) => - client.put(`/health/alerts/${id}/dismiss`, { version }).then((r) => r.data.data as Alert), - - resolve: (id: string, version: number) => - client.put(`/health/alerts/${id}/resolve`, { version }).then((r) => r.data.data as Alert), -}; - -export const alertRuleApi = { - list: (params?: { device_type?: string; page?: number; page_size?: number }) => - client.get('/health/alert-rules', { params }).then((r) => r.data.data as PaginatedResponse), - - create: (data: CreateAlertRuleReq) => - client.post('/health/alert-rules', data).then((r) => r.data.data as AlertRule), - - update: (id: string, data: UpdateAlertRuleReq) => - client.put(`/health/alert-rules/${id}`, data).then((r) => r.data.data as AlertRule), - - deactivate: (id: string, version: number) => - client.put(`/health/alert-rules/${id}/deactivate`, { version }).then((r) => r.data.data as AlertRule), -}; - -// --- Critical Alerts API --- - -export interface CriticalAlert { - id: string; - patient_id: string; - patient_name?: string; - alert_type: string; - severity: string; - title: string; - detail?: Record; - status: string; - acknowledged_by?: string; - acknowledged_at?: string; - notes?: string; - created_at: string; - version: number; -} - -export const criticalAlertApi = { - list: (params?: { page?: number; page_size?: number }) => - client.get('/health/critical-alerts', { params }).then((r) => r.data.data as PaginatedResponse), - - get: (id: string) => - client.get(`/health/critical-alerts/${id}`).then((r) => r.data.data as CriticalAlert), - - acknowledge: (id: string, req: { notes?: string }) => - client.post(`/health/critical-alerts/${id}/acknowledge`, req).then((r) => r.data), -}; diff --git a/apps/web/src/api/health/api.test.ts b/apps/web/src/api/health/api.test.ts deleted file mode 100644 index 9ab5d84..0000000 --- a/apps/web/src/api/health/api.test.ts +++ /dev/null @@ -1,172 +0,0 @@ -/** - * 健康模块新增 API 函数的契约测试 - * - * 验证 dialysisApi / pointsAdminApi / healthDataApi 的日常监测与报告审核函数 - * 是否调用了正确的 HTTP 方法、URL 路径和参数。 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -// --- Mock axios client --- -// 三个被测文件都 import client from '../client',相对路径一致 -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -// 在 mock 生效后导入被测模块 -import { dialysisApi } from './dialysis' -import { pointsAdminApi } from './points' -import { healthDataApi } from './healthData' - -beforeEach(() => { - vi.clearAllMocks() -}) - -// ============================================================ -// dialysisApi -// ============================================================ -describe('dialysisApi', () => { - const fakeResponse = { data: { success: true, data: {} } } - - it('listRecords 应调用 GET /health/patients/:id/dialysis-records 并传递分页参数', async () => { - mockGet.mockResolvedValue(fakeResponse) - await dialysisApi.listRecords('p-001', { page: 2, page_size: 20 }) - - expect(mockGet).toHaveBeenCalledTimes(1) - expect(mockGet).toHaveBeenCalledWith( - '/health/patients/p-001/dialysis-records', - { params: { page: 2, page_size: 20 } }, - ) - }) - - it('getRecord 应调用 GET /health/dialysis-records/:id', async () => { - mockGet.mockResolvedValue(fakeResponse) - await dialysisApi.getRecord('rec-123') - - expect(mockGet).toHaveBeenCalledWith('/health/dialysis-records/rec-123') - }) - - it('createRecord 应调用 POST /health/dialysis-records 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeResponse) - const req = { patient_id: 'p-001', dialysis_date: '2026-04-30', dialysis_type: 'hemodialysis' } - await dialysisApi.createRecord(req) - - expect(mockPost).toHaveBeenCalledWith('/health/dialysis-records', req) - }) - - it('updateRecord 应调用 PUT /health/dialysis-records/:id 并传递请求体', async () => { - mockPut.mockResolvedValue(fakeResponse) - const req = { dry_weight: 65.0, version: 3 } - await dialysisApi.updateRecord('rec-123', req) - - expect(mockPut).toHaveBeenCalledWith('/health/dialysis-records/rec-123', req) - }) - - it('deleteRecord 应调用 DELETE /health/dialysis-records/:id 并在 body 中传递 version', async () => { - mockDelete.mockResolvedValue(undefined) - await dialysisApi.deleteRecord('rec-123', 3) - - expect(mockDelete).toHaveBeenCalledWith('/health/dialysis-records/rec-123', { - data: { version: 3 }, - }) - }) - - it('reviewRecord 应调用 PUT /health/dialysis-records/:id/review', async () => { - mockPut.mockResolvedValue(fakeResponse) - const req = { version: 2, doctor_notes: '指标正常' } - await dialysisApi.reviewRecord('rec-456', req) - - expect(mockPut).toHaveBeenCalledWith('/health/dialysis-records/rec-456/review', req) - }) -}) - -// ============================================================ -// pointsAdminApi -// ============================================================ -describe('pointsAdminApi', () => { - const fakeResponse = { data: { success: true, data: {} } } - - it('getPatientAccount 应调用 GET /health/admin/points/patients/:id/account', async () => { - mockGet.mockResolvedValue(fakeResponse) - await pointsAdminApi.getPatientAccount('p-001') - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/account') - }) - - it('listPatientTransactions 应调用 GET 并传递分页参数', async () => { - mockGet.mockResolvedValue(fakeResponse) - await pointsAdminApi.listPatientTransactions('p-001', { page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith( - '/health/admin/points/patients/p-001/transactions', - { params: { page: 1, page_size: 10 } }, - ) - }) -}) - -// ============================================================ -// healthDataApi — 日常监测 + 报告审核 -// ============================================================ -describe('healthDataApi 日常监测', () => { - const fakeResponse = { data: { success: true, data: {} } } - - it('listDailyMonitoring 应调用 GET /health/patients/:id/daily-monitoring 并传递分页参数', async () => { - mockGet.mockResolvedValue(fakeResponse) - await healthDataApi.listDailyMonitoring('p-001', { page: 1, page_size: 15 }) - - expect(mockGet).toHaveBeenCalledWith( - '/health/patients/p-001/daily-monitoring', - { params: { page: 1, page_size: 15 } }, - ) - }) - - it('createDailyMonitoring 应调用 POST /health/daily-monitoring 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeResponse) - const req = { - patient_id: 'p-001', - record_date: '2026-04-30', - weight: 70.5, - blood_sugar: 5.2, - } - await healthDataApi.createDailyMonitoring(req) - - expect(mockPost).toHaveBeenCalledWith('/health/daily-monitoring', req) - }) - - it('updateDailyMonitoring 应调用 PUT /health/daily-monitoring/:id 并传递请求体', async () => { - mockPut.mockResolvedValue(fakeResponse) - const req = { weight: 71.0, version: 1 } - await healthDataApi.updateDailyMonitoring('dm-123', req) - - expect(mockPut).toHaveBeenCalledWith('/health/daily-monitoring/dm-123', req) - }) - - it('deleteDailyMonitoring 应调用 DELETE /health/daily-monitoring/:id 并在 body 中传递 version', async () => { - mockDelete.mockResolvedValue(undefined) - await healthDataApi.deleteDailyMonitoring('dm-123', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/daily-monitoring/dm-123', { - data: { version: 2 }, - }) - }) - - it('reviewLabReport 应调用 PUT /health/patients/:pid/lab-reports/:rid/review', async () => { - mockPut.mockResolvedValue(fakeResponse) - const req = { version: 1, doctor_notes: '指标略有异常,建议复查' } - await healthDataApi.reviewLabReport('p-001', 'lr-456', req) - - expect(mockPut).toHaveBeenCalledWith( - '/health/patients/p-001/lab-reports/lr-456/review', - req, - ) - }) -}) diff --git a/apps/web/src/api/health/appointments.test.ts b/apps/web/src/api/health/appointments.test.ts deleted file mode 100644 index 49fc27d..0000000 --- a/apps/web/src/api/health/appointments.test.ts +++ /dev/null @@ -1,106 +0,0 @@ -/** - * appointments API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { appointmentApi } from './appointments' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('appointmentApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/appointments 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await appointmentApi.list({ page: 1, page_size: 20, status: 'confirmed', doctor_id: 'd-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/appointments', { - params: { page: 1, page_size: 20, status: 'confirmed', doctor_id: 'd-001' }, - }) - }) - - it('get 应调用 GET /health/appointments/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await appointmentApi.get('appt-001') - - expect(mockGet).toHaveBeenCalledWith('/health/appointments/appt-001') - }) - - it('create 应调用 POST /health/appointments 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { - patient_id: 'p-001', - doctor_id: 'd-001', - appointment_date: '2026-05-10', - start_time: '09:00', - end_time: '09:30', - } - await appointmentApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/appointments', req) - }) - - it('updateStatus 应调用 PUT /health/appointments/:id/status', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { status: 'cancelled', cancel_reason: '时间冲突', version: 2 } - await appointmentApi.updateStatus('appt-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/appointments/appt-001/status', req) - }) - - it('listSchedules 应调用 GET /health/doctor-schedules 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await appointmentApi.listSchedules({ doctor_id: 'd-001', date: '2026-05-10' }) - - expect(mockGet).toHaveBeenCalledWith('/health/doctor-schedules', { - params: { doctor_id: 'd-001', date: '2026-05-10' }, - }) - }) - - it('createSchedule 应调用 POST /health/doctor-schedules', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { - doctor_id: 'd-001', - schedule_date: '2026-05-10', - start_time: '08:00', - end_time: '12:00', - max_appointments: 10, - } - await appointmentApi.createSchedule(req) - - expect(mockPost).toHaveBeenCalledWith('/health/doctor-schedules', req) - }) - - it('updateSchedule 应调用 PUT /health/doctor-schedules/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { max_appointments: 15, version: 1 } - await appointmentApi.updateSchedule('sch-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/doctor-schedules/sch-001', req) - }) - - it('calendar 应调用 GET /health/doctor-schedules/calendar', async () => { - mockGet.mockResolvedValue(fakeRes) - await appointmentApi.calendar({ start_date: '2026-05-01', end_date: '2026-05-31', doctor_id: 'd-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/doctor-schedules/calendar', { - params: { start_date: '2026-05-01', end_date: '2026-05-31', doctor_id: 'd-001' }, - }) - }) -}) diff --git a/apps/web/src/api/health/appointments.ts b/apps/web/src/api/health/appointments.ts deleted file mode 100644 index 5488412..0000000 --- a/apps/web/src/api/health/appointments.ts +++ /dev/null @@ -1,164 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface Appointment { - id: string; - patient_id: string; - doctor_id?: string; - appointment_type: string; - appointment_date: string; - start_time: string; - end_time: string; - status: string; - cancel_reason?: string; - notes?: string; - patient_name?: string; - doctor_name?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateAppointmentReq { - patient_id: string; - doctor_id?: string; - appointment_type?: string; - appointment_date: string; - start_time: string; - end_time: string; - notes?: string; -} - -export interface UpdateAppointmentStatusReq { - status: string; - cancel_reason?: string; -} - -export interface Schedule { - id: string; - doctor_id: string; - schedule_date: string; - period_type: string; - start_time: string; - end_time: string; - max_appointments: number; - current_appointments: number; - status: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateScheduleReq { - doctor_id: string; - schedule_date: string; - period_type?: string; - start_time: string; - end_time: string; - max_appointments: number; -} - -export interface UpdateScheduleReq { - start_time?: string; - end_time?: string; - max_appointments?: number; - status?: string; -} - -export interface CalendarDay { - date: string; - schedules: Schedule[]; -} - -// --- API --- -export const appointmentApi = { - list: async (params: { - page?: number; - page_size?: number; - status?: string; - patient_id?: string; - doctor_id?: string; - date?: string; - search?: string; - appointment_type?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/appointments', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: Appointment; - }>(`/health/appointments/${id}`); - return data.data; - }, - - create: async (req: CreateAppointmentReq) => { - const { data } = await client.post<{ - success: boolean; - data: Appointment; - }>('/health/appointments', req); - return data.data; - }, - - updateStatus: async ( - id: string, - req: UpdateAppointmentStatusReq & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: Appointment; - }>(`/health/appointments/${id}/status`, req); - return data.data; - }, - - // Schedules - listSchedules: async (params: { - page?: number; - page_size?: number; - doctor_id?: string; - date?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/doctor-schedules', { params }); - return data.data; - }, - - createSchedule: async (req: CreateScheduleReq) => { - const { data } = await client.post<{ - success: boolean; - data: Schedule; - }>('/health/doctor-schedules', req); - return data.data; - }, - - updateSchedule: async ( - id: string, - req: UpdateScheduleReq & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: Schedule; - }>(`/health/doctor-schedules/${id}`, req); - return data.data; - }, - - calendar: async (params: { - start_date: string; - end_date: string; - doctor_id?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: CalendarDay[]; - }>('/health/doctor-schedules/calendar', { params }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/articles.test.ts b/apps/web/src/api/health/articles.test.ts deleted file mode 100644 index 3cb9315..0000000 --- a/apps/web/src/api/health/articles.test.ts +++ /dev/null @@ -1,173 +0,0 @@ -/** - * articles API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { articleApi, articleCategoryApi, articleTagApi } from './articles' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('articleApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/articles 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await articleApi.list({ page: 1, page_size: 10, status: 'published', category_id: 'cat-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/articles', { - params: { page: 1, page_size: 10, status: 'published', category_id: 'cat-001' }, - }) - }) - - it('get 应调用 GET /health/articles/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await articleApi.get('art-001') - - expect(mockGet).toHaveBeenCalledWith('/health/articles/art-001') - }) - - it('create 应调用 POST /health/articles', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { title: '健康饮食指南', content: '正文内容', content_type: 'markdown' as const } - await articleApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/articles', req) - }) - - it('update 应调用 PUT /health/articles/:id 并传递请求体', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { title: '健康饮食指南(修订)', version: 1 } - await articleApi.update('art-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/articles/art-001', req) - }) - - it('delete 应调用 DELETE /health/articles/:id', async () => { - mockDelete.mockResolvedValue({ data: { success: true, data: null } }) - await articleApi.delete('art-001', 1) - - expect(mockDelete).toHaveBeenCalledWith('/health/articles/art-001') - }) - - it('submit 应调用 POST /health/articles/:id/submit 并传递 version', async () => { - mockPost.mockResolvedValue(fakeRes) - await articleApi.submit('art-001', 2) - - expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/submit', { version: 2 }) - }) - - it('approve 应调用 POST /health/articles/:id/approve', async () => { - mockPost.mockResolvedValue(fakeRes) - await articleApi.approve('art-001', 2) - - expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/approve', { version: 2 }) - }) - - it('reject 应调用 POST /health/articles/:id/reject 并传递 review_note', async () => { - mockPost.mockResolvedValue(fakeRes) - await articleApi.reject('art-001', 2, '内容需要修改') - - expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/reject', { - version: 2, - review_note: '内容需要修改', - }) - }) - - it('unpublish 应调用 POST /health/articles/:id/unpublish', async () => { - mockPost.mockResolvedValue(fakeRes) - await articleApi.unpublish('art-001', 3) - - expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/unpublish', { version: 3 }) - }) - - it('view 应调用 POST /health/articles/:id/view', async () => { - mockPost.mockResolvedValue(fakeRes) - await articleApi.view('art-001') - - expect(mockPost).toHaveBeenCalledWith('/health/articles/art-001/view') - }) -}) - -describe('articleCategoryApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/article-categories', async () => { - mockGet.mockResolvedValue(fakeRes) - await articleCategoryApi.list() - - expect(mockGet).toHaveBeenCalledWith('/health/article-categories') - }) - - it('create 应调用 POST /health/article-categories', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '营养健康', sort_order: 1 } - await articleCategoryApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/article-categories', req) - }) - - it('update 应调用 PUT /health/article-categories/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { name: '营养健康(更新)' } - await articleCategoryApi.update('cat-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/article-categories/cat-001', req) - }) - - it('delete 应调用 DELETE /health/article-categories/:id', async () => { - mockDelete.mockResolvedValue({ data: { success: true, data: null } }) - await articleCategoryApi.delete('cat-001') - - expect(mockDelete).toHaveBeenCalledWith('/health/article-categories/cat-001') - }) -}) - -describe('articleTagApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/article-tags', async () => { - mockGet.mockResolvedValue(fakeRes) - await articleTagApi.list() - - expect(mockGet).toHaveBeenCalledWith('/health/article-tags') - }) - - it('create 应调用 POST /health/article-tags', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '高血压', color: '#ff0000' } - await articleTagApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/article-tags', req) - }) - - it('update 应调用 PUT /health/article-tags/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { name: '高血压管理', version: 1 } - await articleTagApi.update('tag-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/article-tags/tag-001', req) - }) - - it('delete 应调用 DELETE /health/article-tags/:id 并在 body 传递 version', async () => { - mockDelete.mockResolvedValue({ data: { success: true, data: null } }) - await articleTagApi.delete('tag-001', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/article-tags/tag-001', { data: { version: 2 } }) - }) -}) diff --git a/apps/web/src/api/health/articles.ts b/apps/web/src/api/health/articles.ts deleted file mode 100644 index 7dd070c..0000000 --- a/apps/web/src/api/health/articles.ts +++ /dev/null @@ -1,283 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Article Types --- - -export type ArticleStatus = 'draft' | 'pending_review' | 'published' | 'rejected'; -export type ArticleContentType = 'rich_text' | 'markdown'; - -export interface ArticleListItem { - id: string; - title: string; - summary?: string; - cover_image?: string; - content_type: ArticleContentType; - status: ArticleStatus; - slug?: string; - category_id?: string; - category_name?: string; - tags?: ArticleTagItem[]; - author?: string; - reviewed_by?: string; - reviewed_at?: string; - review_note?: string; - view_count: number; - sort_order: number; - is_public: boolean; - published_at?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface Article extends ArticleListItem { - content?: string; -} - -export interface CreateArticleReq { - title: string; - summary?: string; - content?: string; - content_type?: ArticleContentType; - cover_image?: string; - slug?: string; - category_id?: string; - tag_ids?: string[]; - sort_order?: number; - is_public?: boolean; -} - -export interface UpdateArticleReq { - title?: string; - summary?: string; - content?: string; - content_type?: ArticleContentType; - cover_image?: string; - slug?: string; - category_id?: string; - tag_ids?: string[]; - sort_order?: number; - is_public?: boolean; - version: number; -} - -export interface ArticleListParams { - page?: number; - page_size?: number; - status?: ArticleStatus; - category_id?: string; - tag_id?: string; - keyword?: string; -} - -// --- Category Types --- - -export interface ArticleCategory { - id: string; - name: string; - slug?: string; - parent_id?: string; - parent_name?: string; - sort_order: number; - description?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateCategoryReq { - name: string; - slug?: string; - parent_id?: string; - sort_order?: number; - description?: string; -} - -export interface UpdateCategoryReq { - name?: string; - slug?: string; - parent_id?: string; - sort_order?: number; - description?: string; -} - -// --- Tag Types --- - -export interface ArticleTagItem { - id: string; - name: string; - slug?: string; - color?: string; - created_at: string; - version?: number; -} - -export interface CreateTagReq { - name: string; - slug?: string; - color?: string; -} - -// --- Article API --- - -export const articleApi = { - list: async (params: ArticleListParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/articles', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}`); - return data.data; - }, - - create: async (req: CreateArticleReq) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>('/health/articles', req); - return data.data; - }, - - update: async (id: string, req: UpdateArticleReq) => { - const { data } = await client.put<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/articles/${id}`, { data: { version } }); - return data.data; - }, - - submit: async (id: string, version: number) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}/submit`, { version }); - return data.data; - }, - - approve: async (id: string, version: number) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}/approve`, { version }); - return data.data; - }, - - reject: async (id: string, version: number, review_note: string) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}/reject`, { version, review_note }); - return data.data; - }, - - unpublish: async (id: string, version: number) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}/unpublish`, { version }); - return data.data; - }, - - view: async (id: string) => { - const { data } = await client.post<{ - success: boolean; - data: Article; - }>(`/health/articles/${id}/view`); - return data.data; - }, - - listRevisions: async (id: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse>; - }>(`/health/articles/${id}/revisions`, { params }); - return data.data; - }, -}; - -// --- Category API --- - -export const articleCategoryApi = { - list: async () => { - const { data } = await client.get<{ - success: boolean; - data: ArticleCategory[]; - }>('/health/article-categories'); - return data.data; - }, - - create: async (req: CreateCategoryReq) => { - const { data } = await client.post<{ - success: boolean; - data: ArticleCategory; - }>('/health/article-categories', req); - return data.data; - }, - - update: async (id: string, req: UpdateCategoryReq) => { - const { data } = await client.put<{ - success: boolean; - data: ArticleCategory; - }>(`/health/article-categories/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/article-categories/${id}`, { data: { version } }); - return data.data; - }, -}; - -// --- Tag API --- - -export const articleTagApi = { - list: async () => { - const { data } = await client.get<{ - success: boolean; - data: ArticleTagItem[]; - }>('/health/article-tags'); - return data.data; - }, - - create: async (req: CreateTagReq) => { - const { data } = await client.post<{ - success: boolean; - data: ArticleTagItem; - }>('/health/article-tags', req); - return data.data; - }, - - update: async (id: string, req: { name: string; version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: ArticleTagItem; - }>(`/health/article-tags/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/article-tags/${id}`, { data: { version } }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/banners.ts b/apps/web/src/api/health/banners.ts deleted file mode 100644 index 937e2ec..0000000 --- a/apps/web/src/api/health/banners.ts +++ /dev/null @@ -1,116 +0,0 @@ -import client from '../client'; - -// --------------------------------------------------------------------------- -// 轮播图类型 -// --------------------------------------------------------------------------- - -export interface BannerItem { - id: string; - tenant_id: string; - media_item_id: string; - title?: string; - subtitle?: string; - link_type?: string; - link_target?: string; - sort_order: number; - status: string; - start_time?: string; - end_time?: string; - image_url?: string; - thumbnail_url?: string; - media_deleted: boolean; - created_at: string; - updated_at: string; - created_by?: string; - updated_by?: string; - version: number; -} - -export interface CreateBannerReq { - media_item_id: string; - title?: string; - subtitle?: string; - link_type?: string; - link_target?: string; - sort_order?: number; - status?: string; - start_time?: string; - end_time?: string; -} - -export interface UpdateBannerReq { - media_item_id?: string; - title?: string; - subtitle?: string; - link_type?: string; - link_target?: string; - sort_order?: number; - status?: string; - start_time?: string; - end_time?: string; - version: number; -} - -export interface SortBannerReq { - items: Array<{ id: string; sort_order: number }>; -} - -// --------------------------------------------------------------------------- -// 轮播图 API -// --------------------------------------------------------------------------- - -export const bannerApi = { - /** 获取轮播图列表(可按状态筛选) */ - list: async (status?: string) => { - const { data } = await client.get<{ - success: boolean; - data: BannerItem[]; - }>('/health/banners', { params: status ? { status } : undefined }); - return data.data; - }, - - /** 获取单个轮播图 */ - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: BannerItem; - }>(`/health/banners/${id}`); - return data.data; - }, - - /** 创建轮播图 */ - create: async (req: CreateBannerReq) => { - const { data } = await client.post<{ - success: boolean; - data: BannerItem; - }>('/health/banners', req); - return data.data; - }, - - /** 更新轮播图 */ - update: async (id: string, req: UpdateBannerReq) => { - const { data } = await client.put<{ - success: boolean; - data: BannerItem; - }>(`/health/banners/${id}`, req); - return data.data; - }, - - /** 删除轮播图 */ - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/banners/${id}`, { data: { version } }); - return data.data; - }, - - /** 轮播图排序 */ - sort: async (req: SortBannerReq) => { - const { data } = await client.put<{ - success: boolean; - data: null; - }>('/health/banners/sort', req); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/bleGateways.ts b/apps/web/src/api/health/bleGateways.ts deleted file mode 100644 index 4c1c8b5..0000000 --- a/apps/web/src/api/health/bleGateways.ts +++ /dev/null @@ -1,170 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface BleGateway { - id: string; - tenant_id: string; - gateway_id: string; - name: string; - status: string; - firmware_version?: string; - ip_address?: string; - last_heartbeat_at?: string; - metadata?: Record; - created_at: string; - updated_at: string; - version: number; - api_key?: string; - patient_count?: number; -} - -export interface GatewayBinding { - id: string; - tenant_id: string; - gateway_id: string; - patient_id: string; - peripheral_mac?: string; - device_type?: string; - status: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateBleGatewayReq { - gateway_id: string; - name: string; - firmware_version?: string; - metadata?: Record; -} - -export interface UpdateBleGatewayReq { - name?: string; - status?: string; - firmware_version?: string; - metadata?: Record; -} - -export interface ListBleGatewaysParams { - page?: number; - page_size?: number; - status?: string; -} - -export interface CreateBindingReq { - patient_id: string; - peripheral_mac?: string; - device_type?: string; -} - -export interface BatchBindReq { - bindings: CreateBindingReq[]; -} - -// --- Constants --- - -export const GATEWAY_STATUS_OPTIONS = [ - { label: '在线', value: 'online' }, - { label: '离线', value: 'offline' }, - { label: '未激活', value: 'inactive' }, - { label: '已禁用', value: 'disabled' }, -]; - -export const GATEWAY_STATUS_COLOR: Record = { - online: 'green', - offline: 'red', - inactive: 'default', - disabled: 'error', -}; - -export const GATEWAY_STATUS_LABEL: Record = Object.fromEntries( - GATEWAY_STATUS_OPTIONS.map((o) => [o.value, o.label]), -); - -export const BINDING_STATUS_COLOR: Record = { - active: 'green', - inactive: 'default', - unbound: 'error', -}; - -// --- API --- - -export const bleGatewayApi = { - // --- Gateways --- - - list: async (params?: ListBleGatewaysParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/ble-gateways', { params }); - return data.data; - }, - - get: async (gatewayId: string) => { - const { data } = await client.get<{ - success: boolean; - data: BleGateway; - }>(`/health/ble-gateways/${gatewayId}`); - return data.data; - }, - - create: async (req: CreateBleGatewayReq) => { - const { data } = await client.post<{ - success: boolean; - data: BleGateway; - }>('/health/ble-gateways', req); - return data.data; - }, - - update: async (gatewayId: string, req: UpdateBleGatewayReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: BleGateway; - }>(`/health/ble-gateways/${gatewayId}`, req); - return data.data; - }, - - delete: async (gatewayId: string, version: number) => { - await client.delete(`/health/ble-gateways/${gatewayId}`, { data: { version } }); - }, - - regenerateKey: async (gatewayId: string) => { - const { data } = await client.post<{ - success: boolean; - data: BleGateway; - }>(`/health/ble-gateways/${gatewayId}/regenerate-key`); - return data.data; - }, - - // --- Bindings --- - - listBindings: async (gatewayId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/ble-gateways/${gatewayId}/bindings`, { params }); - return data.data; - }, - - bindPatient: async (gatewayId: string, req: CreateBindingReq) => { - const { data } = await client.post<{ - success: boolean; - data: GatewayBinding; - }>(`/health/ble-gateways/${gatewayId}/bindings`, req); - return data.data; - }, - - batchBind: async (gatewayId: string, req: BatchBindReq) => { - const { data } = await client.post<{ - success: boolean; - data: GatewayBinding[]; - }>(`/health/ble-gateways/${gatewayId}/bindings/batch`, req); - return data.data; - }, - - unbindPatient: async (gatewayId: string, bindingId: string, version: number) => { - await client.delete(`/health/ble-gateways/${gatewayId}/bindings/${bindingId}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/carePlans.ts b/apps/web/src/api/health/carePlans.ts deleted file mode 100644 index 6a8b2c5..0000000 --- a/apps/web/src/api/health/carePlans.ts +++ /dev/null @@ -1,245 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface CarePlan { - id: string; - patient_id: string; - plan_type: string; - status: string; - title: string; - goals?: Record; - start_date?: string; - end_date?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CarePlanItem { - id: string; - plan_id: string; - item_type: string; - title: string; - description?: string; - status: string; - schedule?: string; - sort_order?: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CarePlanOutcome { - id: string; - plan_id: string; - item_id?: string; - metric: string; - baseline_value: string; - target_value: string; - current_value?: string; - measured_at?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateCarePlanReq { - patient_id: string; - plan_type: string; - title: string; - goals?: Record; - start_date?: string; - end_date?: string; - notes?: string; -} - -export interface UpdateCarePlanReq { - plan_type?: string; - title?: string; - status?: string; - goals?: Record; - start_date?: string; - end_date?: string; - notes?: string; -} - -export interface CreateCarePlanItemReq { - item_type: string; - title: string; - description?: string; - schedule?: string; - sort_order?: number; -} - -export interface UpdateCarePlanItemReq { - item_type?: string; - title?: string; - description?: string; - status?: string; - schedule?: string; - sort_order?: number; -} - -export interface CreateCarePlanOutcomeReq { - item_id?: string; - metric: string; - baseline_value: string; - target_value: string; - current_value?: string; - measured_at?: string; - notes?: string; -} - -export interface UpdateCarePlanOutcomeReq { - item_id?: string; - metric?: string; - baseline_value?: string; - target_value?: string; - current_value?: string; - measured_at?: string; - notes?: string; -} - -export interface ListCarePlansParams { - page?: number; - page_size?: number; - patient_id?: string; - plan_type?: string; - status?: string; -} - -// --- Constants --- - -export const PLAN_TYPE_OPTIONS = [ - { label: '血液透析', value: 'hemodialysis' }, - { label: '腹膜透析', value: 'peritoneal' }, - { label: '慢性病管理', value: 'chronic_disease' }, - { label: '康复计划', value: 'rehabilitation' }, -]; - -export const PLAN_STATUS_OPTIONS = [ - { label: '草稿', value: 'draft' }, - { label: '进行中', value: 'active' }, - { label: '已完成', value: 'completed' }, - { label: '已取消', value: 'cancelled' }, -]; - -export const ITEM_TYPE_OPTIONS = [ - { label: '药物干预', value: 'medication' }, - { label: '饮食管理', value: 'diet' }, - { label: '运动计划', value: 'exercise' }, - { label: '监测项目', value: 'monitoring' }, - { label: '教育指导', value: 'education' }, - { label: '其他', value: 'other' }, -]; - -export const PLAN_STATUS_COLOR: Record = { - draft: 'default', - active: 'processing', - completed: 'success', - cancelled: 'error', -}; - -// --- API --- - -export const carePlanApi = { - list: async (params: ListCarePlansParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/care-plans', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: CarePlan; - }>(`/health/care-plans/${id}`); - return data.data; - }, - - create: async (req: CreateCarePlanReq) => { - const { data } = await client.post<{ - success: boolean; - data: CarePlan; - }>('/health/care-plans', req); - return data.data; - }, - - update: async (id: string, req: UpdateCarePlanReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: CarePlan; - }>(`/health/care-plans/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/care-plans/${id}`, { data: { version } }); - }, - - // --- Items --- - - listItems: async (planId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/care-plans/${planId}/items`, { params }); - return data.data; - }, - - createItem: async (planId: string, req: CreateCarePlanItemReq) => { - const { data } = await client.post<{ - success: boolean; - data: CarePlanItem; - }>(`/health/care-plans/${planId}/items`, req); - return data.data; - }, - - updateItem: async (planId: string, itemId: string, req: UpdateCarePlanItemReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: CarePlanItem; - }>(`/health/care-plans/${planId}/items/${itemId}`, req); - return data.data; - }, - - deleteItem: async (planId: string, itemId: string, version: number) => { - await client.delete(`/health/care-plans/${planId}/items/${itemId}`, { data: { version } }); - }, - - // --- Outcomes --- - - listOutcomes: async (planId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/care-plans/${planId}/outcomes`, { params }); - return data.data; - }, - - createOutcome: async (planId: string, req: CreateCarePlanOutcomeReq) => { - const { data } = await client.post<{ - success: boolean; - data: CarePlanOutcome; - }>(`/health/care-plans/${planId}/outcomes`, req); - return data.data; - }, - - updateOutcome: async (planId: string, outcomeId: string, req: UpdateCarePlanOutcomeReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: CarePlanOutcome; - }>(`/health/care-plans/${planId}/outcomes/${outcomeId}`, req); - return data.data; - }, - - deleteOutcome: async (planId: string, outcomeId: string, version: number) => { - await client.delete(`/health/care-plans/${planId}/outcomes/${outcomeId}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/consents.ts b/apps/web/src/api/health/consents.ts deleted file mode 100644 index 94ae960..0000000 --- a/apps/web/src/api/health/consents.ts +++ /dev/null @@ -1,92 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface Consent { - id: string; - patient_id: string; - consent_type: string; - consent_scope: string; - status: string; - granted_at?: string; - revoked_at?: string; - expiry_date?: string; - consent_method?: string; - witness_name?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateConsentReq { - patient_id: string; - consent_type: string; - consent_scope: string; - expiry_date?: string; - consent_method?: string; - witness_name?: string; - notes?: string; -} - -export interface RevokeConsentReq { - notes?: string; - version: number; -} - -// --- Constants --- - -export const CONSENT_TYPE_OPTIONS = [ - { label: '治疗同意', value: 'treatment' }, - { label: '数据共享', value: 'data_sharing' }, - { label: '隐私政策', value: 'privacy' }, - { label: '研究参与', value: 'research' }, -]; - -export const CONSENT_SCOPE_OPTIONS = [ - { label: '全部', value: 'all' }, - { label: '健康数据', value: 'health_data' }, - { label: '基本信息', value: 'basic_info' }, - { label: '体检报告', value: 'examination' }, -]; - -export const CONSENT_STATUS_COLOR: Record = { - active: 'green', - revoked: 'red', - expired: 'default', -}; - -export const CONSENT_STATUS_LABEL: Record = { - active: '生效中', - revoked: '已撤销', - expired: '已过期', -}; - -// --- API --- - -export const consentApi = { - list: async (patientId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/consents`, { params }); - return data.data; - }, - - grant: async (req: CreateConsentReq) => { - const { data } = await client.post<{ - success: boolean; - data: Consent; - }>('/health/consents', req); - return data.data; - }, - - revoke: async (consentId: string, req: RevokeConsentReq) => { - const { data } = await client.put<{ - success: boolean; - data: Consent; - }>(`/health/consents/${consentId}/revoke`, req); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/consultations.test.ts b/apps/web/src/api/health/consultations.test.ts deleted file mode 100644 index 275f53b..0000000 --- a/apps/web/src/api/health/consultations.test.ts +++ /dev/null @@ -1,98 +0,0 @@ -/** - * consultations API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { consultationApi } from './consultations' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('consultationApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listSessions 应调用 GET /health/consultation-sessions 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await consultationApi.listSessions({ page: 1, page_size: 20, status: 'active', patient_id: 'p-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions', { - params: { page: 1, page_size: 20, status: 'active', patient_id: 'p-001' }, - }) - }) - - it('createSession 应调用 POST /health/consultation-sessions', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { patient_id: 'p-001', doctor_id: 'd-001', consultation_type: 'online' } - await consultationApi.createSession(req) - - expect(mockPost).toHaveBeenCalledWith('/health/consultation-sessions', req) - }) - - it('getSession 应调用 GET /health/consultation-sessions/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await consultationApi.getSession('sess-001') - - expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions/sess-001') - }) - - it('closeSession 应调用 PUT /health/consultation-sessions/:id/close', async () => { - mockPut.mockResolvedValue(fakeRes) - await consultationApi.closeSession('sess-001', { version: 1 }) - - expect(mockPut).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/close', { version: 1 }) - }) - - it('listMessages 应调用 GET /health/consultation-sessions/:id/messages 并传递分页参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await consultationApi.listMessages('sess-001', { page: 2, page_size: 50, after_id: 'msg-100' }) - - expect(mockGet).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/messages', { - params: { page: 2, page_size: 50, after_id: 'msg-100' }, - }) - }) - - it('createMessage 应调用 POST /health/consultation-messages', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { session_id: 'sess-001', content_type: 'text', content: '你好' } - await consultationApi.createMessage(req) - - expect(mockPost).toHaveBeenCalledWith('/health/consultation-messages', req) - }) - - it('createFollowUpFromSession 应调用 POST /health/consultation-sessions/:id/follow-up', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { follow_up_type: 'phone', planned_date: '2026-06-01' } - await consultationApi.createFollowUpFromSession('sess-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/follow-up', req) - }) - - it('triggerAiAnalysisFromSession 应调用 POST /health/consultation-sessions/:id/ai-analysis', async () => { - mockPost.mockResolvedValue(fakeRes) - await consultationApi.triggerAiAnalysisFromSession('sess-001') - - expect(mockPost).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/ai-analysis', {}) - }) - - it('triggerAiAnalysisFromSession 传入 analysis_type 时应携带参数', async () => { - mockPost.mockResolvedValue(fakeRes) - await consultationApi.triggerAiAnalysisFromSession('sess-001', { analysis_type: 'trend' }) - - expect(mockPost).toHaveBeenCalledWith('/health/consultation-sessions/sess-001/ai-analysis', { analysis_type: 'trend' }) - }) -}) diff --git a/apps/web/src/api/health/consultations.ts b/apps/web/src/api/health/consultations.ts deleted file mode 100644 index 2bfc355..0000000 --- a/apps/web/src/api/health/consultations.ts +++ /dev/null @@ -1,185 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface Session { - id: string; - patient_id: string; - doctor_id?: string; - patient_name?: string; - doctor_name?: string; - consultation_type: string; - status: string; - last_message_at?: string; - unread_count_patient: number; - unread_count_doctor: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateSessionReq { - patient_id: string; - doctor_id?: string; - consultation_type?: string; -} - -export interface Message { - id: string; - session_id: string; - sender_id: string; - sender_role: string; - content_type: string; - content: string; - is_read: boolean; - created_at: string; -} - -export interface CreateMessageReq { - session_id: string; - content_type?: string; - content: string; -} - -// --- 咨询联动请求类型 --- -export interface CreateFollowUpFromConsultationReq { - follow_up_type: string; - planned_date: string; - assigned_to?: string; - content_template?: string; -} - -export interface FollowUpFromConsultationResp { - task_id: string; - session_id: string; - patient_id: string; -} - -export interface TriggerAiAnalysisReq { - analysis_type?: string; -} - -export interface AiAnalysisTriggeredResp { - session_id: string; - patient_id: string; - analysis_type: string; -} - -// --- API --- -export const consultationApi = { - listSessions: async (params: { - page?: number; - page_size?: number; - status?: string; - patient_id?: string; - doctor_id?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/consultation-sessions', { params }); - return data.data; - }, - - createSession: async (req: CreateSessionReq) => { - const { data } = await client.post<{ - success: boolean; - data: Session; - }>('/health/consultation-sessions', req); - return data.data; - }, - - getSession: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: Session; - }>(`/health/consultation-sessions/${id}`); - return data.data; - }, - - closeSession: async ( - id: string, - req: { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: Session; - }>(`/health/consultation-sessions/${id}/close`, req); - return data.data; - }, - - listMessages: async ( - sessionId: string, - params: { page?: number; page_size?: number; after_id?: string }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/consultation-sessions/${sessionId}/messages`, { params }); - return data.data; - }, - - createMessage: async (req: CreateMessageReq) => { - const { data } = await client.post<{ - success: boolean; - data: Message; - }>('/health/consultation-messages', req); - return data.data; - }, - - pollMessages: async ( - sessionId: string, - afterId?: string, - ) => { - const { data } = await client.get<{ - success: boolean; - data: Message[]; - }>(`/health/consultation-sessions/${sessionId}/messages/poll`, { - params: { after_id: afterId, timeout: 25 }, - timeout: 30000, - }); - return data.data; - }, - - markSessionRead: async (id: string) => { - await client.put(`/health/consultation-sessions/${id}/read`); - }, - - exportSessions: async (params?: { - status?: string; - patient_id?: string; - doctor_id?: string; - page?: number; - page_size?: number; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/consultation-sessions/export', { params }); - return data.data; - }, - - /** 从咨询会话创建随访任务 */ - createFollowUpFromSession: async ( - sessionId: string, - req: CreateFollowUpFromConsultationReq, - ) => { - const { data } = await client.post<{ - success: boolean; - data: FollowUpFromConsultationResp; - }>(`/health/consultation-sessions/${sessionId}/follow-up`, req); - return data.data; - }, - - /** 从咨询会话触发 AI 分析 */ - triggerAiAnalysisFromSession: async ( - sessionId: string, - req?: TriggerAiAnalysisReq, - ) => { - const { data } = await client.post<{ - success: boolean; - data: AiAnalysisTriggeredResp; - }>(`/health/consultation-sessions/${sessionId}/ai-analysis`, req ?? {}); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/criticalValueThresholds.ts b/apps/web/src/api/health/criticalValueThresholds.ts deleted file mode 100644 index 55f75a4..0000000 --- a/apps/web/src/api/health/criticalValueThresholds.ts +++ /dev/null @@ -1,110 +0,0 @@ -import client from '../client'; - -// --- Types --- - -export interface CriticalValueThreshold { - id: string; - tenant_id: string; - indicator: string; - direction: string; - threshold_value: number; - level: string; - department?: string; - age_min?: number; - age_max?: number; - is_active: boolean; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateThresholdReq { - indicator: string; - direction: string; - threshold_value: number; - level?: string; - department?: string; - age_min?: number; - age_max?: number; -} - -export interface UpdateThresholdReq { - threshold_value: number; - level?: string; - department?: string; - age_min?: number; - age_max?: number; - version: number; -} - -// --- Constants --- - -export const INDICATOR_OPTIONS = [ - { label: '收缩压', value: 'systolic_bp' }, - { label: '舒张压', value: 'diastolic_bp' }, - { label: '心率', value: 'heart_rate' }, - { label: '血糖', value: 'blood_sugar' }, - { label: '空腹血糖', value: 'blood_sugar_fasting' }, - { label: '餐后血糖', value: 'blood_sugar_postprandial' }, - { label: '血氧', value: 'blood_oxygen' }, - { label: '体温', value: 'temperature' }, -]; - -export const DIRECTION_OPTIONS = [ - { label: '偏高', value: 'high' }, - { label: '偏低', value: 'low' }, -]; - -export const LEVEL_OPTIONS = [ - { label: '危急', value: 'critical' }, - { label: '警告', value: 'warning' }, -]; - -export const LEVEL_COLOR: Record = { - critical: 'red', - warning: 'orange', -}; - -export const INDICATOR_LABEL: Record = Object.fromEntries( - INDICATOR_OPTIONS.map((o) => [o.value, o.label]), -); - -export const DIRECTION_LABEL: Record = Object.fromEntries( - DIRECTION_OPTIONS.map((o) => [o.value, o.label]), -); - -export const LEVEL_LABEL: Record = Object.fromEntries( - LEVEL_OPTIONS.map((o) => [o.value, o.label]), -); - -// --- API --- - -export const criticalValueThresholdApi = { - list: async () => { - const { data } = await client.get<{ - success: boolean; - data: CriticalValueThreshold[]; - }>('/health/critical-value-thresholds'); - return data.data; - }, - - create: async (req: CreateThresholdReq) => { - const { data } = await client.post<{ - success: boolean; - data: CriticalValueThreshold; - }>('/health/critical-value-thresholds', req); - return data.data; - }, - - update: async (id: string, req: UpdateThresholdReq) => { - const { data } = await client.put<{ - success: boolean; - data: CriticalValueThreshold; - }>(`/health/critical-value-thresholds/${id}`, req); - return data.data; - }, - - delete: async (id: string) => { - await client.delete(`/health/critical-value-thresholds/${id}`); - }, -}; diff --git a/apps/web/src/api/health/dashboard.test.ts b/apps/web/src/api/health/dashboard.test.ts deleted file mode 100644 index a58b82d..0000000 --- a/apps/web/src/api/health/dashboard.test.ts +++ /dev/null @@ -1,105 +0,0 @@ -/** - * dashboard + actionInbox API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { dashboardApi } from './dashboard' -import { actionInboxApi } from './actionInbox' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('dashboardApi', () => { - const fakeRes = { data: { data: {} } } - - it('getSystemHealth 应调用 GET /health/admin/system-health', async () => { - mockGet.mockResolvedValue(fakeRes) - await dashboardApi.getSystemHealth() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/system-health') - }) - - it('getUserActivity 应调用 GET /health/admin/user-activity', async () => { - mockGet.mockResolvedValue(fakeRes) - await dashboardApi.getUserActivity() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/user-activity') - }) - - it('getModuleStatus 应调用 GET /health/admin/modules', async () => { - mockGet.mockResolvedValue(fakeRes) - await dashboardApi.getModuleStatus() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/modules') - }) - - it('getPointsRecentActivity 应调用 GET /health/points/recent-activity', async () => { - mockGet.mockResolvedValue(fakeRes) - await dashboardApi.getPointsRecentActivity() - - expect(mockGet).toHaveBeenCalledWith('/health/points/recent-activity') - }) - - it('getArticleStats 应调用 GET /health/articles/stats', async () => { - mockGet.mockResolvedValue(fakeRes) - await dashboardApi.getArticleStats() - - expect(mockGet).toHaveBeenCalledWith('/health/articles/stats') - }) -}) - -describe('actionInboxApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/action-inbox 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await actionInboxApi.list({ status: 'pending', type: 'alert', page: 1, page_size: 20 }) - - expect(mockGet).toHaveBeenCalledWith('/health/action-inbox', { - params: { status: 'pending', type: 'alert', page: 1, page_size: 20 }, - }) - }) - - it('getThread 应调用 GET /health/action-inbox/:sourceRef/thread', async () => { - mockGet.mockResolvedValue(fakeRes) - await actionInboxApi.getThread('ref-001') - - expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/ref-001/thread') - }) - - it('getThread 应对特殊字符 URL 编码', async () => { - mockGet.mockResolvedValue(fakeRes) - await actionInboxApi.getThread('ref/with:special') - - expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/ref%2Fwith%3Aspecial/thread') - }) - - it('stats 应调用 GET /health/action-inbox/stats', async () => { - mockGet.mockResolvedValue(fakeRes) - await actionInboxApi.stats() - - expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/stats') - }) - - it('team 应调用 GET /health/action-inbox/team', async () => { - mockGet.mockResolvedValue(fakeRes) - await actionInboxApi.team() - - expect(mockGet).toHaveBeenCalledWith('/health/action-inbox/team') - }) -}) diff --git a/apps/web/src/api/health/dashboard.ts b/apps/web/src/api/health/dashboard.ts deleted file mode 100644 index 43201ef..0000000 --- a/apps/web/src/api/health/dashboard.ts +++ /dev/null @@ -1,69 +0,0 @@ -import client from '../client'; - -export interface ServiceHealthStatus { - name: string; - status: string; - message: string; - response_ms: number | null; -} - -export interface SystemHealthResp { - services: ServiceHealthStatus[]; - checked_at: string; -} - -export interface RoleCount { - role: string; - count: number; -} - -export interface UserActivityResp { - daily_active: number; - weekly_active: number; - monthly_active: number; - total_registered: number; - by_role: RoleCount[]; -} - -export interface ModuleStatusResp { - name: string; - display_name: string; - description: string; - active: boolean; - entity_count: number | null; - route_count: number | null; -} - -export interface PointsActivityItem { - id: string; - user_name: string; - detail: string; - amount: string; - type: string; - created_at: string; -} - -export interface ArticleStatsResp { - published: number; - draft: number; - pending_review: number; - rejected: number; - total_views: number; -} - -export const dashboardApi = { - getSystemHealth: () => - client.get('/health/admin/system-health').then((r) => r.data.data as SystemHealthResp), - - getUserActivity: () => - client.get('/health/admin/user-activity').then((r) => r.data.data as UserActivityResp), - - getModuleStatus: () => - client.get('/health/admin/modules').then((r) => r.data.data as ModuleStatusResp[]), - - getPointsRecentActivity: () => - client.get('/health/points/recent-activity').then((r) => r.data.data as PointsActivityItem[]), - - getArticleStats: () => - client.get('/health/articles/stats').then((r) => r.data.data as ArticleStatsResp), -}; diff --git a/apps/web/src/api/health/deviceReadings.test.ts b/apps/web/src/api/health/deviceReadings.test.ts deleted file mode 100644 index ae0b191..0000000 --- a/apps/web/src/api/health/deviceReadings.test.ts +++ /dev/null @@ -1,82 +0,0 @@ -/** - * deviceReadings + devices API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { deviceReadingApi } from './deviceReadings' -import { deviceApi } from './devices' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('deviceReadingApi', () => { - const fakeRes = { data: { data: {} } } - - it('batchCreate 应调用 POST /health/patients/:id/device-readings/batch', async () => { - mockPost.mockResolvedValue(fakeRes) - const data = { - device_id: 'dev-001', - readings: [ - { device_type: 'blood_pressure', values: { systolic: 130, diastolic: 85 }, measured_at: '2026-05-03T08:00:00Z' }, - ], - } - await deviceReadingApi.batchCreate('p-001', data) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/device-readings/batch', data) - }) - - it('query 应调用 GET /health/patients/:id/device-readings 并剥离 patient_id', async () => { - mockGet.mockResolvedValue(fakeRes) - await deviceReadingApi.query({ patient_id: 'p-001', device_type: 'blood_pressure', hours: 24 }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/device-readings', { - params: { device_type: 'blood_pressure', hours: 24 }, - }) - }) - - it('queryHourly 应调用 GET /health/patients/:id/device-readings/hourly', async () => { - mockGet.mockResolvedValue(fakeRes) - await deviceReadingApi.queryHourly({ patient_id: 'p-001', device_type: 'blood_pressure', days: 7 }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/device-readings/hourly', { - params: { device_type: 'blood_pressure', days: 7 }, - }) - }) -}) - -describe('deviceApi', () => { - const fakeRes = { data: { data: {} } } - - it('listDevices 应调用 GET /health/devices 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await deviceApi.listDevices({ patient_id: 'p-001', device_type: 'blood_pressure', page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/health/devices', { - params: { patient_id: 'p-001', device_type: 'blood_pressure', page: 1, page_size: 10 }, - }) - }) - - it('unbindDevice 应调用 DELETE /health/devices/:id 并在 body 传递 version', async () => { - mockDelete.mockResolvedValue(fakeRes) - await deviceApi.unbindDevice('dev-001', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/devices/dev-001', { - data: { version: 2 }, - }) - }) -}) diff --git a/apps/web/src/api/health/deviceReadings.ts b/apps/web/src/api/health/deviceReadings.ts deleted file mode 100644 index 94f0424..0000000 --- a/apps/web/src/api/health/deviceReadings.ts +++ /dev/null @@ -1,72 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface DeviceReading { - id: string; - device_id?: string; - device_type: string; - device_model?: string; - raw_value: Record; - measured_at: string; - created_at: string; -} - -export interface HourlyReading { - id: string; - device_type: string; - hour_start: string; - min_val?: number; - max_val?: number; - avg_val: number; - sample_count: number; -} - -export interface DailyReading { - id: string; - device_type: string; - date_bucket: string; - min_val?: number; - max_val?: number; - avg_val: number; - sample_count: number; - percentile_95?: number; -} - -export interface BatchReadingRequest { - device_id: string; - device_model?: string; - readings: { - device_type: string; - values: Record; - measured_at: string; - }[]; -} - -export interface BatchResult { - accepted: number; - duplicates: number; - earliest?: string; - latest?: string; -} - -// --- API --- -export const deviceReadingApi = { - batchCreate: (patientId: string, data: BatchReadingRequest) => - client.post(`/health/patients/${patientId}/device-readings/batch`, data).then((r) => r.data.data as BatchResult), - - query: (params: { patient_id: string; device_type?: string; hours?: number; page?: number; page_size?: number }) => { - const { patient_id, ...query } = params; - return client.get(`/health/patients/${patient_id}/device-readings`, { params: query }).then((r) => r.data.data as PaginatedResponse); - }, - - queryHourly: (params: { patient_id: string; device_type: string; days?: number; page?: number; page_size?: number }) => { - const { patient_id, ...query } = params; - return client.get(`/health/patients/${patient_id}/device-readings/hourly`, { params: query }).then((r) => r.data.data as PaginatedResponse); - }, - - queryDaily: (params: { patient_id: string; device_type?: string; from_date?: string; to_date?: string; page?: number; page_size?: number }) => { - const { patient_id, ...query } = params; - return client.get(`/health/vital-signs/daily`, { params: { ...query, patient_id } }).then((r) => r.data.data as PaginatedResponse); - }, -}; diff --git a/apps/web/src/api/health/devices.ts b/apps/web/src/api/health/devices.ts deleted file mode 100644 index d7b6cbd..0000000 --- a/apps/web/src/api/health/devices.ts +++ /dev/null @@ -1,37 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface DeviceItem { - id: string; - patient_id: string; - device_id: string; - device_model: string; - device_type: string; - status?: string; - firmware_version?: string; - manufacturer?: string; - connection_type?: string; - metadata?: Record; - bound_at: string; - last_sync_at: string; - version: number; -} - -// --- API --- -export const deviceApi = { - listDevices: (params?: { - patient_id?: string; - device_type?: string; - page?: number; - page_size?: number; - }) => - client - .get('/health/devices', { params }) - .then((r) => r.data.data as PaginatedResponse), - - unbindDevice: (id: string, version: number) => - client - .delete(`/health/devices/${id}`, { data: { version } }) - .then((r) => r.data.data as DeviceItem), -}; diff --git a/apps/web/src/api/health/diagnoses.ts b/apps/web/src/api/health/diagnoses.ts deleted file mode 100644 index 3fe68e7..0000000 --- a/apps/web/src/api/health/diagnoses.ts +++ /dev/null @@ -1,108 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface Diagnosis { - id: string; - patient_id: string; - health_record_id?: string; - icd_code: string; - diagnosis_name: string; - diagnosis_type: string; - diagnosed_date: string; - status: string; - diagnosed_by?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateDiagnosisReq { - icd_code: string; - diagnosis_name: string; - diagnosis_type?: string; - diagnosed_date: string; - status?: string; - health_record_id?: string; - diagnosed_by?: string; - notes?: string; -} - -export interface UpdateDiagnosisReq { - icd_code?: string; - diagnosis_name?: string; - diagnosis_type?: string; - diagnosed_date?: string; - status?: string; - health_record_id?: string; - diagnosed_by?: string; - notes?: string; -} - -// --- Constants --- - -export const DIAGNOSIS_TYPE_OPTIONS = [ - { label: '主要诊断', value: 'primary' }, - { label: '次要诊断', value: 'secondary' }, - { label: '合并症', value: 'comorbid' }, -]; - -export const DIAGNOSIS_STATUS_OPTIONS = [ - { label: '活跃', value: 'active' }, - { label: '已缓解', value: 'resolved' }, - { label: '慢性', value: 'chronic' }, -]; - -export const DIAGNOSIS_TYPE_COLOR: Record = { - primary: 'red', - secondary: 'blue', - comorbid: 'orange', -}; - -export const DIAGNOSIS_STATUS_COLOR: Record = { - active: 'green', - resolved: 'default', - chronic: 'orange', -}; - -export const DIAGNOSIS_TYPE_LABEL: Record = Object.fromEntries( - DIAGNOSIS_TYPE_OPTIONS.map((o) => [o.value, o.label]), -); - -export const DIAGNOSIS_STATUS_LABEL: Record = Object.fromEntries( - DIAGNOSIS_STATUS_OPTIONS.map((o) => [o.value, o.label]), -); - -// --- API --- - -export const diagnosisApi = { - list: async (patientId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/diagnoses`, { params }); - return data.data; - }, - - create: async (patientId: string, req: CreateDiagnosisReq) => { - const { data } = await client.post<{ - success: boolean; - data: Diagnosis; - }>(`/health/patients/${patientId}/diagnoses`, req); - return data.data; - }, - - update: async (diagnosisId: string, req: UpdateDiagnosisReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: Diagnosis; - }>(`/health/diagnoses/${diagnosisId}`, req); - return data.data; - }, - - delete: async (diagnosisId: string, version: number) => { - await client.delete(`/health/diagnoses/${diagnosisId}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/dialysis.ts b/apps/web/src/api/health/dialysis.ts deleted file mode 100644 index 582941e..0000000 --- a/apps/web/src/api/health/dialysis.ts +++ /dev/null @@ -1,116 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface DialysisRecord { - id: string; - patient_id: string; - dialysis_date: string; - start_time?: string; - end_time?: string; - dry_weight?: number; - pre_weight?: number; - post_weight?: number; - pre_bp_systolic?: number; - pre_bp_diastolic?: number; - post_bp_systolic?: number; - post_bp_diastolic?: number; - pre_heart_rate?: number; - post_heart_rate?: number; - ultrafiltration_volume?: number; - dialysis_duration?: number; - blood_flow_rate?: number; - dialysis_type: string; - symptoms?: Record; - complication_notes?: string; - status: string; - reviewed_by?: string; - reviewed_at?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateDialysisRecordReq { - patient_id: string; - dialysis_date: string; - start_time?: string; - end_time?: string; - dry_weight?: number; - pre_weight?: number; - post_weight?: number; - pre_bp_systolic?: number; - pre_bp_diastolic?: number; - post_bp_systolic?: number; - post_bp_diastolic?: number; - pre_heart_rate?: number; - post_heart_rate?: number; - ultrafiltration_volume?: number; - dialysis_duration?: number; - blood_flow_rate?: number; - dialysis_type?: string; - complication_notes?: string; -} - -// --- API --- - -export const dialysisApi = { - listRecords: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/dialysis-records`, { params }); - return data.data; - }, - - getRecord: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: DialysisRecord; - }>(`/health/dialysis-records/${id}`); - return data.data; - }, - - createRecord: async (req: CreateDialysisRecordReq) => { - const { data } = await client.post<{ - success: boolean; - data: DialysisRecord; - }>('/health/dialysis-records', req); - return data.data; - }, - - updateRecord: async ( - id: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: DialysisRecord; - }>(`/health/dialysis-records/${id}`, req); - return data.data; - }, - - deleteRecord: async (id: string, version: number) => { - await client.delete(`/health/dialysis-records/${id}`, { data: { version } }); - }, - - reviewRecord: async (id: string, req: { version: number; doctor_notes?: string }) => { - const { data } = await client.put<{ - success: boolean; - data: Record; - }>(`/health/dialysis-records/${id}/review`, req); - return data.data; - }, - - completeRecord: async (id: string, version: number) => { - const { data } = await client.put<{ - success: boolean; - data: DialysisRecord; - }>(`/health/dialysis-records/${id}/complete`, { version }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/doctors.test.ts b/apps/web/src/api/health/doctors.test.ts deleted file mode 100644 index 1c9996d..0000000 --- a/apps/web/src/api/health/doctors.test.ts +++ /dev/null @@ -1,67 +0,0 @@ -/** - * doctors API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { doctorApi } from './doctors' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('doctorApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/doctors 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await doctorApi.list({ page: 1, page_size: 10, search: '王', department: '内科' }) - - expect(mockGet).toHaveBeenCalledWith('/health/doctors', { - params: { page: 1, page_size: 10, search: '王', department: '内科' }, - }) - }) - - it('get 应调用 GET /health/doctors/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await doctorApi.get('d-001') - - expect(mockGet).toHaveBeenCalledWith('/health/doctors/d-001') - }) - - it('create 应调用 POST /health/doctors 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '王医生', department: '内科', title: '主任医师' } - await doctorApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/doctors', req) - }) - - it('update 应调用 PUT /health/doctors/:id 并传递请求体含 version', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { title: '副主任医师', version: 1 } - await doctorApi.update('d-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/doctors/d-001', req) - }) - - it('delete 应调用 DELETE /health/doctors/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await doctorApi.delete('d-001', 1) - - expect(mockDelete).toHaveBeenCalledWith('/health/doctors/d-001') - }) -}) diff --git a/apps/web/src/api/health/doctors.ts b/apps/web/src/api/health/doctors.ts deleted file mode 100644 index 3dcfedb..0000000 --- a/apps/web/src/api/health/doctors.ts +++ /dev/null @@ -1,83 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface Doctor { - id: string; - user_id?: string; - name: string; - department?: string; - title?: string; - specialty?: string; - license_number?: string; - bio?: string; - online_status: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateDoctorReq { - user_id?: string; - name: string; - department?: string; - title?: string; - specialty?: string; - license_number?: string; - bio?: string; -} - -export interface UpdateDoctorReq { - name?: string; - department?: string; - title?: string; - specialty?: string; - license_number?: string; - bio?: string; - online_status?: string; -} - -// --- API --- -export const doctorApi = { - list: async (params: { - page?: number; - page_size?: number; - search?: string; - department?: string; - title?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/doctors', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: Doctor; - }>(`/health/doctors/${id}`); - return data.data; - }, - - create: async (req: CreateDoctorReq) => { - const { data } = await client.post<{ - success: boolean; - data: Doctor; - }>('/health/doctors', req); - return data.data; - }, - - update: async (id: string, req: UpdateDoctorReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: Doctor; - }>(`/health/doctors/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/doctors/${id}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/familyProxy.ts b/apps/web/src/api/health/familyProxy.ts deleted file mode 100644 index d584c2a..0000000 --- a/apps/web/src/api/health/familyProxy.ts +++ /dev/null @@ -1,109 +0,0 @@ -import client from '../client'; - -// --- Types --- - -export interface FamilyMember { - id: string; - patient_id: string; - name: string; - relationship: string; - phone?: string; - birth_date?: string; - notes?: string; - user_id?: string; - consent_status: string; - access_level: string; - consented_at?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface FamilyPatientSummary { - family_member_id: string; - patient_id: string; - patient_name: string; - relationship: string; - consent_status: string; - access_level: string; - consented_at?: string; -} - -export interface FamilyHealthSummary { - patient_id: string; - patient_name: string; - latest_vital_signs?: Record; - active_care_plan?: Record; - recent_alerts_count: number; - next_appointment?: Record; -} - -export interface GrantAccessReq { - access_level: string; -} - -// --- Constants --- - -export const CONSENT_STATUS_OPTIONS = [ - { label: '已同意', value: 'granted' }, - { label: '待确认', value: 'pending' }, - { label: '已撤销', value: 'revoked' }, - { label: '已过期', value: 'expired' }, -]; - -export const ACCESS_LEVEL_OPTIONS = [ - { label: '完全访问', value: 'full' }, - { label: '只读', value: 'read_only' }, - { label: '摘要', value: 'summary' }, -]; - -export const CONSENT_STATUS_COLOR: Record = { - granted: 'green', - pending: 'orange', - revoked: 'red', - expired: 'default', -}; - -export const ACCESS_LEVEL_LABEL: Record = Object.fromEntries( - ACCESS_LEVEL_OPTIONS.map((o) => [o.value, o.label]), -); - -export const CONSENT_STATUS_LABEL: Record = Object.fromEntries( - CONSENT_STATUS_OPTIONS.map((o) => [o.value, o.label]), -); - -// --- API --- - -export const familyProxyApi = { - grantAccess: async (patientId: string, familyMemberId: string, req: GrantAccessReq, version: number) => { - const { data } = await client.post<{ - success: boolean; - data: FamilyMember; - }>(`/health/patients/${patientId}/family-members/${familyMemberId}/grant-access`, { ...req, version }); - return data.data; - }, - - revokeAccess: async (patientId: string, familyMemberId: string, version: number) => { - const { data } = await client.put<{ - success: boolean; - data: FamilyMember; - }>(`/health/patients/${patientId}/family-members/${familyMemberId}/revoke-access`, { version }); - return data.data; - }, - - listMyPatients: async () => { - const { data } = await client.get<{ - success: boolean; - data: FamilyPatientSummary[]; - }>('/health/family/patients'); - return data.data; - }, - - getHealthSummary: async (patientId: string) => { - const { data } = await client.get<{ - success: boolean; - data: FamilyHealthSummary; - }>(`/health/family/patients/${patientId}/health-summary`); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/followUp.test.ts b/apps/web/src/api/health/followUp.test.ts deleted file mode 100644 index 3460c3d..0000000 --- a/apps/web/src/api/health/followUp.test.ts +++ /dev/null @@ -1,97 +0,0 @@ -/** - * followUp API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { followUpApi } from './followUp' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('followUpApi - Tasks', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listTasks 应调用 GET /health/follow-up-tasks 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await followUpApi.listTasks({ page: 1, page_size: 20, patient_id: 'p-001', status: 'pending' }) - - expect(mockGet).toHaveBeenCalledWith('/health/follow-up-tasks', { - params: { page: 1, page_size: 20, patient_id: 'p-001', status: 'pending' }, - }) - }) - - it('getTask 应调用 GET /health/follow-up-tasks/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await followUpApi.getTask('task-001') - - expect(mockGet).toHaveBeenCalledWith('/health/follow-up-tasks/task-001') - }) - - it('createTask 应调用 POST /health/follow-up-tasks', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { patient_id: 'p-001', follow_up_type: 'phone', planned_date: '2026-05-10' } - await followUpApi.createTask(req) - - expect(mockPost).toHaveBeenCalledWith('/health/follow-up-tasks', req) - }) - - it('updateTask 应调用 PUT /health/follow-up-tasks/:id 并传递 version', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { status: 'completed', version: 1 } - await followUpApi.updateTask('task-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/follow-up-tasks/task-001', req) - }) - - it('deleteTask 应调用 DELETE /health/follow-up-tasks/:id 并在 body 传递 version', async () => { - mockDelete.mockResolvedValue(undefined) - await followUpApi.deleteTask('task-001', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/follow-up-tasks/task-001', { - data: { version: 2 }, - }) - }) -}) - -describe('followUpApi - Records', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listRecords 应调用 GET /health/follow-up-records 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await followUpApi.listRecords({ page: 1, page_size: 10, task_id: 'task-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/follow-up-records', { - params: { page: 1, page_size: 10, task_id: 'task-001' }, - }) - }) - - it('createRecord 应调用 POST /health/follow-up-tasks/:taskId/records 并注入 task_id', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { - executed_date: '2026-05-10', - result: '已完成', - patient_condition: '良好', - } - await followUpApi.createRecord('task-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/follow-up-tasks/task-001/records', { - ...req, - task_id: 'task-001', - }) - }) -}) diff --git a/apps/web/src/api/health/followUp.ts b/apps/web/src/api/health/followUp.ts deleted file mode 100644 index c2b6a44..0000000 --- a/apps/web/src/api/health/followUp.ts +++ /dev/null @@ -1,133 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface FollowUpTask { - id: string; - patient_id: string; - assigned_to?: string; - patient_name?: string; - assigned_to_name?: string; - follow_up_type: string; - planned_date: string; - status: string; - content_template?: string; - related_appointment_id?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateFollowUpTaskReq { - patient_id: string; - assigned_to?: string; - follow_up_type: string; - planned_date: string; - content_template?: string; - related_appointment_id?: string; -} - -export interface UpdateFollowUpTaskReq { - assigned_to?: string; - follow_up_type?: string; - planned_date?: string; - content_template?: string; - status?: string; -} - -export interface FollowUpRecord { - id: string; - task_id: string; - executed_by?: string; - executed_date: string; - result?: string; - patient_condition?: string; - medical_advice?: string; - next_follow_up_date?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateFollowUpRecordReq { - task_id: string; - executed_by?: string; - executed_date: string; - result?: string; - patient_condition?: string; - medical_advice?: string; - next_follow_up_date?: string; -} - -// --- API --- -export const followUpApi = { - // Tasks - listTasks: async (params: { - page?: number; - page_size?: number; - patient_id?: string; - assigned_to?: string; - status?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/follow-up-tasks', { params }); - return data.data; - }, - - getTask: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: FollowUpTask; - }>(`/health/follow-up-tasks/${id}`); - return data.data; - }, - - createTask: async (req: CreateFollowUpTaskReq) => { - const { data } = await client.post<{ - success: boolean; - data: FollowUpTask; - }>('/health/follow-up-tasks', req); - return data.data; - }, - - updateTask: async ( - id: string, - req: UpdateFollowUpTaskReq & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: FollowUpTask; - }>(`/health/follow-up-tasks/${id}`, req); - return data.data; - }, - - deleteTask: async (id: string, version: number) => { - await client.delete(`/health/follow-up-tasks/${id}`, { - data: { version }, - }); - }, - - // Records - listRecords: async (params: { - page?: number; - page_size?: number; - task_id?: string; - patient_id?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/follow-up-records', { params }); - return data.data; - }, - - createRecord: async (taskId: string, req: Omit) => { - const { data } = await client.post<{ - success: boolean; - data: FollowUpRecord; - }>(`/health/follow-up-tasks/${taskId}/records`, { ...req, task_id: taskId }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/followUpTemplates.test.ts b/apps/web/src/api/health/followUpTemplates.test.ts deleted file mode 100644 index 637fb65..0000000 --- a/apps/web/src/api/health/followUpTemplates.test.ts +++ /dev/null @@ -1,75 +0,0 @@ -/** - * followUpTemplates API 契约测试 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { followUpTemplateApi } from './followUpTemplates' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('followUpTemplateApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/follow-up-templates 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await followUpTemplateApi.list({ page: 1, page_size: 10, follow_up_type: 'phone', status: 'active' }) - - expect(mockGet).toHaveBeenCalledWith('/health/follow-up-templates', { - params: { page: 1, page_size: 10, follow_up_type: 'phone', status: 'active' }, - }) - }) - - it('get 应调用 GET /health/follow-up-templates/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await followUpTemplateApi.get('tpl-001') - - expect(mockGet).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001') - }) - - it('create 应调用 POST /health/follow-up-templates 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { - name: '电话随访模板', - follow_up_type: 'phone', - fields: [ - { label: '患者状态', field_key: 'patient_status', field_type: 'select', required: true, options: '良好,一般,较差' }, - ], - } - await followUpTemplateApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/follow-up-templates', req) - }) - - it('update 应调用 PUT /health/follow-up-templates/:id 并传递请求体', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { name: '更新后模板', status: 'active', version: 1 } - await followUpTemplateApi.update('tpl-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001', req) - }) - - it('delete 应调用 DELETE /health/follow-up-templates/:id 并在 body 传递 version', async () => { - mockDelete.mockResolvedValue(undefined) - await followUpTemplateApi.delete('tpl-001', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/follow-up-templates/tpl-001', { - data: { version: 2 }, - }) - }) -}) diff --git a/apps/web/src/api/health/followUpTemplates.ts b/apps/web/src/api/health/followUpTemplates.ts deleted file mode 100644 index 626e72a..0000000 --- a/apps/web/src/api/health/followUpTemplates.ts +++ /dev/null @@ -1,119 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -export type FollowUpType = 'phone' | 'outpatient' | 'home_visit' | 'online' | 'wechat'; -export type TemplateStatus = 'active' | 'draft' | 'archived'; - -export interface TemplateField { - id: string; - template_id: string; - label: string; - field_key: string; - field_type: string; - required: boolean; - options?: string; - placeholder?: string; - validation?: string; - sort_order: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface TemplateFieldReq { - label: string; - field_key: string; - field_type: string; - required?: boolean; - options?: string; - placeholder?: string; - validation?: string; - sort_order?: number; -} - -export interface FollowUpTemplate { - id: string; - name: string; - description?: string; - follow_up_type: string; - applicable_scope?: string; - status: string; - fields: TemplateField[]; - created_at: string; - updated_at: string; - version: number; -} - -export interface FollowUpTemplateListItem { - id: string; - name: string; - description?: string; - follow_up_type: string; - status: string; - field_count: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateTemplateReq { - name: string; - description?: string; - follow_up_type: string; - applicable_scope?: string; - fields: TemplateFieldReq[]; -} - -export interface UpdateTemplateReq { - name?: string; - description?: string; - follow_up_type?: string; - applicable_scope?: string; - status?: string; - fields?: TemplateFieldReq[]; -} - -export const followUpTemplateApi = { - list: async (params?: { - page?: number; - page_size?: number; - follow_up_type?: string; - status?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/follow-up-templates', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: FollowUpTemplate; - }>(`/health/follow-up-templates/${id}`); - return data.data; - }, - - create: async (req: CreateTemplateReq) => { - const { data } = await client.post<{ - success: boolean; - data: FollowUpTemplate; - }>('/health/follow-up-templates', req); - return data.data; - }, - - update: async (id: string, req: UpdateTemplateReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: FollowUpTemplate; - }>(`/health/follow-up-templates/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/follow-up-templates/${id}`, { - data: { version }, - }); - }, -}; diff --git a/apps/web/src/api/health/healthData.test.ts b/apps/web/src/api/health/healthData.test.ts deleted file mode 100644 index 1f9862e..0000000 --- a/apps/web/src/api/health/healthData.test.ts +++ /dev/null @@ -1,135 +0,0 @@ -/** - * healthData API 契约测试(体征/化验报告/健康记录/趋势/日常监测) - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { healthDataApi } from './healthData' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('healthDataApi - Vital Signs', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listVitalSigns 应调用 GET /health/patients/:id/vital-signs 并传递分页', async () => { - mockGet.mockResolvedValue(fakeRes) - await healthDataApi.listVitalSigns('p-001', { page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/vital-signs', { - params: { page: 1, page_size: 10 }, - }) - }) - - it('createVitalSigns 应调用 POST /health/patients/:id/vital-signs', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { record_date: '2026-05-03', systolic_bp_morning: 120, diastolic_bp_morning: 80 } - await healthDataApi.createVitalSigns('p-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/vital-signs', req) - }) - - it('updateVitalSigns 应调用 PUT /health/patients/:pid/vital-signs/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { systolic_bp_morning: 125, version: 1 } - await healthDataApi.updateVitalSigns('p-001', 'vs-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/vital-signs/vs-001', req) - }) - - it('deleteVitalSigns 应调用 DELETE /health/patients/:pid/vital-signs/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await healthDataApi.deleteVitalSigns('p-001', 'vs-001') - - expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/vital-signs/vs-001') - }) -}) - -describe('healthDataApi - Lab Reports', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listLabReports 应调用 GET /health/patients/:id/lab-reports', async () => { - mockGet.mockResolvedValue(fakeRes) - await healthDataApi.listLabReports('p-001', { page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/lab-reports', { - params: { page: 1, page_size: 10 }, - }) - }) - - it('createLabReport 应调用 POST /health/patients/:id/lab-reports', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { report_date: '2026-05-03', report_type: 'blood_test' } - await healthDataApi.createLabReport('p-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/lab-reports', req) - }) - - it('reviewLabReport 应调用 PUT /health/patients/:pid/lab-reports/:rid/review', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { version: 1, doctor_notes: '指标正常' } - await healthDataApi.reviewLabReport('p-001', 'lr-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/lab-reports/lr-001/review', req) - }) - - it('deleteLabReport 应调用 DELETE /health/patients/:pid/lab-reports/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await healthDataApi.deleteLabReport('p-001', 'lr-001') - - expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/lab-reports/lr-001') - }) -}) - -describe('healthDataApi - Health Records', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listHealthRecords 应调用 GET /health/patients/:id/health-records', async () => { - mockGet.mockResolvedValue(fakeRes) - await healthDataApi.listHealthRecords('p-001', { page: 1, page_size: 10 }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/health-records', { - params: { page: 1, page_size: 10 }, - }) - }) - - it('createHealthRecord 应调用 POST /health/patients/:id/health-records', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { record_type: 'checkup', record_date: '2026-05-03', content: '体检结果正常' } - await healthDataApi.createHealthRecord('p-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/health-records', req) - }) -}) - -describe('healthDataApi - Trends', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listTrends 应调用 GET /health/patients/:id/trends', async () => { - mockGet.mockResolvedValue(fakeRes) - await healthDataApi.listTrends('p-001') - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/trends') - }) - - it('getIndicatorTimeseries 应调用 GET /health/patients/:id/trends/:indicator 并编码', async () => { - mockGet.mockResolvedValue(fakeRes) - await healthDataApi.getIndicatorTimeseries('p-001', 'blood_pressure/systolic') - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/trends/blood_pressure%2Fsystolic') - }) -}) diff --git a/apps/web/src/api/health/healthData.ts b/apps/web/src/api/health/healthData.ts deleted file mode 100644 index cc766a7..0000000 --- a/apps/web/src/api/health/healthData.ts +++ /dev/null @@ -1,304 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface VitalSigns { - id: string; - patient_id: string; - record_date: string; - systolic_bp_morning?: number; - diastolic_bp_morning?: number; - systolic_bp_evening?: number; - diastolic_bp_evening?: number; - heart_rate?: number; - weight?: number; - blood_sugar?: number; - water_intake_ml?: number; - urine_output_ml?: number; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateVitalSignsReq { - record_date: string; - systolic_bp_morning?: number; - diastolic_bp_morning?: number; - systolic_bp_evening?: number; - diastolic_bp_evening?: number; - heart_rate?: number; - weight?: number; - blood_sugar?: number; - water_intake_ml?: number; - urine_output_ml?: number; - notes?: string; -} - -export interface LabReport { - id: string; - patient_id: string; - report_date: string; - report_type: string; - items?: unknown; - image_urls?: string[]; - doctor_notes?: string; - source?: string; - status: string; - reviewed_by?: string; - reviewed_at?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateLabReportReq { - report_date: string; - report_type: string; - items?: unknown; - image_urls?: string[]; - doctor_notes?: string; -} - -export interface HealthRecord { - id: string; - patient_id: string; - record_type: string; - record_date: string; - overall_assessment?: string; - report_file_url?: string; - source?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateHealthRecordReq { - record_type: string; - record_date: string; - overall_assessment?: string; - report_file_url?: string; -} - -export interface DailyMonitoring { - id: string; - patient_id: string; - record_date: string; - morning_bp_systolic?: number; - morning_bp_diastolic?: number; - evening_bp_systolic?: number; - evening_bp_diastolic?: number; - weight?: number; - blood_sugar?: number; - fluid_intake?: number; - urine_output?: number; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateDailyMonitoringReq { - patient_id: string; - record_date: string; - morning_bp_systolic?: number; - morning_bp_diastolic?: number; - evening_bp_systolic?: number; - evening_bp_diastolic?: number; - weight?: number; - blood_sugar?: number; - fluid_intake?: number; - urine_output?: number; - notes?: string; -} - -export interface TrendData { - id: string; - patient_id: string; - indicator: string; - trend_data: { date: string; value: number }[]; - generated_at: string; -} - -// --- API --- -export const healthDataApi = { - // Vital Signs - listVitalSigns: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/vital-signs`, { params }); - return data.data; - }, - - createVitalSigns: async (patientId: string, req: CreateVitalSignsReq) => { - const { data } = await client.post<{ - success: boolean; - data: VitalSigns; - }>(`/health/patients/${patientId}/vital-signs`, req); - return data.data; - }, - - updateVitalSigns: async ( - patientId: string, - id: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: VitalSigns; - }>(`/health/patients/${patientId}/vital-signs/${id}`, req); - return data.data; - }, - - deleteVitalSigns: async (patientId: string, id: string) => { - await client.delete(`/health/patients/${patientId}/vital-signs/${id}`); - }, - - // Lab Reports - listLabReports: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/lab-reports`, { params }); - return data.data; - }, - - createLabReport: async (patientId: string, req: CreateLabReportReq) => { - const { data } = await client.post<{ - success: boolean; - data: LabReport; - }>(`/health/patients/${patientId}/lab-reports`, req); - return data.data; - }, - - updateLabReport: async ( - patientId: string, - id: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: LabReport; - }>(`/health/patients/${patientId}/lab-reports/${id}`, req); - return data.data; - }, - - deleteLabReport: async (patientId: string, id: string) => { - await client.delete(`/health/patients/${patientId}/lab-reports/${id}`); - }, - - reviewLabReport: async (patientId: string, reportId: string, req: { version: number; doctor_notes?: string }) => { - const { data } = await client.put<{ - success: boolean; - data: Record; - }>(`/health/patients/${patientId}/lab-reports/${reportId}/review`, req); - return data.data; - }, - - // Health Records - listHealthRecords: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/health-records`, { params }); - return data.data; - }, - - createHealthRecord: async ( - patientId: string, - req: CreateHealthRecordReq, - ) => { - const { data } = await client.post<{ - success: boolean; - data: HealthRecord; - }>(`/health/patients/${patientId}/health-records`, req); - return data.data; - }, - - updateHealthRecord: async ( - patientId: string, - id: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: HealthRecord; - }>(`/health/patients/${patientId}/health-records/${id}`, req); - return data.data; - }, - - deleteHealthRecord: async (patientId: string, id: string) => { - await client.delete(`/health/patients/${patientId}/health-records/${id}`); - }, - - // Trends - listTrends: async (patientId: string) => { - const { data } = await client.get<{ - success: boolean; - data: TrendData[]; - }>(`/health/patients/${patientId}/trends`); - return data.data; - }, - - generateTrend: async (patientId: string, req: { indicator: string; start_date?: string; end_date?: string }) => { - const { data } = await client.post<{ - success: boolean; - data: TrendData; - }>(`/health/patients/${patientId}/trends/generate`, req); - return data.data; - }, - - getIndicatorTimeseries: async (patientId: string, indicator: string) => { - const { data } = await client.get<{ - success: boolean; - data: { date: string; value: number }[]; - }>(`/health/patients/${patientId}/trends/${encodeURIComponent(indicator)}`); - return data.data; - }, - - // Daily Monitoring - listDailyMonitoring: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/daily-monitoring`, { params }); - return data.data; - }, - - createDailyMonitoring: async (req: CreateDailyMonitoringReq) => { - const { data } = await client.post<{ - success: boolean; - data: DailyMonitoring; - }>('/health/daily-monitoring', req); - return data.data; - }, - - updateDailyMonitoring: async ( - id: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: DailyMonitoring; - }>(`/health/daily-monitoring/${id}`, req); - return data.data; - }, - - deleteDailyMonitoring: async (id: string, version: number) => { - await client.delete(`/health/daily-monitoring/${id}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/media.ts b/apps/web/src/api/health/media.ts deleted file mode 100644 index 79ebe38..0000000 --- a/apps/web/src/api/health/media.ts +++ /dev/null @@ -1,208 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --------------------------------------------------------------------------- -// 媒体文件类型 -// --------------------------------------------------------------------------- - -export interface MediaItem { - id: string; - tenant_id: string; - folder_id?: string; - filename: string; - storage_path: string; - thumbnail_path?: string; - content_type: string; - file_size: number; - width?: number; - height?: number; - alt_text?: string; - is_public: boolean; - created_at: string; - updated_at: string; - created_by?: string; - updated_by?: string; - version: number; -} - -export interface MediaListParams { - page?: number; - page_size?: number; - folder_id?: string; - content_type?: string; - keyword?: string; - is_public?: boolean; -} - -export interface UpdateMediaReq { - filename?: string; - alt_text?: string; - is_public?: boolean; - folder_id?: string; - version: number; -} - -export interface MoveMediaReq { - folder_id?: string; - version: number; -} - -export interface CropReq { - x: number; - y: number; - width: number; - height: number; - version: number; -} - -// --------------------------------------------------------------------------- -// 文件夹类型 -// --------------------------------------------------------------------------- - -export interface FolderItem { - id: string; - tenant_id: string; - name: string; - parent_id?: string; - sort_order: number; - children: FolderItem[]; - item_count: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateFolderReq { - name: string; - parent_id?: string; - sort_order?: number; -} - -export interface UpdateFolderReq { - name?: string; - parent_id?: string; - sort_order?: number; - version: number; -} - -// --------------------------------------------------------------------------- -// 媒体文件 API -// --------------------------------------------------------------------------- - -export const mediaApi = { - /** 分页查询媒体文件列表 */ - list: async (params: MediaListParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/media', { params }); - return data.data; - }, - - /** 上传媒体文件(multipart/form-data) */ - upload: async (formData: FormData) => { - const { data } = await client.post<{ - success: boolean; - data: MediaItem; - }>('/health/media/upload', formData, { - headers: { 'Content-Type': 'multipart/form-data' }, - }); - return data.data; - }, - - /** 获取单个媒体文件详情 */ - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: MediaItem; - }>(`/health/media/${id}`); - return data.data; - }, - - /** 更新媒体文件信息 */ - update: async (id: string, req: UpdateMediaReq) => { - const { data } = await client.put<{ - success: boolean; - data: MediaItem; - }>(`/health/media/${id}`, req); - return data.data; - }, - - /** 删除媒体文件 */ - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/media/${id}`, { data: { version } }); - return data.data; - }, - - /** 移动媒体文件到指定文件夹 */ - move: async (id: string, req: MoveMediaReq) => { - const { data } = await client.post<{ - success: boolean; - data: MediaItem; - }>(`/health/media/${id}/move`, req); - return data.data; - }, - - /** 批量删除媒体文件 */ - batchDelete: async (ids: string[]) => { - const { data } = await client.post<{ - success: boolean; - data: null; - }>('/health/media/batch-delete', { ids }); - return data.data; - }, - - /** 裁剪媒体文件 */ - crop: async (id: string, req: CropReq) => { - const { data } = await client.post<{ - success: boolean; - data: MediaItem; - }>(`/health/media/${id}/crop`, req); - return data.data; - }, -}; - -// --------------------------------------------------------------------------- -// 文件夹 API -// --------------------------------------------------------------------------- - -export const mediaFolderApi = { - /** 获取文件夹树形结构 */ - tree: async () => { - const { data } = await client.get<{ - success: boolean; - data: FolderItem[]; - }>('/health/media-folders'); - return data.data; - }, - - /** 创建文件夹 */ - create: async (req: CreateFolderReq) => { - const { data } = await client.post<{ - success: boolean; - data: FolderItem; - }>('/health/media-folders', req); - return data.data; - }, - - /** 更新文件夹 */ - update: async (id: string, req: UpdateFolderReq) => { - const { data } = await client.put<{ - success: boolean; - data: FolderItem; - }>(`/health/media-folders/${id}`, req); - return data.data; - }, - - /** 删除文件夹 */ - delete: async (id: string, version: number) => { - const { data } = await client.delete<{ - success: boolean; - data: null; - }>(`/health/media-folders/${id}`, { data: { version } }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/medicationRecords.ts b/apps/web/src/api/health/medicationRecords.ts deleted file mode 100644 index 6731cef..0000000 --- a/apps/web/src/api/health/medicationRecords.ts +++ /dev/null @@ -1,111 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface MedicationRecord { - id: string; - patient_id: string; - medication_name: string; - generic_name?: string; - dosage?: string; - unit?: string; - frequency?: string; - route?: string; - start_date?: string; - end_date?: string; - is_current: boolean; - prescribed_by?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateMedicationRecordReq { - patient_id: string; - medication_name: string; - generic_name?: string; - dosage?: string; - unit?: string; - frequency?: string; - route?: string; - start_date?: string; - end_date?: string; - is_current?: boolean; - prescribed_by?: string; - notes?: string; -} - -export interface UpdateMedicationRecordReq { - medication_name?: string; - generic_name?: string; - dosage?: string; - unit?: string; - frequency?: string; - route?: string; - start_date?: string; - end_date?: string; - is_current?: boolean; - prescribed_by?: string; - notes?: string; -} - -// --- Constants --- - -export const FREQUENCY_OPTIONS = [ - { label: '每日一次', value: 'QD' }, - { label: '每日两次', value: 'BID' }, - { label: '每日三次', value: 'TID' }, - { label: '每晚一次', value: 'QN' }, - { label: '每周一次', value: 'QW' }, - { label: '必要时', value: 'PRN' }, -]; - -export const ROUTE_OPTIONS = [ - { label: '口服', value: 'oral' }, - { label: '静脉注射', value: 'iv' }, - { label: '皮下注射', value: 'sc' }, - { label: '外用', value: 'topical' }, - { label: '吸入', value: 'inhalation' }, -]; - -// --- API --- - -export const medicationRecordApi = { - list: async (patientId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/medications`, { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: MedicationRecord; - }>(`/health/medications/${id}`); - return data.data; - }, - - create: async (req: CreateMedicationRecordReq) => { - const { data } = await client.post<{ - success: boolean; - data: MedicationRecord; - }>('/health/medications', req); - return data.data; - }, - - update: async (id: string, req: UpdateMedicationRecordReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: MedicationRecord; - }>(`/health/medications/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/medications/${id}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/medicationReminders.ts b/apps/web/src/api/health/medicationReminders.ts deleted file mode 100644 index 51a2841..0000000 --- a/apps/web/src/api/health/medicationReminders.ts +++ /dev/null @@ -1,75 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface MedicationReminder { - id: string; - patient_id: string; - medication_name: string; - dosage?: string; - frequency: string; - reminder_times: unknown; - start_date?: string; - end_date?: string; - is_active: boolean; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateMedicationReminderReq { - patient_id: string; - medication_name: string; - dosage?: string; - frequency?: string; - reminder_times?: unknown; - start_date?: string; - end_date?: string; - is_active?: boolean; - notes?: string; -} - -export interface UpdateMedicationReminderReq { - medication_name?: string; - dosage?: string; - frequency?: string; - reminder_times?: unknown; - start_date?: string; - end_date?: string; - is_active?: boolean; - notes?: string; -} - -// --- API --- - -export const medicationReminderApi = { - list: async (patientId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/patients/${patientId}/medication-reminders`, { params }); - return data.data; - }, - - create: async (req: CreateMedicationReminderReq) => { - const { data } = await client.post<{ - success: boolean; - data: MedicationReminder; - }>('/health/medication-reminders', req); - return data.data; - }, - - update: async (id: string, req: UpdateMedicationReminderReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: MedicationReminder; - }>(`/health/medication-reminders/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/medication-reminders/${id}`, { data: { version } }); - }, -}; diff --git a/apps/web/src/api/health/oauthClients.ts b/apps/web/src/api/health/oauthClients.ts deleted file mode 100644 index 9ff7615..0000000 --- a/apps/web/src/api/health/oauthClients.ts +++ /dev/null @@ -1,73 +0,0 @@ -import client from '../client'; - -// --- Types --- -export interface OAuthClient { - id: string; - client_id: string; - client_name: string; - scopes: string[]; - rate_limit_per_minute: number; - is_active: boolean; - token_lifetime_seconds: number; - created_at: string; - version: number; -} - -export interface OAuthClientDetail extends OAuthClient { - tenant_id: string; - client_secret: string; - allowed_patient_ids?: string[]; -} - -export interface CreateOAuthClientReq { - client_name: string; - scopes: string[]; - allowed_patient_ids?: string[]; - rate_limit_per_minute?: number; - token_lifetime_seconds?: number; -} - -export interface UpdateOAuthClientReq { - client_name?: string; - scopes?: string[]; - allowed_patient_ids?: string[] | null; - rate_limit_per_minute?: number; - is_active?: boolean; - token_lifetime_seconds?: number; - version: number; -} - -export interface RegenerateSecretResp { - client_id: string; - client_secret: string; -} - -// --- FHIR Scope --- -export const FHIR_SCOPE_OPTIONS = [ - { value: 'Patient.read', label: 'Patient.read — 读取患者' }, - { value: 'Observation.read', label: 'Observation.read — 读取体征' }, - { value: 'Device.read', label: 'Device.read — 读取设备' }, - { value: 'DiagnosticReport.read', label: 'DiagnosticReport.read — 读取诊断报告' }, - { value: 'Encounter.read', label: 'Encounter.read — 读取就诊记录' }, - { value: 'Practitioner.read', label: 'Practitioner.read — 读取医护' }, - { value: 'Appointment.read', label: 'Appointment.read — 读取预约' }, - { value: 'Task.read', label: 'Task.read — 读取随访任务' }, -]; - -// --- API --- -export const oauthClientApi = { - list: () => - client.get('/health/oauth/clients').then((r) => r.data.data as OAuthClient[]), - - create: (data: CreateOAuthClientReq) => - client.post('/health/oauth/clients', data).then((r) => r.data.data as OAuthClientDetail), - - update: (id: string, data: UpdateOAuthClientReq) => - client.put(`/health/oauth/clients/${id}`, data).then((r) => r.data.data as OAuthClient), - - delete: (id: string) => - client.delete(`/health/oauth/clients/${id}`).then((r) => r.data), - - regenerateSecret: (id: string) => - client.post(`/health/oauth/clients/${id}/regenerate-secret`).then((r) => r.data.data as RegenerateSecretResp), -}; diff --git a/apps/web/src/api/health/patients.test.ts b/apps/web/src/api/health/patients.test.ts deleted file mode 100644 index 9654cc1..0000000 --- a/apps/web/src/api/health/patients.test.ts +++ /dev/null @@ -1,126 +0,0 @@ -/** - * patients API 契约测试 - * - * 验证 patientApi 各函数调用正确的 HTTP 方法、URL 路径和参数序列化。 - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { patientApi } from './patients' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('patientApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('list 应调用 GET /health/patients 并传递查询参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await patientApi.list({ page: 1, page_size: 20, search: '张三', status: 'active' }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients', { - params: { page: 1, page_size: 20, search: '张三', status: 'active' }, - }) - }) - - it('list 应支持 tag_id 过滤参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await patientApi.list({ tag_id: 'tag-001' }) - - expect(mockGet).toHaveBeenCalledWith('/health/patients', { - params: { tag_id: 'tag-001' }, - }) - }) - - it('get 应调用 GET /health/patients/:id', async () => { - mockGet.mockResolvedValue(fakeRes) - await patientApi.get('p-001') - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001') - }) - - it('create 应调用 POST /health/patients 并传递请求体', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '李四', gender: 'male', birth_date: '1990-01-01' } - await patientApi.create(req) - - expect(mockPost).toHaveBeenCalledWith('/health/patients', req) - }) - - it('update 应调用 PUT /health/patients/:id 并传递请求体含 version', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { name: '李四改', version: 2 } - await patientApi.update('p-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001', req) - }) - - it('delete 应调用 DELETE /health/patients/:id 并在 body 中传递 version', async () => { - mockDelete.mockResolvedValue(undefined) - await patientApi.delete('p-001', 3) - - expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001', { - data: { version: 3 }, - }) - }) - - it('manageTags 应调用 POST /health/patients/:id/tags 并传递 tag_ids', async () => { - mockPost.mockResolvedValue(undefined) - await patientApi.manageTags('p-001', ['tag-1', 'tag-2']) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/tags', { - tag_ids: ['tag-1', 'tag-2'], - }) - }) - - it('listFamilyMembers 应调用 GET /health/patients/:id/family-members', async () => { - mockGet.mockResolvedValue(fakeRes) - await patientApi.listFamilyMembers('p-001') - - expect(mockGet).toHaveBeenCalledWith('/health/patients/p-001/family-members') - }) - - it('createFamilyMember 应调用 POST /health/patients/:id/family-members', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '家属A', relationship: 'spouse', phone: '13800138000' } - await patientApi.createFamilyMember('p-001', req) - - expect(mockPost).toHaveBeenCalledWith('/health/patients/p-001/family-members', req) - }) - - it('updateFamilyMember 应调用 PUT /health/patients/:pid/family-members/:mid', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { name: '家属A改', version: 1 } - await patientApi.updateFamilyMember('p-001', 'fm-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/patients/p-001/family-members/fm-001', req) - }) - - it('deleteFamilyMember 应调用 DELETE /health/patients/:pid/family-members/:mid', async () => { - mockDelete.mockResolvedValue(undefined) - await patientApi.deleteFamilyMember('p-001', 'fm-001') - - expect(mockDelete).toHaveBeenCalledWith('/health/patients/p-001/family-members/fm-001') - }) - - it('listTags 应调用 GET /health/patient-tags', async () => { - mockGet.mockResolvedValue(fakeRes) - await patientApi.listTags() - - expect(mockGet).toHaveBeenCalledWith('/health/patient-tags') - }) -}) diff --git a/apps/web/src/api/health/patients.ts b/apps/web/src/api/health/patients.ts deleted file mode 100644 index 89472a0..0000000 --- a/apps/web/src/api/health/patients.ts +++ /dev/null @@ -1,175 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- -export interface PatientListItem { - id: string; - name: string; - gender?: string; - birth_date?: string; - blood_type?: string; - status: string; - verification_status: string; - source?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface PatientDetail { - id: string; - user_id?: string; - name: string; - gender?: string; - birth_date?: string; - blood_type?: string; - id_number?: string; - allergy_history?: string; - medical_history_summary?: string; - emergency_contact_name?: string; - emergency_contact_phone?: string; - status: string; - verification_status: string; - source?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreatePatientReq { - name: string; - gender?: string; - birth_date?: string; - blood_type?: string; - id_number?: string; - allergy_history?: string; - medical_history_summary?: string; - emergency_contact_name?: string; - emergency_contact_phone?: string; - source?: string; - notes?: string; -} - -export interface UpdatePatientReq extends Partial { - status?: string; - verification_status?: string; -} - -export interface FamilyMember { - id: string; - name: string; - relationship: string; - phone?: string; - id_number?: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateFamilyMemberReq { - name: string; - relationship: string; - phone?: string; - id_number?: string; - notes?: string; -} - -export interface TagItem { - id: string; - name: string; - color: string | null; - description: string | null; -} - -// --- API --- -export const patientApi = { - list: async (params: { - page?: number; - page_size?: number; - search?: string; - status?: string; - tag_id?: string; - }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/patients', { params }); - return data.data; - }, - - get: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: PatientDetail; - }>(`/health/patients/${id}`); - return data.data; - }, - - create: async (req: CreatePatientReq) => { - const { data } = await client.post<{ - success: boolean; - data: PatientDetail; - }>('/health/patients', req); - return data.data; - }, - - update: async (id: string, req: UpdatePatientReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: PatientDetail; - }>(`/health/patients/${id}`, req); - return data.data; - }, - - delete: async (id: string, version: number) => { - await client.delete(`/health/patients/${id}`, { data: { version } }); - }, - - manageTags: async (id: string, tagIds: string[]) => { - await client.post(`/health/patients/${id}/tags`, { tag_ids: tagIds }); - }, - - listFamilyMembers: async (id: string) => { - const { data } = await client.get<{ - success: boolean; - data: FamilyMember[]; - }>(`/health/patients/${id}/family-members`); - return data.data; - }, - - createFamilyMember: async (id: string, req: CreateFamilyMemberReq) => { - const { data } = await client.post<{ - success: boolean; - data: FamilyMember; - }>(`/health/patients/${id}/family-members`, req); - return data.data; - }, - - updateFamilyMember: async ( - patientId: string, - memberId: string, - req: Partial & { version: number }, - ) => { - const { data } = await client.put<{ - success: boolean; - data: FamilyMember; - }>(`/health/patients/${patientId}/family-members/${memberId}`, req); - return data.data; - }, - - deleteFamilyMember: async (patientId: string, memberId: string) => { - await client.delete( - `/health/patients/${patientId}/family-members/${memberId}`, - ); - }, - - listTags: async () => { - const { data } = await client.get<{ - success: boolean; - data: TagItem[]; - }>('/health/patient-tags'); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/points.test.ts b/apps/web/src/api/health/points.test.ts deleted file mode 100644 index a898c99..0000000 --- a/apps/web/src/api/health/points.test.ts +++ /dev/null @@ -1,230 +0,0 @@ -/** - * points API 契约测试(完整覆盖 pointsApi + pointsAdminApi) - */ -import { describe, it, expect, vi, beforeEach } from 'vitest' - -const mockGet = vi.fn() -const mockPost = vi.fn() -const mockPut = vi.fn() -const mockDelete = vi.fn() - -vi.mock('../client', () => ({ - default: { - get: (...args: unknown[]) => mockGet(...args), - post: (...args: unknown[]) => mockPost(...args), - put: (...args: unknown[]) => mockPut(...args), - delete: (...args: unknown[]) => mockDelete(...args), - }, -})) - -import { pointsApi, pointsAdminApi } from './points' - -beforeEach(() => { - vi.clearAllMocks() -}) - -describe('pointsAdminApi', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('getPatientAccount 应调用 GET /health/admin/points/patients/:id/account', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsAdminApi.getPatientAccount('p-001') - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/account') - }) - - it('listPatientTransactions 应调用 GET 并传递分页参数', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsAdminApi.listPatientTransactions('p-001', { page: 2, page_size: 15 }) - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/patients/p-001/transactions', { - params: { page: 2, page_size: 15 }, - }) - }) -}) - -describe('pointsApi - Rules', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listRules 应调用 GET /health/admin/points/rules', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.listRules() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/rules') - }) - - it('createRule 应调用 POST /health/admin/points/rules', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { event_type: 'daily_checkin', name: '每日签到', points_value: 10, daily_cap: 1 } - await pointsApi.createRule(req) - - expect(mockPost).toHaveBeenCalledWith('/health/admin/points/rules', req) - }) - - it('updateRule 应调用 PUT /health/admin/points/rules/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { points_value: 20, version: 1 } - await pointsApi.updateRule('rule-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/admin/points/rules/rule-001', { - data: req, - version: req.version, - }) - }) - - it('deleteRule 应调用 DELETE /health/admin/points/rules/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await pointsApi.deleteRule('rule-001', 2) - - expect(mockDelete).toHaveBeenCalledWith('/health/admin/points/rules/rule-001', { - data: { version: 2 }, - }) - }) -}) - -describe('pointsApi - Products', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listProducts 应调用 GET /health/points/products', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.listProducts() - - expect(mockGet).toHaveBeenCalledWith('/health/points/products', { params: undefined }) - }) - - it('createProduct 应调用 POST /health/admin/points/products', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { name: '体检优惠券', product_type: 'service', points_cost: 500, stock: 100 } - await pointsApi.createProduct(req) - - expect(mockPost).toHaveBeenCalledWith('/health/admin/points/products', req) - }) - - it('updateProduct 应调用 PUT /health/admin/points/products/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { points_cost: 600, version: 1 } - await pointsApi.updateProduct('prod-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/admin/points/products/prod-001', { - data: req, - version: req.version, - }) - }) - - it('deleteProduct 应调用 DELETE /health/admin/points/products/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await pointsApi.deleteProduct('prod-001', 1) - - expect(mockDelete).toHaveBeenCalledWith('/health/admin/points/products/prod-001', { - data: { version: 1 }, - }) - }) -}) - -describe('pointsApi - Orders', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listOrders 应调用 GET /health/admin/points/orders', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.listOrders() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/orders', { params: undefined }) - }) - - it('verifyOrder 应调用 POST /health/points/verify', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { qr_code: 'QR-123456' } - await pointsApi.verifyOrder(req) - - expect(mockPost).toHaveBeenCalledWith('/health/points/verify', req) - }) -}) - -describe('pointsApi - Offline Events', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('listOfflineEvents 应调用 GET /health/admin/offline-events', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.listOfflineEvents() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/offline-events', { params: undefined }) - }) - - it('createOfflineEvent 应调用 POST /health/admin/offline-events', async () => { - mockPost.mockResolvedValue(fakeRes) - const req = { title: '健康讲座', event_date: '2026-05-20', points_reward: 50 } - await pointsApi.createOfflineEvent(req) - - expect(mockPost).toHaveBeenCalledWith('/health/admin/offline-events', req) - }) - - it('updateOfflineEvent 应调用 PUT /health/admin/offline-events/:id', async () => { - mockPut.mockResolvedValue(fakeRes) - const req = { title: '健康讲座(更新)', version: 1 } - await pointsApi.updateOfflineEvent('evt-001', req) - - expect(mockPut).toHaveBeenCalledWith('/health/admin/offline-events/evt-001', req) - }) - - it('deleteOfflineEvent 应调用 DELETE /health/admin/offline-events/:id', async () => { - mockDelete.mockResolvedValue(undefined) - await pointsApi.deleteOfflineEvent('evt-001', 1) - - expect(mockDelete).toHaveBeenCalledWith('/health/admin/offline-events/evt-001', { - data: { version: 1 }, - }) - }) -}) - -describe('pointsApi - Statistics', () => { - const fakeRes = { data: { success: true, data: {} } } - - it('getStatistics 应调用 GET /health/admin/points/statistics', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getStatistics() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/points/statistics') - }) - - it('getPatientStats 应调用 GET /health/admin/statistics/patients', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getPatientStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/patients') - }) - - it('getConsultationStats 应调用 GET /health/admin/statistics/consultations', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getConsultationStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/consultations') - }) - - it('getFollowUpStats 应调用 GET /health/admin/statistics/follow-ups', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getFollowUpStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/follow-ups') - }) - - it('getHealthDataStats 应调用 GET /health/admin/statistics/health-data', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getHealthDataStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/health-data') - }) - - it('getDialysisStats 应调用 GET /health/admin/statistics/dialysis', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getDialysisStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/dialysis') - }) - - it('getPersonalStats 应调用 GET /health/admin/statistics/personal-stats', async () => { - mockGet.mockResolvedValue(fakeRes) - await pointsApi.getPersonalStats() - - expect(mockGet).toHaveBeenCalledWith('/health/admin/statistics/personal-stats') - }) -}) diff --git a/apps/web/src/api/health/points.ts b/apps/web/src/api/health/points.ts deleted file mode 100644 index 0265cbf..0000000 --- a/apps/web/src/api/health/points.ts +++ /dev/null @@ -1,446 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface PointsRule { - id: string; - event_type: string; - name: string; - description: string | null; - points_value: number; - daily_cap: number; - streak_7d_bonus: number; - streak_14d_bonus: number; - streak_30d_bonus: number; - is_active: boolean; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreatePointsRuleReq { - event_type: string; - name: string; - description?: string; - points_value: number; - daily_cap?: number; - streak_7d_bonus?: number; - streak_14d_bonus?: number; - streak_30d_bonus?: number; -} - -export interface PointsProduct { - id: string; - name: string; - product_type: string; // physical / service / privilege - points_cost: number; - stock: number; - image_url: string | null; - description: string | null; - is_active: boolean; - sort_order: number; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreatePointsProductReq { - name: string; - product_type: string; - points_cost: number; - stock: number; - description?: string; - image_url?: string; - sort_order?: number; -} - -export interface PointsOrder { - id: string; - patient_id: string; - product_id: string; - product_name: string | null; - points_cost: number; - status: string; // pending / verified / cancelled / expired - qr_code: string; - verified_by: string | null; - verified_at: string | null; - expires_at: string | null; - notes: string | null; - created_at: string; - updated_at: string; - version: number; -} - -export interface VerifyOrderReq { - qr_code: string; -} - -export interface OfflineEvent { - id: string; - title: string; - description: string | null; - event_date: string; - start_time: string | null; - end_time: string | null; - location: string | null; - points_reward: number; - max_participants: number; - current_participants: number; - status: string; // draft / published / ongoing / completed / cancelled - image_url: string | null; - created_at: string; - updated_at: string; - version: number; -} - -export interface CreateOfflineEventReq { - title: string; - description?: string; - event_date: string; - start_time?: string; - end_time?: string; - location?: string; - points_reward?: number; - max_participants?: number; - status?: string; - image_url?: string; -} - -export interface PointsStatistics { - total_issued: number; - total_spent: number; - total_expired: number; - active_accounts: number; - top_earners: Array<{ - account_id: string; - patient_id: string; - patient_name: string; - total_earned: number; - }>; -} - -export interface PatientStatistics { - total_patients: number; - new_this_month: number; - new_this_week: number; - active_this_month: number; -} - -export interface ConsultationStatistics { - total_sessions: number; - pending_reply: number; - avg_response_time_minutes: number | null; - this_month: number; -} - -export interface FollowUpStatistics { - total_tasks: number; - completed: number; - pending: number; - overdue: number; - completion_rate: number; -} - -export interface PersonalStats { - my_patients: number; - new_patients_this_month: number; - follow_up_rate: number; - consultations_this_month: number; - pending_consultations: number; - vital_signs_report_rate: number; - today_appointments: number; - overdue_follow_ups: number; - today_follow_ups: number; - abnormal_vital_signs: number; - vital_signs_reported: number; - vital_signs_total: number; - pending_lab_reviews: number; - yesterday_my_patients?: number; - yesterday_today_appointments?: number; - yesterday_consultations_this_month?: number; - yesterday_follow_up_rate?: number; - yesterday_today_follow_ups?: number; - yesterday_overdue_follow_ups?: number; -} - -export interface OverviewStatistics { - patients: PatientStatistics; - consultations: ConsultationStatistics; - follow_ups: FollowUpStatistics; - points: PointsStatistics; -} - -// --- Health Data Statistics Types --- - -export interface NameValue { - name: string; - value: number; -} - -export interface DialysisStatistics { - total_records: number; - this_month: number; - type_distribution: NameValue[]; - complication_rate: number; - avg_ultrafiltration: number | null; - avg_duration: number | null; - pending_review: number; -} - -export interface LabReportStatistics { - total_reports: number; - this_month: number; - type_distribution: NameValue[]; - abnormal_items: number; - pending_review: number; - reviewed: number; -} - -export interface AppointmentStatistics { - total_appointments: number; - this_month: number; - status_distribution: NameValue[]; - type_distribution: NameValue[]; - cancel_rate: number; -} - -export interface DailyReportRate { - date: string; - reported: number; - total: number; - rate: number; -} - -export interface VitalSignsReportRate { - total_patients: number; - reported_patients: number; - report_rate: number; - total_records: number; - daily_trend: DailyReportRate[]; -} - -export interface HealthDataStats { - lab_reports: LabReportStatistics; - appointments: AppointmentStatistics; - vital_signs_report_rate: VitalSignsReportRate; -} - -// --- API --- - -export interface PointsAccountDetail { - id: string; - patient_id: string; - balance: number; - total_earned: number; - total_spent: number; - total_expired: number; -} - -export interface PointsTransactionDetail { - id: string; - account_id: string; - transaction_type: string; - amount: number; - remaining_amount: number; - status: string; - expires_at: string | null; - balance_after: number; - description: string | null; - created_at: string; -} - -export const pointsAdminApi = { - getPatientAccount: async (patientId: string) => { - const { data } = await client.get<{ - success: boolean; - data: PointsAccountDetail; - }>(`/health/admin/points/patients/${patientId}/account`); - return data.data; - }, - - listPatientTransactions: async ( - patientId: string, - params: { page?: number; page_size?: number }, - ) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/admin/points/patients/${patientId}/transactions`, { params }); - return data.data; - }, -}; - -// --- API (original) --- - -export const pointsApi = { - // Rules - listRules: async () => { - const { data } = await client.get<{ - success: boolean; - data: PointsRule[]; - }>('/health/admin/points/rules'); - return data.data; - }, - - createRule: async (req: CreatePointsRuleReq) => { - const { data } = await client.post<{ - success: boolean; - data: PointsRule; - }>('/health/admin/points/rules', req); - return data.data; - }, - - updateRule: async (id: string, req: Partial & { is_active?: boolean; version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: PointsRule; - }>(`/health/admin/points/rules/${id}`, req); - return data.data; - }, - - deleteRule: async (id: string, version: number) => { - await client.delete(`/health/admin/points/rules/${id}`, { - data: { version }, - }); - }, - - // Products - listProducts: async (params?: Record) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/points/products', { params }); - return data.data; - }, - - createProduct: async (req: CreatePointsProductReq) => { - const { data } = await client.post<{ - success: boolean; - data: PointsProduct; - }>('/health/admin/points/products', req); - return data.data; - }, - - updateProduct: async (id: string, req: Partial & { is_active?: boolean; version: number }) => { - const { version, ...fields } = req; - const { data } = await client.put<{ - success: boolean; - data: PointsProduct; - }>(`/health/admin/points/products/${id}`, { data: fields, version }); - return data.data; - }, - - deleteProduct: async (id: string, version: number) => { - await client.delete(`/health/admin/points/products/${id}`, { - data: { version }, - }); - }, - - // Orders - listOrders: async (params?: Record) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/admin/points/orders', { params }); - return data.data; - }, - - verifyOrder: async (req: VerifyOrderReq) => { - const { data } = await client.post<{ - success: boolean; - data: PointsOrder; - }>('/health/points/verify', req); - return data.data; - }, - - // Offline Events - listOfflineEvents: async (params?: Record) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/admin/offline-events', { params }); - return data.data; - }, - - createOfflineEvent: async (req: CreateOfflineEventReq) => { - const { data } = await client.post<{ - success: boolean; - data: OfflineEvent; - }>('/health/admin/offline-events', req); - return data.data; - }, - - updateOfflineEvent: async (id: string, req: Partial & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: OfflineEvent; - }>(`/health/admin/offline-events/${id}`, req); - return data.data; - }, - - deleteOfflineEvent: async (id: string, version: number) => { - await client.delete(`/health/admin/offline-events/${id}`, { - data: { version }, - }); - }, - - // Points Statistics - getStatistics: async (opts?: { silent?: boolean }) => { - const { data } = await client.get<{ - success: boolean; - data: PointsStatistics; - }>('/health/admin/points/statistics', { skipGlobalError: opts?.silent }); - return data.data; - }, - - // --- Dashboard Statistics --- - - getPatientStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: PatientStatistics; - }>('/health/admin/statistics/patients', { skipGlobalError: opts?.silent }); - return data.data; - }, - - getConsultationStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: ConsultationStatistics; - }>('/health/admin/statistics/consultations', { skipGlobalError: opts?.silent }); - return data.data; - }, - - getFollowUpStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: FollowUpStatistics; - }>('/health/admin/statistics/follow-ups', { skipGlobalError: opts?.silent }); - return data.data; - }, - - getHealthDataStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: HealthDataStats; - }>('/health/admin/statistics/health-data', { skipGlobalError: opts?.silent }); - return data.data; - }, - - getDialysisStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: DialysisStatistics; - }>('/health/admin/statistics/dialysis', { skipGlobalError: opts?.silent }); - return data.data; - }, - - getPersonalStats: async (opts?: { silent?: boolean }): Promise => { - const { data } = await client.get<{ - success: boolean; - data: PersonalStats; - }>('/health/admin/statistics/personal-stats', { skipGlobalError: opts?.silent }); - return data.data; - }, -}; diff --git a/apps/web/src/api/health/shifts.ts b/apps/web/src/api/health/shifts.ts deleted file mode 100644 index 05f2fbe..0000000 --- a/apps/web/src/api/health/shifts.ts +++ /dev/null @@ -1,247 +0,0 @@ -import client from '../client'; -import type { PaginatedResponse } from '../types'; - -// --- Types --- - -export interface Shift { - id: string; - tenant_id: string; - shift_date: string; - period: string; - nurse_id?: string; - status: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; - patient_count?: number; - critical_count?: number; - attention_count?: number; -} - -export interface PatientAssignment { - id: string; - tenant_id: string; - shift_id: string; - patient_id: string; - care_level: string; - notes?: string; - created_at: string; - updated_at: string; - version: number; - patient_name?: string; -} - -export interface HandoffLog { - id: string; - tenant_id: string; - from_shift_id: string; - to_shift_id: string; - patient_id: string; - notes?: string; - pending_items?: Record; - created_at: string; - updated_at: string; - version: number; - patient_name?: string; -} - -export interface CreateShiftReq { - shift_date: string; - period: string; - nurse_id?: string; - notes?: string; -} - -export interface UpdateShiftReq { - shift_date?: string; - period?: string; - nurse_id?: string; - status?: string; - notes?: string; -} - -export interface ListShiftsParams { - page?: number; - page_size?: number; - shift_date?: string; - period?: string; - nurse_id?: string; - status?: string; -} - -export interface CreatePatientAssignmentReq { - patient_id: string; - care_level?: string; - notes?: string; -} - -export interface BatchAssignReq { - patient_ids: string[]; - care_level?: string; -} - -export interface UpdatePatientAssignmentReq { - care_level?: string; - notes?: string; -} - -export interface CreateHandoffReq { - from_shift_id: string; - to_shift_id: string; - patient_id: string; - notes?: string; - pending_items?: Record; -} - -export interface ListHandoffParams { - page?: number; - page_size?: number; - from_shift_id?: string; - to_shift_id?: string; -} - -// --- Constants --- - -export const PERIOD_OPTIONS = [ - { label: '上午班', value: 'morning' }, - { label: '下午班', value: 'afternoon' }, - { label: '晚班', value: 'evening' }, - { label: '夜班', value: 'night' }, -]; - -export const SHIFT_STATUS_OPTIONS = [ - { label: '待开始', value: 'scheduled' }, - { label: '进行中', value: 'in_progress' }, - { label: '已完成', value: 'completed' }, - { label: '已取消', value: 'cancelled' }, -]; - -export const CARE_LEVEL_OPTIONS = [ - { label: '稳定', value: 'stable' }, - { label: '需关注', value: 'attention' }, - { label: '危重', value: 'critical' }, -]; - -export const PERIOD_LABEL: Record = Object.fromEntries( - PERIOD_OPTIONS.map((o) => [o.value, o.label]), -); - -export const SHIFT_STATUS_LABEL: Record = Object.fromEntries( - SHIFT_STATUS_OPTIONS.map((o) => [o.value, o.label]), -); - -export const SHIFT_STATUS_COLOR: Record = { - scheduled: 'default', - in_progress: 'processing', - completed: 'success', - cancelled: 'error', -}; - -export const CARE_LEVEL_LABEL: Record = Object.fromEntries( - CARE_LEVEL_OPTIONS.map((o) => [o.value, o.label]), -); - -export const CARE_LEVEL_COLOR: Record = { - stable: 'green', - attention: 'orange', - critical: 'red', -}; - -// --- API --- - -export const shiftApi = { - // --- Shifts --- - - list: async (params: ListShiftsParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/shifts', { params }); - return data.data; - }, - - get: async (shiftId: string) => { - const { data } = await client.get<{ - success: boolean; - data: Shift; - }>(`/health/shifts/${shiftId}`); - return data.data; - }, - - create: async (req: CreateShiftReq) => { - const { data } = await client.post<{ - success: boolean; - data: Shift; - }>('/health/shifts', req); - return data.data; - }, - - update: async (shiftId: string, req: UpdateShiftReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: Shift; - }>(`/health/shifts/${shiftId}`, req); - return data.data; - }, - - delete: async (shiftId: string, version: number) => { - await client.delete(`/health/shifts/${shiftId}`, { data: { version } }); - }, - - // --- Assignments --- - - listAssignments: async (shiftId: string, params?: { page?: number; page_size?: number }) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>(`/health/shifts/${shiftId}/assignments`, { params }); - return data.data; - }, - - createAssignment: async (shiftId: string, req: CreatePatientAssignmentReq) => { - const { data } = await client.post<{ - success: boolean; - data: PatientAssignment; - }>(`/health/shifts/${shiftId}/assignments`, req); - return data.data; - }, - - batchAssign: async (shiftId: string, req: BatchAssignReq) => { - const { data } = await client.post<{ - success: boolean; - data: PatientAssignment[]; - }>(`/health/shifts/${shiftId}/assignments/batch`, req); - return data.data; - }, - - updateAssignment: async (shiftId: string, assignmentId: string, req: UpdatePatientAssignmentReq & { version: number }) => { - const { data } = await client.put<{ - success: boolean; - data: PatientAssignment; - }>(`/health/shifts/${shiftId}/assignments/${assignmentId}`, req); - return data.data; - }, - - deleteAssignment: async (shiftId: string, assignmentId: string, version: number) => { - await client.delete(`/health/shifts/${shiftId}/assignments/${assignmentId}`, { data: { version } }); - }, - - // --- Handoff Logs --- - - listHandoffs: async (params?: ListHandoffParams) => { - const { data } = await client.get<{ - success: boolean; - data: PaginatedResponse; - }>('/health/handoff-logs', { params }); - return data.data; - }, - - createHandoff: async (req: CreateHandoffReq) => { - const { data } = await client.post<{ - success: boolean; - data: HandoffLog; - }>('/health/handoff-logs', req); - return data.data; - }, -}; diff --git a/apps/web/src/api/themes.ts b/apps/web/src/api/themes.ts index 4ab075e..2eb8e47 100644 --- a/apps/web/src/api/themes.ts +++ b/apps/web/src/api/themes.ts @@ -33,10 +33,10 @@ export interface BrandConfig { } const BRAND_DEFAULTS: BrandConfig = { - brand_name: 'HMS 健康管理平台', - brand_slogan: '新一代健康管理平台', - brand_features: '患者管理 · 健康监测 · 随访管理 · AI 智能分析', - brand_copyright: 'HMS 健康管理平台 · ©汕头市智界科技有限公司', + brand_name: '暖记管理后台', + brand_slogan: '班级管理·日记审核·成长追踪', + brand_features: '班级管理 · 日记审核 · 老师点评 · 成长追踪', + brand_copyright: '© 暖记 Nuanji', }; export async function getPublicBrand(): Promise { diff --git a/apps/web/src/components/ActionThreadDrawer.tsx b/apps/web/src/components/ActionThreadDrawer.tsx deleted file mode 100644 index 0f986c9..0000000 --- a/apps/web/src/components/ActionThreadDrawer.tsx +++ /dev/null @@ -1,226 +0,0 @@ -import { useCallback, useEffect, useState } from 'react'; -import { Drawer, Timeline, Button, Spin, Result, Space, Tag } from 'antd'; -import { - CheckCircleOutlined, - ClockCircleOutlined, - MinusCircleOutlined, - CloseCircleOutlined, - UserOutlined, -} from '@ant-design/icons'; -import { useNavigate } from 'react-router-dom'; -import { - actionInboxApi, - type ActionItem, - type ThreadResponse, - type ActionPriority, -} from '../api/health/actionInbox'; -import client from '../api/client'; - -interface Props { - open: boolean; - item: ActionItem | null; - onClose: () => void; - onActionComplete?: () => void; -} - -const PRIORITY_COLOR: Record = { - urgent: 'red', - high: 'orange', - medium: 'blue', - low: 'default', -}; - -const PRIORITY_LABEL: Record = { - urgent: '紧急', - high: '高', - medium: '中', - low: '低', -}; - -export default function ActionThreadDrawer({ - open, - item, - onClose, - onActionComplete, -}: Props) { - const navigate = useNavigate(); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - const [threadData, setThreadData] = useState(null); - const [actionLoading, setActionLoading] = useState(null); - - const fetchThread = useCallback(async () => { - if (!item) return; - setLoading(true); - setError(null); - try { - const data = await actionInboxApi.getThread(item.source_ref); - setThreadData(data); - } catch (e: unknown) { - const msg = e instanceof Error ? e.message : '获取线程失败'; - setError(msg); - } finally { - setLoading(false); - } - }, [item]); - - useEffect(() => { - if (open && item) fetchThread(); - if (!open) setThreadData(null); - }, [open, item, fetchThread]); - - const handleAction = async (endpoint: string, key: string) => { - setActionLoading(key); - try { - await client.post(endpoint, { action: key }); - onActionComplete?.(); - fetchThread(); - } catch { - // 全局拦截器处理 - } finally { - setActionLoading(null); - } - }; - - const handleLinkClick = (linkTo?: string) => { - if (linkTo) { - onClose(); - navigate(linkTo); - } - }; - - const timelineDot = (status: string) => { - switch (status) { - case 'completed': - return ; - case 'in_progress': - return ; - case 'dismissed': - return ; - default: - return ; - } - }; - - return ( - - {loading && ( -
- -
- )} - - {error && ( -
- -
- )} - - {threadData && !loading && ( - <> -
-
- {threadData.action_item.title} -
-
- {threadData.action_item.patient_name} - - {PRIORITY_LABEL[threadData.action_item.priority]} - -
-
- -
-
- 处理进度 -
- ({ - color: - evt.status === 'completed' - ? 'green' - : evt.status === 'in_progress' - ? 'blue' - : 'gray', - dot: timelineDot(evt.status), - children: ( -
-
- {evt.label} -
- {evt.detail && ( -
- {evt.detail} -
- )} - {evt.timestamp && ( -
- {new Date(evt.timestamp).toLocaleString('zh-CN')} -
- )} - {evt.link_to && ( - handleLinkClick(evt.link_to)} - style={{ fontSize: 12 }} - > - 查看详情 → - - )} -
- ), - }))} - /> -
- - {threadData.available_actions.length > 0 && ( -
- - {threadData.available_actions.map((action) => ( - - ))} - -
- )} - - )} -
- ); -} diff --git a/apps/web/src/components/Copilot/CopilotAlert.tsx b/apps/web/src/components/Copilot/CopilotAlert.tsx deleted file mode 100644 index b61a69a..0000000 --- a/apps/web/src/components/Copilot/CopilotAlert.tsx +++ /dev/null @@ -1,106 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { Alert, Badge, List, Button, Space, Typography, Spin } from 'antd'; -import { CheckOutlined } from '@ant-design/icons'; -import { listAlerts, dismissInsight } from '../../api/copilot'; -import type { CopilotInsight } from '../../api/copilot'; - -const severityConfig: Record = { - critical: { type: 'error', label: '危急' }, - warning: { type: 'warning', label: '警告' }, - info: { type: 'processing', label: '提示' }, -}; - -export function CopilotAlert() { - const [alerts, setAlerts] = useState([]); - const [loading, setLoading] = useState(false); - const [dismissing, setDismissing] = useState(null); - - const fetchAlerts = useCallback(async () => { - setLoading(true); - try { - const res = await listAlerts({ page_size: 50 }); - const payload = (res.data as { data?: CopilotInsight[] }).data ?? []; - setAlerts(payload); - } catch { - // 静默 - } finally { - setLoading(false); - } - }, []); - - useEffect(() => { - fetchAlerts(); - const timer = setInterval(fetchAlerts, 30_000); - return () => clearInterval(timer); - }, [fetchAlerts]); - - const handleDismiss = async (id: string) => { - setDismissing(id); - try { - await dismissInsight(id); - setAlerts((prev) => prev.filter((a) => a.id !== id)); - } finally { - setDismissing(null); - } - }; - - if (!alerts.length && !loading) return null; - - const criticalCount = alerts.filter((a) => a.severity === 'critical').length; - - return ( -
- {criticalCount > 0 && ( - - )} - {loading && alerts.length === 0 ? ( - - ) : ( - { - const config = severityConfig[item.severity] ?? severityConfig.info; - return ( - } - loading={dismissing === item.id} - onClick={() => handleDismiss(item.id)} - > - 已知悉 - , - ]} - > - - - {item.title} - - } - description={ - item.content?.suggestion ? ( - - {item.content.suggestion as string} - - ) : undefined - } - /> - - ); - }} - /> - )} -
- ); -} diff --git a/apps/web/src/components/Copilot/CopilotBadge.tsx b/apps/web/src/components/Copilot/CopilotBadge.tsx deleted file mode 100644 index 827a50f..0000000 --- a/apps/web/src/components/Copilot/CopilotBadge.tsx +++ /dev/null @@ -1,28 +0,0 @@ -import { Tag, Tooltip } from 'antd'; -import type { RiskLevel } from '../../api/copilot'; -import { useCopilotRisk } from './useCopilotRisk'; - -const levelConfig: Record = { - low: { color: 'green', label: '低风险' }, - medium: { color: 'orange', label: '中风险' }, - high: { color: 'red', label: '高风险' }, - critical: { color: '#cf1322', label: '危急' }, -}; - -interface CopilotBadgeProps { - patientId: string | undefined; -} - -export function CopilotBadge({ patientId }: CopilotBadgeProps) { - const { data, loading } = useCopilotRisk(patientId); - - if (!data || loading) return null; - - const config = levelConfig[data.level as RiskLevel] ?? levelConfig.low; - - return ( - - {config.label} {data.score}/10 - - ); -} diff --git a/apps/web/src/components/Copilot/CopilotCard.tsx b/apps/web/src/components/Copilot/CopilotCard.tsx deleted file mode 100644 index 24abc5f..0000000 --- a/apps/web/src/components/Copilot/CopilotCard.tsx +++ /dev/null @@ -1,76 +0,0 @@ -import { Card, List, Tag, Button, Empty, Spin, Typography } from 'antd'; -import { CloseOutlined } from '@ant-design/icons'; -import { useState } from 'react'; -import { dismissInsight } from '../../api/copilot'; -import type { CopilotInsight } from '../../api/copilot'; -import { useCopilotInsights } from './useCopilotInsights'; - -const severityColor: Record = { - info: 'blue', - warning: 'orange', - critical: 'red', -}; - -interface CopilotCardProps { - patientId: string | undefined; -} - -export function CopilotCard({ patientId }: CopilotCardProps) { - const { data, loading, refresh } = useCopilotInsights(patientId); - const [dismissing, setDismissing] = useState(null); - - const handleDismiss = async (id: string) => { - setDismissing(id); - try { - await dismissInsight(id); - refresh(); - } finally { - setDismissing(null); - } - }; - - return ( - - {loading ? ( - - ) : data.length === 0 ? ( - - ) : ( - ( - } - loading={dismissing === item.id} - onClick={() => handleDismiss(item.id)} - />, - ]} - > - - {item.severity} - {item.title} - - } - description={ - item.llm_supplement ? ( - - {item.llm_supplement} - - ) : undefined - } - /> - - )} - /> - )} - - ); -} diff --git a/apps/web/src/components/Copilot/index.ts b/apps/web/src/components/Copilot/index.ts deleted file mode 100644 index 888a35b..0000000 --- a/apps/web/src/components/Copilot/index.ts +++ /dev/null @@ -1,5 +0,0 @@ -export { CopilotBadge } from './CopilotBadge'; -export { CopilotCard } from './CopilotCard'; -export { CopilotAlert } from './CopilotAlert'; -export { useCopilotRisk } from './useCopilotRisk'; -export { useCopilotInsights } from './useCopilotInsights'; diff --git a/apps/web/src/components/Copilot/useCopilotInsights.ts b/apps/web/src/components/Copilot/useCopilotInsights.ts deleted file mode 100644 index 5f319b5..0000000 --- a/apps/web/src/components/Copilot/useCopilotInsights.ts +++ /dev/null @@ -1,31 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { listInsights } from '../../api/copilot'; -import type { CopilotInsight } from '../../api/copilot'; - -export function useCopilotInsights(patientId: string | undefined) { - const [data, setData] = useState([]); - const [total, setTotal] = useState(0); - const [loading, setLoading] = useState(false); - - const fetch = useCallback(async () => { - if (!patientId) return; - setLoading(true); - try { - const res = await listInsights({ patient_id: patientId, page_size: 20 }); - const payload = (res.data as { data?: CopilotInsight[]; total?: number }).data ?? []; - const total = (res.data as { total?: number }).total ?? 0; - setData(payload); - setTotal(total); - } catch { - // 静默失败 - } finally { - setLoading(false); - } - }, [patientId]); - - useEffect(() => { - fetch(); - }, [fetch]); - - return { data, total, loading, refresh: fetch }; -} diff --git a/apps/web/src/components/Copilot/useCopilotRisk.ts b/apps/web/src/components/Copilot/useCopilotRisk.ts deleted file mode 100644 index 38243c8..0000000 --- a/apps/web/src/components/Copilot/useCopilotRisk.ts +++ /dev/null @@ -1,29 +0,0 @@ -import { useState, useEffect, useCallback } from 'react'; -import { getPatientRisk } from '../../api/copilot'; -import type { RiskScore } from '../../api/copilot'; - -export function useCopilotRisk(patientId: string | undefined) { - const [data, setData] = useState(null); - const [loading, setLoading] = useState(false); - const [error, setError] = useState(null); - - const fetch = useCallback(async () => { - if (!patientId) return; - setLoading(true); - setError(null); - try { - const res = await getPatientRisk(patientId); - setData(res ?? null); - } catch (err) { - setError(err instanceof Error ? err.message : '加载风险评分失败'); - } finally { - setLoading(false); - } - }, [patientId]); - - useEffect(() => { - fetch(); - }, [fetch]); - - return { data, loading, error, refresh: fetch }; -} diff --git a/apps/web/src/components/MediaPicker/index.tsx b/apps/web/src/components/MediaPicker/index.tsx index e347ddd..1ad88e3 100644 --- a/apps/web/src/components/MediaPicker/index.tsx +++ b/apps/web/src/components/MediaPicker/index.tsx @@ -2,8 +2,17 @@ import { useState, useEffect, useCallback } from 'react'; import { Modal, Input, Upload, Image, Empty, Spin, message } from 'antd'; import { SearchOutlined, UploadOutlined } from '@ant-design/icons'; import { resolveMediaUrl } from '../../utils/media'; -import { mediaApi, type MediaItem } from '../../api/health/media'; import { uploadFile } from '../../api/upload'; +import client from '../../api/client'; + +export interface MediaItem { + id: string; + filename: string; + storage_path: string; + thumbnail_path?: string; + content_type: string; + alt_text?: string; +} interface MediaPickerProps { open: boolean; @@ -25,14 +34,16 @@ export default function MediaPicker({ open, onClose, onSelect, accept = 'image/* const loadData = useCallback(async () => { setLoading(true); try { - const result = await mediaApi.list({ - page, - page_size: PAGE_SIZE, - keyword: keyword || undefined, - content_type: accept === 'image/*' ? 'image' : undefined, + const { data } = await client.get('/media', { + params: { + page, + page_size: PAGE_SIZE, + keyword: keyword || undefined, + content_type: accept === 'image/*' ? 'image' : undefined, + }, }); - setItems(result.data); - setTotal(result.total); + setItems(data?.data ?? []); + setTotal(data?.total ?? 0); } catch { setItems([]); } finally { @@ -130,7 +141,7 @@ export default function MediaPicker({ open, onClose, onSelect, accept = 'image/* background: '#fafafa', transition: 'border-color 0.2s', }} - onMouseEnter={(e) => { (e.currentTarget.style.borderColor = '#1677ff'); }} + onMouseEnter={(e) => { (e.currentTarget.style.borderColor = 'var(--ant-color-primary)'); }} onMouseLeave={(e) => { (e.currentTarget.style.borderColor = '#f0f0f0'); }} >
diff --git a/apps/web/src/components/NotificationPanel.tsx b/apps/web/src/components/NotificationPanel.tsx index 82f9013..15de521 100644 --- a/apps/web/src/components/NotificationPanel.tsx +++ b/apps/web/src/components/NotificationPanel.tsx @@ -1,10 +1,9 @@ -import { useCallback, useEffect, useRef, useState } from 'react'; +import { useCallback, useEffect, useRef } from 'react'; import { Badge, Divider, List, Popover, Button, Empty, Typography } from 'antd'; import { BellOutlined } from '@ant-design/icons'; import { useNavigate } from 'react-router-dom'; import { useMessageStore } from '../stores/message'; import { useThemeMode } from '../hooks/useThemeMode'; -import { actionInboxApi, type ActionItem } from '../api/health/actionInbox'; const { Text } = Typography; @@ -16,42 +15,33 @@ export default function NotificationPanel() { const isDark = useThemeMode(); const initializedRef = useRef(false); - const [pendingActions, setPendingActions] = useState([]); - - const fetchPendingActions = useCallback(async () => { - try { - const resp = await actionInboxApi.list({ status: 'pending', page_size: 3 }); - setPendingActions(resp.data); - } catch { - // 静默失败,不影响通知面板 - } - }, []); - - useEffect(() => { - if (initializedRef.current) return; - initializedRef.current = true; - + const init = useCallback(async () => { const { fetchUnreadCount, fetchRecentMessages, connectSSE } = useMessageStore.getState(); fetchUnreadCount(); fetchRecentMessages(); - fetchPendingActions(); const disconnectSSE = connectSSE(); const interval = setInterval(() => { fetchUnreadCount(); fetchRecentMessages(); - fetchPendingActions(); }, 60000); return () => { clearInterval(interval); disconnectSSE(); + }; + }, []); + + useEffect(() => { + if (initializedRef.current) return; + initializedRef.current = true; + const cleanup = init(); + return () => { + cleanup.then((fn) => fn?.()); initializedRef.current = false; }; - }, [fetchPendingActions]); - - const totalBadge = unreadCount + pendingActions.length; + }, [init]); const content = (
@@ -67,7 +57,7 @@ export default function NotificationPanel() {
)} - {/* 待办预览区域 */} + {/* 待办预览 */}
navigate('/health/action-inbox')} + onClick={() => navigate('/workflow')} style={{ fontSize: 12, padding: 0 }} > 查看全部
- {pendingActions.length === 0 ? ( - - 暂无待办 - - ) : ( - ( - navigate('/health/action-inbox')} - > - {item.title}} - description={ - - {item.patient_name} - - } - /> - - )} - /> - )} + + 暂无待办 +
); @@ -236,7 +204,7 @@ export default function NotificationPanel() { e.currentTarget.style.background = 'transparent'; }} > - + ('idle'); - const [content, setContent] = useState(''); - const [errorMsg, setErrorMsg] = useState(''); - - const handleStart = useCallback(async () => { - setState('loading'); - setContent(''); - setErrorMsg(''); - - const body: Record = {}; - if (analysisType === 'lab-report' || analysisType === 'report-summary') { - body.report_id = sourceRef; - } - if (analysisType === 'trends' || analysisType === 'checkup-plan') { - body.patient_id = sourceRef; - } - if (analysisType === 'follow-up-summary') { - body.source_id = taskId || sourceRef; - } - if (metrics) { - body.metrics = metrics; - } - - try { - await startAnalysis(analysisType, body, { - onChunk: (chunk) => setContent(prev => prev + chunk), - onError: (msg) => { - setErrorMsg(msg); - setState('error'); - }, - onDone: () => setState('success'), - }); - } catch { - setErrorMsg('分析请求失败'); - setState('error'); - } - }, [analysisType, sourceRef, metrics, taskId]); - - const handleReset = useCallback(() => { - setState('idle'); - setContent(''); - setErrorMsg(''); - }, []); - - const TriggerButton = permission ? ( - - - - ) : ( - - ); - - if (state === 'idle') { - return TriggerButton; - } - - if (state === 'loading') { - return ( - -
- } /> -
- AI 正在分析... -
-
-
- ); - } - - if (state === 'error') { - return ( - - } onClick={handleStart}> - 重试 - - } - /> - - ); - } - - if (!content) { - return ; - } - - return ( - {triggerLabel}结果} - size="small" - style={{ marginTop: 12 }} - extra={ - - } - > -
- {content} -
-
- ); -} diff --git a/apps/web/src/components/ai/AiSidebar.tsx b/apps/web/src/components/ai/AiSidebar.tsx deleted file mode 100644 index 006bde1..0000000 --- a/apps/web/src/components/ai/AiSidebar.tsx +++ /dev/null @@ -1,415 +0,0 @@ -import { useState, useRef, useEffect, useCallback } from 'react'; -import { - Drawer, - Input, - Button, - Space, - Typography, - Spin, - Tag, - Card, - theme, -} from 'antd'; -import { - SendOutlined, - RobotOutlined, - DeleteOutlined, - WarningOutlined, - SafetyCertificateOutlined, -} from '@ant-design/icons'; -import { useLocation } from 'react-router-dom'; -import { aiChatApi, type ChatHistoryItem, type DisplayHint } from '../../api/ai/chat'; -import { analysisApi, type HealthSummaryResponse } from '../../api/ai/analysis'; -import { useAuthStore } from '../../stores/auth'; -import RichMessage from './RichMessage'; - -const { Text } = Typography; -const { TextArea } = Input; - -interface ChatMessage { - id: string; - role: 'user' | 'assistant'; - content: string; - displayHints?: DisplayHint[]; -} - -function extractPatientId(pathname: string): string | null { - const match = pathname.match(/\/health\/patients\/([0-9a-f-]+)/i); - return match?.[1] ?? null; -} - -const RISK_CONFIG: Record = { - low: { color: 'green', label: '低风险' }, - medium: { color: 'orange', label: '中风险' }, - high: { color: 'red', label: '高风险' }, - critical: { color: '#cf1322', label: '严重' }, -}; - -export default function AiSidebar({ - open, - onClose, -}: { - open: boolean; - onClose: () => void; -}) { - const [messages, setMessages] = useState([]); - const [input, setInput] = useState(''); - const [loading, setLoading] = useState(false); - const [summary, setSummary] = useState(null); - const [summaryLoading, setSummaryLoading] = useState(false); - const messagesEndRef = useRef(null); - const location = useLocation(); - const { token } = theme.useToken(); - - const patientId = extractPatientId(location.pathname); - const permissions = useAuthStore((s) => s.permissions); - const canChat = permissions.includes('ai.chat.send'); - const canViewSummary = permissions.includes('ai.analysis.list'); - - // 欢迎消息 - useEffect(() => { - if (open && messages.length === 0) { - setMessages([ - { - id: 'welcome', - role: 'assistant', - content: patientId - ? '你好!我是 AI 健康助手。当前已关联患者档案,你可以问我关于该患者的体征、化验报告、用药等信息。' - : '你好!我是 AI 健康助手。你可以向我咨询健康相关问题,或打开患者详情页查看患者数据。', - }, - ]); - } - }, [open]); // eslint-disable-line react-hooks/exhaustive-deps - - // 自动加载患者健康摘要 - useEffect(() => { - if (!open || !patientId || !canViewSummary) { - setSummary(null); - return; - } - let cancelled = false; - setSummaryLoading(true); - analysisApi - .getHealthSummary(patientId) - .then((data) => { - if (!cancelled) setSummary(data); - }) - .catch(() => { - if (!cancelled) setSummary(null); - }) - .finally(() => { - if (!cancelled) setSummaryLoading(false); - }); - return () => { - cancelled = true; - }; - }, [open, patientId, canViewSummary]); - - const scrollToBottom = useCallback(() => { - setTimeout(() => { - messagesEndRef.current?.scrollIntoView({ behavior: 'smooth' }); - }, 100); - }, []); - - useEffect(() => { - scrollToBottom(); - }, [messages, scrollToBottom]); - - const handleSend = async () => { - const text = input.trim(); - if (!text || loading || !canChat) return; - - const userMsg: ChatMessage = { - id: `u-${Date.now()}`, - role: 'user', - content: text, - }; - - const newMessages = [...messages, userMsg]; - setMessages(newMessages); - setInput(''); - setLoading(true); - - try { - const history: ChatHistoryItem[] = newMessages - .filter((m) => m.id !== 'welcome') - .map((m) => ({ - role: m.role, - content: m.content, - })); - - const resp = await aiChatApi.sendMessage( - text, - history, - patientId ?? undefined - ); - - setMessages((prev) => [ - ...prev, - { - id: resp.message_id, - role: 'assistant' as const, - content: resp.reply, - displayHints: resp.display_hints, - }, - ]); - } catch { - setMessages((prev) => [ - ...prev, - { - id: `err-${Date.now()}`, - role: 'assistant', - content: '抱歉,AI 服务暂时不可用,请稍后再试。', - }, - ]); - } finally { - setLoading(false); - } - }; - - const handleKeyDown = (e: React.KeyboardEvent) => { - if (e.key === 'Enter' && !e.shiftKey) { - e.preventDefault(); - handleSend(); - } - }; - - const handleClear = () => { - setMessages([ - { - id: 'welcome', - role: 'assistant', - content: '对话已清空。有什么可以帮你的?', - }, - ]); - }; - - const riskInfo = summary ? RISK_CONFIG[summary.risk_level] ?? RISK_CONFIG.low : null; - - return ( - - - AI 健康助手 - {patientId && ( - - 已关联患者 - - )} - - } - placement="right" - size={400} - open={open} - onClose={onClose} - styles={{ - body: { display: 'flex', flexDirection: 'column', padding: 0 }, - }} - extra={ -