feat(web): 透析 API + 积分账户组件 + 工作台 store + 统计页修复
- dialysis.ts: 新增透析管理 API 模块 - PointsAccountTab.tsx: 积分账户标签页组件 - workbenchStore.ts: 工作台状态管理 - StatisticsDashboard.tsx: 统计页空列表修复 - auth.test.ts: 修复权限码拼写 health.alert → health.alerts - api.test.ts: API 契约测试
This commit is contained in:
172
apps/web/src/api/health/api.test.ts
Normal file
172
apps/web/src/api/health/api.test.ts
Normal file
@@ -0,0 +1,172 @@
|
||||
/**
|
||||
* 健康模块新增 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,
|
||||
)
|
||||
})
|
||||
})
|
||||
108
apps/web/src/api/health/dialysis.ts
Normal file
108
apps/web/src/api/health/dialysis.ts
Normal file
@@ -0,0 +1,108 @@
|
||||
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<string, unknown>;
|
||||
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<DialysisRecord>;
|
||||
}>(`/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<CreateDialysisRecordReq> & { 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<string, unknown>;
|
||||
}>(`/health/dialysis-records/${id}/review`, req);
|
||||
return data.data;
|
||||
},
|
||||
};
|
||||
Reference in New Issue
Block a user