import assert from 'node:assert/strict'; import { readFileSync } from 'node:fs'; import { join } from 'node:path'; const root = new URL('..', import.meta.url).pathname.replace(/^\/([A-Za-z]:)/, '$1'); const read = (path) => readFileSync(join(root, path), 'utf8'); const packageJson = JSON.parse(read('admin/package.json')); assert.equal(packageJson.dependencies.vue.startsWith('^3.'), true); assert.ok(packageJson.dependencies['element-plus']); assert.ok(packageJson.dependencies['@lucide/vue']); const envExample = read('admin/.env.example'); assert.match(envExample, /https:\/\/api\.txyundm\.cn\/admin-api/); assert.doesNotMatch(envExample, /localhost|127\.0\.0\.1|http:\/\//); const vite = read('admin/vite.config.ts'); assert.match(vite, /base: '\/admin\/'/); assert.match(vite, /'\/admin-api'/); const app = read('admin/src/App.vue'); for (const pattern of [ 'CleaningTasksPanel', 'CleaningSettlementsPanel', 'CleaningStatisticsPanel', 'CleanersPanel', 'qipai.admin.token', 'loadTasks', 'loadSettlements', 'loadStatistics', 'loadCleaners', 'handleSyncTransfer', 'handleSyncTransferMany', 'handleCompleteMany', 'handleRejectMany', '运营待处理', 'jumpToTaskStatus', 'jumpToSettlementStatus', 'draftSettlementTotal', 'operationalQueueTotal', 'exportOperationalQueue', 'cleaning-operational-queue', '导出待处理', 'settlementFilters.payoutState', '待确认结算', '待发放金额', 'CleaningFieldHandoffPanel', '联调' ]) { assert.match(app, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const api = read('admin/src/api.ts'); for (const route of [ '/cleaning/tasks?', '/cleaning/statistics', 'storeId', 'cleanerUserId', '/users?', '/staff', '/reset-sessions', '/assign', '/complete', '/reject', '/members', '/cleaning/reclaim-timeouts', '/cleaning/settlements?', '/cleaning/settlements', 'payoutState', '/wechat-transfer', '/wechat-transfer/preflight', '/wechat-transfer/sync', '/payout-failure' ]) { assert.match(api, new RegExp(route.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const tasks = read('admin/src/components/CleaningTasksPanel.vue'); for (const pattern of [ '指派', '验收', '驳回', '回收', '成员', '保洁任务详情', '操作流水', '协作分账', 'listCleaningTaskEvents', 'listCleaningTaskMembers', 'loadingEvents', 'loadingMembers', '批量验收', '批量驳回', 'selectedSubmittedTasks', 'complete-many', 'reject-many', 'photo-grid', 'timeline-grid', 'el-image', '协作者管理', 'photoUrls', 'el-select', 'cleanerLabel', 'filterDraft', '门店 ID', '筛选', '导出', 'exportTasks', 'exportTaskDetail', 'exportTaskEvents', 'exportTaskMembers', 'cleaning-tasks', 'cleaning-task-detail', 'cleaning-task-events', 'cleaning-task-members', '导出详情', '导出成员', 'downloadCsv', 'csvCell', 'photoUrls?.length' ]) { assert.match(tasks, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const settlements = read('admin/src/components/CleaningSettlementsPanel.vue'); for (const pattern of [ '详情', '生成', '确认', '转账', '同步', '批量同步', 'selectedSyncableSettlements', 'sync-transfer-many', '失败', '结算单详情', '微信确认参数', 'payoutPackageInfo', 'getCleaningSettlementDetail', '微信转账预检', 'preflightWechatTransfer', '凭据引用', '授权状态', '通知URL', '平台证书', '保洁员OpenID', 'credentialRefMasked', 'platformCertificateCount', 'configuredText', '导出预检', 'exportPreflight', 'cleaning-transfer-preflight', 'transferPreflightTitle', 'transferCanSubmit', 'preflightLoading', 'listCleaningSettlementCandidates', 'candidateDialog', 'useCandidateFilters', 'exportSettlements', 'exportCandidates', 'exportSettlementDetail', 'cleaning-settlement-detail', 'downloadCsv', 'MOCK', 'el-select', 'cleanerLabel', 'filterDraft', 'payoutStateOptions', '转账状态', 'WAIT_USER_CONFIRM', '门店 ID', '筛选' ]) { assert.match(settlements, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const statistics = read('admin/src/components/CleaningStatisticsPanel.vue'); for (const pattern of [ '今日', '近7天', '本月', '任务状态', '结算口径', '每日趋势', 'trendWidth', 'paidSettlementCents', '门店明细', '成员分账', 'pendingSettlementCents', 'filterDraft.cleanerUserId', 'cleanerLabel', 'exportStatistics', 'cleaning-statistics', 'downloadCsv', 'csvCell', 'el-select' ]) { assert.match(statistics, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const cleaners = read('admin/src/components/CleanersPanel.vue'); for (const pattern of [ '新增保洁员', '门店 ID,逗号分隔', '已绑微信', '待补资料', 'profileComplete', 'profileIssues', 'exportCleaners', 'exportCleanerProfileIssues', 'cleaning-cleaners', 'cleaning-cleaner-profile-issues', '导出待补', 'downloadCsv', 'csvCell', '会话', 'status-toggle', 'reset-sessions' ]) { assert.match(cleaners, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const handoff = read('admin/src/components/CleaningFieldHandoffPanel.vue'); for (const pattern of [ '现场联调进度', 'qipai.cleaning.field-handoff', 'qipai.cleaning.field-handoff.session', '联调批次', '现场/门店', '验收人', '收口结论', '下一步', 'acceptor', 'closeoutVerdict', 'nextAction', 'createEmptySessionMeta', '最后更新', '证据记录', 'evidenceRecordedCount', '待补证据', 'evidenceMissingItems', 'evidenceMissingCount', 'overdueItems', 'isOverdueDraft', 'parseDeadline', 'copyOverdueSummary', '逾期检查项', '逾期清单已复制', 'copyEvidenceSummary', '待补证据清单已复制', '现场交接报告已复制', 'copyHandoffReport', 'exportHandoffReport', 'exportCloseoutArchive', 'buildCloseoutArchive', 'serializeHandoffItem', 'buildHandoffReport', 'formatReportItem', 'statusText', 'cleaning-field-handoff-report', 'cleaning-field-closeout-archive', 'qipai.cleaning.field-handoff.closeout-archive.v1', '归档包', 'evidenceRef', 'normalizeDraft', 'sessionMeta', 'updatedAt', 'persistSessionMeta', 'formatLocalMinute', '快照', '导入', 'exportSnapshot', 'importSnapshot', 'qipai.cleaning.field-handoff.snapshot.v1', 'cleaning-field-handoff-snapshot', 'categoryProgress', 'passPercent', 'handoff-category-progress', 'stageProgress', 'copyStageSummary', '执行阶段看板', '阶段摘要已复制', '可进入现场验收收口', 'handoffReady', 'missingCloseoutFields', 'closeoutReady', 'copyVerdict', 'copyCloseoutGate', '收口门禁已复制', '归档门禁', '现场记录可归档', '收口资料待补齐', '检查项状态', '现场证据', '联调结论已复制', '商户凭据引用', '小程序合法域名', '小额真实转账', '订单权限开门', '控电和插座联动', 'BLOCKED', 'statusFilter', 'handoff-status-select', '全部状态', 'ownerFilter', 'ownerOptions', 'ownerProgress', 'copyOwnerSummary', '负责人看板', '负责人摘要已复制', 'handoff-owner-select', '全部负责人', '未分派', '批量标记', 'bulkForm', 'deadline', '期限', 'YYYY-MM-DD', 'applyBulkMark', '应用到当前筛选', '阻断摘要', 'blockedItems', 'riskItems', 'copyBlockedSummary', 'navigator.clipboard.writeText', 'exportChecklist', 'exportRiskRegister', 'cleaning-field-handoff', 'cleaning-field-risk-register', '缺证据', 'localStorage', 'downloadCsv' ]) { assert.match(handoff, new RegExp(pattern.replace(/[.*+?^${}()|[\]\\]/g, '\\$&'))); } const styles = read('admin/src/styles.css'); assert.match(styles, /\.cleaner-select/); assert.match(styles, /\.option-row/); assert.match(styles, /\.filter-select/); assert.match(styles, /\.narrow-input/); assert.match(styles, /\.action-board/); assert.match(styles, /\.action-tile/); assert.match(styles, /\.handoff-summary/); assert.match(styles, /\.handoff-body/); assert.match(styles, /\.handoff-session/); assert.match(styles, /\.handoff-category-progress/); assert.match(styles, /\.handoff-category-row/); assert.match(styles, /\.handoff-stage-board/); assert.match(styles, /\.handoff-stage-grid/); assert.match(styles, /\.handoff-stage-card/); assert.match(styles, /\.handoff-owner-board/); assert.match(styles, /\.handoff-owner-grid/); assert.match(styles, /\.handoff-owner-card/); assert.match(styles, /\.handoff-verdict/); assert.match(styles, /\.handoff-status-select/); assert.match(styles, /\.handoff-bulk-form/); assert.match(styles, /\.handoff-blockers/); assert.match(styles, /\.hidden-file-input/); assert.match(styles, /\.transfer-preflight-alert/); assert.match(styles, /@media \(max-width: 980px\)/); assert.match(styles, /@media \(max-width: 560px\)/); console.log('PASS: M08-B admin cleaning workspace covers task, settlement, transfer and preflight controls.');