diff --git a/apps/web/src/api/health/articles.test.ts b/apps/web/src/api/health/articles.test.ts index e9d4c35..3cb9315 100644 --- a/apps/web/src/api/health/articles.test.ts +++ b/apps/web/src/api/health/articles.test.ts @@ -44,7 +44,7 @@ describe('articleApi', () => { it('create 应调用 POST /health/articles', async () => { mockPost.mockResolvedValue(fakeRes) - const req = { title: '健康饮食指南', content: '正文内容', content_type: 'markdown' } + const req = { title: '健康饮食指南', content: '正文内容', content_type: 'markdown' as const } await articleApi.create(req) expect(mockPost).toHaveBeenCalledWith('/health/articles', req) @@ -60,7 +60,7 @@ describe('articleApi', () => { it('delete 应调用 DELETE /health/articles/:id', async () => { mockDelete.mockResolvedValue({ data: { success: true, data: null } }) - await articleApi.delete('art-001') + await articleApi.delete('art-001', 1) expect(mockDelete).toHaveBeenCalledWith('/health/articles/art-001') }) diff --git a/apps/web/src/api/health/doctors.test.ts b/apps/web/src/api/health/doctors.test.ts index aabdb7b..1c9996d 100644 --- a/apps/web/src/api/health/doctors.test.ts +++ b/apps/web/src/api/health/doctors.test.ts @@ -60,7 +60,7 @@ describe('doctorApi', () => { it('delete 应调用 DELETE /health/doctors/:id', async () => { mockDelete.mockResolvedValue(undefined) - await doctorApi.delete('d-001') + await doctorApi.delete('d-001', 1) expect(mockDelete).toHaveBeenCalledWith('/health/doctors/d-001') }) diff --git a/apps/web/src/api/menus.ts b/apps/web/src/api/menus.ts index 55eac10..f159088 100644 --- a/apps/web/src/api/menus.ts +++ b/apps/web/src/api/menus.ts @@ -10,7 +10,7 @@ export interface MenuInfo { visible: boolean; menu_type: string; permission?: string; - children: MenuInfo[]; + children?: MenuInfo[]; version: number; } diff --git a/apps/web/src/components/Copilot/CopilotAlert.tsx b/apps/web/src/components/Copilot/CopilotAlert.tsx index 188606d..b61a69a 100644 --- a/apps/web/src/components/Copilot/CopilotAlert.tsx +++ b/apps/web/src/components/Copilot/CopilotAlert.tsx @@ -4,10 +4,10 @@ import { CheckOutlined } from '@ant-design/icons'; import { listAlerts, dismissInsight } from '../../api/copilot'; import type { CopilotInsight } from '../../api/copilot'; -const severityConfig: Record = { +const severityConfig: Record = { critical: { type: 'error', label: '危急' }, warning: { type: 'warning', label: '警告' }, - info: { type: 'info', label: '提示' }, + info: { type: 'processing', label: '提示' }, }; export function CopilotAlert() { diff --git a/apps/web/src/components/Copilot/useCopilotRisk.ts b/apps/web/src/components/Copilot/useCopilotRisk.ts index 26becd9..38243c8 100644 --- a/apps/web/src/components/Copilot/useCopilotRisk.ts +++ b/apps/web/src/components/Copilot/useCopilotRisk.ts @@ -13,8 +13,7 @@ export function useCopilotRisk(patientId: string | undefined) { setError(null); try { const res = await getPatientRisk(patientId); - const payload = (res.data as { data?: RiskScore }).data ?? null; - setData(payload); + setData(res ?? null); } catch (err) { setError(err instanceof Error ? err.message : '加载风险评分失败'); } finally { diff --git a/apps/web/src/components/PageContainer.tsx b/apps/web/src/components/PageContainer.tsx index bf85f25..af08451 100644 --- a/apps/web/src/components/PageContainer.tsx +++ b/apps/web/src/components/PageContainer.tsx @@ -12,6 +12,7 @@ interface PageContainerProps { batchActions?: React.ReactNode; selectedCount?: number; onClearSelection?: () => void; + onBack?: () => void; children: React.ReactNode; loading?: boolean; } @@ -26,6 +27,7 @@ export function PageContainer({ batchActions, selectedCount, onClearSelection, + onBack, children, loading, }: PageContainerProps) { @@ -36,6 +38,11 @@ export function PageContainer({
+ {onBack && ( + + )} {title} {subtitle && ( diff --git a/apps/web/src/layouts/MainLayout.tsx b/apps/web/src/layouts/MainLayout.tsx index 46afeb0..ed749db 100644 --- a/apps/web/src/layouts/MainLayout.tsx +++ b/apps/web/src/layouts/MainLayout.tsx @@ -43,6 +43,8 @@ const routeTitleFallback: Record = { '/health/follow-up-records': '随访记录', '/health/article-categories': '分类管理', '/health/article-tags': '标签管理', + '/health/schedules': '排班管理', + '/health/appointments': '预约管理', }; function getTitleFromMenus(path: string, menus: MenuInfo[]): string | undefined { diff --git a/apps/web/src/pages/health/AlertRuleList.test.tsx b/apps/web/src/pages/health/AlertRuleList.test.tsx index 59b8e5b..b81442e 100644 --- a/apps/web/src/pages/health/AlertRuleList.test.tsx +++ b/apps/web/src/pages/health/AlertRuleList.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { screen, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { createListPageTests } from '../../test/factories/listPageTests'; import { createFixtureList, createAlertRuleFixture } from '../../test/fixtures'; import { renderWithProviders } from '../../test/utils/renderWithProviders'; diff --git a/apps/web/src/pages/health/ArticleManageList.test.tsx b/apps/web/src/pages/health/ArticleManageList.test.tsx index d66baa4..769dd73 100644 --- a/apps/web/src/pages/health/ArticleManageList.test.tsx +++ b/apps/web/src/pages/health/ArticleManageList.test.tsx @@ -1,5 +1,5 @@ import { describe, it, expect, vi } from 'vitest'; -import { screen, waitFor } from '@testing-library/react'; +import { waitFor } from '@testing-library/react'; import { createListPageTests } from '../../test/factories/listPageTests'; import { createFixtureList, createArticleFixture } from '../../test/fixtures'; import { renderWithProviders } from '../../test/utils/renderWithProviders'; diff --git a/apps/web/src/pages/health/BannerManage.tsx b/apps/web/src/pages/health/BannerManage.tsx index a628ffc..fb8957c 100644 --- a/apps/web/src/pages/health/BannerManage.tsx +++ b/apps/web/src/pages/health/BannerManage.tsx @@ -430,7 +430,7 @@ export default function BannerManage() { rowClassName={(record) => record.status === 'inactive' ? 'ant-table-row-inactive' - : undefined + : '' } /> diff --git a/apps/web/src/pages/health/BleGatewayDetail.tsx b/apps/web/src/pages/health/BleGatewayDetail.tsx index 7cefcbc..0803604 100644 --- a/apps/web/src/pages/health/BleGatewayDetail.tsx +++ b/apps/web/src/pages/health/BleGatewayDetail.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { - Button, Descriptions, Form, Input, message, Modal, Popconfirm, Select, Space, Table, Tag, Tabs, + Button, Descriptions, Form, Input, message, Modal, Popconfirm, Select, Table, Tag, Tabs, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; diff --git a/apps/web/src/pages/health/CriticalValueThresholdList.tsx b/apps/web/src/pages/health/CriticalValueThresholdList.tsx index c7778c2..40b0923 100644 --- a/apps/web/src/pages/health/CriticalValueThresholdList.tsx +++ b/apps/web/src/pages/health/CriticalValueThresholdList.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect } from 'react'; import { - Button, Form, Input, InputNumber, message, Modal, Popconfirm, Result, Select, Space, Switch, Table, Tag, + Button, Form, Input, InputNumber, message, Modal, Popconfirm, Result, Select, Space, Table, Tag, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; diff --git a/apps/web/src/pages/health/DeviceManage.tsx b/apps/web/src/pages/health/DeviceManage.tsx index 199dd82..be9d991 100644 --- a/apps/web/src/pages/health/DeviceManage.tsx +++ b/apps/web/src/pages/health/DeviceManage.tsx @@ -1,5 +1,5 @@ import { useCallback, useEffect, useState } from 'react'; -import { Button, Input, message, Popconfirm, Result, Select, Space, Table, Tag, Badge } from 'antd'; +import { Button, message, Popconfirm, Result, Select, Space, Table, Tag, Badge } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; diff --git a/apps/web/src/pages/health/DoctorList.tsx b/apps/web/src/pages/health/DoctorList.tsx index ad423cb..a8fa14e 100644 --- a/apps/web/src/pages/health/DoctorList.tsx +++ b/apps/web/src/pages/health/DoctorList.tsx @@ -191,9 +191,9 @@ export default function DoctorList() { }; // ---- 删除 ---- - const handleDelete = async (id: string) => { + const handleDelete = async (id: string, version: number) => { try { - await doctorApi.delete(id); + await doctorApi.delete(id, version); message.success('删除成功'); refresh(); } catch { @@ -286,7 +286,7 @@ export default function DoctorList() { handleDelete(record.id)} + onConfirm={() => handleDelete(record.id, record.version)} okText="确定" cancelText="取消" > diff --git a/apps/web/src/pages/health/FamilyProxyPage.tsx b/apps/web/src/pages/health/FamilyProxyPage.tsx index 02732e9..c56024b 100644 --- a/apps/web/src/pages/health/FamilyProxyPage.tsx +++ b/apps/web/src/pages/health/FamilyProxyPage.tsx @@ -1,6 +1,6 @@ import { useState, useCallback } from 'react'; import { - Button, Card, Descriptions, Form, Input, message, Modal, Popconfirm, Result, Select, Space, Table, Tag, + Button, Card, Descriptions, Form, message, Modal, Popconfirm, Result, Select, Space, Table, Tag, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; import dayjs from 'dayjs'; diff --git a/apps/web/src/pages/health/FollowUpTemplateList.tsx b/apps/web/src/pages/health/FollowUpTemplateList.tsx index 71f5743..5e34723 100644 --- a/apps/web/src/pages/health/FollowUpTemplateList.tsx +++ b/apps/web/src/pages/health/FollowUpTemplateList.tsx @@ -4,7 +4,7 @@ import { Popconfirm, InputNumber, Switch, Card, Typography, } from 'antd'; import { - PlusOutlined, DeleteOutlined, EditOutlined, + PlusOutlined, DeleteOutlined, } from '@ant-design/icons'; import type { ColumnsType } from 'antd/es/table'; import { @@ -35,9 +35,9 @@ const FIELD_TYPE_OPTIONS = [ { value: 'scale', label: '量表' }, ]; -function FieldEditor({ value, onChange }: { - value: TemplateFieldReq[]; - onChange: (v: TemplateFieldReq[]) => void; +function FieldEditor({ value = [], onChange = () => {} }: { + value?: TemplateFieldReq[]; + onChange?: (v: TemplateFieldReq[]) => void; }) { const add = () => { onChange([...value, { diff --git a/apps/web/src/pages/health/MediaLibrary.tsx b/apps/web/src/pages/health/MediaLibrary.tsx index 87d9f44..5c7ceba 100644 --- a/apps/web/src/pages/health/MediaLibrary.tsx +++ b/apps/web/src/pages/health/MediaLibrary.tsx @@ -194,9 +194,9 @@ export default function MediaLibrary() {
{foldersLoading ?
: ( - { + titleRender={(node: any) => { if (node.id === '__all__') return {node.name}; const matched = folders.find((f) => f.id === node.id); return ( diff --git a/apps/web/src/pages/health/MedicationRecordList.tsx b/apps/web/src/pages/health/MedicationRecordList.tsx index c6175a7..165d48d 100644 --- a/apps/web/src/pages/health/MedicationRecordList.tsx +++ b/apps/web/src/pages/health/MedicationRecordList.tsx @@ -1,4 +1,4 @@ -import { useState, useCallback, useEffect, useMemo } from 'react'; +import { useState, useCallback, useMemo } from 'react'; import { Button, DatePicker, Form, Input, message, Modal, Popconfirm, Result, Select, Space, Switch, Table, Tag, diff --git a/apps/web/src/pages/health/PatientList.test.tsx b/apps/web/src/pages/health/PatientList.test.tsx index 5badc4f..dfdc78e 100644 --- a/apps/web/src/pages/health/PatientList.test.tsx +++ b/apps/web/src/pages/health/PatientList.test.tsx @@ -1,5 +1,3 @@ -import { http, HttpResponse } from 'msw'; -import { server } from '../../test/mocks/server'; import { createListPageTests } from '../../test/factories/listPageTests'; import { createFixtureList, createPatientFixture } from '../../test/fixtures'; import PatientList from './PatientList'; diff --git a/apps/web/src/pages/health/RealtimeMonitor.tsx b/apps/web/src/pages/health/RealtimeMonitor.tsx index 17b3f50..43bc68f 100644 --- a/apps/web/src/pages/health/RealtimeMonitor.tsx +++ b/apps/web/src/pages/health/RealtimeMonitor.tsx @@ -5,7 +5,7 @@ import { useVitalSSE } from '../../hooks/useVitalSSE'; import { usePermission } from '../../hooks/usePermission'; import { alertApi, type Alert } from '../../api/health/alerts'; import { PageContainer } from '../../components/PageContainer'; -import { SEVERITY_COLOR, SEVERITY_LABEL, VITAL_CARD_METRICS } from '../../constants/health'; +import { VITAL_CARD_METRICS } from '../../constants/health'; interface PatientAlertSummary { patient_id: string; @@ -22,7 +22,7 @@ interface PatientAlertSummary { */ export default function RealtimeMonitor() { const { hasPermission } = usePermission('health.alerts.list'); - const [alerts, setAlerts] = useState([]); + const [_alerts, setAlerts] = useState([]); const [loading, setLoading] = useState(true); const [selectedPatientId, setSelectedPatientId] = useState(null); const [alertSummary, setAlertSummary] = useState([]); diff --git a/apps/web/src/pages/health/ShiftList.tsx b/apps/web/src/pages/health/ShiftList.tsx index d3e9766..19c242e 100644 --- a/apps/web/src/pages/health/ShiftList.tsx +++ b/apps/web/src/pages/health/ShiftList.tsx @@ -1,6 +1,6 @@ import { useState, useCallback, useEffect, useMemo } from 'react'; import { - Badge, Button, DatePicker, Form, Input, message, Modal, Popconfirm, + Button, DatePicker, Form, Input, message, Modal, Popconfirm, Result, Select, Space, Table, Tag, } from 'antd'; import type { ColumnsType } from 'antd/es/table'; diff --git a/apps/web/src/pages/health/articleEditor/ArticleStyleLibrary.tsx b/apps/web/src/pages/health/articleEditor/ArticleStyleLibrary.tsx index d34aeba..e96cb51 100644 --- a/apps/web/src/pages/health/articleEditor/ArticleStyleLibrary.tsx +++ b/apps/web/src/pages/health/articleEditor/ArticleStyleLibrary.tsx @@ -14,7 +14,7 @@ interface ArticleStyleLibraryProps { onThemeChange: (themeId: string) => void; } -const sectionLabel = (text: string, isDark: boolean) => ({ +const sectionLabel = (_text: string, isDark: boolean) => ({ fontSize: 12, fontWeight: 600, color: isDark ? '#64748b' : '#86868b', diff --git a/apps/web/src/pages/health/articleEditor/styledBlockPlugin.ts b/apps/web/src/pages/health/articleEditor/styledBlockPlugin.ts index f56227b..e12930e 100644 --- a/apps/web/src/pages/health/articleEditor/styledBlockPlugin.ts +++ b/apps/web/src/pages/health/articleEditor/styledBlockPlugin.ts @@ -19,7 +19,7 @@ function parseStyleStr(str: string): Record { const renderElemConf = { type: TYPE, renderElem(elemNode: SlateElement): VNode { - const node = elemNode as Record; + const node = elemNode as unknown as Record; const style = (node.style as string) || ''; const innerHtml = (node.innerHtml as string) || ''; return h( @@ -44,7 +44,7 @@ const renderElemConf = { const elemToHtmlConf = { type: TYPE, elemToHtml(elemNode: SlateElement): string { - const node = elemNode as Record; + const node = elemNode as unknown as Record; const style = (node.style as string) || ''; const innerHtml = (node.innerHtml as string) || ''; return `
${innerHtml}
`; @@ -53,7 +53,7 @@ const elemToHtmlConf = { const parseElemHtmlConf = { selector: `div[data-w-e-type="${TYPE}"]`, - parseElemHtml($elem: HTMLElement): SlateElement { + parseElemHtml($elem: Element): SlateElement { return { type: TYPE, style: $elem.getAttribute('style') || '', diff --git a/apps/web/src/pages/health/components/workbench/ActionDetailDrawer.tsx b/apps/web/src/pages/health/components/workbench/ActionDetailDrawer.tsx index 05cd9f2..ac71997 100644 --- a/apps/web/src/pages/health/components/workbench/ActionDetailDrawer.tsx +++ b/apps/web/src/pages/health/components/workbench/ActionDetailDrawer.tsx @@ -38,6 +38,7 @@ interface ActionDetailDrawerProps { open: boolean; onClose: () => void; onActionComplete?: () => void; + onRefresh?: () => void; } export default function ActionDetailDrawer({ @@ -45,6 +46,7 @@ export default function ActionDetailDrawer({ open, onClose, onActionComplete, + onRefresh: _onRefresh, }: ActionDetailDrawerProps) { const [thread, setThread] = useState(null); const [loading, setLoading] = useState(false); diff --git a/apps/web/src/stores/app.test.ts b/apps/web/src/stores/app.test.ts index 2f7cbfd..60bd8f8 100644 --- a/apps/web/src/stores/app.test.ts +++ b/apps/web/src/stores/app.test.ts @@ -7,7 +7,7 @@ * - 侧边栏折叠状态 * - 远程主题配置加载 */ -import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest' +import { describe, it, expect, vi, beforeEach } from 'vitest' // --- Mock localStorage --- const localStorageStore: Record = {} @@ -39,7 +39,8 @@ vi.mock('../api/themes', () => ({ // --- Mock zustand 内部不依赖真实存储 --- // 在 mock 生效后导入被测模块 import { useAppStore, THEME_OPTIONS } from './app' -import type { ThemeName, ThemeConfig } from './app' +import type { ThemeName } from './app' +import type { ThemeConfig } from '../api/themes' beforeEach(() => { vi.clearAllMocks() diff --git a/apps/web/src/stores/message.test.ts b/apps/web/src/stores/message.test.ts index 5869b85..86925b2 100644 --- a/apps/web/src/stores/message.test.ts +++ b/apps/web/src/stores/message.test.ts @@ -312,7 +312,7 @@ describe('connectSSE', () => { it('有 token 时创建 EventSource 并传递正确 URL', () => { localStorage.setItem('access_token', 'my-jwt-token') - process.env.VITE_API_BASE_URL = undefined + vi.stubEnv('VITE_API_BASE_URL', '') const dispose = useMessageStore.getState().connectSSE() @@ -339,7 +339,7 @@ describe('connectSSE', () => { const dispose = useMessageStore.getState().connectSSE() const eventTypes = mockEventSourceAddEventListener.mock.calls.map( - (call: [string, unknown]) => call[0], + (call: any[]) => call[0], ) expect(eventTypes).toContain('message') expect(eventTypes).toContain('alert') @@ -355,7 +355,7 @@ describe('connectSSE', () => { // 找到 message 事件的回调 const messageCall = mockEventSourceAddEventListener.mock.calls.find( - (call: [string, unknown]) => call[0] === 'message', + (call: any[]) => call[0] === 'message', ) const messageCallback = messageCall![1] as () => void @@ -376,7 +376,7 @@ describe('connectSSE', () => { const dispose = useMessageStore.getState().connectSSE() const alertCall = mockEventSourceAddEventListener.mock.calls.find( - (call: [string, unknown]) => call[0] === 'alert', + (call: any[]) => call[0] === 'alert', ) const alertCallback = alertCall![1] as () => void diff --git a/apps/web/src/stores/plugin.test.ts b/apps/web/src/stores/plugin.test.ts index a38eec9..6953b55 100644 --- a/apps/web/src/stores/plugin.test.ts +++ b/apps/web/src/stores/plugin.test.ts @@ -355,7 +355,7 @@ describe('usePluginStore', () => { const schema = createFakeSchema({ ui: { pages: [ - { type: 'tabs' as const, label: '综合视图', icon: 'LayoutOutlined' }, + { type: 'tabs' as const, label: '综合视图', icon: 'LayoutOutlined', tabs: [] }, ], }, }); diff --git a/apps/web/src/test/setup.ts b/apps/web/src/test/setup.ts index a688822..f65421b 100644 --- a/apps/web/src/test/setup.ts +++ b/apps/web/src/test/setup.ts @@ -18,7 +18,7 @@ Object.defineProperty(window, 'matchMedia', { }); // ResizeObserver mock(Ant Design Table 依赖) -global.ResizeObserver = class ResizeObserver { +globalThis.ResizeObserver = class ResizeObserver { observe() {} unobserve() {} disconnect() {} diff --git a/apps/web/src/test/utils/renderWithProviders.tsx b/apps/web/src/test/utils/renderWithProviders.tsx index a876bdf..0cb70be 100644 --- a/apps/web/src/test/utils/renderWithProviders.tsx +++ b/apps/web/src/test/utils/renderWithProviders.tsx @@ -1,5 +1,5 @@ -import { ReactElement } from 'react'; -import { render, RenderOptions } from '@testing-library/react'; +import type { ReactElement } from 'react'; +import { render, type RenderOptions } from '@testing-library/react'; import { MemoryRouter } from 'react-router-dom'; import { ConfigProvider } from 'antd'; import zhCN from 'antd/locale/zh_CN'; diff --git a/apps/web/vitest.config.ts b/apps/web/vitest.config.ts index d0a3d4b..0ea2c95 100644 --- a/apps/web/vitest.config.ts +++ b/apps/web/vitest.config.ts @@ -9,6 +9,8 @@ export default defineConfig({ globals: true, setupFiles: ['./src/test/setup.ts'], exclude: ['e2e/**', 'node_modules/**'], + testTimeout: 15000, + poolTimeout: 60000, coverage: { provider: 'v8', reporter: ['text', 'lcov'],