import { getCampuses } from '@/api/management/organization' import { unwrapApiResponse } from '@/utils/api-response' import { mockFlags } from '@/utils/mock-flags' import { request } from '@/utils/request' import type { ApiResponse } from '@/types/common' import type { BackendSigningMethod, BackendSigningTaskStatus, HomeRankingPeriod, SigningMethod, WorkbenchCampus, WorkbenchDocumentRanking, WorkbenchOverviewQuery, WorkbenchOverviewRequestDto, WorkbenchOverviewResponse, WorkbenchOverviewResponseDto, WorkbenchTodoStatus, WorkbenchTodoTask, WorkbenchTrendPoint, } from './types' const useMockData = mockFlags.workbenchHome interface MockCampusOverview { summary: WorkbenchOverviewResponse['summary'] /** 趋势的日均人次基数(按「院区 + 日期」播种出每天的具体值,见 createTrend) */ trendBase: number todos: WorkbenchTodoTask[] ranking: Array & { baseCount: number }> } const mockCampusOverviews: Record = { 本部院区: { summary: { todaySigned: 36, todaySignedChange: 2, pendingPatientSigning: 18, todayOverdue: 2, availableTemplates: 128, coveredDepartments: 9, }, trendBase: 32, todos: [ { id: 'T0818003', patientId: 'P202610005', patientName: '陈志强', documentName: '住院须知及告知书', department: '消化内科', status: 'pending', method: 'pad', updatedAt: '2026-08-27 11:20', }, { id: 'T0818002', patientId: 'P202610002', patientName: '李淑芬', documentName: 'MRI 磁共振检查知情同意书', department: '放射科', status: 'pending', method: 'sms', updatedAt: '2026-08-27 10:05', }, { id: 'T0815001', patientId: 'P202610001', patientName: '王建国', documentName: '心包穿刺知情同意书', department: '心内科', status: 'overdue', method: 'sms', updatedAt: '2026-08-27 09:40', }, { // Pad 移动端:待办里要四种方式都有样本,徽标配色才看得出区别 id: 'T0818004', patientId: 'P202610003', patientName: '赵某某', documentName: '住院须知及告知书', department: '消化内科', status: 'pending', method: 'ipad', updatedAt: '2026-08-27 09:05', }, { id: 'T0818005', patientId: 'P202610007', patientName: '孙浩然', documentName: '内镜检查知情同意书', department: '普外科', status: 'pending', method: 'wx', updatedAt: '2026-08-27 08:50', }, ], ranking: [ { id: 'doc-001', documentName: '住院患者知情同意书', department: '全院通用', baseCount: 58 }, { id: 'doc-002', documentName: '入院须知及告知书', department: '全院通用', baseCount: 51 }, { id: 'doc-003', documentName: 'MRI 磁共振检查知情同意书', department: '放射科', baseCount: 43, }, { id: 'doc-004', documentName: '手术知情同意书', department: '普外科', baseCount: 39 }, { id: 'doc-005', documentName: '麻醉知情同意书', department: '麻醉科', baseCount: 35 }, { id: 'doc-006', documentName: '患者授权委托书', department: '医务处', baseCount: 31 }, { id: 'doc-007', documentName: '输血治疗知情同意书', department: '血液科', baseCount: 27 }, { id: 'doc-008', documentName: '内镜检查知情同意书', department: '消化内科', baseCount: 24 }, { id: 'doc-009', documentName: '介入治疗知情同意书', department: '心内科', baseCount: 21 }, { id: 'doc-010', documentName: '特殊检查风险告知书', department: '影像科', baseCount: 18 }, ], }, 东院区: { summary: { todaySigned: 22, todaySignedChange: 1, pendingPatientSigning: 11, todayOverdue: 1, availableTemplates: 76, coveredDepartments: 6, }, trendBase: 20, todos: [ { id: 'T0818001', patientId: 'P202610004', patientName: '赵美英', documentName: '体检报告发放及知情同意书', department: '健康管理中心', status: 'pending', method: 'pad', updatedAt: '2026-08-27 09:12', }, { id: 'T0818011', patientId: 'P202610011', patientName: '周雅琴', documentName: '检查前注意事项告知书', department: '体检中心', status: 'overdue', method: 'sms', updatedAt: '2026-08-27 08:30', }, { id: 'T0818012', patientId: 'P202610003', patientName: '赵某某', documentName: '住院须知及告知书', department: '消化内科', status: 'pending', method: 'ipad', updatedAt: '2026-08-27 09:05', }, { id: 'T0818013', patientId: 'P202610007', patientName: '孙浩然', documentName: '内镜检查知情同意书', department: '普外科', status: 'pending', method: 'wx', updatedAt: '2026-08-27 08:50', }, ], ranking: [ { id: 'doc-011', documentName: '体检报告发放及知情同意书', department: '健康管理中心', baseCount: 46, }, { id: 'doc-012', documentName: '检前告知书', department: '健康管理中心', baseCount: 39 }, { id: 'doc-013', documentName: '超声检查知情同意书', department: '超声医学科', baseCount: 32, }, { id: 'doc-014', documentName: '胃肠镜检查知情同意书', department: '消化内科', baseCount: 28, }, { id: 'doc-015', documentName: '采血及检验告知书', department: '检验科', baseCount: 25 }, { id: 'doc-016', documentName: '影像检查告知书', department: '影像科', baseCount: 21 }, { id: 'doc-017', documentName: '健康体检知情同意书', department: '体检中心', baseCount: 19 }, { id: 'doc-018', documentName: '隐私保护告知书', department: '健康管理中心', baseCount: 16 }, { id: 'doc-019', documentName: '麻醉检查知情同意书', department: '麻醉科', baseCount: 14 }, { id: 'doc-020', documentName: '报告领取授权书', department: '健康管理中心', baseCount: 12 }, ], }, 西院区: { summary: { todaySigned: 17, todaySignedChange: 2, pendingPatientSigning: 9, todayOverdue: 2, availableTemplates: 54, coveredDepartments: 5, }, trendBase: 15, todos: [ { id: 'T0818021', patientId: 'P202610021', patientName: '孙浩然', documentName: '心脏介入手术知情同意书', department: '心内科', status: 'pending', method: 'pad', updatedAt: '2026-08-27 10:15', }, { id: 'T0818022', patientId: 'P202610022', patientName: '刘晓琴', documentName: '骨科手术知情同意书', department: '骨科', status: 'pending', method: 'sms', updatedAt: '2026-08-27 09:52', }, { id: 'T0818023', patientId: 'P202610023', patientName: '何文博', documentName: '术前麻醉访视告知书', department: '麻醉科', status: 'overdue', method: 'pad', updatedAt: '2026-08-27 08:05', }, { id: 'T0818024', patientId: 'P202610003', patientName: '赵某某', documentName: '住院须知及告知书', department: '消化内科', status: 'pending', method: 'ipad', updatedAt: '2026-08-27 09:05', }, { id: 'T0818025', patientId: 'P202610007', patientName: '孙浩然', documentName: '内镜检查知情同意书', department: '普外科', status: 'pending', method: 'wx', updatedAt: '2026-08-27 08:50', }, ], ranking: [ { id: 'doc-021', documentName: '心脏介入手术知情同意书', department: '心内科', baseCount: 37, }, { id: 'doc-022', documentName: '骨科手术知情同意书', department: '骨科', baseCount: 33 }, { id: 'doc-023', documentName: '术前麻醉访视告知书', department: '麻醉科', baseCount: 28 }, { id: 'doc-024', documentName: '住院患者知情同意书', department: '全院通用', baseCount: 25 }, { id: 'doc-025', documentName: '输血治疗知情同意书', department: '血液科', baseCount: 22 }, { id: 'doc-026', documentName: '介入治疗知情同意书', department: '心内科', baseCount: 20 }, { id: 'doc-027', documentName: '术后康复告知书', department: '骨科', baseCount: 18 }, { id: 'doc-028', documentName: '特殊检查风险告知书', department: '影像科', baseCount: 16 }, { id: 'doc-029', documentName: '患者授权委托书', department: '医务处', baseCount: 14 }, { id: 'doc-030', documentName: '手术部位标识确认书', department: '手术部', baseCount: 12 }, ], }, } function getPeriodFactor(period: HomeRankingPeriod) { return period === 7 ? 1 : period === 14 ? 2 : 3.5 } /** 「院区 + 日期」→ 固定种子:同一天同一院区的数值不随刷新、切换时段或翻页而变 */ function hashSeed(text: string) { let hash = 2166136261 for (let index = 0; index < text.length; index += 1) { hash ^= text.charCodeAt(index) hash = Math.imul(hash, 16777619) } return hash >>> 0 } /** mulberry32:小、快、同种子可复现(与 api/workbench/report-analytics.ts 同一套做法) */ function createRandom(seed: number) { let state = seed >>> 0 return () => { state = (state + 0x6d2b79f5) >>> 0 let t = state t = Math.imul(t ^ (t >>> 15), t | 1) t ^= t + Math.imul(t ^ (t >>> 7), t | 61) return ((t ^ (t >>> 14)) >>> 0) / 4294967296 } } /** * 演示趋势:近 period 天(含今天)的已签署人次。 * * 为什么不用写死的日期数组:写死 8/21–8/27 之后,演示日一过「近 7 天」窗口就全是过期日期 * (仓库的演示数据铁律:**日期相对** + **确定性**,不要 Math.random)。 * 末点取今日已签署,与指标卡同源 —— 原型里趋势最后一点就是今天已签署的任务数。 */ function createTrend( campus: WorkbenchCampus, summary: WorkbenchOverviewResponse['summary'], period: HomeRankingPeriod, ): WorkbenchTrendPoint[] { const points: WorkbenchTrendPoint[] = [] const base = (mockCampusOverviews[campus] ?? mockCampusOverviews['本部院区']).trendBase for (let offset = period - 1; offset >= 0; offset -= 1) { const day = new Date() day.setDate(day.getDate() - offset) const label = `${day.getMonth() + 1}/${day.getDate()}` const value = offset === 0 ? summary.todaySigned : Math.max(base + Math.round(createRandom(hashSeed(`${campus}-${label}`))() * 12 - 5), 0) points.push({ label, value }) } return points } function createRanking( rows: MockCampusOverview['ranking'], period: HomeRankingPeriod, ): WorkbenchDocumentRanking[] { const factor = getPeriodFactor(period) return rows .map((row, index) => ({ id: row.id, documentName: row.documentName, department: row.department, signedCount: Math.round(row.baseCount * factor + (index % 3)), })) .sort((left, right) => right.signedCount - left.signedCount) } /** * 首页「我的待办」最多几条。 * * 原型 `renderDash()` 是 `tasks.slice(0, 5)`(最多 5 条)。 * 仓库收成 **3 条**(2026-09-30 定):趋势卡与待办卡同行,待办越多行高越高, * 图是按宽度定高的、贴底后会在标题与图之间留大片空白 —— 3 条时两张卡高度基本齐平。 * 该上限同时作为 `todoLimit` 传给后端,避免取回一批再丢掉。 */ const TODO_LIMIT = 3 const RANKING_LIMIT = 10 let campusIds: Map | null = null let campusIdsRequest: Promise> | null = null function mapTodoStatus(status: BackendSigningTaskStatus): WorkbenchTodoStatus { switch (status) { case 'CREATED': return 'created' case 'WAITING_SIGN': return 'pending' case 'GENERATING': return 'generating' case 'EXPIRED': return 'overdue' case 'SIGNED': return 'signed' case 'VOIDED': return 'voided' case 'FAILED': default: return 'failed' } } const METHOD_BY_BACKEND: Record = { PAD: 'pad', SMS: 'sms', IPAD: 'ipad', WX: 'wx', } function mapSigningMethod(method: BackendSigningMethod): SigningMethod { // 后端理论上只回这四个;未知取值按手写板兜底,避免整行渲染不出来 return METHOD_BY_BACKEND[method] ?? 'pad' } function mapWorkbenchOverview(dto: WorkbenchOverviewResponseDto): WorkbenchOverviewResponse { const summary = dto.summary return { summary: { todaySigned: summary?.todaySigned ?? 0, todaySignedChange: summary?.todaySignedChange ?? 0, pendingPatientSigning: summary?.pendingPatientSigning ?? 0, todayOverdue: summary?.todayOverdue ?? 0, availableTemplates: summary?.availableTemplates ?? 0, coveredDepartments: summary?.coveredDepartments ?? 0, }, trend: (dto.trend ?? []).map((point) => ({ label: point.label || point.date, value: point.signedCount, })), todos: (dto.todos ?? []).map((task) => ({ id: task.taskId, patientId: task.patientId, patientName: task.patientName, documentName: task.documentName, department: task.departmentName, status: mapTodoStatus(task.status), method: mapSigningMethod(task.signMethod), updatedAt: task.updatedAt, })), documentRanking: (dto.documentRanking ?? []).map((item) => ({ id: item.templateId, documentName: item.documentName, department: item.departmentName, signedCount: item.signedCount, })), } } function getCampusIds() { if (campusIds) { return Promise.resolve(campusIds) } if (!campusIdsRequest) { campusIdsRequest = getCampuses() .then((campuses) => { campusIds = new Map(campuses.map((campus) => [campus.name, campus.id])) return campusIds }) .finally(() => { campusIdsRequest = null }) } return campusIdsRequest } async function resolveCampusId(campusName: WorkbenchCampus) { const campusId = (await getCampusIds()).get(campusName) if (!campusId) { throw new Error('未找到院区:' + campusName) } return campusId } export async function getWorkbenchOverview( query: WorkbenchOverviewQuery, ): Promise { if (useMockData) { const overview = mockCampusOverviews[query.campus] ?? mockCampusOverviews['本部院区'] const summary = { ...overview.summary } return { summary, trend: createTrend(query.campus, summary, query.rankingPeriod), todos: overview.todos.slice(0, TODO_LIMIT).map((task) => ({ ...task })), documentRanking: createRanking(overview.ranking, query.rankingPeriod), } } const campusId = query.campusId || (await resolveCampusId(query.campus)) const params: WorkbenchOverviewRequestDto = { campusId, period: query.rankingPeriod, todoLimit: TODO_LIMIT, rankingLimit: RANKING_LIMIT, } const response = await request.get>( '/v1/workbench/overview', { params }, ) return mapWorkbenchOverview(unwrapApiResponse(response)) }