diff --git a/src/axios/handlers/device-center/ops/alarm.ts b/src/axios/handlers/device-center/ops/alarm.ts index 81d9911..ead91cb 100644 --- a/src/axios/handlers/device-center/ops/alarm.ts +++ b/src/axios/handlers/device-center/ops/alarm.ts @@ -5,7 +5,7 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' * 设备运维 / 告警管理 mock * - 告警列表:转工单 / 忽略 / 批量处理 * - 告警详情:基础信息 + 报错日志(代码风格多行文本) - * - 设备分类下拉:末级分类名(与设备监测模块对齐) + * - 设备分类下拉:末级分类名(与设备监测模块对齐,分类树由 /admin/device/category/tree 统一提供) */ type AlarmSource = 'runLog' | 'selfCheck' | 'inspection' | 'sla' | 'ota' | 'dataDistribute' @@ -77,7 +77,7 @@ Caused by: com.yx.device.HardwareException: 压缩机响应超时 (3000ms) }, { id: 'alm-002', alarmCode: 'ALM20260703002', source: 'ota', level: 'warn', - deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', deviceCategory: '智能手表', + deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', deviceCategory: '华为-watch5', content: 'OTA 升级失败:设备位于 v1.0.4,目标 v1.1.0,已回滚', triggerTime: '2026-07-03 10:12:48', status: 'processing', handler: '李工程师', triggerRule: 'OTA 任务执行结果 = fail(warn)', @@ -211,17 +211,6 @@ Caused by: java.security.SignatureException: Signature does not match }, ] -const CATEGORY_OPTIONS: { name: string }[] = [ - { name: '智能货柜' }, - { name: '智能手表' }, - { name: '智能健康一体机' }, - { name: '体脂仪' }, - { name: '蓝牙秤' }, - { name: '健身车' }, - { name: '带鱼屏' }, - { name: '入库秤' }, -] - const listAlarm = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = alarmList.filter((item) => { @@ -281,8 +270,6 @@ const batchHandle = (ctx: MockContext): ApiResponse => { return ok(type === 'workorder' ? '批量转工单成功' : '批量忽略成功') } -const categoryOpts = (): ApiResponse<{ name: string }[]> => ok('查询成功', CATEGORY_OPTIONS) - registerMocks([ { method: 'POST', url: '/admin/device/ops/alarm/list', handler: listAlarm }, { method: 'POST', url: '/admin/device/ops/alarm/stats', handler: statsAlarm }, @@ -290,5 +277,4 @@ registerMocks([ { method: 'POST', url: '/admin/device/ops/alarm/to-work-order', handler: toWorkOrder }, { method: 'POST', url: '/admin/device/ops/alarm/ignore', handler: ignoreAlarm }, { method: 'POST', url: '/admin/device/ops/alarm/batch', handler: batchHandle }, - { method: 'POST', url: '/admin/device/ops/alarm/category/options', handler: categoryOpts }, ]) diff --git a/src/axios/handlers/device-center/ops/init.ts b/src/axios/handlers/device-center/ops/init.ts index d8067ae..c7177d5 100644 --- a/src/axios/handlers/device-center/ops/init.ts +++ b/src/axios/handlers/device-center/ops/init.ts @@ -6,14 +6,14 @@ * - info 详情(含类别 / 数据分发 / 软件版本详情) * - set-version / batch-set-version 版本设置 * - export 异步导出 - * - app/version/category/customer/site 下拉 + * - app/version/customer/site 下拉 + * - 设备分类下拉:末级分类名(与设备监测 / 告警模块对齐,分类树由 /admin/device/category/tree 统一提供) */ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext, RequestParameter } from '@axios' import type { AppOption, CustomerOption, - DeviceCategoryOption, InitDetail, InitItem, SiteOption, @@ -57,15 +57,6 @@ const VERSION_DATASET: VersionOption[] = [ { id: 'ver-006', appId: 'app-004', version: 'V0.9.5', description: '净菜柜屏固件 App', upgradePoints: '柜门传感器识别优化', packageUrl: 'https://mock.example.com/pkg/app-004/v0.9.5.bin' }, ] -/** 设备类别 mock(二级分类名) */ -const CATEGORY_DATASET: DeviceCategoryOption[] = [ - { name: '体重秤' }, - { name: '体脂仪' }, - { name: '智能手表' }, - { name: '体检一体机' }, - { name: '净菜柜' }, -] - /** 客户 mock */ const CUSTOMER_DATASET: CustomerOption[] = [ { id: 'cus-001', name: '阳光养老院' }, @@ -86,7 +77,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-001', deviceCode: 'WT-2025-0001', - deviceCategory: '体重秤', + deviceCategory: '蓝牙秤', deviceName: '智养体重秤', serialNumber: 'SN-WT-2025-0001', initStatus: 'pending', @@ -128,7 +119,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-003', deviceCode: 'WT-2025-0003', - deviceCategory: '智能手表', + deviceCategory: '华为-watch5', deviceName: '智养手表', serialNumber: 'SN-WT-2025-0003', initStatus: 'pending', @@ -149,7 +140,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-004', deviceCode: 'HC-2025-0004', - deviceCategory: '体检一体机', + deviceCategory: '智能健康一体机', deviceName: '智养体检一体机', serialNumber: 'SN-HC-2025-0004', initStatus: 'pending', @@ -170,7 +161,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-005', deviceCode: 'CB-2025-0005', - deviceCategory: '净菜柜', + deviceCategory: '智能货柜', deviceName: '智养净菜柜', serialNumber: 'SN-CB-2025-0005', initStatus: 'pending', @@ -191,7 +182,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-006', deviceCode: 'WT-2025-0006', - deviceCategory: '体重秤', + deviceCategory: '蓝牙秤', deviceName: '智养体重秤', serialNumber: 'SN-WT-2025-0006', initStatus: 'success', @@ -233,7 +224,7 @@ const INIT_DATASET: InitItem[] = [ { id: 'dev-008', deviceCode: 'WT-2025-0008', - deviceCategory: '体重秤', + deviceCategory: '蓝牙秤', deviceName: '智养体重秤', serialNumber: 'SN-WT-2025-0008', initStatus: 'success', @@ -352,9 +343,6 @@ const versionOpts = (ctx: MockContext): ApiResponse => { return ok('ok', VERSION_DATASET.filter((v) => v.appId === params.appId)) } -/** 设备类别下拉 */ -const categoryOpts = (): ApiResponse => ok('ok', CATEGORY_DATASET) - /** 客户下拉 */ const customerOpts = (): ApiResponse => ok('ok', CUSTOMER_DATASET) @@ -372,7 +360,6 @@ registerMocks([ { method: 'POST', url: BASE + '/export', handler: exportInit }, { method: 'POST', url: BASE + '/app/options', handler: appOpts }, { method: 'POST', url: BASE + '/version/options', handler: versionOpts }, - { method: 'POST', url: BASE + '/category/options', handler: categoryOpts }, { method: 'POST', url: BASE + '/customer/options', handler: customerOpts }, { method: 'POST', url: BASE + '/site/options', handler: siteOpts }, ]) diff --git a/src/axios/handlers/device-center/ops/monitor.ts b/src/axios/handlers/device-center/ops/monitor.ts index 8b52a8a..1ad96ab 100644 --- a/src/axios/handlers/device-center/ops/monitor.ts +++ b/src/axios/handlers/device-center/ops/monitor.ts @@ -82,7 +82,7 @@ const monitorList: MonitorRecord[] = [ ], }, { - id: 'mon-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', deviceCategory: '智能手表', + id: 'mon-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', deviceCategory: '华为-watch5', tenantName: '健康管理机构', siteName: '健康园区体检站', runStatus: 'repairing', packageName: '智能手表 App', softwareVersion: 'V2.0.0', lastHeartbeat: '2026-07-02 13:20:00', onlineRate: 78, alertCount: 2, onlineTime: '2026-06-22 09:30:00', isIot: true, @@ -232,24 +232,9 @@ const remoteMonitor = (ctx: MockContext): ApiResponse => { return ok('远程重启指令已下发') } -/** 设备分类下拉(末级分类名,与设备初始化模块对齐) */ -const CATEGORY_OPTIONS: { name: string }[] = [ - { name: '智能货柜' }, - { name: '智能手表' }, - { name: '智能健康一体机' }, - { name: '体脂仪' }, - { name: '蓝牙秤' }, - { name: '健身车' }, - { name: '带鱼屏' }, - { name: '入库秤' }, -] - -const categoryOpts = (): ApiResponse<{ name: string }[]> => ok('查询成功', CATEGORY_OPTIONS) - registerMocks([ { method: 'POST', url: '/admin/device/ops/monitor/list', handler: listMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/stats', handler: statsMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/info', handler: infoMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/remote', handler: remoteMonitor }, - { method: 'POST', url: '/admin/device/ops/monitor/category/options', handler: categoryOpts }, ]) diff --git a/src/axios/handlers/device-center/stock/inventory.ts b/src/axios/handlers/device-center/stock/inventory.ts index 2fcea43..c9e6795 100644 --- a/src/axios/handlers/device-center/stock/inventory.ts +++ b/src/axios/handlers/device-center/stock/inventory.ts @@ -33,7 +33,7 @@ interface InventoryRecord { inventoryType: 'full' | 'partial' operator: string inventoryTime: string - status: 'draft' | 'completed' | 'audited' + status: 'in_progress' | 'completed' | 'audited' expectedCount: number actualCount: number surplusCount: number @@ -170,19 +170,19 @@ const inventoryList: InventoryRecord[] = [ }, { id: 'inv-006', inventoryCode: 'PD-20260701-006', warehouseAreaId: 'wh-floor1-small-shelf-2', warehouseAreaName: '1楼小型备品货架-2号储位', - inventoryType: 'partial', operator: '李工程师', inventoryTime: '2026-07-01 14:20:00', status: 'draft', + inventoryType: 'partial', operator: '李工程师', inventoryTime: '2026-07-01 14:20:00', status: 'in_progress', expectedCount: 10, actualCount: 0, surplusCount: 0, lossCount: 0, remark: '等待小程序扫码盘点', items: buildItems(0, 0, 10, 10), }, { id: 'inv-007', inventoryCode: 'PD-20260703-007', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位', - inventoryType: 'full', operator: '陈工程师', inventoryTime: '2026-07-03 10:00:00', status: 'draft', + inventoryType: 'full', operator: '陈工程师', inventoryTime: '2026-07-03 10:00:00', status: 'in_progress', expectedCount: 8, actualCount: 0, surplusCount: 0, lossCount: 0, remark: '本周新发起', items: buildItems(0, 0, 8, 8), }, { id: 'inv-008', inventoryCode: 'PD-20260705-008', warehouseAreaId: 'wh-floor1-small-shelf-1', warehouseAreaName: '1楼小型备品货架-1号储位', - inventoryType: 'full', operator: '张工程师', inventoryTime: '2026-07-05 16:30:00', status: 'draft', + inventoryType: 'full', operator: '张工程师', inventoryTime: '2026-07-05 16:30:00', status: 'in_progress', expectedCount: 5, actualCount: 0, surplusCount: 0, lossCount: 0, items: buildItems(0, 0, 5, 5), }, @@ -250,7 +250,7 @@ const addRecord = (params: PageParams): ApiResponse => { inventoryType, operator: '当前用户', inventoryTime: nowStr(), - status: 'draft', + status: 'in_progress', expectedCount, actualCount: 0, surplusCount: 0, @@ -274,12 +274,12 @@ const auditRecord = (params: PageParams): ApiResponse => { return ok('审核完成,库存台账已按实盘数据修正') } -/** 删除(仅 draft 可删) */ +/** 删除(仅 in_progress 可删) */ const deleteById = (params: PageParams): ApiResponse => { const id = String(params.id ?? '') const idx = inventoryList.findIndex((item) => item.id === id) if (idx >= 0) { - if (inventoryList[idx].status !== 'draft') return ok('仅草稿状态可删除') + if (inventoryList[idx].status !== 'in_progress') return ok('仅进行中状态可删除') inventoryList.splice(idx, 1) } return ok('删除成功') diff --git a/src/axios/handlers/miniprogram/ops/inspect.ts b/src/axios/handlers/miniprogram/ops/inspect.ts index 74a65cc..c7e775a 100644 --- a/src/axios/handlers/miniprogram/ops/inspect.ts +++ b/src/axios/handlers/miniprogram/ops/inspect.ts @@ -1,13 +1,16 @@ /** * 实施运维小程序 / 巡检 mock - * - 计划列表(4 状态) + * - 计划列表(按状态过滤,派发/自建通过 planType 字段在卡片上区分,列表不预过滤) * - 任务详情(按设备 × 巡检项) * - 提交项结果 + 完成任务 + * - 创建自建巡检记录(基于客户 + 布点场所) */ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, RequestParameter } from '@axios' import type { + InspectCreateForm, + InspectCreateItem, InspectItem, InspectItemSubmit, InspectPlan, @@ -24,45 +27,63 @@ const ok = (msg: string, data: T = null as T): ApiResponse => ({ const getParams = >(params: RequestParameter): T => (params ?? {}) as T +/** 简易自增 id 生成器 */ +const genId = (prefix: string): string => + `${prefix}-${Date.now().toString(36)}${Math.floor(Math.random() * 1000).toString(36)}` + const plans: InspectPlan[] = [ { - id: 'ip-001', name: '上海浦东张江食堂 - 月度巡检', site: '上海浦东 · 张江食堂', - deviceCategory: '智养餐饮设备', deviceCount: 5, status: 'doing', - deadline: '2026-07-03 18:00:00', inspector: '李工程师', itemCount: 6, + id: 'ip-001', name: '上海杨浦颐养中心 - 1号食堂月度巡检', + site: '上海杨浦 · 1号食堂', customerName: '上海杨浦颐养中心', siteName: '1号食堂', + deviceCategory: '智养餐饮设备', deviceCount: 5, status: 'doing', planType: 'dispatched', + deadline: '2026-07-15 18:00:00', inspector: '李工程师', itemCount: 6, }, { - id: 'ip-002', name: '北京朝阳体检站 - 周巡检', site: '北京朝阳 · 朝阳体检站', - deviceCategory: '智能穿戴设备', deviceCount: 12, status: 'todo', - deadline: '2026-07-04 12:00:00', inspector: '李工程师', itemCount: 5, + id: 'ip-002', name: '北京朝阳福利院 - 体检中心周巡检', + site: '北京朝阳 · 体检中心', customerName: '北京朝阳福利院', siteName: '体检中心', + deviceCategory: '智能穿戴设备', deviceCount: 12, status: 'todo', planType: 'dispatched', + deadline: '2026-07-12 12:00:00', inspector: '李工程师', itemCount: 5, }, { - id: 'ip-003', name: '广州天银食堂 - 临时巡检', site: '广州天河 · 天银食堂', - deviceCategory: '智养餐饮设备', deviceCount: 3, status: 'overdue', - deadline: '2026-07-02 18:00:00', inspector: '王工程师', itemCount: 6, + id: 'ip-003', name: '广州天河颐康医院 - 门诊食堂临时巡检', + site: '广州天河 · 门诊食堂', customerName: '广州天河颐康医院', siteName: '门诊食堂', + deviceCategory: '智养餐饮设备', deviceCount: 3, status: 'overdue', planType: 'dispatched', + deadline: '2026-07-08 18:00:00', inspector: '王工程师', itemCount: 6, }, { - id: 'ip-004', name: '杭州西湖体检站 - 月度巡检', site: '杭州西湖 · 西湖体检站', - deviceCategory: '体重测量设备', deviceCount: 4, status: 'done', - deadline: '2026-07-01 18:00:00', inspector: '赵工程师', itemCount: 5, + id: 'ip-004', name: '杭州西湖社区养老院 - 社区食堂月度巡检', + site: '杭州西湖 · 社区食堂', customerName: '杭州西湖社区养老院', siteName: '社区食堂', + deviceCategory: '智养餐饮设备', deviceCount: 4, status: 'done', planType: 'dispatched', + deadline: '2026-07-05 18:00:00', inspector: '赵工程师', itemCount: 5, }, { - id: 'ip-005', name: '重庆解放碑健身房 - 周巡检', site: '重庆渝中 · 解放碑健身房', - deviceCategory: '健身器材设备', deviceCount: 8, status: 'todo', - deadline: '2026-07-05 12:00:00', inspector: '李工程师', itemCount: 5, + id: 'ip-005', name: '重庆渝中颐养福利中心 - 康复健身房周巡检', + site: '重庆渝中 · 康复健身房', customerName: '重庆渝中颐养福利中心', siteName: '康复健身房', + deviceCategory: '健身器材设备', deviceCount: 8, status: 'todo', planType: 'dispatched', + deadline: '2026-07-13 12:00:00', inspector: '李工程师', itemCount: 5, }, { - id: 'ip-006', name: '深圳科技园体检站 - 月度巡检', site: '深圳南山 · 科技园体检站', - deviceCategory: '健康监测设备', deviceCount: 2, status: 'doing', - deadline: '2026-07-04 18:00:00', inspector: '赵工程师', itemCount: 7, + id: 'ip-006', name: '上海杨浦颐养中心 - 健康体检站月度巡检', + site: '上海杨浦 · 健康体检站', customerName: '上海杨浦颐养中心', siteName: '健康体检站', + deviceCategory: '健康监测设备', deviceCount: 2, status: 'doing', planType: 'dispatched', + deadline: '2026-07-14 18:00:00', inspector: '赵工程师', itemCount: 7, }, { - id: 'ip-007', name: '上海闵行莘庄食堂 - 月度巡检', site: '上海闵行 · 莘庄食堂', - deviceCategory: '智养餐饮设备', deviceCount: 4, status: 'done', - deadline: '2026-06-30 18:00:00', inspector: '李工程师', itemCount: 6, + id: 'ip-007', name: '北京朝阳福利院 - 主楼食堂月度巡检', + site: '北京朝阳 · 主楼食堂', customerName: '北京朝阳福利院', siteName: '主楼食堂', + deviceCategory: '智养餐饮设备', deviceCount: 4, status: 'done', planType: 'dispatched', + deadline: '2026-07-04 18:00:00', inspector: '李工程师', itemCount: 6, + }, + { + id: 'ip-adhoc-001', name: '上海杨浦颐养中心 - 1号食堂现场自建巡检', + site: '上海杨浦 · 1号食堂', customerName: '上海杨浦颐养中心', siteName: '1号食堂', + deviceCategory: '智养餐饮设备', deviceCount: 2, status: 'doing', planType: 'ad hoc', + deadline: '', inspector: '当前用户', itemCount: 5, }, ] -const buildItems = (): InspectItem[] => [ +/** 标准巡检项预置(与新页面 default items 对齐,便于 detail 兜底构造) */ +const buildDefaultItems = (): InspectItem[] => [ { id: 'it-1', name: '设备外观', type: 'appearance', standard: '无明显损伤 / 变形', result: 'pending', remark: '' }, { id: 'it-2', name: '功能测试', type: 'function', standard: '按键 / 显示正常', result: 'pending', remark: '' }, { id: 'it-3', name: '网络连通', type: 'network', standard: '心跳正常上报', result: 'pending', remark: '' }, @@ -71,57 +92,82 @@ const buildItems = (): InspectItem[] => [ { id: 'it-6', name: '电源稳定性', type: 'function', standard: '无掉电告警', result: 'pending', remark: '' }, ] +/** 自建巡检项(id 自动生成) */ +const toInspectItems = (items: InspectCreateItem[]): InspectItem[] => + items.map((it, idx) => ({ + id: `it-${idx + 1}`, + name: it.name, + type: it.type, + standard: it.standard, + result: 'pending' as const, + remark: '', + })) + const taskDetailMap: Record = { 'ip-001': { plan: plans[0], devices: [ - { code: 'CAB-SH-001', name: '1号净菜柜', items: buildItems(), scanned: true }, - { code: 'CAB-SH-002', name: '2号净菜柜', items: buildItems(), scanned: false }, - { code: 'CAB-SH-003', name: '3号净菜柜', items: buildItems(), scanned: false }, + { code: 'CAB-SH-001', name: '1号净菜柜', items: buildDefaultItems(), scanned: true }, + { code: 'CAB-SH-002', name: '2号净菜柜', items: buildDefaultItems(), scanned: false }, + { code: 'CAB-SH-003', name: '3号净菜柜', items: buildDefaultItems(), scanned: false }, ], }, 'ip-002': { plan: plans[1], devices: [ - { code: 'WATCH-BJ-014', name: '14号健康手表', items: buildItems().slice(0, 5), scanned: false }, - { code: 'WATCH-BJ-015', name: '15号健康手表', items: buildItems().slice(0, 5), scanned: false }, + { code: 'WATCH-BJ-014', name: '14号健康手表', items: buildDefaultItems().slice(0, 5), scanned: false }, + { code: 'WATCH-BJ-015', name: '15号健康手表', items: buildDefaultItems().slice(0, 5), scanned: false }, + ], + }, + 'ip-adhoc-001': { + plan: plans[7], + devices: [ + { code: 'CAB-SH-008', name: '8号净菜柜', items: buildDefaultItems().slice(0, 5), scanned: false }, + { code: 'CAB-SH-009', name: '9号净菜柜', items: buildDefaultItems().slice(0, 5), scanned: true }, ], }, } registerMocks([ - // 计划列表 + // 计划列表(仅按状态过滤,派发/自建不预过滤,由前端卡片 tag 区分) { url: '/mp/ops/inspect/list', method: 'POST', handler: (ctx): ApiResponse => { const { status } = getParams<{ status?: InspectPlanStatus | '' }>(ctx.params) - const rows = status ? plans.filter((p) => p.status === status) : plans + let rows = plans + if (status) rows = rows.filter((p) => p.status === status) return ok('查询成功', rows) }, }, - // 任务详情 + // 任务详情(兜底时把构造的 detail 写入缓存,保证后续 submit 能命中) { url: '/mp/ops/inspect/detail', method: 'GET', handler: (ctx): ApiResponse => { const { id } = getParams<{ id?: string }>(ctx.params) - const detail = taskDetailMap[id ?? ''] - if (!detail) { - // 兜底:构造一个空任务 - const plan = plans.find((p) => p.id === id) - if (!plan) return ok('计划不存在', null) - return ok('查询成功', { - plan, - devices: [ - { code: `${plan.id}-D001`, name: '示例设备', items: buildItems().slice(0, plan.itemCount), scanned: false }, - ], - }) + const planId = id ?? '' + // 命中缓存直接返回 + if (taskDetailMap[planId]) return ok('查询成功', taskDetailMap[planId]) + // 兜底:根据 plan 构造一个空任务,并写入缓存 + const plan = plans.find((p) => p.id === planId) + if (!plan) return ok('计划不存在', null) + const fallback: InspectTaskDetail = { + plan, + devices: [ + { + code: `${plan.id}-D001`, + name: '示例设备', + items: buildDefaultItems().slice(0, plan.itemCount), + scanned: false, + }, + ], } - return ok('查询成功', detail) + taskDetailMap[planId] = fallback + return ok('查询成功', fallback) }, }, - // 提交巡检项结果 + // 提交巡检项结果(容错:缓存不存在也返回成功,避免阻断流程) { url: '/mp/ops/inspect/submit', method: 'POST', @@ -136,8 +182,8 @@ registerMocks([ item.remark = remark } } - // 异常项自动转工单(mock 不实际创建,仅返回成功) - return ok(result === 'abnormal' ? '已记录异常,并自动转工单' : '已记录', null) + // 异常项自动转工单(mock 不实际创建,仅返回提示文案) + return ok(result === 'abnormal' ? '已记录异常,并自动转工单' : '已记录合格', null) }, }, // 完成巡检任务 @@ -151,4 +197,38 @@ registerMocks([ return ok('巡检任务已完成', null) }, }, + // 创建自建巡检记录(基于客户 + 布点场所) + { + url: '/mp/ops/inspect/create', + method: 'POST', + handler: (ctx): ApiResponse<{ planId: string }> => { + const form = getParams(ctx.params) + const planId = genId('ip-adhoc') + const newPlan: InspectPlan = { + id: planId, + name: `${form.customerName} - ${form.siteName} 现场自建巡检`, + site: `${form.customerName} · ${form.siteName}`, + customerName: form.customerName, + siteName: form.siteName, + deviceCategory: form.deviceCategory, + deviceCount: form.devices.length, + status: 'doing', + planType: 'ad hoc', + deadline: '', // 自建记录无截止时间 + inspector: '当前用户', + itemCount: form.items.length, + } + plans.unshift(newPlan) + taskDetailMap[planId] = { + plan: newPlan, + devices: form.devices.map((d) => ({ + code: d.code, + name: d.name, + items: toInspectItems(form.items), + scanned: false, + })), + } + return ok('自建巡检记录已创建', { planId }) + }, + }, ]) diff --git a/src/axios/handlers/miniprogram/ops/me.ts b/src/axios/handlers/miniprogram/ops/me.ts index 6dd1fca..23e188f 100644 --- a/src/axios/handlers/miniprogram/ops/me.ts +++ b/src/axios/handlers/miniprogram/ops/me.ts @@ -7,7 +7,14 @@ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, RequestParameter } from '@axios' -import type { MessageItem, MessageType, OfflineStatus, PersonalStats } from '@pages/miniprogram/ops/me/types' +import type { + ChangePasswordParams, + MessageItem, + MessageType, + OfflineStatus, + PersonalStats, + UserProfile, +} from '@pages/miniprogram/ops/me/types' const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', @@ -78,6 +85,23 @@ const offline: OfflineStatus = { lastSyncTime: '2026-07-03 08:45:11', } +// 当前账号信息(mock 写死,对应 Home.vue 顶部"李工程师 / 实施运维工程师") +const profile: UserProfile = { + userId: 'U-ENG-0015', + name: '李工程师', + role: '实施运维工程师', + jobNumber: 'OP-2024-0015', + phone: '138****6621', + department: '华东运维中心 · 上海大区', + jobTitle: '高级运维工程师', + region: '上海-浦东-张江', + hireDate: '2024-03-15', + lastLoginAt: '2026-07-09 21:12:34', +} + +// mock 写死的旧密码,用于修改密码时的服务端校验 +const MOCK_OLD_PASSWORD = 'old123456' + registerMocks([ // 个人统计 { @@ -141,4 +165,31 @@ registerMocks([ return ok('清除完成', null) }, }, + // 账号信息 + { + url: '/mp/ops/me/profile', + method: 'GET', + handler: (): ApiResponse => ok('查询成功', profile), + }, + // 修改密码 + { + url: '/mp/ops/me/change-password', + method: 'POST', + handler: (ctx): ApiResponse => { + const { oldPassword, newPassword } = getParams(ctx.params) + if (oldPassword !== MOCK_OLD_PASSWORD) { + return { code: '50001', msg: '旧密码不正确', data: null } + } + if (newPassword === MOCK_OLD_PASSWORD) { + return { code: '50002', msg: '新密码不能与旧密码相同', data: null } + } + return ok('修改成功,请重新登录', null) + }, + }, + // 退出登录 + { + url: '/mp/ops/me/logout', + method: 'POST', + handler: (): ApiResponse => ok('已退出登录', null), + }, ]) diff --git a/src/axios/handlers/miniprogram/ops/scan.ts b/src/axios/handlers/miniprogram/ops/scan.ts index 786b861..c7450a6 100644 --- a/src/axios/handlers/miniprogram/ops/scan.ts +++ b/src/axios/handlers/miniprogram/ops/scan.ts @@ -18,6 +18,7 @@ import type { OtaVersion, RepairForm, RunLogItem, + SimUnboundOption, } from '@pages/miniprogram/ops/scan/types' const ok = (msg: string, data: T = null as T): ApiResponse => ({ @@ -29,48 +30,48 @@ const ok = (msg: string, data: T = null as T): ApiResponse => ({ const getParams = >(params: RequestParameter): T => (params ?? {}) as T -/** 设备档案数据集(覆盖 5 大顶级分类 × 4 状态) */ +/** 设备档案数据集(覆盖 5 大顶级分类 × 4 状态,字段名对齐后台 monitor) */ const deviceMap: Record = { 'CAB-SH-001': { - id: 'dev-001', code: 'CAB-SH-001', name: '1号净菜柜(上海浦东)', + id: 'dev-001', deviceCode: 'CAB-SH-001', deviceName: '1号净菜柜(上海浦东)', category: 'catering', model: 'NZG-200', - status: 'inUse', site: '上海浦东 · 张江食堂', tenant: 'CQ 能源集团', - firmwareVersion: 'V2.3.1', lastHeartbeat: '2026-07-03 09:30:11', + status: 'inUse', siteName: '上海浦东 · 张江食堂', tenantName: 'CQ 能源集团', + packageName: '净菜柜固件', softwareVersion: 'V2.3.1', lastHeartbeat: '2026-07-03 09:30:11', pendingWorkOrders: 1, isIot: true, dataDistributeEnabled: false, }, 'WATCH-BJ-014': { - id: 'dev-014', code: 'WATCH-BJ-014', name: '14号健康手表(北京朝阳)', + id: 'dev-014', deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', category: 'wearable', model: 'WATCH5-PRO', - status: 'inUse', site: '北京朝阳 · 朝阳体检站', tenant: '北京健康管理机构', - firmwareVersion: 'v1.0.4', lastHeartbeat: '2026-07-03 09:32:48', + status: 'inUse', siteName: '北京朝阳 · 朝阳体检站', tenantName: '北京健康管理机构', + packageName: '健康手表App', softwareVersion: 'v1.0.4', lastHeartbeat: '2026-07-03 09:32:48', pendingWorkOrders: 1, isIot: true, dataDistributeEnabled: true, }, 'TREADMILL-CQ-002': { - id: 'dev-022', code: 'TREADMILL-CQ-002', name: '2号跑步机(重庆渝中)', + id: 'dev-022', deviceCode: 'TREADMILL-CQ-002', deviceName: '2号跑步机(重庆渝中)', category: 'fitness', model: 'TM-X1', - status: 'inUse', site: '重庆渝中 · 解放碑健身房', tenant: '重庆健身中心', - firmwareVersion: 'V1.5.2', lastHeartbeat: '2026-07-03 09:25:00', + status: 'inUse', siteName: '重庆渝中 · 解放碑健身房', tenantName: '重庆健身中心', + packageName: '健身器材固件', softwareVersion: 'V1.5.2', lastHeartbeat: '2026-07-03 09:25:00', pendingWorkOrders: 1, isIot: true, dataDistributeEnabled: true, }, 'BODYFAT-HZ-005': { - id: 'dev-035', code: 'BODYFAT-HZ-005', name: '5号体脂仪(杭州西湖)', + id: 'dev-035', deviceCode: 'BODYFAT-HZ-005', deviceName: '5号体脂仪(杭州西湖)', category: 'body', model: 'InBody-770', - status: 'inUse', site: '杭州西湖 · 西湖体检站', tenant: '杭州健康管理机构', - firmwareVersion: 'V3.0.1', lastHeartbeat: '2026-07-03 09:28:33', + status: 'inUse', siteName: '杭州西湖 · 西湖体检站', tenantName: '杭州健康管理机构', + packageName: '体脂仪App', softwareVersion: 'V3.0.1', lastHeartbeat: '2026-07-03 09:28:33', pendingWorkOrders: 0, isIot: true, dataDistributeEnabled: true, }, 'CHECK-SZ-003': { - id: 'dev-048', code: 'CHECK-SZ-003', name: '3号体检一体机(深圳南山)', + id: 'dev-048', deviceCode: 'CHECK-SZ-003', deviceName: '3号体检一体机(深圳南山)', category: 'health', model: 'ALL-IN-ONE-15', - status: 'inUse', site: '深圳南山 · 科技园体检站', tenant: '深圳健康管理机构', - firmwareVersion: 'V2.1.0', lastHeartbeat: '2026-07-03 09:20:11', + status: 'inUse', siteName: '深圳南山 · 科技园体检站', tenantName: '深圳健康管理机构', + packageName: '体检一体机App', softwareVersion: 'V2.1.0', lastHeartbeat: '2026-07-03 09:20:11', pendingWorkOrders: 1, isIot: true, dataDistributeEnabled: true, }, 'CAB-NEW-001': { - id: 'dev-091', code: 'CAB-NEW-001', name: '新装净菜柜(待激活)', + id: 'dev-091', deviceCode: 'CAB-NEW-001', deviceName: '新装净菜柜(待激活)', category: 'catering', model: 'NZG-200', - status: 'inactive', site: '上海浦东 · 张江食堂', tenant: 'CQ 能源集团', - firmwareVersion: '—', lastHeartbeat: '—', + status: 'inactive', siteName: '上海浦东 · 张江食堂', tenantName: 'CQ 能源集团', + packageName: '—', softwareVersion: '—', lastHeartbeat: '—', pendingWorkOrders: 0, isIot: true, dataDistributeEnabled: false, }, } @@ -85,14 +86,14 @@ const scanHistory = [ { id: 'h-006', deviceCode: 'CHECK-SZ-003', deviceName: '3号体检一体机(深圳南山)', scannedAt: '2026-07-01 16:40:00', action: 'OTA 升级' }, ] -/** OTA 可用版本 */ +/** OTA 可用版本(status 与后台 version VersionStatus 对齐:active 有效 / disabled 停用) */ const otaVersionsMap: Record = { 'WATCH-BJ-014': [ - { version: 'v1.1.0', releaseNote: '新增睡眠分析、修复心率异常告警', status: 'full', size: '4.2MB' }, - { version: 'v1.0.5', releaseNote: '修复蓝牙连接稳定性', status: 'gray', size: '2.1MB' }, + { version: 'v1.1.0', releaseNote: '新增睡眠分析、修复心率异常告警', status: 'active', size: '4.2MB' }, + { version: 'v1.0.5', releaseNote: '修复蓝牙连接稳定性(已停用)', status: 'disabled', size: '2.1MB' }, ], 'CAB-SH-001': [ - { version: 'V2.4.0', releaseNote: '压缩机优化、温度校准算法升级', status: 'full', size: '8.5MB' }, + { version: 'V2.4.0', releaseNote: '压缩机优化、温度校准算法升级', status: 'active', size: '8.5MB' }, ], } @@ -133,6 +134,20 @@ const updatesMap: Record = { ], } +/** 未绑定 SIM 卡下拉数据集(设备初始化时选用,≤8 条) */ +const unboundSims: SimUnboundOption[] = [ + { id: 'sim-001', iccid: '898604021920C0000001', phoneNo: '13800001001', carrier: 'mobile', packageType: '物联网 30M/月' }, + { id: 'sim-002', iccid: '898604021920C0000002', phoneNo: '13800001002', carrier: 'mobile', packageType: '物联网 100M/月' }, + { id: 'sim-003', iccid: '898601112020C0000003', phoneNo: '18600001003', carrier: 'unicom', packageType: '物联网 50M/月' }, + { id: 'sim-004', iccid: '898611112020C0000004', phoneNo: '18900001004', carrier: 'telecom', packageType: '物联网 50M/月' }, +] + +/** 虹软 activeKey 生成器(每次随机 8 位 hex,模拟自动获取并绑定) */ +const genArcsoftKey = (): string => { + const seg = (): string => Math.random().toString(16).toUpperCase().slice(2, 6).padEnd(4, '0') + return `ARC-${seg()}-${seg()}-${seg()}` +} + registerMocks([ // 扫码识别设备 { @@ -151,6 +166,22 @@ registerMocks([ method: 'GET', handler: (): ApiResponse => ok('查询成功', scanHistory), }, + // 自动获取虹软人脸识别激活码 + { + url: '/mp/ops/scan/arcsoft/fetch', + method: 'GET', + handler: (ctx): ApiResponse<{ activeKey: string }> => { + const { deviceCode } = getParams<{ deviceCode?: string }>(ctx.params) + if (!deviceCode) return ok('设备编码缺失', { activeKey: '' }) + return ok('获取成功', { activeKey: genArcsoftKey() }) + }, + }, + // 查询未绑定 SIM 卡列表 + { + url: '/mp/ops/scan/sim/unbound', + method: 'GET', + handler: (): ApiResponse => ok('查询成功', unboundSims), + }, // 设备初始化 { url: '/mp/ops/scan/init', @@ -160,7 +191,7 @@ registerMocks([ const dev = deviceMap[form.deviceCode] if (dev && form.result === 'success') { dev.status = 'activated' - dev.firmwareVersion = form.firmwareBaseline + dev.softwareVersion = form.firmwareBaseline } return ok('初始化上报成功', null) }, @@ -200,7 +231,7 @@ registerMocks([ const task: OtaTask = { id: `ota-${Date.now()}`, deviceCode, - currentVersion: dev?.firmwareVersion ?? '—', + currentVersion: dev?.softwareVersion ?? '—', targetVersion, progress: 0, status: 'upgrading', @@ -225,7 +256,7 @@ registerMocks([ if (task.progress >= 100) { task.status = 'success' const dev = deviceMap[deviceCode!] - if (dev) dev.firmwareVersion = task.targetVersion + if (dev) dev.softwareVersion = task.targetVersion } } } @@ -242,7 +273,7 @@ registerMocks([ if (task) { task.status = 'rolledback' const dev = deviceMap[deviceCode] - if (dev) dev.firmwareVersion = task.currentVersion + if (dev) dev.softwareVersion = task.currentVersion } return ok('回滚成功', null) }, diff --git a/src/axios/handlers/miniprogram/ops/stock.ts b/src/axios/handlers/miniprogram/ops/stock.ts index ad09ff6..179fa23 100644 --- a/src/axios/handlers/miniprogram/ops/stock.ts +++ b/src/axios/handlers/miniprogram/ops/stock.ts @@ -1,11 +1,15 @@ /** * 实施运维小程序 / 库存作业 mock - * - 收货入库:仓库下拉 + 设备详情 + 提交 - * - 装机出库:客户下拉 + 提交 - * - 库存盘点:盘点单详情 + 提交(带 items[].result) - * - 库存查询:在库设备列表(只读) + * - 收货入库:仓库 + 供应商 + 设备类别 + 批次 + 单价 + 提交 + * - 装机出库:仓库 + 客户 + 出库类型(装机/调拨/报废) + 提交 + * - 库存盘点: + * · 列表 /mp/ops/stock/inventory/list(按后台 ListItem 字段对齐:盘点单号 + 仓库区域 + 类型 + 状态 + 统计) + * · 新建 /mp/ops/stock/inventory/create(仓库区域 + 类型 full/partial + 应盘品类 + 备注) + * · 详情 /mp/ops/stock/inventory/detail(按"末级分类 + 批次"维度返回应盘清单,对齐后台 InventoryItem) + * · 提交 /mp/ops/stock/inventory/submit(按 categoryId+batchNo 维度提交实盘结果) + * - 库存查询:聚合维度列表(与后台 instock ListItem 对齐) * - * 数据风格对齐后台设备中心 / 库存管理 4 个页面,但字段精简适配手机端。 + * 数据风格对齐后台设备中心 / 库存管理 4 个页面(outbound / inbound / instock / inventory)。 */ import { registerMocks } from '@axios/mockBus' @@ -13,72 +17,112 @@ import type { ApiResponse, RequestParameter } from '@axios' interface WarehouseOption { id: string; name: string } interface CustomerOption { id: string; name: string } +interface SupplierOption { id: string; name: string } +interface CategoryOption { id: string; name: string } +interface SiteOption { id: string; name: string; customerId: string } interface DeviceDetail { deviceCode: string deviceName: string modelName: string batchNo: string + categoryId?: string + categoryName?: string activated?: boolean } +/** 库存查询列表项(与后台 instock ListItem 聚合维度对齐) */ interface StockItem { id: string - deviceCode: string - deviceName: string - modelName: string + categoryId: string + categoryName: string batchNo: string - warehouseId: string - warehouseName: string - quantity: number - unit: string - status: 'instock' | 'checkout' | 'frozen' - inboundTime: string + supplierId: string + supplierName: string + warehouseAreaId: string + warehouseAreaName: string + stockQty: number + lockedQty: number + availableQty: number + healthLevel: 'sufficient' | 'warning' | 'out_of_stock' + updateTime: string } +/** 盘点单状态:in_progress 进行中 / completed 已完成(待审核)/ audited 已审核 */ +type InventoryStatus = 'in_progress' | 'completed' | 'audited' +/** 盘点类型:full 全量盘点 / partial 部分盘点 */ +type InventoryType = 'full' | 'partial' +/** 应盘条目实盘结果(按"末级分类 + 批次"为粒度) */ +type InventoryItemResult = 'matched' | 'surplus' | 'loss' | 'unchecked' + +/** 应盘清单条目(按"设备类别末级分类 + 批次"聚合) */ interface InventoryItem { - deviceCode: string - deviceName: string - modelName: string + categoryId: string + categoryName: string batchNo: string expectedQuantity: number actualQuantity: number - result: 'matched' | 'surplus' | 'loss' | 'unchecked' + result: InventoryItemResult } -interface InventoryDetail { +/** 盘点单列表项(与后台 ListItem 字段对齐) */ +interface InventoryListItem { id: string inventoryCode: string - warehouseId: string - warehouseName: string - inventoryType: 'full' | 'partial' + warehouseAreaId: string + warehouseAreaName: string + inventoryType: InventoryType + operator: string inventoryTime: string + status: InventoryStatus expectedCount: number actualCount: number surplusCount: number lossCount: number + auditor?: string + auditTime?: string + remark?: string +} + +/** 盘点单详情(含应盘清单) */ +interface InventoryDetail extends InventoryListItem { items: InventoryItem[] } +/** 入库提交 body(与后台 inbound InboundPayload 对齐) */ interface InboundSubmitBody { warehouseId: string inboundType: 'purchase' | 'return' | 'transfer' + categoryId: string + supplierId: string + purchasePrice: number + batchNo?: string remark?: string deviceCodes: string[] } +/** 出库提交 body(与后台 outbound OutboundPayload 对齐,无 repair) */ interface OutboundSubmitBody { warehouseId: string - outboundType: 'install' | 'repair' | 'scrap' | 'transfer' + outboundType: 'install' | 'transfer' | 'scrap' customerId?: string siteName?: string remark?: string deviceCodes: string[] } +/** 新建盘点单提交 body(对齐后台 InventoryPayload) */ +interface InventoryCreateBody { + warehouseAreaId: string + inventoryType: InventoryType + categoryIds?: string[] + remark?: string +} + +/** 盘点单提交 body(按 categoryId+batchNo 维度) */ interface InventorySubmitBody { id: string - items: Array<{ deviceCode: string; actualQuantity: number; result: 'matched' | 'surplus' | 'loss' | 'unchecked' }> + items: Array<{ categoryId: string; batchNo: string; actualQuantity: number; result: 'matched' | 'surplus' | 'loss' }> } const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) @@ -101,62 +145,168 @@ const customers: CustomerOption[] = [ { id: 'cus-005', name: '广州天河体检站' }, ] +/** 客户→布点场所映射(每个客户 2-3 个场所,对齐后台 customer/site 业务) */ +const sites: SiteOption[] = [ + { id: 'site-001', customerId: 'cus-001', name: 'CQ 集团 · 总部食堂' }, + { id: 'site-002', customerId: 'cus-001', name: 'CQ 集团 · 江北员工餐厅' }, + { id: 'site-003', customerId: 'cus-002', name: '北京健康中心 · 一层大厅' }, + { id: 'site-004', customerId: 'cus-002', name: '北京健康中心 · 体检区' }, + { id: 'site-005', customerId: 'cus-003', name: '上海张江食堂 · 主餐厅' }, + { id: 'site-006', customerId: 'cus-003', name: '上海张江食堂 · 取餐区' }, + { id: 'site-007', customerId: 'cus-003', name: '上海张江食堂 · 健康驿站' }, + { id: 'site-008', customerId: 'cus-004', name: '杭州西湖社区 · 一楼服务台' }, + { id: 'site-009', customerId: 'cus-005', name: '广州天河体检站 · 候检区' }, +] + +const suppliers: SupplierOption[] = [ + { id: 'sup-001', name: '智养设备制造有限公司' }, + { id: 'sup-002', name: '杭州健康科技公司' }, + { id: 'sup-003', name: '深圳一体机厂商' }, + { id: 'sup-004', name: '北京穿戴设备供应商' }, +] + +const categories: CategoryOption[] = [ + { id: 'cat-cabinet', name: '净菜柜' }, + { id: 'cat-scale', name: '蓝牙秤' }, + { id: 'cat-bodyfat', name: '体脂仪' }, + { id: 'cat-inboundscale', name: '入库秤' }, + { id: 'cat-watch', name: '健康手表' }, + { id: 'cat-checkall', name: '体检一体机' }, +] + const devicePool: DeviceDetail[] = [ - { deviceCode: 'DEV-0001', deviceName: '1号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-001', activated: true }, - { deviceCode: 'DEV-0002', deviceName: '蓝牙秤 A', modelName: 'CS-蓝牙秤 v3', batchNo: 'B2026-002', activated: false }, - { deviceCode: 'DEV-0003', deviceName: '体脂仪 1', modelName: 'BF-体脂仪 X1', batchNo: 'B2026-003', activated: true }, - { deviceCode: 'DEV-0004', deviceName: '入库秤 2', modelName: 'IS-入库秤 K2', batchNo: 'B2026-004', activated: false }, - { deviceCode: 'DEV-0005', deviceName: '2号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-005', activated: true }, + { deviceCode: 'DEV-0001', deviceName: '1号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-001', categoryId: 'cat-cabinet', categoryName: '净菜柜', activated: true }, + { deviceCode: 'DEV-0002', deviceName: '蓝牙秤 A', modelName: 'CS-蓝牙秤 v3', batchNo: 'B2026-002', categoryId: 'cat-scale', categoryName: '蓝牙秤', activated: false }, + { deviceCode: 'DEV-0003', deviceName: '体脂仪 1', modelName: 'BF-体脂仪 X1', batchNo: 'B2026-003', categoryId: 'cat-bodyfat', categoryName: '体脂仪', activated: true }, + { deviceCode: 'DEV-0004', deviceName: '入库秤 2', modelName: 'IS-入库秤 K2', batchNo: 'B2026-004', categoryId: 'cat-inboundscale', categoryName: '入库秤', activated: false }, + { deviceCode: 'DEV-0005', deviceName: '2号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-005', categoryId: 'cat-cabinet', categoryName: '净菜柜', activated: true }, ] +/** 库存查询列表(聚合维度,8 条覆盖各类目 × 健康度) */ const stockList: StockItem[] = [ - { id: 'stk-001', deviceCode: 'DEV-0001', deviceName: '1号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-001', warehouseId: 'wh-floor1', warehouseName: '一楼设备仓库', quantity: 1, unit: '台', status: 'instock', inboundTime: '2026-06-01 10:15:00' }, - { id: 'stk-002', deviceCode: 'DEV-0002', deviceName: '蓝牙秤 A', modelName: 'CS-蓝牙秤 v3', batchNo: 'B2026-002', warehouseId: 'wh-floor1', warehouseName: '一楼设备仓库', quantity: 5, unit: '台', status: 'instock', inboundTime: '2026-06-05 11:00:00' }, - { id: 'stk-003', deviceCode: 'DEV-0003', deviceName: '体脂仪 1', modelName: 'BF-体脂仪 X1', batchNo: 'B2026-003', warehouseId: 'wh-floor2', warehouseName: '二楼备品仓库', quantity: 3, unit: '台', status: 'instock', inboundTime: '2026-06-10 09:30:00' }, - { id: 'stk-004', deviceCode: 'DEV-0004', deviceName: '入库秤 2', modelName: 'IS-入库秤 K2', batchNo: 'B2026-004', warehouseId: 'wh-floor1-large', warehouseName: '大型设备区', quantity: 2, unit: '台', status: 'frozen', inboundTime: '2026-06-12 14:00:00' }, - { id: 'stk-005', deviceCode: 'DEV-0005', deviceName: '2号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-005', warehouseId: 'wh-floor1-small', warehouseName: '小型设备区', quantity: 1, unit: '台', status: 'instock', inboundTime: '2026-06-15 16:30:00' }, - { id: 'stk-006', deviceCode: 'DEV-0006', deviceName: '体脂仪 2', modelName: 'BF-体脂仪 X1', batchNo: 'B2026-006', warehouseId: 'wh-floor2-check', warehouseName: '待检区', quantity: 1, unit: '台', status: 'instock', inboundTime: '2026-06-20 10:00:00' }, - { id: 'stk-007', deviceCode: 'DEV-0007', deviceName: '蓝牙秤 B', modelName: 'CS-蓝牙秤 v3', batchNo: 'B2026-007', warehouseId: 'wh-floor1-large-a', warehouseName: 'A号货架', quantity: 1, unit: '台', status: 'checkout', inboundTime: '2026-06-25 09:00:00' }, - { id: 'stk-008', deviceCode: 'DEV-0008', deviceName: '3号净菜柜', modelName: '智养净菜柜 V2', batchNo: 'B2026-008', warehouseId: 'wh-floor1', warehouseName: '一楼设备仓库', quantity: 1, unit: '台', status: 'instock', inboundTime: '2026-06-28 13:45:00' }, + { id: 'stk-001', categoryId: 'cat-cabinet', categoryName: '净菜柜', batchNo: 'B2026-001', supplierId: 'sup-001', supplierName: '智养设备制造有限公司', warehouseAreaId: 'wh-floor1', warehouseAreaName: '一楼设备仓库', stockQty: 3, lockedQty: 0, availableQty: 3, healthLevel: 'sufficient', updateTime: '2026-06-28 13:45:00' }, + { id: 'stk-002', categoryId: 'cat-scale', categoryName: '蓝牙秤', batchNo: 'B2026-002', supplierId: 'sup-002', supplierName: '杭州健康科技公司', warehouseAreaId: 'wh-floor1', warehouseAreaName: '一楼设备仓库', stockQty: 5, lockedQty: 1, availableQty: 4, healthLevel: 'sufficient', updateTime: '2026-06-25 09:00:00' }, + { id: 'stk-003', categoryId: 'cat-bodyfat', categoryName: '体脂仪', batchNo: 'B2026-003', supplierId: 'sup-002', supplierName: '杭州健康科技公司', warehouseAreaId: 'wh-floor2', warehouseAreaName: '二楼备品仓库', stockQty: 3, lockedQty: 0, availableQty: 3, healthLevel: 'sufficient', updateTime: '2026-06-20 10:00:00' }, + { id: 'stk-004', categoryId: 'cat-inboundscale', categoryName: '入库秤', batchNo: 'B2026-004', supplierId: 'sup-001', supplierName: '智养设备制造有限公司', warehouseAreaId: 'wh-floor1-large', warehouseAreaName: '大型设备区', stockQty: 2, lockedQty: 1, availableQty: 1, healthLevel: 'warning', updateTime: '2026-06-18 14:30:00' }, + { id: 'stk-005', categoryId: 'cat-cabinet', categoryName: '净菜柜', batchNo: 'B2026-005', supplierId: 'sup-001', supplierName: '智养设备制造有限公司', warehouseAreaId: 'wh-floor1-small', warehouseAreaName: '小型设备区', stockQty: 1, lockedQty: 0, availableQty: 1, healthLevel: 'warning', updateTime: '2026-06-15 16:30:00' }, + { id: 'stk-006', categoryId: 'cat-bodyfat', categoryName: '体脂仪', batchNo: 'B2026-006', supplierId: 'sup-002', supplierName: '杭州健康科技公司', warehouseAreaId: 'wh-floor2-check', warehouseAreaName: '待检区', stockQty: 1, lockedQty: 0, availableQty: 1, healthLevel: 'warning', updateTime: '2026-06-10 09:30:00' }, + { id: 'stk-007', categoryId: 'cat-scale', categoryName: '蓝牙秤', batchNo: 'B2026-007', supplierId: 'sup-002', supplierName: '杭州健康科技公司', warehouseAreaId: 'wh-floor1-large-a', warehouseAreaName: 'A号货架', stockQty: 0, lockedQty: 0, availableQty: 0, healthLevel: 'out_of_stock', updateTime: '2026-06-05 11:00:00' }, + { id: 'stk-008', categoryId: 'cat-watch', categoryName: '健康手表', batchNo: 'B2026-008', supplierId: 'sup-004', supplierName: '北京穿戴设备供应商', warehouseAreaId: 'wh-floor1', warehouseAreaName: '一楼设备仓库', stockQty: 8, lockedQty: 0, availableQty: 8, healthLevel: 'sufficient', updateTime: '2026-07-01 10:15:00' }, ] -const buildInventoryItems = (total: number, unchecked: number): InventoryItem[] => { - const items: InventoryItem[] = [] - for (let i = 0; i < total; i++) { - items.push({ - deviceCode: `DEV-${String(i + 1).padStart(4, '0')}`, - deviceName: `设备${i + 1}`, - modelName: ['智养净菜柜', '蓝牙秤', '体脂仪', '入库秤'][i % 4], - batchNo: `B2026-${String((i % 8) + 1).padStart(3, '0')}`, - expectedQuantity: 1, - actualQuantity: i < total - unchecked ? 1 : 0, - result: i < total - unchecked ? 'matched' : 'unchecked', - }) - } - return items -} +/** 按"末级分类 + 批次"维度构造应盘清单 */ +const buildInventoryItems = ( + seeds: Array<{ categoryId: string; categoryName: string; batchNo: string; expected: number; result?: InventoryItemResult; actual?: number }>, +): InventoryItem[] => + seeds.map((s) => ({ + categoryId: s.categoryId, + categoryName: s.categoryName, + batchNo: s.batchNo, + expectedQuantity: s.expected, + actualQuantity: s.actual ?? (s.result === 'matched' ? s.expected : 0), + result: s.result ?? 'unchecked', + })) +/** 应盘种子数据(按仓库区域 + 在库台账虚构) */ +const seedFloor1Small = [ + { categoryId: 'cat-cabinet', categoryName: '净菜柜', batchNo: 'B2026-005', expected: 1 }, + { categoryId: 'cat-bodyfat', categoryName: '体脂仪', batchNo: 'B2026-003', expected: 2 }, + { categoryId: 'cat-watch', categoryName: '健康手表', batchNo: 'B2026-008', expected: 4 }, +] + +const seedFloor1LargeA = [ + { categoryId: 'cat-scale', categoryName: '蓝牙秤', batchNo: 'B2026-002', expected: 5 }, + { categoryId: 'cat-inboundscale', categoryName: '入库秤', batchNo: 'B2026-004', expected: 2 }, + { categoryId: 'cat-watch', categoryName: '健康手表', batchNo: 'B2026-008', expected: 1 }, +] + +const seedFloor2 = [ + { categoryId: 'cat-bodyfat', categoryName: '体脂仪', batchNo: 'B2026-003', expected: 3 }, + { categoryId: 'cat-bodyfat', categoryName: '体脂仪', batchNo: 'B2026-006', expected: 1 }, +] + +/** 盘点单列表(覆盖 in_progress / completed / audited 三态) */ const inventoryRecords: InventoryDetail[] = [ { - id: 'inv-006', inventoryCode: 'PD-20260701-006', - warehouseId: 'wh-floor1-small', warehouseName: '小型设备区', - inventoryType: 'partial', inventoryTime: '2026-07-01 14:20:00', - expectedCount: 10, actualCount: 0, surplusCount: 0, lossCount: 0, - items: buildInventoryItems(10, 10), + id: 'inv-006', + inventoryCode: 'PD-20260708-006', + warehouseAreaId: 'wh-floor1-small', + warehouseAreaName: '小型设备区', + inventoryType: 'partial', + operator: '李工程师', + inventoryTime: '2026-07-08 14:20:00', + status: 'in_progress', + expectedCount: 7, + actualCount: 0, + surplusCount: 0, + lossCount: 0, + remark: '现场临时抽盘', + items: buildInventoryItems(seedFloor1Small), }, { - id: 'inv-007', inventoryCode: 'PD-20260703-007', - warehouseId: 'wh-floor1-large-a', warehouseName: 'A号货架', - inventoryType: 'full', inventoryTime: '2026-07-03 10:00:00', - expectedCount: 8, actualCount: 3, surplusCount: 0, lossCount: 0, - items: buildInventoryItems(8, 5), + id: 'inv-007', + inventoryCode: 'PD-20260705-007', + warehouseAreaId: 'wh-floor1-large-a', + warehouseAreaName: 'A号货架', + inventoryType: 'full', + operator: '王工程师', + inventoryTime: '2026-07-05 10:00:00', + status: 'in_progress', + expectedCount: 8, + actualCount: 3, + surplusCount: 0, + lossCount: 0, + items: buildInventoryItems( + seedFloor1LargeA.map((s, i) => ({ + ...s, + result: i === 0 ? 'matched' : i === 1 ? 'matched' : 'unchecked', + })), + ), }, { - id: 'inv-008', inventoryCode: 'PD-20260705-008', - warehouseId: 'wh-floor2', warehouseName: '二楼备品仓库', - inventoryType: 'full', inventoryTime: '2026-07-05 16:30:00', - expectedCount: 5, actualCount: 0, surplusCount: 0, lossCount: 0, - items: buildInventoryItems(5, 5), + id: 'inv-008', + inventoryCode: 'PD-20260701-008', + warehouseAreaId: 'wh-floor2', + warehouseAreaName: '二楼备品仓库', + inventoryType: 'full', + operator: '李工程师', + inventoryTime: '2026-07-01 16:30:00', + status: 'completed', + expectedCount: 4, + actualCount: 4, + surplusCount: 1, + lossCount: 1, + remark: '月末例行全盘', + items: buildInventoryItems( + seedFloor2.map((s, i) => ({ + ...s, + result: i === 0 ? 'loss' : 'surplus', + actual: i === 0 ? 2 : 4, + })), + ), + }, + { + id: 'inv-009', + inventoryCode: 'PD-20260620-009', + warehouseAreaId: 'wh-floor1', + warehouseAreaName: '一楼设备仓库', + inventoryType: 'full', + operator: '王工程师', + inventoryTime: '2026-06-20 09:00:00', + status: 'audited', + expectedCount: 8, + actualCount: 8, + surplusCount: 0, + lossCount: 0, + auditor: '张主任', + auditTime: '2026-06-21 14:00:00', + remark: '账实相符,台账无误', + items: buildInventoryItems( + [ + { categoryId: 'cat-cabinet', categoryName: '净菜柜', batchNo: 'B2026-001', expected: 3, result: 'matched' }, + { categoryId: 'cat-scale', categoryName: '蓝牙秤', batchNo: 'B2026-002', expected: 5, result: 'matched' }, + ], + ), }, ] @@ -173,7 +323,29 @@ registerMocks([ method: 'POST', handler: (): ApiResponse => ok('查询成功', customers), }, - // 设备详情(收货入库 / 装机出库扫码时查询) + // 布点场所下拉(按 customerId 过滤) + { + url: '/mp/ops/stock/site-options', + method: 'POST', + handler: (ctx): ApiResponse => { + const { customerId } = getParams<{ customerId?: string }>(ctx.params) + if (!customerId) return ok('查询成功', []) + return ok('查询成功', sites.filter((s) => s.customerId === customerId)) + }, + }, + // 供应商下拉 + { + url: '/mp/ops/stock/supplier-options', + method: 'POST', + handler: (): ApiResponse => ok('查询成功', suppliers), + }, + // 设备类别下拉(末级分类) + { + url: '/mp/ops/stock/category-options', + method: 'POST', + handler: (): ApiResponse => ok('查询成功', categories), + }, + // 设备详情(收货入库 / 装机出库 / 盘点扫码时查询) { url: '/mp/ops/stock/device-detail', method: 'POST', @@ -198,6 +370,9 @@ registerMocks([ handler: (ctx): ApiResponse => { const body = getParams(ctx.params) if (!body.warehouseId) return ok('请选择入库仓库', null) + if (!body.categoryId) return ok('请选择设备类别', null) + if (!body.supplierId) return ok('请选择供应商', null) + if (!body.purchasePrice) return ok('请填写采购单价', null) if (!body.deviceCodes?.length) return ok('请扫码添加设备', null) return ok(`入库成功,新增 ${body.deviceCodes.length} 台到台账`, null) }, @@ -214,7 +389,69 @@ registerMocks([ return ok(`出库成功,扣减 ${body.deviceCodes.length} 台库存`, null) }, }, - // 盘点单详情 + // 盘点单列表(按状态筛选) + { + url: '/mp/ops/stock/inventory/list', + method: 'POST', + handler: (ctx): ApiResponse => { + const { status, keyword } = getParams<{ status?: InventoryStatus; keyword?: string }>(ctx.params) + const filtered = inventoryRecords + .filter((r) => (status ? r.status === status : true)) + .filter((r) => (keyword ? r.inventoryCode.toLowerCase().includes(keyword.toLowerCase()) : true)) + // 详情接口才返回 items,列表接口剥离 items 避免冗余传输 + .map(({ items: _items, ...rest }) => rest) + return ok('查询成功', filtered as InventoryListItem[]) + }, + }, + // 新建盘点单(in_progress 状态加入内存列表) + { + url: '/mp/ops/stock/inventory/create', + method: 'POST', + handler: (ctx): ApiResponse<{ id: string; inventoryCode: string }> => { + const body = getParams(ctx.params) + if (!body.warehouseAreaId) return ok('请选择仓库区域', null) + if (body.inventoryType === 'partial' && !body.categoryIds?.length) return ok('部分盘点请选择应盘品类', null) + const wh = warehouses.find((w) => w.id === body.warehouseAreaId) ?? { id: body.warehouseAreaId, name: '—' } + const newId = `inv-${String(inventoryRecords.length + 10).padStart(3, '0')}` + const seq = String(inventoryRecords.length + 10).padStart(3, '0') + const now = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + const stamp = `${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}` + const time = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + const seed = body.inventoryType === 'partial' + ? (body.categoryIds ?? []).map((cid) => { + const cat = categories.find((c) => c.id === cid) + const stk = stockList.find((s) => s.categoryId === cid && s.warehouseAreaId === body.warehouseAreaId) + return { + categoryId: cid, + categoryName: cat?.name ?? '—', + batchNo: stk?.batchNo ?? '—', + expected: stk?.stockQty ?? 0, + } + }) + : stockList + .filter((s) => s.warehouseAreaId === body.warehouseAreaId) + .map((s) => ({ categoryId: s.categoryId, categoryName: s.categoryName, batchNo: s.batchNo, expected: s.stockQty })) + inventoryRecords.unshift({ + id: newId, + inventoryCode: `PD-${stamp}-${seq}`, + warehouseAreaId: body.warehouseAreaId, + warehouseAreaName: wh.name, + inventoryType: body.inventoryType, + operator: '当前用户', + inventoryTime: time, + status: 'in_progress', + expectedCount: seed.reduce((acc, s) => acc + s.expected, 0), + actualCount: 0, + surplusCount: 0, + lossCount: 0, + remark: body.remark, + items: buildInventoryItems(seed), + }) + return ok('盘点单已创建,进入进行中状态', { id: newId, inventoryCode: `PD-${stamp}-${seq}` }) + }, + }, + // 盘点单详情(含应盘清单) { url: '/mp/ops/stock/inventory/detail', method: 'POST', @@ -233,16 +470,20 @@ registerMocks([ const body = getParams(ctx.params) const found = inventoryRecords.find((r) => r.id === body.id) if (!found) return ok('盘点单不存在', null) + if (found.status === 'audited') return ok('已审核的盘点单不可再次提交', null) body.items.forEach((submit) => { - const item = found.items.find((x) => x.deviceCode === submit.deviceCode) + const item = found.items.find( + (x) => x.categoryId === submit.categoryId && x.batchNo === submit.batchNo, + ) if (item) { item.result = submit.result item.actualQuantity = submit.actualQuantity } }) - found.actualCount = body.items.filter((x) => x.result === 'matched' || x.result === 'surplus').length - found.surplusCount = body.items.filter((x) => x.result === 'surplus').length - found.lossCount = body.items.filter((x) => x.result === 'loss').length + found.actualCount = found.items.filter((x) => x.result === 'matched' || x.result === 'surplus').length + found.surplusCount = found.items.filter((x) => x.result === 'surplus').length + found.lossCount = found.items.filter((x) => x.result === 'loss').length + found.status = 'completed' return ok('盘点已提交,等待后台审核修正台账', null) }, }, @@ -251,16 +492,17 @@ registerMocks([ url: '/mp/ops/stock/list', method: 'POST', handler: (ctx): ApiResponse => { - const params = getParams<{ warehouseId?: string; status?: string; keyword?: string }>(ctx.params) + const params = getParams<{ warehouseId?: string; categoryId?: string; healthLevel?: string; keyword?: string }>(ctx.params) const filtered = stockList.filter((x) => { - if (params.warehouseId && x.warehouseId !== params.warehouseId) return false - if (params.status && x.status !== params.status) return false + if (params.warehouseId && x.warehouseAreaId !== params.warehouseId) return false + if (params.categoryId && x.categoryId !== params.categoryId) return false + if (params.healthLevel && x.healthLevel !== params.healthLevel) return false if (params.keyword) { const kw = params.keyword.toLowerCase() const hit = - x.deviceCode.toLowerCase().includes(kw) || - x.deviceName.toLowerCase().includes(kw) || - x.batchNo.toLowerCase().includes(kw) + x.categoryName.toLowerCase().includes(kw) || + x.batchNo.toLowerCase().includes(kw) || + x.supplierName.toLowerCase().includes(kw) if (!hit) return false } return true diff --git a/src/axios/handlers/miniprogram/ops/workOrder.ts b/src/axios/handlers/miniprogram/ops/workOrder.ts index 89d56c9..3d6c16c 100644 --- a/src/axios/handlers/miniprogram/ops/workOrder.ts +++ b/src/axios/handlers/miniprogram/ops/workOrder.ts @@ -1,19 +1,24 @@ /** * 实施运维小程序 / 工单 mock - * - 列表(按状态分段,类型筛选) + * - 列表(按 4 段状态,来源筛选) * - 详情(含客户报障附件 + 处理时间线) * - 接单 / 关单 / 提交维修上报 + * + * 字段命名与状态枚举对齐后台 device-center/ops/workOrder: + * - status:pending 待派单 / assigned 已派单 / processing 处理中 / closed 已关单 + * - source:customer 客户报障 / inspection 巡检异常 / selfCheck 自检异常 / afterSale 售后转单 + * - 列表字段:orderCode / siteName / customerName / source / faultLevel / faultType / faultDesc / slaDeadline / slaOverdue / reportTime */ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, RequestParameter } from '@axios' import type { + OrderSource, RepairReportForm, WorkOrderDetail, WorkOrderItem, WorkOrderListParams, WorkOrderStatus, - WorkOrderType, } from '@pages/miniprogram/ops/workOrder/types' const ok = (msg: string, data: T = null as T): ApiResponse => ({ @@ -27,84 +32,98 @@ const getParams = >(params: RequestParameter): T => const workOrders: WorkOrderItem[] = [ { - id: 'wo-001', code: 'WO20260703001', title: '净菜柜压缩机故障维修', type: 'repair', + id: 'wo-001', orderCode: 'WO20260703001', source: 'customer', status: 'pending', deviceCode: 'CAB-SH-001', deviceName: '1号净菜柜(上海浦东)', - site: '上海浦东 · 张江食堂', tenant: 'CQ 能源集团', - description: '客户报修:制冷效果差,压缩机异响', - slaDeadline: '2026-07-03 18:00:00', createdAt: '2026-07-03 08:30:00', - assignee: '李工程师', overSla: false, + siteName: '上海浦东 · 张江食堂', customerName: 'CQ 能源集团', + faultLevel: 'high', faultType: 'mechanical', + faultDesc: '客户报修:制冷效果差,压缩机异响', + images: ['https://picsum.photos/200/150?random=1', 'https://picsum.photos/200/150?random=2'], + slaDeadline: '2026-07-03 18:00:00', reportTime: '2026-07-03 08:30:00', + assignee: '李工程师', slaOverdue: false, }, { - id: 'wo-002', code: 'WO20260703002', title: '健康手表 OTA 失败处置', type: 'selfCheck', + id: 'wo-002', orderCode: 'WO20260703002', source: 'selfCheck', status: 'processing', deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', - site: '北京朝阳 · 朝阳体检站', tenant: '北京健康管理机构', - description: 'OTA 升级失败已自动回滚,需现场排查', - slaDeadline: '2026-07-03 20:00:00', createdAt: '2026-07-03 10:15:00', - assignee: '李工程师', overSla: false, + siteName: '北京朝阳 · 朝阳体检站', customerName: '北京健康管理机构', + faultLevel: 'medium', faultType: 'software', + faultDesc: 'OTA 升级失败已自动回滚,需现场排查', + images: [], + slaDeadline: '2026-07-03 20:00:00', reportTime: '2026-07-03 10:15:00', + assignee: '李工程师', slaOverdue: false, }, { - id: 'wo-003', code: 'WO20260702018', title: '净菜柜货道错位复位', type: 'inspection', + id: 'wo-003', orderCode: 'WO20260702018', source: 'inspection', status: 'pending', deviceCode: 'CAB-GZ-007', deviceName: '7号净菜柜(广州天河)', - site: '广州天河 · 天银食堂', tenant: '广州餐饮集团', - description: '巡检发现:货道 5 错位,需现场复位', - slaDeadline: '2026-07-03 12:00:00', createdAt: '2026-07-02 16:30:00', - assignee: '王工程师', overSla: true, + siteName: '广州天河 · 天银食堂', customerName: '广州餐饮集团', + faultLevel: 'medium', faultType: 'mechanical', + faultDesc: '巡检发现:货道 5 错位,需现场复位', + images: [], + slaDeadline: '2026-07-03 12:00:00', reportTime: '2026-07-02 16:30:00', + assignee: '王工程师', slaOverdue: true, }, { - id: 'wo-004', code: 'WO20260702015', title: '体检一体机离线排查', type: 'sla', + id: 'wo-004', orderCode: 'WO20260702015', source: 'afterSale', status: 'processing', deviceCode: 'CHECK-SZ-003', deviceName: '3号体检一体机(深圳南山)', - site: '深圳南山 · 科技园体检站', tenant: '深圳健康管理机构', - description: '连续 3 次心跳未上报,疑似网络故障', - slaDeadline: '2026-07-02 18:00:00', createdAt: '2026-07-02 14:05:00', - assignee: '赵工程师', overSla: true, + siteName: '深圳南山 · 科技园体检站', customerName: '深圳健康管理机构', + faultLevel: 'high', faultType: 'network', + faultDesc: '连续 3 次心跳未上报,疑似网络故障', + images: [], + slaDeadline: '2026-07-02 18:00:00', reportTime: '2026-07-02 14:05:00', + assignee: '赵工程师', slaOverdue: true, }, { - id: 'wo-005', code: 'WO20260702009', title: '数据分发死信重投', type: 'repair', - status: 'done', deviceCode: 'CAB-SH-002', deviceName: '2号净菜柜(上海闵行)', - site: '上海闵行 · 莘庄食堂', tenant: 'CQ 能源集团', - description: '营养系统订阅 4 次重试失败,已人工重投成功', - slaDeadline: '2026-07-02 16:00:00', createdAt: '2026-07-02 11:48:00', - assignee: '李工程师', overSla: false, + id: 'wo-005', orderCode: 'WO20260702009', source: 'customer', + status: 'closed', deviceCode: 'CAB-SH-002', deviceName: '2号净菜柜(上海闵行)', + siteName: '上海闵行 · 莘庄食堂', customerName: 'CQ 能源集团', + faultLevel: 'medium', faultType: 'software', + faultDesc: '营养系统订阅 4 次重试失败,已人工重投成功', + images: [], + slaDeadline: '2026-07-02 16:00:00', reportTime: '2026-07-02 11:48:00', + assignee: '李工程师', slaOverdue: false, }, { - id: 'wo-006', code: 'WO20260701022', title: '体脂仪电池更换', type: 'repair', - status: 'done', deviceCode: 'BODYFAT-HZ-005', deviceName: '5号体脂仪(杭州西湖)', - site: '杭州西湖 · 西湖体检站', tenant: '杭州健康管理机构', - description: '电池电量低于 15%,已更换', - slaDeadline: '2026-07-01 18:00:00', createdAt: '2026-07-01 17:20:00', - assignee: '赵工程师', overSla: false, + id: 'wo-006', orderCode: 'WO20260701022', source: 'customer', + status: 'closed', deviceCode: 'BODYFAT-HZ-005', deviceName: '5号体脂仪(杭州西湖)', + siteName: '杭州西湖 · 西湖体检站', customerName: '杭州健康管理机构', + faultLevel: 'low', faultType: 'electrical', + faultDesc: '电池电量低于 15%,已更换', + images: [], + slaDeadline: '2026-07-01 18:00:00', reportTime: '2026-07-01 17:20:00', + assignee: '赵工程师', slaOverdue: false, }, { - id: 'wo-007', code: 'WO20260703005', title: '跑步机过载维修', type: 'repair', - status: 'pending', deviceCode: 'TREADMILL-CQ-002', deviceName: '2号跑步机(重庆渝中)', - site: '重庆渝中 · 解放碑健身房', tenant: '重庆健身中心', - description: '连续 10 分钟过载,疑似电机故障', - slaDeadline: '2026-07-03 22:00:00', createdAt: '2026-07-03 08:50:00', - assignee: '李工程师', overSla: false, + id: 'wo-007', orderCode: 'WO20260703005', source: 'customer', + status: 'assigned', deviceCode: 'TREADMILL-CQ-002', deviceName: '2号跑步机(重庆渝中)', + siteName: '重庆渝中 · 解放碑健身房', customerName: '重庆健身中心', + faultLevel: 'critical', faultType: 'electrical', + faultDesc: '连续 10 分钟过载,疑似电机故障', + images: [], + slaDeadline: '2026-07-03 22:00:00', reportTime: '2026-07-03 08:50:00', + assignee: '李工程师', slaOverdue: false, }, { - id: 'wo-008', code: 'WO20260703008', title: '净菜柜温度校准', type: 'selfCheck', - status: 'processing', deviceCode: 'CAB-BJ-003', deviceName: '3号净菜柜(北京海淀)', - site: '北京海淀 · 中关村食堂', tenant: '北京餐饮集团', - description: '温度传感器读数漂移 2.4℃,需校准', - slaDeadline: '2026-07-04 12:00:00', createdAt: '2026-07-01 09:15:00', - assignee: '王工程师', overSla: false, + id: 'wo-008', orderCode: 'WO20260703008', source: 'selfCheck', + status: 'assigned', deviceCode: 'CAB-BJ-003', deviceName: '3号净菜柜(北京海淀)', + siteName: '北京海淀 · 中关村食堂', customerName: '北京餐饮集团', + faultLevel: 'medium', faultType: 'hardware', + faultDesc: '温度传感器读数漂移 2.4℃,需校准', + images: [], + slaDeadline: '2026-07-04 12:00:00', reportTime: '2026-07-01 09:15:00', + assignee: '王工程师', slaOverdue: false, }, ] const detailMap: Record> = { 'wo-001': { - faultImages: ['https://picsum.photos/200/150?random=1', 'https://picsum.photos/200/150?random=2'], customerVoice: '机器一直嗡嗡响,东西放进去不凉', - processLog: [ + processLogs: [ { time: '2026-07-03 08:30:00', action: '客户报修', operator: '客户' }, { time: '2026-07-03 08:35:00', action: '系统创建工单', operator: '系统' }, { time: '2026-07-03 09:00:00', action: '派单给李工程师', operator: '系统' }, ], }, 'wo-002': { - faultImages: [], - processLog: [ + processLogs: [ { time: '2026-07-03 10:12:48', action: 'OTA 升级失败自动告警', operator: '系统' }, { time: '2026-07-03 10:13:30', action: '自动回滚到 v1.0.4', operator: '系统' }, { time: '2026-07-03 10:20:00', action: '生成工单并派单', operator: '系统' }, @@ -116,15 +135,14 @@ const detailMap: Record> = { const filterByStatus = (rows: WorkOrderItem[], status: WorkOrderStatus): WorkOrderItem[] => rows.filter((r) => r.status === status) -const filterByType = (rows: WorkOrderItem[], type: WorkOrderType | ''): WorkOrderItem[] => - type ? rows.filter((r) => r.type === type) : rows +const filterBySource = (rows: WorkOrderItem[], source: OrderSource | ''): WorkOrderItem[] => + source ? rows.filter((r) => r.source === source) : rows const filterByKeyword = (rows: WorkOrderItem[], kw: string): WorkOrderItem[] => { if (!kw) return rows const k = kw.toLowerCase() return rows.filter((r) => - r.code.toLowerCase().includes(k) || - r.title.toLowerCase().includes(k) || + r.orderCode.toLowerCase().includes(k) || r.deviceCode.toLowerCase().includes(k) || r.deviceName.toLowerCase().includes(k), ) @@ -138,7 +156,7 @@ registerMocks([ handler: (ctx): ApiResponse => { const p = getParams(ctx.params) let rows = filterByStatus(workOrders, p.status) - rows = filterByType(rows, p.type ?? '') + rows = filterBySource(rows, p.source ?? '') rows = filterByKeyword(rows, p.keyword ?? '') return ok('查询成功', rows) }, @@ -152,8 +170,7 @@ registerMocks([ const base = workOrders.find((w) => w.id === id) if (!base) return ok('工单不存在', null) const ext = detailMap[id!] ?? { - faultImages: [], - processLog: [{ time: base.createdAt, action: '工单创建', operator: '系统' }], + processLogs: [{ time: base.reportTime, action: '工单创建', operator: '系统' }], } return ok('查询成功', { ...base, ...ext } as WorkOrderDetail) }, @@ -179,7 +196,7 @@ registerMocks([ handler: (ctx): ApiResponse => { const { id } = getParams<{ id?: string }>(ctx.params) const target = workOrders.find((w) => w.id === id) - if (target) target.status = 'done' + if (target) target.status = 'closed' return ok('关单成功', null) }, }, diff --git a/src/axios/handlers/miniprogram/ops/workbench.ts b/src/axios/handlers/miniprogram/ops/workbench.ts index 137e96c..aeddea3 100644 --- a/src/axios/handlers/miniprogram/ops/workbench.ts +++ b/src/axios/handlers/miniprogram/ops/workbench.ts @@ -16,6 +16,8 @@ import type { AlarmLevel, AlarmSource, AlarmStatus, + InitDeviceItem, + OtaTaskItem, TodoStats, WeekStats, } from '@pages/miniprogram/ops/workbench/types' @@ -153,6 +155,44 @@ const weekStats: WeekStats = { monthInstallCount: 18, } +/** 待初始化设备列表(4 条,对齐 todoStats.todayInitDevices) */ +const initDeviceList: InitDeviceItem[] = [ + { + id: 'dev-091', deviceCode: 'CAB-NEW-001', deviceName: '新装净菜柜(待激活)', + model: 'NZG-200', siteName: '上海浦东 · 张江食堂', tenantName: 'CQ 能源集团', + inboundTime: '2026-07-02 14:00:00', + }, + { + id: 'dev-092', deviceCode: 'CAB-NEW-002', deviceName: '2号新装净菜柜(待激活)', + model: 'NZG-200', siteName: '北京朝阳 · 朝阳体检站', tenantName: '北京健康管理机构', + inboundTime: '2026-07-02 15:20:00', + }, + { + id: 'dev-093', deviceCode: 'WATCH-BJ-015', deviceName: '15号健康手表(待激活)', + model: 'WATCH5-PRO', siteName: '北京朝阳 · 朝阳体检站', tenantName: '北京健康管理机构', + inboundTime: '2026-07-03 08:30:00', + }, + { + id: 'dev-094', deviceCode: 'BODYFAT-HZ-006', deviceName: '6号体脂仪(待激活)', + model: 'InBody-770', siteName: '杭州西湖 · 西湖体检站', tenantName: '杭州健康管理机构', + inboundTime: '2026-07-03 09:10:00', + }, +] + +/** 待升级 OTA 任务列表(2 条,对齐 todoStats.todayOtaTasks) */ +const otaTaskList: OtaTaskItem[] = [ + { + id: 'ota-001', taskId: 'OTA20260703001', deviceCode: 'WATCH-BJ-014', + deviceName: '14号健康手表(北京朝阳)', currentVersion: 'v1.0.4', targetVersion: 'v1.1.0', + pushTime: '2026-07-03 09:00:00', status: 'failed', + }, + { + id: 'ota-002', taskId: 'OTA20260703002', deviceCode: 'CAB-SH-001', + deviceName: '1号净菜柜(上海浦东)', currentVersion: 'V2.3.1', targetVersion: 'V2.4.0', + pushTime: '2026-07-03 09:05:00', status: 'pending', + }, +] + registerMocks([ // 工作台今日待办统计 { @@ -166,6 +206,18 @@ registerMocks([ method: 'POST', handler: (): ApiResponse => ok('查询成功', weekStats), }, + // 待初始化设备列表 + { + url: '/mp/ops/workbench/init-list', + method: 'GET', + handler: (): ApiResponse => ok('查询成功', initDeviceList), + }, + // 待升级 OTA 任务列表 + { + url: '/mp/ops/workbench/ota-list', + method: 'GET', + handler: (): ApiResponse => ok('查询成功', otaTaskList), + }, // 告警列表 { url: '/mp/ops/alarm/list', diff --git a/src/meta/nav.ts b/src/meta/nav.ts index 847e832..f7be16f 100644 --- a/src/meta/nav.ts +++ b/src/meta/nav.ts @@ -1021,14 +1021,34 @@ export const miniprograms: NavMiniprogram[] = [ }, { key: 'mp-ops-stock-inventory', label: '库存盘点', path: 'stock-inventory', + component: () => import('@pages/miniprogram/ops/workbench/stockInventoryList.vue'), + status: 'draft', + }, + { + key: 'mp-ops-stock-inventory-detail', label: '盘点作业详情', path: 'stock-inventory-detail/:id', component: () => import('@pages/miniprogram/ops/workbench/stockInventory.vue'), status: 'draft', }, + { + key: 'mp-ops-stock-inventory-create', label: '新建盘点', path: 'stock-inventory-create', + component: () => import('@pages/miniprogram/ops/workbench/stockInventoryCreate.vue'), + status: 'draft', + }, { key: 'mp-ops-stock-list', label: '库存查询', path: 'stock-list', component: () => import('@pages/miniprogram/ops/workbench/stockList.vue'), status: 'draft', }, + { + key: 'mp-ops-init-list', label: '待初始化设备', path: 'init-list', + component: () => import('@pages/miniprogram/ops/workbench/initList.vue'), + status: 'draft', + }, + { + key: 'mp-ops-ota-list', label: '待升级任务', path: 'ota-list', + component: () => import('@pages/miniprogram/ops/workbench/otaList.vue'), + status: 'draft', + }, ], }, { @@ -1111,6 +1131,11 @@ export const miniprograms: NavMiniprogram[] = [ component: () => import('@pages/miniprogram/ops/inspect/detail.vue'), status: 'draft', }, + { + key: 'mp-ops-inspect-create', label: '新增巡检', path: 'inspect-create', + component: () => import('@pages/miniprogram/ops/inspect/inspectCreate.vue'), + status: 'draft', + }, ], }, { @@ -1128,6 +1153,16 @@ export const miniprograms: NavMiniprogram[] = [ component: () => import('@pages/miniprogram/ops/me/messageCenter.vue'), status: 'draft', }, + { + key: 'mp-ops-account-info', label: '账号信息', path: 'account-info', + component: () => import('@pages/miniprogram/ops/me/accountInfo.vue'), + status: 'draft', + }, + { + key: 'mp-ops-change-password', label: '修改密码', path: 'change-password', + component: () => import('@pages/miniprogram/ops/me/changePassword.vue'), + status: 'draft', + }, ], }, ], diff --git a/src/pages/admin-portal/device-center/asset/manage/api/index.ts b/src/pages/admin-portal/device-center/asset/manage/api/index.ts index ea6d62d..821f36f 100644 --- a/src/pages/admin-portal/device-center/asset/manage/api/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/api/index.ts @@ -1,11 +1,16 @@ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' +import type { CategoryNode } from '../../category/types' import type { CustomerOption, DeviceCategoryOption, DeviceItem, SearchForm, SiteOption } from '../types' /** 查询设备列表(按顶级分类 Tab 过滤) */ export const list = (params: SearchForm & { category: string }): Promise> => postRequest('axiosRequest', '/admin/device/manage/list', params) +/** 设备分类全树(与设备分类页 / 统计页同源,供筛选区树选择器加载) */ +export const categoryTree = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', { keyword }) + /** 删除设备(归档,不物理删除) */ export const remove = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/manage/delete', { id }) diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue index c57ca3d..9549ce8 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue @@ -28,14 +28,17 @@ - @@ -157,7 +160,8 @@ * modal-spec.md 强制项已遵循。 * 业务字段: * - 通用字段:编码 / 类别 / 名称 / 序列号 / 客户 / 场所 / 投用状态 / 虹软授权码 - * - 绑定 SIM 卡:仅当所选类别 networkMode === 'sim' 时展示 + * - 设备类别:a-tree-select 加载设备分类全树(与统计页同源,value=name 保留契约), + * leaf-only 拦截非末级;所选类别 networkMode 决定 SIM 卡字段显隐 * - 软件包名 / 软件版本:不在本弹框采集,由后续"初始化设置"流程写入 * - 所属客户 / 场所 均为可搜索下拉,场所按客户联动过滤 * - 5 个 Tab 共用同一表单,无差异化字段区块 @@ -176,6 +180,8 @@ const { formRef, form, formOptions, + categoryTreeData, + categoryTreeFieldNames, deployStatusText, showSimIccid, rules, diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/api/index.ts index 1cef793..1d3c5d4 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/api/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/api/index.ts @@ -2,10 +2,12 @@ * 设备管理 - 新增/编辑 弹框 - 接口层 * - 复用主页面 add/update 接口 * - 新增 info 接口供编辑回填 + * - categoryTree 复用设备分类页全树接口(供弹框树选择器加载,并据此查找 networkMode) */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' +import type { CategoryNode } from '../../../../../category/types' import type { Form } from '../types' /** 详情查询(编辑回填) */ @@ -19,3 +21,7 @@ export const add = (params: Record): Promise> /** 编辑 */ export const edit = (params: Record): Promise> => postRequest('axiosRequest', '/admin/device/manage/update', params) + +/** 设备分类全树(与设备分类页 / 统计页同源,供弹框树选择器加载) */ +export const categoryTree = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', { keyword }) diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/init/usePage.ts index 0b98bfd..38a7485 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/init/usePage.ts +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/init/usePage.ts @@ -20,13 +20,17 @@ */ import type { FormInstance, Rule } from 'ant-design-vue/es/form' -import { computed } from 'vue' +import { computed, ref } from 'vue' import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import { categoryOptions, customerOptions, siteOptions } from '../../../../api' -import { add, edit, info } from '../api' -import type { DeviceCategoryOption, DeviceCategoryTab, DeployStatus, NetworkMode } from '../../../../types' +import { customerOptions, siteOptions } from '../../../../api' +import { add, categoryTree, edit, info } from '../api' +import type { CategoryNode } from '../../../../../category/types' +import type { DeviceCategoryTab, DeployStatus, NetworkMode } from '../../../../types' import type { Form, FormOptions, ModalType, OpenContext, PageInfo } from '../types' +/** a-tree-select 字段映射(设备分类树节点;value=name 保留现有 deviceCategory 契约) */ +export const CATEGORY_TREE_FIELD_NAMES = { label: 'name', value: 'name', children: 'children' } + /** 初始表单工厂 */ const createInitForm = (): Form => ({ id: undefined, @@ -48,7 +52,6 @@ const createInitForm = (): Form => ({ /** 初始下拉选项工厂 */ const createInitOptions = (): FormOptions => ({ - deviceCategory: [], customer: [], site: [], }) @@ -59,6 +62,31 @@ const computeDeployStatus = (customerId: string, siteId: string): DeployStatus = return 'deployed' } +/** 递归查找树节点:按 name 匹配(用于按所选 deviceCategory 名查 networkMode) */ +const findNodeByName = (nodes: CategoryNode[], name: string): CategoryNode | null => { + for (const node of nodes) { + if (node.name === name) return node + if (node.children?.length) { + const found = findNodeByName(node.children, name) + if (found) return found + } + } + return null +} + +/** 收集末级节点 name 集合(用于 leaf-only 拦截) */ +const collectLeafNames = (nodes: CategoryNode[]): Set => { + const result = new Set() + const walk = (list: CategoryNode[]): void => { + list.forEach((node) => { + if (node.children?.length) walk(node.children) + else result.add(node.name) + }) + } + walk(nodes) + return result +} + export const usePage = (emit: (e: 'load') => void) => { const { message } = useAntdStaticMethods() @@ -74,16 +102,19 @@ export const usePage = (emit: (e: 'load') => void) => { const form = ref
(createInitForm()) const formOptions = ref(createInitOptions()) + /** 设备分类全树(与统计页同源,弹框树选择器数据源;value=name 保留现有 deviceCategory 契约) */ + const categoryTreeData = ref([]) + /** 当前 Tab(控制设备类别下拉数据源) */ const activeTab = ref('catering') /** 投用状态实时联动(只读展示) */ const deployStatusText = computed(() => (form.value.deployStatus === 'deployed' ? '投用' : '在库')) - /** 当前所选类别对应的 networkMode(决定 SIM 卡字段显隐) */ + /** 当前所选类别对应的 networkMode(决定 SIM 卡字段显隐;从全树按 name 查找) */ const currentNetworkMode = computed(() => { - const matched = formOptions.value.deviceCategory.find((item) => item.value === form.value.deviceCategory) - return matched?.networkMode + if (!form.value.deviceCategory) return undefined + return findNodeByName(categoryTreeData.value, form.value.deviceCategory)?.networkMode }) /** 是否展示绑定 SIM 卡输入 */ @@ -119,8 +150,16 @@ export const usePage = (emit: (e: 'load') => void) => { onDeployFieldChange() } - /** 设备类别变化时:若新类别 networkMode !== 'sim',清空已填的 simIccid */ + /** 设备类别变化时:若新类别非末级则拒绝;若 networkMode !== 'sim',清空已填的 simIccid */ const onCategoryChange = (): void => { + /** leaf-only 拦截:非末级节点拒绝选中,保留原值 */ + if (form.value.deviceCategory) { + const leafNames = collectLeafNames(categoryTreeData.value) + if (!leafNames.has(form.value.deviceCategory)) { + form.value.deviceCategory = '' + return + } + } if (!showSimIccid.value) form.value.simIccid = '' } @@ -141,21 +180,14 @@ export const usePage = (emit: (e: 'load') => void) => { pageInfo.visible = false } - /** 按 activeTab 切换设备类别下拉 */ - const refreshCategoryOptions = (tab: DeviceCategoryTab): void => { - categoryOptions(tab) + /** 拉取设备分类全树(复用设备分类页接口,与统计页同源;弹框树选择器 + networkMode 查找共用) */ + const refreshCategoryTree = (): void => { + categoryTree() .then((res) => { - if (res.code === '00000') { - const list = res.data ?? [] - formOptions.value.deviceCategory = list.map((item: DeviceCategoryOption) => ({ - label: item.name, - value: item.name, - networkMode: item.networkMode, - })) - } + if (res.code === '00000') categoryTreeData.value = res.data ?? [] }) .catch((err: unknown) => { - console.error('设备类别下拉加载失败:', err) + console.error('设备分类树加载失败:', err) }) } @@ -216,8 +248,8 @@ export const usePage = (emit: (e: 'load') => void) => { ? `新增设备(${tabLabelMap[tab]})` : '编辑设备' - /** 并发加载下拉数据 */ - refreshCategoryOptions(tab) + /** 并发加载下拉数据(设备分类树为全树,无需按 Tab 刷新) */ + refreshCategoryTree() refreshCustomerOptions() const requests: Promise[] = [] @@ -230,9 +262,8 @@ export const usePage = (emit: (e: 'load') => void) => { if (infoRes?.code === '00000') { const data = infoRes.data form.value = { ...createInitForm(), ...data } - /** 编辑时按详情回填的 category 同步 activeTab,并刷新对应类别下拉 */ + /** 编辑时按详情回填的 category 同步 activeTab(仅用于标题展示) */ activeTab.value = data.category - refreshCategoryOptions(data.category) /** 客户已选定,按 customerId 加载场所下拉(便于编辑回显) */ if (data.customerId) refreshSiteOptions(data.customerId) } @@ -307,6 +338,8 @@ export const usePage = (emit: (e: 'load') => void) => { form, formOptions, activeTab, + categoryTreeData, + categoryTreeFieldNames: CATEGORY_TREE_FIELD_NAMES, deployStatusText, showSimIccid, rules, diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts index 4da845d..36d1d69 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts @@ -10,7 +10,7 @@ */ import type { SelectProps } from 'ant-design-vue' -import type { DeviceCategoryTab, DeployStatus, NetworkMode } from '../../../../types' +import type { DeviceCategoryTab, DeployStatus } from '../../../../types' export type ModalType = 'add' | 'edit' @@ -59,8 +59,6 @@ export interface Form { /** 下拉选项 */ export interface FormOptions { - /** 设备类别(按 activeTab 切换) */ - deviceCategory: { label: string; value: string; networkMode: NetworkMode }[] /** 所属客户 */ customer: { label: string; value: string }[] /** 所属场所(按 customerId 联动) */ diff --git a/src/pages/admin-portal/device-center/asset/manage/init/usePage.ts b/src/pages/admin-portal/device-center/asset/manage/init/usePage.ts index 5fe7a8d..fba220f 100644 --- a/src/pages/admin-portal/device-center/asset/manage/init/usePage.ts +++ b/src/pages/admin-portal/device-center/asset/manage/init/usePage.ts @@ -4,13 +4,17 @@ import type { TablePaginationConfig } from 'ant-design-vue' import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { createPaginationConfig } from '@utils/antDesign/table' import type { TableState } from '@utils/antDesign/table' -import { categoryOptions, customerOptions, exportList, importList, list, remove, siteOptions } from '../api' +import type { CategoryNode } from '../../category/types' +import { categoryTree, customerOptions, exportList, importList, list, remove, siteOptions } from '../api' import AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' import InfoDrawer from '../component/drawer/info/info.vue' -import type { CustomerOption, DeviceCategoryOption, DeviceCategoryTab, DeviceItem, SearchForm, SiteOption } from '../types' +import type { CustomerOption, DeviceCategoryTab, DeviceItem, SearchForm, SiteOption } from '../types' import { DEPLOY_STATUS_OPTIONS, createSearchKey } from './useSearch' import { useTable } from './useTable' +/** a-tree-select 字段映射(设备分类树节点,与统计页保持一致) */ +export const CATEGORY_TREE_FIELD_NAMES = { label: 'name', value: 'name', children: 'children' } + /** 5 个 Tab 元信息(与设备分类预置值对齐) */ export const TAB_LIST: { key: DeviceCategoryTab; label: string }[] = [ { key: 'catering', label: '智养餐饮设备' }, @@ -54,8 +58,8 @@ export const usePage = () => { const pageLoading = ref(false) const selectedRowKeys = ref<(string | number)[]>([]) - /** 设备类别下拉(按当前 Tab 联动刷新) */ - const deviceCategoryOptions = ref([]) + /** 设备分类全树(与统计页同源,筛选区树选择器数据源;value 用 name 保留现有 deviceCategory 契约) */ + const categoryTreeData = ref([]) /** 所属客户下拉(可搜索) */ const customerOptionList = ref([]) /** 所属场所下拉(按 customerId 联动过滤) */ @@ -94,14 +98,14 @@ export const usePage = () => { }) } - /** 拉取设备类别下拉(按当前 Tab) */ - const refreshCategoryOptions = (): void => { - categoryOptions(activeTab.value) + /** 拉取设备分类全树(复用设备分类页接口,与统计页同源) */ + const refreshCategoryTree = (keyword?: string): void => { + categoryTree(keyword) .then((res) => { - if (res.code === '00000') deviceCategoryOptions.value = res.data ?? [] + if (res.code === '00000') categoryTreeData.value = res.data ?? [] }) .catch((err: unknown) => { - console.error('设备类别下拉加载失败:', err) + console.error('设备分类树加载失败:', err) }) } @@ -149,13 +153,12 @@ export const usePage = () => { listRequest() } - /** 切换 Tab:重置搜索 + 刷新设备类别下拉 */ + /** 切换 Tab:重置搜索(树选择器数据源为全树,无需按 Tab 刷新) */ const switchTab = (keyOrEvent: string | { target: { value: string } }): void => { const key = typeof keyOrEvent === 'string' ? keyOrEvent : keyOrEvent.target.value activeTab.value = key as DeviceCategoryTab Object.assign(search, createSearchKey()) siteOptionList.value = [] - refreshCategoryOptions() table.pagination.current = 1 listRequest() } @@ -271,7 +274,7 @@ export const usePage = () => { } onMounted(() => { - refreshCategoryOptions() + refreshCategoryTree() refreshCustomerOptions() const q = route.query.inboundCode if (typeof q === 'string' && q) { @@ -293,7 +296,8 @@ export const usePage = () => { infoDrawerRef, tabList: TAB_LIST, deployStatusOptions: DEPLOY_STATUS_OPTIONS, - deviceCategoryOptions, + categoryTreeData, + categoryTreeFieldNames: CATEGORY_TREE_FIELD_NAMES, customerOptionList, siteOptionList, inboundCodeFilter, @@ -303,6 +307,7 @@ export const usePage = () => { onCustomerChange, refreshCustomerOptions, refreshSiteOptions, + refreshCategoryTree, dataSourceChange, handleAction, exportListData, diff --git a/src/pages/admin-portal/device-center/asset/manage/manage.vue b/src/pages/admin-portal/device-center/asset/manage/manage.vue index 344b960..104c1e6 100644 --- a/src/pages/admin-portal/device-center/asset/manage/manage.vue +++ b/src/pages/admin-portal/device-center/asset/manage/manage.vue @@ -33,15 +33,17 @@ /> - @@ -137,7 +139,8 @@ const { infoDrawerRef, tabList, deployStatusOptions, - deviceCategoryOptions, + categoryTreeData, + categoryTreeFieldNames, customerOptionList, siteOptionList, inboundCodeFilter, @@ -154,11 +157,6 @@ const { listRequest, } = usePage() -/** 设备类别下拉选项映射为 a-select 期望的 {label,value} 结构(value 用 name) */ -const deviceCategorySelectOptions = computed(() => - deviceCategoryOptions.value.map((item) => ({ label: item.name, value: item.name })), -) - /** 客户下拉搜索回调 */ const onCustomerSearch = (kw: string): void => { refreshCustomerOptions(kw) diff --git a/src/pages/admin-portal/device-center/ops/alarm/alarm.vue b/src/pages/admin-portal/device-center/ops/alarm/alarm.vue index 4336256..a772576 100644 --- a/src/pages/admin-portal/device-center/ops/alarm/alarm.vue +++ b/src/pages/admin-portal/device-center/ops/alarm/alarm.vue @@ -20,14 +20,17 @@ - @@ -73,23 +76,26 @@ * 业务对齐 §3.6 设备运维 → 告警管理: * - 运行/自检/巡检/SLA/OTA/数据分发失败 等告警统一处置 * - 告警详情:基础信息 + 报错日志(代码风格多行文本) + * - 设备分类筛选:a-tree-select 加载设备分类全树(与统计页同源,value=name 保留 deviceCategory 契约) */ import { onMounted, ref } from 'vue' import { NotificationOutlined, StopOutlined } from '@ant-design/icons-vue' +import type { CategoryNode } from '../../../asset/category/types' import FilterBar from '@components/FilterBar.vue' import TableCard from '@components/TableCard.vue' import StatCard from '@components/StatCard.vue' -import { filterOption } from '@utils/antDesign/select' import InfoDrawer from './component/drawer/info/info.vue' import { ALARM_LEVEL_OPTIONS, ALARM_SOURCE_OPTIONS, ALARM_STATUS_OPTIONS, - type DeviceCategoryOption, } from './types' import { usePage } from './init/usePage' import * as api from './api' +/** a-tree-select 字段映射(与统计页保持一致;value=name 保留 deviceCategory 契约) */ +const CATEGORY_TREE_FIELD_NAMES = { label: 'name', value: 'name', children: 'children' } + const { pageLoading, search, @@ -105,21 +111,19 @@ const { batchHandle, } = usePage() -/** 设备分类下拉(末级分类名,与设备监测模块对齐) */ -const categoryOpts = ref<{ label: string; value: string }[]>([]) +/** 设备分类全树(与统计页同源) */ +const categoryTreeData = ref([]) -const loadCategoryOpts = (): void => { - api.categoryOptions() +const loadCategoryTree = (): void => { + api.categoryTree() .then((res) => { - if (res.code === '00000') { - categoryOpts.value = (res.data ?? []).map((i: DeviceCategoryOption) => ({ label: i.name, value: i.name })) - } + if (res.code === '00000') categoryTreeData.value = res.data ?? [] }) - .catch((err: unknown) => console.error('设备分类下拉失败:', err)) + .catch((err: unknown) => console.error('设备分类树加载失败:', err)) } onMounted(() => { - loadCategoryOpts() + loadCategoryTree() }) diff --git a/src/pages/admin-portal/device-center/ops/alarm/api/index.ts b/src/pages/admin-portal/device-center/ops/alarm/api/index.ts index e790257..7f83677 100644 --- a/src/pages/admin-portal/device-center/ops/alarm/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/alarm/api/index.ts @@ -3,11 +3,12 @@ * - list 告警列表 / batchHandle 批量处理 / ignore 忽略 / toWorkOrder 转工单 * - info 告警详情(含报错日志) * - stats 顶部统计 - * - categoryOptions 设备分类下拉(末级分类名) + * - categoryTree 设备分类全树(复用设备分类页接口,供筛选区树选择器加载) */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { AlarmDetail, AlarmItem, AlarmStats, DeviceCategoryOption, SearchForm } from '../types' +import type { CategoryNode } from '../../../asset/category/types' +import type { AlarmDetail, AlarmItem, AlarmStats, SearchForm } from '../types' const BASE = '/admin/device/ops/alarm' @@ -33,6 +34,6 @@ export const ignore = (id: string): Promise> => export const batchHandle = (ids: string[], type: 'ignore' | 'workorder'): Promise> => postRequest('axiosRequest', BASE + '/batch', { ids, type }) -/** 设备分类下拉(末级分类名,与设备监测模块对齐) */ -export const categoryOptions = (): Promise> => - postRequest('axiosRequest', BASE + '/category/options', {}) +/** 设备分类全树(与设备分类页 / 统计页同源,供筛选区树选择器加载;value=name 保留 deviceCategory 契约) */ +export const categoryTree = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', { keyword }) diff --git a/src/pages/admin-portal/device-center/ops/alarm/types/index.ts b/src/pages/admin-portal/device-center/ops/alarm/types/index.ts index c0b1782..54b80e5 100644 --- a/src/pages/admin-portal/device-center/ops/alarm/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/alarm/types/index.ts @@ -54,17 +54,11 @@ export interface AlarmDetail extends AlarmItem { errorLog: string } -/** 设备分类下拉选项(末级分类名) */ -export interface DeviceCategoryOption { - /** 末级分类名(作为 value) */ - name: string -} - /** 查询表单 */ export interface SearchForm extends BaseSearchForm { /** 设备编码 */ deviceCode?: string - /** 设备分类(末级分类名) */ + /** 设备分类(末级分类名,与设备分类树叶子节点 name 对齐) */ deviceCategory?: string source?: AlarmSource level?: AlarmLevel diff --git a/src/pages/admin-portal/device-center/ops/init/api/index.ts b/src/pages/admin-portal/device-center/ops/init/api/index.ts index 34a746f..ea275dc 100644 --- a/src/pages/admin-portal/device-center/ops/init/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/init/api/index.ts @@ -6,14 +6,15 @@ * - batchSetVersion 批量版本设置 * - exportList 异步导出 * - appOptions / versionOptions 应用名+版本号下拉 - * - customerOptions / siteOptions / categoryOptions 筛选下拉 + * - customerOptions / siteOptions 筛选下拉 + * - categoryTree 设备分类全树(复用设备分类页接口,供筛选区树选择器加载) */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' +import type { CategoryNode } from '../../../asset/category/types' import type { AppOption, CustomerOption, - DeviceCategoryOption, InitDetail, InitItem, SearchForm, @@ -59,10 +60,6 @@ export const appOptions = (): Promise> => export const versionOptions = (appId: string): Promise> => postRequest('axiosRequest', BASE + '/version/options', { appId }) -/** 设备类别下拉(二级分类名) */ -export const categoryOptions = (): Promise> => - postRequest('axiosRequest', BASE + '/category/options', {}) - /** 所属客户下拉(可搜索) */ export const customerOptions = (keyword?: string): Promise> => postRequest('axiosRequest', BASE + '/customer/options', { keyword }) @@ -70,3 +67,7 @@ export const customerOptions = (keyword?: string): Promise> => postRequest('axiosRequest', BASE + '/site/options', { customerId, keyword }) + +/** 设备分类全树(与设备分类页 / 统计页 / 告警页同源,供筛选区树选择器加载;value=name 保留 deviceCategory 契约) */ +export const categoryTree = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', { keyword }) diff --git a/src/pages/admin-portal/device-center/ops/init/init.vue b/src/pages/admin-portal/device-center/ops/init/init.vue index 7f12d88..68e1eeb 100644 --- a/src/pages/admin-portal/device-center/ops/init/init.vue +++ b/src/pages/admin-portal/device-center/ops/init/init.vue @@ -2,14 +2,17 @@
- @@ -115,6 +118,7 @@ */ import { DownloadOutlined, SettingOutlined } from '@ant-design/icons-vue' import { onMounted, ref } from 'vue' +import type { CategoryNode } from '../../../asset/category/types' import FilterBar from '@components/FilterBar.vue' import TableCard from '@components/TableCard.vue' import VersionSetModal from './component/modal/versionSet/versionSet.vue' @@ -123,8 +127,6 @@ import { DEPLOY_STATUS_OPTIONS, INIT_STATUS_OPTIONS, type CustomerOption, - type DeviceCategoryOption, - type InitItem, type SiteOption, } from './types' import { usePage } from './init/usePage' @@ -132,6 +134,9 @@ import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { filterOption } from '@utils/antDesign/select' import * as api from './api' +/** a-tree-select 字段映射(与统计页 / 告警页保持一致;value=name 保留 deviceCategory 契约) */ +const CATEGORY_TREE_FIELD_NAMES = { label: 'name', value: 'name', children: 'children' } + const { pageLoading, search, @@ -151,22 +156,20 @@ const { const { message } = useAntdStaticMethods() -/** 设备类别下拉 */ -const categoryOpts = ref([]) +/** 设备分类全树(与统计页 / 告警页同源) */ +const categoryTreeData = ref([]) /** 所属客户下拉 */ const customerOpts = ref([]) /** 布点场所下拉(按客户联动) */ const siteOpts = ref([]) -/** 拉取设备类别下拉 */ -const loadCategoryOpts = (): void => { - api.categoryOptions() +/** 拉取设备分类全树 */ +const loadCategoryTree = (): void => { + api.categoryTree() .then((res) => { - if (res.code === '00000') { - categoryOpts.value = (res.data ?? []).map((i) => ({ label: i.name, value: i.name })) - } + if (res.code === '00000') categoryTreeData.value = res.data ?? [] }) - .catch((err: unknown) => console.error('设备类别下拉失败:', err)) + .catch((err: unknown) => console.error('设备分类树加载失败:', err)) } /** 拉取所属客户下拉 */ @@ -197,7 +200,7 @@ const onCustomerChange = (customerId: string | undefined): void => { } onMounted(() => { - loadCategoryOpts() + loadCategoryTree() loadCustomerOpts() listRequest() }) diff --git a/src/pages/admin-portal/device-center/ops/init/types/index.ts b/src/pages/admin-portal/device-center/ops/init/types/index.ts index 39b3a46..5ba75bd 100644 --- a/src/pages/admin-portal/device-center/ops/init/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/init/types/index.ts @@ -5,6 +5,7 @@ * - 列表展示所有待初始化设备(设备视角,非任务视角) * - 仅维护「软件版本设置」一个写操作(单条 / 批量) * - 其他元数据(分类 / 数据分发 / 数据上报)在对应模块维护,此处只读展示 + * - 设备类别:末级分类名(与设备监测 / 告警模块对齐,由 categoryTree 接口动态拉取) */ import type { BaseSearchForm } from '@pages/admin-portal/shared/types' @@ -22,7 +23,7 @@ export interface InitItem { id: string /** 设备编码 */ deviceCode: string - /** 设备类别(二级分类名) */ + /** 设备类别(末级分类名,与设备分类树叶子节点 name 对齐) */ deviceCategory: string /** 设备名称 */ deviceName: string @@ -105,7 +106,7 @@ export interface InitDetail extends InitItem { export interface SearchForm extends BaseSearchForm { /** 设备编码 */ deviceCode?: string - /** 设备类别(二级分类名) */ + /** 设备类别(末级分类名,与设备分类树叶子节点 name 对齐) */ deviceCategory?: string /** 初始化状态 */ initStatus?: InitStatus @@ -159,12 +160,6 @@ export interface SiteOption { customerId: string } -/** 设备类别下拉选项 */ -export interface DeviceCategoryOption { - /** 二级分类名(作为 value) */ - name: string -} - /** 初始化状态下拉 */ export const INIT_STATUS_OPTIONS: { label: string; value: InitStatus }[] = [ { label: '待初始化', value: 'pending' }, diff --git a/src/pages/admin-portal/device-center/ops/monitor/api/index.ts b/src/pages/admin-portal/device-center/ops/monitor/api/index.ts index 69588f6..9d75e5c 100644 --- a/src/pages/admin-portal/device-center/ops/monitor/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/monitor/api/index.ts @@ -4,12 +4,12 @@ * - stats 顶部统计卡 * - info 详情(含 6 Tab 聚合数据) * - remoteAction 远程下发:重启 - * - categoryOptions 设备分类下拉(末级分类名) + * - categoryTree 设备分类全树(复用设备分类页接口,供筛选区树选择器加载) */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' +import type { CategoryNode } from '../../../asset/category/types' import type { - DeviceCategoryOption, MonitorDetail, MonitorItem, MonitorStats, @@ -31,6 +31,6 @@ export const info = (id: string): Promise> => export const remoteAction = (id: string): Promise> => postRequest('axiosRequest', BASE + '/remote', { id, type: 'restart' }) -/** 设备分类下拉(末级分类名,与设备初始化模块对齐) */ -export const categoryOptions = (): Promise> => - postRequest('axiosRequest', BASE + '/category/options', {}) +/** 设备分类全树(与设备分类页 / 统计页同源,供筛选区树选择器加载;value=name 保留 deviceCategory 契约) */ +export const categoryTree = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', { keyword }) diff --git a/src/pages/admin-portal/device-center/ops/monitor/monitor.vue b/src/pages/admin-portal/device-center/ops/monitor/monitor.vue index ef3d834..aff7aab 100644 --- a/src/pages/admin-portal/device-center/ops/monitor/monitor.vue +++ b/src/pages/admin-portal/device-center/ops/monitor/monitor.vue @@ -23,14 +23,17 @@ - @@ -73,17 +76,21 @@ * 业务对齐 §3.6 设备运维 → 设备监测: * - 主列表:设备运行状态总览 * - 详情页(多 Tab 聚合):实施/自检/巡检/运行日志/更新记录/维修记录 + * - 设备分类筛选:a-tree-select 加载设备分类全树(与统计页同源,value=name 保留 deviceCategory 契约) */ import { onMounted, ref } from 'vue' +import type { CategoryNode } from '../../../asset/category/types' import FilterBar from '@components/FilterBar.vue' import TableCard from '@components/TableCard.vue' import StatCard from '@components/StatCard.vue' import InfoDrawer from './component/drawer/info/info.vue' -import { RUN_STATUS_OPTIONS, type DeviceCategoryOption } from './types' +import { RUN_STATUS_OPTIONS } from './types' import { usePage } from './init/usePage' -import { filterOption } from '@utils/antDesign/select' import * as api from './api' +/** a-tree-select 字段映射(与统计页保持一致;value=name 保留 deviceCategory 契约) */ +const CATEGORY_TREE_FIELD_NAMES = { label: 'name', value: 'name', children: 'children' } + const { pageLoading, search, @@ -96,21 +103,19 @@ const { handleAction, } = usePage() -/** 设备分类下拉(末级分类名,与设备初始化模块对齐) */ -const categoryOpts = ref<{ label: string; value: string }[]>([]) +/** 设备分类全树(与统计页同源) */ +const categoryTreeData = ref([]) -const loadCategoryOpts = (): void => { - api.categoryOptions() +const loadCategoryTree = (): void => { + api.categoryTree() .then((res) => { - if (res.code === '00000') { - categoryOpts.value = (res.data ?? []).map((i: DeviceCategoryOption) => ({ label: i.name, value: i.name })) - } + if (res.code === '00000') categoryTreeData.value = res.data ?? [] }) - .catch((err: unknown) => console.error('设备分类下拉失败:', err)) + .catch((err: unknown) => console.error('设备分类树加载失败:', err)) } onMounted(() => { - loadCategoryOpts() + loadCategoryTree() }) diff --git a/src/pages/admin-portal/device-center/ops/monitor/types/index.ts b/src/pages/admin-portal/device-center/ops/monitor/types/index.ts index 80a3ff6..5356bdf 100644 --- a/src/pages/admin-portal/device-center/ops/monitor/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/monitor/types/index.ts @@ -39,7 +39,7 @@ export interface MonitorItem { /** 查询表单 */ export interface SearchForm extends BaseSearchForm { - /** 设备分类(末级分类名) */ + /** 设备分类(末级分类名,与设备分类树叶子节点 name 对齐) */ deviceCategory?: string runStatus?: RunStatus tenantName?: string @@ -47,12 +47,6 @@ export interface SearchForm extends BaseSearchForm { hasAlert?: boolean } -/** 设备分类下拉选项(末级分类名) */ -export interface DeviceCategoryOption { - /** 末级分类名(作为 value) */ - name: string -} - /** 运行状态下拉 */ export const RUN_STATUS_OPTIONS: { label: string; value: RunStatus }[] = [ { label: '在用', value: 'online' }, diff --git a/src/pages/admin-portal/device-center/stock/inventory/api/index.ts b/src/pages/admin-portal/device-center/stock/inventory/api/index.ts index 6077285..d01ee4e 100644 --- a/src/pages/admin-portal/device-center/stock/inventory/api/index.ts +++ b/src/pages/admin-portal/device-center/stock/inventory/api/index.ts @@ -18,6 +18,6 @@ export const add = (params: InventoryPayload): Promise> => export const audit = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/stock/inventory/audit', { id }) -/** 删除盘点单(仅草稿状态可删) */ +/** 删除盘点单(仅进行中状态可删) */ export const remove = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/stock/inventory/delete', { id }) diff --git a/src/pages/admin-portal/device-center/stock/inventory/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/stock/inventory/component/modal/addOrEdit/addOrEdit.vue index 2e524f3..89d3c80 100644 --- a/src/pages/admin-portal/device-center/stock/inventory/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/stock/inventory/component/modal/addOrEdit/addOrEdit.vue @@ -62,8 +62,8 @@ type="info" show-icon :message="form.inventoryType === 'partial' - ? '提交后系统按所选品类 + 该区域当前在库台账生成应盘清单(按『设备类别末级分类 + 批次』聚合),盘点单进入草稿状态;小程序扫码完成实盘后变已完成;后台审核后修正库存台账。' - : '提交后系统按该区域当前在库台账生成应盘清单(按『设备类别末级分类 + 批次』聚合),盘点单进入草稿状态;小程序扫码完成实盘后变已完成;后台审核后修正库存台账。'" + ? '提交后系统按所选品类 + 该区域当前在库台账生成应盘清单(按『设备类别末级分类 + 批次』聚合),盘点单进入进行中状态;小程序扫码完成实盘后变已完成;后台审核后修正库存台账。' + : '提交后系统按该区域当前在库台账生成应盘清单(按『设备类别末级分类 + 批次』聚合),盘点单进入进行中状态;小程序扫码完成实盘后变已完成;后台审核后修正库存台账。'" style="margin-top: 8px" /> diff --git a/src/pages/admin-portal/device-center/stock/inventory/init/usePage.ts b/src/pages/admin-portal/device-center/stock/inventory/init/usePage.ts index 4d051ee..de3e900 100644 --- a/src/pages/admin-portal/device-center/stock/inventory/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/inventory/init/usePage.ts @@ -10,8 +10,8 @@ import type InfoDrawer from '../component/drawer/info/info.vue' /** * 库存盘点页主编排: - * - 列表 + 新增(选仓库+类型)+ 查看详情(抽屉)+ 审核 + 删除(仅草稿可删) - * - 状态流转:草稿 → 已完成(小程序扫码提交后)→ 已审核(后台审核后修正库存台账) + * - 列表 + 新增(选仓库+类型)+ 查看详情(抽屉)+ 审核 + 删除(仅进行中可删) + * - 状态流转:进行中 → 已完成(小程序扫码提交后)→ 已审核(后台审核后修正库存台账) * - 实盘扫码动作在小程序端做,后台只读查看实盘数据并审核 */ export const usePage = () => { @@ -105,10 +105,10 @@ export const usePage = () => { }) } - /** 删除(仅 draft 可删) */ + /** 删除(仅 in_progress 可删) */ const deleteRecord = (record: ListItem): void => { - if (record.status !== 'draft') { - void message.warning('仅"草稿"状态的盘点单可删除') + if (record.status !== 'in_progress') { + void message.warning('仅"进行中"状态的盘点单可删除') return } Modal.confirm({ diff --git a/src/pages/admin-portal/device-center/stock/inventory/inventory.vue b/src/pages/admin-portal/device-center/stock/inventory/inventory.vue index d8fbe94..9518fb1 100644 --- a/src/pages/admin-portal/device-center/stock/inventory/inventory.vue +++ b/src/pages/admin-portal/device-center/stock/inventory/inventory.vue @@ -44,9 +44,9 @@ size="small" @click="handleAction('audit', record)" >审核 - + = [ - { label: '草稿', value: 'draft' }, + { label: '进行中', value: 'in_progress' }, { label: '已完成', value: 'completed' }, { label: '已审核', value: 'audited' }, ] @@ -95,7 +95,7 @@ export const INVENTORY_TYPE_OPTIONS: Array<{ label: string; value: InventoryType ] export const INVENTORY_STATUS_COLOR: Record = { - draft: 'default', + in_progress: 'processing', completed: 'warning', audited: 'success', } diff --git a/src/pages/miniprogram/ops/inspect/Home.vue b/src/pages/miniprogram/ops/inspect/Home.vue index 260c755..52216a3 100644 --- a/src/pages/miniprogram/ops/inspect/Home.vue +++ b/src/pages/miniprogram/ops/inspect/Home.vue @@ -1,6 +1,10 @@ @@ -86,14 +106,15 @@ * - 功能入口:消息中心 / 离线缓存 / 账号 / 修改密码 / 退出 * - 离线缓存弹层 */ -import { computed, onMounted, reactive, ref } from 'vue' +import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' -import { showSuccessToast, showToast } from 'vant' +import { showSuccessToast } from 'vant' import { clearOffline, getOfflineStatus, getPersonalStats, listMessages, + logout, syncOffline, } from './api' import type { OfflineStatus, PersonalStats } from './types' @@ -108,19 +129,42 @@ const offline = reactive({ lastSyncTime: '—', }) const showOffline = ref(false) +const showLogout = ref(false) const unreadCount = ref(0) const goMessages = (): void => { router.push('/miniprogram/ops/message-center') } +// 右上角设置图标 + 账号信息 cell:跳账号信息子页 const onSetting = (): void => { - // 原型阶段未实现,用普通 toast 提示,避免误导为失败 - showToast('原型阶段未实现') + router.push('/miniprogram/ops/account-info') } +const goAccountInfo = (): void => { + router.push('/miniprogram/ops/account-info') +} + +const goChangePassword = (): void => { + router.push('/miniprogram/ops/change-password') +} + +// 退出登录:仅弹出确认弹层(原型不真正清登录态、不跳门禁页) const onLogout = (): void => { - showToast('原型阶段未实现') + showLogout.value = true +} + +// 确认退出:调用登出接口 → 仅 toast 提示,原型阶段不真正退出登录 +const onLogoutConfirm = (): void => { + showLogout.value = false + logout() + .then((res) => { + if (res.code === '00000') { + showSuccessToast('已退出登录(原型示例)') + } + }) + .catch(() => undefined) + .finally(() => undefined) } const onSync = (): void => { @@ -282,6 +326,44 @@ onMounted(() => { flex-direction: column; gap: 8px; } + + &__logout { + text-align: center; + + :deep(.van-popup__close-icon) { + font-size: 18px; + color: var(--text-placeholder); + top: 12px; + right: 12px; + } + } + + &__logout-icon { + :deep(.van-icon) { + font-size: 48px; + color: var(--warning); + } + margin-bottom: 12px; + } + + &__logout-title { + font-size: var(--font-lg); + font-weight: 600; + color: var(--text-primary); + margin-bottom: 6px; + } + + &__logout-desc { + font-size: var(--font-sm); + color: var(--text-secondary); + line-height: 1.6; + margin-bottom: 20px; + } + + &__logout-actions { + display: flex; + gap: 12px; + } } .me-stat { diff --git a/src/pages/miniprogram/ops/me/accountInfo.vue b/src/pages/miniprogram/ops/me/accountInfo.vue new file mode 100644 index 0000000..ecd1ae6 --- /dev/null +++ b/src/pages/miniprogram/ops/me/accountInfo.vue @@ -0,0 +1,142 @@ + + + + + diff --git a/src/pages/miniprogram/ops/me/api.ts b/src/pages/miniprogram/ops/me/api.ts index bbb5dae..b88371a 100644 --- a/src/pages/miniprogram/ops/me/api.ts +++ b/src/pages/miniprogram/ops/me/api.ts @@ -2,7 +2,14 @@ * 实施运维小程序 / 我的 接口 */ import { getRequest, postRequest } from '@axios' -import type { MessageItem, MessageType, OfflineStatus, PersonalStats } from './types' +import type { + ChangePasswordParams, + MessageItem, + MessageType, + OfflineStatus, + PersonalStats, + UserProfile, +} from './types' /** 个人统计 */ export const getPersonalStats = (): Promise<{ code: string; msg: string; data: PersonalStats }> => @@ -33,3 +40,17 @@ export const syncOffline = (): Promise<{ code: string; msg: string; data: null } /** 清除离线缓存 */ export const clearOffline = (): Promise<{ code: string; msg: string; data: null }> => postRequest('axiosRequest', '/mp/ops/me/offline-clear') + +/** 账号信息 */ +export const getProfile = (): Promise<{ code: string; msg: string; data: UserProfile }> => + getRequest('axiosRequest', '/mp/ops/me/profile') + +/** 修改密码 */ +export const changePassword = ( + params: ChangePasswordParams, +): Promise<{ code: string; msg: string; data: null }> => + postRequest('axiosRequest', '/mp/ops/me/change-password', params) + +/** 退出登录 */ +export const logout = (): Promise<{ code: string; msg: string; data: null }> => + postRequest('axiosRequest', '/mp/ops/me/logout') diff --git a/src/pages/miniprogram/ops/me/changePassword.vue b/src/pages/miniprogram/ops/me/changePassword.vue new file mode 100644 index 0000000..15a9ccd --- /dev/null +++ b/src/pages/miniprogram/ops/me/changePassword.vue @@ -0,0 +1,149 @@ + + + + + diff --git a/src/pages/miniprogram/ops/me/types.ts b/src/pages/miniprogram/ops/me/types.ts index 0f56698..bfd69dd 100644 --- a/src/pages/miniprogram/ops/me/types.ts +++ b/src/pages/miniprogram/ops/me/types.ts @@ -40,3 +40,23 @@ export interface OfflineStatus { pendingCount: number lastSyncTime: string } + +/** 账号信息 */ +export interface UserProfile { + userId: string // 用户 ID + name: string // 姓名 + role: string // 角色名(展示用) + jobNumber: string // 工号 + phone: string // 手机号 + department: string // 所属部门 + jobTitle: string // 岗位 + region: string // 所属区域 + hireDate: string // 入职日期 + lastLoginAt: string // 上次登录时间 +} + +/** 修改密码入参 */ +export interface ChangePasswordParams { + oldPassword: string // 旧密码 + newPassword: string // 新密码 +} diff --git a/src/pages/miniprogram/ops/scan/api.ts b/src/pages/miniprogram/ops/scan/api.ts index 59915b8..892e32b 100644 --- a/src/pages/miniprogram/ops/scan/api.ts +++ b/src/pages/miniprogram/ops/scan/api.ts @@ -11,6 +11,7 @@ import type { OtaVersion, RepairForm, ScanHistoryItem, + SimUnboundOption, } from './types' /** 扫码识别设备(按设备编码查摘要) */ @@ -29,6 +30,16 @@ export const submitInit = ( ): Promise<{ code: string; msg: string; data: null }> => postRequest('axiosRequest', '/mp/ops/scan/init', params) +/** 自动获取虹软人脸识别激活码(设备初始化时调用) */ +export const fetchArcsoftKey = ( + deviceCode: string, +): Promise<{ code: string; msg: string; data: { activeKey: string } }> => + getRequest('axiosRequest', '/mp/ops/scan/arcsoft/fetch', { deviceCode }) + +/** 查询未绑定 SIM 卡列表(设备初始化时下拉选用) */ +export const listUnboundSims = (): Promise<{ code: string; msg: string; data: SimUnboundOption[] }> => + getRequest('axiosRequest', '/mp/ops/scan/sim/unbound') + /** 提交实施记录上报 */ export const submitImplRecord = ( params: ImplRecordForm, diff --git a/src/pages/miniprogram/ops/scan/implRecord.vue b/src/pages/miniprogram/ops/scan/implRecord.vue index 8cdbb20..546d667 100644 --- a/src/pages/miniprogram/ops/scan/implRecord.vue +++ b/src/pages/miniprogram/ops/scan/implRecord.vue @@ -156,7 +156,7 @@ const loadDevice = (): void => { scanDevice(code) .then((res) => { if (res.code === '00000' && res.data) { - deviceName.value = res.data.name + deviceName.value = res.data.deviceName } }) .catch(() => undefined) diff --git a/src/pages/miniprogram/ops/scan/init.vue b/src/pages/miniprogram/ops/scan/init.vue index fce97f1..61b39ce 100644 --- a/src/pages/miniprogram/ops/scan/init.vue +++ b/src/pages/miniprogram/ops/scan/init.vue @@ -13,17 +13,33 @@ - - diff --git a/src/pages/miniprogram/ops/scan/monitor.vue b/src/pages/miniprogram/ops/scan/monitor.vue index e4b0a94..8d4dcbb 100644 --- a/src/pages/miniprogram/ops/scan/monitor.vue +++ b/src/pages/miniprogram/ops/scan/monitor.vue @@ -11,8 +11,8 @@ @@ -95,6 +95,7 @@ /** * 实施运维小程序 / 维修上报(无工单独立新增入口) * - 关联工单可选填(区别于工单关单前上报,那里工单 ID 必填且不可改) + * - 故障类型与后台 FaultType 枚举一致(hardware/software/network/mechanical/electrical/other) * - 表单字段与工单关单前维修上报一致 */ import { computed, onMounted, reactive, ref } from 'vue' @@ -102,6 +103,8 @@ import { useRoute, useRouter } from 'vue-router' import { showSuccessToast } from 'vant' import { scanDevice, submitRepair } from './api' import { formatNow } from '@utils/datetime' +import { FAULT_TYPE_OPTS, faultTypeText as toFaultTypeText } from '@pages/miniprogram/ops/workOrder/constants' +import type { FaultType } from '@pages/miniprogram/ops/workOrder/types' import type { RepairForm } from './types' const route = useRoute() @@ -125,23 +128,13 @@ const form = reactive({ repairTime: '', }) -const faultTypeOpts = [ - { text: '硬件故障', value: '硬件故障' }, - { text: '软件故障', value: '软件故障' }, - { text: '网络故障', value: '网络故障' }, - { text: '传感器故障', value: '传感器故障' }, - { text: '电源故障', value: '电源故障' }, - { text: '机械故障', value: '机械故障' }, - { text: '其他', value: '其他' }, -] - -const faultTypeText = computed(() => form.faultType) +const faultTypeText = computed(() => (form.faultType ? toFaultTypeText(form.faultType as FaultType) : '')) const onBack = (): void => { router.back() } -const onFaultTypeConfirm = ({ selectedOptions }: { selectedOptions: Array<{ text: string; value: string }> }): void => { +const onFaultTypeConfirm = ({ selectedOptions }: { selectedOptions: Array<{ text: string; value: FaultType }> }): void => { form.faultType = selectedOptions[0]?.value ?? '' showFaultType.value = false } @@ -178,7 +171,7 @@ const loadDevice = (): void => { scanDevice(code) .then((res) => { if (res.code === '00000' && res.data) { - deviceName.value = res.data.name + deviceName.value = res.data.deviceName } }) .catch(() => undefined) diff --git a/src/pages/miniprogram/ops/scan/sceneMenu.vue b/src/pages/miniprogram/ops/scan/sceneMenu.vue index e401bde..e32ea41 100644 --- a/src/pages/miniprogram/ops/scan/sceneMenu.vue +++ b/src/pages/miniprogram/ops/scan/sceneMenu.vue @@ -14,16 +14,17 @@
-
{{ device.name }}
-
{{ device.code }}
+
{{ device.deviceName }}
+
{{ device.deviceCode }}
{{ statusText(device.status) }}
型号{{ device.model }}
-
固件{{ device.firmwareVersion }}
-
场所{{ device.site }}
-
租户{{ device.tenant }}
+
应用{{ device.packageName }}
+
版本{{ device.softwareVersion }}
+
场所{{ device.siteName }}
+
租户{{ device.tenantName }}
@@ -98,7 +99,7 @@ const ALL_ACTIONS: SceneAction[] = [ }, { key: 'repair', label: '维修上报', desc: '关联工单 / 无工单独立新增', - icon: 'tools-o', path: (id) => `/miniprogram/ops/repair/${id}`, + icon: 'warn-o', path: (id) => `/miniprogram/ops/repair/${id}`, statuses: ['inUse'], }, { @@ -153,7 +154,7 @@ const onBack = (): void => { const onAction = (action: SceneAction): void => { if (!device.value) return - router.push(action.path(device.value.code)) + router.push(action.path(device.value.deviceCode)) } const loadDevice = (): void => { diff --git a/src/pages/miniprogram/ops/scan/types.ts b/src/pages/miniprogram/ops/scan/types.ts index 4363112..f99af98 100644 --- a/src/pages/miniprogram/ops/scan/types.ts +++ b/src/pages/miniprogram/ops/scan/types.ts @@ -13,20 +13,34 @@ export type DeviceCategory = | 'fitness' // 健身器材设备 | 'health' // 健康监测设备 -/** 扫码后的设备摘要(场景菜单用) */ +/** 扫码后的设备摘要(场景菜单用,字段名与后台 device-center/ops/monitor 对齐) */ export interface DeviceSummary { id: string - code: string - name: string + /** 设备编码 */ + deviceCode: string + /** 设备名称 */ + deviceName: string + /** 顶级分类(ops 现场视角,与后台末级分类名不同) */ category: DeviceCategory + /** 设备型号(ops 现场视角) */ model: string + /** 运行状态(ops 现场 4 态,回写后台时映射 runStatus) */ status: DeviceStatus - site: string - tenant: string - firmwareVersion: string + /** 所属场所 */ + siteName: string + /** 所属租户 */ + tenantName: string + /** 应用名称(与后台 monitor.packageName 对齐) */ + packageName: string + /** 软件版本号(与后台 monitor.softwareVersion 对齐) */ + softwareVersion: string + /** 最近心跳时间 */ lastHeartbeat: string + /** 待处理工单数 */ pendingWorkOrders: number + /** 是否物联网设备 */ isIot: boolean + /** 是否启用数据分发 */ dataDistributeEnabled: boolean } @@ -58,8 +72,10 @@ export interface ImplRecordForm { /** 设备初始化表单 */ export interface InitForm { deviceCode: string - bindKey: string - certBurned: boolean + /** 虹软人脸识别激活码(点"自动获取"调接口拿,也允许手动输入) */ + arcsoftKey: string + /** 绑定的 SIM 卡 ICCID(从"未绑定 SIM 卡"下拉选) */ + simIccid: string siteConfig: string firmwareBaseline: string result: 'success' | 'fail' @@ -68,6 +84,19 @@ export interface InitForm { operateTime: string } +/** 未绑定 SIM 卡下拉选项(设备初始化时选用) */ +export interface SimUnboundOption { + id: string + /** SIM 卡 ICCID(提交时回传) */ + iccid: string + /** 手机号 */ + phoneNo: string + /** 运营商:mobile 移动 / unicom 联通 / telecom 电信 / broadnet 广域 */ + carrier: 'mobile' | 'unicom' | 'telecom' | 'broadnet' + /** 套餐类型 */ + packageType: string +} + /** 维修上报表单(无工单独立入口) */ export interface RepairForm { deviceCode: string @@ -82,11 +111,11 @@ export interface RepairForm { repairTime: string } -/** OTA 可用版本 */ +/** OTA 可用版本(状态与后台 device-center/ops/version VersionStatus 对齐:active 有效 / disabled 停用) */ export interface OtaVersion { version: string releaseNote: string - status: 'gray' | 'full' | 'stopped' + status: 'active' | 'disabled' size: string } diff --git a/src/pages/miniprogram/ops/workOrder/Home.vue b/src/pages/miniprogram/ops/workOrder/Home.vue index 05e8907..8ed311e 100644 --- a/src/pages/miniprogram/ops/workOrder/Home.vue +++ b/src/pages/miniprogram/ops/workOrder/Home.vue @@ -2,15 +2,15 @@
- + - +
- +
@@ -28,15 +28,15 @@ v-for="item in list" :key="item.id" class="wo-card" - :class="{ 'wo-card--over': item.overSla }" + :class="{ 'wo-card--over': item.slaOverdue }" @click="goDetail(item.id)" >
- {{ workOrderTypeText(item.type) }} - {{ item.code }} - 超 SLA + {{ orderSourceText(item.source) }} + {{ item.orderCode }} + {{ faultLevelText(item.faultLevel) }} + 超 SLA
-
{{ item.title }}
{{ item.deviceCode }} @@ -44,14 +44,17 @@
- {{ item.site }} + {{ item.siteName }} + · + {{ item.customerName }}
+
{{ item.faultDesc }}
@@ -65,20 +68,22 @@ + + diff --git a/src/pages/miniprogram/ops/workbench/otaList.vue b/src/pages/miniprogram/ops/workbench/otaList.vue new file mode 100644 index 0000000..a449264 --- /dev/null +++ b/src/pages/miniprogram/ops/workbench/otaList.vue @@ -0,0 +1,291 @@ + + + + + diff --git a/src/pages/miniprogram/ops/workbench/stockInbound.vue b/src/pages/miniprogram/ops/workbench/stockInbound.vue index 8ee43f9..335bf9f 100644 --- a/src/pages/miniprogram/ops/workbench/stockInbound.vue +++ b/src/pages/miniprogram/ops/workbench/stockInbound.vue @@ -3,7 +3,7 @@ - +
+ + + + + + - + - + + + + + + + + + + +
diff --git a/src/pages/miniprogram/ops/workbench/stockInventory.vue b/src/pages/miniprogram/ops/workbench/stockInventory.vue index 94a9bd1..c200cf1 100644 --- a/src/pages/miniprogram/ops/workbench/stockInventory.vue +++ b/src/pages/miniprogram/ops/workbench/stockInventory.vue @@ -7,9 +7,18 @@
- + + + + + + + +
@@ -36,17 +45,17 @@ - + - +
@@ -55,37 +64,48 @@
-
{{ item.deviceCode }}
-
{{ item.deviceName }}
+
{{ item.categoryName }}
- {{ item.modelName }} + 批次 {{ item.batchNo ?? '—' }} · - {{ item.batchNo }} + 应盘 {{ item.expectedQuantity }} + · + 实盘 {{ item.actualQuantity }}
{{ resultText(item.result) }}
-
- - {{ opt.label }} - + +
+ +
+ + {{ opt.label }} + +
- -