From 74a5419028d2c96b5d2d441f1c16e0155de5f9ce Mon Sep 17 00:00:00 2001 From: fengpu <18691763612@163.com> Date: Wed, 8 Jul 2026 18:45:19 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E5=A4=87=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E8=AF=A6=E6=83=85/=E7=BC=96=E8=BE=91=E5=85=A8=E9=87=8F?= =?UTF-8?q?=E5=AE=8C=E5=96=84=20+=20SIM=E5=8D=A1=E7=AE=A1=E7=90=86?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=20+=20=E5=85=AC=E5=85=B1=E9=80=89=E6=8B=A9?= =?UTF-8?q?=E5=99=A8=E7=BB=84=E4=BB=B6=E6=8A=BD=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 资产/库存/客户/第三方授权:详情抽屉与编辑弹框完善,mock 数据补齐 - 新增页面:SIM 卡管理(含绑定记录/绑定设备/导入弹框)、虹软批量导入、客户档案站点简报、出库详情抽屉、仓库库区库存汇总 - 公共组件:DeviceCategoryPicker / ExportTaskDrawer / WarehouseAreaPicker - 运维小程序:工单/告警/库存作业页交互打磨 - 删除:库存退货弹框(returnGoods) Co-Authored-By: Claude Opus 4.7 --- src/api/index.ts | 30 + .../handlers/device-center/asset/category.ts | 58 +- .../handlers/device-center/asset/inbound.ts | 178 ++++- .../handlers/device-center/asset/instock.ts | 108 +-- .../handlers/device-center/asset/manage.ts | 642 +++++++++++++----- .../handlers/device-center/asset/outbound.ts | 130 +++- .../device-center/asset/statistics.ts | 303 ++++++--- .../device-center/customer/archive.ts | 28 +- .../handlers/device-center/customer/site.ts | 124 ++-- .../device-center/customer/supplier.ts | 16 + .../handlers/device-center/export-task.ts | 93 +++ .../handlers/device-center/stock/inventory.ts | 155 +++-- .../handlers/device-center/stock/warehouse.ts | 57 +- .../handlers/device-center/third-auth/amap.ts | 45 +- .../device-center/third-auth/arcsoft.ts | 66 +- .../handlers/device-center/third-auth/sim.ts | 259 +++++++ .../device-center/workbench/overview.ts | 2 +- src/axios/handlers/index.ts | 4 + src/axios/index.ts | 3 + src/components/DeviceCategoryPicker.vue | 117 ++++ src/components/ExportTaskDrawer.vue | 168 +++++ src/components/OpsBottomActions.vue | 14 +- src/components/WarehouseAreaPicker.vue | 113 +++ src/layouts/MiniprogramLayout.vue | 8 + src/meta/nav.ts | 9 +- .../device-center/asset/category/category.vue | 9 + .../component/modal/addOrEdit/addOrEdit.vue | 94 ++- .../component/modal/addOrEdit/init/usePage.ts | 18 +- .../component/modal/addOrEdit/types/index.ts | 6 + .../asset/category/init/usePage.ts | 2 + .../asset/category/types/index.ts | 22 + .../device-center/asset/manage/api/index.ts | 22 +- .../manage/component/drawer/info/info.vue | 205 ++---- .../component/drawer/info/init/usePage.ts | 35 +- .../component/drawer/info/types/index.ts | 86 +-- .../component/modal/addOrEdit/addOrEdit.vue | 255 +++---- .../component/modal/addOrEdit/init/usePage.ts | 249 +++---- .../component/modal/addOrEdit/types/index.ts | 111 +-- .../asset/manage/init/usePage.ts | 136 +++- .../asset/manage/init/useSearch.ts | 17 +- .../asset/manage/init/useTable.ts | 107 +-- .../device-center/asset/manage/manage.vue | 76 ++- .../device-center/asset/manage/types/index.ts | 93 ++- .../asset/statistics/api/index.ts | 26 +- .../asset/statistics/init/usePage.ts | 107 ++- .../asset/statistics/init/useSearch.ts | 24 +- .../asset/statistics/init/useTable.ts | 110 ++- .../asset/statistics/statistics.vue | 104 +-- .../asset/statistics/types/index.ts | 57 +- .../customer/archive/archive.vue | 3 + .../archive/component/drawer/info/info.vue | 10 +- .../component/drawer/info/types/index.ts | 8 + .../component/modal/siteBrief/api/index.ts | 12 + .../component/modal/siteBrief/init/usePage.ts | 65 ++ .../component/modal/siteBrief/siteBrief.vue | 56 ++ .../component/modal/siteBrief/types/index.ts | 32 + .../customer/archive/init/usePage.ts | 16 +- .../customer/archive/init/useTable.ts | 32 +- .../device-center/customer/site/api/index.ts | 6 +- .../site/component/drawer/info/info.vue | 34 +- .../site/component/drawer/info/types/index.ts | 22 +- .../component/modal/addOrEdit/addOrEdit.vue | 12 +- .../component/modal/addOrEdit/init/usePage.ts | 4 +- .../component/modal/addOrEdit/types/index.ts | 4 +- .../customer/site/init/usePage.ts | 24 +- .../customer/site/init/useSearch.ts | 2 +- .../customer/site/init/useTable.ts | 28 +- .../device-center/customer/site/site.vue | 12 +- .../customer/site/types/index.ts | 30 +- .../device-center/stock/inbound/api/index.ts | 4 + .../component/modal/addOrEdit/addOrEdit.vue | 223 ++++-- .../component/modal/addOrEdit/api/index.ts | 6 +- .../component/modal/addOrEdit/init/usePage.ts | 156 ++++- .../component/modal/addOrEdit/types/index.ts | 28 +- .../device-center/stock/inbound/inbound.vue | 53 +- .../stock/inbound/init/usePage.ts | 44 +- .../stock/inbound/init/useSearch.ts | 3 + .../stock/inbound/init/useTable.ts | 9 +- .../stock/inbound/types/index.ts | 80 ++- .../device-center/stock/instock/api/index.ts | 6 +- .../instock/component/drawer/info/info.vue | 27 +- .../component/modal/returnGoods/api/index.ts | 10 - .../modal/returnGoods/init/usePage.ts | 92 --- .../stock/instock/init/usePage.ts | 25 +- .../stock/instock/init/useSearch.ts | 5 +- .../stock/instock/init/useTable.ts | 35 +- .../device-center/stock/instock/instock.vue | 42 +- .../stock/instock/types/index.ts | 95 +-- .../stock/inventory/api/index.ts | 6 +- .../inventory/component/drawer/info/info.vue | 10 +- .../component/drawer/info/init/usePage.ts | 17 +- .../component/modal/addOrEdit/addOrEdit.vue | 51 +- .../component/modal/addOrEdit/api/index.ts | 9 - .../component/modal/addOrEdit/init/usePage.ts | 78 ++- .../component/modal/addOrEdit/types/index.ts | 21 +- .../stock/inventory/init/usePage.ts | 2 +- .../stock/inventory/init/useSearch.ts | 2 +- .../stock/inventory/init/useTable.ts | 2 +- .../stock/inventory/inventory.vue | 12 +- .../stock/inventory/types/index.ts | 50 +- .../device-center/stock/outbound/api/index.ts | 10 +- .../component/drawer/info/api/index.ts | 10 + .../outbound/component/drawer/info/info.vue | 74 ++ .../component/drawer/info/init/usePage.ts | 53 ++ .../component/drawer/info/types/index.ts | 19 + .../component/modal/addOrEdit/addOrEdit.vue | 93 ++- .../component/modal/addOrEdit/api/index.ts | 6 +- .../component/modal/addOrEdit/init/usePage.ts | 40 +- .../component/modal/addOrEdit/types/index.ts | 23 +- .../stock/outbound/init/usePage.ts | 74 +- .../stock/outbound/init/useSearch.ts | 3 + .../stock/outbound/init/useTable.ts | 11 +- .../device-center/stock/outbound/outbound.vue | 69 +- .../stock/outbound/types/index.ts | 60 +- .../drawer/areaStockSummary/api/index.ts | 15 + .../areaStockSummary/areaStockSummary.vue | 46 ++ .../drawer/areaStockSummary/init/usePage.ts | 84 +++ .../drawer/areaStockSummary/types/index.ts | 20 + .../component/modal/addOrEdit/addOrEdit.vue | 20 +- .../component/modal/addOrEdit/init/usePage.ts | 21 +- .../component/modal/addOrEdit/types/index.ts | 5 +- .../stock/warehouse/init/usePage.ts | 20 + .../stock/warehouse/types/index.ts | 20 +- .../stock/warehouse/warehouse.vue | 13 + .../device-center/third-auth/amap/amap.vue | 15 +- .../amap/component/drawer/info/info.vue | 5 +- .../amap/component/drawer/info/types/index.ts | 6 +- .../component/modal/addOrEdit/addOrEdit.vue | 18 +- .../component/modal/addOrEdit/init/usePage.ts | 1 + .../component/modal/addOrEdit/types/index.ts | 6 +- .../third-auth/amap/init/useSearch.ts | 3 +- .../third-auth/amap/init/useTable.ts | 9 +- .../third-auth/amap/types/index.ts | 14 +- .../third-auth/arcsoft/api/index.ts | 11 + .../third-auth/arcsoft/arcsoft.vue | 83 ++- .../arcsoft/component/drawer/info/info.vue | 53 +- .../component/drawer/info/types/index.ts | 16 +- .../component/modal/addOrEdit/addOrEdit.vue | 140 ++-- .../component/modal/addOrEdit/init/usePage.ts | 74 +- .../component/modal/addOrEdit/types/index.ts | 20 +- .../modal/importArcsoft/importArcsoft.vue | 152 +++++ .../third-auth/arcsoft/init/usePage.ts | 37 +- .../third-auth/arcsoft/init/useSearch.ts | 5 +- .../third-auth/arcsoft/init/useTable.ts | 31 +- .../third-auth/arcsoft/types/index.ts | 31 +- .../device-center/third-auth/sim/api/index.ts | 46 ++ .../component/drawer/bindRecord/api/index.ts | 11 + .../drawer/bindRecord/bindRecord.vue | 49 ++ .../drawer/bindRecord/init/usePage.ts | 77 +++ .../drawer/bindRecord/types/index.ts | 45 ++ .../component/modal/addOrEdit/addOrEdit.vue | 143 ++++ .../component/modal/addOrEdit/api/index.ts | 20 + .../component/modal/addOrEdit/init/usePage.ts | 154 +++++ .../component/modal/addOrEdit/types/index.ts | 47 ++ .../component/modal/bindDevice/api/index.ts | 11 + .../modal/bindDevice/bindDevice.vue} | 48 +- .../modal/bindDevice/init/usePage.ts | 94 +++ .../modal/bindDevice}/types/index.ts | 28 +- .../component/modal/importSim/importSim.vue | 152 +++++ .../third-auth/sim/init/usePage.ts | 242 +++++++ .../third-auth/sim/init/useSearch.ts | 8 + .../third-auth/sim/init/useTable.ts | 35 + .../device-center/third-auth/sim/sim.vue | 115 ++++ .../third-auth/sim/types/index.ts | 91 +++ src/pages/miniprogram/ops/workOrder/Home.vue | 27 +- .../miniprogram/ops/workbench/alarmList.vue | 7 + .../ops/workbench/stockInbound.vue | 18 +- .../ops/workbench/stockInventory.vue | 18 +- .../miniprogram/ops/workbench/stockList.vue | 7 + .../ops/workbench/stockOutbound.vue | 18 +- 170 files changed, 6983 insertions(+), 2383 deletions(-) create mode 100644 src/axios/handlers/device-center/export-task.ts create mode 100644 src/axios/handlers/device-center/third-auth/sim.ts create mode 100644 src/components/DeviceCategoryPicker.vue create mode 100644 src/components/ExportTaskDrawer.vue create mode 100644 src/components/WarehouseAreaPicker.vue create mode 100644 src/pages/admin-portal/device-center/customer/archive/component/modal/siteBrief/api/index.ts create mode 100644 src/pages/admin-portal/device-center/customer/archive/component/modal/siteBrief/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/customer/archive/component/modal/siteBrief/siteBrief.vue create mode 100644 src/pages/admin-portal/device-center/customer/archive/component/modal/siteBrief/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/info.vue create mode 100644 src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/types/index.ts create mode 100644 src/pages/admin-portal/device-center/stock/warehouse/component/drawer/areaStockSummary/api/index.ts create mode 100644 src/pages/admin-portal/device-center/stock/warehouse/component/drawer/areaStockSummary/areaStockSummary.vue create mode 100644 src/pages/admin-portal/device-center/stock/warehouse/component/drawer/areaStockSummary/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/stock/warehouse/component/drawer/areaStockSummary/types/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/arcsoft/component/modal/importArcsoft/importArcsoft.vue create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/api/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/api/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/bindRecord.vue create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/types/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/addOrEdit.vue create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/api/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/types/index.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/api/index.ts rename src/pages/admin-portal/device-center/{stock/instock/component/modal/returnGoods/returnGoods.vue => third-auth/sim/component/modal/bindDevice/bindDevice.vue} (51%) create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/init/usePage.ts rename src/pages/admin-portal/device-center/{stock/instock/component/modal/returnGoods => third-auth/sim/component/modal/bindDevice}/types/index.ts (61%) create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/component/modal/importSim/importSim.vue create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/init/useSearch.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/init/useTable.ts create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/sim.vue create mode 100644 src/pages/admin-portal/device-center/third-auth/sim/types/index.ts diff --git a/src/api/index.ts b/src/api/index.ts index 83ef8de..8177ddd 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -34,3 +34,33 @@ export const departLevelTree = (params?: RequestParameter): Promise> => postRequest('axiosRequest', '/sys/depart/listLevel', params) + +/** + * 设备分类树节点(最小子集类型,仅包含下拉/树形展示所需字段) + * + * 与 asset/category 模块的 CategoryNode 同源,但此处只暴露公共消费方需要的字段, + * 避免其他模块跨应用 import 设备分类模块的完整类型。 + */ +export interface DeviceCategoryTreeNode { + /** 节点 ID */ + id: string + /** 节点名称 */ + name: string + /** 节点编码(D01 / D01A01 / ...) */ + code: string + /** 层级:1=顶级分类,2=二级分类,依此类推 */ + level: number + /** 父节点 ID(顶级为空) */ + parentId?: string + /** 子节点 */ + children?: DeviceCategoryTreeNode[] +} + +/** + * 获取设备分类全树(供其他模块作为"设备类别"下拉数据源使用) + * + * 数据源:/admin/device/category/tree(与设备分类菜单同接口) + * 调用方:虹软授权等需要按设备类别筛选/选择的页面 + */ +export const deviceCategoryTree = (params?: RequestParameter): Promise> => + postRequest('axiosRequest', '/admin/device/category/tree', params) diff --git a/src/axios/handlers/device-center/asset/category.ts b/src/axios/handlers/device-center/asset/category.ts index 9f1ca6c..97fe455 100644 --- a/src/axios/handlers/device-center/asset/category.ts +++ b/src/axios/handlers/device-center/asset/category.ts @@ -18,6 +18,10 @@ interface CategoryNode { dataTransit?: boolean /** 是否启用数据上报(仅 level >= 2 节点配置) */ dataReport?: boolean + /** 是否支持人脸识别(仅 level >= 2 节点配置;如食堂人脸支付、门禁识别等场景) */ + faceRecognition?: boolean + /** 配网方式(仅 level >= 2 节点配置;可选,标记该分类下设备的主流入网方式) */ + networkMode?: 'wifi' | 'bluetooth' | 'sim' | 'ethernet' | 'other_iot' /** 厂商(字典 device_manufacturer 的 value;仅叶子节点配置) */ manufacturer?: number /** 型号(仅叶子节点配置) */ @@ -40,6 +44,8 @@ interface CategoryPayload { isIot?: boolean dataTransit?: boolean dataReport?: boolean + faceRecognition?: boolean + networkMode?: 'wifi' | 'bluetooth' | 'sim' | 'ethernet' | 'other_iot' /** 叶子节点专属 */ manufacturer?: number model?: string @@ -90,50 +96,50 @@ const categoryTree: CategoryNode[] = [ id: TOP.catering, name: '智养餐饮设备', code: 'D01', level: 1, sortOrder: 1, remark: '食堂类设备,业务数据不过平台', createTime: '2026-05-10 09:20:00', children: [ - { id: 'cat-catering-plate', name: '吐盘机', code: 'D01A01', level: 2, parentId: TOP.catering, sortOrder: 1, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.shanghe, model: 'SH-TP-200', createTime: '2026-05-11 10:00:00' }, - { id: 'cat-catering-cabinet', name: '智能货柜', code: 'D01A02', level: 2, parentId: TOP.catering, sortOrder: 2, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.huawei, model: 'HW-CAB-100L', createTime: '2026-05-11 10:05:00' }, - { id: 'cat-catering-nutri-scale', name: '营养秤', code: 'D01A03', level: 2, parentId: TOP.catering, sortOrder: 3, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.yuyue, model: 'YY-NS-5KG', createTime: '2026-05-11 10:10:00' }, - { id: 'cat-catering-ratio-scale', name: '配比秤', code: 'D01A04', level: 2, parentId: TOP.catering, sortOrder: 4, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.yuyue, model: 'YY-RS-15KG', createTime: '2026-05-11 10:15:00' }, - { id: 'cat-catering-checkout', name: '结算台', code: 'D01A05', level: 2, parentId: TOP.catering, sortOrder: 5, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.huawei, model: 'HW-CK-21IN', createTime: '2026-05-11 10:20:00' }, - { id: 'cat-catering-inbound-scale', name: '入库秤', code: 'D01A06', level: 2, parentId: TOP.catering, sortOrder: 6, isIot: true, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.yuyue, model: 'YY-IS-30KG', createTime: '2026-05-11 10:25:00' }, - { id: 'cat-catering-band-screen', name: '带鱼屏', code: 'D01A07', level: 2, parentId: TOP.catering, sortOrder: 7, isIot: false, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.shanghe, model: 'SH-BS-25IN', createTime: '2026-05-11 10:30:00' }, - { id: 'cat-catering-baifei', name: '波菲炉', code: 'D01A08', level: 2, parentId: TOP.catering, sortOrder: 8, isIot: false, dataTransit: false, dataReport: false, manufacturer: MANUFACTURER.shanghe, model: 'SH-BF-4GN', createTime: '2026-05-11 10:35:00' }, + { id: 'cat-catering-plate', name: '吐盘机', code: 'D01A01', level: 2, parentId: TOP.catering, sortOrder: 1, isIot: true, dataTransit: false, dataReport: false, faceRecognition: false, networkMode: 'wifi', manufacturer: MANUFACTURER.shanghe, model: 'SH-TP-200', createTime: '2026-05-11 10:00:00' }, + { id: 'cat-catering-cabinet', name: '智能货柜', code: 'D01A02', level: 2, parentId: TOP.catering, sortOrder: 2, isIot: true, dataTransit: false, dataReport: false, faceRecognition: true, networkMode: 'wifi', manufacturer: MANUFACTURER.huawei, model: 'HW-CAB-100L', createTime: '2026-05-11 10:05:00' }, + { id: 'cat-catering-nutri-scale', name: '营养秤', code: 'D01A03', level: 2, parentId: TOP.catering, sortOrder: 3, isIot: true, dataTransit: false, dataReport: false, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.yuyue, model: 'YY-NS-5KG', createTime: '2026-05-11 10:10:00' }, + { id: 'cat-catering-ratio-scale', name: '配比秤', code: 'D01A04', level: 2, parentId: TOP.catering, sortOrder: 4, isIot: true, dataTransit: false, dataReport: false, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.yuyue, model: 'YY-RS-15KG', createTime: '2026-05-11 10:15:00' }, + { id: 'cat-catering-checkout', name: '结算台', code: 'D01A05', level: 2, parentId: TOP.catering, sortOrder: 5, isIot: true, dataTransit: false, dataReport: false, faceRecognition: true, networkMode: 'ethernet', manufacturer: MANUFACTURER.huawei, model: 'HW-CK-21IN', createTime: '2026-05-11 10:20:00' }, + { id: 'cat-catering-inbound-scale', name: '入库秤', code: 'D01A06', level: 2, parentId: TOP.catering, sortOrder: 6, isIot: true, dataTransit: false, dataReport: false, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.yuyue, model: 'YY-IS-30KG', createTime: '2026-05-11 10:25:00' }, + { id: 'cat-catering-band-screen', name: '带鱼屏', code: 'D01A07', level: 2, parentId: TOP.catering, sortOrder: 7, isIot: false, dataTransit: false, dataReport: false, faceRecognition: true, manufacturer: MANUFACTURER.shanghe, model: 'SH-BS-25IN', createTime: '2026-05-11 10:30:00' }, + { id: 'cat-catering-baifei', name: '波菲炉', code: 'D01A08', level: 2, parentId: TOP.catering, sortOrder: 8, isIot: false, dataTransit: false, dataReport: false, faceRecognition: false, manufacturer: MANUFACTURER.shanghe, model: 'SH-BF-4GN', createTime: '2026-05-11 10:35:00' }, ], }, { id: TOP.body, name: '体重测量设备', code: 'D02', level: 1, sortOrder: 2, remark: '业务数据过平台中转', createTime: '2026-05-10 09:25:00', children: [ - { id: 'cat-body-all-in-one', name: '一体机', code: 'D02A01', level: 2, parentId: TOP.body, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.luojiefu, model: 'LJF-AIO-V3', createTime: '2026-05-11 11:00:00' }, - { id: 'cat-body-bt-scale', name: '蓝牙秤', code: 'D02A02', level: 2, parentId: TOP.body, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yuyue, model: 'YY-BT-180KG', createTime: '2026-05-11 11:05:00' }, - { id: 'cat-body-fat', name: '体脂仪', code: 'D02A03', level: 2, parentId: TOP.body, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.huawei, model: 'HW-FAT-BH', createTime: '2026-05-11 11:10:00' }, + { id: 'cat-body-all-in-one', name: '一体机', code: 'D02A01', level: 2, parentId: TOP.body, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, faceRecognition: true, networkMode: 'wifi', manufacturer: MANUFACTURER.luojiefu, model: 'LJF-AIO-V3', createTime: '2026-05-11 11:00:00' }, + { id: 'cat-body-bt-scale', name: '蓝牙秤', code: 'D02A02', level: 2, parentId: TOP.body, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.yuyue, model: 'YY-BT-180KG', createTime: '2026-05-11 11:05:00' }, + { id: 'cat-body-fat', name: '体脂仪', code: 'D02A03', level: 2, parentId: TOP.body, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.huawei, model: 'HW-FAT-BH', createTime: '2026-05-11 11:10:00' }, ], }, { id: TOP.wearable, name: '智能穿戴设备', code: 'D03', level: 1, sortOrder: 3, remark: '业务数据过平台中转', createTime: '2026-05-10 09:30:00', children: [ - { id: 'cat-wearable-hw5', name: '华为-watch5', code: 'D03A01', level: 2, parentId: TOP.wearable, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.huawei, model: 'Watch-5', createTime: '2026-05-11 11:30:00' }, - { id: 'cat-wearable-hw4', name: '华为-watch4', code: 'D03A02', level: 2, parentId: TOP.wearable, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.huawei, model: 'Watch-4', createTime: '2026-05-11 11:35:00' }, - { id: 'cat-wearable-ljf01', name: '罗杰夫-LJF01', code: 'D03A03', level: 2, parentId: TOP.wearable, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.luojiefu, model: 'LJF-01', createTime: '2026-05-11 11:40:00' }, + { id: 'cat-wearable-hw5', name: '华为-watch5', code: 'D03A01', level: 2, parentId: TOP.wearable, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.huawei, model: 'Watch-5', createTime: '2026-05-11 11:30:00' }, + { id: 'cat-wearable-hw4', name: '华为-watch4', code: 'D03A02', level: 2, parentId: TOP.wearable, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.huawei, model: 'Watch-4', createTime: '2026-05-11 11:35:00' }, + { id: 'cat-wearable-ljf01', name: '罗杰夫-LJF01', code: 'D03A03', level: 2, parentId: TOP.wearable, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.luojiefu, model: 'LJF-01', createTime: '2026-05-11 11:40:00' }, ], }, { id: TOP.fitness, name: '健身器材设备', code: 'D04', level: 1, sortOrder: 4, remark: '业务数据过平台中转', createTime: '2026-05-10 09:35:00', children: [ - { id: 'cat-fitness-treadmill', name: '跑步机', code: 'D04A01', level: 2, parentId: TOP.fitness, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yixiong, model: 'YX-TM-Pro', createTime: '2026-05-11 12:00:00' }, - { id: 'cat-fitness-elliptical', name: '椭圆机', code: 'D04A02', level: 2, parentId: TOP.fitness, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yixiong, model: 'YX-EL-X1', createTime: '2026-05-11 12:05:00' }, - { id: 'cat-fitness-bike', name: '健身车', code: 'D04A03', level: 2, parentId: TOP.fitness, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yixiong, model: 'YX-BK-M3', createTime: '2026-05-11 12:10:00' }, + { id: 'cat-fitness-treadmill', name: '跑步机', code: 'D04A01', level: 2, parentId: TOP.fitness, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'wifi', manufacturer: MANUFACTURER.yixiong, model: 'YX-TM-Pro', createTime: '2026-05-11 12:00:00' }, + { id: 'cat-fitness-elliptical', name: '椭圆机', code: 'D04A02', level: 2, parentId: TOP.fitness, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'wifi', manufacturer: MANUFACTURER.yixiong, model: 'YX-EL-X1', createTime: '2026-05-11 12:05:00' }, + { id: 'cat-fitness-bike', name: '健身车', code: 'D04A03', level: 2, parentId: TOP.fitness, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'wifi', manufacturer: MANUFACTURER.yixiong, model: 'YX-BK-M3', createTime: '2026-05-11 12:10:00' }, ], }, { id: TOP.monitor, name: '健康监测设备', code: 'D05', level: 1, sortOrder: 5, remark: '业务数据过平台中转', createTime: '2026-05-10 09:40:00', children: [ - { id: 'cat-monitor-sugar', name: '血糖监测仪', code: 'D05A01', level: 2, parentId: TOP.monitor, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yuyue, model: 'YY-BG-530', createTime: '2026-05-11 12:30:00' }, - { id: 'cat-monitor-pressure', name: '高血压治疗仪', code: 'D05A02', level: 2, parentId: TOP.monitor, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.yuyue, model: 'YY-BP-680', createTime: '2026-05-11 12:35:00' }, - { id: 'cat-monitor-all-in-one', name: '智能健康一体机', code: 'D05A03', level: 2, parentId: TOP.monitor, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, manufacturer: MANUFACTURER.luojiefu, model: 'LJF-HM-Z1', createTime: '2026-05-11 12:40:00' }, + { id: 'cat-monitor-sugar', name: '血糖监测仪', code: 'D05A01', level: 2, parentId: TOP.monitor, sortOrder: 1, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'sim', manufacturer: MANUFACTURER.yuyue, model: 'YY-BG-530', createTime: '2026-05-11 12:30:00' }, + { id: 'cat-monitor-pressure', name: '高血压治疗仪', code: 'D05A02', level: 2, parentId: TOP.monitor, sortOrder: 2, isIot: true, dataTransit: true, dataReport: true, faceRecognition: false, networkMode: 'bluetooth', manufacturer: MANUFACTURER.yuyue, model: 'YY-BP-680', createTime: '2026-05-11 12:35:00' }, + { id: 'cat-monitor-all-in-one', name: '智能健康一体机', code: 'D05A03', level: 2, parentId: TOP.monitor, sortOrder: 3, isIot: true, dataTransit: true, dataReport: true, faceRecognition: true, networkMode: 'wifi', manufacturer: MANUFACTURER.luojiefu, model: 'LJF-HM-Z1', createTime: '2026-05-11 12:40:00' }, ], }, ] @@ -235,6 +241,11 @@ const handleAdd = (ctx: MockContext): ApiResponse => { newNode.isIot = payload.isIot ?? false newNode.dataTransit = payload.dataTransit ?? false newNode.dataReport = payload.dataReport ?? false + newNode.faceRecognition = payload.faceRecognition ?? false + /** 仅 IOT 设备携带配网方式 */ + if (newNode.isIot) { + newNode.networkMode = payload.networkMode + } /** 叶子节点(新建时必然无子)携带厂商/型号 */ newNode.manufacturer = payload.manufacturer newNode.model = payload.model @@ -264,6 +275,13 @@ const handleUpdate = (ctx: MockContext): ApiResponse => { node.isIot = payload.isIot ?? false node.dataTransit = payload.dataTransit ?? false node.dataReport = payload.dataReport ?? false + node.faceRecognition = payload.faceRecognition ?? false + /** 仅 IOT 设备更新配网方式;非 IOT 时清空历史残留值 */ + if (node.isIot) { + node.networkMode = payload.networkMode + } else { + delete node.networkMode + } /** 叶子节点(无子)才更新厂商/型号 */ if (!node.children?.length) { node.manufacturer = payload.manufacturer diff --git a/src/axios/handlers/device-center/asset/inbound.ts b/src/axios/handlers/device-center/asset/inbound.ts index 5fe5058..8d7ed1a 100644 --- a/src/axios/handlers/device-center/asset/inbound.ts +++ b/src/axios/handlers/device-center/asset/inbound.ts @@ -1,5 +1,6 @@ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext, RequestParameter } from '@axios' +import { pushDevicesFromInbound } from './manage' interface PageParams { pageNum?: number @@ -10,16 +11,22 @@ interface PageParams { status?: string inboundType?: string supplierId?: string + categoryId?: string + warehouseAreaId?: string [key: string]: unknown } +interface InboundDetailItem { + deviceCode: string + serialNumber: string +} + interface InboundRecord { id: string inboundCode: string - deviceCode: string - deviceName: string - modelName: string - batchNo: string + categoryId: string + categoryName: string + batchNo?: string supplierId: string supplierName: string quantity: number @@ -27,9 +34,12 @@ interface InboundRecord { inboundTime: string operator: string inboundType: 'purchase' | 'return' | 'transfer' - status: 'pending' | 'completed' | 'cancelled' - storageLocation?: string + status: 'pending' | 'completed' | 'audited' | 'cancelled' + warehouseAreaId?: string + warehouseAreaName?: string remark?: string + /** 设备明细(已入库待审核时录入) */ + items?: InboundDetailItem[] } const getParams = (params: RequestParameter): PageParams => (params ?? {}) as PageParams @@ -63,15 +73,68 @@ const nowStr = (): string => { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` } +/** + * 设备类别末级 ID → 名称映射(与 statistics mock / category mock 同源) + * 用于 add/update 时按 categoryId 反查 categoryName(模拟后端 join 分类表) + */ +const CATEGORY_LEAF_NAME: Record = { + 'cat-catering-plate': '吐盘机', + 'cat-catering-cabinet': '智能货柜', + 'cat-catering-nutri-scale': '营养秤', + 'cat-catering-ratio-scale': '配比秤', + 'cat-catering-checkout': '结算台', + 'cat-catering-inbound-scale': '入库秤', + 'cat-catering-band-screen': '带鱼屏', + 'cat-catering-baifei': '波菲炉', + 'cat-body-all-in-one': '一体机', + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-fitness-bike': '健身车', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +/** + * 仓库区域末级节点 ID → 祖先链名称映射(与 warehouse mock 同源) + * 用于 add/update 时按 warehouseAreaId 反查 warehouseAreaName + */ +const WAREHOUSE_AREA_NAME: Record = { + 'wh-floor1-large-a-1': '1楼大型A号货架-1号储位', + 'wh-floor1-large-a-2': '1楼大型A号货架-2号储位', + 'wh-floor1-large-b-1': '1楼大型B号货架-1号储位', + 'wh-floor1-small-shelf-1': '1楼小型备品货架-1号储位', + 'wh-floor1-small-shelf-2': '1楼小型备品货架-2号储位', + 'wh-floor2-check-1': '2楼待检区-临时储位', +} + +/** + * 入库单 mock 数据集 + * - 删除 deviceCode/deviceName/modelName/storageLocation 字段 + * - 用 categoryId+categoryName 表达设备品类(末级分类) + * - 用 warehouseAreaId+warehouseAreaName 表达仓库区域(末级节点) + * - 入库类型含 return(回库),不再单独维护回库入口 + */ const inboundList: InboundRecord[] = [ - { id: 'inb-001', inboundCode: 'INB-20260601-001', deviceCode: 'CAB-A1-0003', deviceName: '智能货柜A1', modelName: '智能货柜', batchNo: 'B2026-001', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 5, purchasePrice: 4500, inboundTime: '2026-06-01 09:30:00', operator: '张三', inboundType: 'purchase', status: 'completed', storageLocation: 'A区-01', remark: '首次采购' }, - { id: 'inb-002', inboundCode: 'INB-20260605-002', deviceCode: 'WAT-W1-0002', deviceName: '鹏升手表W1', modelName: '华为-watch5', batchNo: 'B2026-002', supplierId: 'sup-002', supplierName: '鹏升电子', quantity: 10, purchasePrice: 880, inboundTime: '2026-06-05 14:20:00', operator: '李四', inboundType: 'purchase', status: 'completed', storageLocation: 'B区-02' }, - { id: 'inb-003', inboundCode: 'INB-20260610-003', deviceCode: 'CHK-M1-0002', deviceName: '华盛体检机', modelName: '智能健康一体机', batchNo: 'B2026-003', supplierId: 'sup-003', supplierName: '华盛医疗', quantity: 2, purchasePrice: 28000, inboundTime: '2026-06-10 11:00:00', operator: '王五', inboundType: 'purchase', status: 'pending', storageLocation: 'C区-03', remark: '等待验收' }, - { id: 'inb-004', inboundCode: 'INB-20260615-004', deviceCode: 'SCL-X1-0002', deviceName: '入库秤X1', modelName: '入库秤', batchNo: 'B2026-004', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 3, purchasePrice: 1200, inboundTime: '2026-06-15 16:45:00', operator: '赵六', inboundType: 'transfer', status: 'completed', storageLocation: 'A区-02', remark: '从杭州仓调拨' }, - { id: 'inb-005', inboundCode: 'INB-20260618-005', deviceCode: 'FAT-INBODY-0002', deviceName: 'InBody体脂秤', modelName: '体脂仪', batchNo: 'B2026-005', supplierId: 'sup-004', supplierName: 'InBody中国', quantity: 1, purchasePrice: 6500, inboundTime: '2026-06-18 10:15:00', operator: '孙七', inboundType: 'return', status: 'cancelled', storageLocation: 'D区-01', remark: '质量问题已退回' }, - { id: 'inb-006', inboundCode: 'INB-20260620-006', deviceCode: 'FIT-S1-0002', deviceName: '顿米健身车', modelName: '健身车', batchNo: 'B2026-006', supplierId: 'sup-005', supplierName: '顿米科技', quantity: 4, purchasePrice: 3200, inboundTime: '2026-06-20 09:00:00', operator: '张三', inboundType: 'purchase', status: 'completed', storageLocation: 'E区-01' }, - { id: 'inb-007', inboundCode: 'INB-20260622-007', deviceCode: 'SCR-P1-0002', deviceName: '大屏P1', modelName: '带鱼屏', batchNo: 'B2026-007', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 2, purchasePrice: 5800, inboundTime: '2026-06-22 14:30:00', operator: '李四', inboundType: 'purchase', status: 'pending', storageLocation: 'A区-03' }, - { id: 'inb-008', inboundCode: 'INB-20260625-008', deviceCode: 'SCL-W-0002', deviceName: '体重秤', modelName: '蓝牙秤', batchNo: 'B2026-008', supplierId: 'sup-002', supplierName: '鹏升电子', quantity: 6, purchasePrice: 480, inboundTime: '2026-06-25 11:45:00', operator: '王五', inboundType: 'purchase', status: 'completed', storageLocation: 'B区-03' }, + { id: 'inb-001', inboundCode: 'INB-20260601-001', categoryId: 'cat-catering-cabinet', categoryName: '智能货柜', batchNo: 'B2026-001', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 5, purchasePrice: 4500, inboundTime: '2026-06-01 09:30:00', operator: '张三', inboundType: 'purchase', status: 'completed', warehouseAreaId: 'wh-floor1-large-a-2', warehouseAreaName: '1楼大型A号货架-2号储位', remark: '首次采购', items: [ + { deviceCode: 'CAB-2026-001', serialNumber: 'SN-CAB-001' }, + { deviceCode: 'CAB-2026-002', serialNumber: 'SN-CAB-002' }, + { deviceCode: 'CAB-2026-003', serialNumber: 'SN-CAB-003' }, + { deviceCode: 'CAB-2026-004', serialNumber: 'SN-CAB-004' }, + { deviceCode: 'CAB-2026-005', serialNumber: 'SN-CAB-005' }, + ] }, + { id: 'inb-002', inboundCode: 'INB-20260605-002', categoryId: 'cat-wearable-hw5', categoryName: '华为-watch5', batchNo: 'B2026-002', supplierId: 'sup-002', supplierName: '鹏升电子', quantity: 10, purchasePrice: 880, inboundTime: '2026-06-05 14:20:00', operator: '李四', inboundType: 'purchase', status: 'completed', warehouseAreaId: 'wh-floor1-small-shelf-1', warehouseAreaName: '1楼小型备品货架-1号储位' }, + { id: 'inb-003', inboundCode: 'INB-20260610-003', categoryId: 'cat-monitor-all-in-one', categoryName: '智能健康一体机', batchNo: 'B2026-003', supplierId: 'sup-003', supplierName: '华盛医疗', quantity: 2, purchasePrice: 28000, inboundTime: '2026-06-10 11:00:00', operator: '王五', inboundType: 'purchase', status: 'pending', warehouseAreaId: 'wh-floor1-large-b-1', warehouseAreaName: '1楼大型B号货架-1号储位', remark: '等待验收' }, + { id: 'inb-004', inboundCode: 'INB-20260615-004', categoryId: 'cat-catering-inbound-scale', categoryName: '入库秤', batchNo: 'B2026-004', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 3, purchasePrice: 1200, inboundTime: '2026-06-15 16:45:00', operator: '赵六', inboundType: 'transfer', status: 'completed', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位', remark: '从杭州仓调拨' }, + { id: 'inb-005', inboundCode: 'INB-20260618-005', categoryId: 'cat-body-fat', categoryName: '体脂仪', batchNo: 'B2026-005', supplierId: 'sup-004', supplierName: 'InBody中国', quantity: 1, purchasePrice: 6500, inboundTime: '2026-06-18 10:15:00', operator: '孙七', inboundType: 'return', status: 'cancelled', warehouseAreaId: 'wh-floor2-check-1', warehouseAreaName: '2楼待检区-临时储位', remark: '客户退回,待检测' }, + { id: 'inb-006', inboundCode: 'INB-20260620-006', categoryId: 'cat-fitness-bike', categoryName: '健身车', batchNo: 'B2026-006', supplierId: 'sup-005', supplierName: '顿米科技', quantity: 4, purchasePrice: 3200, inboundTime: '2026-06-20 09:00:00', operator: '张三', inboundType: 'purchase', status: 'completed', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位' }, + { id: 'inb-007', inboundCode: 'INB-20260622-007', categoryId: 'cat-catering-band-screen', categoryName: '带鱼屏', batchNo: 'B2026-007', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', quantity: 2, purchasePrice: 5800, inboundTime: '2026-06-22 14:30:00', operator: '李四', inboundType: 'purchase', status: 'pending', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位' }, + { id: 'inb-008', inboundCode: 'INB-20260625-008', categoryId: 'cat-body-bt-scale', categoryName: '蓝牙秤', batchNo: 'B2026-008', supplierId: 'sup-002', supplierName: '鹏升电子', quantity: 6, purchasePrice: 480, inboundTime: '2026-06-25 11:45:00', operator: '王五', inboundType: 'purchase', status: 'completed', warehouseAreaId: 'wh-floor1-small-shelf-2', warehouseAreaName: '1楼小型备品货架-2号储位' }, ] const listPage = (ctx: MockContext): ApiResponse => { @@ -80,7 +143,9 @@ const listPage = (ctx: MockContext): ApiResponse => { if (params.status && item.status !== params.status) return false if (params.inboundType && item.inboundType !== params.inboundType) return false if (params.supplierId && item.supplierId !== params.supplierId) return false - if (params.keyword && !includes(item.inboundCode, params.keyword) && !includes(item.deviceCode, params.keyword) && !includes(item.deviceName, params.keyword)) return false + if (params.categoryId && item.categoryId !== params.categoryId) return false + if (params.warehouseAreaId && item.warehouseAreaId !== params.warehouseAreaId) return false + if (params.keyword && !includes(item.inboundCode, params.keyword)) return false return true }) if (params.column === 'inboundTime' && params.order) { @@ -114,25 +179,56 @@ const genInboundCode = (): string => { return `INB-${ymd}-${seq}` } +/** 按 categoryId 反查末级分类名(模拟后端 join) */ +const resolveCategoryName = (categoryId?: string): string => { + if (!categoryId) return '' + return CATEGORY_LEAF_NAME[categoryId] ?? '' +} + +/** 按 warehouseAreaId 反查区域祖先链名(模拟后端 join) */ +const resolveWarehouseAreaName = (warehouseAreaId?: string): string => { + if (!warehouseAreaId) return '' + return WAREHOUSE_AREA_NAME[warehouseAreaId] ?? '' +} + +/** 解析 items 数组参数 */ +const parseItems = (raw: unknown): InboundDetailItem[] => { + if (!Array.isArray(raw)) return [] + return raw + .map((entry) => { + if (!entry || typeof entry !== 'object') return null + const obj = entry as { deviceCode?: unknown; serialNumber?: unknown } + return { + deviceCode: String(obj.deviceCode ?? '').trim(), + serialNumber: String(obj.serialNumber ?? '').trim(), + } + }) + .filter((item): item is InboundDetailItem => item !== null && item.deviceCode.length > 0) +} + /** 新增 */ const addRecord = (params: PageParams): ApiResponse => { + const categoryId = String(params.categoryId ?? '') + const warehouseAreaId = params.warehouseAreaId ? String(params.warehouseAreaId) : undefined + const items = parseItems(params.items) const record: InboundRecord = { id: `inb-${Date.now().toString(36)}`, inboundCode: String(params.inboundCode ?? genInboundCode()), - deviceCode: String(params.deviceCode ?? ''), - deviceName: String(params.deviceName ?? ''), - modelName: String(params.modelName ?? ''), - batchNo: String(params.batchNo ?? ''), + categoryId, + categoryName: resolveCategoryName(categoryId), + batchNo: params.batchNo ? String(params.batchNo) : undefined, supplierId: String(params.supplierId ?? ''), supplierName: String(params.supplierName ?? ''), - quantity: Number(params.quantity ?? 1), + quantity: items.length || Number(params.quantity ?? 1), purchasePrice: Number(params.purchasePrice ?? 0), inboundTime: nowStr(), operator: '当前用户', inboundType: (params.inboundType as InboundRecord['inboundType']) ?? 'purchase', status: 'completed', - storageLocation: params.storageLocation ? String(params.storageLocation) : undefined, + warehouseAreaId, + warehouseAreaName: resolveWarehouseAreaName(warehouseAreaId), remark: params.remark ? String(params.remark) : undefined, + items, } inboundList.unshift(record) return ok('新增成功') @@ -143,25 +239,55 @@ const updateRecord = (params: PageParams): ApiResponse => { const id = String(params.id ?? '') const record = inboundList.find((item) => item.id === id) if (!record) return ok('入库单不存在') - if (params.deviceCode !== undefined) record.deviceCode = String(params.deviceCode) - if (params.deviceName !== undefined) record.deviceName = String(params.deviceName) - if (params.modelName !== undefined) record.modelName = String(params.modelName) - if (params.batchNo !== undefined) record.batchNo = String(params.batchNo) + if (params.categoryId !== undefined) { + record.categoryId = String(params.categoryId) + record.categoryName = resolveCategoryName(record.categoryId) + } + if (params.batchNo !== undefined) record.batchNo = params.batchNo ? String(params.batchNo) : undefined if (params.supplierId !== undefined) record.supplierId = String(params.supplierId) if (params.supplierName !== undefined) record.supplierName = String(params.supplierName) if (params.quantity !== undefined) record.quantity = Number(params.quantity) if (params.purchasePrice !== undefined) record.purchasePrice = Number(params.purchasePrice) if (params.inboundType !== undefined) record.inboundType = params.inboundType as InboundRecord['inboundType'] - if (params.storageLocation !== undefined) record.storageLocation = String(params.storageLocation) + if (params.warehouseAreaId !== undefined) { + record.warehouseAreaId = params.warehouseAreaId ? String(params.warehouseAreaId) : undefined + record.warehouseAreaName = resolveWarehouseAreaName(record.warehouseAreaId) + } if (params.remark !== undefined) record.remark = params.remark ? String(params.remark) : undefined + if (params.items !== undefined) { + record.items = parseItems(params.items) + record.quantity = record.items.length || record.quantity + } return ok('更新成功') } +/** + * 审核:将入库单 status 从 completed → audited,并按 items 跨 handler 写设备台账 + * - 仅 completed 状态可审核 + * - 调用 manage mock 的 pushDevicesFromInbound 生成 N 条台账 + */ +const auditRecord = (params: PageParams): ApiResponse<{ pushed: number }> => { + const id = String(params.id ?? '') + const record = inboundList.find((item) => item.id === id) + if (!record) return ok('入库单不存在') + if (record.status !== 'completed') return ok('仅已入库状态可审核') + if (!record.items?.length) return ok('设备明细为空,无法审核') + const pushed = pushDevicesFromInbound({ + inboundCode: record.inboundCode, + categoryId: record.categoryId, + categoryName: record.categoryName, + items: record.items, + }) + record.status = 'audited' + return ok(`审核成功,已生成 ${pushed} 条设备台账`, { pushed }) +} + registerMocks([ { method: 'POST', url: '/admin/device/stock/inbound/page', handler: listPage }, { method: 'POST', url: '/admin/device/stock/inbound/info', handler: (ctx) => infoById(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inbound/add', handler: (ctx) => addRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inbound/update', handler: (ctx) => updateRecord(getParams(ctx.params)) }, + { method: 'POST', url: '/admin/device/stock/inbound/audit', handler: (ctx) => auditRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inbound/delete', handler: (ctx) => deleteById(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inbound/export', handler: () => ok('导出任务已提交') }, ]) diff --git a/src/axios/handlers/device-center/asset/instock.ts b/src/axios/handlers/device-center/asset/instock.ts index 72367a5..8bad691 100644 --- a/src/axios/handlers/device-center/asset/instock.ts +++ b/src/axios/handlers/device-center/asset/instock.ts @@ -7,33 +7,28 @@ interface PageParams { column?: string order?: 'ascend' | 'descend' | null keyword?: string - deviceType?: string - stockStatus?: string + categoryId?: string + warehouseAreaId?: string + healthLevel?: string id?: string - returnQty?: number - storageLocation?: string - remark?: string [key: string]: unknown } -type StockStatus = 'instock' | 'inbound' | 'shipped' | 'returned' +type StockHealthLevel = 'sufficient' | 'warning' | 'out_of_stock' interface StockRecord { id: string - modelCode: string - modelName: string - deviceType: string - typeName: string + categoryId: string + categoryName: string batchNo: string + supplierId: string supplierName: string - purchasePrice: number - inboundTime: string - storageLocation: string - stockStatus: StockStatus + warehouseAreaId?: string + warehouseAreaName?: string stockQty: number - inTransitQty: number + lockedQty: number availableQty: number - shippedQty: number + healthLevel: StockHealthLevel updateTime: string } @@ -50,29 +45,62 @@ const paginate = (list: T[], params: PageParams): ApiResp return { code: '00000', msg: '查询成功', data: list.slice(start, start + pageSize), total: list.length } } -const nowStr = (): string => { - const d = new Date() - const pad = (n: number): string => String(n).padStart(2, '0') - return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` +/** 设备类别末级 ID → 名称映射(与 statistics/category mock 同源) */ +const CATEGORY_LEAF_NAME: Record = { + 'cat-catering-plate': '吐盘机', + 'cat-catering-cabinet': '智能货柜', + 'cat-catering-nutri-scale': '营养秤', + 'cat-catering-ratio-scale': '配比秤', + 'cat-catering-checkout': '结算台', + 'cat-catering-inbound-scale': '入库秤', + 'cat-catering-band-screen': '带鱼屏', + 'cat-catering-baifei': '波菲炉', + 'cat-body-all-in-one': '一体机', + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-fitness-bike': '健身车', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', } -const stockList: StockRecord[] = [ - { id: 'stk-001', modelCode: 'CABINET-A1', modelName: '智能货柜A1', deviceType: 'cabinet', typeName: '餐盘柜', batchNo: 'B2026-001', supplierName: '深圳智能设备供应商', purchasePrice: 8500, inboundTime: '2026-06-22 09:30:00', storageLocation: 'A区-01', stockStatus: 'instock', stockQty: 5, inTransitQty: 2, availableQty: 3, shippedQty: 2, updateTime: '2026-06-25 09:30:00' }, - { id: 'stk-002', modelCode: 'SCREEN-P1', modelName: '大屏P1', deviceType: 'screen', typeName: '大屏', batchNo: 'B2026-002', supplierName: '深圳智能设备供应商', purchasePrice: 12000, inboundTime: '2026-06-22 10:00:00', storageLocation: 'A区-02', stockStatus: 'instock', stockQty: 3, inTransitQty: 0, availableQty: 3, shippedQty: 1, updateTime: '2026-06-25 10:00:00' }, - { id: 'stk-003', modelCode: 'WATCH-W1-PEN', modelName: '鹏升手表W1', deviceType: 'watch', typeName: '手表', batchNo: 'B2026-003', supplierName: '鹏升电子', purchasePrice: 980, inboundTime: '2026-06-22 11:15:00', storageLocation: 'B区-01', stockStatus: 'instock', stockQty: 12, inTransitQty: 5, availableQty: 7, shippedQty: 6, updateTime: '2026-06-25 11:15:00' }, - { id: 'stk-004', modelCode: 'SCALE-X1', modelName: '入库秤X1', deviceType: 'scale-inbound', typeName: '入库秤', batchNo: 'B2026-004', supplierName: '深圳智能设备供应商', purchasePrice: 1800, inboundTime: '2026-06-23 13:45:00', storageLocation: 'B区-02', stockStatus: 'instock', stockQty: 2, inTransitQty: 0, availableQty: 2, shippedQty: 1, updateTime: '2026-06-25 13:45:00' }, - { id: 'stk-005', modelCode: 'SCALE-Y2', modelName: '配比秤Y2', deviceType: 'scale-ratio', typeName: '配比秤', batchNo: 'B2026-005', supplierName: '深圳智能设备供应商', purchasePrice: 2200, inboundTime: '2026-06-23 14:30:00', storageLocation: 'B区-03', stockStatus: 'inbound', stockQty: 1, inTransitQty: 1, availableQty: 1, shippedQty: 0, updateTime: '2026-06-25 14:30:00' }, - { id: 'stk-006', modelCode: 'FIT-S1-DUNMI', modelName: '顿米健身车S1', deviceType: 'fitness', typeName: '健身器材', batchNo: 'B2026-006', supplierName: '顿米科技', purchasePrice: 5600, inboundTime: '2026-06-23 15:00:00', storageLocation: 'C区-01', stockStatus: 'instock', stockQty: 4, inTransitQty: 0, availableQty: 4, shippedQty: 2, updateTime: '2026-06-25 15:00:00' }, - { id: 'stk-007', modelCode: 'CHK-M1-HUASHENG', modelName: '华盛体检机M1', deviceType: 'checkup', typeName: '体检机', batchNo: 'B2026-007', supplierName: '华盛医疗', purchasePrice: 32000, inboundTime: '2026-06-24 16:20:00', storageLocation: 'C区-02', stockStatus: 'inbound', stockQty: 0, inTransitQty: 1, availableQty: 0, shippedQty: 0, updateTime: '2026-06-25 16:20:00' }, - { id: 'stk-008', modelCode: 'FAT-INBODY', modelName: 'InBody体脂秤', deviceType: 'scale-fat', typeName: '体脂秤', batchNo: 'B2026-008', supplierName: 'InBody中国', purchasePrice: 6800, inboundTime: '2026-06-24 17:00:00', storageLocation: 'B区-04', stockStatus: 'shipped', stockQty: 0, inTransitQty: 0, availableQty: 0, shippedQty: 2, updateTime: '2026-06-25 17:00:00' }, +/** 仓库区域末级节点 ID → 祖先链名称映射(与 warehouse mock 同源) */ +const WAREHOUSE_AREA_NAME: Record = { + 'wh-floor1-large-a-1': '1楼大型A号货架-1号储位', + 'wh-floor1-large-a-2': '1楼大型A号货架-2号储位', + 'wh-floor1-large-b-1': '1楼大型B号货架-1号储位', + 'wh-floor1-small-shelf-1': '1楼小型备品货架-1号储位', + 'wh-floor1-small-shelf-2': '1楼小型备品货架-2号储位', + 'wh-floor2-check-1': '2楼待检区-临时储位', +} + +/** + * 在库库存数据集(导出供 warehouse 等模块跨 handler 反查状态使用) + * 健康度规则(mock 内自洽):stockQty = 0 → out_of_stock;lockedQty >= stockQty - 1 且 stockQty <= 2 → warning;否则 sufficient + */ +export const stockList: StockRecord[] = [ + { id: 'stk-001', categoryId: 'cat-catering-cabinet', categoryName: '智能货柜', batchNo: 'B2026-001', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位', stockQty: 5, lockedQty: 2, availableQty: 3, healthLevel: 'sufficient', updateTime: '2026-06-25 09:30:00' }, + { id: 'stk-002', categoryId: 'cat-catering-band-screen', categoryName: '带鱼屏', batchNo: 'B2026-002', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', warehouseAreaId: 'wh-floor1-large-a-2', warehouseAreaName: '1楼大型A号货架-2号储位', stockQty: 3, lockedQty: 0, availableQty: 3, healthLevel: 'sufficient', updateTime: '2026-06-25 10:00:00' }, + { id: 'stk-003', categoryId: 'cat-wearable-hw5', categoryName: '华为-watch5', batchNo: 'B2026-003', supplierId: 'sup-002', supplierName: '鹏升电子', warehouseAreaId: 'wh-floor1-small-shelf-1', warehouseAreaName: '1楼小型备品货架-1号储位', stockQty: 12, lockedQty: 5, availableQty: 7, healthLevel: 'sufficient', updateTime: '2026-06-25 11:15:00' }, + { id: 'stk-004', categoryId: 'cat-catering-inbound-scale', categoryName: '入库秤', batchNo: 'B2026-004', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', warehouseAreaId: 'wh-floor1-small-shelf-2', warehouseAreaName: '1楼小型备品货架-2号储位', stockQty: 2, lockedQty: 1, availableQty: 1, healthLevel: 'warning', updateTime: '2026-06-25 13:45:00' }, + { id: 'stk-005', categoryId: 'cat-catering-ratio-scale', categoryName: '配比秤', batchNo: 'B2026-005', supplierId: 'sup-001', supplierName: '深圳智能设备供应商', warehouseAreaId: 'wh-floor1-large-b-1', warehouseAreaName: '1楼大型B号货架-1号储位', stockQty: 1, lockedQty: 0, availableQty: 1, healthLevel: 'warning', updateTime: '2026-06-25 14:30:00' }, + { id: 'stk-006', categoryId: 'cat-fitness-bike', categoryName: '健身车', batchNo: 'B2026-006', supplierId: 'sup-003', supplierName: '顿米科技', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位', stockQty: 4, lockedQty: 0, availableQty: 4, healthLevel: 'sufficient', updateTime: '2026-06-25 15:00:00' }, + { id: 'stk-007', categoryId: 'cat-monitor-all-in-one', categoryName: '智能健康一体机', batchNo: 'B2026-007', supplierId: 'sup-004', supplierName: '华盛医疗', warehouseAreaId: 'wh-floor2-check-1', warehouseAreaName: '2楼待检区-临时储位', stockQty: 0, lockedQty: 0, availableQty: 0, healthLevel: 'out_of_stock', updateTime: '2026-06-25 16:20:00' }, + { id: 'stk-008', categoryId: 'cat-body-fat', categoryName: '体脂仪', batchNo: 'B2026-008', supplierId: 'sup-005', supplierName: 'InBody中国', warehouseAreaId: 'wh-floor1-small-shelf-2', warehouseAreaName: '1楼小型备品货架-2号储位', stockQty: 0, lockedQty: 0, availableQty: 0, healthLevel: 'out_of_stock', updateTime: '2026-06-25 17:00:00' }, ] const listPage = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = stockList.filter((item) => { - if (params.deviceType && item.deviceType !== params.deviceType) return false - if (params.stockStatus && item.stockStatus !== params.stockStatus) return false - if (params.keyword && !includes(item.modelCode, params.keyword) && !includes(item.modelName, params.keyword) && !includes(item.batchNo, params.keyword)) return false + if (params.categoryId && item.categoryId !== params.categoryId) return false + if (params.warehouseAreaId && item.warehouseAreaId !== params.warehouseAreaId) return false + if (params.healthLevel && item.healthLevel !== params.healthLevel) return false + if (params.keyword && !includes(item.batchNo, params.keyword) && !includes(item.supplierName, params.keyword)) return false return true }) if (params.column && params.order) { @@ -95,24 +123,8 @@ const infoById = (params: PageParams): ApiResponse => { return ok('查询成功', record ?? null) } -/** 回库登记:状态变更 + 数量调整 */ -const returnRecord = (params: PageParams): ApiResponse => { - const id = String(params.id ?? '') - const record = stockList.find((item) => item.id === id) - if (!record) return ok('库存记录不存在') - const returnQty = Number(params.returnQty ?? 0) - record.stockQty += returnQty - record.availableQty += returnQty - record.shippedQty = Math.max(0, record.shippedQty - returnQty) - record.stockStatus = 'returned' - if (params.storageLocation) record.storageLocation = String(params.storageLocation) - record.updateTime = nowStr() - return ok('回库登记成功') -} - registerMocks([ { method: 'POST', url: '/admin/device/stock/instock/page', handler: listPage }, { method: 'POST', url: '/admin/device/stock/instock/info', handler: (ctx) => infoById(getParams(ctx.params)) }, - { method: 'POST', url: '/admin/device/stock/instock/return', handler: (ctx) => returnRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/instock/export', handler: () => ok('导出任务已提交') }, ]) diff --git a/src/axios/handlers/device-center/asset/manage.ts b/src/axios/handlers/device-center/asset/manage.ts index 56883d9..a24dd18 100644 --- a/src/axios/handlers/device-center/asset/manage.ts +++ b/src/axios/handlers/device-center/asset/manage.ts @@ -2,56 +2,76 @@ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext } from '@axios' type CategoryTab = 'catering' | 'body' | 'wearable' | 'fitness' | 'monitor' -type RunStatus = 'online' | 'offline' | 'repairing' | 'scrapped' type DeployStatus = 'in_stock' | 'deployed' +type NetworkMode = 'wifi' | 'bluetooth' | 'sim' | 'ethernet' | 'other_iot' + +interface CategoryMeta { + /** 二级分类名称 */ + name: string + /** 顶级分类 Tab */ + category: CategoryTab + /** 是否 IOT 设备 */ + isIot: boolean + /** 是否启用数据分发 */ + dataTransit: boolean + /** 是否启用数据上报 */ + dataReport: boolean + /** 配网方式 */ + networkMode: NetworkMode + /** 厂商 */ + manufacturer: string + /** 型号 */ + model: string +} interface DeviceItem { id: string deviceCode: string name: string - modelName: string + serialNumber: string + deviceCategory: string category: CategoryTab - tenantName: string + customerId: string + customerName: string + siteId: string siteName: string deployStatus: DeployStatus - runStatus: RunStatus softwareVersion: string onlineTime: string deployTime: string - lastHeartbeat: string + arcLicense: string + simIccid: string + // 类别元数据(冗余在设备记录里,由后端按 deviceCategory 回填) isIot: boolean - [key: string]: string | boolean | undefined + dataTransit: boolean + dataReport: boolean + networkMode: NetworkMode + manufacturer: string + model: string + /** 入库单号(审核入库单时写入,便于按入库单反查台账) */ + inboundCode?: string } -interface RunLogItem { +interface CustomerOption { id: string - eventType: string - content: string - occurTime: string + name: string } -interface RepairItem { +interface SiteOption { id: string - workOrderNo: string - faultType: string - repairMethod: string - repairer: string - workHours: number - finishTime: string -} - -interface DetailInfo extends DeviceItem { - runLogs?: RunLogItem[] - repairs?: RepairItem[] + name: string + customerId: string } interface ListParams { keyword?: string - runStatus?: RunStatus deployStatus?: DeployStatus - tenantName?: string - siteName?: string + deviceCategory?: string + customerId?: string + siteId?: string category: CategoryTab + /** 入库单号(按入库单反查设备台账) */ + inboundCode?: string } const ok = (msg: string, data: T = null as T): ApiResponse => ({ @@ -60,198 +80,406 @@ const ok = (msg: string, data: T = null as T): ApiResponse => ({ data, }) -/** 依据租户 / 场所自动判定投用状态 */ -const computeDeployStatus = (tenantName: string, siteName: string): DeployStatus => { - if (!tenantName && !siteName) return 'in_stock' +/** 依据客户 / 场所自动判定投用状态 */ +const computeDeployStatus = (customerId: string, siteId: string): DeployStatus => { + if (!customerId && !siteId) return 'in_stock' return 'deployed' } +/** + * 设备类别元数据(与设备分类树 mock 数据对齐) + * - 用于新增/编辑时按所选 deviceCategory 冗余回填到设备记录 + * - 用于设备类别下拉接口返回 + */ +const CATEGORY_META: Record = { + catering: [ + { name: '吐盘机', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '商合', model: 'SH-TP-200' }, + { name: '智能货柜', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '华为', model: 'HW-CAB-100L' }, + { name: '营养秤', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-NS-5KG' }, + { name: '配比秤', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-RS-15KG' }, + { name: '结算台', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '华为', model: 'HW-CK-21IN' }, + { name: '入库秤', category: 'catering', isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-IS-30KG' }, + { name: '带鱼屏', category: 'catering', isIot: false, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '商合', model: 'SH-BS-25IN' }, + { name: '波菲炉', category: 'catering', isIot: false, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '商合', model: 'SH-BF-4GN' }, + ], + body: [ + { name: '一体机', category: 'body', isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '罗杰夫', model: 'LJF-AIO-V3' }, + { name: '蓝牙秤', category: 'body', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-BT-180KG' }, + { name: '体脂仪', category: 'body', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'HW-FAT-BH' }, + ], + wearable: [ + { name: '华为-watch5', category: 'wearable', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'Watch-5' }, + { name: '华为-watch4', category: 'wearable', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'Watch-4' }, + { name: '罗杰夫-LJF01', category: 'wearable', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '罗杰夫', model: 'LJF-01' }, + ], + fitness: [ + { name: '跑步机', category: 'fitness', isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-TM-Pro' }, + { name: '椭圆机', category: 'fitness', isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-EL-X1' }, + { name: '健身车', category: 'fitness', isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-BK-M3' }, + ], + monitor: [ + { name: '血糖监测仪', category: 'monitor', isIot: true, dataTransit: true, dataReport: true, networkMode: 'sim', manufacturer: '鱼跃', model: 'YY-BG-530' }, + { name: '高血压治疗仪', category: 'monitor', isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-BP-680' }, + { name: '智能健康一体机', category: 'monitor', isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '罗杰夫', model: 'LJF-HM-Z1' }, + ], +} + +/** 客户下拉数据源(与 customer/archive mock 对齐,精简为 id+name) */ +const customerOptionList: CustomerOption[] = [ + { id: 'cust-001', name: '长庆能源总部食堂' }, + { id: 'cust-002', name: '第一采油厂' }, + { id: 'cust-003', name: '炼化分公司' }, + { id: 'cust-004', name: '健康示范园区' }, + { id: 'cust-005', name: '示范餐饮集团' }, + { id: 'cust-006', name: '健身中心B' }, +] + +/** 场所下拉数据源(与 customer/site mock 对齐,精简为 id+name+customerId) */ +const siteOptionList: SiteOption[] = [ + { id: 'site-001', name: '长庆总部一食堂', customerId: 'cust-001' }, + { id: 'site-002', name: '采油一厂食堂', customerId: 'cust-002' }, + { id: 'site-003', name: '炼化员工餐厅', customerId: 'cust-003' }, + { id: 'site-004', name: '健康园区体检站', customerId: 'cust-004' }, + { id: 'site-005', name: '总部食堂', customerId: 'cust-005' }, + { id: 'site-006', name: '东区食堂', customerId: 'cust-005' }, + { id: 'site-007', name: '体检站1', customerId: 'cust-004' }, + { id: 'site-008', name: '体检站2', customerId: 'cust-004' }, + { id: 'site-009', name: '健身房1', customerId: 'cust-006' }, +] + +/** 按 deviceCategory 名称查找类别元数据 */ +const findCategoryMeta = (name: string): CategoryMeta | null => { + for (const tab of Object.keys(CATEGORY_META) as CategoryTab[]) { + const found = CATEGORY_META[tab].find((item) => item.name === name) + if (found) return found + } + return null +} + +/** 按 deviceCategory 名称查找所属顶级分类 Tab */ +const resolveCategoryTab = (name: string): CategoryTab => { + const meta = findCategoryMeta(name) + return meta?.category ?? 'catering' +} + +/** 按 customerId 查询客户名称 */ +const findCustomerName = (id: string): string => + customerOptionList.find((item) => item.id === id)?.name ?? '' + +/** 按 siteId 查询场所名称 */ +const findSiteName = (id: string): string => + siteOptionList.find((item) => item.id === id)?.name ?? '' + /** 按 Tab 维度的 mock 数据集(每类 ≤ 8 条) */ const dataset: Record = { catering: [ { - id: 'dev-c-1', deviceCode: 'CATERING-PM-0001', name: '一号吐盘机', modelName: '吐盘机', category: 'catering', - tenantName: '示范餐饮集团', siteName: '总部食堂', runStatus: 'online', softwareVersion: 'V1.2.0', - deployStatus: 'deployed', onlineTime: '2026-05-12 10:00:00', deployTime: '2026-05-20 09:00:00', - lastHeartbeat: '2026-07-02 09:30:00', isIot: true, - trackNo: 'T-01', placement: '落地', boundScreen: '大屏-A1', stall: '主食档', clientIp: '192.168.10.11', + id: 'dev-c-1', deviceCode: 'CATERING-PM-0001', name: '一号吐盘机', serialNumber: 'SN-PM-0001', + deviceCategory: '吐盘机', category: 'catering', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-005', siteName: '总部食堂', + deployStatus: 'deployed', softwareVersion: 'V1.2.0', + onlineTime: '2026-05-12 10:00:00', deployTime: '2026-05-20 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '商合', model: 'SH-TP-200', }, { - id: 'dev-c-2', deviceCode: 'CATERING-CB-0002', name: '智能货柜-B02', modelName: '智能货柜', category: 'catering', - tenantName: '示范餐饮集团', siteName: '东区食堂', runStatus: 'online', softwareVersion: 'V2.0.1', - deployStatus: 'deployed', onlineTime: '2026-05-13 11:00:00', deployTime: '2026-05-22 10:00:00', - lastHeartbeat: '2026-07-02 09:31:00', isIot: true, - trackNo: 'T-02', placement: '嵌入式', boundScreen: '—', stall: '副食档', clientIp: '192.168.10.12', + id: 'dev-c-2', deviceCode: 'CATERING-CB-0002', name: '智能货柜-B02', serialNumber: 'SN-CB-0002', + deviceCategory: '智能货柜', category: 'catering', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-006', siteName: '东区食堂', + deployStatus: 'deployed', softwareVersion: 'V2.0.1', + onlineTime: '2026-05-13 11:00:00', deployTime: '2026-05-22 10:00:00', + arcLicense: 'ARC-CAB-2026-0002', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '华为', model: 'HW-CAB-100L', }, { - id: 'dev-c-3', deviceCode: 'CATERING-NS-0003', name: '营养秤-03', modelName: '营养秤', category: 'catering', - tenantName: '', siteName: '', runStatus: 'offline', softwareVersion: 'V1.0.5', - deployStatus: 'in_stock', onlineTime: '2026-05-14 09:30:00', deployTime: '', - lastHeartbeat: '2026-07-01 18:20:00', isIot: true, - trackNo: '—', placement: '台面', boundScreen: '—', stall: '—', clientIp: '—', + id: 'dev-c-3', deviceCode: 'CATERING-NS-0003', name: '', serialNumber: 'SN-NS-0003', + deviceCategory: '营养秤', category: 'catering', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V1.0.5', + onlineTime: '2026-05-14 09:30:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-NS-5KG', }, { - id: 'dev-c-4', deviceCode: 'CATERING-CO-0004', name: '结算台-04', modelName: '结算台', category: 'catering', - tenantName: '健康机构A', siteName: '体检站1', runStatus: 'repairing', softwareVersion: 'V1.3.0', - deployStatus: 'deployed', onlineTime: '2026-05-15 14:00:00', deployTime: '2026-05-25 14:00:00', - lastHeartbeat: '2026-06-30 12:00:00', isIot: true, - trackNo: '—', placement: '落地', boundScreen: '结算屏-04', stall: '出口档', clientIp: '192.168.10.14', + id: 'dev-c-4', deviceCode: 'CATERING-CO-0004', name: '结算台-04', serialNumber: 'SN-CO-0004', + deviceCategory: '结算台', category: 'catering', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-007', siteName: '体检站1', + deployStatus: 'deployed', softwareVersion: 'V1.3.0', + onlineTime: '2026-05-15 14:00:00', deployTime: '2026-05-25 14:00:00', + arcLicense: 'ARC-CK-2026-0004', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '华为', model: 'HW-CK-21IN', + }, + { + id: 'dev-c-5', deviceCode: 'CATERING-RS-0005', name: '配比秤-05', serialNumber: 'SN-RS-0005', + deviceCategory: '配比秤', category: 'catering', + customerId: 'cust-002', customerName: '第一采油厂', siteId: 'site-002', siteName: '采油一厂食堂', + deployStatus: 'deployed', softwareVersion: 'V1.1.0', + onlineTime: '2026-05-16 09:00:00', deployTime: '2026-05-24 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-RS-15KG', + }, + { + id: 'dev-c-6', deviceCode: 'CATERING-IS-0006', name: '入库秤-06', serialNumber: 'SN-IS-0006', + deviceCategory: '入库秤', category: 'catering', + customerId: 'cust-003', customerName: '炼化分公司', siteId: 'site-003', siteName: '炼化员工餐厅', + deployStatus: 'deployed', softwareVersion: 'V1.0.8', + onlineTime: '2026-05-17 10:30:00', deployTime: '2026-05-26 10:30:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-IS-30KG', + }, + { + id: 'dev-c-7', deviceCode: 'CATERING-BS-0007', name: '带鱼屏-A1', serialNumber: 'SN-BS-0007', + deviceCategory: '带鱼屏', category: 'catering', + customerId: 'cust-001', customerName: '长庆能源总部食堂', siteId: 'site-001', siteName: '长庆总部一食堂', + deployStatus: 'deployed', softwareVersion: 'V2.3.0', + onlineTime: '2026-05-18 14:30:00', deployTime: '2026-05-28 14:30:00', + arcLicense: 'ARC-BS-2026-0007', simIccid: '', + isIot: false, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '商合', model: 'SH-BS-25IN', + }, + { + id: 'dev-c-8', deviceCode: 'CATERING-BF-0008', name: '波菲炉-08', serialNumber: 'SN-BF-0008', + deviceCategory: '波菲炉', category: 'catering', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-005', siteName: '总部食堂', + deployStatus: 'deployed', softwareVersion: 'V1.0.0', + onlineTime: '2026-05-19 11:00:00', deployTime: '2026-05-29 11:00:00', + arcLicense: '', simIccid: '', + isIot: false, dataTransit: false, dataReport: false, networkMode: 'ethernet', manufacturer: '商合', model: 'SH-BF-4GN', + }, + { + id: 'dev-c-9', deviceCode: 'CATERING-CB-0009', name: '', serialNumber: 'SN-CB-0009', + deviceCategory: '智能货柜', category: 'catering', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V2.0.1', + onlineTime: '2026-06-02 09:00:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '华为', model: 'HW-CAB-100L', + }, + { + id: 'dev-c-10', deviceCode: 'CATERING-PM-0010', name: '', serialNumber: 'SN-PM-0010', + deviceCategory: '吐盘机', category: 'catering', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V1.2.0', + onlineTime: '2026-06-05 15:00:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '商合', model: 'SH-TP-200', }, ], body: [ { - id: 'dev-b-1', deviceCode: 'BODY-AIO-0001', name: '体检一体机-01', modelName: '一体机', category: 'body', - tenantName: '健康机构A', siteName: '体检站1', runStatus: 'online', softwareVersion: 'V3.1.0', - deployStatus: 'deployed', onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-18 09:00:00', - lastHeartbeat: '2026-07-02 09:32:00', isIot: true, - measureIndicators: '体重 / 体脂 / BMI', range: '0-200kg', accuracy: '±0.1kg', unit: 'kg', measureMethod: '站姿', + id: 'dev-b-1', deviceCode: 'BODY-AIO-0001', name: '体检一体机-01', serialNumber: 'SN-AIO-0001', + deviceCategory: '一体机', category: 'body', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-007', siteName: '体检站1', + deployStatus: 'deployed', softwareVersion: 'V3.1.0', + onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-18 09:00:00', + arcLicense: 'ARC-AIO-2026-0001', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '罗杰夫', model: 'LJF-AIO-V3', }, { - id: 'dev-b-2', deviceCode: 'BODY-BT-0002', name: '蓝牙秤-02', modelName: '蓝牙秤', category: 'body', - tenantName: '健康机构A', siteName: '体检站2', runStatus: 'online', softwareVersion: 'V2.4.0', - deployStatus: 'deployed', onlineTime: '2026-05-11 10:00:00', deployTime: '2026-05-19 10:00:00', - lastHeartbeat: '2026-07-02 09:30:00', isIot: true, - measureIndicators: '体重', range: '0-150kg', accuracy: '±0.05kg', unit: 'kg', measureMethod: '站姿', + id: 'dev-b-2', deviceCode: 'BODY-BT-0002', name: '蓝牙秤-02', serialNumber: 'SN-BT-0002', + deviceCategory: '蓝牙秤', category: 'body', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-008', siteName: '体检站2', + deployStatus: 'deployed', softwareVersion: 'V2.4.0', + onlineTime: '2026-05-11 10:00:00', deployTime: '2026-05-19 10:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-BT-180KG', }, { - id: 'dev-b-3', deviceCode: 'BODY-FAT-0003', name: '体脂仪-03', modelName: '体脂仪', category: 'body', - tenantName: '', siteName: '', runStatus: 'offline', softwareVersion: 'V1.8.0', - deployStatus: 'in_stock', onlineTime: '2026-05-12 11:00:00', deployTime: '', - lastHeartbeat: '2026-07-01 16:00:00', isIot: true, - measureIndicators: '体重 / 体脂 / 水分', range: '0-200kg', accuracy: '±0.1kg', unit: 'kg', measureMethod: '站姿', + id: 'dev-b-3', deviceCode: 'BODY-FAT-0003', name: '', serialNumber: 'SN-FAT-0003', + deviceCategory: '体脂仪', category: 'body', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V1.8.0', + onlineTime: '2026-05-12 11:00:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'HW-FAT-BH', }, ], wearable: [ { - id: 'dev-w-1', deviceCode: 'WEAR-HW5-0001', name: '华为手表5-老人甲', modelName: '华为-watch5', category: 'wearable', - tenantName: '健康机构A', siteName: '体检站1', runStatus: 'online', softwareVersion: 'V5.2.0', - deployStatus: 'deployed', onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', - lastHeartbeat: '2026-07-02 09:33:00', isIot: true, - bandNo: 'BN-0001', simCard: '8986011224567890', boundUser: '张三 (UID-001)', battery: '85%', protocol: '4G', + id: 'dev-w-1', deviceCode: 'WEAR-HW5-0001', name: '华为手表5-老人甲', serialNumber: 'SN-HW5-0001', + deviceCategory: '华为-watch5', category: 'wearable', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-007', siteName: '体检站1', + deployStatus: 'deployed', softwareVersion: 'V5.2.0', + onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'Watch-5', }, { - id: 'dev-w-2', deviceCode: 'WEAR-HW4-0002', name: '华为手表4-老人乙', modelName: '华为-watch4', category: 'wearable', - tenantName: '健康机构A', siteName: '体检站2', runStatus: 'online', softwareVersion: 'V4.1.0', - deployStatus: 'deployed', onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-16 09:00:00', - lastHeartbeat: '2026-07-02 09:30:00', isIot: true, - bandNo: 'BN-0002', simCard: '8986011224567891', boundUser: '李四 (UID-002)', battery: '62%', protocol: '4G', + id: 'dev-w-2', deviceCode: 'WEAR-HW4-0002', name: '华为手表4-老人乙', serialNumber: 'SN-HW4-0002', + deviceCategory: '华为-watch4', category: 'wearable', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-008', siteName: '体检站2', + deployStatus: 'deployed', softwareVersion: 'V4.1.0', + onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-16 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '华为', model: 'Watch-4', }, { - id: 'dev-w-3', deviceCode: 'WEAR-LJF-0003', name: '罗杰夫-丙', modelName: '罗杰夫-LJF01', category: 'wearable', - tenantName: '示范餐饮集团', siteName: '总部食堂', runStatus: 'repairing', softwareVersion: 'V1.0.3', - deployStatus: 'deployed', onlineTime: '2026-05-11 09:00:00', deployTime: '2026-05-17 09:00:00', - lastHeartbeat: '2026-06-28 10:00:00', isIot: true, - bandNo: 'BN-0003', simCard: '8986011224567892', boundUser: '王五 (UID-003)', battery: '20%', protocol: 'BLE', + id: 'dev-w-3', deviceCode: 'WEAR-LJF-0003', name: '罗杰夫-丙', serialNumber: 'SN-LJF-0003', + deviceCategory: '罗杰夫-LJF01', category: 'wearable', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-005', siteName: '总部食堂', + deployStatus: 'deployed', softwareVersion: 'V1.0.3', + onlineTime: '2026-05-11 09:00:00', deployTime: '2026-05-17 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '罗杰夫', model: 'LJF-01', }, ], fitness: [ { - id: 'dev-f-1', deviceCode: 'FIT-TM-0001', name: '跑步机-01', modelName: '跑步机', category: 'fitness', - tenantName: '健身中心B', siteName: '健身房1', runStatus: 'online', softwareVersion: 'V2.0.0', - deployStatus: 'deployed', onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', - lastHeartbeat: '2026-07-02 09:30:00', isIot: true, - equipmentType: '有氧', resistanceMode: '电磁', trainDataType: '时长 / 卡路里 / 距离', location: '一层东区', + id: 'dev-f-1', deviceCode: 'FIT-TM-0001', name: '跑步机-01', serialNumber: 'SN-TM-0001', + deviceCategory: '跑步机', category: 'fitness', + customerId: 'cust-006', customerName: '健身中心B', siteId: 'site-009', siteName: '健身房1', + deployStatus: 'deployed', softwareVersion: 'V2.0.0', + onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-TM-Pro', }, { - id: 'dev-f-2', deviceCode: 'FIT-EL-0002', name: '椭圆机-02', modelName: '椭圆机', category: 'fitness', - tenantName: '健身中心B', siteName: '健身房1', runStatus: 'online', softwareVersion: 'V1.5.0', - deployStatus: 'deployed', onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', - lastHeartbeat: '2026-07-02 09:31:00', isIot: true, - equipmentType: '有氧', resistanceMode: '电磁', trainDataType: '时长 / 卡路里', location: '一层东区', + id: 'dev-f-2', deviceCode: 'FIT-EL-0002', name: '椭圆机-02', serialNumber: 'SN-EL-0002', + deviceCategory: '椭圆机', category: 'fitness', + customerId: 'cust-006', customerName: '健身中心B', siteId: 'site-009', siteName: '健身房1', + deployStatus: 'deployed', softwareVersion: 'V1.5.0', + onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-EL-X1', }, { - id: 'dev-f-3', deviceCode: 'FIT-BK-0003', name: '健身车-03', modelName: '健身车', category: 'fitness', - tenantName: '', siteName: '', runStatus: 'offline', softwareVersion: 'V1.2.0', - deployStatus: 'in_stock', onlineTime: '2026-05-10 09:00:00', deployTime: '', - lastHeartbeat: '2026-07-01 17:00:00', isIot: true, - equipmentType: '有氧', resistanceMode: '液压', trainDataType: '时长 / 卡路里', location: '—', + id: 'dev-f-3', deviceCode: 'FIT-BK-0003', name: '', serialNumber: 'SN-BK-0003', + deviceCategory: '健身车', category: 'fitness', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V1.2.0', + onlineTime: '2026-05-10 09:00:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '毅雄', model: 'YX-BK-M3', }, ], monitor: [ { - id: 'dev-m-1', deviceCode: 'MON-SG-0001', name: '血糖仪-01', modelName: '血糖监测仪', category: 'monitor', - tenantName: '健康机构A', siteName: '体检站1', runStatus: 'online', softwareVersion: 'V1.4.0', - deployStatus: 'deployed', onlineTime: '2026-05-07 09:00:00', deployTime: '2026-05-13 09:00:00', - lastHeartbeat: '2026-07-02 09:32:00', isIot: true, - monitorIndicators: '血糖', sampleFrequency: '按需', measureMethod: '采血', reportMode: '云端', + id: 'dev-m-1', deviceCode: 'MON-SG-0001', name: '血糖仪-01', serialNumber: 'SN-SG-0001', + deviceCategory: '血糖监测仪', category: 'monitor', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-007', siteName: '体检站1', + deployStatus: 'deployed', softwareVersion: 'V1.4.0', + onlineTime: '2026-05-07 09:00:00', deployTime: '2026-05-13 09:00:00', + arcLicense: '', simIccid: '8986011224567890', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'sim', manufacturer: '鱼跃', model: 'YY-BG-530', }, { - id: 'dev-m-2', deviceCode: 'MON-BP-0002', name: '高血压治疗仪-02', modelName: '高血压治疗仪', category: 'monitor', - tenantName: '健康机构A', siteName: '体检站2', runStatus: 'online', softwareVersion: 'V2.1.0', - deployStatus: 'deployed', onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', - lastHeartbeat: '2026-07-02 09:30:00', isIot: true, - monitorIndicators: '血压 / 心率', sampleFrequency: '每5分钟', measureMethod: '袖带', reportMode: '本地', + id: 'dev-m-2', deviceCode: 'MON-BP-0002', name: '高血压治疗仪-02', serialNumber: 'SN-BP-0002', + deviceCategory: '高血压治疗仪', category: 'monitor', + customerId: 'cust-004', customerName: '健康示范园区', siteId: 'site-008', siteName: '体检站2', + deployStatus: 'deployed', softwareVersion: 'V2.1.0', + onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'bluetooth', manufacturer: '鱼跃', model: 'YY-BP-680', }, { - id: 'dev-m-3', deviceCode: 'MON-AIO-0003', name: '健康一体机-03', modelName: '智能健康一体机', category: 'monitor', - tenantName: '', siteName: '', runStatus: 'scrapped', softwareVersion: 'V3.0.0', - deployStatus: 'in_stock', onlineTime: '2026-05-09 09:00:00', deployTime: '', - lastHeartbeat: '2026-06-15 11:00:00', isIot: true, - monitorIndicators: '血压 / 血糖 / 血氧 / 心电', sampleFrequency: '按需', measureMethod: '光电', reportMode: '云端', + id: 'dev-m-3', deviceCode: 'MON-AIO-0003', name: '健康一体机-03', serialNumber: 'SN-AIO-0003', + deviceCategory: '智能健康一体机', category: 'monitor', + customerId: '', customerName: '', siteId: '', siteName: '', + deployStatus: 'in_stock', softwareVersion: 'V3.0.0', + onlineTime: '2026-05-09 09:00:00', deployTime: '', + arcLicense: '', simIccid: '', + isIot: true, dataTransit: true, dataReport: true, networkMode: 'wifi', manufacturer: '罗杰夫', model: 'LJF-HM-Z1', }, ], } const handleList = (ctx: MockContext): ApiResponse => { const params = (ctx.params ?? {}) as ListParams + /** 若按入库单号反查,跨 Tab 聚合 */ + if (params.inboundCode) { + const kw = params.inboundCode.toLowerCase() + const all: DeviceItem[] = [] + for (const cat of Object.keys(dataset) as CategoryTab[]) { + for (const item of dataset[cat]) { + if ((item.inboundCode ?? '').toLowerCase().includes(kw)) all.push(item) + } + } + return ok('查询成功', all) + } let list = dataset[params.category] ?? [] if (params.keyword) { const kw = params.keyword.toLowerCase() list = list.filter( (item) => item.deviceCode.toLowerCase().includes(kw) || - item.name.toLowerCase().includes(kw), + (item.name ?? '').toLowerCase().includes(kw), ) } - if (params.runStatus) { - list = list.filter((item) => item.runStatus === params.runStatus) - } if (params.deployStatus) { list = list.filter((item) => item.deployStatus === params.deployStatus) } - if (params.tenantName) { - list = list.filter((item) => (item.tenantName ?? '').includes(params.tenantName as string)) + if (params.deviceCategory) { + list = list.filter((item) => item.deviceCategory === params.deviceCategory) } - if (params.siteName) { - list = list.filter((item) => (item.siteName ?? '').includes(params.siteName as string)) + if (params.customerId) { + list = list.filter((item) => item.customerId === params.customerId) + } + if (params.siteId) { + list = list.filter((item) => item.siteId === params.siteId) } return ok('查询成功', list) } +/** 按 deviceCategory 冗余回填类别元数据到设备记录 */ +const applyCategoryMeta = (device: DeviceItem): DeviceItem => { + const meta = findCategoryMeta(device.deviceCategory) + if (!meta) return device + return { + ...device, + category: meta.category, + isIot: meta.isIot, + dataTransit: meta.dataTransit, + dataReport: meta.dataReport, + networkMode: meta.networkMode, + manufacturer: meta.manufacturer, + model: meta.model, + } +} + const handleAdd = (ctx: MockContext): ApiResponse => { const payload = (ctx.params ?? {}) as DeviceItem - if (!payload.name) return ok('设备名称不能为空') - const category = payload.category + if (!payload.deviceCode) return ok('设备编码不能为空') + if (!payload.deviceCategory) return ok('设备类别不能为空') const now = new Date().toISOString().replace('T', ' ').slice(0, 19) - const deployStatus = computeDeployStatus(payload.tenantName ?? '', payload.siteName ?? '') - const newItem: DeviceItem = { + const deployStatus = computeDeployStatus(payload.customerId ?? '', payload.siteId ?? '') + const customerName = findCustomerName(payload.customerId ?? '') + const siteName = findSiteName(payload.siteId ?? '') + const categoryTab = resolveCategoryTab(payload.deviceCategory) + const base: DeviceItem = { ...payload, - id: `dev-${category.slice(0, 1)}-${Date.now().toString(36)}`, - deviceCode: payload.deviceCode || `DEV-${Date.now()}`, + customerName, + siteName, deployStatus, - onlineTime: payload.onlineTime || now, deployTime: deployStatus === 'deployed' ? (payload.deployTime || now) : '', - lastHeartbeat: now, - isIot: true, + onlineTime: payload.onlineTime || now, } - dataset[category].unshift(newItem) + const newItem: DeviceItem = { + ...base, + id: `dev-${categoryTab.slice(0, 1)}-${Date.now().toString(36)}`, + category: categoryTab, + } + dataset[categoryTab].unshift(applyCategoryMeta(newItem)) return ok('新增成功') } const handleUpdate = (ctx: MockContext): ApiResponse => { const payload = (ctx.params ?? {}) as DeviceItem if (!payload.id) return ok('ID 不能为空') - const category = payload.category - const idx = dataset[category].findIndex((item) => item.id === payload.id) + const categoryTab = resolveCategoryTab(payload.deviceCategory) + const idx = dataset[categoryTab].findIndex((item) => item.id === payload.id) if (idx < 0) return ok('设备不存在') - /** 编辑时按租户 / 场所重新计算投用状态 */ - const deployStatus = computeDeployStatus(payload.tenantName ?? '', payload.siteName ?? '') - dataset[category][idx] = { - ...dataset[category][idx], + const deployStatus = computeDeployStatus(payload.customerId ?? '', payload.siteId ?? '') + const customerName = findCustomerName(payload.customerId ?? '') + const siteName = findSiteName(payload.siteId ?? '') + const merged: DeviceItem = { + ...dataset[categoryTab][idx], ...payload, + customerName, + siteName, + category: categoryTab, deployStatus, - deployTime: deployStatus === 'deployed' ? (payload.deployTime || dataset[category][idx].deployTime) : '', + deployTime: deployStatus === 'deployed' ? (payload.deployTime || dataset[categoryTab][idx].deployTime) : '', } + dataset[categoryTab][idx] = applyCategoryMeta(merged) return ok('更新成功') } @@ -274,64 +502,110 @@ const findDevice = (id: string): DeviceItem | null => { return null } -/** 生成运行日志(基于设备状态模拟) */ -const buildRunLogs = (device: DeviceItem): RunLogItem[] => { - const logs: RunLogItem[] = [ - { id: 'log-1', eventType: '心跳', content: '设备心跳上报正常', occurTime: device.lastHeartbeat }, - { id: 'log-2', eventType: '心跳', content: '设备心跳上报正常', occurTime: '2026-07-02 08:30:00' }, - { id: 'log-3', eventType: '关键事件', content: '完成一次测量任务', occurTime: '2026-07-02 08:00:00' }, - { id: 'log-4', eventType: '开机', content: '设备开机上线', occurTime: device.onlineTime }, - ] - if (device.runStatus === 'offline') { - logs.unshift({ id: 'log-0', eventType: '离线', content: '设备连续 3 次心跳未上报,标记为离线', occurTime: device.lastHeartbeat }) - } - if (device.runStatus === 'repairing') { - logs.unshift({ id: 'log-r', eventType: '异常', content: '硬件自检异常,已自动派单维修', occurTime: device.lastHeartbeat }) - } - return logs.slice(0, 8) -} - -/** 生成维修履历(按需) */ -const buildRepairs = (device: DeviceItem): RepairItem[] => { - if (device.runStatus !== 'repairing' && device.runStatus !== 'scrapped') return [] - return [ - { - id: 'rep-1', - workOrderNo: 'WO-2026-0601-001', - faultType: '传感器异常', - repairMethod: '更换传感器模组', - repairer: '李师傅', - workHours: 2.5, - finishTime: '2026-06-30 18:00:00', - }, - { - id: 'rep-2', - workOrderNo: 'WO-2026-0512-008', - faultType: '通讯模块故障', - repairMethod: '更换 4G 模块', - repairer: '王师傅', - workHours: 1.5, - finishTime: '2026-05-12 16:30:00', - }, - ] -} - -const handleInfo = (ctx: MockContext): ApiResponse => { +const handleInfo = (ctx: MockContext): ApiResponse => { const params = (ctx.params ?? {}) as { id?: string } if (!params.id) return ok('ID 不能为空') const device = findDevice(params.id) if (!device) return ok('设备不存在') - const detail: DetailInfo = { - ...device, - runLogs: buildRunLogs(device), - repairs: buildRepairs(device), - } - return ok('查询成功', detail) + return ok('查询成功', device) +} + +/** 设备类别下拉(按顶级分类 Tab) */ +const handleCategoryOptions = (ctx: MockContext): ApiResponse<{ name: string; category: CategoryTab; networkMode: NetworkMode }[]> => { + const params = (ctx.params ?? {}) as { category?: CategoryTab } + const tab = params.category ?? 'catering' + const list = (CATEGORY_META[tab] ?? []).map((item) => ({ + name: item.name, + category: item.category, + networkMode: item.networkMode, + })) + return ok('查询成功', list) +} + +/** 所属客户下拉(支持关键字搜索) */ +const handleCustomerOptions = (ctx: MockContext): ApiResponse => { + const params = (ctx.params ?? {}) as { keyword?: string } + const kw = (params.keyword ?? '').toLowerCase() + const list = kw + ? customerOptionList.filter((item) => item.name.toLowerCase().includes(kw)) + : customerOptionList + return ok('查询成功', list) +} + +/** 所属场所下拉(按 customerId 联动 + 关键字搜索) */ +const handleSiteOptions = (ctx: MockContext): ApiResponse => { + const params = (ctx.params ?? {}) as { customerId?: string; keyword?: string } + if (!params.customerId) return ok('查询成功', []) + const kw = (params.keyword ?? '').toLowerCase() + const list = siteOptionList.filter( + (item) => + item.customerId === params.customerId && + (!kw || item.name.toLowerCase().includes(kw)), + ) + return ok('查询成功', list) } /** 异步导出:提交后立即返回任务已接收 */ const handleExport = (): ApiResponse => ok('导出任务已提交,可在"查看导出任务"中查看进度') +/** 批量导入:原型阶段占位 */ +const handleImport = (): ApiResponse => ok('导入任务已提交,请稍后刷新列表查看结果') + +/** + * 由入库单审核触发:按入库单的设备明细批量生成设备台账 + * - 入库单 status=audited 时调用 + * - 每条明细生成一条 DeviceItem,inboundCode 回填入库单号 + * - 默认 in_stock(待投用),无客户/场所关联 + */ +export interface InboundDetailInput { + deviceCode: string + serialNumber: string +} +export interface InboundRecordInput { + inboundCode: string + categoryId: string + categoryName: string + items: InboundDetailInput[] +} +export const pushDevicesFromInbound = (record: InboundRecordInput): number => { + if (!record.items?.length) return 0 + const tab = resolveCategoryTab(record.categoryName) + const meta = findCategoryMeta(record.categoryName) + const now = new Date().toISOString().replace('T', ' ').slice(0, 19) + let pushed = 0 + for (const item of record.items) { + if (!item.deviceCode) continue + const newItem: DeviceItem = { + id: `dev-${tab.slice(0, 1)}-${Date.now().toString(36)}-${pushed}`, + deviceCode: item.deviceCode, + name: '', + serialNumber: item.serialNumber ?? '', + deviceCategory: record.categoryName, + category: tab, + customerId: '', + customerName: '', + siteId: '', + siteName: '', + deployStatus: 'in_stock', + softwareVersion: '', + onlineTime: now, + deployTime: '', + arcLicense: '', + simIccid: '', + isIot: meta?.isIot ?? false, + dataTransit: meta?.dataTransit ?? false, + dataReport: meta?.dataReport ?? false, + networkMode: meta?.networkMode ?? 'wifi', + manufacturer: meta?.manufacturer ?? '', + model: meta?.model ?? '', + inboundCode: record.inboundCode, + } + dataset[tab].unshift(newItem) + pushed += 1 + } + return pushed +} + registerMocks([ { method: 'POST', url: '/admin/device/manage/list', handler: handleList }, { method: 'POST', url: '/admin/device/manage/info', handler: handleInfo }, @@ -339,4 +613,8 @@ registerMocks([ { method: 'POST', url: '/admin/device/manage/update', handler: handleUpdate }, { method: 'POST', url: '/admin/device/manage/delete', handler: handleDelete }, { method: 'POST', url: '/admin/device/manage/export', handler: handleExport }, + { method: 'POST', url: '/admin/device/manage/import', handler: handleImport }, + { method: 'POST', url: '/admin/device/manage/category/options', handler: handleCategoryOptions }, + { method: 'POST', url: '/admin/device/manage/customer/options', handler: handleCustomerOptions }, + { method: 'POST', url: '/admin/device/manage/site/options', handler: handleSiteOptions }, ]) diff --git a/src/axios/handlers/device-center/asset/outbound.ts b/src/axios/handlers/device-center/asset/outbound.ts index 0b7e18e..b2c06bd 100644 --- a/src/axios/handlers/device-center/asset/outbound.ts +++ b/src/axios/handlers/device-center/asset/outbound.ts @@ -9,24 +9,30 @@ interface PageParams { keyword?: string status?: string outboundType?: string + categoryId?: string + warehouseAreaId?: string + customerId?: string [key: string]: unknown } interface OutboundRecord { id: string outboundCode: string - deviceCode: string - deviceName: string - modelName: string + categoryId: string + categoryName: string quantity: number recipient: string targetSite: string - targetCustomer: string + customerId: string + customerName: string outboundType: 'install' | 'transfer' | 'scrap' operator: string outboundTime: string signedTime?: string + cancelledTime?: string status: 'pending' | 'completed' | 'signed' | 'cancelled' + warehouseAreaId?: string + warehouseAreaName?: string remark?: string } @@ -49,15 +55,64 @@ const nowStr = (): string => { return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` } +/** 设备类别末级 ID → 名称映射(与 statistics/category mock 同源) */ +const CATEGORY_LEAF_NAME: Record = { + 'cat-catering-plate': '吐盘机', + 'cat-catering-cabinet': '智能货柜', + 'cat-catering-nutri-scale': '营养秤', + 'cat-catering-ratio-scale': '配比秤', + 'cat-catering-checkout': '结算台', + 'cat-catering-inbound-scale': '入库秤', + 'cat-catering-band-screen': '带鱼屏', + 'cat-catering-baifei': '波菲炉', + 'cat-body-all-in-one': '一体机', + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-fitness-bike': '健身车', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +/** 仓库区域末级节点 ID → 祖先链名称映射(与 warehouse mock 同源) */ +const WAREHOUSE_AREA_NAME: Record = { + 'wh-floor1-large-a-1': '1楼大型A号货架-1号储位', + 'wh-floor1-large-a-2': '1楼大型A号货架-2号储位', + 'wh-floor1-large-b-1': '1楼大型B号货架-1号储位', + 'wh-floor1-small-shelf-1': '1楼小型备品货架-1号储位', + 'wh-floor1-small-shelf-2': '1楼小型备品货架-2号储位', + 'wh-floor2-check-1': '2楼待检区-临时储位', +} + +/** 客户 ID → 名称映射(与 manage/statistics mock 同源) */ +const CUSTOMER_NAME: Record = { + 'cust-001': '长庆能源总部食堂', + 'cust-002': '第一采油厂', + 'cust-003': '炼化分公司', + 'cust-004': '健康示范园区', + 'cust-005': '示范餐饮集团', + 'cust-006': '健身中心B', + 'cust-007': '健康机构A', +} + +const resolveCategoryName = (id?: string): string => (id ? CATEGORY_LEAF_NAME[id] ?? '' : '') +const resolveWarehouseAreaName = (id?: string): string => (id ? WAREHOUSE_AREA_NAME[id] ?? '' : '') +const resolveCustomerName = (id?: string): string => (id ? CUSTOMER_NAME[id] ?? '' : '') + const outboundList: OutboundRecord[] = [ - { id: 'oub-001', outboundCode: 'OUB-20260603-001', deviceCode: 'CAB-A1-0003', deviceName: '智能货柜A1', modelName: '智能货柜', quantity: 2, recipient: '王师傅', targetSite: '总部食堂', targetCustomer: '智养餐饮集团', outboundType: 'install', operator: '张三', outboundTime: '2026-06-03 10:00:00', signedTime: '2026-06-03 14:30:00', status: 'signed' }, - { id: 'oub-002', outboundCode: 'OUB-20260608-002', deviceCode: 'WAT-W1-0002', deviceName: '鹏升手表W1', modelName: '华为-watch5', quantity: 5, recipient: '李主任', targetSite: '健康站01', targetCustomer: '健康管理机构', outboundType: 'install', operator: '李四', outboundTime: '2026-06-08 15:30:00', signedTime: '2026-06-09 09:00:00', status: 'signed' }, - { id: 'oub-003', outboundCode: 'OUB-20260612-003', deviceCode: 'CHK-M1-0002', deviceName: '华盛体检机', modelName: '智能健康一体机', quantity: 1, recipient: '赵工', targetSite: '东区体检站', targetCustomer: '健康管理机构', outboundType: 'install', operator: '王五', outboundTime: '2026-06-12 09:15:00', status: 'completed' }, - { id: 'oub-004', outboundCode: 'OUB-20260616-004', deviceCode: 'SCL-X1-0002', deviceName: '入库秤X1', modelName: '入库秤', quantity: 1, recipient: '孙师傅', targetSite: '总部食堂', targetCustomer: '智养餐饮集团', outboundType: 'install', operator: '赵六', outboundTime: '2026-06-16 14:00:00', signedTime: '2026-06-16 18:00:00', status: 'signed' }, - { id: 'oub-005', outboundCode: 'OUB-20260620-005', deviceCode: 'FIT-S1-0002', deviceName: '顿米健身车', modelName: '健身车', quantity: 2, recipient: '周教练', targetSite: '总部健身房', targetCustomer: '智养餐饮集团', outboundType: 'transfer', operator: '孙七', outboundTime: '2026-06-20 11:45:00', status: 'cancelled', remark: '客户取消订单' }, - { id: 'oub-006', outboundCode: 'OUB-20260623-006', deviceCode: 'SCR-P1-0002', deviceName: '大屏P1', modelName: '带鱼屏', quantity: 1, recipient: '陈师傅', targetSite: '总部食堂', targetCustomer: '智养餐饮集团', outboundType: 'install', operator: '张三', outboundTime: '2026-06-23 16:20:00', status: 'pending' }, - { id: 'oub-007', outboundCode: 'OUB-20260626-007', deviceCode: 'SCL-W-0002', deviceName: '体重秤', modelName: '蓝牙秤', quantity: 3, recipient: '吴主任', targetSite: '健康站01', targetCustomer: '健康管理机构', outboundType: 'install', operator: '李四', outboundTime: '2026-06-26 10:30:00', signedTime: '2026-06-27 09:00:00', status: 'signed' }, - { id: 'oub-008', outboundCode: 'OUB-20260628-008', deviceCode: 'FAT-INBODY-0002', deviceName: 'InBody体脂秤', modelName: '体脂仪', quantity: 1, recipient: '郑医生', targetSite: '东区体检站', targetCustomer: '健康管理机构', outboundType: 'install', operator: '王五', outboundTime: '2026-06-28 13:10:00', status: 'completed' }, + { id: 'oub-001', outboundCode: 'OUB-20260603-001', categoryId: 'cat-catering-cabinet', categoryName: '智能货柜', quantity: 2, recipient: '王师傅', targetSite: '总部食堂', customerId: 'cust-005', customerName: '示范餐饮集团', outboundType: 'install', operator: '张三', outboundTime: '2026-06-03 10:00:00', signedTime: '2026-06-03 14:30:00', status: 'signed', warehouseAreaId: 'wh-floor1-large-a-2', warehouseAreaName: '1楼大型A号货架-2号储位' }, + { id: 'oub-002', outboundCode: 'OUB-20260608-002', categoryId: 'cat-wearable-hw5', categoryName: '华为-watch5', quantity: 5, recipient: '李主任', targetSite: '健康站01', customerId: 'cust-007', customerName: '健康机构A', outboundType: 'install', operator: '李四', outboundTime: '2026-06-08 15:30:00', signedTime: '2026-06-09 09:00:00', status: 'signed', warehouseAreaId: 'wh-floor1-small-shelf-1', warehouseAreaName: '1楼小型备品货架-1号储位' }, + { id: 'oub-003', outboundCode: 'OUB-20260612-003', categoryId: 'cat-monitor-all-in-one', categoryName: '智能健康一体机', quantity: 1, recipient: '赵工', targetSite: '东区体检站', customerId: 'cust-007', customerName: '健康机构A', outboundType: 'install', operator: '王五', outboundTime: '2026-06-12 09:15:00', status: 'completed', warehouseAreaId: 'wh-floor1-large-b-1', warehouseAreaName: '1楼大型B号货架-1号储位' }, + { id: 'oub-004', outboundCode: 'OUB-20260616-004', categoryId: 'cat-catering-inbound-scale', categoryName: '入库秤', quantity: 1, recipient: '孙师傅', targetSite: '总部食堂', customerId: 'cust-005', customerName: '示范餐饮集团', outboundType: 'install', operator: '赵六', outboundTime: '2026-06-16 14:00:00', signedTime: '2026-06-16 18:00:00', status: 'signed', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位' }, + { id: 'oub-005', outboundCode: 'OUB-20260620-005', categoryId: 'cat-fitness-bike', categoryName: '健身车', quantity: 2, recipient: '周教练', targetSite: '总部健身房', customerId: 'cust-005', customerName: '示范餐饮集团', outboundType: 'transfer', operator: '孙七', outboundTime: '2026-06-20 11:45:00', cancelledTime: '2026-06-20 16:00:00', status: 'cancelled', remark: '客户取消订单', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位' }, + { id: 'oub-006', outboundCode: 'OUB-20260623-006', categoryId: 'cat-catering-band-screen', categoryName: '带鱼屏', quantity: 1, recipient: '陈师傅', targetSite: '总部食堂', customerId: 'cust-005', customerName: '示范餐饮集团', outboundType: 'install', operator: '张三', outboundTime: '2026-06-23 16:20:00', status: 'pending', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位' }, + { id: 'oub-007', outboundCode: 'OUB-20260626-007', categoryId: 'cat-body-bt-scale', categoryName: '蓝牙秤', quantity: 3, recipient: '吴主任', targetSite: '健康站01', customerId: 'cust-007', customerName: '健康机构A', outboundType: 'install', operator: '李四', outboundTime: '2026-06-26 10:30:00', signedTime: '2026-06-27 09:00:00', status: 'signed', warehouseAreaId: 'wh-floor1-small-shelf-2', warehouseAreaName: '1楼小型备品货架-2号储位' }, + { id: 'oub-008', outboundCode: 'OUB-20260628-008', categoryId: 'cat-body-fat', categoryName: '体脂仪', quantity: 1, recipient: '郑医生', targetSite: '东区体检站', customerId: 'cust-007', customerName: '健康机构A', outboundType: 'install', operator: '王五', outboundTime: '2026-06-28 13:10:00', status: 'completed', warehouseAreaId: 'wh-floor2-check-1', warehouseAreaName: '2楼待检区-临时储位' }, ] const listPage = (ctx: MockContext): ApiResponse => { @@ -65,7 +120,10 @@ const listPage = (ctx: MockContext): ApiResponse => { const filtered = outboundList.filter((item) => { if (params.status && item.status !== params.status) return false if (params.outboundType && item.outboundType !== params.outboundType) return false - if (params.keyword && !includes(item.outboundCode, params.keyword) && !includes(item.deviceCode, params.keyword) && !includes(item.deviceName, params.keyword)) return false + if (params.categoryId && item.categoryId !== params.categoryId) return false + if (params.warehouseAreaId && item.warehouseAreaId !== params.warehouseAreaId) return false + if (params.customerId && item.customerId !== params.customerId) return false + if (params.keyword && !includes(item.outboundCode, params.keyword)) return false return true }) if (params.column === 'outboundTime' && params.order) { @@ -98,20 +156,25 @@ const genOutboundCode = (): string => { } const addRecord = (params: PageParams): ApiResponse => { + const categoryId = String(params.categoryId ?? '') + const customerId = String(params.customerId ?? '') + const warehouseAreaId = params.warehouseAreaId ? String(params.warehouseAreaId) : undefined const record: OutboundRecord = { id: `oub-${Date.now().toString(36)}`, outboundCode: String(params.outboundCode ?? genOutboundCode()), - deviceCode: String(params.deviceCode ?? ''), - deviceName: String(params.deviceName ?? ''), - modelName: String(params.modelName ?? ''), + categoryId, + categoryName: resolveCategoryName(categoryId), quantity: Number(params.quantity ?? 1), recipient: String(params.recipient ?? ''), targetSite: String(params.targetSite ?? ''), - targetCustomer: String(params.targetCustomer ?? ''), + customerId, + customerName: resolveCustomerName(customerId), outboundType: (params.outboundType as OutboundRecord['outboundType']) ?? 'install', operator: '当前用户', outboundTime: nowStr(), status: 'completed', + warehouseAreaId, + warehouseAreaName: resolveWarehouseAreaName(warehouseAreaId), remark: params.remark ? String(params.remark) : undefined, } outboundList.unshift(record) @@ -122,34 +185,55 @@ const updateRecord = (params: PageParams): ApiResponse => { const id = String(params.id ?? '') const record = outboundList.find((item) => item.id === id) if (!record) return ok('出库单不存在') - if (params.deviceCode !== undefined) record.deviceCode = String(params.deviceCode) - if (params.deviceName !== undefined) record.deviceName = String(params.deviceName) - if (params.modelName !== undefined) record.modelName = String(params.modelName) + if (params.categoryId !== undefined) { + record.categoryId = String(params.categoryId) + record.categoryName = resolveCategoryName(record.categoryId) + } if (params.quantity !== undefined) record.quantity = Number(params.quantity) if (params.recipient !== undefined) record.recipient = String(params.recipient) if (params.targetSite !== undefined) record.targetSite = String(params.targetSite) - if (params.targetCustomer !== undefined) record.targetCustomer = String(params.targetCustomer) + if (params.customerId !== undefined) { + record.customerId = String(params.customerId) + record.customerName = resolveCustomerName(record.customerId) + } if (params.outboundType !== undefined) record.outboundType = params.outboundType as OutboundRecord['outboundType'] + if (params.warehouseAreaId !== undefined) { + record.warehouseAreaId = params.warehouseAreaId ? String(params.warehouseAreaId) : undefined + record.warehouseAreaName = resolveWarehouseAreaName(record.warehouseAreaId) + } if (params.remark !== undefined) record.remark = params.remark ? String(params.remark) : undefined return ok('更新成功') } -/** 撤回(已出库 / 待出库 状态可撤回) */ +/** 撤回(仅待出库状态可撤回) */ const withdrawRecord = (params: PageParams): ApiResponse => { const id = String(params.id ?? '') const record = outboundList.find((item) => item.id === id) if (!record) return ok('出库单不存在') - if (record.status === 'signed') return ok('已签收的出库单不可撤回') + if (record.status !== 'pending') return ok('仅待出库状态的出库单可撤回') record.status = 'cancelled' + record.cancelledTime = nowStr() return ok('撤回成功') } +/** 签收(已出库 → 已签收,写入签收时间) */ +const signRecord = (params: PageParams): ApiResponse => { + const id = String(params.id ?? '') + const record = outboundList.find((item) => item.id === id) + if (!record) return ok('出库单不存在') + if (record.status !== 'completed') return ok('仅已出库状态的出库单可签收') + record.status = 'signed' + record.signedTime = nowStr() + return ok('签收成功') +} + registerMocks([ { method: 'POST', url: '/admin/device/stock/outbound/page', handler: listPage }, { method: 'POST', url: '/admin/device/stock/outbound/info', handler: (ctx) => infoById(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/outbound/add', handler: (ctx) => addRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/outbound/update', handler: (ctx) => updateRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/outbound/withdraw', handler: (ctx) => withdrawRecord(getParams(ctx.params)) }, + { method: 'POST', url: '/admin/device/stock/outbound/sign', handler: (ctx) => signRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/outbound/delete', handler: (ctx) => deleteById(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/outbound/export', handler: () => ok('导出任务已提交') }, ]) diff --git a/src/axios/handlers/device-center/asset/statistics.ts b/src/axios/handlers/device-center/asset/statistics.ts index 12afc8d..7bedba3 100644 --- a/src/axios/handlers/device-center/asset/statistics.ts +++ b/src/axios/handlers/device-center/asset/statistics.ts @@ -7,15 +7,18 @@ type DeployStatus = 'in_stock' | 'deployed' interface DeviceStatItem { id: string + /** 末级分类名(list 接口按 categoryId 回填,不在 dataset 中冗余) */ categoryLabel: string category: CategoryTab + /** 设备分类树节点 ID(叶子节点,用于按类别筛选命中子树) */ + categoryId: string name: string deviceCode: string isIot: boolean deployStatus: DeployStatus - dataDispatch: boolean - dataReport: boolean - tenantName: string + customerId: string + customerName: string + siteId: string siteName: string softwareVersion: string runStatus: RunStatus @@ -24,15 +27,13 @@ interface DeviceStatItem { } interface SearchForm { - keyword?: string - category?: CategoryTab + /** 设备分类树节点 ID(命中该节点及其子树) */ + categoryId?: string deployStatus?: DeployStatus runStatus?: RunStatus isIot?: boolean - dataDispatch?: boolean - dataReport?: boolean - tenantName?: string - siteName?: string + customerId?: string + siteId?: string softwareVersion?: string onlineStart?: string onlineEnd?: string @@ -40,13 +41,29 @@ interface SearchForm { deployEnd?: string } +interface CustomerOption { + id: string + name: string +} + +interface SiteOption { + id: string + name: string + customerId: string +} + +interface SoftwareVersionOption { + value: string + label: string +} + const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data, }) -/** 顶级分类中文标签 */ +/** 顶级分类中文标签(用于推导 dataset.category 字段,不在列表展示) */ const CATEGORY_LABEL: Record = { catering: '智养餐饮设备', body: '体重测量设备', @@ -55,130 +72,200 @@ const CATEGORY_LABEL: Record = { monitor: '健康监测设备', } -/** 复用 manage 数据集,扁平化为统计列表行(不直接 import manage.ts,避免模块耦合,原型阶段保持独立副本) */ -const dataset: DeviceStatItem[] = [ +/** + * 末级分类名映射(categoryId → 叶子名,与 category mock 树叶子节点 name 对齐) + * - dataset 只存 categoryId,list 返回时按此映射回填 categoryLabel(模拟后端 join 分类表) + */ +const CATEGORY_LEAF_NAME: Record = { + 'cat-catering-plate': '吐盘机', + 'cat-catering-cabinet': '智能货柜', + 'cat-catering-nutri-scale': '营养秤', + 'cat-catering-ratio-scale': '配比秤', + 'cat-catering-checkout': '结算台', + 'cat-catering-inbound-scale': '入库秤', + 'cat-catering-band-screen': '带鱼屏', + 'cat-catering-baifei': '波菲炉', + 'cat-body-all-in-one': '一体机', + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-fitness-bike': '健身车', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +/** 客户下拉数据源(与 manage mock 同源,保持 id+name 一致) */ +const customerOptionList: CustomerOption[] = [ + { id: 'cust-001', name: '长庆能源总部食堂' }, + { id: 'cust-002', name: '第一采油厂' }, + { id: 'cust-003', name: '炼化分公司' }, + { id: 'cust-004', name: '健康示范园区' }, + { id: 'cust-005', name: '示范餐饮集团' }, + { id: 'cust-006', name: '健身中心B' }, + { id: 'cust-007', name: '健康机构A' }, +] + +/** 场所下拉数据源(与 manage mock 同源,按 customerId 联动) */ +const siteOptionList: SiteOption[] = [ + { id: 'site-001', name: '长庆总部一食堂', customerId: 'cust-001' }, + { id: 'site-002', name: '采油一厂食堂', customerId: 'cust-002' }, + { id: 'site-003', name: '炼化员工餐厅', customerId: 'cust-003' }, + { id: 'site-004', name: '健康园区体检站', customerId: 'cust-004' }, + { id: 'site-005', name: '总部食堂', customerId: 'cust-005' }, + { id: 'site-006', name: '东区食堂', customerId: 'cust-005' }, + { id: 'site-007', name: '体检站1', customerId: 'cust-004' }, + { id: 'site-008', name: '体检站2', customerId: 'cust-004' }, + { id: 'site-009', name: '健身房1', customerId: 'cust-006' }, +] + +/** + * 复用 manage 数据集,扁平化为统计列表行(不直接 import manage.ts,避免模块耦合,原型阶段保持独立副本) + * - dataset 不冗余 categoryLabel(末级分类名),由 handleList 按 categoryId 用 CATEGORY_LEAF_NAME 回填 + * - 与研发项目对齐:后端在 list 接口按 categoryId join 分类表返回末级分类名 + */ +type DatasetRow = Omit + +const dataset: DatasetRow[] = [ // 智养餐饮设备 { - id: 'dev-c-1', category: 'catering', categoryLabel: CATEGORY_LABEL.catering, + id: 'dev-c-1', category: 'catering', categoryId: 'cat-catering-plate', name: '一号吐盘机', deviceCode: 'CATERING-PM-0001', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '示范餐饮集团', siteName: '总部食堂', softwareVersion: 'V1.2.0', + deployStatus: 'deployed', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-005', siteName: '总部食堂', + softwareVersion: 'V1.2.0', runStatus: 'online', onlineTime: '2026-05-12 10:00:00', deployTime: '2026-05-20 09:00:00', }, { - id: 'dev-c-2', category: 'catering', categoryLabel: CATEGORY_LABEL.catering, + id: 'dev-c-2', category: 'catering', categoryId: 'cat-catering-cabinet', name: '智能货柜-B02', deviceCode: 'CATERING-CB-0002', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '示范餐饮集团', siteName: '东区食堂', softwareVersion: 'V2.0.1', + deployStatus: 'deployed', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-006', siteName: '东区食堂', + softwareVersion: 'V2.0.1', runStatus: 'online', onlineTime: '2026-05-13 11:00:00', deployTime: '2026-05-22 10:00:00', }, { - id: 'dev-c-3', category: 'catering', categoryLabel: CATEGORY_LABEL.catering, + id: 'dev-c-3', category: 'catering', categoryId: 'cat-catering-nutri-scale', name: '营养秤-03', deviceCode: 'CATERING-NS-0003', isIot: true, - deployStatus: 'in_stock', dataDispatch: false, dataReport: false, - tenantName: '', siteName: '', softwareVersion: 'V1.0.5', + deployStatus: 'in_stock', + customerId: '', customerName: '', siteId: '', siteName: '', + softwareVersion: 'V1.0.5', runStatus: 'offline', onlineTime: '2026-05-14 09:30:00', deployTime: '', }, { - id: 'dev-c-4', category: 'catering', categoryLabel: CATEGORY_LABEL.catering, + id: 'dev-c-4', category: 'catering', categoryId: 'cat-catering-checkout', name: '结算台-04', deviceCode: 'CATERING-CO-0004', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: false, - tenantName: '健康机构A', siteName: '体检站1', softwareVersion: 'V1.3.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-007', siteName: '体检站1', + softwareVersion: 'V1.3.0', runStatus: 'repairing', onlineTime: '2026-05-15 14:00:00', deployTime: '2026-05-25 14:00:00', }, // 体重测量设备 { - id: 'dev-b-1', category: 'body', categoryLabel: CATEGORY_LABEL.body, + id: 'dev-b-1', category: 'body', categoryId: 'cat-body-all-in-one', name: '体检一体机-01', deviceCode: 'BODY-AIO-0001', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站1', softwareVersion: 'V3.1.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-007', siteName: '体检站1', + softwareVersion: 'V3.1.0', runStatus: 'online', onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-18 09:00:00', }, { - id: 'dev-b-2', category: 'body', categoryLabel: CATEGORY_LABEL.body, + id: 'dev-b-2', category: 'body', categoryId: 'cat-body-bt-scale', name: '蓝牙秤-02', deviceCode: 'BODY-BT-0002', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站2', softwareVersion: 'V2.4.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-008', siteName: '体检站2', + softwareVersion: 'V2.4.0', runStatus: 'online', onlineTime: '2026-05-11 10:00:00', deployTime: '2026-05-19 10:00:00', }, { - id: 'dev-b-3', category: 'body', categoryLabel: CATEGORY_LABEL.body, + id: 'dev-b-3', category: 'body', categoryId: 'cat-body-fat', name: '体脂仪-03', deviceCode: 'BODY-FAT-0003', isIot: true, - deployStatus: 'in_stock', dataDispatch: false, dataReport: false, - tenantName: '', siteName: '', softwareVersion: 'V1.8.0', + deployStatus: 'in_stock', + customerId: '', customerName: '', siteId: '', siteName: '', + softwareVersion: 'V1.8.0', runStatus: 'offline', onlineTime: '2026-05-12 11:00:00', deployTime: '', }, // 智能穿戴设备 { - id: 'dev-w-1', category: 'wearable', categoryLabel: CATEGORY_LABEL.wearable, + id: 'dev-w-1', category: 'wearable', categoryId: 'cat-wearable-hw5', name: '华为手表5-老人甲', deviceCode: 'WEAR-HW5-0001', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站1', softwareVersion: 'V5.2.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-007', siteName: '体检站1', + softwareVersion: 'V5.2.0', runStatus: 'online', onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', }, { - id: 'dev-w-2', category: 'wearable', categoryLabel: CATEGORY_LABEL.wearable, + id: 'dev-w-2', category: 'wearable', categoryId: 'cat-wearable-hw4', name: '华为手表4-老人乙', deviceCode: 'WEAR-HW4-0002', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站2', softwareVersion: 'V4.1.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-008', siteName: '体检站2', + softwareVersion: 'V4.1.0', runStatus: 'online', onlineTime: '2026-05-10 09:00:00', deployTime: '2026-05-16 09:00:00', }, { - id: 'dev-w-3', category: 'wearable', categoryLabel: CATEGORY_LABEL.wearable, + id: 'dev-w-3', category: 'wearable', categoryId: 'cat-wearable-ljf01', name: '罗杰夫-丙', deviceCode: 'WEAR-LJF-0003', isIot: true, - deployStatus: 'deployed', dataDispatch: false, dataReport: true, - tenantName: '示范餐饮集团', siteName: '总部食堂', softwareVersion: 'V1.0.3', + deployStatus: 'deployed', + customerId: 'cust-005', customerName: '示范餐饮集团', siteId: 'site-005', siteName: '总部食堂', + softwareVersion: 'V1.0.3', runStatus: 'repairing', onlineTime: '2026-05-11 09:00:00', deployTime: '2026-05-17 09:00:00', }, // 健身器材设备 { - id: 'dev-f-1', category: 'fitness', categoryLabel: CATEGORY_LABEL.fitness, + id: 'dev-f-1', category: 'fitness', categoryId: 'cat-fitness-treadmill', name: '跑步机-01', deviceCode: 'FIT-TM-0001', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健身中心B', siteName: '健身房1', softwareVersion: 'V2.0.0', + deployStatus: 'deployed', + customerId: 'cust-006', customerName: '健身中心B', siteId: 'site-009', siteName: '健身房1', + softwareVersion: 'V2.0.0', runStatus: 'online', onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', }, { - id: 'dev-f-2', category: 'fitness', categoryLabel: CATEGORY_LABEL.fitness, + id: 'dev-f-2', category: 'fitness', categoryId: 'cat-fitness-elliptical', name: '椭圆机-02', deviceCode: 'FIT-EL-0002', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: false, - tenantName: '健身中心B', siteName: '健身房1', softwareVersion: 'V1.5.0', + deployStatus: 'deployed', + customerId: 'cust-006', customerName: '健身中心B', siteId: 'site-009', siteName: '健身房1', + softwareVersion: 'V1.5.0', runStatus: 'online', onlineTime: '2026-05-09 09:00:00', deployTime: '2026-05-15 09:00:00', }, { - id: 'dev-f-3', category: 'fitness', categoryLabel: CATEGORY_LABEL.fitness, + id: 'dev-f-3', category: 'fitness', categoryId: 'cat-fitness-bike', name: '健身车-03', deviceCode: 'FIT-BK-0003', isIot: true, - deployStatus: 'in_stock', dataDispatch: false, dataReport: false, - tenantName: '', siteName: '', softwareVersion: 'V1.2.0', + deployStatus: 'in_stock', + customerId: '', customerName: '', siteId: '', siteName: '', + softwareVersion: 'V1.2.0', runStatus: 'offline', onlineTime: '2026-05-10 09:00:00', deployTime: '', }, // 健康监测设备 { - id: 'dev-m-1', category: 'monitor', categoryLabel: CATEGORY_LABEL.monitor, + id: 'dev-m-1', category: 'monitor', categoryId: 'cat-monitor-sugar', name: '血糖仪-01', deviceCode: 'MON-SG-0001', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站1', softwareVersion: 'V1.4.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-007', siteName: '体检站1', + softwareVersion: 'V1.4.0', runStatus: 'online', onlineTime: '2026-05-07 09:00:00', deployTime: '2026-05-13 09:00:00', }, { - id: 'dev-m-2', category: 'monitor', categoryLabel: CATEGORY_LABEL.monitor, + id: 'dev-m-2', category: 'monitor', categoryId: 'cat-monitor-pressure', name: '高血压治疗仪-02', deviceCode: 'MON-BP-0002', isIot: true, - deployStatus: 'deployed', dataDispatch: true, dataReport: true, - tenantName: '健康机构A', siteName: '体检站2', softwareVersion: 'V2.1.0', + deployStatus: 'deployed', + customerId: 'cust-007', customerName: '健康机构A', siteId: 'site-008', siteName: '体检站2', + softwareVersion: 'V2.1.0', runStatus: 'online', onlineTime: '2026-05-08 09:00:00', deployTime: '2026-05-14 09:00:00', }, { - id: 'dev-m-3', category: 'monitor', categoryLabel: CATEGORY_LABEL.monitor, + id: 'dev-m-3', category: 'monitor', categoryId: 'cat-monitor-all-in-one', name: '健康一体机-03', deviceCode: 'MON-AIO-0003', isIot: true, - deployStatus: 'in_stock', dataDispatch: false, dataReport: false, - tenantName: '', siteName: '', softwareVersion: 'V3.0.0', + deployStatus: 'in_stock', + customerId: '', customerName: '', siteId: '', siteName: '', + softwareVersion: 'V3.0.0', runStatus: 'scrapped', onlineTime: '2026-05-09 09:00:00', deployTime: '', }, ] -/** 字符串包含匹配(空值匹配所有) */ -const includes = (src: string, kw: string): boolean => (src ?? '').toLowerCase().includes(kw.toLowerCase()) - /** 日期范围匹配(YYYY-MM-DD 字符串比较) */ const inRange = (val: string, start?: string, end?: string): boolean => { if (!start && !end) return true @@ -188,33 +275,58 @@ const inRange = (val: string, start?: string, end?: string): boolean => { return true } -/** 多维条件过滤 */ -const filterData = (params: SearchForm): DeviceStatItem[] => { +/** + * 简化的设备分类树父子关系(与 category mock 同源,原型阶段独立副本避免跨模块耦合) + * - key: 节点 id;value: 该节点的直接子节点 id 列表 + * - 仅承载"父→子"关系,不复制节点业务字段;树结构变更时同步此处即可 + */ +const CATEGORY_CHILDREN: Record = { + 'cat-catering': ['cat-catering-plate', 'cat-catering-cabinet', 'cat-catering-nutri-scale', 'cat-catering-ratio-scale', 'cat-catering-checkout', 'cat-catering-inbound-scale', 'cat-catering-band-screen', 'cat-catering-baifei'], + 'cat-body': ['cat-body-all-in-one', 'cat-body-bt-scale', 'cat-body-fat'], + 'cat-wearable': ['cat-wearable-hw5', 'cat-wearable-hw4', 'cat-wearable-ljf01'], + 'cat-fitness': ['cat-fitness-treadmill', 'cat-fitness-elliptical', 'cat-fitness-bike'], + 'cat-monitor': ['cat-monitor-sugar', 'cat-monitor-pressure', 'cat-monitor-all-in-one'], +} + +/** 收集 nodeId 子树下所有 id(含自身,支持任意层级递归) */ +const collectSubtreeIds = (nodeId: string): string[] => { + const result: string[] = [nodeId] + const stack = [...(CATEGORY_CHILDREN[nodeId] ?? [])] + while (stack.length) { + const id = stack.pop() as string + result.push(id) + stack.push(...(CATEGORY_CHILDREN[id] ?? [])) + } + return result +} + +/** 多维条件过滤(返回 DatasetRow[],未回填 categoryLabel) */ +const filterData = (params: SearchForm): DatasetRow[] => { + /** 选了分类节点时,预收集子树 id 集合,避免每条数据重复计算 */ + const categoryHitIds = params.categoryId ? new Set(collectSubtreeIds(params.categoryId)) : null return dataset.filter((item) => { - if (params.keyword) { - const kw = params.keyword - if (!includes(item.deviceCode, kw) && !includes(item.name, kw)) return false - } - if (params.category && item.category !== params.category) return false + if (categoryHitIds && !categoryHitIds.has(item.categoryId)) return false if (params.deployStatus && item.deployStatus !== params.deployStatus) return false if (params.runStatus && item.runStatus !== params.runStatus) return false if (params.isIot !== undefined && item.isIot !== params.isIot) return false - if (params.dataDispatch !== undefined && item.dataDispatch !== params.dataDispatch) return false - if (params.dataReport !== undefined && item.dataReport !== params.dataReport) return false - if (params.tenantName && !includes(item.tenantName, params.tenantName)) return false - if (params.siteName && !includes(item.siteName, params.siteName)) return false - if (params.softwareVersion && !includes(item.softwareVersion, params.softwareVersion)) return false + if (params.customerId && item.customerId !== params.customerId) return false + if (params.siteId && item.siteId !== params.siteId) return false + if (params.softwareVersion && item.softwareVersion !== params.softwareVersion) return false if (!inRange(item.onlineTime, params.onlineStart, params.onlineEnd)) return false if (!inRange(item.deployTime, params.deployStart, params.deployEnd)) return false return true }) } -/** 列表查询 */ +/** 列表查询:按 categoryId 回填末级分类名(模拟后端 join 分类表) */ const handleList = (ctx: MockContext): ApiResponse => { const params = (ctx.params ?? {}) as SearchForm const rows = filterData(params) - return ok('查询成功', rows) + const result: DeviceStatItem[] = rows.map((r) => ({ + ...r, + categoryLabel: CATEGORY_LEAF_NAME[r.categoryId] ?? '', + })) + return ok('查询成功', result) } /** 顶部统计卡:基于过滤后的数据集动态计算 */ @@ -239,8 +351,45 @@ const handleOverview = (ctx: MockContext): ApiResponse<{ overview: { key: string /** 异步导出:提交后立即返回任务已接收 */ const handleExport = (): ApiResponse => ok('导出任务已提交,可在"查看导出任务"中查看进度') +/** 所属客户下拉(支持关键字搜索) */ +const handleCustomerOptions = (ctx: MockContext): ApiResponse => { + const params = (ctx.params ?? {}) as { keyword?: string } + const kw = (params.keyword ?? '').toLowerCase() + const list = kw + ? customerOptionList.filter((item) => item.name.toLowerCase().includes(kw)) + : customerOptionList + return ok('查询成功', list) +} + +/** 所属场所下拉(按 customerId 联动 + 关键字搜索) */ +const handleSiteOptions = (ctx: MockContext): ApiResponse => { + const params = (ctx.params ?? {}) as { customerId?: string; keyword?: string } + if (!params.customerId) return ok('查询成功', []) + const kw = (params.keyword ?? '').toLowerCase() + const list = siteOptionList.filter( + (item) => + item.customerId === params.customerId && + (!kw || item.name.toLowerCase().includes(kw)), + ) + return ok('查询成功', list) +} + +/** 软件版本下拉(来自数据集 unique 值,支持关键字搜索) */ +const handleSoftwareVersionOptions = (ctx: MockContext): ApiResponse => { + const params = (ctx.params ?? {}) as { keyword?: string } + const kw = (params.keyword ?? '').toLowerCase() + const uniqueVersions = Array.from(new Set(dataset.map((item) => item.softwareVersion).filter(Boolean))) + const list = uniqueVersions + .filter((v) => !kw || v.toLowerCase().includes(kw)) + .map((v) => ({ value: v, label: v })) + return ok('查询成功', list) +} + registerMocks([ { method: 'POST', url: '/admin/device/statistics/list', handler: handleList }, { method: 'POST', url: '/admin/device/statistics/overview', handler: handleOverview }, { method: 'POST', url: '/admin/device/statistics/export', handler: handleExport }, + { method: 'POST', url: '/admin/device/statistics/customer/options', handler: handleCustomerOptions }, + { method: 'POST', url: '/admin/device/statistics/site/options', handler: handleSiteOptions }, + { method: 'POST', url: '/admin/device/statistics/software-version/options', handler: handleSoftwareVersionOptions }, ]) diff --git a/src/axios/handlers/device-center/customer/archive.ts b/src/axios/handlers/device-center/customer/archive.ts index e9c5cce..fe0c3ea 100644 --- a/src/axios/handlers/device-center/customer/archive.ts +++ b/src/axios/handlers/device-center/customer/archive.ts @@ -25,6 +25,14 @@ interface ArchiveRecord { status: boolean remark?: string createTime: string + /** 数据分发地址 */ + dispatchUrl: string + /** 鉴权账号 */ + authAccount: string + /** 鉴权秘钥 */ + authSecret: string + /** 鉴权接口地址 */ + authApiUrl: string } interface ArchiveParams { @@ -55,14 +63,14 @@ const paginate = (list: T[], params: ArchiveParams): Ap const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) const archiveList: ArchiveRecord[] = [ - { id: 'cust-001', code: 'CQ-001', name: '长庆能源总部食堂', type: 'enterprise', contactName: '王经理', contactPhone: '13900010001', email: 'wang@cq-energy.com', address: '西安市未央区凤城八路 1 号', expireTime: '2027-06-30', appCount: 5, siteCount: 3, status: true, remark: '集团总部客户', createTime: '2026-04-08 09:18:30' }, - { id: 'cust-002', code: 'OILF-01', name: '第一采油厂', type: 'enterprise', contactName: '刘主管', contactPhone: '13900010002', email: 'liu@oilfield-01.com', address: '延安市宝塔区河庄坪', expireTime: '2027-05-31', appCount: 4, siteCount: 2, status: true, createTime: '2026-04-14 10:24:48' }, - { id: 'cust-003', code: 'REF-01', name: '炼化分公司', type: 'enterprise', contactName: '陈主任', contactPhone: '13900010003', email: 'chen@refining.com', address: '兰州市西固区福利路 88 号', expireTime: '2026-12-31', appCount: 3, siteCount: 2, status: true, createTime: '2026-04-16 14:08:11' }, - { id: 'cust-004', code: 'HP-001', name: '健康示范园区', type: 'gov', contactName: '张医生', contactPhone: '13900010004', email: 'zhang@health-park.gov', address: '成都市高新区天府三街 199 号', expireTime: '2026-10-31', appCount: 6, siteCount: 2, status: false, remark: '园区示范项目', createTime: '2026-04-18 16:45:09' }, - { id: 'cust-005', code: 'EDU-001', name: '江南大学后勤处', type: 'edu', contactName: '李老师', contactPhone: '13900010005', email: 'li@jiangnan-edu.cn', address: '无锡市滨湖区蠡湖大道 1800 号', expireTime: '2027-09-01', appCount: 2, siteCount: 4, status: true, createTime: '2026-04-22 11:33:24' }, - { id: 'cust-006', code: 'MED-001', name: '市第一人民医院', type: 'medical', contactName: '孙主任', contactPhone: '13900010006', email: 'sun@first-hospital.com', address: '南京市鼓楼区中山路 321 号', expireTime: '2027-03-15', appCount: 3, siteCount: 1, status: true, createTime: '2026-04-25 09:12:46' }, - { id: 'cust-007', code: 'GOV-001', name: '市民政局养老服务科', type: 'gov', contactName: '周科长', contactPhone: '13900010007', email: 'zhou@civil-affairs.gov', address: '武汉市江岸区沿江大道 188 号', expireTime: '2027-08-31', appCount: 1, siteCount: 5, status: true, remark: '社区养老站点', createTime: '2026-04-28 15:47:18' }, - { id: 'cust-008', code: 'OTHER-001', name: '锦绣物业管理公司', type: 'other', contactName: '吴经理', contactPhone: '13900010008', email: 'wu@jinxiu-prop.com', address: '苏州市姑苏区干将东路 88 号', expireTime: '2026-11-30', appCount: 2, siteCount: 3, status: true, createTime: '2026-05-02 08:55:32' }, + { id: 'cust-001', code: 'CQ-001', name: '长庆能源总部食堂', type: 'enterprise', contactName: '王经理', contactPhone: '13900010001', email: 'wang@cq-energy.com', address: '西安市未央区凤城八路 1 号', expireTime: '2027-06-30', appCount: 5, siteCount: 3, status: true, remark: '集团总部客户', createTime: '2026-04-08 09:18:30', dispatchUrl: 'https://dispatch.cq-energy.com/v1/data', authAccount: 'cq_dispatch', authSecret: 'Cq#2026$Dispatch', authApiUrl: 'https://auth.cq-energy.com/v1/token' }, + { id: 'cust-002', code: 'OILF-01', name: '第一采油厂', type: 'enterprise', contactName: '刘主管', contactPhone: '13900010002', email: 'liu@oilfield-01.com', address: '延安市宝塔区河庄坪', expireTime: '2027-05-31', appCount: 4, siteCount: 2, status: true, createTime: '2026-04-14 10:24:48', dispatchUrl: 'https://dispatch.oilfield-01.com/v1/data', authAccount: 'oilf_dispatch', authSecret: 'Oilf#2026$Key', authApiUrl: 'https://auth.oilfield-01.com/v1/token' }, + { id: 'cust-003', code: 'REF-01', name: '炼化分公司', type: 'enterprise', contactName: '陈主任', contactPhone: '13900010003', email: 'chen@refining.com', address: '兰州市西固区福利路 88 号', expireTime: '2026-12-31', appCount: 3, siteCount: 2, status: true, createTime: '2026-04-16 14:08:11', dispatchUrl: 'https://dispatch.refining.com/v1/data', authAccount: 'ref_dispatch', authSecret: 'Ref#2026$Secret', authApiUrl: 'https://auth.refining.com/v1/token' }, + { id: 'cust-004', code: 'HP-001', name: '健康示范园区', type: 'gov', contactName: '张医生', contactPhone: '13900010004', email: 'zhang@health-park.gov', address: '成都市高新区天府三街 199 号', expireTime: '2026-10-31', appCount: 6, siteCount: 2, status: false, remark: '园区示范项目', createTime: '2026-04-18 16:45:09', dispatchUrl: 'https://dispatch.health-park.gov/v1/data', authAccount: 'hp_dispatch', authSecret: 'Hp#2026$Park', authApiUrl: 'https://auth.health-park.gov/v1/token' }, + { id: 'cust-005', code: 'EDU-001', name: '江南大学后勤处', type: 'edu', contactName: '李老师', contactPhone: '13900010005', email: 'li@jiangnan-edu.cn', address: '无锡市滨湖区蠡湖大道 1800 号', expireTime: '2027-09-01', appCount: 2, siteCount: 4, status: true, createTime: '2026-04-22 11:33:24', dispatchUrl: 'https://dispatch.jiangnan-edu.cn/v1/data', authAccount: 'edu_dispatch', authSecret: 'Edu#2026$Jn', authApiUrl: 'https://auth.jiangnan-edu.cn/v1/token' }, + { id: 'cust-006', code: 'MED-001', name: '市第一人民医院', type: 'medical', contactName: '孙主任', contactPhone: '13900010006', email: 'sun@first-hospital.com', address: '南京市鼓楼区中山路 321 号', expireTime: '2027-03-15', appCount: 3, siteCount: 1, status: true, createTime: '2026-04-25 09:12:46', dispatchUrl: 'https://dispatch.first-hospital.com/v1/data', authAccount: 'med_dispatch', authSecret: 'Med#2026$Hosp', authApiUrl: 'https://auth.first-hospital.com/v1/token' }, + { id: 'cust-007', code: 'GOV-001', name: '市民政局养老服务科', type: 'gov', contactName: '周科长', contactPhone: '13900010007', email: 'zhou@civil-affairs.gov', address: '武汉市江岸区沿江大道 188 号', expireTime: '2027-08-31', appCount: 1, siteCount: 5, status: true, remark: '社区养老站点', createTime: '2026-04-28 15:47:18', dispatchUrl: 'https://dispatch.civil-affairs.gov/v1/data', authAccount: 'gov_dispatch', authSecret: 'Gov#2026$Civil', authApiUrl: 'https://auth.civil-affairs.gov/v1/token' }, + { id: 'cust-008', code: 'OTHER-001', name: '锦绣物业管理公司', type: 'other', contactName: '吴经理', contactPhone: '13900010008', email: 'wu@jinxiu-prop.com', address: '苏州市姑苏区干将东路 88 号', expireTime: '2026-11-30', appCount: 2, siteCount: 3, status: true, createTime: '2026-05-02 08:55:32', dispatchUrl: 'https://dispatch.jinxiu-prop.com/v1/data', authAccount: 'other_dispatch', authSecret: 'Other#2026$Jx', authApiUrl: 'https://auth.jinxiu-prop.com/v1/token' }, ] /** 关键词搜索:编码 / 名称 / 联系人 */ @@ -115,6 +123,10 @@ const addArchive = (ctx: MockContext): ApiResponse => { status: typeof params.status === 'boolean' ? params.status : true, remark: params.remark ? String(params.remark) : undefined, createTime, + dispatchUrl: '', + authAccount: '', + authSecret: '', + authApiUrl: '', } archiveList.unshift(record) return ok('新增成功') diff --git a/src/axios/handlers/device-center/customer/site.ts b/src/axios/handlers/device-center/customer/site.ts index 679548e..1d2218a 100644 --- a/src/axios/handlers/device-center/customer/site.ts +++ b/src/axios/handlers/device-center/customer/site.ts @@ -3,27 +3,40 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 布点场所 mock 数据 - * - 场所与租户为 N:1 关系(同一租户可拥有多个场所) - * - customFlag 为运营自定义标识,用于数据中转寻址 / 同名场所区分 - * - clientHeartbeat / mqHeartbeat 为只读展示,由设备中转上报回填 + * - 场所与客户档案(archive)为 N:1 关系,同一客户可拥有多个场所 + * - customFlag 为运营自定义标识,用于数据中转寻址 / 同名场所区分(示例样式 A01A02A02) + * - hasFrontendServer 标识是否部署前置服务器,仅 true 时详情额外展示「数据上报配置」 */ interface SiteRecord { id: string name: string code: string - tenantId: string - tenantName: string + /** 所属客户 ID */ + customerId: string + /** 所属客户名称(联表回填) */ + customerName: string department?: string address?: string contactName?: string contactPhone?: string siteType: string customFlag: string - clientHeartbeat: 'online' | 'offline' - mqHeartbeat: 'online' | 'offline' + /** 是否部署前置服务器 */ + hasFrontendServer: boolean + /** 前置服务器状态(hasFrontendServer=true 时有意义) */ + frontendServerStatus?: 'online' | 'offline' + /** 最后心跳时间 */ + lastHeartbeatTime?: string + /** MQ 通讯地址 */ + mqAddress?: string + /** 业务服务请求地址 */ + businessServiceUrl?: string + /** 已挂载设备数 */ deviceCount: number + /** 启用状态 */ status: boolean + /** 创建时间 */ createTime: string } @@ -33,7 +46,7 @@ interface SiteParams { column?: string order?: 'ascend' | 'descend' | null keyword?: string - tenantId?: string + customerId?: string siteType?: string status?: boolean [key: string]: unknown @@ -57,52 +70,64 @@ const ok = (msg: string, data: T = null as T): ApiResponse => ({ co const siteList: SiteRecord[] = [ { - id: 'site-001', name: '长庆总部一食堂', code: 'CQ-CTN-01', tenantId: 'tenant-001', tenantName: '长庆能源总部食堂', + id: 'site-001', name: '长庆总部一食堂', code: 'CQ-CTN-01', customerId: 'cust-001', customerName: '长庆能源总部食堂', department: '总部后勤', address: '西安市未央区凤城八路 1 号', contactName: '王经理', contactPhone: '13900010001', - siteType: 'canteen', customFlag: 'CQ-HQ-CANTEEN-01', - clientHeartbeat: 'online', mqHeartbeat: 'online', deviceCount: 12, status: true, createTime: '2026-05-12 09:18:30', + siteType: 'canteen', customFlag: 'A01A02A01', + hasFrontendServer: true, frontendServerStatus: 'online', lastHeartbeatTime: '2026-07-08 09:18:30', + mqAddress: 'mq://10.10.1.21:61616', businessServiceUrl: 'http://10.10.1.22:8080/api', + deviceCount: 12, status: true, createTime: '2026-05-12 09:18:30', }, { - id: 'site-002', name: '采油一厂食堂', code: 'OILF-01-CTN', tenantId: 'tenant-002', tenantName: '第一采油厂', + id: 'site-002', name: '采油一厂食堂', code: 'OILF-01-CTN', customerId: 'cust-002', customerName: '第一采油厂', department: '采油厂综合办', address: '延安市宝塔区河庄坪', contactName: '刘主管', contactPhone: '13900010002', - siteType: 'canteen', customFlag: 'OILF-01-CANTEEN', - clientHeartbeat: 'online', mqHeartbeat: 'offline', deviceCount: 8, status: true, createTime: '2026-05-14 10:24:48', + siteType: 'canteen', customFlag: 'B02A03A02', + hasFrontendServer: true, frontendServerStatus: 'offline', lastHeartbeatTime: '2026-07-06 14:22:51', + mqAddress: 'mq://10.20.2.11:61616', businessServiceUrl: 'http://10.20.2.12:8080/api', + deviceCount: 8, status: true, createTime: '2026-05-14 10:24:48', }, { - id: 'site-003', name: '炼化员工餐厅', code: 'REF-DIN-01', tenantId: 'tenant-003', tenantName: '炼化分公司', + id: 'site-003', name: '炼化员工餐厅', code: 'REF-DIN-01', customerId: 'cust-003', customerName: '炼化分公司', department: '行政服务中心', address: '兰州市西固区福利路 88 号', contactName: '陈主任', contactPhone: '13900010003', - siteType: 'restaurant', customFlag: 'REF-DINING-01', - clientHeartbeat: 'offline', mqHeartbeat: 'offline', deviceCount: 6, status: true, createTime: '2026-05-16 14:08:11', + siteType: 'restaurant', customFlag: 'C03B02A01', + hasFrontendServer: false, + deviceCount: 6, status: true, createTime: '2026-05-16 14:08:11', }, { - id: 'site-004', name: '健康园区体检站', code: 'HP-CHK-01', tenantId: 'tenant-004', tenantName: '健康示范园区', + id: 'site-004', name: '健康园区体检站', code: 'HP-CHK-01', customerId: 'cust-004', customerName: '健康示范园区', department: '园区医务部', address: '成都市高新区天府三街 199 号', contactName: '张医生', contactPhone: '13900010004', - siteType: 'checkup-station', customFlag: 'HP-CHKUP-01', - clientHeartbeat: 'online', mqHeartbeat: 'online', deviceCount: 4, status: true, createTime: '2026-05-18 16:45:09', + siteType: 'checkup-station', customFlag: 'D04A01A03', + hasFrontendServer: true, frontendServerStatus: 'online', lastHeartbeatTime: '2026-07-08 10:05:14', + mqAddress: 'mq://10.30.3.31:61616', businessServiceUrl: 'http://10.30.3.32:8080/api', + deviceCount: 4, status: true, createTime: '2026-05-18 16:45:09', }, { - id: 'site-005', name: '总部体重测量点 A', code: 'CQ-MSR-A', tenantId: 'tenant-001', tenantName: '长庆能源总部食堂', + id: 'site-005', name: '总部体重测量点 A', code: 'CQ-MSR-A', customerId: 'cust-001', customerName: '长庆能源总部食堂', department: '总部后勤', address: '西安市未央区凤城八路 1 号 二楼', contactName: '李护士', contactPhone: '13900010005', - siteType: 'measure-point', customFlag: 'CQ-HQ-MEASURE-A', - clientHeartbeat: 'online', mqHeartbeat: 'online', deviceCount: 2, status: true, createTime: '2026-05-22 11:33:24', + siteType: 'measure-point', customFlag: 'A01B02A02', + hasFrontendServer: false, + deviceCount: 2, status: true, createTime: '2026-05-22 11:33:24', }, { - id: 'site-006', name: '园区员工健身房', code: 'HP-GYM-01', tenantId: 'tenant-004', tenantName: '健康示范园区', + id: 'site-006', name: '园区员工健身房', code: 'HP-GYM-01', customerId: 'cust-004', customerName: '健康示范园区', department: '园区工会', address: '成都市高新区天府三街 199 号 B 栋', contactName: '吴教练', contactPhone: '13900010006', - siteType: 'gym', customFlag: 'HP-GYM-01', - clientHeartbeat: 'offline', mqHeartbeat: 'online', deviceCount: 5, status: false, createTime: '2026-05-25 09:12:46', + siteType: 'gym', customFlag: 'D04C03A01', + hasFrontendServer: false, + deviceCount: 5, status: false, createTime: '2026-05-25 09:12:46', }, { - id: 'site-007', name: '采油一厂社区站', code: 'OILF-01-COM', tenantId: 'tenant-002', tenantName: '第一采油厂', + id: 'site-007', name: '采油一厂社区站', code: 'OILF-01-COM', customerId: 'cust-002', customerName: '第一采油厂', department: '矿区社区服务', address: '延安市宝塔区河庄坪社区', contactName: '孙社工', contactPhone: '13900010007', - siteType: 'community-station', customFlag: 'OILF-01-COMMUNITY', - clientHeartbeat: 'online', mqHeartbeat: 'online', deviceCount: 3, status: true, createTime: '2026-05-28 15:47:18', + siteType: 'community-station', customFlag: 'B02A04A01', + hasFrontendServer: true, frontendServerStatus: 'online', lastHeartbeatTime: '2026-07-08 08:51:02', + mqAddress: 'mq://10.20.4.21:61616', businessServiceUrl: 'http://10.20.4.22:8080/api', + deviceCount: 3, status: true, createTime: '2026-05-28 15:47:18', }, { - id: 'site-008', name: '炼化二食堂', code: 'REF-CTN-02', tenantId: 'tenant-003', tenantName: '炼化分公司', + id: 'site-008', name: '炼化二食堂', code: 'REF-CTN-02', customerId: 'cust-003', customerName: '炼化分公司', department: '行政服务中心', address: '兰州市西固区福利路 100 号', contactName: '周班长', contactPhone: '13900010008', - siteType: 'canteen', customFlag: 'REF-CANTEEN-02', - clientHeartbeat: 'online', mqHeartbeat: 'offline', deviceCount: 9, status: true, createTime: '2026-06-02 08:55:32', + siteType: 'canteen', customFlag: 'C03A02A02', + hasFrontendServer: false, + deviceCount: 9, status: true, createTime: '2026-06-02 08:55:32', }, ] @@ -116,7 +141,7 @@ const listSite = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = siteList.filter((item) => { if (!matchKeyword(item, params.keyword)) return false - if (params.tenantId && item.tenantId !== params.tenantId) return false + if (params.customerId && item.customerId !== params.customerId) return false if (params.siteType && item.siteType !== params.siteType) return false if (typeof params.status === 'boolean' && item.status !== params.status) return false return true @@ -130,32 +155,35 @@ const listSite = (ctx: MockContext): ApiResponse => { return paginate(filtered, params) } -/** 租户字典(与 tenant.ts mock 同源,用于 tenantId → tenantName 反查) */ -const tenantDict: Record = { - 'tenant-001': '长庆能源总部食堂', - 'tenant-002': '第一采油厂', - 'tenant-003': '炼化分公司', - 'tenant-004': '健康示范园区', +/** 客户档案字典(与 archive.ts mock 同源,用于 customerId → customerName 反查) */ +const customerDict: Record = { + 'cust-001': '长庆能源总部食堂', + 'cust-002': '第一采油厂', + 'cust-003': '炼化分公司', + 'cust-004': '健康示范园区', + 'cust-005': '江南大学后勤处', + 'cust-006': '市第一人民医院', + 'cust-007': '市民政局养老服务科', + 'cust-008': '锦绣物业管理公司', } const addSite = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const id = `site-${String(siteList.length + 1).padStart(3, '0')}` - const tenantId = String(params.tenantId ?? '') + const customerId = String(params.customerId ?? '') const record: SiteRecord = { id, name: String(params.name ?? ''), code: String(params.code ?? ''), - tenantId, - tenantName: params.tenantName ? String(params.tenantName) : (tenantDict[tenantId] ?? ''), + customerId, + customerName: params.customerName ? String(params.customerName) : (customerDict[customerId] ?? ''), department: params.department ? String(params.department) : undefined, address: params.address ? String(params.address) : undefined, contactName: params.contactName ? String(params.contactName) : undefined, contactPhone: params.contactPhone ? String(params.contactPhone) : undefined, siteType: String(params.siteType ?? ''), customFlag: String(params.customFlag ?? ''), - clientHeartbeat: 'offline', - mqHeartbeat: 'offline', + hasFrontendServer: false, deviceCount: 0, status: typeof params.status === 'boolean' ? params.status : true, createTime: new Date().toISOString().replace('T', ' ').slice(0, 19), @@ -171,10 +199,10 @@ const updateSite = (ctx: MockContext): ApiResponse => { if (!target) return ok('记录不存在', null) if (params.name !== undefined) target.name = String(params.name) if (params.code !== undefined) target.code = String(params.code) - if (params.tenantId !== undefined) { - const tid = String(params.tenantId) - target.tenantId = tid - target.tenantName = params.tenantName ? String(params.tenantName) : (tenantDict[tid] ?? target.tenantName) + if (params.customerId !== undefined) { + const cid = String(params.customerId) + target.customerId = cid + target.customerName = params.customerName ? String(params.customerName) : (customerDict[cid] ?? target.customerName) } target.department = params.department !== undefined ? String(params.department) : target.department target.address = params.address !== undefined ? String(params.address) : target.address diff --git a/src/axios/handlers/device-center/customer/supplier.ts b/src/axios/handlers/device-center/customer/supplier.ts index c41fc05..02dcbec 100644 --- a/src/axios/handlers/device-center/customer/supplier.ts +++ b/src/axios/handlers/device-center/customer/supplier.ts @@ -114,10 +114,26 @@ const updateRecord = (params: PageParams): ApiResponse => { return ok('更新成功') } +/** 供应商下拉(仅返回合作中的供应商,支持关键字搜索,供入库管理等模块复用) */ +interface SupplierOption { + id: string + name: string +} +const handleSupplierOptions = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const kw = (params.keyword ?? '').toLowerCase() + const list = supplierList + .filter((item) => item.cooperationStatus === 'active') + .map((item) => ({ id: item.id, name: item.name })) + .filter((item) => (kw ? item.name.toLowerCase().includes(kw) : true)) + return ok('查询成功', list) +} + registerMocks([ { method: 'POST', url: '/admin/device/customer/supplier/page', handler: listPage }, { method: 'POST', url: '/admin/device/customer/supplier/info', handler: (ctx) => infoById(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/customer/supplier/add', handler: (ctx) => addRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/customer/supplier/update', handler: (ctx) => updateRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/customer/supplier/delete', handler: (ctx) => deleteById(getParams(ctx.params)) }, + { method: 'POST', url: '/admin/device/customer/supplier/options', handler: handleSupplierOptions }, ]) diff --git a/src/axios/handlers/device-center/export-task.ts b/src/axios/handlers/device-center/export-task.ts new file mode 100644 index 0000000..b7bc327 --- /dev/null +++ b/src/axios/handlers/device-center/export-task.ts @@ -0,0 +1,93 @@ +import { registerMocks } from '@axios/mockBus' +import type { ApiResponse, MockContext, RequestParameter } from '@axios' + +/** + * 公共导出任务 mock 数据 + * + * 设计:所有模块的"查看导出任务"抽屉统一走 /admin/device/export-tasks/list 接口 + * 各模块通过 moduleKey 区分(sim / stock-inbound / stock-outbound / stock-instock / ...) + * mock 数据按 moduleKey 返回不同前缀的任务名,模拟真实业务隔离 + */ + +type ExportTaskStatus = 'pending' | 'processing' | 'success' | 'failed' + +interface ExportTaskRecord { + id: string + taskName: string + status: ExportTaskStatus + submitter: string + submitTime: string + finishTime: string + rowCount: number + fileSize: string + failReason?: string +} + +interface ExportTaskParams { + moduleKey?: string + [key: string]: unknown +} + +const getParams = (params: RequestParameter): ExportTaskParams => (params ?? {}) as ExportTaskParams + +const ok = (msg: string, data: T = null as T): ApiResponse => ({ + code: '00000', + msg, + data, +}) + +/** 各 moduleKey 对应的任务名前缀(用于 mock 数据展示) */ +const TASK_NAME_PREFIX: Record = { + sim: 'SIM卡列表', + arcsoft: '虹软授权列表', + 'stock-inbound': '入库单列表', + 'stock-outbound': '出库单列表', + 'stock-instock': '在库库存列表', + 'stock-inventory': '盘点清单列表', +} + +/** 各 moduleKey 对应的 mock 任务数据集 */ +const TASK_DATASET: Record = { + sim: [ + { id: 'exp-sim-001', taskName: 'SIM卡列表_20260702_093000', status: 'success', submitter: '张敏', submitTime: '2026-07-02 09:30:00', finishTime: '2026-07-02 09:30:12', rowCount: 8, fileSize: '24.6 KB' }, + { id: 'exp-sim-002', taskName: 'SIM卡列表_20260705_145200', status: 'processing', submitter: '李强', submitTime: '2026-07-05 14:52:00', finishTime: '—', rowCount: 0, fileSize: '—' }, + { id: 'exp-sim-003', taskName: 'SIM卡列表_20260708_081500', status: 'failed', submitter: '王勇', submitTime: '2026-07-08 08:15:00', finishTime: '2026-07-08 08:15:08', rowCount: 0, fileSize: '—', failReason: '导出超时,请缩小查询范围后重试' }, + ], + arcsoft: [ + { id: 'exp-arc-001', taskName: '虹软授权列表_20260702_101500', status: 'success', submitter: '张敏', submitTime: '2026-07-02 10:15:00', finishTime: '2026-07-02 10:15:09', rowCount: 8, fileSize: '21.3 KB' }, + { id: 'exp-arc-002', taskName: '虹软授权列表_20260705_163000', status: 'processing', submitter: '李强', submitTime: '2026-07-05 16:30:00', finishTime: '—', rowCount: 0, fileSize: '—' }, + { id: 'exp-arc-003', taskName: '虹软授权列表_20260708_092000', status: 'failed', submitter: '王勇', submitTime: '2026-07-08 09:20:00', finishTime: '2026-07-08 09:20:07', rowCount: 0, fileSize: '—', failReason: '查询条件超限,请缩小时间范围后重试' }, + ], + 'stock-inbound': [ + { id: 'exp-inb-001', taskName: '入库单列表_20260703_101500', status: 'success', submitter: '张敏', submitTime: '2026-07-03 10:15:00', finishTime: '2026-07-03 10:15:09', rowCount: 6, fileSize: '18.2 KB' }, + { id: 'exp-inb-002', taskName: '入库单列表_20260706_143000', status: 'processing', submitter: '李强', submitTime: '2026-07-06 14:30:00', finishTime: '—', rowCount: 0, fileSize: '—' }, + ], + 'stock-outbound': [ + { id: 'exp-out-001', taskName: '出库单列表_20260704_112000', status: 'success', submitter: '王勇', submitTime: '2026-07-04 11:20:00', finishTime: '2026-07-04 11:20:11', rowCount: 5, fileSize: '15.8 KB' }, + { id: 'exp-out-002', taskName: '出库单列表_20260707_090500', status: 'failed', submitter: '李强', submitTime: '2026-07-07 09:05:00', finishTime: '2026-07-07 09:05:07', rowCount: 0, fileSize: '—', failReason: '查询条件超限,请缩小时间范围后重试' }, + ], + 'stock-instock': [ + { id: 'exp-inst-001', taskName: '在库库存列表_20260705_160000', status: 'success', submitter: '张敏', submitTime: '2026-07-05 16:00:00', finishTime: '2026-07-05 16:00:08', rowCount: 12, fileSize: '32.5 KB' }, + ], + 'stock-inventory': [ + { id: 'exp-inv-001', taskName: '盘点清单列表_20260706_173000', status: 'success', submitter: '王勇', submitTime: '2026-07-06 17:30:00', finishTime: '2026-07-06 17:30:14', rowCount: 20, fileSize: '45.7 KB' }, + ], +} + +/** 列表查询:按 moduleKey 过滤;未识别的 moduleKey 返回空列表 */ +const listExportTasks = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const moduleKey = params.moduleKey ?? '' + const prefix = TASK_NAME_PREFIX[moduleKey] ?? '导出任务' + const dataset = TASK_DATASET[moduleKey] ?? [] + // 标准化:保证未配置前缀时也有合理的任务名展示 + const standardized = dataset.map((item) => ({ + ...item, + taskName: item.taskName.startsWith(prefix) ? item.taskName : `${prefix}_${item.id}`, + })) + return ok('查询成功', standardized) +} + +registerMocks([ + { method: 'POST', url: '/admin/device/export-tasks/list', handler: listExportTasks }, +]) diff --git a/src/axios/handlers/device-center/stock/inventory.ts b/src/axios/handlers/device-center/stock/inventory.ts index 4f98a1d..2fcea43 100644 --- a/src/axios/handlers/device-center/stock/inventory.ts +++ b/src/axios/handlers/device-center/stock/inventory.ts @@ -9,14 +9,16 @@ interface PageParams { keyword?: string status?: string inventoryType?: string - warehouseId?: string + warehouseAreaId?: string + id?: string + remark?: string + categoryIds?: string[] [key: string]: unknown } interface InventoryItem { - deviceCode: string - deviceName: string - modelName: string + categoryId: string + categoryName: string batchNo: string expectedQuantity: number actualQuantity: number @@ -26,8 +28,8 @@ interface InventoryItem { interface InventoryRecord { id: string inventoryCode: string - warehouseId: string - warehouseName: string + warehouseAreaId: string + warehouseAreaName: string inventoryType: 'full' | 'partial' operator: string inventoryTime: string @@ -55,28 +57,79 @@ const paginate = (list: T[], params: PageParams): Api return { code: '00000', msg: '查询成功', data: list.slice(start, start + pageSize), total: list.length } } -/** 生成应盘清单(mock:固定模板,每条记录复用) */ -const buildItems = (surplus: number, loss: number, unchecked: number, total: number): InventoryItem[] => { +/** 仓库区域末级节点 ID → 祖先链名称映射(与 warehouse mock 同源) */ +const WAREHOUSE_AREA_NAME: Record = { + 'wh-floor1-large-a-1': '1楼大型A号货架-1号储位', + 'wh-floor1-large-a-2': '1楼大型A号货架-2号储位', + 'wh-floor1-large-b-1': '1楼大型B号货架-1号储位', + 'wh-floor1-small-shelf-1': '1楼小型备品货架-1号储位', + 'wh-floor1-small-shelf-2': '1楼小型备品货架-2号储位', + 'wh-floor2-check-1': '2楼待检区-临时储位', +} + +const resolveWarehouseAreaName = (id?: string): string => (id ? WAREHOUSE_AREA_NAME[id] ?? '' : '') + +/** 设备类别末级 ID → 名称映射(与 statistics/category mock 同源) */ +const CATEGORY_LEAF_NAME: Record = { + 'cat-catering-plate': '吐盘机', + 'cat-catering-cabinet': '智能货柜', + 'cat-catering-nutri-scale': '营养秤', + 'cat-catering-ratio-scale': '配比秤', + 'cat-catering-checkout': '结算台', + 'cat-catering-inbound-scale': '入库秤', + 'cat-catering-band-screen': '带鱼屏', + 'cat-catering-baifei': '波菲炉', + 'cat-body-all-in-one': '一体机', + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-fitness-bike': '健身车', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +/** + * 生成应盘清单模板(按"末级分类 + 批次"聚合,mock 用固定模板) + * - 不传 selectedCategoryIds:全量盘点,从全部分类中循环取 + * - 传 selectedCategoryIds:部分盘点,仅按所选品类生成(每品类展开 1 条) + */ +const buildItems = ( + surplus: number, + loss: number, + unchecked: number, + total: number, + selectedCategoryIds?: string[], +): InventoryItem[] => { + const allCategoryIds = Object.keys(CATEGORY_LEAF_NAME) + const useCategoryIds = (selectedCategoryIds && selectedCategoryIds.length) + ? selectedCategoryIds + : allCategoryIds const items: InventoryItem[] = [] for (let i = 0; i < total; i++) { - let actual = 1 + const categoryId = useCategoryIds[i % useCategoryIds.length] + const expected = ((i + 1) % 5) + 1 + let actual = expected let result: InventoryItem['result'] = 'matched' if (i < surplus) { - actual = 2 + actual = expected + 1 result = 'surplus' } else if (i < surplus + loss) { - actual = 0 + actual = Math.max(0, expected - 1) result = 'loss' } else if (i < surplus + loss + unchecked) { actual = 0 result = 'unchecked' } items.push({ - deviceCode: `DEV-${String(i + 1).padStart(4, '0')}`, - deviceName: `设备${i + 1}`, - modelName: ['智能货柜', '蓝牙秤', '体脂仪', '入库秤'][i % 4], + categoryId, + categoryName: CATEGORY_LEAF_NAME[categoryId] ?? '', batchNo: `B2026-${String((i % 8) + 1).padStart(3, '0')}`, - expectedQuantity: 1, + expectedQuantity: expected, actualQuantity: actual, result, }) @@ -86,49 +139,49 @@ const buildItems = (surplus: number, loss: number, unchecked: number, total: num const inventoryList: InventoryRecord[] = [ { - id: 'inv-001', inventoryCode: 'PD-20260601-001', warehouseId: 'wh-floor1', warehouseName: '一楼设备仓库', + id: 'inv-001', inventoryCode: 'PD-20260601-001', warehouseAreaId: 'wh-floor1-large-a-1', warehouseAreaName: '1楼大型A号货架-1号储位', inventoryType: 'full', operator: '李工程师', inventoryTime: '2026-06-01 14:00:00', status: 'audited', expectedCount: 12, actualCount: 11, surplusCount: 0, lossCount: 1, auditor: '王主管', auditTime: '2026-06-01 18:30:00', - remark: '盘亏 1 台已转入工单核查', items: buildItems(0, 1, 0, 12), + remark: '盘亏 1 条已转入工单核查', items: buildItems(0, 1, 0, 12), }, { - id: 'inv-002', inventoryCode: 'PD-20260610-002', warehouseId: 'wh-floor2', warehouseName: '二楼备品仓库', + id: 'inv-002', inventoryCode: 'PD-20260610-002', warehouseAreaId: 'wh-floor1-small-shelf-1', warehouseAreaName: '1楼小型备品货架-1号储位', inventoryType: 'partial', operator: '张工程师', inventoryTime: '2026-06-10 10:15:00', status: 'audited', expectedCount: 8, actualCount: 9, surplusCount: 1, lossCount: 0, auditor: '王主管', auditTime: '2026-06-10 16:00:00', - remark: '盘盈 1 台入库登记', items: buildItems(1, 0, 0, 8), + remark: '盘盈 1 条入库登记', items: buildItems(1, 0, 0, 8), }, { - id: 'inv-003', inventoryCode: 'PD-20260620-003', warehouseId: 'wh-floor1', warehouseName: '一楼设备仓库', + id: 'inv-003', inventoryCode: 'PD-20260620-003', warehouseAreaId: 'wh-floor1-large-a-2', warehouseAreaName: '1楼大型A号货架-2号储位', inventoryType: 'full', operator: '李工程师', inventoryTime: '2026-06-20 15:30:00', status: 'completed', expectedCount: 15, actualCount: 13, surplusCount: 0, lossCount: 2, - remark: '待审核,盘亏 2 台待核查', items: buildItems(0, 2, 0, 15), + remark: '待审核,盘亏 2 条待核查', items: buildItems(0, 2, 0, 15), }, { - id: 'inv-004', inventoryCode: 'PD-20260625-004', warehouseId: 'wh-floor1-large', warehouseName: '大型设备区', + id: 'inv-004', inventoryCode: 'PD-20260625-004', warehouseAreaId: 'wh-floor1-large-b-1', warehouseAreaName: '1楼大型B号货架-1号储位', inventoryType: 'partial', operator: '陈工程师', inventoryTime: '2026-06-25 09:00:00', status: 'completed', expectedCount: 6, actualCount: 5, surplusCount: 1, lossCount: 2, remark: '待审核', items: buildItems(1, 2, 0, 6), }, { - id: 'inv-005', inventoryCode: 'PD-20260628-005', warehouseId: 'wh-floor2-check', warehouseName: '待检区', + id: 'inv-005', inventoryCode: 'PD-20260628-005', warehouseAreaId: 'wh-floor2-check-1', warehouseAreaName: '2楼待检区-临时储位', inventoryType: 'full', operator: '张工程师', inventoryTime: '2026-06-28 11:45:00', status: 'completed', expectedCount: 4, actualCount: 4, surplusCount: 0, lossCount: 0, remark: '本期盘点结果一致', items: buildItems(0, 0, 0, 4), }, { - id: 'inv-006', inventoryCode: 'PD-20260701-006', warehouseId: 'wh-floor1-small', warehouseName: '小型设备区', + 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', expectedCount: 10, actualCount: 0, surplusCount: 0, lossCount: 0, remark: '等待小程序扫码盘点', items: buildItems(0, 0, 10, 10), }, { - id: 'inv-007', inventoryCode: 'PD-20260703-007', warehouseId: 'wh-floor1-large-a', warehouseName: 'A号货架', + 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', expectedCount: 8, actualCount: 0, surplusCount: 0, lossCount: 0, remark: '本周新发起', items: buildItems(0, 0, 8, 8), }, { - id: 'inv-008', inventoryCode: 'PD-20260705-008', warehouseId: 'wh-floor2', warehouseName: '二楼备品仓库', + 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', expectedCount: 5, actualCount: 0, surplusCount: 0, lossCount: 0, items: buildItems(0, 0, 5, 5), @@ -140,7 +193,7 @@ const listPage = (ctx: MockContext): ApiResponse => { const filtered = inventoryList.filter((item) => { if (params.status && item.status !== params.status) return false if (params.inventoryType && item.inventoryType !== params.inventoryType) return false - if (params.warehouseId && item.warehouseId !== params.warehouseId) return false + if (params.warehouseAreaId && item.warehouseAreaId !== params.warehouseAreaId) return false if (params.keyword && !includes(item.inventoryCode, params.keyword)) return false return true }) @@ -162,19 +215,6 @@ const infoById = (params: PageParams): ApiResponse => { return ok('查询成功', JSON.parse(JSON.stringify(record))) } -/** 仓库下拉(mock 简化:固定 6 个常用仓库) */ -const warehouseOptions = (): ApiResponse> => { - const options = [ - { id: 'wh-floor1', name: '一楼设备仓库' }, - { id: 'wh-floor2', name: '二楼备品仓库' }, - { id: 'wh-floor1-large', name: '大型设备区' }, - { id: 'wh-floor1-small', name: '小型设备区' }, - { id: 'wh-floor2-check', name: '待检区' }, - { id: 'wh-floor1-large-a', name: 'A号货架' }, - ] - return ok('查询成功', options) -} - /** 生成盘点单号 */ const genInventoryCode = (): string => { const d = new Date() @@ -183,27 +223,43 @@ const genInventoryCode = (): string => { return `PD-${ymd}-${seq}` } -/** 新增(系统按仓库当前在库台账生成应盘清单,mock 用 8 条固定模板) */ +const nowStr = (): string => { + const d = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}` +} + +/** + * 新增(系统按区域当前在库台账生成应盘清单) + * - 全量盘点:按区域所有在库台账生成(mock 用 8 条固定模板) + * - 部分盘点:按所选品类过滤后生成(mock 用所选品类数量与固定模板组合) + */ const addRecord = (params: PageParams): ApiResponse => { - const expectedCount = 8 + const warehouseAreaId = String(params.warehouseAreaId ?? '') + const inventoryType = (params.inventoryType as InventoryRecord['inventoryType']) ?? 'full' + // 部分盘点:取所选品类 ID 列表(空数组降级为空清单提示) + const categoryIds = Array.isArray(params.categoryIds) ? params.categoryIds as string[] : [] + const expectedCount = inventoryType === 'partial' + ? Math.max(1, Math.min(categoryIds.length || 1, 8)) + : 8 const record: InventoryRecord = { id: `inv-${Date.now().toString(36)}`, inventoryCode: genInventoryCode(), - warehouseId: String(params.warehouseId ?? ''), - warehouseName: String(params.warehouseName ?? ''), - inventoryType: (params.inventoryType as InventoryRecord['inventoryType']) ?? 'full', + warehouseAreaId, + warehouseAreaName: resolveWarehouseAreaName(warehouseAreaId), + inventoryType, operator: '当前用户', - inventoryTime: new Date().toISOString().replace('T', ' ').slice(0, 19), + inventoryTime: nowStr(), status: 'draft', expectedCount, actualCount: 0, surplusCount: 0, lossCount: 0, remark: params.remark ? String(params.remark) : undefined, - items: buildItems(0, 0, expectedCount, expectedCount), + items: buildItems(0, 0, expectedCount, expectedCount, categoryIds), } inventoryList.unshift(record) - return ok('新增成功,已按仓库在库台账生成应盘清单') + return ok('新增成功,已按区域在库台账生成应盘清单') } /** 审核(已审核 → 库存台账以实盘数据为准) */ @@ -214,7 +270,7 @@ const auditRecord = (params: PageParams): ApiResponse => { if (record.status !== 'completed') return ok('仅已完成状态的盘点单可审核') record.status = 'audited' record.auditor = '当前用户' - record.auditTime = new Date().toISOString().replace('T', ' ').slice(0, 19) + record.auditTime = nowStr() return ok('审核完成,库存台账已按实盘数据修正') } @@ -235,5 +291,4 @@ registerMocks([ { method: 'POST', url: '/admin/device/stock/inventory/add', handler: (ctx) => addRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inventory/audit', handler: (ctx) => auditRecord(getParams(ctx.params)) }, { method: 'POST', url: '/admin/device/stock/inventory/delete', handler: (ctx) => deleteById(getParams(ctx.params)) }, - { method: 'POST', url: '/admin/device/stock/inventory/warehouse-options', handler: () => warehouseOptions() }, ]) diff --git a/src/axios/handlers/device-center/stock/warehouse.ts b/src/axios/handlers/device-center/stock/warehouse.ts index d073c13..9b45bb6 100644 --- a/src/axios/handlers/device-center/stock/warehouse.ts +++ b/src/axios/handlers/device-center/stock/warehouse.ts @@ -1,5 +1,6 @@ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext } from '@axios' +import { stockList } from '../asset/instock' interface WarehouseNode { id: string @@ -12,8 +13,8 @@ interface WarehouseNode { /** 父级名称(仅 info 回填时注入) */ parentName?: string sortOrder: number - /** 末级状态:仅叶子节点(无 children)有效 */ - status?: 'idle' | 'occupied' | 'maintenance' + /** 末级状态:仅叶子节点(无 children)有效;由在库库存反查注入 */ + status?: 'idle' | 'occupied' remark?: string children?: WarehouseNode[] createTime?: string @@ -28,7 +29,6 @@ interface WarehousePayload { parentId?: string parentName?: string sortOrder: number - status?: 'idle' | 'occupied' | 'maintenance' remark?: string } @@ -56,7 +56,9 @@ const TOP = { * * 字段规则: * - 任意节点通用:name / code / level / parentId / sortOrder / remark - * - 仅末级节点(无 children):携带 status + * - 仅末级节点(无 children):status 由在库库存(instock mock)反查注入 + * → 该末级储位存在 stockQty > 0 的在库记录 → 'occupied' + * → 否则 → 'idle' * - 入库时区域必须选末级 */ const warehouseTree: WarehouseNode[] = [ @@ -72,15 +74,15 @@ const warehouseTree: WarehouseNode[] = [ id: 'wh-floor1-large-a', name: 'A号货架', code: 'W01A01A01', level: 3, parentId: 'wh-floor1-large', sortOrder: 1, createTime: '2026-05-11 10:05:00', children: [ - { id: 'wh-floor1-large-a-1', name: '1号储位', code: 'W01A01A01A01', level: 4, parentId: 'wh-floor1-large-a', sortOrder: 1, status: 'idle', createTime: '2026-05-11 10:10:00' }, - { id: 'wh-floor1-large-a-2', name: '2号储位', code: 'W01A01A01A02', level: 4, parentId: 'wh-floor1-large-a', sortOrder: 2, status: 'occupied', createTime: '2026-05-11 10:15:00' }, + { id: 'wh-floor1-large-a-1', name: '1号储位', code: 'W01A01A01A01', level: 4, parentId: 'wh-floor1-large-a', sortOrder: 1, createTime: '2026-05-11 10:10:00' }, + { id: 'wh-floor1-large-a-2', name: '2号储位', code: 'W01A01A01A02', level: 4, parentId: 'wh-floor1-large-a', sortOrder: 2, createTime: '2026-05-11 10:15:00' }, ], }, { id: 'wh-floor1-large-b', name: 'B号货架', code: 'W01A01A02', level: 3, parentId: 'wh-floor1-large', sortOrder: 2, createTime: '2026-05-11 10:20:00', children: [ - { id: 'wh-floor1-large-b-1', name: '1号储位', code: 'W01A01A02A01', level: 4, parentId: 'wh-floor1-large-b', sortOrder: 1, status: 'idle', createTime: '2026-05-11 10:25:00' }, + { id: 'wh-floor1-large-b-1', name: '1号储位', code: 'W01A01A02A01', level: 4, parentId: 'wh-floor1-large-b', sortOrder: 1, createTime: '2026-05-11 10:25:00' }, ], }, ], @@ -93,8 +95,8 @@ const warehouseTree: WarehouseNode[] = [ id: 'wh-floor1-small-shelf', name: '备品货架', code: 'W01A02A01', level: 3, parentId: 'wh-floor1-small', sortOrder: 1, createTime: '2026-05-11 11:05:00', children: [ - { id: 'wh-floor1-small-shelf-1', name: '1号储位', code: 'W01A02A01A01', level: 4, parentId: 'wh-floor1-small-shelf', sortOrder: 1, status: 'occupied', createTime: '2026-05-11 11:10:00' }, - { id: 'wh-floor1-small-shelf-2', name: '2号储位', code: 'W01A02A01A02', level: 4, parentId: 'wh-floor1-small-shelf', sortOrder: 2, status: 'maintenance', createTime: '2026-05-11 11:15:00' }, + { id: 'wh-floor1-small-shelf-1', name: '1号储位', code: 'W01A02A01A01', level: 4, parentId: 'wh-floor1-small-shelf', sortOrder: 1, createTime: '2026-05-11 11:10:00' }, + { id: 'wh-floor1-small-shelf-2', name: '2号储位', code: 'W01A02A01A02', level: 4, parentId: 'wh-floor1-small-shelf', sortOrder: 2, createTime: '2026-05-11 11:15:00' }, ], }, ], @@ -109,13 +111,31 @@ const warehouseTree: WarehouseNode[] = [ id: 'wh-floor2-check', name: '待检区', code: 'W02A01', level: 2, parentId: TOP.floor2, sortOrder: 1, createTime: '2026-05-11 12:00:00', children: [ - { id: 'wh-floor2-check-1', name: '临时储位', code: 'W02A01A01', level: 3, parentId: 'wh-floor2-check', sortOrder: 1, status: 'idle', createTime: '2026-05-11 12:05:00' }, + { id: 'wh-floor2-check-1', name: '临时储位', code: 'W02A01A01', level: 3, parentId: 'wh-floor2-check', sortOrder: 1, createTime: '2026-05-11 12:05:00' }, ], }, ], }, ] +/** 末级节点状态反查:扫描在库库存,存在 stockQty > 0 记录即为 'occupied',否则 'idle' */ +const resolveLeafStatus = (leafId: string): 'idle' | 'occupied' => { + const hasStock = stockList.some((item) => item.warehouseAreaId === leafId && item.stockQty > 0) + return hasStock ? 'occupied' : 'idle' +} + +/** 递归注入末级状态(仅对叶子节点写入 status,非叶子节点不动) */ +const injectStatus = (nodes: WarehouseNode[]): WarehouseNode[] => { + for (const node of nodes) { + if (!node.children || node.children.length === 0) { + node.status = resolveLeafStatus(node.id) + } else { + injectStatus(node.children) + } + } + return nodes +} + /** 按关键字过滤树(保留命中节点的祖先链) */ const filterTree = (nodes: WarehouseNode[], keyword: string): WarehouseNode[] => { const lower = keyword.toLowerCase() @@ -164,9 +184,6 @@ const findNode = (nodes: WarehouseNode[], id: string): WarehouseNode | null => { return null } -/** 判断节点是否为叶子(无 children 或 children 为空) */ -const isLeafNode = (node: WarehouseNode): boolean => !node.children || node.children.length === 0 - /** 生成新 ID */ const genId = (prefix: string): string => `${prefix}-${Date.now().toString(36)}-${Math.random().toString(36).slice(2, 6)}` @@ -190,6 +207,8 @@ const genCode = (tree: WarehouseNode[], parentId?: string): string => { const handleTree = (ctx: MockContext): ApiResponse => { const params = (ctx.params ?? {}) as { keyword?: string } const data = cloneTree(warehouseTree) + /** 注入末级状态(按在库库存反查) */ + injectStatus(data) const result = params.keyword ? filterTree(data, params.keyword) : data return ok('查询成功', result) } @@ -211,8 +230,7 @@ const handleAdd = (ctx: MockContext): ApiResponse => { remark: payload.remark, createTime: now(), } - /** 末级节点(新建必然无子)携带状态,默认空闲 */ - newNode.status = payload.status ?? 'idle' + /** 末级节点状态由在库库存反查,新建时无在库记录即为 'idle'(不入库 status,等 tree 接口反查注入) */ if (payload.parentId) { const parent = findNode(warehouseTree, payload.parentId) @@ -234,10 +252,7 @@ const handleUpdate = (ctx: MockContext): ApiResponse => { node.name = payload.name node.sortOrder = payload.sortOrder node.remark = payload.remark - /** 末级节点才更新状态 */ - if (isLeafNode(node)) { - node.status = payload.status ?? 'idle' - } + /** 末级节点状态由在库库存反查,不接受前端编辑 */ return ok('更新成功') } @@ -252,6 +267,10 @@ const handleInfo = (ctx: MockContext): ApiResponse => { const parent = findNode(warehouseTree, node.parentId) result.parentName = parent?.name } + /** 末级节点注入反查状态 */ + if (!node.children || node.children.length === 0) { + result.status = resolveLeafStatus(node.id) + } return ok('查询成功', result) } diff --git a/src/axios/handlers/device-center/third-auth/amap.ts b/src/axios/handlers/device-center/third-auth/amap.ts index 89b1bde..99388a5 100644 --- a/src/axios/handlers/device-center/third-auth/amap.ts +++ b/src/axios/handlers/device-center/third-auth/amap.ts @@ -3,19 +3,28 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 高德地图应用 mock 数据 - * - 1 条记录 = 1 个高德开放平台应用(含 API Key + 安全密钥) + * - 1 条记录 = 1 个高德开放平台应用(含 API Key + 安全秘钥) * - key 不区分类型(Web服务 / JS API / 小程序等共用同一条记录) * - status 为启用/停用开关,停用后前端调用将拒绝加载地图 */ interface AmapRecord { id: string + /** 应用软件名称 */ name: string + /** 高德 API Key */ apiKey: string + /** 安全秘钥 */ securityCode?: string + /** 认证企业 */ + authenticatedCompany?: string + /** 用途说明 */ usage?: string + /** 启用状态 */ status: boolean + /** 备注 */ remark?: string + /** 创建时间 */ createTime: string } @@ -24,7 +33,8 @@ interface AmapParams { pageSize?: number column?: string order?: 'ascend' | 'descend' | null - keyword?: string + apiKey?: string + appName?: string status?: boolean [key: string]: unknown } @@ -46,26 +56,27 @@ const paginate = (list: T[], params: AmapParams): ApiRespo const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) const amapList: AmapRecord[] = [ - { id: 'amap-001', name: '园区地图导航服务', apiKey: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6', securityCode: 'yx-sec-2026-001', usage: '健康园区/食堂地图展示与路径规划', status: true, remark: '主应用,Web 端 JS API 调用', createTime: '2026-03-08 09:30:12' }, - { id: 'amap-002', name: '设备定位上报服务', apiKey: 'b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7', securityCode: 'yx-sec-2026-002', usage: '货柜/体检设备 GPS 定位回传', status: true, createTime: '2026-03-12 14:22:45' }, - { id: 'amap-003', name: '地址解析与逆地理编码', apiKey: 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8', securityCode: 'yx-sec-2026-003', usage: '用户地址 → 坐标 / 坐标 → 地址', status: true, remark: 'Web 服务 REST API', createTime: '2026-03-15 10:08:33' }, - { id: 'amap-004', name: '小程序地图组件', apiKey: 'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9', securityCode: 'yx-sec-2026-004', usage: 'C 端小程序内置地图与定位', status: true, createTime: '2026-03-20 16:45:09' }, - { id: 'amap-005', name: '食堂数据大屏地图', apiKey: 'e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0', securityCode: 'yx-sec-2026-005', usage: '运营大屏热力图与分布展示', status: false, remark: '大屏重构中,临时停用', createTime: '2026-04-02 11:18:27' }, - { id: 'amap-006', name: '巡检路径追踪服务', apiKey: 'f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1', securityCode: 'yx-sec-2026-006', usage: '运维小程序巡检轨迹记录与回放', status: true, createTime: '2026-04-10 08:55:41' }, - { id: 'amap-007', name: '行政区域查询服务', apiKey: 'a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2', securityCode: 'yx-sec-2026-007', usage: '租户档案省市县三级联动', status: true, createTime: '2026-05-18 13:40:55' }, - { id: 'amap-008', name: '健康园区打卡围栏', apiKey: 'b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3', usage: '体检站电子围栏与自动打卡', status: false, remark: '围栏方案调整,暂停使用', createTime: '2026-06-22 15:12:18' }, + { id: 'amap-001', name: '园区地图导航服务', apiKey: 'a1b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6', securityCode: 'yx-sec-2026-001', authenticatedCompany: '上海寰汐科技有限公司', usage: '健康园区/食堂地图展示与路径规划', status: true, remark: '主应用,Web 端 JS API 调用', createTime: '2026-03-08 09:30:12' }, + { id: 'amap-002', name: '设备定位上报服务', apiKey: 'b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7', securityCode: 'yx-sec-2026-002', authenticatedCompany: '上海寰汐科技有限公司', usage: '货柜/体检设备 GPS 定位回传', status: true, createTime: '2026-03-12 14:22:45' }, + { id: 'amap-003', name: '地址解析与逆地理编码', apiKey: 'c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8', securityCode: 'yx-sec-2026-003', authenticatedCompany: '上海寰汐科技有限公司', usage: '用户地址 → 坐标 / 坐标 → 地址', status: true, remark: 'Web 服务 REST API', createTime: '2026-03-15 10:08:33' }, + { id: 'amap-004', name: '小程序地图组件', apiKey: 'd4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9', securityCode: 'yx-sec-2026-004', authenticatedCompany: '杭州智养餐饮联盟', usage: 'C 端小程序内置地图与定位', status: true, createTime: '2026-03-20 16:45:09' }, + { id: 'amap-005', name: '食堂数据大屏地图', apiKey: 'e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0', securityCode: 'yx-sec-2026-005', authenticatedCompany: '杭州智养餐饮联盟', usage: '运营大屏热力图与分布展示', status: false, remark: '大屏重构中,临时停用', createTime: '2026-04-02 11:18:27' }, + { id: 'amap-006', name: '巡检路径追踪服务', apiKey: 'f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1', securityCode: 'yx-sec-2026-006', authenticatedCompany: '上海寰汐科技有限公司', usage: '运维小程序巡检轨迹记录与回放', status: true, createTime: '2026-04-10 08:55:41' }, + { id: 'amap-007', name: '行政区域查询服务', apiKey: 'a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2', securityCode: 'yx-sec-2026-007', authenticatedCompany: '杭州智养餐饮联盟', usage: '租户档案省市县三级联动', status: true, createTime: '2026-05-18 13:40:55' }, + { id: 'amap-008', name: '健康园区打卡围栏', apiKey: 'b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3', authenticatedCompany: '北京健康园区运营公司', usage: '体检站电子围栏与自动打卡', status: false, remark: '围栏方案调整,暂停使用', createTime: '2026-06-22 15:12:18' }, ] -/** 关键词搜索:应用名称 / API Key / 用途说明 */ -const matchKeyword = (item: AmapRecord, keyword?: string): boolean => { - if (!keyword) return true - return includes(item.name, keyword) || includes(item.apiKey, keyword) || includes(item.usage, keyword) -} +/** 高德 ApiKey 模糊匹配 */ +const matchApiKey = (item: AmapRecord, apiKey?: string): boolean => includes(item.apiKey, apiKey) + +/** 应用软件名称模糊匹配 */ +const matchAppName = (item: AmapRecord, appName?: string): boolean => includes(item.name, appName) const listAmap = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = amapList.filter((item) => { - if (!matchKeyword(item, params.keyword)) return false + if (!matchApiKey(item, params.apiKey)) return false + if (!matchAppName(item, params.appName)) return false if (typeof params.status === 'boolean' && item.status !== params.status) return false return true }) @@ -95,6 +106,7 @@ const addAmap = (ctx: MockContext): ApiResponse => { name: String(params.name ?? ''), apiKey: String(params.apiKey ?? ''), securityCode: params.securityCode ? String(params.securityCode) : undefined, + authenticatedCompany: params.authenticatedCompany ? String(params.authenticatedCompany) : undefined, usage: params.usage ? String(params.usage) : undefined, status: typeof params.status === 'boolean' ? params.status : true, remark: params.remark ? String(params.remark) : undefined, @@ -112,6 +124,7 @@ const updateAmap = (ctx: MockContext): ApiResponse => { if (params.name !== undefined) target.name = String(params.name) if (params.apiKey !== undefined) target.apiKey = String(params.apiKey) target.securityCode = params.securityCode !== undefined ? (params.securityCode ? String(params.securityCode) : undefined) : target.securityCode + target.authenticatedCompany = params.authenticatedCompany !== undefined ? (params.authenticatedCompany ? String(params.authenticatedCompany) : undefined) : target.authenticatedCompany target.usage = params.usage !== undefined ? (params.usage ? String(params.usage) : undefined) : target.usage if (typeof params.status === 'boolean') target.status = params.status target.remark = params.remark !== undefined ? (params.remark ? String(params.remark) : undefined) : target.remark diff --git a/src/axios/handlers/device-center/third-auth/arcsoft.ts b/src/axios/handlers/device-center/third-auth/arcsoft.ts index 6ce0901..eb1d3de 100644 --- a/src/axios/handlers/device-center/third-auth/arcsoft.ts +++ b/src/axios/handlers/device-center/third-auth/arcsoft.ts @@ -5,6 +5,8 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' * 虹软人脸授权 mock 数据 * - 1 条记录 = 1 台设备的激活码(每台设备硬件绑定一个 activeKey) * - appId / sdkKey / sdkSecret 为虹软开发者平台应用凭证(同租户共享,但激活后不可换绑设备) + * - 设备类别 deviceCategoryId 引用设备分类树(/admin/device/category/tree)的叶子节点 id + * - 到期时间 expireTime 为空字符串表示"永久有效"(大部分虹软授权为永久授权) * - 状态:未激活 inactive / 已激活 active / 已过期 expired / 已停用 disabled */ @@ -19,7 +21,12 @@ interface ArcRecord { deviceSn: string deviceName?: string deviceModel?: string + /** 设备类别 ID(引用设备分类树叶子节点 id) */ + deviceCategoryId: string + /** 设备类别名称(列表展示用,冗余存储) */ + deviceCategoryName?: string activateTime: string + /** 空字符串 = 永久有效 */ expireTime: string status: ArcStatus remark?: string @@ -31,8 +38,12 @@ interface ArcParams { pageSize?: number column?: string order?: 'ascend' | 'descend' | null - keyword?: string + activeKey?: string + deviceCategoryId?: string + deviceSn?: string + appId?: string status?: ArcStatus + id?: string [key: string]: unknown } @@ -53,26 +64,23 @@ const paginate = (list: T[], params: ArcParams): ApiRespons const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) const arcList: ArcRecord[] = [ - { id: 'arc-001', activeKey: 'ARC-ACT-CQ-001-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-001', deviceName: '总部一食堂货柜', deviceModel: 'SC-1080P', activateTime: '2026-04-10', expireTime: '2027-04-10', status: 'active', remark: '总部主入口人脸识别', createTime: '2026-04-10 09:18:30' }, - { id: 'arc-002', activeKey: 'ARC-ACT-CQ-002-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-002', deviceName: '总部二食堂货柜', deviceModel: 'SC-1080P', activateTime: '2026-04-12', expireTime: '2027-04-12', status: 'active', createTime: '2026-04-12 10:24:48' }, - { id: 'arc-003', activeKey: 'ARC-ACT-OILF-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-OILF-01', deviceName: '采油一厂食堂货柜', deviceModel: 'SC-720P', activateTime: '2026-04-15', expireTime: '2026-07-15', status: 'expired', remark: '授权过期,待续期', createTime: '2026-04-15 14:08:11' }, - { id: 'arc-004', activeKey: 'ARC-ACT-REF-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-REF-01', deviceName: '炼化员工餐厅货柜', deviceModel: 'SC-1080P', activateTime: '2026-04-18', expireTime: '2027-04-18', status: 'active', createTime: '2026-04-18 16:45:09' }, - { id: 'arc-005', activeKey: 'ARC-ACT-HP-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-HP-01', deviceName: '健康园区体检站终端', deviceModel: 'ST-1080P', activateTime: '2026-04-22', expireTime: '2027-04-22', status: 'active', createTime: '2026-04-22 11:33:24' }, - { id: 'arc-006', activeKey: 'ARC-ACT-HP-02-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-HP-02', deviceName: '园区健身房体测机', deviceModel: 'INB-1280', activateTime: '2026-04-25', expireTime: '2027-04-25', status: 'disabled', remark: '设备已下线,授权停用', createTime: '2026-04-25 09:12:46' }, - { id: 'arc-007', activeKey: 'ARC-ACT-MED-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-MED-01', deviceName: '市一医院营养秤终端', deviceModel: 'NS-1280', activateTime: '2026-04-28', expireTime: '2027-04-28', status: 'active', createTime: '2026-04-28 15:47:18' }, - { id: 'arc-008', activeKey: 'ARC-ACT-EDU-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-EDU-01', deviceName: '江南大学档口机', deviceModel: 'ST-1920P', activateTime: '', expireTime: '2027-08-31', status: 'inactive', remark: '设备入库未启用', createTime: '2026-05-02 08:55:32' }, + { id: 'arc-001', activeKey: 'ARC-ACT-CQ-001-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-001', deviceName: '总部一食堂货柜', deviceModel: 'SC-1080P', deviceCategoryId: 'cat-catering-cabinet', deviceCategoryName: '智能货柜', activateTime: '2026-04-10', expireTime: '', status: 'active', remark: '总部主入口人脸识别', createTime: '2026-04-10 09:18:30' }, + { id: 'arc-002', activeKey: 'ARC-ACT-CQ-002-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-002', deviceName: '总部二食堂货柜', deviceModel: 'SC-1080P', deviceCategoryId: 'cat-catering-cabinet', deviceCategoryName: '智能货柜', activateTime: '2026-04-12', expireTime: '', status: 'active', createTime: '2026-04-12 10:24:48' }, + { id: 'arc-003', activeKey: 'ARC-ACT-OILF-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-OILF-01', deviceName: '采油一厂食堂货柜', deviceModel: 'SC-720P', deviceCategoryId: 'cat-catering-cabinet', deviceCategoryName: '智能货柜', activateTime: '2026-04-15', expireTime: '2026-07-15', status: 'expired', remark: '授权过期,待续期', createTime: '2026-04-15 14:08:11' }, + { id: 'arc-004', activeKey: 'ARC-ACT-REF-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-REF-01', deviceName: '炼化员工餐厅体测机', deviceModel: 'AIO-V3', deviceCategoryId: 'cat-body-all-in-one', deviceCategoryName: '一体机', activateTime: '2026-04-18', expireTime: '', status: 'active', createTime: '2026-04-18 16:45:09' }, + { id: 'arc-005', activeKey: 'ARC-ACT-HP-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-HP-01', deviceName: '健康园区体检站终端', deviceModel: 'HM-Z1', deviceCategoryId: 'cat-monitor-all-in-one', deviceCategoryName: '智能健康一体机', activateTime: '2026-04-22', expireTime: '', status: 'active', createTime: '2026-04-22 11:33:24' }, + { id: 'arc-006', activeKey: 'ARC-ACT-HP-02-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-HP-02', deviceName: '园区健身房体测机', deviceModel: 'AIO-V3', deviceCategoryId: 'cat-body-all-in-one', deviceCategoryName: '一体机', activateTime: '2026-04-25', expireTime: '', status: 'disabled', remark: '设备已下线,授权停用', createTime: '2026-04-25 09:12:46' }, + { id: 'arc-007', activeKey: 'ARC-ACT-MED-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-MED-01', deviceName: '市一医院营养秤终端', deviceModel: 'NS-5KG', deviceCategoryId: 'cat-catering-nutri-scale', deviceCategoryName: '营养秤', activateTime: '2026-04-28', expireTime: '', status: 'active', createTime: '2026-04-28 15:47:18' }, + { id: 'arc-008', activeKey: 'ARC-ACT-EDU-01-2026', appId: '7JMH4F9x3KqYc8rN2L6tXbA1d', sdkKey: 'CZ8s4Fq9xR7nHm2Lp6Yv3KbN1aD5tX', sdkSecret: 'B3pK9rN2LqYc8rZ7mH4F6vXbA1dTs5T', deviceSn: 'CABINET-SN-EDU-01', deviceName: '江南大学档口机', deviceModel: 'BS-25IN', deviceCategoryId: 'cat-catering-band-screen', deviceCategoryName: '带鱼屏', activateTime: '', expireTime: '2027-08-31', status: 'inactive', remark: '设备入库未启用,短期试用授权', createTime: '2026-05-02 08:55:32' }, ] -/** 关键词搜索:激活码 / 设备 SN / 设备名称 */ -const matchKeyword = (item: ArcRecord, keyword?: string): boolean => { - if (!keyword) return true - return includes(item.activeKey, keyword) || includes(item.deviceSn, keyword) || includes(item.deviceName, keyword) -} - const listArc = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = arcList.filter((item) => { - if (!matchKeyword(item, params.keyword)) return false + if (params.activeKey && !includes(item.activeKey, params.activeKey)) return false + if (params.deviceCategoryId && item.deviceCategoryId !== params.deviceCategoryId) return false + if (params.deviceSn && !includes(item.deviceSn, params.deviceSn)) return false + if (params.appId && !includes(item.appId, params.appId)) return false if (params.status && item.status !== params.status) return false return true }) @@ -97,15 +105,23 @@ const addArc = (ctx: MockContext): ApiResponse => { const now = new Date() const pad = (n: number): string => String(n).padStart(2, '0') const createTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + /** + * SDK Secret 为敏感字段,前端不录入;后端按 AppID 从同租户已有应用凭证中自动填充。 + * 此处模拟该行为:从已有记录中按 appId 查 sdkSecret;查不到则留空字符串。 + */ + const appId = String(params.appId ?? '') + const existed = arcList.find((item) => item.appId === appId) const record: ArcRecord = { id: `arc-${Date.now().toString(36)}`, activeKey: String(params.activeKey ?? ''), - appId: String(params.appId ?? ''), + appId, sdkKey: String(params.sdkKey ?? ''), - sdkSecret: String(params.sdkSecret ?? ''), + sdkSecret: existed?.sdkSecret ?? '', deviceSn: String(params.deviceSn ?? ''), deviceName: params.deviceName ? String(params.deviceName) : undefined, deviceModel: params.deviceModel ? String(params.deviceModel) : undefined, + deviceCategoryId: String(params.deviceCategoryId ?? ''), + deviceCategoryName: params.deviceCategoryName ? String(params.deviceCategoryName) : undefined, activateTime: String(params.activateTime ?? ''), expireTime: String(params.expireTime ?? ''), status: (params.status as ArcStatus) ?? 'inactive', @@ -128,6 +144,12 @@ const updateArc = (ctx: MockContext): ApiResponse => { if (params.deviceSn !== undefined) target.deviceSn = String(params.deviceSn) target.deviceName = params.deviceName !== undefined ? (params.deviceName ? String(params.deviceName) : undefined) : target.deviceName target.deviceModel = params.deviceModel !== undefined ? (params.deviceModel ? String(params.deviceModel) : undefined) : target.deviceModel + if (params.deviceCategoryId !== undefined) { + target.deviceCategoryId = String(params.deviceCategoryId) + if (params.deviceCategoryName !== undefined) { + target.deviceCategoryName = params.deviceCategoryName ? String(params.deviceCategoryName) : undefined + } + } if (params.activateTime !== undefined) target.activateTime = String(params.activateTime) if (params.expireTime !== undefined) target.expireTime = String(params.expireTime) if (params.status !== undefined) target.status = params.status as ArcStatus @@ -143,10 +165,18 @@ const deleteArc = (ctx: MockContext): ApiResponse => { return ok('删除成功') } +/** 异步导出:立即返回任务已接收 */ +const exportArc = (): ApiResponse => ok('导出任务已提交,可在"查看导出任务"中查看进度') + +/** 批量导入:原型阶段直接返回成功 */ +const importArc = (): ApiResponse => ok('批量导入成功,共新增 0 条,更新 0 条') + registerMocks([ { method: 'POST', url: '/admin/device/third-auth/arcsoft/page', handler: listArc }, { method: 'POST', url: '/admin/device/third-auth/arcsoft/info', handler: infoArc }, { method: 'POST', url: '/admin/device/third-auth/arcsoft/add', handler: addArc }, { method: 'POST', url: '/admin/device/third-auth/arcsoft/update', handler: updateArc }, { method: 'POST', url: '/admin/device/third-auth/arcsoft/delete', handler: deleteArc }, + { method: 'POST', url: '/admin/device/third-auth/arcsoft/export', handler: exportArc }, + { method: 'POST', url: '/admin/device/third-auth/arcsoft/import', handler: importArc }, ]) diff --git a/src/axios/handlers/device-center/third-auth/sim.ts b/src/axios/handlers/device-center/third-auth/sim.ts new file mode 100644 index 0000000..5ae2092 --- /dev/null +++ b/src/axios/handlers/device-center/third-auth/sim.ts @@ -0,0 +1,259 @@ +import { registerMocks } from '@axios/mockBus' +import type { ApiResponse, MockContext, RequestParameter } from '@axios' + +/** + * SIM 卡管理 mock 数据 + * - 1 条记录 = 1 张 SIM 卡(ICCID 唯一) + * - 状态:未激活 unactivated / 正常 normal / 已欠费 arrears / 已停用 stopped + * - 绑定设备 SN:空表示未绑定,可走"绑定设备"操作;非空可走"解绑设备" + * - 设备分类别 deviceCategory:货柜 / 体检机 / 打卡机 等 + */ + +type SimStatus = 'unactivated' | 'normal' | 'arrears' | 'stopped' +type Carrier = 'mobile' | 'unicom' | 'telecom' | 'broadnet' + +interface SimRecord { + id: string + iccid: string + phoneNo: string + carrier: Carrier + packageType: string + status: SimStatus + boundDeviceSn: string + boundDeviceName?: string + /** 设备分类别 */ + deviceCategory?: string + activateTime: string + expireTime: string + remark?: string + createTime: string +} + +interface SimParams { + pageNum?: number + pageSize?: number + column?: string + order?: 'ascend' | 'descend' | null + keyword?: string + carrier?: Carrier + status?: SimStatus + id?: string + simId?: string + deviceSn?: string + [key: string]: unknown +} + +const getParams = (params: RequestParameter): SimParams => (params ?? {}) as SimParams + +const includes = (value: unknown, keyword: unknown): boolean => { + if (!keyword) return true + return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) +} + +const paginate = (list: T[], params: SimParams): ApiResponse => { + const pageNum = Number(params.pageNum ?? 1) + const pageSize = Number(params.pageSize ?? 30) + const start = (pageNum - 1) * pageSize + return { code: '00000', msg: '查询成功', data: list.slice(start, start + pageSize), total: list.length } +} + +const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) + +const simList: SimRecord[] = [ + { id: 'sim-001', iccid: '8986011224567890123', phoneNo: '13800138001', carrier: 'mobile', packageType: '物联30G/月', status: 'normal', boundDeviceSn: 'CABINET-SN-001', boundDeviceName: '总部一食堂货柜', deviceCategory: '智能货柜', activateTime: '2026-04-10', expireTime: '2027-04-10', remark: '总部主入口人脸识别货柜', createTime: '2026-04-10 09:18:30' }, + { id: 'sim-002', iccid: '8986011224567890124', phoneNo: '13800138002', carrier: 'mobile', packageType: '物联30G/月', status: 'normal', boundDeviceSn: 'CABINET-SN-002', boundDeviceName: '总部二食堂货柜', deviceCategory: '智能货柜', activateTime: '2026-04-12', expireTime: '2027-04-12', createTime: '2026-04-12 10:24:48' }, + { id: 'sim-003', iccid: '8986011724567890125', phoneNo: '16600138003', carrier: 'unicom', packageType: '物联50G/月', status: 'arrears', boundDeviceSn: 'CABINET-SN-OILF-01', boundDeviceName: '采油一厂食堂货柜', deviceCategory: '智能货柜', activateTime: '2026-04-15', expireTime: '2026-07-15', remark: '已欠费,待续费', createTime: '2026-04-15 14:08:11' }, + { id: 'sim-004', iccid: '8986011924567890126', phoneNo: '18900138004', carrier: 'telecom', packageType: '物联30G/月', status: 'normal', boundDeviceSn: 'CABINET-SN-REF-01', boundDeviceName: '炼化员工餐厅货柜', deviceCategory: '智能货柜', activateTime: '2026-04-18', expireTime: '2027-04-18', createTime: '2026-04-18 16:45:09' }, + { id: 'sim-005', iccid: '8986011824567890127', phoneNo: '13300138005', carrier: 'telecom', packageType: '语音100分钟+10G', status: 'stopped', boundDeviceSn: '', deviceCategory: '', activateTime: '2026-04-22', expireTime: '2027-04-22', remark: '设备下线,已停用保号', createTime: '2026-04-22 11:33:24' }, + { id: 'sim-006', iccid: '8986011924567890128', phoneNo: '18900138006', carrier: 'telecom', packageType: '物联30G/月', status: 'normal', boundDeviceSn: 'CABINET-SN-HP-01', boundDeviceName: '健康园区体检站终端', deviceCategory: '体检终端', activateTime: '2026-04-25', expireTime: '2027-04-25', createTime: '2026-04-25 09:12:46' }, + { id: 'sim-007', iccid: '8986011724567890129', phoneNo: '16600138007', carrier: 'unicom', packageType: '物联10G/月', status: 'unactivated', boundDeviceSn: '', deviceCategory: '', activateTime: '', expireTime: '2027-08-31', remark: '新购入待分配', createTime: '2026-05-02 08:55:32' }, + { id: 'sim-008', iccid: '8986040524567890130', phoneNo: '17000138008', carrier: 'broadnet', packageType: '物联20G/月', status: 'normal', boundDeviceSn: 'CABINET-SN-EDU-01', boundDeviceName: '江南大学档口机', deviceCategory: '档口机', activateTime: '2026-05-05', expireTime: '2027-05-05', createTime: '2026-05-05 13:20:09' }, +] + +/** 关键词搜索:ICCID / 手机号 / 设备 SN */ +const matchKeyword = (item: SimRecord, keyword?: string): boolean => { + if (!keyword) return true + return includes(item.iccid, keyword) || includes(item.phoneNo, keyword) || includes(item.boundDeviceSn, keyword) +} + +const listSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const filtered = simList.filter((item) => { + if (!matchKeyword(item, params.keyword)) return false + if (params.carrier && item.carrier !== params.carrier) return false + if (params.status && item.status !== params.status) return false + return true + }) + if (params.column === 'createTime' && params.order) { + filtered.sort((a, b) => { + const cmp = a.createTime.localeCompare(b.createTime) + return params.order === 'ascend' ? cmp : -cmp + }) + } + return paginate(filtered, params) +} + +const infoSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = String(params.id ?? '') + return ok('查询成功', simList.find((item) => item.id === id) ?? null) +} + +const addSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const now = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + const createTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + const record: SimRecord = { + id: `sim-${Date.now().toString(36)}`, + iccid: String(params.iccid ?? ''), + phoneNo: String(params.phoneNo ?? ''), + carrier: (params.carrier as Carrier) ?? 'mobile', + packageType: String(params.packageType ?? ''), + status: (params.status as SimStatus) ?? 'unactivated', + boundDeviceSn: '', + deviceCategory: '', + activateTime: String(params.activateTime ?? ''), + expireTime: String(params.expireTime ?? ''), + remark: params.remark ? String(params.remark) : undefined, + createTime, + } + simList.unshift(record) + return ok('新增成功') +} + +const updateSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = String(params.id ?? '') + const target = simList.find((item) => item.id === id) + if (!target) return ok('记录不存在', null) + if (params.iccid !== undefined) target.iccid = String(params.iccid) + if (params.phoneNo !== undefined) target.phoneNo = String(params.phoneNo) + if (params.carrier !== undefined) target.carrier = params.carrier as Carrier + if (params.packageType !== undefined) target.packageType = String(params.packageType) + if (params.activateTime !== undefined) target.activateTime = String(params.activateTime) + if (params.expireTime !== undefined) target.expireTime = String(params.expireTime) + if (params.status !== undefined) target.status = params.status as SimStatus + target.remark = params.remark !== undefined ? (params.remark ? String(params.remark) : undefined) : target.remark + return ok('更新成功') +} + +const deleteSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = String(params.id ?? '') + const idx = simList.findIndex((item) => item.id === id) + if (idx >= 0) simList.splice(idx, 1) + return ok('删除成功') +} + +/** 绑定设备:写入 boundDeviceSn / boundDeviceName / deviceCategory */ +const bindSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const simId = String(params.simId ?? '') + const target = simList.find((item) => item.id === simId) + if (!target) return ok('SIM 卡不存在', null) + if (target.boundDeviceSn) return ok('该 SIM 卡已绑定设备,请先解绑', null) + const deviceSn = String(params.deviceSn ?? '') + if (!deviceSn) return ok('请选择设备', null) + target.boundDeviceSn = deviceSn + target.boundDeviceName = params.deviceName ? String(params.deviceName) : deviceSn + if (params.deviceCategory) target.deviceCategory = String(params.deviceCategory) + return ok('绑定成功') +} + +/** 解绑设备:清空 boundDeviceSn / boundDeviceName / deviceCategory */ +const unbindSim = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const simId = String(params.simId ?? '') + const target = simList.find((item) => item.id === simId) + if (!target) return ok('SIM 卡不存在', null) + if (!target.boundDeviceSn) return ok('该 SIM 卡未绑定设备', null) + target.boundDeviceSn = '' + target.boundDeviceName = undefined + target.deviceCategory = '' + return ok('解绑成功') +} + +/** 异步导出:立即返回任务已接收 */ +const exportSim = (): ApiResponse => ok('导出任务已提交,可在"查看导出任务"中查看进度') + +/** 批量导入:原型阶段直接返回成功 */ +const importSim = (): ApiResponse => ok('批量导入成功,共新增 0 条,更新 0 条') + +/** 可绑定设备下拉(从已存在但未绑定的设备池里返回若干条) */ +const deviceOptionsSim = (): ApiResponse<{ label: string; value: string }[]> => { + const pool: { label: string; value: string }[] = [ + { label: 'CABINET-SN-003(东区食堂货柜)', value: 'CABINET-SN-003' }, + { label: 'CABINET-SN-004(健身房东区体测机)', value: 'CABINET-SN-004' }, + { label: 'CABINET-SN-005(健身房体测机)', value: 'CABINET-SN-005' }, + { label: 'CABINET-SN-006(仓库备用货柜)', value: 'CABINET-SN-006' }, + { label: 'CABINET-SN-007(采油二厂食堂货柜)', value: 'CABINET-SN-007' }, + ] + const used = new Set(simList.filter((item) => item.boundDeviceSn).map((item) => item.boundDeviceSn)) + return ok('查询成功', pool.filter((item) => !used.has(item.value))) +} + +/** 导出任务列表已迁移到公共 handler:handlers/device-center/export-task.ts + * - 统一接口:POST /admin/device/export-tasks/list { moduleKey: 'sim' } + * - 各业务模块通过 moduleKey 区分,mock 数据集中维护 + */ + +/** 绑定记录状态 */ +type BindRecordStatus = 'bound' | 'unbound' + +/** SIM 卡历史绑定记录(1 条 = 1 次绑定/解绑事件) */ +interface SimBindRecord { + id: string + /** 所属 SIM 卡 ID */ + simId: string + /** 绑定时间 */ + bindTime: string + /** 解绑时间(status='unbound' 时必有) */ + unbindTime?: string + /** 设备编码 */ + deviceSn: string + /** 设备名称 */ + deviceName: string + /** 设备分类别 */ + deviceCategory: string + /** 操作人 */ + operator: string + /** 状态:已绑定 / 已解绑 */ + status: BindRecordStatus +} + +/** 绑定记录池(按 simId 过滤后返回,单卡最多 2 条,总量 8 条) */ +const simBindRecords: SimBindRecord[] = [ + { id: 'br-001', simId: 'sim-001', bindTime: '2026-04-10 09:18:30', deviceSn: 'CABINET-SN-001', deviceName: '总部一食堂货柜', deviceCategory: '智能货柜', operator: '李晓', status: 'bound' }, + { id: 'br-002', simId: 'sim-001', bindTime: '2026-03-15 10:00:00', unbindTime: '2026-04-09 18:00:00', deviceSn: 'CABINET-SN-OLD-01', deviceName: '总部旧货柜(已下线)', deviceCategory: '智能货柜', operator: '李晓', status: 'unbound' }, + { id: 'br-003', simId: 'sim-002', bindTime: '2026-04-12 10:24:48', deviceSn: 'CABINET-SN-002', deviceName: '总部二食堂货柜', deviceCategory: '智能货柜', operator: '李晓', status: 'bound' }, + { id: 'br-004', simId: 'sim-003', bindTime: '2026-04-15 14:08:11', deviceSn: 'CABINET-SN-OILF-01', deviceName: '采油一厂食堂货柜', deviceCategory: '智能货柜', operator: '王磊', status: 'bound' }, + { id: 'br-005', simId: 'sim-004', bindTime: '2026-04-18 16:45:09', deviceSn: 'CABINET-SN-REF-01', deviceName: '炼化员工餐厅货柜', deviceCategory: '智能货柜', operator: '王磊', status: 'bound' }, + { id: 'br-006', simId: 'sim-005', bindTime: '2026-04-22 11:33:24', unbindTime: '2026-06-10 09:50:12', deviceSn: 'CABINET-SN-EDU-02', deviceName: '江南大学实训机', deviceCategory: '档口机', operator: '赵敏', status: 'unbound' }, + { id: 'br-007', simId: 'sim-006', bindTime: '2026-04-25 09:12:46', deviceSn: 'CABINET-SN-HP-01', deviceName: '健康园区体检站终端', deviceCategory: '体检终端', operator: '赵敏', status: 'bound' }, + { id: 'br-008', simId: 'sim-008', bindTime: '2026-05-05 13:20:09', deviceSn: 'CABINET-SN-EDU-01', deviceName: '江南大学档口机', deviceCategory: '档口机', operator: '李晓', status: 'bound' }, +] + +/** 查询某张 SIM 卡的绑定记录(按绑定时间倒序) */ +const listBindRecords = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const simId = String(params.simId ?? '') + const filtered = simBindRecords + .filter((item) => item.simId === simId) + .sort((a, b) => b.bindTime.localeCompare(a.bindTime)) + return ok('查询成功', filtered) +} + +registerMocks([ + { method: 'POST', url: '/admin/device/third-auth/sim/page', handler: listSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/info', handler: infoSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/add', handler: addSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/update', handler: updateSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/delete', handler: deleteSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/bind', handler: bindSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/unbind', handler: unbindSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/bind-records', handler: listBindRecords }, + { method: 'POST', url: '/admin/device/third-auth/sim/export', handler: exportSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/import', handler: importSim }, + { method: 'POST', url: '/admin/device/third-auth/sim/device-options', handler: deviceOptionsSim }, +]) diff --git a/src/axios/handlers/device-center/workbench/overview.ts b/src/axios/handlers/device-center/workbench/overview.ts index 0c1bc1f..8f86081 100644 --- a/src/axios/handlers/device-center/workbench/overview.ts +++ b/src/axios/handlers/device-center/workbench/overview.ts @@ -25,7 +25,7 @@ const handleOverview = (_ctx: MockContext): ApiResponse => { { key: 'monthAdd', label: '本月新增', value: 8, unit: '台', hint: '较上月 +33%', trend: 'up', icon: '🚀', color: 'purple' }, { key: 'todayData', label: '今日数据量', value: 18642, unit: '条', hint: '较昨日 +12%', trend: 'up', icon: '📈', color: 'blue' }, { key: 'todayDist', label: '今日分发量', value: 18260, unit: '条', hint: '较昨日 +10%', trend: 'up', icon: '🔄', color: 'green' }, - { key: 'tenant', label: '下游租户数', value: 24, unit: '家', hint: '本月新增 2 家', trend: 'up', icon: '🏢', color: 'orange' }, + { key: 'tenant', label: '下游客户数', value: 24, unit: '家', hint: '本月新增 2 家', trend: 'up', icon: '🏢', color: 'orange' }, { key: 'successRate', label: '分发成功率', value: 98, unit: '%', hint: '较昨日 +0.5%', trend: 'up', icon: '✅', color: 'green' }, ], /** 设备资产结构(按顶级分类) */ diff --git a/src/axios/handlers/index.ts b/src/axios/handlers/index.ts index d6bbc39..8ff74f0 100644 --- a/src/axios/handlers/index.ts +++ b/src/axios/handlers/index.ts @@ -46,6 +46,8 @@ import './device-center/asset/instock' import './device-center/customer/supplier' // 设备中心 - 库存管理 / 仓库区域(树形 CRUD,末级状态由入库出库联动) import './device-center/stock/warehouse' +// 设备中心 - 公共导出任务(按 moduleKey 区分:sim / stock-inbound / stock-outbound / stock-instock) +import './device-center/export-task' // 设备中心 - 库存管理 / 库存盘点(应盘清单 + 实盘结果 + 审核修正台账) import './device-center/stock/inventory' // 设备中心 - 设备运维 / 工单管理(含流转) @@ -74,6 +76,8 @@ import './device-center/distribute/deadletter' import './device-center/third-auth/arcsoft' // 设备中心 - 第三方授权 / 高德地图应用(API Key + 安全密钥) import './device-center/third-auth/amap' +// 设备中心 - 第三方授权 / SIM 管理(CRUD + 绑定/解绑设备 + 导入/导出) +import './device-center/third-auth/sim' // 实施运维小程序 - 工作台 + 告警(今日待办 / 本周统计 / 告警列表 + 详情 + 处置) import './miniprogram/ops/workbench' // 实施运维小程序 - 库存作业(收货入库 / 装机出库 / 库存盘点 / 库存查询) diff --git a/src/axios/index.ts b/src/axios/index.ts index a58eb48..d61e4c7 100644 --- a/src/axios/index.ts +++ b/src/axios/index.ts @@ -153,6 +153,9 @@ export const restfulRequest = ( export type { ApiResponse, HttpMethod, + MockContext, + MockHandler, + MockRoute, RequestHeaders, RequestParameter, ServiceType, diff --git a/src/components/DeviceCategoryPicker.vue b/src/components/DeviceCategoryPicker.vue new file mode 100644 index 0000000..0f0dc87 --- /dev/null +++ b/src/components/DeviceCategoryPicker.vue @@ -0,0 +1,117 @@ + + + diff --git a/src/components/ExportTaskDrawer.vue b/src/components/ExportTaskDrawer.vue new file mode 100644 index 0000000..65dce92 --- /dev/null +++ b/src/components/ExportTaskDrawer.vue @@ -0,0 +1,168 @@ + + + + + diff --git a/src/components/OpsBottomActions.vue b/src/components/OpsBottomActions.vue index 0f9109f..d16ca85 100644 --- a/src/components/OpsBottomActions.vue +++ b/src/components/OpsBottomActions.vue @@ -6,13 +6,13 @@ diff --git a/src/layouts/MiniprogramLayout.vue b/src/layouts/MiniprogramLayout.vue index b8cf76d..c187935 100644 --- a/src/layouts/MiniprogramLayout.vue +++ b/src/layouts/MiniprogramLayout.vue @@ -151,6 +151,14 @@ const onSwitchMp = ({ key }: { key: string }): void => { display: flex; flex-direction: column; position: relative; + /** + * 创建定位包含块:让 phone-shell 内所有 position:fixed 后代以 phone-shell 为参照, + * 而非浏览器视口。原因:本原型把 phone-shell(375×812) 居中放在桌面视口(如 1920×1080)里渲染, + * 默认 fixed 元素会跑到桌面视口边缘(如 OpsBottomActions 的 bottom:0 会贴视口底部, + * 而非 phone-shell 底部),导致按钮栏跑到手机壳外。 + * 真机 375 视口下 phone-shell 等同于视口,表现不变。 + */ + transform: translateZ(0); } .phone-shell__status { diff --git a/src/meta/nav.ts b/src/meta/nav.ts index a3891e0..fe34710 100644 --- a/src/meta/nav.ts +++ b/src/meta/nav.ts @@ -535,11 +535,18 @@ export const adminSections: NavSection[] = [ }, { key: 'admin-device-third-auth-amap', - label: '高德地图应用', + label: '高德地图授权', path: 'third-auth/amap', status: 'ready', component: () => import('@pages/admin-portal/device-center/third-auth/amap/amap.vue'), }, + { + key: 'admin-device-third-auth-sim', + label: 'SIM卡管理', + path: 'third-auth/sim', + status: 'ready', + component: () => import('@pages/admin-portal/device-center/third-auth/sim/sim.vue'), + }, ], }, ], diff --git a/src/pages/admin-portal/device-center/asset/category/category.vue b/src/pages/admin-portal/device-center/asset/category/category.vue index 5d525d0..6529537 100644 --- a/src/pages/admin-portal/device-center/asset/category/category.vue +++ b/src/pages/admin-portal/device-center/asset/category/category.vue @@ -69,6 +69,14 @@ {{ selectedNode.dataReport ? '是' : '否' }} + + + {{ selectedNode.faceRecognition ? '是' : '否' }} + + + + {{ networkModeLabel(selectedNode.networkMode) }} + diff --git a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/api/index.ts index 20b06d6..ddc350a 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/api/index.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/api/index.ts @@ -4,7 +4,7 @@ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { Form } from '../types' +import type { Form, SupplierOption } from '../types' /** 详情查询(编辑回填) */ export const info = (id: string): Promise> => @@ -17,3 +17,7 @@ export const add = (params: Record): Promise> /** 编辑 */ export const edit = (params: Record): Promise> => postRequest('axiosRequest', '/admin/device/stock/inbound/update', params) + +/** 供应商下拉(数据来源于供应商管理,可搜索) */ +export const supplierOptions = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/customer/supplier/options', { keyword }) diff --git a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/init/usePage.ts index 6d48ecf..189c9b7 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/init/usePage.ts @@ -1,29 +1,46 @@ /** * 入库管理 - 新增/编辑 弹框 - 主逻辑 + * - 设备明细子表:每行一台设备(deviceCode 必填、serialNumber 可选) + * - 批量录入:多行文本粘贴,按换行分隔,每行解析为 deviceCode[,serialNumber] + * - 文件导入:.txt/.csv 文本文件,按行解析同批量录入 + * - 数量字段自动同步为 items.length,不允许手动改 */ import type { FormInstance, Rule } from 'ant-design-vue/es/form' import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import { add, edit, info } from '../api' -import type { Form, ModalType, OpenContext, PageInfo } from '../types' +import { filterOption } from '@utils/antDesign/select' +import { add, edit, info, supplierOptions } from '../api' +import type { Form, ModalType, OpenContext, PageInfo, SupplierOption } from '../types' +import type { InboundDetailItem } from '../../../types' const createInitForm = (): Form => ({ id: undefined, inboundCode: '', - deviceCode: '', - deviceName: '', - modelName: '', + inboundType: 'purchase', + categoryId: undefined, batchNo: '', supplierId: '', supplierName: '', quantity: 1, purchasePrice: 0, - inboundType: 'purchase', + warehouseAreaId: undefined, status: 'completed', - storageLocation: '', remark: '', + items: [], }) +/** 解析批量录入文本:每行一条,支持逗号 / 制表符分隔 deviceCode 与 serialNumber */ +const parseBatchInput = (text: string): InboundDetailItem[] => { + const lines = text.split(/[\r\n]+/).map((line) => line.trim()).filter(Boolean) + return lines.map((line) => { + const [deviceCode, ...rest] = line.split(/[,\t]/).map((s) => s.trim()) + return { + deviceCode: deviceCode ?? '', + serialNumber: rest.filter(Boolean).join(',') || '', + } + }).filter((item) => item.deviceCode) +} + export const usePage = (emit: (e: 'load') => void) => { const { message } = useAntdStaticMethods() @@ -38,15 +55,26 @@ export const usePage = (emit: (e: 'load') => void) => { const formRef = ref() const form = ref
(createInitForm()) + /** 供应商下拉列表(数据来源于供应商管理) */ + const supplierOptionList = ref([]) + + const refreshSupplierOptions = (keyword?: string): void => { + supplierOptions(keyword) + .then((res) => { + if (res.code === '00000') supplierOptionList.value = res.data ?? [] + }) + .catch((err: unknown) => { + console.error('供应商下拉加载失败:', err) + }) + } + const rules: Record = { - deviceCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }], - deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], - modelName: [{ required: true, message: '请输入二级型号', trigger: 'blur' }], - batchNo: [{ required: true, message: '请输入批次号', trigger: 'blur' }], - supplierName: [{ required: true, message: '请输入供应商', trigger: 'blur' }], + inboundType: [{ required: true, message: '请选择入库类型', trigger: 'change' }], + categoryId: [{ required: true, message: '请选择设备类别', trigger: 'change' }], + supplierId: [{ required: true, message: '请选择供应商', trigger: 'change' }], quantity: [{ required: true, type: 'number', message: '请输入数量', trigger: 'blur' }], purchasePrice: [{ required: true, type: 'number', message: '请输入采购价', trigger: 'blur' }], - inboundType: [{ required: true, message: '请选择入库类型', trigger: 'change' }], + warehouseAreaId: [{ required: true, message: '请选择仓库区域', trigger: 'change' }], } const resetForm = (): void => { @@ -59,6 +87,92 @@ export const usePage = (emit: (e: 'load') => void) => { pageInfo.visible = false } + /** 同步数量字段 = items.length */ + const syncQuantity = (): void => { + form.value.quantity = Math.max(1, form.value.items.length) + } + + /** 新增空行 */ + const addRow = (): void => { + form.value.items.push({ deviceCode: '', serialNumber: '' }) + syncQuantity() + } + + /** 删除指定行 */ + const removeRow = (index: number): void => { + form.value.items.splice(index, 1) + syncQuantity() + } + + /** 批量录入(多行文本),追加解析后的条目 */ + const batchInputRef = ref<{ visible: boolean; text: string }>({ visible: false, text: '' }) + + const openBatchInput = (): void => { + batchInputRef.value.visible = true + batchInputRef.value.text = '' + } + + const cancelBatchInput = (): void => { + batchInputRef.value.visible = false + batchInputRef.value.text = '' + } + + const confirmBatchInput = (): void => { + const parsed = parseBatchInput(batchInputRef.value.text) + if (!parsed.length) { + void message.warning('未解析出有效设备编码,请按"每行一条"格式录入') + return + } + const existing = new Set(form.value.items.map((item) => item.deviceCode)) + let appended = 0 + for (const item of parsed) { + if (existing.has(item.deviceCode)) continue + form.value.items.push(item) + appended += 1 + } + syncQuantity() + cancelBatchInput() + void message.success(`已追加 ${appended} 条明细${appended < parsed.length ? `(去重 ${parsed.length - appended} 条)` : ''}`) + } + + /** 文件导入:读取文本文件后按批量录入解析 */ + const fileInputRef = ref(null) + + const triggerFileImport = (): void => { + fileInputRef.value?.click() + } + + const handleFileChange = (e: Event): void => { + const target = e.target as HTMLInputElement + const file = target.files?.[0] + if (!file) return + const reader = new FileReader() + reader.onload = () => { + const text = String(reader.result ?? '') + const parsed = parseBatchInput(text) + if (!parsed.length) { + void message.warning('文件未解析出有效设备编码') + target.value = '' + return + } + const existing = new Set(form.value.items.map((item) => item.deviceCode)) + let appended = 0 + for (const item of parsed) { + if (existing.has(item.deviceCode)) continue + form.value.items.push(item) + appended += 1 + } + syncQuantity() + void message.success(`已导入 ${appended} 条明细${appended < parsed.length ? `(去重 ${parsed.length - appended} 条)` : ''}`) + target.value = '' + } + reader.onerror = () => { + void message.error('文件读取失败') + target.value = '' + } + reader.readAsText(file) + } + const infoRequest = (id: string) => info(id) const openModal = (type: ModalType, ctx?: OpenContext): void => { @@ -66,6 +180,7 @@ export const usePage = (emit: (e: 'load') => void) => { pageInfo.spin = true pageInfo.type = type resetForm() + refreshSupplierOptions() pageInfo.title = type === 'add' ? '新增入库单' : '编辑入库单' @@ -129,6 +244,9 @@ export const usePage = (emit: (e: 'load') => void) => { ?.validate() .then(() => { const params = JSON.parse(JSON.stringify(form.value)) as Record + // 按 supplierId 从下拉列表反查 supplierName 同步(保留双字段以兼容既有列表展示与 mock 数据) + const supplier = supplierOptionList.value.find((item) => item.id === form.value.supplierId) + params.supplierName = supplier?.name ?? '' if (params.id) editRequest(params) else addRequest(params) }) @@ -143,8 +261,20 @@ export const usePage = (emit: (e: 'load') => void) => { formRef, form, rules, + supplierOptionList, + refreshSupplierOptions, + filterOption, + batchInputRef, + fileInputRef, openModal, closeModal, submit, + addRow, + removeRow, + openBatchInput, + cancelBatchInput, + confirmBatchInput, + triggerFileImport, + handleFileChange, } } diff --git a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/types/index.ts index 2641bfd..86a6621 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/types/index.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/component/modal/addOrEdit/types/index.ts @@ -1,5 +1,5 @@ import type { FormInstance } from 'ant-design-vue/es/form' -import type { InboundStatus, InboundType } from '../../../types' +import type { InboundDetailItem, InboundStatus, InboundType } from '../../../types' /** 弹框类型 */ export type ModalType = 'add' | 'edit' @@ -24,13 +24,11 @@ export interface Form { id: string | undefined /** 入库单号(编辑时只读) */ inboundCode: string - /** 设备编码 */ - deviceCode: string - /** 设备名称 */ - deviceName: string - /** 二级型号 */ - modelName: string - /** 批次号 */ + /** 入库类型 */ + inboundType: InboundType + /** 设备类别 ID(末级分类) */ + categoryId: string | undefined + /** 批次号(可选,回库场景可空) */ batchNo: string /** 供应商 ID */ supplierId: string @@ -40,14 +38,14 @@ export interface Form { quantity: number /** 采购价(单价) */ purchasePrice: number - /** 入库类型 */ - inboundType: InboundType + /** 仓库区域 ID(末级节点) */ + warehouseAreaId: string | undefined /** 状态 */ status: InboundStatus - /** 仓储位置 */ - storageLocation: string /** 备注 */ remark: string + /** 设备明细(每行一台设备,审核后生成台账) */ + items: InboundDetailItem[] } /** 暴露给父组件的 ref 类型 */ @@ -55,3 +53,9 @@ export type FormRef = ReturnType /** 仅用于类型推导的占位函数 */ declare function usePageRef(): FormInstance | undefined + +/** 供应商下拉选项(数据来源于供应商管理,可搜索) */ +export interface SupplierOption { + id: string + name: string +} diff --git a/src/pages/admin-portal/device-center/stock/inbound/inbound.vue b/src/pages/admin-portal/device-center/stock/inbound/inbound.vue index fff37e3..eefbe7d 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/inbound.vue +++ b/src/pages/admin-portal/device-center/stock/inbound/inbound.vue @@ -2,7 +2,13 @@
- + + + + + + + 导出 + + + 查看导出任务 + +
diff --git a/src/pages/admin-portal/device-center/stock/inbound/init/usePage.ts b/src/pages/admin-portal/device-center/stock/inbound/init/usePage.ts index a76be71..4306e8c 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/init/usePage.ts @@ -1,11 +1,13 @@ +import { useRouter } from 'vue-router' import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { useAdminTable } from '@pages/admin-portal/shared/useAdminTable' import { useAdminSearch } from '@pages/admin-portal/shared/useAdminSearch' -import { exportList, list, remove } from '../api' +import { audit, exportList, list, remove } from '../api' import { tableColumns } from './useTable' import { createSearchKey } from './useSearch' import type { ListItem, SearchForm } from '../types' import type AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' +import type ExportTaskDrawer from '@components/ExportTaskDrawer.vue' /** * 入库管理页主编排: @@ -14,10 +16,12 @@ import type AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' */ export const usePage = () => { const { message, Modal } = useAntdStaticMethods() + const router = useRouter() const { search, initOptions, resetSearch } = useAdminSearch(createSearchKey) const pageLoading = ref(false) const addOrEditRef = ref | null>(null) + const exportTaskRef = ref | null>(null) const buildParams = () => ({ ...search, @@ -105,10 +109,44 @@ export const usePage = () => { }) } + const viewExportTask = (): void => { + exportTaskRef.value?.openDrawer() + } + + /** 审核入库单:completed → audited,触发设备台账生成 */ + const auditRecord = (record: ListItem): void => { + Modal.confirm({ + title: '确认审核?', + content: `入库单「${record.inboundCode}」审核后将生成 ${record.quantity} 条设备台账,且不可撤销。`, + okText: '确认审核', + cancelText: '取消', + onOk: () => audit(record.id) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg || '审核成功') + listRequest() + } + }) + .catch((err: unknown) => { + console.error('入库单审核失败:', err) + }), + }) + } + + /** 查看设备台账:跳转到设备管理列表,按 inboundCode 过滤 */ + const viewDeviceLedger = (record: ListItem): void => { + void router.push({ + path: '/admin-portal/device-center/asset/manage', + query: { inboundCode: record.inboundCode }, + }) + } + const handleAction = (type: string, record?: ListItem): void => { if (type === 'add') openAddModal() else if (type === 'edit' && record) openEditModal(record) else if (type === 'delete' && record) deleteRecord(record) + else if (type === 'audit' && record) auditRecord(record) + else if (type === 'ledger' && record) viewDeviceLedger(record) } const loadData = (): void => { @@ -127,11 +165,15 @@ export const usePage = () => { selectedRowKeys, rowSelection, addOrEditRef, + exportTaskRef, dataSourceChange, searchQuery, resetQuery, handleAction, listRequest, exportRecord, + viewExportTask, + auditRecord, + viewDeviceLedger, } } diff --git a/src/pages/admin-portal/device-center/stock/inbound/init/useSearch.ts b/src/pages/admin-portal/device-center/stock/inbound/init/useSearch.ts index 6e9d6ba..5c2e994 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/init/useSearch.ts @@ -4,4 +4,7 @@ export const createSearchKey = (): SearchForm => ({ keyword: undefined, supplierId: undefined, status: undefined, + inboundType: undefined, + categoryId: undefined, + warehouseAreaId: undefined, }) diff --git a/src/pages/admin-portal/device-center/stock/inbound/init/useTable.ts b/src/pages/admin-portal/device-center/stock/inbound/init/useTable.ts index 62da602..c75f7d1 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/init/useTable.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/init/useTable.ts @@ -18,10 +18,8 @@ const typeText = (type: string): string => export const tableColumns: TableColumnsType = [ { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, { title: '入库单号', dataIndex: 'inboundCode', align: 'left', width: 170, fixed: 'left', resizable: true, ellipsis: true }, - { title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 150, resizable: true, ellipsis: true }, - { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 140, resizable: true, ellipsis: true }, - { title: '二级型号', dataIndex: 'modelName', align: 'left', width: 130, resizable: true, ellipsis: true }, - { title: '批次号', dataIndex: 'batchNo', align: 'left', width: 130, resizable: true, ellipsis: true }, + { title: '设备类别', dataIndex: 'categoryName', align: 'left', width: 150, resizable: true, ellipsis: true }, + { title: '批次号', dataIndex: 'batchNo', align: 'left', width: 130, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, { title: '供应商', dataIndex: 'supplierName', align: 'left', width: 180, resizable: true, ellipsis: true }, { title: '数量', dataIndex: 'quantity', align: 'center', width: 80 }, { title: '采购价(元)', dataIndex: 'purchasePrice', align: 'right', width: 110, customRender: ({ value }: { value: number }) => value?.toFixed(2) ?? '—' }, @@ -33,6 +31,7 @@ export const tableColumns: TableColumnsType = [ customRender: ({ value }: { value: InboundType }) => h(Tag, { color: INBOUND_TYPE_COLOR[value] ?? 'default' }, () => typeText(value)), }, + { title: '仓库区域', dataIndex: 'warehouseAreaName', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, { title: '状态', dataIndex: 'status', @@ -43,5 +42,5 @@ export const tableColumns: TableColumnsType = [ }, { title: '入库时间', dataIndex: 'inboundTime', align: 'center', width: 170, sorter: true, resizable: true }, { title: '入库人', dataIndex: 'operator', align: 'center', width: 110, resizable: true, ellipsis: true }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 240 }, ] diff --git a/src/pages/admin-portal/device-center/stock/inbound/types/index.ts b/src/pages/admin-portal/device-center/stock/inbound/types/index.ts index 4b0c211..c647869 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/types/index.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/types/index.ts @@ -1,13 +1,24 @@ import type { BaseListParams, BaseRecord, BaseSearchForm } from '@pages/admin-portal/shared/types' -/** 入库单状态 */ -export type InboundStatus = 'pending' | 'completed' | 'cancelled' +/** + * 入库单状态:pending 待入库 / completed 已入库(待审核)/ audited 已审核(已生成台账)/ cancelled 已取消 + * 注:completed 表示实物已到并扫码登记 SN,但尚未进入资产台账;audited 才是终态(正式生成设备台账) + */ +export type InboundStatus = 'pending' | 'completed' | 'audited' | 'cancelled' -/** 入库类型 */ +/** 入库类型:采购入库 / 回库 / 调拨入库(回库通过入库单类型实现,不再单独入口) */ export type InboundType = 'purchase' | 'return' | 'transfer' +/** 入库设备明细:审核后按行生成设备台账 */ +export interface InboundDetailItem { + /** 设备编码(必填,唯一) */ + deviceCode: string + /** 序列号 SN(可选) */ + serialNumber: string +} + export interface SearchForm extends BaseSearchForm { - /** 入库单号 / 设备编码 */ + /** 入库单号 */ keyword?: string /** 供应商 ID */ supplierId?: string @@ -15,19 +26,21 @@ export interface SearchForm extends BaseSearchForm { status?: string /** 入库类型 */ inboundType?: string + /** 设备类别(末级分类 ID) */ + categoryId?: string + /** 仓库区域(末级节点 ID) */ + warehouseAreaId?: string } export interface ListItem extends BaseRecord { /** 入库单号 */ inboundCode: string - /** 设备编码 */ - deviceCode: string - /** 设备名称 */ - deviceName: string - /** 二级型号 */ - modelName: string - /** 批次号 */ - batchNo: string + /** 设备类别 ID(末级分类) */ + categoryId: string + /** 设备类别名称(末级分类名,接口回填) */ + categoryName: string + /** 批次号(可选,回库场景可空) */ + batchNo?: string /** 供应商 ID */ supplierId: string /** 供应商名称 */ @@ -44,38 +57,52 @@ export interface ListItem extends BaseRecord { inboundType: InboundType /** 状态 */ status: InboundStatus - /** 仓储位置 */ - storageLocation?: string + /** 仓库区域 ID(末级节点) */ + warehouseAreaId?: string + /** 仓库区域名称(接口回填) */ + warehouseAreaName?: string /** 备注 */ remark?: string + /** 设备明细(已入库待审核时录入,审核后生成台账) */ + items?: InboundDetailItem[] } export interface ListParams extends BaseListParams, SearchForm {} export interface InboundPayload { id?: string - deviceCode: string - deviceName: string - modelName: string - batchNo: string - supplierId: string - supplierName: string - quantity: number - purchasePrice: number + /** 入库类型 */ inboundType: InboundType - storageLocation?: string + /** 设备类别 ID(末级分类) */ + categoryId: string + /** 批次号(可选) */ + batchNo?: string + /** 供应商 ID */ + supplierId: string + /** 供应商名称 */ + supplierName: string + /** 数量 */ + quantity: number + /** 采购价(单价) */ + purchasePrice: number + /** 仓库区域 ID(末级节点) */ + warehouseAreaId?: string + /** 备注 */ remark?: string + /** 设备明细(每行一台设备,审核后生成台账) */ + items?: InboundDetailItem[] } export const INBOUND_STATUS_OPTIONS = [ { value: 'pending', label: '待入库' }, - { value: 'completed', label: '已入库' }, + { value: 'completed', label: '已入库/待审核' }, + { value: 'audited', label: '已审核' }, { value: 'cancelled', label: '已取消' }, ] export const INBOUND_TYPE_OPTIONS = [ { value: 'purchase', label: '采购入库' }, - { value: 'return', label: '回库入库' }, + { value: 'return', label: '回库' }, { value: 'transfer', label: '调拨入库' }, ] @@ -87,6 +114,7 @@ export const INBOUND_TYPE_COLOR: Record = { export const INBOUND_STATUS_COLOR: Record = { pending: 'warning', - completed: 'success', + completed: 'processing', + audited: 'success', cancelled: 'default', } diff --git a/src/pages/admin-portal/device-center/stock/instock/api/index.ts b/src/pages/admin-portal/device-center/stock/instock/api/index.ts index f5dc108..83ab9ca 100644 --- a/src/pages/admin-portal/device-center/stock/instock/api/index.ts +++ b/src/pages/admin-portal/device-center/stock/instock/api/index.ts @@ -1,6 +1,6 @@ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { ListItem, ListParams, ReturnPayload } from '../types' +import type { ListItem, ListParams } from '../types' export const list = (params: ListParams): Promise> => postRequest('axiosRequest', '/admin/device/stock/instock/page', params) @@ -9,9 +9,5 @@ export const list = (params: ListParams): Promise> => export const info = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/stock/instock/info', { id }) -/** 回库登记 */ -export const returnGoods = (payload: ReturnPayload): Promise> => - postRequest('axiosRequest', '/admin/device/stock/instock/return', payload) - export const exportList = (params: ListParams): Promise> => postRequest('axiosRequest', '/admin/device/stock/instock/export', params) diff --git a/src/pages/admin-portal/device-center/stock/instock/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/stock/instock/component/drawer/info/info.vue index 77a5678..3f6433a 100644 --- a/src/pages/admin-portal/device-center/stock/instock/component/drawer/info/info.vue +++ b/src/pages/admin-portal/device-center/stock/instock/component/drawer/info/info.vue @@ -10,24 +10,19 @@ > - {{ detail.modelCode ?? '—' }} - {{ detail.modelName ?? '—' }} - {{ detail.typeName ?? '—' }} + {{ detail.categoryName ?? '—' }} {{ detail.batchNo ?? '—' }} {{ detail.supplierName ?? '—' }} - {{ detail.purchasePrice ?? '—' }} - {{ detail.inboundTime ?? '—' }} - {{ detail.storageLocation ?? '—' }} + {{ detail.warehouseAreaName ?? '—' }} {{ detail.stockQty ?? 0 }} - {{ detail.inTransitQty ?? 0 }} + {{ detail.lockedQty ?? 0 }} {{ detail.availableQty ?? 0 }} - {{ detail.shippedQty ?? 0 }} - - - {{ statusText(detail.stockStatus) }} + + + {{ healthText(detail.healthLevel) }} - {{ detail.updateTime ?? '—' }} + {{ detail.updateTime ?? '—' }} @@ -41,11 +36,11 @@ diff --git a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/api/index.ts b/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/api/index.ts deleted file mode 100644 index 472920a..0000000 --- a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/api/index.ts +++ /dev/null @@ -1,10 +0,0 @@ -/** - * 在库库存 - 回库登记 弹框 - 接口层 - */ - -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' - -/** 回库登记 */ -export const returnGoods = (params: Record): Promise> => - postRequest('axiosRequest', '/admin/device/stock/instock/return', params) diff --git a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/init/usePage.ts b/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/init/usePage.ts deleted file mode 100644 index a230e14..0000000 --- a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/init/usePage.ts +++ /dev/null @@ -1,92 +0,0 @@ -/** - * 在库库存 - 回库登记 弹框 - 主逻辑 - */ - -import type { FormInstance, Rule } from 'ant-design-vue/es/form' -import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import { returnGoods } from '../api' -import type { Form, OpenContext, PageInfo } from '../types' - -const createInitForm = (): Form => ({ - id: undefined, - returnQty: 1, - storageLocation: '', - remark: '', -}) - -export const usePage = (emit: (e: 'load') => void) => { - const { message } = useAntdStaticMethods() - - const pageInfo = reactive({ - visible: false, - title: '回库登记', - width: 520, - spin: false, - }) - - const formRef = ref() - const form = ref(createInitForm()) - - const rules: Record = { - returnQty: [{ required: true, type: 'number', message: '请输入回库数量', trigger: 'blur' }], - storageLocation: [{ required: true, message: '请输入仓储位置', trigger: 'blur' }], - } - - const resetForm = (): void => { - formRef.value?.clearValidate() - form.value = createInitForm() - } - - const closeModal = (): void => { - resetForm() - pageInfo.visible = false - } - - const openModal = (ctx: OpenContext): void => { - pageInfo.visible = true - pageInfo.spin = false - resetForm() - form.value.id = ctx.id - form.value.storageLocation = ctx.storageLocation ?? '' - pageInfo.title = ctx.modelName ? `回库登记 · ${ctx.modelName}` : '回库登记' - } - - const successRequest = (msg: string): void => { - void message.success(msg) - emit('load') - closeModal() - } - - const submit = (): void => { - pageInfo.spin = true - formRef.value - ?.validate() - .then(() => { - const params = JSON.parse(JSON.stringify(form.value)) as Record - returnGoods(params) - .then((res) => { - if (res.code === '00000') successRequest(res.msg) - }) - .catch((err: unknown) => { - console.error('回库登记失败:', err) - }) - .finally(() => { - pageInfo.spin = false - }) - }) - .catch((err: unknown) => { - pageInfo.spin = false - console.error('表单验证失败:', err) - }) - } - - return { - pageInfo, - formRef, - form, - rules, - openModal, - closeModal, - submit, - } -} diff --git a/src/pages/admin-portal/device-center/stock/instock/init/usePage.ts b/src/pages/admin-portal/device-center/stock/instock/init/usePage.ts index 6192d5c..0607cfe 100644 --- a/src/pages/admin-portal/device-center/stock/instock/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/instock/init/usePage.ts @@ -6,11 +6,12 @@ import { tableColumns } from './useTable' import { createSearchKey } from './useSearch' import type { ListItem, SearchForm } from '../types' import type InfoDrawer from '../component/drawer/info/info.vue' -import type ReturnGoodsModal from '../component/modal/returnGoods/returnGoods.vue' +import type ExportTaskDrawer from '@components/ExportTaskDrawer.vue' /** * 在库库存页主编排: - * - 列表(只读查询) + 详情抽屉 + 回库登记 + 导出 + * - 列表(只读查询) + 详情抽屉 + 导出 + * - 回库通过入库单类型=回库实现,本页不再单独提供回库登记入口 */ export const usePage = () => { const { message } = useAntdStaticMethods() @@ -18,7 +19,7 @@ export const usePage = () => { const pageLoading = ref(false) const infoDrawerRef = ref | null>(null) - const returnGoodsRef = ref | null>(null) + const exportTaskRef = ref | null>(null) const buildParams = () => ({ ...search, @@ -67,15 +68,7 @@ export const usePage = () => { } const openInfoDrawer = (record: ListItem): void => { - infoDrawerRef.value?.openDrawer({ id: record.id, title: record.modelName }) - } - - const openReturnModal = (record: ListItem): void => { - returnGoodsRef.value?.openModal({ - id: record.id, - modelName: record.modelName, - storageLocation: record.storageLocation, - }) + infoDrawerRef.value?.openDrawer({ id: record.id, title: record.categoryName }) } const exportRecord = (): void => { @@ -88,9 +81,12 @@ export const usePage = () => { }) } + const viewExportTask = (): void => { + exportTaskRef.value?.openDrawer() + } + const handleAction = (type: string, record?: ListItem): void => { if (type === 'info' && record) openInfoDrawer(record) - else if (type === 'return' && record) openReturnModal(record) } const loadData = (): void => { @@ -109,12 +105,13 @@ export const usePage = () => { selectedRowKeys, rowSelection, infoDrawerRef, - returnGoodsRef, + exportTaskRef, dataSourceChange, searchQuery, resetQuery, handleAction, listRequest, exportRecord, + viewExportTask, } } diff --git a/src/pages/admin-portal/device-center/stock/instock/init/useSearch.ts b/src/pages/admin-portal/device-center/stock/instock/init/useSearch.ts index fc8332b..4e4ca22 100644 --- a/src/pages/admin-portal/device-center/stock/instock/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/stock/instock/init/useSearch.ts @@ -2,6 +2,7 @@ import type { SearchForm } from '../types' export const createSearchKey = (): SearchForm => ({ keyword: undefined, - deviceType: undefined, - stockStatus: undefined, + categoryId: undefined, + warehouseAreaId: undefined, + healthLevel: undefined, }) diff --git a/src/pages/admin-portal/device-center/stock/instock/init/useTable.ts b/src/pages/admin-portal/device-center/stock/instock/init/useTable.ts index 7fdf369..ba13764 100644 --- a/src/pages/admin-portal/device-center/stock/instock/init/useTable.ts +++ b/src/pages/admin-portal/device-center/stock/instock/init/useTable.ts @@ -1,34 +1,29 @@ import type { TableColumnsType } from 'ant-design-vue' import { h } from 'vue' import { Tag } from 'ant-design-vue' -import type { ListItem, StockStatus } from '../types' -import { STOCK_STATUS_COLOR, STOCK_STATUS_OPTIONS } from '../types' +import type { ListItem, StockHealthLevel } from '../types' +import { STOCK_HEALTH_COLOR, STOCK_HEALTH_OPTIONS } from '../types' -const statusText = (status: string): string => - STOCK_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '—' +const healthText = (level: string): string => + STOCK_HEALTH_OPTIONS.find((item) => item.value === level)?.label ?? '—' export const tableColumns: TableColumnsType = [ { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, - { title: '型号编码', dataIndex: 'modelCode', align: 'left', width: 150, fixed: 'left', resizable: true, ellipsis: true }, - { title: '二级型号', dataIndex: 'modelName', align: 'left', width: 150, resizable: true, ellipsis: true }, - { title: '设备类型', dataIndex: 'typeName', align: 'center', width: 110, resizable: true, ellipsis: true }, - { title: '批次号', dataIndex: 'batchNo', align: 'left', width: 130, resizable: true, ellipsis: true }, - { title: '供应商', dataIndex: 'supplierName', align: 'left', width: 160, resizable: true, ellipsis: true }, - { title: '采购价(元)', dataIndex: 'purchasePrice', align: 'right', width: 110, sorter: true }, - { title: '入库时间', dataIndex: 'inboundTime', align: 'center', width: 170, sorter: true, resizable: true }, - { title: '仓储位置', dataIndex: 'storageLocation', align: 'left', width: 130, resizable: true, ellipsis: true }, + { title: '设备类别', dataIndex: 'categoryName', align: 'left', width: 160, fixed: 'left', resizable: true, ellipsis: true }, + { title: '批次号', dataIndex: 'batchNo', align: 'left', width: 140, resizable: true, ellipsis: true }, + { title: '供应商', dataIndex: 'supplierName', align: 'left', width: 170, resizable: true, ellipsis: true }, + { title: '仓库区域', dataIndex: 'warehouseAreaName', align: 'left', width: 170, resizable: true, ellipsis: true }, { title: '库存数量', dataIndex: 'stockQty', align: 'center', width: 100, sorter: true }, - { title: '在途数量', dataIndex: 'inTransitQty', align: 'center', width: 100 }, - { title: '已出库数量', dataIndex: 'shippedQty', align: 'center', width: 110 }, + { title: '锁定数量', dataIndex: 'lockedQty', align: 'center', width: 100 }, { title: '可用数量', dataIndex: 'availableQty', align: 'center', width: 100, sorter: true }, { - title: '库存状态', - dataIndex: 'stockStatus', + title: '库存健康度', + dataIndex: 'healthLevel', align: 'center', - width: 100, - customRender: ({ value }: { value: StockStatus }) => - h(Tag, { color: STOCK_STATUS_COLOR[value] ?? 'default' }, () => statusText(value)), + width: 110, + customRender: ({ value }: { value: StockHealthLevel }) => + h(Tag, { color: STOCK_HEALTH_COLOR[value] ?? 'default' }, () => healthText(value)), }, { title: '更新时间', dataIndex: 'updateTime', align: 'center', width: 170, resizable: true }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 120 }, ] diff --git a/src/pages/admin-portal/device-center/stock/instock/instock.vue b/src/pages/admin-portal/device-center/stock/instock/instock.vue index 407838b..d19fab1 100644 --- a/src/pages/admin-portal/device-center/stock/instock/instock.vue +++ b/src/pages/admin-portal/device-center/stock/instock/instock.vue @@ -1,26 +1,23 @@ - + diff --git a/src/pages/admin-portal/device-center/stock/instock/types/index.ts b/src/pages/admin-portal/device-center/stock/instock/types/index.ts index 011849a..ebea531 100644 --- a/src/pages/admin-portal/device-center/stock/instock/types/index.ts +++ b/src/pages/admin-portal/device-center/stock/instock/types/index.ts @@ -1,83 +1,56 @@ import type { BaseListParams, BaseRecord, BaseSearchForm } from '@pages/admin-portal/shared/types' -/** 库存状态:在库 / 在途 / 已出库 / 已回库 */ -export type StockStatus = 'instock' | 'inbound' | 'shipped' | 'returned' +/** 库存健康度:充足 / 断货预警 / 暂无库存(由后端按可用数量与阈值规则计算) */ +export type StockHealthLevel = 'sufficient' | 'warning' | 'out_of_stock' export interface SearchForm extends BaseSearchForm { + /** 批次号 / 供应商 关键字 */ keyword?: string - deviceType?: string - stockStatus?: string + /** 设备类别(末级分类 ID) */ + categoryId?: string + /** 仓库区域(末级节点 ID) */ + warehouseAreaId?: string + /** 库存健康度 */ + healthLevel?: string } export interface ListItem extends BaseRecord { - /** 型号编码 */ - modelCode: string - /** 二级型号 */ - modelName: string - /** 设备类型 */ - deviceType: string - /** 设备类型中文名 */ - typeName: string + /** 设备类别 ID(末级分类) */ + categoryId: string + /** 设备类别名称(末级分类名,接口回填) */ + categoryName: string /** 批次号 */ batchNo: string - /** 供应商 */ + /** 供应商 ID */ + supplierId: string + /** 供应商名称 */ supplierName: string - /** 采购价(单价) */ - purchasePrice: number - /** 入库时间 */ - inboundTime: string - /** 仓储位置 */ - storageLocation: string - /** 库存状态 */ - stockStatus: StockStatus - /** 库存数量 */ + /** 仓库区域 ID(末级节点) */ + warehouseAreaId?: string + /** 仓库区域名称(接口回填) */ + warehouseAreaName?: string + /** 库存数量(在库台账数量) */ stockQty: number - /** 在途数量 */ - inTransitQty: number - /** 可用数量 */ + /** 锁定数量(待出库单已占未发) */ + lockedQty: number + /** 可用数量(=库存数量 - 锁定数量,由后端计算) */ availableQty: number - /** 已出库数量 */ - shippedQty: number + /** 库存健康度 */ + healthLevel: StockHealthLevel /** 最近更新时间 */ updateTime: string } export interface ListParams extends BaseListParams, SearchForm {} -/** 回库登记 payload */ -export interface ReturnPayload { - id: string - /** 回库数量 */ - returnQty: number - /** 入库后仓储位置 */ - storageLocation: string - /** 备注 */ - remark?: string -} - -export const DEVICE_TYPE_OPTIONS = [ - { value: 'screen', label: '大屏' }, - { value: 'cabinet', label: '餐盘柜' }, - { value: 'shelf', label: '货架' }, - { value: 'scale-inbound', label: '入库秤' }, - { value: 'scale-ratio', label: '配比秤' }, - { value: 'scale-weight', label: '体重秤' }, - { value: 'scale-fat', label: '体脂秤' }, - { value: 'watch', label: '手表' }, - { value: 'fitness', label: '健身器材' }, - { value: 'checkup', label: '体检机' }, +export const STOCK_HEALTH_OPTIONS = [ + { value: 'sufficient', label: '充足' }, + { value: 'warning', label: '断货预警' }, + { value: 'out_of_stock', label: '暂无库存' }, ] -export const STOCK_STATUS_OPTIONS = [ - { value: 'instock', label: '在库' }, - { value: 'inbound', label: '在途' }, - { value: 'shipped', label: '已出库' }, - { value: 'returned', label: '已回库' }, -] - -export const STOCK_STATUS_COLOR: Record = { - instock: 'success', - inbound: 'processing', - shipped: 'default', - returned: 'warning', +export const STOCK_HEALTH_COLOR: Record = { + sufficient: 'success', + warning: 'warning', + out_of_stock: 'default', } 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 b148d15..6077285 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 @@ -10,7 +10,7 @@ export const list = (params: ListParams): Promise> => export const info = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/stock/inventory/info', { id }) -/** 新增盘点单(选仓库 + 类型,系统自动从在库库存生成应盘清单) */ +/** 新增盘点单(选仓库区域 + 类型,系统自动从在库库存生成应盘清单) */ export const add = (params: InventoryPayload): Promise> => postRequest('axiosRequest', '/admin/device/stock/inventory/add', params) @@ -21,7 +21,3 @@ export const audit = (id: string): Promise> => /** 删除盘点单(仅草稿状态可删) */ export const remove = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/stock/inventory/delete', { id }) - -/** 查询仓库区域下拉数据(用于新增弹框选仓库) */ -export const warehouseOptions = (): Promise>> => - postRequest('axiosRequest', '/admin/device/stock/inventory/warehouse-options', {}) diff --git a/src/pages/admin-portal/device-center/stock/inventory/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/stock/inventory/component/drawer/info/info.vue index e2d2973..a0a94fe 100644 --- a/src/pages/admin-portal/device-center/stock/inventory/component/drawer/info/info.vue +++ b/src/pages/admin-portal/device-center/stock/inventory/component/drawer/info/info.vue @@ -16,7 +16,7 @@ {{ statusLabel(detail.status) }} - {{ detail.warehouseName ?? '—' }} + {{ detail.warehouseAreaName ?? '—' }} {{ typeLabel(detail.inventoryType) }} @@ -30,10 +30,10 @@ - + - + @@ -49,7 +49,7 @@ :columns="itemColumns" :data-source="detail.items ?? []" :pagination="false" - row-key="deviceCode" + row-key="categoryId" size="small" :scroll="{ y: 360 }" > @@ -73,7 +73,7 @@ diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/init/usePage.ts new file mode 100644 index 0000000..58f78ea --- /dev/null +++ b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/init/usePage.ts @@ -0,0 +1,53 @@ +/** + * 出库管理 - 详情抽屉 - 主逻辑(只读) + * - 展示出库单全字段 + 节点变更时间(出库时间 / 签收时间 / 撤回时间) + */ + +import { ref } from 'vue' +import { getInfo } from '../api' +import type { DetailInfo, OpenContext, PageInfo } from '../types' + +export const usePage = () => { + const pageInfo = reactive({ + visible: false, + title: '出库单详情', + spin: false, + width: 720, + }) + + const detail = ref(null) + + const loadData = (id: string): void => { + pageInfo.spin = true + getInfo(id) + .then((res) => { + if (res.code === '00000') { + detail.value = res.data + } + }) + .catch((err: unknown) => { + console.error('获取出库单详情失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + const openDrawer = (ctx: OpenContext): void => { + detail.value = null + pageInfo.visible = true + pageInfo.title = ctx.title ? `出库单详情 · ${ctx.title}` : '出库单详情' + loadData(ctx.id) + } + + const closeDrawer = (): void => { + pageInfo.visible = false + } + + return { + pageInfo, + detail, + openDrawer, + closeDrawer, + } +} diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/types/index.ts new file mode 100644 index 0000000..7bb836d --- /dev/null +++ b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/types/index.ts @@ -0,0 +1,19 @@ +import type { ListItem } from '../../../types' + +/** 抽屉状态 */ +export interface PageInfo { + visible: boolean + title: string + spin: boolean + width: number +} + +/** 详情数据(复用列表项结构) */ +export type DetailInfo = ListItem + +/** 打开抽屉上下文 */ +export interface OpenContext { + id: string + /** 标题后缀,如出库单号 */ + title?: string +} diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/addOrEdit.vue index 47876a8..16166a5 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/addOrEdit.vue @@ -19,47 +19,21 @@ - - - - - - - - - - - - - - - - - - + + + + + @@ -74,6 +48,23 @@ /> + + + + + + + + - - - - - - - - + + + + /** * 出库管理 - 新增/编辑 弹框 - * - 字段:出库单号(编辑只读)/ 设备编码 / 设备名称 / 二级型号 / 出库类型 / 数量 / 领用人 / 目的场所 / 所属租户 / 备注 + * - 字段:出库类型 / 设备类别(末级)/ 数量 / 领用客户(下拉来自客户管理)/ 领用人 / 目的场所 / 仓库区域(末级,出库源)/ 备注 + * - 设备明细子表(从在库设备中勾选 N 台)属二期范围,本表单暂不含 */ +import DeviceCategoryPicker from '@components/DeviceCategoryPicker.vue' +import WarehouseAreaPicker from '@components/WarehouseAreaPicker.vue' +import { filterOption } from '@utils/antDesign/select' import { usePage } from './init/usePage' import { OUTBOUND_TYPE_OPTIONS } from '../../../types' @@ -154,6 +144,7 @@ const { formRef, form, rules, + customerOptionList, openModal, closeModal, submit, diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/api/index.ts index 9ed7154..683ae83 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/api/index.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/api/index.ts @@ -4,7 +4,7 @@ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { Form } from '../types' +import type { CustomerOption, Form } from '../types' /** 详情查询(编辑回填) */ export const info = (id: string): Promise> => @@ -17,3 +17,7 @@ export const add = (params: Record): Promise> /** 编辑 */ export const edit = (params: Record): Promise> => postRequest('axiosRequest', '/admin/device/stock/outbound/update', params) + +/** 领用客户下拉(复用 manage 同源接口) */ +export const customerOptions = (keyword?: string): Promise> => + postRequest('axiosRequest', '/admin/device/manage/customer/options', { keyword }) diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/init/usePage.ts index 0946053..f962c68 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/init/usePage.ts @@ -4,20 +4,19 @@ import type { FormInstance, Rule } from 'ant-design-vue/es/form' import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import { add, edit, info } from '../api' -import type { Form, ModalType, OpenContext, PageInfo } from '../types' +import { add, customerOptions, edit, info } from '../api' +import type { CustomerOption, Form, ModalType, OpenContext, PageInfo } from '../types' const createInitForm = (): Form => ({ id: undefined, outboundCode: '', - deviceCode: '', - deviceName: '', - modelName: '', + outboundType: 'install', + categoryId: undefined, quantity: 1, recipient: '', targetSite: '', - targetCustomer: '', - outboundType: 'install', + customerId: undefined, + warehouseAreaId: undefined, status: 'completed', remark: '', }) @@ -36,15 +35,17 @@ export const usePage = (emit: (e: 'load') => void) => { const formRef = ref() const form = ref(createInitForm()) + /** 领用客户下拉(弹框内独立加载) */ + const customerOptionList = ref([]) + const rules: Record = { - deviceCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }], - deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], - modelName: [{ required: true, message: '请输入二级型号', trigger: 'blur' }], + outboundType: [{ required: true, message: '请选择出库类型', trigger: 'change' }], + categoryId: [{ required: true, message: '请选择设备类别', trigger: 'change' }], quantity: [{ required: true, type: 'number', message: '请输入数量', trigger: 'blur' }], recipient: [{ required: true, message: '请输入领用人', trigger: 'blur' }], targetSite: [{ required: true, message: '请输入目的场所', trigger: 'blur' }], - targetCustomer: [{ required: true, message: '请输入所属租户', trigger: 'blur' }], - outboundType: [{ required: true, message: '请选择出库类型', trigger: 'change' }], + customerId: [{ required: true, message: '请选择领用客户', trigger: 'change' }], + warehouseAreaId: [{ required: true, message: '请选择仓库区域', trigger: 'change' }], } const resetForm = (): void => { @@ -59,6 +60,17 @@ export const usePage = (emit: (e: 'load') => void) => { const infoRequest = (id: string) => info(id) + /** 加载领用客户下拉 */ + const refreshCustomerOptions = (keyword?: string): void => { + customerOptions(keyword) + .then((res) => { + if (res.code === '00000') customerOptionList.value = res.data ?? [] + }) + .catch((err: unknown) => { + console.error('领用客户下拉加载失败:', err) + }) + } + const openModal = (type: ModalType, ctx?: OpenContext): void => { pageInfo.visible = true pageInfo.spin = true @@ -67,6 +79,8 @@ export const usePage = (emit: (e: 'load') => void) => { pageInfo.title = type === 'add' ? '新增出库单' : '编辑出库单' + refreshCustomerOptions() + const requests: Promise[] = [] if (type === 'edit' && ctx?.id) requests.push(infoRequest(ctx.id)) @@ -141,6 +155,8 @@ export const usePage = (emit: (e: 'load') => void) => { formRef, form, rules, + customerOptionList, + refreshCustomerOptions, openModal, closeModal, submit, diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/types/index.ts index b7b1609..b96ec45 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/types/index.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/component/modal/addOrEdit/types/index.ts @@ -1,5 +1,5 @@ import type { FormInstance } from 'ant-design-vue/es/form' -import type { OutboundStatus, OutboundType } from '../../../types' +import type { CustomerOption, OutboundStatus, OutboundType } from '../../../types' /** 弹框类型 */ export type ModalType = 'add' | 'edit' @@ -24,22 +24,20 @@ export interface Form { id: string | undefined /** 出库单号(编辑时只读) */ outboundCode: string - /** 设备编码 */ - deviceCode: string - /** 设备名称 */ - deviceName: string - /** 二级型号 */ - modelName: string + /** 出库类型 */ + outboundType: OutboundType + /** 设备类别 ID(末级分类) */ + categoryId: string | undefined /** 数量 */ quantity: number /** 领用人 */ recipient: string /** 目的场所 */ targetSite: string - /** 所属租户 */ - targetCustomer: string - /** 出库类型 */ - outboundType: OutboundType + /** 领用客户 ID */ + customerId: string | undefined + /** 仓库区域 ID(末级节点) */ + warehouseAreaId: string | undefined /** 状态 */ status: OutboundStatus /** 备注 */ @@ -51,3 +49,6 @@ export type FormRef = ReturnType /** 仅用于类型推导的占位函数 */ declare function usePageRef(): FormInstance | undefined + +/** 重新导出客户下拉类型,便于子组件使用 */ +export type { CustomerOption } diff --git a/src/pages/admin-portal/device-center/stock/outbound/init/usePage.ts b/src/pages/admin-portal/device-center/stock/outbound/init/usePage.ts index 55425a9..738aef6 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/init/usePage.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/init/usePage.ts @@ -1,16 +1,18 @@ import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { useAdminTable } from '@pages/admin-portal/shared/useAdminTable' import { useAdminSearch } from '@pages/admin-portal/shared/useAdminSearch' -import { exportList, list, remove, withdraw } from '../api' +import { customerOptions, exportList, list, remove, signReceive, withdraw } from '../api' import { tableColumns } from './useTable' import { createSearchKey } from './useSearch' -import type { ListItem, SearchForm } from '../types' +import type { CustomerOption, ListItem, SearchForm } from '../types' import type AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' +import type InfoDrawer from '../component/drawer/info/info.vue' +import type ExportTaskDrawer from '@components/ExportTaskDrawer.vue' /** * 出库管理页主编排: * - 列表 + 新增 + 编辑 + 删除 + 撤回 + 导出 - * - 完整表单弹框(含出库类型 / 领用人 / 目的场所 / 所属租户) + * - 筛选区"领用客户"下拉来自客户管理(复用 manage 同源接口) */ export const usePage = () => { const { message, Modal } = useAntdStaticMethods() @@ -18,8 +20,23 @@ export const usePage = () => { const pageLoading = ref(false) const addOrEditRef = ref | null>(null) + const infoDrawerRef = ref | null>(null) + const exportTaskRef = ref | null>(null) - const buildParams = () => ({ + /** 领用客户下拉(列表筛选用,可搜索) */ + const customerOptionList = ref([]) + + const refreshCustomerOptions = (keyword?: string): void => { + customerOptions(keyword) + .then((res) => { + if (res.code === '00000') customerOptionList.value = res.data ?? [] + }) + .catch((err: unknown) => { + console.error('领用客户下拉加载失败:', err) + }) + } + + const buildParams = (): SearchForm & { pageNum?: number; pageSize?: number; order?: 'ascend' | 'descend' | null; column?: string } => ({ ...search, pageNum: table.pagination.current ?? 1, pageSize: table.pagination.pageSize ?? 30, @@ -73,6 +90,10 @@ export const usePage = () => { addOrEditRef.value?.openModal('edit', { id: record.id }) } + const openInfoDrawer = (record: ListItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id, title: record.outboundCode }) + } + const deleteRecord = (record: ListItem): void => { Modal.confirm({ title: '确认删除?', @@ -95,10 +116,10 @@ export const usePage = () => { }) } - /** 撤回:已签收的出库单禁止撤回 */ + /** 撤回:仅待出库状态可撤回 */ const withdrawRecord = (record: ListItem): void => { - if (record.status === 'signed') { - void message.warning('已签收的出库单不可撤回') + if (record.status !== 'pending') { + void message.warning('仅待出库状态的出库单可撤回') return } Modal.confirm({ @@ -122,6 +143,33 @@ export const usePage = () => { }) } + /** 签收:已出库 → 已签收 */ + const signReceiveRecord = (record: ListItem): void => { + if (record.status !== 'completed') { + void message.warning('仅已出库状态的出库单可签收') + return + } + Modal.confirm({ + title: '确认签收?', + content: `出库单「${record.outboundCode}」签收后状态将变更为已签收。`, + okType: 'primary', + okText: '确认签收', + cancelText: '取消', + onOk: () => { + signReceive(record.id) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg) + listRequest() + } + }) + .catch((err: unknown) => { + console.error('出库单签收失败:', err) + }) + }, + }) + } + const exportRecord = (): void => { exportList(buildParams()) .then((res) => { @@ -132,15 +180,22 @@ export const usePage = () => { }) } + const viewExportTask = (): void => { + exportTaskRef.value?.openDrawer() + } + const handleAction = (type: string, record?: ListItem): void => { if (type === 'add') openAddModal() else if (type === 'edit' && record) openEditModal(record) + else if (type === 'info' && record) openInfoDrawer(record) else if (type === 'delete' && record) deleteRecord(record) else if (type === 'withdraw' && record) withdrawRecord(record) + else if (type === 'sign' && record) signReceiveRecord(record) } const loadData = (): void => { initOptions() + refreshCustomerOptions() listRequest() } @@ -155,11 +210,16 @@ export const usePage = () => { selectedRowKeys, rowSelection, addOrEditRef, + infoDrawerRef, + exportTaskRef, + customerOptionList, + refreshCustomerOptions, dataSourceChange, searchQuery, resetQuery, handleAction, listRequest, exportRecord, + viewExportTask, } } diff --git a/src/pages/admin-portal/device-center/stock/outbound/init/useSearch.ts b/src/pages/admin-portal/device-center/stock/outbound/init/useSearch.ts index 68ba8e8..1810e44 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/init/useSearch.ts @@ -4,4 +4,7 @@ export const createSearchKey = (): SearchForm => ({ keyword: undefined, status: undefined, outboundType: undefined, + categoryId: undefined, + warehouseAreaId: undefined, + customerId: undefined, }) diff --git a/src/pages/admin-portal/device-center/stock/outbound/init/useTable.ts b/src/pages/admin-portal/device-center/stock/outbound/init/useTable.ts index 8f79de4..dfd108b 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/init/useTable.ts +++ b/src/pages/admin-portal/device-center/stock/outbound/init/useTable.ts @@ -18,12 +18,10 @@ const typeText = (type: string): string => export const tableColumns: TableColumnsType = [ { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, { title: '出库单号', dataIndex: 'outboundCode', align: 'left', width: 170, fixed: 'left', resizable: true, ellipsis: true }, - { title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 150, resizable: true, ellipsis: true }, - { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 140, resizable: true, ellipsis: true }, - { title: '二级型号', dataIndex: 'modelName', align: 'left', width: 130, resizable: true, ellipsis: true }, - { title: '所属租户', dataIndex: 'targetCustomer', align: 'left', width: 150, resizable: true, ellipsis: true }, - { title: '目的场所', dataIndex: 'targetSite', align: 'left', width: 150, resizable: true, ellipsis: true }, + { title: '设备类别', dataIndex: 'categoryName', align: 'left', width: 150, resizable: true, ellipsis: true }, { title: '数量', dataIndex: 'quantity', align: 'center', width: 80 }, + { title: '领用客户', dataIndex: 'customerName', align: 'left', width: 160, resizable: true, ellipsis: true }, + { title: '目的场所', dataIndex: 'targetSite', align: 'left', width: 150, resizable: true, ellipsis: true }, { title: '领用人', dataIndex: 'recipient', align: 'center', width: 100, resizable: true, ellipsis: true }, { title: '出库类型', @@ -33,6 +31,7 @@ export const tableColumns: TableColumnsType = [ customRender: ({ value }: { value: OutboundType }) => h(Tag, { color: OUTBOUND_TYPE_COLOR[value] ?? 'default' }, () => typeText(value)), }, + { title: '仓库区域', dataIndex: 'warehouseAreaName', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, { title: '状态', dataIndex: 'status', @@ -43,5 +42,5 @@ export const tableColumns: TableColumnsType = [ }, { title: '出库时间', dataIndex: 'outboundTime', align: 'center', width: 170, sorter: true, resizable: true }, { title: '经办人', dataIndex: 'operator', align: 'center', width: 110, resizable: true, ellipsis: true }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 220 }, ] diff --git a/src/pages/admin-portal/device-center/stock/outbound/outbound.vue b/src/pages/admin-portal/device-center/stock/outbound/outbound.vue index 7b1d9cc..7e83431 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/outbound.vue +++ b/src/pages/admin-portal/device-center/stock/outbound/outbound.vue @@ -2,7 +2,26 @@
- + + + + + + + + + + 导出 + + + 查看导出任务 + + +
diff --git a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/usePage.ts b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/usePage.ts index c7626b9..5909861 100644 --- a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/usePage.ts +++ b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/usePage.ts @@ -4,8 +4,10 @@ import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { createPaginationConfig } from '@utils/antDesign/table' import type { TableState } from '@utils/antDesign/table' import AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' +import ImportArcsoft from '../component/modal/importArcsoft/importArcsoft.vue' +import ExportTaskDrawer from '@components/ExportTaskDrawer.vue' import InfoDrawer from '../component/drawer/info/info.vue' -import { list, remove } from '../api' +import { exportList, list, remove } from '../api' import type { ArcItem, SearchForm } from '../types' import { createSearchKey } from './useSearch' import { tableColumns } from './useTable' @@ -13,6 +15,7 @@ import { tableColumns } from './useTable' /** * 虹软人脸授权页主编排: * - 标准 CRUD + 详情抽屉 + * - 批量导入授权码 / 异步导出 / 查看导出任务 * - 每条记录 = 一台设备的激活码,不可重复绑定 */ export const usePage = () => { @@ -32,8 +35,12 @@ export const usePage = () => { /** 新增/编辑弹框 ref */ const addOrEditRef = ref | null>(null) + /** 批量导入弹框 ref */ + const importArcsoftRef = ref | null>(null) /** 详情抽屉 ref */ const infoDrawerRef = ref | null>(null) + /** 导出任务抽屉 ref */ + const exportTaskRef = ref | null>(null) const listRequest = (): void => { pageLoading.value = true @@ -90,6 +97,29 @@ export const usePage = () => { infoDrawerRef.value?.openDrawer({ id: record.id }) } + /** 打开批量导入弹框 */ + const openImportModal = (): void => { + importArcsoftRef.value?.openModal() + } + + /** 异步导出 */ + const exportListData = (): void => { + exportList(search) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg || '导出任务已提交,可在"查看导出任务"中查看进度') + } + }) + .catch((err: unknown) => { + console.error('虹软授权导出失败:', err) + }) + } + + /** 打开查看导出任务抽屉 */ + const viewExportTask = (): void => { + exportTaskRef.value?.openDrawer() + } + const deleteRecord = (record: ArcItem): void => { Modal.confirm({ title: '确认删除?', @@ -141,11 +171,16 @@ export const usePage = () => { selectedRowKeys, rowSelection, addOrEditRef, + importArcsoftRef, infoDrawerRef, + exportTaskRef, searchQuery, resetQuery, dataSourceChange, handleAction, + exportListData, + viewExportTask, + openImportModal, listRequest, } } diff --git a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useSearch.ts b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useSearch.ts index 7f0ffc2..e1be750 100644 --- a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useSearch.ts @@ -2,6 +2,9 @@ import type { SearchForm } from '../types' /** 搜索表单工厂:统一重置入口 */ export const createSearchKey = (): SearchForm => ({ - keyword: undefined, + activeKey: undefined, + deviceCategoryId: undefined, + deviceSn: undefined, + appId: undefined, status: undefined, }) diff --git a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useTable.ts b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useTable.ts index b93b2bf..5fa6cd9 100644 --- a/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useTable.ts +++ b/src/pages/admin-portal/device-center/third-auth/arcsoft/init/useTable.ts @@ -2,18 +2,20 @@ import type { TableColumnsType } from 'ant-design-vue' import { h } from 'vue' import { Tag } from 'ant-design-vue' import type { ArcItem, ArcStatus } from '../types' -import { arcStatusColor, arcStatusText } from '../types' +import { arcStatusColor, arcStatusText, isPermanentExpire } from '../types' -/** 虹软授权列定义 */ +/** + * 虹软授权列定义 + * + * 字段顺序:序号 / 授权码 / AppID / SdkKey / 状态 / 设备编码 / 设备类别 / 激活时间 / 到期时间 / 创建时间 / 操作 + * - 到期时间为空字符串时渲染为"永久有效" Tag(大部分虹软授权为永久授权) + * - 设备编码 = 设备 SN(dataIndex 沿用 deviceSn 保持与搜索字段对齐) + */ export const tableColumns: TableColumnsType = [ { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, - { title: '激活码', dataIndex: 'activeKey', align: 'left', width: 240, resizable: true, ellipsis: true }, - { title: '设备 SN', dataIndex: 'deviceSn', align: 'left', width: 160, resizable: true, ellipsis: true }, - { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 180, resizable: true, ellipsis: true }, - { title: '设备型号', dataIndex: 'deviceModel', align: 'center', width: 120, resizable: true, ellipsis: true }, + { title: '授权码', dataIndex: 'activeKey', align: 'left', width: 240, resizable: true, ellipsis: true }, { title: 'AppID', dataIndex: 'appId', align: 'left', width: 200, resizable: true, ellipsis: true }, - { title: '激活时间', dataIndex: 'activateTime', align: 'center', width: 120, resizable: true }, - { title: '到期时间', dataIndex: 'expireTime', align: 'center', width: 120, resizable: true }, + { title: 'SdkKey', dataIndex: 'sdkKey', align: 'left', width: 220, resizable: true, ellipsis: true }, { title: '状态', dataIndex: 'status', @@ -22,6 +24,19 @@ export const tableColumns: TableColumnsType = [ customRender: ({ value }: { value: ArcStatus }) => h(Tag, { color: arcStatusColor(value) }, () => arcStatusText(value)), }, + { title: '设备编码', dataIndex: 'deviceSn', align: 'left', width: 160, resizable: true, ellipsis: true }, + { title: '设备类别', dataIndex: 'deviceCategoryName', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, + { title: '激活时间', dataIndex: 'activateTime', align: 'center', width: 120, resizable: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, + { + title: '到期时间', + dataIndex: 'expireTime', + align: 'center', + width: 130, + customRender: ({ value }: { value?: string }) => + isPermanentExpire(value) + ? h(Tag, { color: 'blue' }, () => '永久有效') + : (value ?? '—'), + }, { title: '创建时间', dataIndex: 'createTime', align: 'center', width: 170, sorter: true, resizable: true }, { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, ] diff --git a/src/pages/admin-portal/device-center/third-auth/arcsoft/types/index.ts b/src/pages/admin-portal/device-center/third-auth/arcsoft/types/index.ts index ad9fdd1..9a17b76 100644 --- a/src/pages/admin-portal/device-center/third-auth/arcsoft/types/index.ts +++ b/src/pages/admin-portal/device-center/third-auth/arcsoft/types/index.ts @@ -13,15 +13,19 @@ export interface ArcItem extends BaseRecord { sdkKey: string /** SDK Secret */ sdkSecret: string - /** 绑定设备 SN */ + /** 绑定设备编码(设备 SN) */ deviceSn: string /** 设备名称 */ deviceName?: string /** 设备型号 */ deviceModel?: string + /** 设备类别 ID(引用设备分类树节点 id) */ + deviceCategoryId: string + /** 设备类别名称(列表展示用,后端 join 返回;mock 阶段冗余写入) */ + deviceCategoryName?: string /** 激活时间 */ activateTime: string - /** 到期时间 */ + /** 到期时间(空字符串表示永久有效) */ expireTime: string /** 授权状态 */ status: ArcStatus @@ -33,7 +37,15 @@ export interface ArcItem extends BaseRecord { /** 搜索表单 */ export interface SearchForm extends BaseSearchForm { - keyword?: string + /** 激活码模糊匹配 */ + activeKey?: string + /** 设备类别 ID 等值匹配 */ + deviceCategoryId?: string + /** 设备编码模糊匹配 */ + deviceSn?: string + /** AppID 模糊匹配 */ + appId?: string + /** 授权状态等值匹配 */ status?: ArcStatus } @@ -42,12 +54,20 @@ export interface ArcPayload { id?: string activeKey: string appId: string + /** SDK Key */ sdkKey: string - sdkSecret: string + /** SDK Secret(可选;敏感字段,前端不录入,后端按 AppID 自动填充) */ + sdkSecret?: string + /** 使用设备编码(该授权被哪台设备使用) */ deviceSn: string + /** 使用设备类别 ID */ + deviceCategoryId: string + /** 使用设备名称(可选,便于识别) */ deviceName?: string + /** 使用设备型号(可选) */ deviceModel?: string activateTime?: string + /** 到期时间:空字符串表示永久有效 */ expireTime: string status: ArcStatus remark?: string @@ -75,3 +95,6 @@ export const arcStatusColor = (value: ArcStatus): string => { } return map[value] ?? 'default' } + +/** 到期时间是否为永久有效(约定空字符串 = 永久) */ +export const isPermanentExpire = (value?: string): boolean => !value diff --git a/src/pages/admin-portal/device-center/third-auth/sim/api/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/api/index.ts new file mode 100644 index 0000000..2965ace --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/api/index.ts @@ -0,0 +1,46 @@ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { SearchForm, SimItem, SimPayload } from '../types' + +/** 查询 SIM 卡列表 */ +export const list = (params: SearchForm): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/page', params) + +/** 查询 SIM 卡详情 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/info', { id }) + +/** 新增 SIM 卡 */ +export const add = (payload: SimPayload): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/add', payload) + +/** 更新 SIM 卡 */ +export const update = (payload: SimPayload): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/update', payload) + +/** 删除 SIM 卡 */ +export const remove = (id: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/delete', { id }) + +/** 绑定设备 */ +export const bindDevice = (simId: string, deviceSn: string, deviceName?: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/bind', { simId, deviceSn, deviceName }) + +/** 解绑设备 */ +export const unbindDevice = (simId: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/unbind', { simId }) + +/** 异步导出 */ +export const exportList = (params: SearchForm): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/export', params) + +/** 批量导入 */ +export const importSim = (file: File): Promise> => { + const formData = new FormData() + formData.append('file', file) + return postRequest('axiosRequest', '/admin/device/third-auth/sim/import', formData, { 'Content-Type': 'multipart/form-data' }) +} + +/** 可绑定设备下拉 */ +export const deviceOptions = (): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/device-options', {}) diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/api/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/api/index.ts new file mode 100644 index 0000000..9780b01 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/api/index.ts @@ -0,0 +1,11 @@ +/** + * SIM 卡 - 绑定记录抽屉 - 接口层 + */ + +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { BindRecordItem } from '../types' + +/** 查询 SIM 卡历史绑定记录 */ +export const listBindRecords = (simId: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/bind-records', { simId }) diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/bindRecord.vue b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/bindRecord.vue new file mode 100644 index 0000000..eff6e96 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/bindRecord.vue @@ -0,0 +1,49 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/init/usePage.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/init/usePage.ts new file mode 100644 index 0000000..3628530 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/init/usePage.ts @@ -0,0 +1,77 @@ +import type { TableColumnsType } from 'ant-design-vue' +import { h } from 'vue' +import { Tag } from 'ant-design-vue' +import { listBindRecords } from '../api' +import type { BindRecordItem, BindRecordStatus, OpenContext, PageInfo } from '../types' + +/** 绑定记录列定义 */ +const tableColumns: TableColumnsType = [ + { title: '序号', dataIndex: 'index', align: 'center', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, + { title: '绑定时间', dataIndex: 'bindTime', align: 'center', width: 170 }, + { title: '解绑时间', dataIndex: 'unbindTime', align: 'center', width: 170, customRender: ({ value }: { value?: string }) => value ?? '—' }, + { title: '设备分类别', dataIndex: 'deviceCategory', align: 'left', width: 140, ellipsis: true }, + { title: '设备编码', dataIndex: 'deviceSn', align: 'left', width: 180, ellipsis: true }, + { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 200, ellipsis: true }, + { title: '操作人', dataIndex: 'operator', align: 'center', width: 120 }, + { + title: '状态', + dataIndex: 'status', + align: 'center', + width: 100, + customRender: ({ value }: { value: BindRecordStatus }) => + h(Tag, { color: value === 'bound' ? 'green' : 'default' }, () => (value === 'bound' ? '已绑定' : '已解绑')), + }, +] + +/** + * SIM 卡 - 绑定记录抽屉 - 主逻辑 + * - 打开时按 simId 拉取历史绑定记录 + * - 只读展示,按绑定时间倒序 + */ +export const usePage = () => { + const pageInfo = reactive({ + visible: false, + title: '绑定记录', + width: 1100, + spin: false, + }) + + const iccid = ref('') + const dataSource = ref([]) + const columns = tableColumns + + const closeDrawer = (): void => { + pageInfo.visible = false + iccid.value = '' + dataSource.value = [] + } + + const openDrawer = (ctx: OpenContext): void => { + pageInfo.visible = true + pageInfo.spin = true + iccid.value = ctx.iccid + dataSource.value = [] + + listBindRecords(ctx.simId) + .then((res) => { + if (res.code === '00000') { + dataSource.value = res.data ?? [] + } + }) + .catch((err: unknown) => { + console.error('SIM 卡绑定记录查询失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + return { + pageInfo, + iccid, + dataSource, + columns, + openDrawer, + closeDrawer, + } +} diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/types/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/types/index.ts new file mode 100644 index 0000000..4c30059 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/drawer/bindRecord/types/index.ts @@ -0,0 +1,45 @@ +import type { FormInstance } from 'ant-design-vue/es/form' + +/** 抽屉页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 打开抽屉上下文 */ +export interface OpenContext { + /** SIM 卡 ID */ + simId: string + /** ICCID(仅展示) */ + iccid: string +} + +/** 绑定记录状态 */ +export type BindRecordStatus = 'bound' | 'unbound' + +/** 绑定记录项 */ +export interface BindRecordItem { + id: string + /** 绑定时间 */ + bindTime: string + /** 解绑时间(已解绑的有) */ + unbindTime?: string + /** 设备编码 */ + deviceSn: string + /** 设备名称 */ + deviceName: string + /** 设备分类别 */ + deviceCategory: string + /** 操作人 */ + operator: string + /** 状态:已绑定 / 已解绑 */ + status: BindRecordStatus +} + +/** 暴露给父组件的 ref 类型 */ +export type FormRef = ReturnType + +/** 仅用于类型推导的占位函数 */ +declare function usePageRef(): FormInstance | undefined diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/addOrEdit.vue new file mode 100644 index 0000000..02c8b59 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/addOrEdit.vue @@ -0,0 +1,143 @@ + + + diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/api/index.ts new file mode 100644 index 0000000..e270adc --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/api/index.ts @@ -0,0 +1,20 @@ +/** + * SIM 卡 - 新增/编辑 弹框 - 接口层 + * - 复用主页的 info / add / update + */ + +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { Form } from '../types' + +/** 详情查询(编辑回填) */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/info', { id }) + +/** 新增 */ +export const add = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/add', params) + +/** 编辑 */ +export const edit = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/update', params) diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/init/usePage.ts new file mode 100644 index 0000000..6b041c2 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/init/usePage.ts @@ -0,0 +1,154 @@ +/** + * SIM 卡 - 新增/编辑 弹框 - 主逻辑 + * + * 严格遵循 modal-spec.md: + * - createInitForm 工厂函数 + * - openModal 用 Promise.all 拉取详情 + * - addRequest 必须 delete params.id + * - submit 开头立即 pageInfo.spin = true + * - 用 params.id 判断新增/编辑 + */ + +import type { FormInstance, Rule } from 'ant-design-vue/es/form' +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { add, edit, info } from '../api' +import type { Form, ModalType, OpenContext, PageInfo } from '../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + id: undefined, + iccid: '', + phoneNo: '', + carrier: 'mobile', + packageType: '', + activateTime: '', + expireTime: '', + status: 'unactivated', + remark: '', +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + type: 'add', + title: '', + width: 720, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + + const rules: Record = { + iccid: [ + { required: true, message: '请输入 ICCID', trigger: 'blur' }, + { pattern: /^\d{19,20}$/, message: 'ICCID 应为 19~20 位数字', trigger: 'blur' }, + ], + phoneNo: [{ required: true, message: '请输入手机号', trigger: 'blur' }], + carrier: [{ required: true, message: '请选择运营商', trigger: 'change' }], + packageType: [{ required: true, message: '请输入套餐类型', trigger: 'blur' }], + expireTime: [{ required: true, message: '请选择到期时间', trigger: 'change' }], + status: [{ required: true, message: '请选择状态', trigger: 'change' }], + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + } + + const closeModal = (): void => { + resetForm() + pageInfo.visible = false + } + + const infoRequest = (id: string) => info(id) + + /** 打开弹框 */ + const openModal = (type: ModalType, ctx?: OpenContext): void => { + pageInfo.visible = true + pageInfo.spin = true + pageInfo.type = type + resetForm() + + pageInfo.title = type === 'add' ? '新增 SIM 卡' : '编辑 SIM 卡' + + const requests: Promise[] = [] + if (type === 'edit' && ctx?.id) requests.push(infoRequest(ctx.id)) + + Promise.all(requests) + .then((results) => { + if (type === 'edit' && ctx?.id) { + const infoRes = results[0] as { code: string; data: Form } + if (infoRes?.code === '00000' && infoRes.data) { + form.value = { ...createInitForm(), ...infoRes.data } + } + } + }) + .catch((err: unknown) => { + console.error('打开弹框初始化失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + const successRequest = (msg: string): void => { + void message.success(msg) + emit('load') + closeModal() + } + + const addRequest = (params: Record): void => { + delete params.id + add(params) + .then((res) => { + if (res.code === '00000') successRequest(res.msg) + }) + .catch((err: unknown) => { + console.error('新增失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + const editRequest = (params: Record): void => { + edit(params) + .then((res) => { + if (res.code === '00000') successRequest(res.msg) + }) + .catch((err: unknown) => { + console.error('编辑失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + const submit = (): void => { + pageInfo.spin = true + formRef.value + ?.validate() + .then(() => { + const params = JSON.parse(JSON.stringify(form.value)) as Record + if (params.id) editRequest(params) + else addRequest(params) + }) + .catch((err: unknown) => { + pageInfo.spin = false + console.error('表单验证失败:', err) + }) + } + + return { + pageInfo, + formRef, + form, + rules, + openModal, + closeModal, + submit, + } +} diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/types/index.ts new file mode 100644 index 0000000..1471209 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/addOrEdit/types/index.ts @@ -0,0 +1,47 @@ +import type { FormInstance } from 'ant-design-vue/es/form' +import type { Carrier, SimStatus } from '../../../../types' + +/** 弹框类型 */ +export type ModalType = 'add' | 'edit' + +/** 弹框页面状态 */ +export interface PageInfo { + visible: boolean + type: ModalType + title: string + width: number + spin: boolean +} + +/** 打开弹框上下文 */ +export interface OpenContext { + /** 编辑模式必传 */ + id?: string +} + +/** 表单字段(对齐 SimPayload) */ +export interface Form { + id: string | undefined + /** ICCID */ + iccid: string + /** 手机号 */ + phoneNo: string + /** 运营商 */ + carrier: Carrier + /** 套餐类型 */ + packageType: string + /** 激活时间 */ + activateTime: string + /** 到期时间 */ + expireTime: string + /** 状态 */ + status: SimStatus + /** 备注 */ + remark: string +} + +/** 暴露给父组件的 ref 类型 */ +export type FormRef = ReturnType + +/** 仅用于类型推导的占位函数 */ +declare function usePageRef(): FormInstance | undefined diff --git a/src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/api/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/api/index.ts new file mode 100644 index 0000000..adb7232 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/api/index.ts @@ -0,0 +1,11 @@ +/** + * SIM 卡 - 绑定设备 弹框 - 接口层 + */ + +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { DeviceOption } from '../types' + +/** 拉取可绑定设备下拉 */ +export const listDeviceOptions = (): Promise> => + postRequest('axiosRequest', '/admin/device/third-auth/sim/device-options', {}) diff --git a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/returnGoods.vue b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/bindDevice.vue similarity index 51% rename from src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/returnGoods.vue rename to src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/bindDevice.vue index 60db4da..a280494 100644 --- a/src/pages/admin-portal/device-center/stock/instock/component/modal/returnGoods/returnGoods.vue +++ b/src/pages/admin-portal/device-center/third-auth/sim/component/modal/bindDevice/bindDevice.vue @@ -13,33 +13,25 @@ :rules="rules" :label-col="{ style: { width: '110px', minWidth: '110px' } }" > - - + + - - + - - - + @@ -50,21 +42,22 @@ :disabled="pageInfo.spin" :loading="pageInfo.spin" @click="submit" - >确定 + >确认绑定 + + diff --git a/src/pages/admin-portal/device-center/third-auth/sim/init/usePage.ts b/src/pages/admin-portal/device-center/third-auth/sim/init/usePage.ts new file mode 100644 index 0000000..c27fe84 --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/init/usePage.ts @@ -0,0 +1,242 @@ +import { onMounted, reactive, ref } from 'vue' +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 AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' +import BindDevice from '../component/modal/bindDevice/bindDevice.vue' +import BindRecordDrawer from '../component/drawer/bindRecord/bindRecord.vue' +import ImportSim from '../component/modal/importSim/importSim.vue' +import ExportTask from '@components/ExportTaskDrawer.vue' +import { bindDevice as bindApi, exportList, list, remove, unbindDevice as unbindApi } from '../api' +import type { SimItem, SearchForm } from '../types' +import { createSearchKey } from './useSearch' +import { tableColumns } from './useTable' + +/** + * SIM 管理页主编排: + * - 标准 CRUD + 绑定/解绑设备 + 绑定记录 + 导出 / 批量导入 / 查看导出任务 + * - 操作列"绑定设备 / 解绑设备"按 boundDeviceSn 互斥显示 + * - "绑定记录"打开只读抽屉,展示历史绑定变更 + */ +export const usePage = () => { + const { message, Modal } = useAntdStaticMethods() + + const search = reactive(createSearchKey()) + + const table = reactive>({ + columns: tableColumns, + dataSource: [], + sort: { field: '', order: null }, + pagination: createPaginationConfig() as TablePaginationConfig, + }) + + const pageLoading = ref(false) + const selectedRowKeys = ref<(string | number)[]>([]) + + /** 新增/编辑弹框 ref */ + const addOrEditRef = ref | null>(null) + /** 绑定设备弹框 ref */ + const bindDeviceRef = ref | null>(null) + /** 绑定记录抽屉 ref */ + const bindRecordRef = ref | null>(null) + /** 批量导入弹框 ref */ + const importSimRef = ref | null>(null) + /** 导出任务抽屉 ref */ + const exportTaskRef = ref | null>(null) + + const listRequest = (): void => { + pageLoading.value = true + list(search) + .then((res) => { + if (res.code === '00000') { + table.dataSource = res.data ?? [] + table.pagination.total = table.dataSource.length + } + }) + .catch((err: unknown) => { + console.error('SIM 列表请求失败:', err) + }) + .finally(() => { + pageLoading.value = false + }) + } + + const searchQuery = (): void => { + table.pagination.current = 1 + listRequest() + } + + const resetQuery = (): void => { + Object.assign(search, createSearchKey()) + table.pagination.current = 1 + listRequest() + } + + const dataSourceChange = (pagination: TablePaginationConfig): void => { + table.pagination.current = pagination.current ?? 1 + table.pagination.pageSize = pagination.pageSize ?? 30 + } + + const rowSelection = { + selectedRowKeys, + onChange: (keys: (string | number)[]): void => { + selectedRowKeys.value = keys + }, + } + + /** 打开新增弹框 */ + const openAddModal = (): void => { + addOrEditRef.value?.openModal('add') + } + + /** 打开编辑弹框 */ + const openEditModal = (record: SimItem): void => { + addOrEditRef.value?.openModal('edit', { id: record.id }) + } + + /** 打开绑定设备弹框 */ + const openBindModal = (record: SimItem): void => { + bindDeviceRef.value?.openModal({ simId: record.id, iccid: record.iccid }) + } + + /** 打开绑定记录抽屉 */ + const openBindRecordDrawer = (record: SimItem): void => { + bindRecordRef.value?.openDrawer({ simId: record.id, iccid: record.iccid }) + } + + /** 解绑设备(二次确认) */ + const openUnbindConfirm = (record: SimItem): void => { + Modal.confirm({ + title: '确认解绑设备?', + content: `SIM 卡「${record.iccid}」将与设备「${record.boundDeviceSn ?? '—'}」解绑,解绑后该设备将无法继续联网上报。`, + okType: 'danger', + okText: '确认解绑', + cancelText: '取消', + onOk: () => + unbindApi(record.id) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg) + listRequest() + } + }) + .catch((err: unknown) => { + console.error('SIM 解绑失败:', err) + }), + }) + } + + /** 异步导出 */ + const exportListData = (): void => { + exportList(search) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg || '导出任务已提交,可在"查看导出任务"中查看进度') + } + }) + .catch((err: unknown) => { + console.error('SIM 导出失败:', err) + }) + } + + /** 打开查看导出任务抽屉 */ + const viewExportTask = (): void => { + exportTaskRef.value?.openDrawer() + } + + /** 打开批量导入弹框 */ + const openImportModal = (): void => { + importSimRef.value?.openModal() + } + + /** 删除(二次确认) */ + const deleteRecord = (record: SimItem): void => { + Modal.confirm({ + title: '确认删除?', + content: `SIM 卡「${record.iccid}」删除后不可恢复,若已绑定设备将自动解绑,请谨慎操作。`, + okType: 'danger', + okText: '确认删除', + cancelText: '取消', + onOk: () => { + remove(record.id) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg) + listRequest() + } + }) + .catch((err: unknown) => { + console.error('SIM 删除失败:', err) + }) + }, + }) + } + + /** 绑定设备提交(由 bindDevice 子组件 emit('ok') 触发) */ + const handleBindSubmit = (payload: { simId: string; deviceSn: string; deviceName?: string }, reset: () => void) => { + bindApi(payload.simId, payload.deviceSn, payload.deviceName) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg) + reset() + listRequest() + } + }) + .catch((err: unknown) => { + console.error('SIM 绑定失败:', err) + }) + } + + const handleAction = (type: string, record?: SimItem): void => { + if (type === 'add') { + openAddModal() + return + } + if (type === 'edit' && record) { + openEditModal(record) + return + } + if (type === 'bind' && record) { + openBindModal(record) + return + } + if (type === 'unbind' && record) { + openUnbindConfirm(record) + return + } + if (type === 'bindRecord' && record) { + openBindRecordDrawer(record) + return + } + if (type === 'delete' && record) { + deleteRecord(record) + } + } + + onMounted(() => { + listRequest() + }) + + return { + pageLoading, + search, + table, + selectedRowKeys, + rowSelection, + addOrEditRef, + bindDeviceRef, + bindRecordRef, + importSimRef, + exportTaskRef, + searchQuery, + resetQuery, + dataSourceChange, + handleAction, + exportListData, + viewExportTask, + openImportModal, + handleBindSubmit, + listRequest, + } +} diff --git a/src/pages/admin-portal/device-center/third-auth/sim/init/useSearch.ts b/src/pages/admin-portal/device-center/third-auth/sim/init/useSearch.ts new file mode 100644 index 0000000..c5bd58d --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/init/useSearch.ts @@ -0,0 +1,8 @@ +import type { SearchForm } from '../types' + +/** 搜索表单工厂:统一重置入口 */ +export const createSearchKey = (): SearchForm => ({ + keyword: undefined, + carrier: undefined, + status: undefined, +}) diff --git a/src/pages/admin-portal/device-center/third-auth/sim/init/useTable.ts b/src/pages/admin-portal/device-center/third-auth/sim/init/useTable.ts new file mode 100644 index 0000000..565ca5d --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/init/useTable.ts @@ -0,0 +1,35 @@ +import type { TableColumnsType } from 'ant-design-vue' +import { h } from 'vue' +import { Tag } from 'ant-design-vue' +import type { Carrier, SimItem, SimStatus } from '../types' +import { carrierText, simStatusColor, simStatusText } from '../types' + +/** SIM 管理列定义 */ +export const tableColumns: TableColumnsType = [ + { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, + { title: 'ICCID', dataIndex: 'iccid', align: 'left', width: 220, resizable: true, ellipsis: true }, + { title: '手机号', dataIndex: 'phoneNo', align: 'left', width: 140, resizable: true, ellipsis: true }, + { + title: '运营商', + dataIndex: 'carrier', + align: 'center', + width: 110, + customRender: ({ value }: { value: Carrier }) => carrierText(value), + }, + { title: '套餐类型', dataIndex: 'packageType', align: 'left', width: 160, resizable: true, ellipsis: true }, + { + title: '状态', + dataIndex: 'status', + align: 'center', + width: 100, + customRender: ({ value }: { value: SimStatus }) => + h(Tag, { color: simStatusColor(value) }, () => simStatusText(value)), + }, + { title: '设备分类别', dataIndex: 'deviceCategory', align: 'left', width: 140, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, + { title: '设备编码', dataIndex: 'boundDeviceSn', align: 'left', width: 180, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, + { title: '激活时间', dataIndex: 'activateTime', align: 'center', width: 120, resizable: true, customRender: ({ value }: { value: string }) => value ?? '—' }, + { title: '创建时间', dataIndex: 'createTime', align: 'center', width: 170, sorter: true, resizable: true }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 320 }, +] + +export type { SimItem } diff --git a/src/pages/admin-portal/device-center/third-auth/sim/sim.vue b/src/pages/admin-portal/device-center/third-auth/sim/sim.vue new file mode 100644 index 0000000..cf32fab --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/sim.vue @@ -0,0 +1,115 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/third-auth/sim/types/index.ts b/src/pages/admin-portal/device-center/third-auth/sim/types/index.ts new file mode 100644 index 0000000..73f06ff --- /dev/null +++ b/src/pages/admin-portal/device-center/third-auth/sim/types/index.ts @@ -0,0 +1,91 @@ +import type { BaseSearchForm } from '@pages/admin-portal/shared/types' + +/** SIM 卡状态 */ +export type SimStatus = 'unactivated' | 'normal' | 'arrears' | 'stopped' + +/** 运营商 */ +export type Carrier = 'mobile' | 'unicom' | 'telecom' | 'broadnet' + +/** SIM 卡列表项 */ +export interface SimItem { + id: string + /** SIM 卡 ICCID(19~20 位,与硬件 1:1) */ + iccid: string + /** 手机号 */ + phoneNo: string + /** 运营商 */ + carrier: Carrier + /** 套餐类型 */ + packageType: string + /** 状态 */ + status: SimStatus + /** 绑定设备 SN(空表示未绑定,列表展示为"设备编码") */ + boundDeviceSn: string + /** 绑定设备名称 */ + boundDeviceName?: string + /** 设备分类别(如:货柜 / 体检机 / 打卡机) */ + deviceCategory?: string + /** 激活时间 */ + activateTime: string + /** 到期时间 */ + expireTime: string + /** 备注 */ + remark?: string + /** 创建时间 */ + createTime: string +} + +/** 搜索表单 */ +export interface SearchForm extends BaseSearchForm { + keyword?: string + carrier?: Carrier + status?: SimStatus +} + +/** 新增/编辑提交载荷 */ +export interface SimPayload { + id?: string + iccid: string + phoneNo: string + carrier: Carrier + packageType: string + activateTime?: string + expireTime: string + status: SimStatus + remark?: string +} + +/** 状态下拉选项 */ +export const SIM_STATUS_OPTIONS = [ + { label: '未激活', value: 'unactivated' }, + { label: '正常', value: 'normal' }, + { label: '已欠费', value: 'arrears' }, + { label: '已停用', value: 'stopped' }, +] + +/** 运营商下拉选项 */ +export const CARRIER_OPTIONS = [ + { label: '中国移动', value: 'mobile' }, + { label: '中国联通', value: 'unicom' }, + { label: '中国电信', value: 'telecom' }, + { label: '中国广电', value: 'broadnet' }, +] + +/** 状态 → 中文 */ +export const simStatusText = (value: string): string => + SIM_STATUS_OPTIONS.find((item) => item.value === value)?.label ?? value ?? '—' + +/** 状态 → Tag 颜色 */ +export const simStatusColor = (value: SimStatus): string => { + const map: Record = { + unactivated: 'default', + normal: 'green', + arrears: 'orange', + stopped: 'red', + } + return map[value] ?? 'default' +} + +/** 运营商 → 中文 */ +export const carrierText = (value: string): string => + CARRIER_OPTIONS.find((item) => item.value === value)?.label ?? value ?? '—' diff --git a/src/pages/miniprogram/ops/workOrder/Home.vue b/src/pages/miniprogram/ops/workOrder/Home.vue index ef6b56c..05e8907 100644 --- a/src/pages/miniprogram/ops/workOrder/Home.vue +++ b/src/pages/miniprogram/ops/workOrder/Home.vue @@ -58,10 +58,7 @@ -
- - 扫码建单 -
+ @@ -71,7 +68,6 @@ * - 3 段状态 Tab(待处理 / 处理中 / 已完成) * - 类型筛选 + 关键字 * - 列表项点击进入详情 - * - 浮动按钮:扫码建单(跳扫码 Tab) */ import { onMounted, reactive, ref } from 'vue' import { useRouter } from 'vue-router' @@ -102,10 +98,6 @@ const goDetail = (id: string): void => { router.push(`/miniprogram/ops/work-order-detail/${id}`) } -const goScan = (): void => { - router.push('/miniprogram/ops/scan') -} - const loadList = (): void => { const params: WorkOrderListParams = { status: activeTab.value, @@ -148,28 +140,13 @@ onMounted(() => { .wo-home { min-height: 100%; background: var(--background); - padding-bottom: 80px; + padding-bottom: 16px; &__filter { background: #fff; border-bottom: 1px solid var(--border); } - &__add { - position: fixed; - right: 16px; - bottom: 80px; - background: var(--primary); - color: #fff; - border-radius: 24px; - padding: 10px 16px; - display: flex; - align-items: center; - gap: 4px; - font-size: var(--font-sm); - box-shadow: var(--shadow-md); - } - &__empty { padding: 32px 0; background: transparent; diff --git a/src/pages/miniprogram/ops/workbench/alarmList.vue b/src/pages/miniprogram/ops/workbench/alarmList.vue index 18046b8..beb01f6 100644 --- a/src/pages/miniprogram/ops/workbench/alarmList.vue +++ b/src/pages/miniprogram/ops/workbench/alarmList.vue @@ -1,5 +1,8 @@