From 68ea2a1167bcc1b34d2cf5a09318498e19a58ef4 Mon Sep 17 00:00:00 2001 From: fengpu <18691763612@163.com> Date: Thu, 9 Jul 2026 20:10:43 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E8=AE=BE=E5=A4=87=E4=B8=AD=E5=BF=83?= =?UTF-8?q?=E8=BF=90=E7=BB=B4/=E5=88=86=E5=8F=91/=E5=BA=93=E5=AD=98?= =?UTF-8?q?=E5=85=A8=E9=87=8F=E5=AE=8C=E5=96=84=20+=20=E8=8F=9C=E5=8D=95?= =?UTF-8?q?=E4=B8=8E=E5=85=AC=E5=85=B1=E7=BB=84=E4=BB=B6=E7=BB=86=E8=8A=82?= =?UTF-8?q?=E8=B0=83=E6=95=B4?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 运维(ops): 告警/工单/初始化/版本/监测详情抽屉与弹框全量完善,新增 versionList 子组件与 versionSet 弹框,废弃 config 模块 - 分发(distribute): 删除死信模块,记录下沉为配置子页,新增上报模块 - 库存(stock): 入库详情抽屉新增,出库详情/编辑完善 - 第三方认证: 高德/虹眼新增编辑流程完善 - 公共: FilterBar/Sidebar/MenuTreeNode/nav 细节调整 - 截图: SIM 卡导出任务/列表页截图归档 Co-Authored-By: Claude Opus 4.7 --- src/axios/handlers/admin/system.ts | 2 +- .../handlers/device-center/asset/manage.ts | 56 +- .../handlers/device-center/asset/outbound.ts | 79 ++- .../device-center/asset/statistics.ts | 4 +- .../handlers/device-center/asset/workOrder.ts | 238 ++++++- .../device-center/customer/archive.ts | 2 +- .../handlers/device-center/customer/site.ts | 6 +- .../device-center/distribute/config.ts | 408 ++++++------ .../device-center/distribute/deadletter.ts | 254 -------- .../device-center/distribute/record.ts | 56 +- .../device-center/distribute/report.ts | 310 +++++++++ src/axios/handlers/device-center/ops/alarm.ts | 240 ++++--- .../handlers/device-center/ops/config.ts | 203 ------ src/axios/handlers/device-center/ops/init.ts | 589 ++++++++++-------- .../handlers/device-center/ops/monitor.ts | 99 +-- .../handlers/device-center/ops/version.ts | 326 ++++++---- .../handlers/device-center/third-auth/amap.ts | 41 +- src/axios/handlers/index.ts | 10 +- src/components/FilterBar.vue | 8 +- src/layouts/components/MenuTreeNode.vue | 11 +- src/layouts/components/Sidebar.vue | 11 +- src/meta/nav.ts | 40 +- .../manage/component/drawer/info/info.vue | 19 +- .../component/drawer/info/types/index.ts | 4 +- .../component/modal/addOrEdit/addOrEdit.vue | 22 +- .../component/modal/addOrEdit/init/usePage.ts | 5 +- .../component/modal/addOrEdit/types/index.ts | 3 +- .../asset/manage/init/useTable.ts | 18 +- .../device-center/asset/manage/types/index.ts | 4 +- .../component/modal/addOrEdit/addOrEdit.vue | 2 +- .../distribute/config/api/index.ts | 37 ++ .../config/component/drawer/info/api/index.ts | 12 + .../config/component/drawer/info/info.vue | 64 ++ .../drawer/info/init/usePage.ts | 35 +- .../component/drawer/info/types/index.ts | 17 + .../component/modal/addOrEdit/addOrEdit.vue | 203 ++++++ .../component/modal/addOrEdit/api/index.ts | 11 +- .../component/modal/addOrEdit/init/usePage.ts | 236 +++++++ .../component/modal/addOrEdit/types/index.ts | 59 ++ .../component}/record/api/index.ts | 17 +- .../record/component/drawer/info/api/index.ts | 12 + .../record/component/drawer/info/info.vue | 43 +- .../component/drawer/info/init/usePage.ts | 9 +- .../component/drawer/info/types/index.ts | 17 + .../component}/record/init/usePage.ts | 30 +- .../config/component/record/init/useSearch.ts | 9 + .../component}/record/init/useTable.ts | 2 +- .../{ => config/component}/record/record.vue | 36 +- .../component}/record/types/index.ts | 8 +- .../component/tabs/downstream/api/index.ts | 32 - .../component/drawer/info/api/index.ts | 8 - .../downstream/component/drawer/info/info.vue | 53 -- .../component/drawer/info/types/index.ts | 15 - .../component/modal/addOrEdit/addOrEdit.vue | 108 ---- .../component/modal/addOrEdit/api/index.ts | 11 - .../component/modal/addOrEdit/init/usePage.ts | 85 --- .../component/modal/addOrEdit/types/index.ts | 12 - .../component/tabs/downstream/downstream.vue | 78 --- .../tabs/downstream/init/useTable.ts | 59 -- .../component/tabs/downstream/types/index.ts | 110 ---- .../component/tabs/subscribe/api/index.ts | 29 - .../component/modal/addOrEdit/addOrEdit.vue | 52 -- .../component/modal/addOrEdit/api/index.ts | 11 - .../component/modal/addOrEdit/init/usePage.ts | 74 --- .../component/modal/addOrEdit/types/index.ts | 15 - .../component/tabs/subscribe/init/usePage.ts | 161 ----- .../tabs/subscribe/init/useSearch.ts | 12 - .../component/tabs/subscribe/init/useTable.ts | 31 - .../component/tabs/subscribe/subscribe.vue | 76 --- .../component/tabs/subscribe/types/index.ts | 43 -- .../distribute/config/config.vue | 131 +++- .../distribute/config/init/usePage.ts | 172 +++++ .../tabs/downstream => }/init/useSearch.ts | 9 +- .../distribute/config/init/useTable.ts | 47 ++ .../distribute/config/types/index.ts | 124 ++++ .../distribute/deadletter/api/index.ts | 34 - .../deadletter/component/drawer/info/info.vue | 99 --- .../component/drawer/info/init/usePage.ts | 43 -- .../component/drawer/info/types/index.ts | 18 - .../distribute/deadletter/deadletter.vue | 134 ---- .../distribute/deadletter/init/usePage.ts | 252 -------- .../distribute/deadletter/init/useSearch.ts | 13 - .../distribute/deadletter/init/useTable.ts | 46 -- .../distribute/deadletter/types/index.ts | 86 --- .../record/component/drawer/info/api/index.ts | 11 - .../distribute/report/api/index.ts | 39 ++ .../report/component/drawer/info/api/index.ts | 12 + .../report/component/drawer/info/info.vue | 57 ++ .../component/drawer/info/init/usePage.ts | 47 ++ .../component/drawer/info/types/index.ts | 17 + .../component/modal/addOrEdit/addOrEdit.vue | 213 +++++++ .../component/modal/addOrEdit/api/index.ts | 21 + .../component/modal/addOrEdit/init/usePage.ts | 273 ++++++++ .../component/modal/addOrEdit/types/index.ts | 58 ++ .../distribute/report/init/usePage.ts | 166 +++++ .../{record => report}/init/useSearch.ts | 7 +- .../distribute/report/init/useTable.ts | 44 ++ .../distribute/report/report.vue | 113 ++++ .../distribute/report/types/index.ts | 116 ++++ .../device-center/ops/alarm/alarm.vue | 185 +++--- .../device-center/ops/alarm/api/index.ts | 15 +- .../alarm/component/drawer/info/api/index.ts | 9 + .../ops/alarm/component/drawer/info/info.vue | 122 ++++ .../component/drawer/info/init/usePage.ts | 76 +++ .../component/drawer/info/types/index.ts | 19 + .../device-center/ops/alarm/init/usePage.ts | 77 +-- .../device-center/ops/alarm/init/useSearch.ts | 13 +- .../device-center/ops/alarm/init/useTable.ts | 28 +- .../device-center/ops/alarm/types/index.ts | 67 +- .../device-center/ops/config/api/index.ts | 31 - .../component/drawer/history/api/index.ts | 11 - .../component/drawer/history/history.vue | 112 ---- .../component/drawer/history/init/usePage.ts | 55 -- .../component/drawer/history/types/index.ts | 18 - .../config/component/modal/edit/api/index.ts | 19 - .../ops/config/component/modal/edit/edit.vue | 63 -- .../component/modal/edit/init/usePage.ts | 122 ---- .../component/modal/edit/types/index.ts | 27 - .../device-center/ops/config/config.vue | 82 --- .../device-center/ops/config/init/usePage.ts | 95 --- .../ops/config/init/useSearch.ts | 8 - .../device-center/ops/config/init/useTable.ts | 31 - .../device-center/ops/config/types/index.ts | 81 --- .../device-center/ops/init/api/index.ts | 69 +- .../init/component/drawer/info/api/index.ts | 8 +- .../ops/init/component/drawer/info/info.vue | 212 ++++--- .../component/drawer/info/init/usePage.ts | 53 +- .../init/component/drawer/info/types/index.ts | 30 +- .../component/modal/addOrEdit/addOrEdit.vue | 171 ----- .../component/modal/addOrEdit/init/usePage.ts | 162 ----- .../component/modal/addOrEdit/types/index.ts | 50 -- .../component/modal/versionSet/api/index.ts | 25 + .../modal/versionSet/init/usePage.ts | 107 ++++ .../component/modal/versionSet/types/index.ts | 16 + .../component/modal/versionSet/versionSet.vue | 79 +++ .../device-center/ops/init/init.vue | 199 ++++-- .../device-center/ops/init/init/usePage.ts | 97 +-- .../device-center/ops/init/init/useSearch.ts | 9 +- .../device-center/ops/init/init/useTable.ts | 81 ++- .../device-center/ops/init/types/index.ts | 270 +++++--- .../device-center/ops/monitor/api/index.ts | 21 +- .../monitor/component/drawer/info/info.vue | 61 +- .../device-center/ops/monitor/init/usePage.ts | 17 +- .../ops/monitor/init/useTable.ts | 48 +- .../device-center/ops/monitor/monitor.vue | 34 +- .../device-center/ops/monitor/types/index.ts | 47 +- .../device-center/ops/version/api/index.ts | 22 +- .../version/component/drawer/info/info.vue | 82 --- .../component/drawer/info/types/index.ts | 18 - .../component/modal/addOrEdit/addOrEdit.vue | 35 +- .../component/modal/addOrEdit/api/index.ts | 6 +- .../component/modal/addOrEdit/init/usePage.ts | 33 +- .../component/modal/addOrEdit/types/index.ts | 20 +- .../component/versionList/api/index.ts | 26 + .../component}/drawer/info/api/index.ts | 5 +- .../component/drawer/info/info.vue | 90 +++ .../component}/drawer/info/init/usePage.ts | 5 +- .../component/drawer/info/types/index.ts | 9 +- .../component/modal/addOrEdit/addOrEdit.vue | 126 ++++ .../component/modal/addOrEdit/api/index.ts | 10 + .../component/modal/addOrEdit/init/usePage.ts | 152 +++++ .../component/modal/addOrEdit/types/index.ts | 53 ++ .../component/versionList}/init/usePage.ts | 106 ++-- .../component/versionList/init/useSearch.ts | 10 + .../component/versionList/init/useTable.ts | 18 + .../component/versionList/types/index.ts | 70 +++ .../component/versionList/versionList.vue | 117 ++++ .../device-center/ops/version/init/usePage.ts | 85 +-- .../ops/version/init/useSearch.ts | 6 +- .../ops/version/init/useTable.ts | 64 +- .../device-center/ops/version/types/index.ts | 169 ++--- .../device-center/ops/version/version.vue | 49 +- .../device-center/ops/workOrder/api/index.ts | 22 +- .../component/drawer/info/api/index.ts | 7 + .../workOrder/component/drawer/info/info.vue | 102 +++ .../component/drawer/info/init/usePage.ts | 49 ++ .../component/drawer/info/types/index.ts | 23 + .../component/modal/addOrEdit/addOrEdit.vue | 147 +++++ .../component/modal/addOrEdit/api/index.ts | 6 + .../component/modal/addOrEdit/init/usePage.ts | 104 ++++ .../component/modal/addOrEdit/types/index.ts | 27 + .../component/modal/dispatch/api/index.ts | 6 + .../component/modal/dispatch/dispatch.vue | 43 ++ .../component/modal/dispatch/init/usePage.ts | 87 +++ .../component/modal/dispatch/types/index.ts | 17 + .../component/modal/process/api/index.ts | 6 + .../component/modal/process/init/usePage.ts | 90 +++ .../component/modal/process/process.vue | 70 +++ .../component/modal/process/types/index.ts | 19 + .../ops/workOrder/init/usePage.ts | 167 ++++- .../ops/workOrder/init/useTable.ts | 2 +- .../ops/workOrder/types/index.ts | 87 ++- .../device-center/ops/workOrder/workOrder.vue | 37 +- .../component/drawer/info/api/index.ts | 5 +- .../inbound/component/drawer/info/info.vue | 124 ++++ .../component/drawer/info/init/usePage.ts | 53 ++ .../component/drawer/info/types/index.ts | 19 + .../device-center/stock/inbound/inbound.vue | 11 +- .../stock/inbound/init/usePage.ts | 11 +- .../outbound/component/drawer/info/info.vue | 53 ++ .../component/modal/addOrEdit/addOrEdit.vue | 142 ++++- .../component/modal/addOrEdit/init/usePage.ts | 118 +++- .../component/modal/addOrEdit/types/index.ts | 4 +- .../stock/outbound/init/useSearch.ts | 1 + .../device-center/stock/outbound/outbound.vue | 3 + .../stock/outbound/types/index.ts | 14 + .../device-center/third-auth/amap/amap.vue | 2 +- .../third-auth/amap/api/index.ts | 6 +- .../component/modal/addOrEdit/addOrEdit.vue | 29 +- .../component/modal/addOrEdit/init/usePage.ts | 30 +- .../component/modal/addOrEdit/types/index.ts | 4 +- .../third-auth/amap/types/index.ts | 10 + .../component/modal/addOrEdit/init/usePage.ts | 3 - 213 files changed, 8142 insertions(+), 5640 deletions(-) delete mode 100644 src/axios/handlers/device-center/distribute/deadletter.ts create mode 100644 src/axios/handlers/device-center/distribute/report.ts delete mode 100644 src/axios/handlers/device-center/ops/config.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/drawer/info/info.vue rename src/pages/admin-portal/device-center/distribute/config/component/{tabs/downstream/component => }/drawer/info/init/usePage.ts (58%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/drawer/info/types/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/addOrEdit.vue rename src/pages/admin-portal/device-center/{ops/init => distribute/config}/component/modal/addOrEdit/api/index.ts (64%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/types/index.ts rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/api/index.ts (56%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/api/index.ts rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/component/drawer/info/info.vue (65%) rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/component/drawer/info/init/usePage.ts (85%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/types/index.ts rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/init/usePage.ts (82%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/component/record/init/useSearch.ts rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/init/useTable.ts (97%) rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/record.vue (74%) rename src/pages/admin-portal/device-center/distribute/{ => config/component}/record/types/index.ts (92%) delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/info.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/addOrEdit.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/downstream.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useTable.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/addOrEdit.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useSearch.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useTable.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/subscribe.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/types/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/init/usePage.ts rename src/pages/admin-portal/device-center/distribute/config/{component/tabs/downstream => }/init/useSearch.ts (61%) create mode 100644 src/pages/admin-portal/device-center/distribute/config/init/useTable.ts create mode 100644 src/pages/admin-portal/device-center/distribute/config/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/info.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/deadletter.vue delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/init/useSearch.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/init/useTable.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/deadletter/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/distribute/record/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/drawer/info/info.vue create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/drawer/info/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/drawer/info/types/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/addOrEdit.vue create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/api/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/types/index.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/init/usePage.ts rename src/pages/admin-portal/device-center/distribute/{record => report}/init/useSearch.ts (64%) create mode 100644 src/pages/admin-portal/device-center/distribute/report/init/useTable.ts create mode 100644 src/pages/admin-portal/device-center/distribute/report/report.vue create mode 100644 src/pages/admin-portal/device-center/distribute/report/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/alarm/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/alarm/component/drawer/info/info.vue create mode 100644 src/pages/admin-portal/device-center/ops/alarm/component/drawer/info/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/alarm/component/drawer/info/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/drawer/history/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/drawer/history/history.vue delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/drawer/history/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/drawer/history/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/modal/edit/api/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/modal/edit/edit.vue delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/modal/edit/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/component/modal/edit/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/config.vue delete mode 100644 src/pages/admin-portal/device-center/ops/config/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/init/useSearch.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/init/useTable.ts delete mode 100644 src/pages/admin-portal/device-center/ops/config/types/index.ts delete mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/addOrEdit.vue delete mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/init/usePage.ts delete mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/versionSet/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/versionSet/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/versionSet/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/init/component/modal/versionSet/versionSet.vue delete mode 100644 src/pages/admin-portal/device-center/ops/version/component/drawer/info/info.vue delete mode 100644 src/pages/admin-portal/device-center/ops/version/component/drawer/info/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/api/index.ts rename src/pages/admin-portal/device-center/ops/version/component/{ => versionList/component}/drawer/info/api/index.ts (64%) create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/info.vue rename src/pages/admin-portal/device-center/ops/version/component/{ => versionList/component}/drawer/info/init/usePage.ts (88%) rename src/pages/admin-portal/device-center/{distribute/record => ops/version/component/versionList}/component/drawer/info/types/index.ts (63%) create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/addOrEdit.vue create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/types/index.ts rename src/pages/admin-portal/device-center/{distribute/config/component/tabs/downstream => ops/version/component/versionList}/init/usePage.ts (53%) create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/init/useSearch.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/init/useTable.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/version/component/versionList/versionList.vue create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/info.vue create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/addOrEdit.vue create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/dispatch.vue create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/types/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/api/index.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/process.vue create mode 100644 src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/types/index.ts rename src/pages/admin-portal/device-center/{distribute/deadletter => stock/inbound}/component/drawer/info/api/index.ts (63%) create mode 100644 src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/info.vue create mode 100644 src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/init/usePage.ts create mode 100644 src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/types/index.ts diff --git a/src/axios/handlers/admin/system.ts b/src/axios/handlers/admin/system.ts index 6876c95..c22a814 100644 --- a/src/axios/handlers/admin/system.ts +++ b/src/axios/handlers/admin/system.ts @@ -86,7 +86,7 @@ const dictionaryList: AdminRecord[] = [ ] const tenantList: AdminRecord[] = [ - { id: 'tenant-001', name: '长庆能源总部食堂', code: 'cq-energy', expireTime: '2027-06-30', status: true, createTime: '2026-05-08 09:18:00' }, + { id: 'tenant-001', name: '国庆能源总部食堂', code: 'cq-energy', expireTime: '2027-06-30', status: true, createTime: '2026-05-08 09:18:00' }, { id: 'tenant-002', name: '第一采油厂', code: 'oilfield-01', expireTime: '2027-05-31', status: true, createTime: '2026-05-09 10:22:00' }, { id: 'tenant-003', name: '炼化分公司', code: 'refining', expireTime: '2026-12-31', status: true, createTime: '2026-05-10 11:35:00' }, { id: 'tenant-004', name: '健康示范园区', code: 'health-park', expireTime: '2026-10-31', status: false, createTime: '2026-05-11 14:06:00' }, diff --git a/src/axios/handlers/device-center/asset/manage.ts b/src/axios/handlers/device-center/asset/manage.ts index a24dd18..e539d99 100644 --- a/src/axios/handlers/device-center/asset/manage.ts +++ b/src/axios/handlers/device-center/asset/manage.ts @@ -36,6 +36,8 @@ interface DeviceItem { siteId: string siteName: string deployStatus: DeployStatus + /** 软件包名(初始化设置时写入,与 softwareVersion 同时产生) */ + packageName: string softwareVersion: string onlineTime: string deployTime: string @@ -126,7 +128,7 @@ const CATEGORY_META: Record = { /** 客户下拉数据源(与 customer/archive mock 对齐,精简为 id+name) */ const customerOptionList: CustomerOption[] = [ - { id: 'cust-001', name: '长庆能源总部食堂' }, + { id: 'cust-001', name: '国庆能源总部食堂' }, { id: 'cust-002', name: '第一采油厂' }, { id: 'cust-003', name: '炼化分公司' }, { id: 'cust-004', name: '健康示范园区' }, @@ -136,7 +138,7 @@ const customerOptionList: CustomerOption[] = [ /** 场所下拉数据源(与 customer/site mock 对齐,精简为 id+name+customerId) */ const siteOptionList: SiteOption[] = [ - { id: 'site-001', name: '长庆总部一食堂', customerId: 'cust-001' }, + { 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' }, @@ -177,7 +179,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -186,7 +188,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -195,7 +197,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '智养餐饮', 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', @@ -204,7 +206,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -213,7 +215,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -222,7 +224,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -230,8 +232,8 @@ const dataset: Record = { { 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', + customerId: 'cust-001', customerName: '国庆能源总部食堂', siteId: 'site-001', siteName: '国庆总部一食堂', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -240,7 +242,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智养餐饮', 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', @@ -249,7 +251,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '', softwareVersion: '', onlineTime: '2026-06-02 09:00:00', deployTime: '', arcLicense: '', simIccid: '', isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '华为', model: 'HW-CAB-100L', @@ -258,7 +260,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '', softwareVersion: '', onlineTime: '2026-06-05 15:00:00', deployTime: '', arcLicense: '', simIccid: '', isIot: true, dataTransit: false, dataReport: false, networkMode: 'wifi', manufacturer: '商合', model: 'SH-TP-200', @@ -269,7 +271,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '体重测量', 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', @@ -278,7 +280,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '体重测量', 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', @@ -287,7 +289,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '体重测量', 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', @@ -298,7 +300,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智能穿戴', 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', @@ -307,7 +309,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智能穿戴', 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', @@ -316,7 +318,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '智能穿戴', 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', @@ -327,7 +329,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '健身管理', 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', @@ -336,7 +338,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '健身管理', 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', @@ -345,7 +347,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '健身管理', 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', @@ -356,7 +358,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '健康监测', 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', @@ -365,7 +367,7 @@ const dataset: Record = { 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', + deployStatus: 'deployed', packageName: '健康监测', 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', @@ -374,7 +376,7 @@ const dataset: Record = { 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', + deployStatus: 'in_stock', packageName: '健康监测', 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', @@ -446,6 +448,9 @@ const handleAdd = (ctx: MockContext): ApiResponse => { const categoryTab = resolveCategoryTab(payload.deviceCategory) const base: DeviceItem = { ...payload, + /** 新增设备未经初始化设置,packageName / softwareVersion 由后续初始化写入 */ + packageName: '', + softwareVersion: '', customerName, siteName, deployStatus, @@ -587,6 +592,7 @@ export const pushDevicesFromInbound = (record: InboundRecordInput): number => { siteId: '', siteName: '', deployStatus: 'in_stock', + packageName: '', softwareVersion: '', onlineTime: now, deployTime: '', diff --git a/src/axios/handlers/device-center/asset/outbound.ts b/src/axios/handlers/device-center/asset/outbound.ts index b2c06bd..837e90c 100644 --- a/src/axios/handlers/device-center/asset/outbound.ts +++ b/src/axios/handlers/device-center/asset/outbound.ts @@ -12,9 +12,15 @@ interface PageParams { categoryId?: string warehouseAreaId?: string customerId?: string + deviceCode?: string [key: string]: unknown } +interface OutboundDetailItem { + deviceCode: string + serialNumber: string +} + interface OutboundRecord { id: string outboundCode: string @@ -34,6 +40,8 @@ interface OutboundRecord { warehouseAreaId?: string warehouseAreaName?: string remark?: string + /** 设备明细(每行一台出库设备) */ + items?: OutboundDetailItem[] } const getParams = (params: RequestParameter): PageParams => (params ?? {}) as PageParams @@ -91,7 +99,7 @@ const WAREHOUSE_AREA_NAME: Record = { /** 客户 ID → 名称映射(与 manage/statistics mock 同源) */ const CUSTOMER_NAME: Record = { - 'cust-001': '长庆能源总部食堂', + 'cust-001': '国庆能源总部食堂', 'cust-002': '第一采油厂', 'cust-003': '炼化分公司', 'cust-004': '健康示范园区', @@ -105,14 +113,38 @@ const resolveWarehouseAreaName = (id?: string): string => (id ? WAREHOUSE_AREA_N const resolveCustomerName = (id?: string): string => (id ? CUSTOMER_NAME[id] ?? '' : '') const outboundList: OutboundRecord[] = [ - { 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楼待检区-临时储位' }, + { 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号储位', items: [ + { deviceCode: 'CAB-2026-001', serialNumber: 'SN-CAB-001' }, + { deviceCode: 'CAB-2026-002', serialNumber: 'SN-CAB-002' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'HW5-2026-001', serialNumber: 'SN-HW5-001' }, + { deviceCode: 'HW5-2026-002', serialNumber: 'SN-HW5-002' }, + { deviceCode: 'HW5-2026-003', serialNumber: 'SN-HW5-003' }, + { deviceCode: 'HW5-2026-004', serialNumber: 'SN-HW5-004' }, + { deviceCode: 'HW5-2026-005', serialNumber: 'SN-HW5-005' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'AIO-2026-001', serialNumber: 'SN-AIO-001' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'IBS-2026-001', serialNumber: 'SN-IBS-001' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'BIKE-2026-001', serialNumber: 'SN-BIKE-001' }, + { deviceCode: 'BIKE-2026-002', serialNumber: 'SN-BIKE-002' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'BFS-2026-001', serialNumber: 'SN-BFS-001' }, + ] }, + { 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号储位', items: [ + { deviceCode: 'BTS-2026-001', serialNumber: 'SN-BTS-001' }, + { deviceCode: 'BTS-2026-002', serialNumber: 'SN-BTS-002' }, + { deviceCode: 'BTS-2026-003', serialNumber: 'SN-BTS-003' }, + ] }, + { 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楼待检区-临时储位', items: [ + { deviceCode: 'BFT-2026-001', serialNumber: 'SN-BFT-001' }, + ] }, ] const listPage = (ctx: MockContext): ApiResponse => { @@ -124,6 +156,11 @@ const listPage = (ctx: MockContext): ApiResponse => { 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 + // 设备编码:按出库明细 items[].deviceCode 模糊匹配,空明细跳过 + if (params.deviceCode) { + const matched = (item.items ?? []).some((entry) => includes(entry.deviceCode, params.deviceCode)) + if (!matched) return false + } return true }) if (params.column === 'outboundTime' && params.order) { @@ -155,16 +192,32 @@ const genOutboundCode = (): string => { return `OUB-${ymd}-${seq}` } +/** 解析 items 数组参数 */ +const parseItems = (raw: unknown): OutboundDetailItem[] => { + 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 OutboundDetailItem => item !== null && item.deviceCode.length > 0) +} + const addRecord = (params: PageParams): ApiResponse => { const categoryId = String(params.categoryId ?? '') const customerId = String(params.customerId ?? '') const warehouseAreaId = params.warehouseAreaId ? String(params.warehouseAreaId) : undefined + const items = parseItems(params.items) const record: OutboundRecord = { id: `oub-${Date.now().toString(36)}`, outboundCode: String(params.outboundCode ?? genOutboundCode()), categoryId, categoryName: resolveCategoryName(categoryId), - quantity: Number(params.quantity ?? 1), + quantity: items.length || Number(params.quantity ?? 1), recipient: String(params.recipient ?? ''), targetSite: String(params.targetSite ?? ''), customerId, @@ -176,6 +229,7 @@ const addRecord = (params: PageParams): ApiResponse => { warehouseAreaId, warehouseAreaName: resolveWarehouseAreaName(warehouseAreaId), remark: params.remark ? String(params.remark) : undefined, + items, } outboundList.unshift(record) return ok('新增成功') @@ -189,7 +243,6 @@ const updateRecord = (params: PageParams): ApiResponse => { 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.customerId !== undefined) { @@ -202,6 +255,10 @@ const updateRecord = (params: PageParams): ApiResponse => { 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('更新成功') } diff --git a/src/axios/handlers/device-center/asset/statistics.ts b/src/axios/handlers/device-center/asset/statistics.ts index 7bedba3..ff37a6f 100644 --- a/src/axios/handlers/device-center/asset/statistics.ts +++ b/src/axios/handlers/device-center/asset/statistics.ts @@ -101,7 +101,7 @@ const CATEGORY_LEAF_NAME: Record = { /** 客户下拉数据源(与 manage mock 同源,保持 id+name 一致) */ const customerOptionList: CustomerOption[] = [ - { id: 'cust-001', name: '长庆能源总部食堂' }, + { id: 'cust-001', name: '国庆能源总部食堂' }, { id: 'cust-002', name: '第一采油厂' }, { id: 'cust-003', name: '炼化分公司' }, { id: 'cust-004', name: '健康示范园区' }, @@ -112,7 +112,7 @@ const customerOptionList: CustomerOption[] = [ /** 场所下拉数据源(与 manage mock 同源,按 customerId 联动) */ const siteOptionList: SiteOption[] = [ - { id: 'site-001', name: '长庆总部一食堂', customerId: 'cust-001' }, + { 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' }, diff --git a/src/axios/handlers/device-center/asset/workOrder.ts b/src/axios/handlers/device-center/asset/workOrder.ts index 63b5d27..598a2aa 100644 --- a/src/axios/handlers/device-center/asset/workOrder.ts +++ b/src/axios/handlers/device-center/asset/workOrder.ts @@ -1,6 +1,12 @@ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext, RequestParameter } from '@axios' +/** + * 设备运维 / 工单管理 mock + * - 支持分页查询、详情(含处理记录时间线)、新增、派单、处理、删除 + * - 状态流转:pending → assigned → processing → closed + */ + interface PageParams { pageNum?: number pageSize?: number @@ -15,6 +21,14 @@ interface PageParams { [key: string]: unknown } +interface ProcessLog { + id: string + action: 'create' | 'dispatch' | 'start' | 'progress' | 'close' + operator: string + operateTime: string + remark: string +} + interface WorkOrderRecord { id: string orderCode: string @@ -24,20 +38,26 @@ interface WorkOrderRecord { customerName: string source: 'customer' | 'inspection' | 'self-check' | 'after-sale' faultLevel: 'low' | 'medium' | 'high' | 'critical' + faultType: 'hardware' | 'software' | 'network' | 'mechanical' | 'electrical' | 'other' faultDesc: string + images: string[] status: 'pending' | 'assigned' | 'processing' | 'closed' assignee?: string reportTime: string slaDeadline: string slaOverdue: boolean + processLogs: ProcessLog[] } const getParams = (params: RequestParameter): PageParams => (params ?? {}) as PageParams + const includes = (value: unknown, keyword: unknown): boolean => { if (!keyword) return true return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) } + const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) + const paginate = (list: T[], params: PageParams): ApiResponse => { const pageNum = Number(params.pageNum ?? 1) const pageSize = Number(params.pageSize ?? 30) @@ -45,15 +65,117 @@ const paginate = (list: T[], params: PageParams): Api return { code: '00000', msg: '查询成功', data: list.slice(start, start + pageSize), total: list.length } } +const createProcessLogs = (recordId: string): Record => ({ + 'wo-001': [ + { id: 'pl-001-1', action: 'create', operator: '张客户', operateTime: '2026-06-20 09:15:00', remark: '客户报障:柜门无法开启,疑似锁具故障' }, + { id: 'pl-001-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-20 09:25:00', remark: '派单给 李工程师' }, + { id: 'pl-001-3', action: 'start', operator: '李工程师', operateTime: '2026-06-20 10:30:00', remark: '已到达现场,开始排查' }, + { id: 'pl-001-4', action: 'progress', operator: '李工程师', operateTime: '2026-06-20 14:20:00', remark: '更换锁具主板,正在测试' }, + { id: 'pl-001-5', action: 'close', operator: '李工程师', operateTime: '2026-06-20 16:45:00', remark: '维修完成,柜门开关正常' }, + ], + 'wo-002': [ + { id: 'pl-002-1', action: 'create', operator: '系统', operateTime: '2026-06-22 14:30:00', remark: '巡检异常:心率采集异常,间歇性无数据' }, + { id: 'pl-002-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-22 15:00:00', remark: '派单给 王工程师' }, + { id: 'pl-002-3', action: 'start', operator: '王工程师', operateTime: '2026-06-22 16:10:00', remark: '开始排查传感器' }, + { id: 'pl-002-4', action: 'progress', operator: '王工程师', operateTime: '2026-06-23 09:20:00', remark: '已更换心率传感器,正在测试中' }, + ], + 'wo-003': [ + { id: 'pl-003-1', action: 'create', operator: '李经理', operateTime: '2026-06-24 10:00:00', remark: '设备完全无响应,无法开机' }, + ], + 'wo-004': [ + { id: 'pl-004-1', action: 'create', operator: '系统', operateTime: '2026-06-25 16:20:00', remark: '自检异常:称重数据偏差,校准无效' }, + { id: 'pl-004-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-25 16:30:00', remark: '派单给 赵工程师' }, + ], + 'wo-005': [ + { id: 'pl-005-1', action: 'create', operator: '客服', operateTime: '2026-06-26 11:45:00', remark: '售后转单:用户反馈界面字体显示异常' }, + { id: 'pl-005-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-26 12:00:00', remark: '派单给 孙工程师' }, + { id: 'pl-005-3', action: 'start', operator: '孙工程师', operateTime: '2026-06-26 13:30:00', remark: '远程连接,开始排查' }, + { id: 'pl-005-4', action: 'progress', operator: '孙工程师', operateTime: '2026-06-26 15:00:00', remark: '更新固件到 V2.1.0' }, + { id: 'pl-005-5', action: 'close', operator: '孙工程师', operateTime: '2026-06-27 09:00:00', remark: '字体显示恢复正常,客户确认' }, + ], + 'wo-006': [ + { id: 'pl-006-1', action: 'create', operator: '张主管', operateTime: '2026-06-27 09:00:00', remark: '客户报障:阻力调节失灵,无法增减阻力' }, + { id: 'pl-006-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-27 09:15:00', remark: '派单给 李工程师' }, + { id: 'pl-006-3', action: 'start', operator: '李工程师', operateTime: '2026-06-27 10:00:00', remark: '到达现场,开始排查' }, + { id: 'pl-006-4', action: 'progress', operator: '李工程师', operateTime: '2026-06-27 14:00:00', remark: '阻力电机已更换,测试通过' }, + ], + 'wo-007': [ + { id: 'pl-007-1', action: 'create', operator: '巡检系统', operateTime: '2026-06-28 15:30:00', remark: '巡检异常:屏幕局部亮点,不影响显示' }, + ], + 'wo-008': [ + { id: 'pl-008-1', action: 'create', operator: '王客户', operateTime: '2026-06-29 13:00:00', remark: '客户报障:蓝牙连接频繁断开' }, + { id: 'pl-008-2', action: 'dispatch', operator: '系统', operateTime: '2026-06-29 13:15:00', remark: '派单给 王工程师' }, + { id: 'pl-008-3', action: 'start', operator: '王工程师', operateTime: '2026-06-29 14:00:00', remark: '开始排查蓝牙模块' }, + { id: 'pl-008-4', action: 'progress', operator: '王工程师', operateTime: '2026-06-29 16:30:00', remark: '更换蓝牙天线,信号强度正常' }, + { id: 'pl-008-5', action: 'close', operator: '王工程师', operateTime: '2026-06-30 09:00:00', remark: '蓝牙连接稳定,已交付客户' }, + ], +}) + const workOrderList: WorkOrderRecord[] = [ - { id: 'wo-001', orderCode: 'WO-20260620-001', deviceCode: 'CAB-A1-0001', deviceName: '总部食堂货柜', siteName: '总部食堂', customerName: '智养餐饮集团', source: 'customer', faultLevel: 'high', faultDesc: '柜门无法开启,疑似锁具故障', status: 'closed', assignee: '李工程师', reportTime: '2026-06-20 09:15:00', slaDeadline: '2026-06-20 18:00:00', slaOverdue: false }, - { id: 'wo-002', orderCode: 'WO-20260622-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', siteName: '健康站01', customerName: '健康管理机构', source: 'inspection', faultLevel: 'medium', faultDesc: '心率采集异常,间歇性无数据', status: 'processing', assignee: '王工程师', reportTime: '2026-06-22 14:30:00', slaDeadline: '2026-06-23 18:00:00', slaOverdue: false }, - { id: 'wo-003', orderCode: 'WO-20260624-003', deviceCode: 'CHK-M1-0001', deviceName: '体检站一体机', siteName: '东区体检站', customerName: '健康管理机构', source: 'customer', faultLevel: 'critical', faultDesc: '设备完全无响应,无法开机', status: 'pending', reportTime: '2026-06-24 10:00:00', slaDeadline: '2026-06-24 22:00:00', slaOverdue: true }, - { id: 'wo-004', orderCode: 'WO-20260625-004', deviceCode: 'SCL-X1-0001', deviceName: '总部入库秤', siteName: '总部食堂', customerName: '智养餐饮集团', source: 'self-check', faultLevel: 'medium', faultDesc: '称重数据偏差,校准无效', status: 'assigned', assignee: '赵工程师', reportTime: '2026-06-25 16:20:00', slaDeadline: '2026-06-26 18:00:00', slaOverdue: false }, - { id: 'wo-005', orderCode: 'WO-20260626-005', deviceCode: 'FAT-INBODY-0001', deviceName: '体检站体脂秤', siteName: '东区体检站', customerName: '健康管理机构', source: 'after-sale', faultLevel: 'low', faultDesc: '用户反馈界面字体显示异常', status: 'closed', assignee: '孙工程师', reportTime: '2026-06-26 11:45:00', slaDeadline: '2026-06-28 18:00:00', slaOverdue: false }, - { id: 'wo-006', orderCode: 'WO-20260627-006', deviceCode: 'FIT-S1-0001', deviceName: '健身房顿米车', siteName: '总部健身房', customerName: '智养餐饮集团', source: 'customer', faultLevel: 'high', faultDesc: '阻力调节失灵,无法增减阻力', status: 'processing', assignee: '李工程师', reportTime: '2026-06-27 09:00:00', slaDeadline: '2026-06-27 22:00:00', slaOverdue: true }, - { id: 'wo-007', orderCode: 'WO-20260628-007', deviceCode: 'SCR-P1-0001', deviceName: '总部大屏', siteName: '总部食堂', customerName: '智养餐饮集团', source: 'inspection', faultLevel: 'low', faultDesc: '屏幕局部亮点,不影响显示', status: 'pending', reportTime: '2026-06-28 15:30:00', slaDeadline: '2026-07-01 18:00:00', slaOverdue: false }, - { id: 'wo-008', orderCode: 'WO-20260629-008', deviceCode: 'SCL-W-0001', deviceName: '健康站体重秤', siteName: '健康站01', customerName: '健康管理机构', source: 'customer', faultLevel: 'medium', faultDesc: '蓝牙连接频繁断开', status: 'closed', assignee: '王工程师', reportTime: '2026-06-29 13:00:00', slaDeadline: '2026-06-30 18:00:00', slaOverdue: false }, + { + id: 'wo-001', orderCode: 'WO-20260620-001', deviceCode: 'CAB-A1-0001', deviceName: '总部食堂货柜', + siteName: '总部食堂', customerName: '智养餐饮集团', source: 'customer', faultLevel: 'high', + faultType: 'mechanical', faultDesc: '柜门无法开启,疑似锁具故障', images: [], + status: 'closed', assignee: '李工程师', reportTime: '2026-06-20 09:15:00', + slaDeadline: '2026-06-20 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-001')['wo-001'], + }, + { + id: 'wo-002', orderCode: 'WO-20260622-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', + siteName: '健康站01', customerName: '健康管理机构', source: 'inspection', faultLevel: 'medium', + faultType: 'hardware', faultDesc: '心率采集异常,间歇性无数据', images: [], + status: 'processing', assignee: '王工程师', reportTime: '2026-06-22 14:30:00', + slaDeadline: '2026-06-23 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-002')['wo-002'], + }, + { + id: 'wo-003', orderCode: 'WO-20260624-003', deviceCode: 'CHK-M1-0001', deviceName: '体检站一体机', + siteName: '东区体检站', customerName: '健康管理机构', source: 'customer', faultLevel: 'critical', + faultType: 'electrical', faultDesc: '设备完全无响应,无法开机', images: [], + status: 'pending', reportTime: '2026-06-24 10:00:00', + slaDeadline: '2026-06-24 22:00:00', slaOverdue: true, + processLogs: createProcessLogs('wo-003')['wo-003'], + }, + { + id: 'wo-004', orderCode: 'WO-20260625-004', deviceCode: 'SCL-X1-0001', deviceName: '总部入库秤', + siteName: '总部食堂', customerName: '智养餐饮集团', source: 'self-check', faultLevel: 'medium', + faultType: 'hardware', faultDesc: '称重数据偏差,校准无效', images: [], + status: 'assigned', assignee: '赵工程师', reportTime: '2026-06-25 16:20:00', + slaDeadline: '2026-06-26 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-004')['wo-004'], + }, + { + id: 'wo-005', orderCode: 'WO-20260626-005', deviceCode: 'FAT-INBODY-0001', deviceName: '体检站体脂秤', + siteName: '东区体检站', customerName: '健康管理机构', source: 'after-sale', faultLevel: 'low', + faultType: 'software', faultDesc: '用户反馈界面字体显示异常', images: [], + status: 'closed', assignee: '孙工程师', reportTime: '2026-06-26 11:45:00', + slaDeadline: '2026-06-28 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-005')['wo-005'], + }, + { + id: 'wo-006', orderCode: 'WO-20260627-006', deviceCode: 'FIT-S1-0001', deviceName: '健身房顿米车', + siteName: '总部健身房', customerName: '智养餐饮集团', source: 'customer', faultLevel: 'high', + faultType: 'mechanical', faultDesc: '阻力调节失灵,无法增减阻力', images: [], + status: 'processing', assignee: '李工程师', reportTime: '2026-06-27 09:00:00', + slaDeadline: '2026-06-27 22:00:00', slaOverdue: true, + processLogs: createProcessLogs('wo-006')['wo-006'], + }, + { + id: 'wo-007', orderCode: 'WO-20260628-007', deviceCode: 'SCR-P1-0001', deviceName: '总部大屏', + siteName: '总部食堂', customerName: '智养餐饮集团', source: 'inspection', faultLevel: 'low', + faultType: 'hardware', faultDesc: '屏幕局部亮点,不影响显示', images: [], + status: 'pending', reportTime: '2026-06-28 15:30:00', + slaDeadline: '2026-07-01 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-007')['wo-007'], + }, + { + id: 'wo-008', orderCode: 'WO-20260629-008', deviceCode: 'SCL-W-0001', deviceName: '健康站体重秤', + siteName: '健康站01', customerName: '健康管理机构', source: 'customer', faultLevel: 'medium', + faultType: 'hardware', faultDesc: '蓝牙连接频繁断开', images: [], + status: 'closed', assignee: '王工程师', reportTime: '2026-06-29 13:00:00', + slaDeadline: '2026-06-30 18:00:00', slaOverdue: false, + processLogs: createProcessLogs('wo-008')['wo-008'], + }, ] const listPage = (ctx: MockContext): ApiResponse => { @@ -73,19 +195,96 @@ const listPage = (ctx: MockContext): ApiResponse => { return paginate(filtered, params) } -const handleTransition = (params: PageParams): ApiResponse => { +const infoDetail = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) const id = String(params.id ?? '') const item = workOrderList.find((o) => o.id === id) if (!item) return ok('工单不存在') - const toStatus = params.toStatus as WorkOrderRecord['status'] - if (!toStatus) return ok('目标状态不能为空') - item.status = toStatus - if (toStatus === 'closed') { - item.slaOverdue = false - } else if (params.assignee) { - item.assignee = params.assignee + return ok('查询成功', item) +} + +const addWorkOrder = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const now = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + const orderCode = `WO-${now.getFullYear()}${pad(now.getMonth() + 1)}${pad(now.getDate())}-${String(workOrderList.length + 1).padStart(3, '0')}` + const createTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + const record: WorkOrderRecord = { + id: `wo-${Date.now().toString(36)}`, + orderCode, + deviceCode: String(params.deviceCode ?? ''), + deviceName: String(params.deviceName ?? ''), + siteName: String(params.siteName ?? ''), + customerName: String(params.customerName ?? ''), + source: (params.source as WorkOrderRecord['source']) ?? 'customer', + faultLevel: (params.faultLevel as WorkOrderRecord['faultLevel']) ?? 'low', + faultType: (params.faultType as WorkOrderRecord['faultType']) ?? 'other', + faultDesc: String(params.faultDesc ?? ''), + images: Array.isArray(params.images) ? params.images.map(String) : [], + status: 'pending', + reportTime: createTime, + slaDeadline: String(params.slaDeadline ?? ''), + slaOverdue: false, + processLogs: [ + { id: `pl-${Date.now()}-0`, action: 'create', operator: '当前用户', operateTime: createTime, remark: String(params.faultDesc ?? '') }, + ], } - return ok('工单状态已更新') + workOrderList.unshift(record) + return ok('新增成功') +} + +const dispatchWorkOrder = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = String(params.id ?? '') + const item = workOrderList.find((o) => o.id === id) + if (!item) return ok('工单不存在') + const assignee = String(params.assignee ?? '') + const now = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + const operateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + item.assignee = assignee + item.status = 'assigned' + item.processLogs.push({ + id: `pl-${Date.now()}`, + action: 'dispatch', + operator: '当前用户', + operateTime, + remark: `派单给 ${assignee}${params.remark ? ',备注:' + String(params.remark) : ''}`, + }) + return ok('派单成功') +} + +const processWorkOrder = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = String(params.id ?? '') + const item = workOrderList.find((o) => o.id === id) + if (!item) return ok('工单不存在') + const remark = String(params.remark ?? '') + const now = new Date() + const pad = (n: number): string => String(n).padStart(2, '0') + const operateTime = `${now.getFullYear()}-${pad(now.getMonth() + 1)}-${pad(now.getDate())} ${pad(now.getHours())}:${pad(now.getMinutes())}:${pad(now.getSeconds())}` + const fromStatus = item.status + if (fromStatus === 'pending' || fromStatus === 'assigned') { + item.status = 'processing' + item.processLogs.push({ + id: `pl-${Date.now()}`, + action: 'start', + operator: '当前用户', + operateTime, + remark, + }) + } else if (fromStatus === 'processing') { + item.status = 'closed' + item.slaOverdue = false + item.processLogs.push({ + id: `pl-${Date.now()}`, + action: 'close', + operator: '当前用户', + operateTime, + remark, + }) + } + return ok('处理成功') } const deleteById = (params: PageParams): ApiResponse => { @@ -97,6 +296,9 @@ const deleteById = (params: PageParams): ApiResponse => { registerMocks([ { method: 'POST', url: '/admin/device/repair/work-order/page', handler: listPage }, - { method: 'POST', url: '/admin/device/repair/work-order/transition', handler: (ctx) => handleTransition(getParams(ctx.params)) }, + { method: 'POST', url: '/admin/device/repair/work-order/info', handler: infoDetail }, + { method: 'POST', url: '/admin/device/repair/work-order/add', handler: addWorkOrder }, + { method: 'POST', url: '/admin/device/repair/work-order/dispatch', handler: dispatchWorkOrder }, + { method: 'POST', url: '/admin/device/repair/work-order/process', handler: processWorkOrder }, { method: 'POST', url: '/admin/device/repair/work-order/delete', handler: (ctx) => deleteById(getParams(ctx.params)) }, ]) diff --git a/src/axios/handlers/device-center/customer/archive.ts b/src/axios/handlers/device-center/customer/archive.ts index fe0c3ea..ba2e0cc 100644 --- a/src/axios/handlers/device-center/customer/archive.ts +++ b/src/axios/handlers/device-center/customer/archive.ts @@ -63,7 +63,7 @@ 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', 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-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' }, diff --git a/src/axios/handlers/device-center/customer/site.ts b/src/axios/handlers/device-center/customer/site.ts index 1d2218a..a55621d 100644 --- a/src/axios/handlers/device-center/customer/site.ts +++ b/src/axios/handlers/device-center/customer/site.ts @@ -70,7 +70,7 @@ const ok = (msg: string, data: T = null as T): ApiResponse => ({ co const siteList: SiteRecord[] = [ { - id: 'site-001', name: '长庆总部一食堂', code: 'CQ-CTN-01', customerId: 'cust-001', customerName: '长庆能源总部食堂', + id: 'site-001', name: '国庆总部一食堂', code: 'CQ-CTN-01', customerId: 'cust-001', customerName: '国庆能源总部食堂', department: '总部后勤', address: '西安市未央区凤城八路 1 号', contactName: '王经理', contactPhone: '13900010001', siteType: 'canteen', customFlag: 'A01A02A01', hasFrontendServer: true, frontendServerStatus: 'online', lastHeartbeatTime: '2026-07-08 09:18:30', @@ -101,7 +101,7 @@ const siteList: SiteRecord[] = [ deviceCount: 4, status: true, createTime: '2026-05-18 16:45:09', }, { - id: 'site-005', name: '总部体重测量点 A', code: 'CQ-MSR-A', customerId: 'cust-001', customerName: '长庆能源总部食堂', + id: 'site-005', name: '总部体重测量点 A', code: 'CQ-MSR-A', customerId: 'cust-001', customerName: '国庆能源总部食堂', department: '总部后勤', address: '西安市未央区凤城八路 1 号 二楼', contactName: '李护士', contactPhone: '13900010005', siteType: 'measure-point', customFlag: 'A01B02A02', hasFrontendServer: false, @@ -157,7 +157,7 @@ const listSite = (ctx: MockContext): ApiResponse => { /** 客户档案字典(与 archive.ts mock 同源,用于 customerId → customerName 反查) */ const customerDict: Record = { - 'cust-001': '长庆能源总部食堂', + 'cust-001': '国庆能源总部食堂', 'cust-002': '第一采油厂', 'cust-003': '炼化分公司', 'cust-004': '健康示范园区', diff --git a/src/axios/handlers/device-center/distribute/config.ts b/src/axios/handlers/device-center/distribute/config.ts index 3c3d89f..77d7ec6 100644 --- a/src/axios/handlers/device-center/distribute/config.ts +++ b/src/axios/handlers/device-center/distribute/config.ts @@ -3,53 +3,52 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 分发配置 mock 数据 - * - 下游系统管理(CRUD + 启用/禁用) - * - 订阅映射(CRUD + 启用/禁用 + 下游下拉选项) + * - 单列表 CRUD(无 Tab) + * - 字段:下游系统名称 / 系统标识 / 所属客户 / 数据分发地址 / 分发模式 / 鉴权开关 / + * 鉴权账号 / 鉴权秘钥 / 鉴权接口地址 / 状态 / 覆盖设备(设备分类多选) / + * 最大重试次数 / 最新通讯时间 / 备注 + * - 客户下拉复用客户档案列表接口(/admin/device/customer/archive/page,由 customer/archive.ts 注册) + * - 设备分类树复用 /admin/device/category/tree(由 asset/category.ts 注册) */ -interface DownstreamItem { +interface ConfigItem { id: string name: string code: string + customerId: string + customerName: string callbackUrl: string - authType: 'none' | 'apikey' | 'hmac' | 'token' - authKey: string - dataTypes: string[] - retryStrategy: 'exponential' | 'fixed' | 'none' + dispatchMode: 'realtime' | 'polling' + authEnabled: boolean + authAccount?: string + authSecret?: string + authApiUrl?: string + status: 'enabled' | 'disabled' + deviceCategoryIds: string[] + deviceCategoryNames: string[] maxRetry: number - status: 'enabled' | 'disabled' + lastCommunicateTime?: string remark?: string - createdAt: string + createTime?: string } -interface SubscribeItem { - id: string - downstreamId: string - downstreamName: string - deviceCode: string - deviceName: string - dataType: string - filterRule: string - status: 'enabled' | 'disabled' - createdAt: string -} - -interface BaseParams { +interface ConfigParams { id?: string name?: string - authType?: string - retryStrategy?: string - status?: string - downstreamId?: string - deviceCode?: string - dataType?: string code?: string + customerId?: string callbackUrl?: string - authKey?: string - dataTypes?: string[] + dispatchMode?: string + authEnabled?: boolean + authAccount?: string + authSecret?: string + authApiUrl?: string + status?: string + deviceCategoryIds?: string[] maxRetry?: number remark?: string - filterRule?: string + lastCommunicateTimeStart?: string + lastCommunicateTimeEnd?: string column?: string order?: 'ascend' | 'descend' | null pageNum?: number @@ -57,269 +56,216 @@ interface BaseParams { [key: string]: unknown } -const getP = (params: RequestParameter): BaseParams => (params ?? {}) as BaseParams +const getP = (params: RequestParameter): ConfigParams => (params ?? {}) as ConfigParams const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) -const downstreamList: DownstreamItem[] = [ +/** 设备分类 ID → 名称映射(与 asset/category mock 真实树节点保持一致) */ +const categoryDict: Record = { + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-body-all-in-one': '一体机', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-bike': '健身车', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +const categoryName = (id: string): string => categoryDict[id] ?? id + +const configList: ConfigItem[] = [ { id: 'DS-001', name: '体重管理系统', code: 'WEIGHT-MGMT', + customerId: 'cust-001', customerName: '国庆能源总部食堂', callbackUrl: 'https://weight.example.com/api/v1/iot/callback', - authType: 'apikey', authKey: 'ak_xxx_weight_dev', - dataTypes: ['weight', 'bodyFat'], retryStrategy: 'exponential', maxRetry: 5, - status: 'enabled', remark: '总部 + 园区体重数据', createdAt: '2026-06-12 10:30:00', + dispatchMode: 'realtime', authEnabled: true, + authAccount: 'weight_appid_001', authSecret: 'ak_xxx_weight_secret', authApiUrl: 'https://weight.example.com/api/v1/auth/token', + status: 'enabled', deviceCategoryIds: ['cat-body-bt-scale', 'cat-body-fat'], deviceCategoryNames: ['蓝牙秤', '体脂仪'], + maxRetry: 5, lastCommunicateTime: '2026-07-09 09:12:30', remark: '总部 + 园区体重数据', + createTime: '2026-06-12 10:30:00', }, { id: 'DS-002', name: '健康管理系统', code: 'HEALTH-MGMT', + customerId: 'cust-004', customerName: '健康示范园区', callbackUrl: 'https://health.example.com/open/iot/receive', - authType: 'hmac', authKey: 'hmac_xxx_health_prod', - dataTypes: ['heartRate', 'sleep', 'stepCount', 'healthCheck'], - retryStrategy: 'exponential', maxRetry: 5, - status: 'enabled', remark: '个人健康数据聚合', createdAt: '2026-06-12 11:20:00', + dispatchMode: 'realtime', authEnabled: true, + authAccount: 'health_appid_002', authSecret: 'hmac_xxx_health_secret', authApiUrl: 'https://health.example.com/open/auth/token', + status: 'enabled', deviceCategoryIds: ['cat-wearable-hw5', 'cat-monitor-all-in-one', 'cat-monitor-pressure'], deviceCategoryNames: ['华为-watch5', '智能健康一体机', '高血压治疗仪'], + maxRetry: 5, lastCommunicateTime: '2026-07-09 09:15:48', remark: '个人健康数据聚合', + createTime: '2026-06-12 11:20:00', }, { id: 'DS-003', name: '运动管理系统', code: 'SPORT-MGMT', + customerId: 'cust-008', customerName: '锦绣物业管理公司', callbackUrl: 'https://sport.example.com/api/iot/v2', - authType: 'token', authKey: 'bearer_xxx_sport_dev', - dataTypes: ['training'], retryStrategy: 'fixed', maxRetry: 3, - status: 'enabled', remark: '健身房运动数据', createdAt: '2026-06-15 09:00:00', + dispatchMode: 'polling', authEnabled: false, + status: 'enabled', deviceCategoryIds: ['cat-fitness-bike', 'cat-wearable-hw5'], deviceCategoryNames: ['健身车', '华为-watch5'], + maxRetry: 3, lastCommunicateTime: '2026-07-09 08:30:00', remark: '健身房运动数据(轮询 5 分钟)', + createTime: '2026-06-15 09:00:00', }, { id: 'DS-004', name: '营养分析系统', code: 'NUTRI-ANALYSIS', + customerId: 'cust-005', customerName: '江南大学后勤处', callbackUrl: 'https://nutri.example.com/api/iot/weight', - authType: 'apikey', authKey: 'ak_xxx_nutri_test', - dataTypes: ['weight', 'bodyFat', 'heartRate'], - retryStrategy: 'exponential', maxRetry: 5, - status: 'disabled', remark: '营养摄入关联分析(停用中)', createdAt: '2026-06-20 14:00:00', + dispatchMode: 'realtime', authEnabled: true, + authAccount: 'nutri_appid_004', authSecret: 'ak_xxx_nutri_secret', authApiUrl: 'https://nutri.example.com/api/v1/auth/token', + status: 'disabled', deviceCategoryIds: ['cat-body-bt-scale', 'cat-body-fat', 'cat-monitor-sugar'], deviceCategoryNames: ['蓝牙秤', '体脂仪', '血糖监测仪'], + maxRetry: 5, lastCommunicateTime: '2026-07-08 22:10:00', remark: '营养摄入关联分析(停用中)', + createTime: '2026-06-20 14:00:00', }, { id: 'DS-005', name: '应急监测平台', code: 'EMERGENCY', + customerId: 'cust-007', customerName: '市民政局养老服务科', callbackUrl: 'https://emerg.example.com/api/iot/alert', - authType: 'none', authKey: '', - dataTypes: ['heartRate', 'healthCheck'], - retryStrategy: 'none', maxRetry: 0, - status: 'enabled', remark: '高风险健康事件实时推送', createdAt: '2026-06-25 16:30:00', + dispatchMode: 'realtime', authEnabled: false, + status: 'enabled', deviceCategoryIds: ['cat-wearable-hw5', 'cat-monitor-pressure'], deviceCategoryNames: ['华为-watch5', '高血压治疗仪'], + maxRetry: 0, lastCommunicateTime: '2026-07-09 09:20:11', remark: '高风险健康事件实时推送', + createTime: '2026-06-25 16:30:00', + }, + { + id: 'DS-006', name: '体检数据归档', code: 'HEALTH-ARCHIVE', + customerId: 'cust-006', customerName: '市第一人民医院', + callbackUrl: 'https://archive.example.com/api/iot/healthCheck', + dispatchMode: 'polling', authEnabled: true, + authAccount: 'archive_appid_006', authSecret: 'ak_xxx_archive_secret', authApiUrl: 'https://archive.example.com/api/v1/auth/token', + status: 'enabled', deviceCategoryIds: ['cat-monitor-all-in-one'], deviceCategoryNames: ['智能健康一体机'], + maxRetry: 3, lastCommunicateTime: '2026-07-09 06:00:00', remark: '体检数据按日归档(轮询 1 小时)', + createTime: '2026-06-28 10:00:00', + }, + { + /** 真实场景:同一客户(健康示范园区)有多个分发配置,不同设备数据走不同回调地址 */ + id: 'DS-007', name: '手表心率实时上报', code: 'WATCH-HEART', + customerId: 'cust-004', customerName: '健康示范园区', + callbackUrl: 'https://health.example.com/open/iot/watch/heart', + dispatchMode: 'realtime', authEnabled: true, + authAccount: 'health_appid_007', authSecret: 'hmac_xxx_watch_secret', authApiUrl: 'https://health.example.com/open/auth/token', + status: 'enabled', deviceCategoryIds: ['cat-wearable-hw5'], deviceCategoryNames: ['华为-watch5'], + maxRetry: 5, lastCommunicateTime: '2026-07-09 09:25:33', remark: '园区手表心率数据专用通道', + createTime: '2026-06-30 14:30:00', + }, + { + /** 同一客户的第二条配置:体脂 + 血糖双设备聚合(样例:一个配置覆盖两类设备) */ + id: 'DS-008', name: '体脂数据分析通道', code: 'BODY-FAT-ANA', + customerId: 'cust-004', customerName: '健康示范园区', + callbackUrl: 'https://health.example.com/open/iot/body/analysis', + dispatchMode: 'realtime', authEnabled: true, + authAccount: 'health_appid_008', authSecret: 'hmac_xxx_body_secret', authApiUrl: 'https://health.example.com/open/auth/token', + status: 'enabled', deviceCategoryIds: ['cat-body-fat', 'cat-monitor-sugar'], deviceCategoryNames: ['体脂仪', '血糖监测仪'], + maxRetry: 5, lastCommunicateTime: '2026-07-09 09:28:12', remark: '体脂 + 血糖数据合并分析通道', + createTime: '2026-07-02 09:00:00', }, ] -const deviceDict: Record = { - 'WD-SCALE-0001': '总部体重秤 A', - 'WD-SCALE-0002': '分部体重秤 B', - 'WD-INBODY-0012': '园区体脂仪', - 'WD-WATCH-0042': '李四的华为手表', - 'WD-WATCH-0051': '王五的华为手表', - 'WD-BIKE-0008': '健身房 08 号健身车', - 'WD-HEALTH-0023': '体检一体机 23', - 'WD-BP-0007': '血压计 07', -} - -const subscribeList: SubscribeItem[] = [ - { - id: 'SUB-001', downstreamId: 'DS-001', downstreamName: '体重管理系统', - deviceCode: 'WD-SCALE-0001', deviceName: '总部体重秤 A', - dataType: 'weight', filterRule: '', status: 'enabled', createdAt: '2026-06-12 10:35:00', - }, - { - id: 'SUB-002', downstreamId: 'DS-001', downstreamName: '体重管理系统', - deviceCode: 'WD-INBODY-0012', deviceName: '园区体脂仪', - dataType: 'bodyFat', filterRule: 'weight > 40', status: 'enabled', createdAt: '2026-06-12 10:36:00', - }, - { - id: 'SUB-003', downstreamId: 'DS-002', downstreamName: '健康管理系统', - deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', - dataType: 'heartRate', filterRule: '', status: 'enabled', createdAt: '2026-06-12 11:25:00', - }, - { - id: 'SUB-004', downstreamId: 'DS-002', downstreamName: '健康管理系统', - deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', - dataType: 'sleep', filterRule: '', status: 'enabled', createdAt: '2026-06-12 11:26:00', - }, - { - id: 'SUB-005', downstreamId: 'DS-003', downstreamName: '运动管理系统', - deviceCode: 'WD-BIKE-0008', deviceName: '健身房 08 号健身车', - dataType: 'training', filterRule: 'duration > 5', status: 'enabled', createdAt: '2026-06-15 09:05:00', - }, - { - id: 'SUB-006', downstreamId: 'DS-002', downstreamName: '健康管理系统', - deviceCode: 'WD-HEALTH-0023', deviceName: '体检一体机 23', - dataType: 'healthCheck', filterRule: '', status: 'enabled', createdAt: '2026-06-15 09:30:00', - }, - { - id: 'SUB-007', downstreamId: 'DS-005', downstreamName: '应急监测平台', - deviceCode: 'WD-BP-0007', deviceName: '血压计 07', - dataType: 'heartRate', filterRule: 'heartRate > 120', status: 'enabled', createdAt: '2026-06-25 16:35:00', - }, - { - id: 'SUB-008', downstreamId: 'DS-004', downstreamName: '营养分析系统', - deviceCode: 'WD-SCALE-0002', deviceName: '分部体重秤 B', - dataType: 'weight', filterRule: '', status: 'disabled', createdAt: '2026-06-20 14:10:00', - }, -] - -// ============= 下游系统接口 ============= - -const downstreamListHandler = (ctx: MockContext): ApiResponse => { +/** 列表筛选 */ +const listConfig = (ctx: MockContext): ApiResponse => { const p = getP(ctx.params) - const filtered = downstreamList.filter((item) => { + const filtered = configList.filter((item) => { + if (p.customerId && item.customerId !== p.customerId) return false if (p.name && !item.name.toLowerCase().includes(p.name.toLowerCase())) return false - if (p.authType && item.authType !== p.authType) return false - if (p.retryStrategy && item.retryStrategy !== p.retryStrategy) return false if (p.status && item.status !== p.status) return false + if (p.authEnabled !== undefined && p.authEnabled !== null && item.authEnabled !== p.authEnabled) return false + if (p.lastCommunicateTimeStart && (item.lastCommunicateTime ?? '') < p.lastCommunicateTimeStart) return false + if (p.lastCommunicateTimeEnd && (item.lastCommunicateTime ?? '') > p.lastCommunicateTimeEnd + ' 23:59:59') return false return true }) + + if (p.column === 'lastCommunicateTime' && p.order) { + filtered.sort((a, b) => { + const cmp = (a.lastCommunicateTime ?? '').localeCompare(b.lastCommunicateTime ?? '') + return p.order === 'ascend' ? cmp : -cmp + }) + } + return { code: '00000', msg: '查询成功', data: filtered, total: filtered.length } } -const downstreamInfo = (ctx: MockContext): ApiResponse => { +/** 详情 */ +const infoConfig = (ctx: MockContext): ApiResponse => { const p = getP(ctx.params) - const target = downstreamList.find((i) => i.id === p.id) + const target = configList.find((i) => i.id === p.id) if (!target) return ok('记录不存在', null) return ok('查询成功', target) } -const downstreamAdd = (ctx: MockContext): ApiResponse => { +/** 新增 */ +const addConfig = (ctx: MockContext): ApiResponse => { const p = getP(ctx.params) - const id = 'DS-' + String(downstreamList.length + 1).padStart(3, '0') - downstreamList.push({ + const id = 'DS-' + String(configList.length + 1).padStart(3, '0') + const ids = (p.deviceCategoryIds ?? []) as string[] + configList.push({ id, name: String(p.name ?? ''), code: String(p.code ?? ''), + customerId: String(p.customerId ?? ''), + customerName: '', callbackUrl: String(p.callbackUrl ?? ''), - authType: (p.authType as DownstreamItem['authType']) ?? 'none', - authKey: String(p.authKey ?? ''), - dataTypes: p.dataTypes ?? [], - retryStrategy: (p.retryStrategy as DownstreamItem['retryStrategy']) ?? 'exponential', + dispatchMode: (p.dispatchMode as ConfigItem['dispatchMode']) ?? 'realtime', + authEnabled: Boolean(p.authEnabled), + authAccount: p.authEnabled ? String(p.authAccount ?? '') : '', + authSecret: p.authEnabled ? String(p.authSecret ?? '') : '', + authApiUrl: p.authEnabled ? String(p.authApiUrl ?? '') : '', + status: (p.status as ConfigItem['status']) ?? 'enabled', + deviceCategoryIds: ids, + deviceCategoryNames: ids.map(categoryName), maxRetry: Number(p.maxRetry ?? 0), - status: (p.status as DownstreamItem['status']) ?? 'enabled', + lastCommunicateTime: '', remark: p.remark ?? '', - createdAt: '2026-07-02 10:00:00', + createTime: '2026-07-09 10:00:00', }) return ok('新增成功') } -const downstreamUpdate = (ctx: MockContext): ApiResponse => { +/** 编辑 */ +const updateConfig = (ctx: MockContext): ApiResponse => { const p = getP(ctx.params) const id = String(p.id ?? '') - const target = downstreamList.find((i) => i.id === id) + const target = configList.find((i) => i.id === id) if (!target) return ok('记录不存在', null) if (p.name !== undefined) target.name = String(p.name) + if (p.customerId !== undefined) target.customerId = String(p.customerId) if (p.callbackUrl !== undefined) target.callbackUrl = String(p.callbackUrl) - if (p.authType !== undefined) target.authType = p.authType as DownstreamItem['authType'] - if (p.authKey !== undefined) target.authKey = String(p.authKey) - if (p.dataTypes !== undefined) target.dataTypes = p.dataTypes as string[] - if (p.retryStrategy !== undefined) target.retryStrategy = p.retryStrategy as DownstreamItem['retryStrategy'] + if (p.dispatchMode !== undefined) target.dispatchMode = p.dispatchMode as ConfigItem['dispatchMode'] + if (p.authEnabled !== undefined) { + target.authEnabled = Boolean(p.authEnabled) + if (!target.authEnabled) { + target.authAccount = '' + target.authSecret = '' + target.authApiUrl = '' + } else { + target.authAccount = String(p.authAccount ?? '') + target.authSecret = String(p.authSecret ?? '') + target.authApiUrl = String(p.authApiUrl ?? '') + } + } + if (p.status !== undefined) target.status = p.status as ConfigItem['status'] + if (p.deviceCategoryIds !== undefined) { + const ids = p.deviceCategoryIds as string[] + target.deviceCategoryIds = ids + target.deviceCategoryNames = ids.map(categoryName) + } if (p.maxRetry !== undefined) target.maxRetry = Number(p.maxRetry) - if (p.status !== undefined) target.status = p.status as DownstreamItem['status'] if (p.remark !== undefined) target.remark = p.remark return ok('更新成功') } -const downstreamDelete = (ctx: MockContext): ApiResponse => { +/** 切换启用状态 */ +const toggleStatusConfig = (ctx: MockContext): ApiResponse => { const p = getP(ctx.params) const id = String(p.id ?? '') - const idx = downstreamList.findIndex((i) => i.id === id) - if (idx < 0) return ok('记录不存在', null) - downstreamList.splice(idx, 1) - // 级联删除订阅 - for (let i = subscribeList.length - 1; i >= 0; i--) { - if (subscribeList[i].downstreamId === id) subscribeList.splice(i, 1) - } - return ok('删除成功') -} - -const downstreamToggle = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const id = String(p.id ?? '') - const target = downstreamList.find((i) => i.id === id) + const target = configList.find((i) => i.id === id) if (!target) return ok('记录不存在', null) target.status = target.status === 'enabled' ? 'disabled' : 'enabled' - return ok(target.status === 'enabled' ? '已启用' : '已禁用') -} - -// ============= 订阅映射接口 ============= - -const subscribeListHandler = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const filtered = subscribeList.filter((item) => { - if (p.downstreamId && item.downstreamId !== p.downstreamId) return false - if (p.deviceCode && !item.deviceCode.toLowerCase().includes(p.deviceCode.toLowerCase())) return false - if (p.dataType && item.dataType !== p.dataType) return false - if (p.status && item.status !== p.status) return false - return true - }) - return { code: '00000', msg: '查询成功', data: filtered, total: filtered.length } -} - -const subscribeOptions = (): ApiResponse<{ downstream: { label: string; value: string }[] }> => { - return ok('查询成功', { - downstream: downstreamList - .filter((i) => i.status === 'enabled') - .map((i) => ({ label: i.name, value: i.id })), - }) -} - -const subscribeAdd = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const ds = downstreamList.find((i) => i.id === p.downstreamId) - if (!ds) return ok('下游系统不存在', null) - const id = 'SUB-' + String(subscribeList.length + 1).padStart(3, '0') - subscribeList.push({ - id, - downstreamId: ds.id, - downstreamName: ds.name, - deviceCode: String(p.deviceCode ?? ''), - deviceName: deviceDict[String(p.deviceCode ?? '')] ?? '未知设备', - dataType: String(p.dataType ?? ''), - filterRule: String(p.filterRule ?? ''), - status: (p.status as SubscribeItem['status']) ?? 'enabled', - createdAt: '2026-07-02 10:00:00', - }) - return ok('新增成功') -} - -const subscribeUpdate = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const id = String(p.id ?? '') - const target = subscribeList.find((i) => i.id === id) - if (!target) return ok('记录不存在', null) - if (p.deviceCode !== undefined) { - target.deviceCode = String(p.deviceCode) - target.deviceName = deviceDict[target.deviceCode] ?? '未知设备' - } - if (p.dataType !== undefined) target.dataType = String(p.dataType) - if (p.filterRule !== undefined) target.filterRule = String(p.filterRule) - if (p.status !== undefined) target.status = p.status as SubscribeItem['status'] - return ok('更新成功') -} - -const subscribeDelete = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const id = String(p.id ?? '') - const idx = subscribeList.findIndex((i) => i.id === id) - if (idx < 0) return ok('记录不存在', null) - subscribeList.splice(idx, 1) - return ok('删除成功') -} - -const subscribeToggle = (ctx: MockContext): ApiResponse => { - const p = getP(ctx.params) - const id = String(p.id ?? '') - const target = subscribeList.find((i) => i.id === id) - if (!target) return ok('记录不存在', null) - target.status = target.status === 'enabled' ? 'disabled' : 'enabled' - return ok(target.status === 'enabled' ? '已启用' : '已禁用') + return ok(target.status === 'enabled' ? '已启用' : '已停用') } registerMocks([ - { method: 'POST', url: '/admin/device/distribute/config/downstream/list', handler: downstreamListHandler }, - { method: 'POST', url: '/admin/device/distribute/config/downstream/info', handler: downstreamInfo }, - { method: 'POST', url: '/admin/device/distribute/config/downstream/add', handler: downstreamAdd }, - { method: 'POST', url: '/admin/device/distribute/config/downstream/update', handler: downstreamUpdate }, - { method: 'POST', url: '/admin/device/distribute/config/downstream/delete', handler: downstreamDelete }, - { method: 'POST', url: '/admin/device/distribute/config/downstream/toggleStatus', handler: downstreamToggle }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/list', handler: subscribeListHandler }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/options', handler: subscribeOptions }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/add', handler: subscribeAdd }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/update', handler: subscribeUpdate }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/delete', handler: subscribeDelete }, - { method: 'POST', url: '/admin/device/distribute/config/subscribe/toggleStatus', handler: subscribeToggle }, + { method: 'POST', url: '/admin/device/distribute/config/list', handler: listConfig }, + { method: 'POST', url: '/admin/device/distribute/config/info', handler: infoConfig }, + { method: 'POST', url: '/admin/device/distribute/config/add', handler: addConfig }, + { method: 'POST', url: '/admin/device/distribute/config/update', handler: updateConfig }, + { method: 'POST', url: '/admin/device/distribute/config/toggleStatus', handler: toggleStatusConfig }, ]) diff --git a/src/axios/handlers/device-center/distribute/deadletter.ts b/src/axios/handlers/device-center/distribute/deadletter.ts deleted file mode 100644 index 253677d..0000000 --- a/src/axios/handlers/device-center/distribute/deadletter.ts +++ /dev/null @@ -1,254 +0,0 @@ -import { registerMocks } from '@axios/mockBus' -import type { ApiResponse, MockContext, RequestParameter } from '@axios' - -/** - * 失败重投(死信队列) mock 数据 - * - 记录分发失败进入死信队列的样本 - * - 自动重投策略:指数退避(1min / 5min / 30min / 2h / 12h),最多 5 次后转人工 - * - 状态:pending 待重投 / retrying 重投中 / recovered 已恢复 / discarded 已丢弃 - */ - -interface DeadletterItem { - id: string - originalRecordId: string - deviceCode: string - deviceName: string - dataType: 'weight' | 'bodyFat' | 'heartRate' | 'sleep' | 'stepCount' | 'training' | 'healthCheck' - downstreamId: string - downstreamName: string - firstFailTime: string - lastRetryTime: string | null - retryCount: number - nextRetryTime: string | null - failReason: string - status: 'pending' | 'retrying' | 'recovered' | 'discarded' -} - -interface DeadletterDetail extends DeadletterItem { - requestBody: string - responseBody: string - authKey: string - strategy: string - thresholdWarn: string -} - -interface DeadletterParams { - deviceCode?: string - dataType?: string - downstreamId?: string - status?: string - column?: string - order?: 'ascend' | 'descend' | null - pageNum?: number - pageSize?: number - ids?: string[] - id?: string - [key: string]: unknown -} - -const getParams = (params: RequestParameter): DeadletterParams => (params ?? {}) as DeadletterParams - -const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) - -const RETRY_STRATEGY = '指数退避:1min → 5min → 30min → 2h → 12h,最多 5 次后转人工' - -const sampleRequestBody = (dataType: string, deviceCode: string): string => - JSON.stringify({ - deviceCode, - dataType, - timestamp: '2026-07-02T10:30:00Z', - payload: { - weight: dataType === 'weight' ? 65.3 : undefined, - bodyFat: dataType === 'bodyFat' ? 18.5 : undefined, - heartRate: dataType === 'heartRate' ? 72 : undefined, - steps: dataType === 'stepCount' ? 8542 : undefined, - }, - }) - -const sampleResponseBody = (status: string): string => - JSON.stringify({ - code: status === 'recovered' ? 200 : 500, - message: status === 'recovered' ? 'OK' : 'Internal Server Error', - receivedAt: '2026-07-02T10:30:01Z', - }) - -const deadletterList: DeadletterItem[] = [ - { - id: 'DL-001', originalRecordId: 'DPR-002', deviceCode: 'WD-INBODY-0012', deviceName: '园区体脂仪', - dataType: 'bodyFat', downstreamId: 'DS-001', downstreamName: '体重管理系统', - firstFailTime: '2026-07-02 09:13:12', lastRetryTime: '2026-07-02 09:33:12', - retryCount: 3, nextRetryTime: '2026-07-02 11:33:12', - failReason: '下游服务 500 内部错误,已重试 3 次仍未恢复', status: 'pending', - }, - { - id: 'DL-002', originalRecordId: 'DPR-004', deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', - dataType: 'sleep', downstreamId: 'DS-002', downstreamName: '健康管理系统', - firstFailTime: '2026-07-02 09:15:20', lastRetryTime: '2026-07-02 09:20:20', - retryCount: 1, nextRetryTime: '2026-07-02 09:45:20', - failReason: '请求超时 408,进入指数退避重投队列', status: 'retrying', - }, - { - id: 'DL-003', originalRecordId: 'DPR-008', deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', - dataType: 'stepCount', downstreamId: 'DS-002', downstreamName: '健康管理系统', - firstFailTime: '2026-07-02 09:19:18', lastRetryTime: '2026-07-02 10:19:18', - retryCount: 5, nextRetryTime: null, - failReason: '下游服务不可用 503,已耗尽 5 次自动重投,待人工介入', status: 'pending', - }, - { - id: 'DL-004', originalRecordId: 'DPR-010', deviceCode: 'WD-SCALE-0002', deviceName: '分部体重秤 B', - dataType: 'weight', downstreamId: 'DS-001', downstreamName: '体重管理系统', - firstFailTime: '2026-07-02 08:42:11', lastRetryTime: '2026-07-02 08:47:11', - retryCount: 2, nextRetryTime: '2026-07-02 09:17:11', - failReason: '鉴权密钥过期,下游返回 401', status: 'pending', - }, - { - id: 'DL-005', originalRecordId: 'DPR-012', deviceCode: 'WD-BP-0007', deviceName: '血压计 07', - dataType: 'heartRate', downstreamId: 'DS-002', downstreamName: '健康管理系统', - firstFailTime: '2026-07-01 17:30:55', lastRetryTime: '2026-07-01 18:30:55', - retryCount: 4, nextRetryTime: '2026-07-02 06:30:55', - failReason: '下游数据库连接失败,连续 4 次重试失败', status: 'retrying', - }, - { - id: 'DL-006', originalRecordId: 'DPR-015', deviceCode: 'WD-BIKE-0008', deviceName: '健身房 08 号健身车', - dataType: 'training', downstreamId: 'DS-003', downstreamName: '运动管理系统', - firstFailTime: '2026-07-01 14:22:30', lastRetryTime: '2026-07-01 14:27:30', - retryCount: 2, nextRetryTime: null, - failReason: '运维确认下游已升级,重投成功', status: 'recovered', - }, - { - id: 'DL-007', originalRecordId: 'DPR-018', deviceCode: 'WD-HEALTH-0023', deviceName: '体检一体机 23', - dataType: 'healthCheck', downstreamId: 'DS-002', downstreamName: '健康管理系统', - firstFailTime: '2026-07-01 10:08:14', lastRetryTime: '2026-07-01 22:08:14', - retryCount: 5, nextRetryTime: null, - failReason: '下游接口字段不兼容,已耗尽自动重投,运维永久丢弃', status: 'discarded', - }, - { - id: 'DL-008', originalRecordId: 'DPR-020', deviceCode: 'WD-WATCH-0051', deviceName: '王五的华为手表', - dataType: 'sleep', downstreamId: 'DS-002', downstreamName: '健康管理系统', - firstFailTime: '2026-07-02 07:55:42', lastRetryTime: null, - retryCount: 0, nextRetryTime: '2026-07-02 07:56:42', - failReason: '下游服务瞬时不可用,等待首次重投', status: 'pending', - }, -] - -/** 列表筛选 + 分页 */ -const listDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const filtered = deadletterList.filter((item) => { - if (params.deviceCode && !item.deviceCode.toLowerCase().includes(params.deviceCode.toLowerCase())) return false - if (params.dataType && item.dataType !== params.dataType) return false - if (params.downstreamId && item.downstreamId !== params.downstreamId) return false - if (params.status && item.status !== params.status) return false - return true - }) - - if (params.column === 'firstFailTime' && params.order) { - filtered.sort((a, b) => { - const cmp = a.firstFailTime.localeCompare(b.firstFailTime) - return params.order === 'ascend' ? cmp : -cmp - }) - } - - const pageNum = Number(params.pageNum ?? 1) - const pageSize = Number(params.pageSize ?? 30) - const start = (pageNum - 1) * pageSize - return { code: '00000', msg: '查询成功', data: filtered.slice(start, start + pageSize), total: filtered.length } -} - -/** 详情(含请求体/响应体 + 策略说明) */ -const infoDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const target = deadletterList.find((item) => item.id === id) - if (!target) return ok('记录不存在', null) - return ok('查询成功', { - ...target, - requestBody: sampleRequestBody(target.dataType, target.deviceCode), - responseBody: sampleResponseBody(target.status), - authKey: 'ak_xxx_' + target.downstreamId.toLowerCase() + '_dev', - strategy: RETRY_STRATEGY, - thresholdWarn: '死信累积超 100 条将自动告警到「设备运维 → 告警管理」', - }) -} - -/** 顶部统计卡 */ -const statDeadletter = (): ApiResponse<{ pendingCount: number; retryingCount: number; recoveredCount: number; discardedCount: number }> => { - const count = (s: string) => deadletterList.filter((i) => i.status === s).length - return ok('查询成功', { - pendingCount: count('pending'), - retryingCount: count('retrying'), - recoveredCount: count('recovered'), - discardedCount: count('discarded'), - }) -} - -/** 手动重投(单条) */ -const retryDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const target = deadletterList.find((item) => item.id === id) - if (!target) return ok('记录不存在', null) - if (target.status === 'discarded') return ok('该死信已丢弃,无法重投') - target.status = 'retrying' - target.retryCount += 1 - target.lastRetryTime = '2026-07-02 10:00:00' - target.failReason = '运维手动触发重投,等待下游响应' - return ok('重投任务已提交') -} - -/** 批量重投 */ -const batchRetryDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const ids = (params.ids ?? []) as string[] - let count = 0 - ids.forEach((id) => { - const target = deadletterList.find((item) => item.id === id) - if (target && target.status !== 'discarded') { - target.status = 'retrying' - target.retryCount += 1 - target.lastRetryTime = '2026-07-02 10:00:00' - target.failReason = '批量手动触发重投,等待下游响应' - count += 1 - } - }) - return ok(`已提交 ${count} 条重投任务`) -} - -/** 永久丢弃(单条) */ -const discardDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const target = deadletterList.find((item) => item.id === id) - if (!target) return ok('记录不存在', null) - target.status = 'discarded' - target.nextRetryTime = null - target.failReason = '运维永久丢弃,不再自动重投' - return ok('已永久丢弃') -} - -/** 批量丢弃 */ -const batchDiscardDeadletter = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const ids = (params.ids ?? []) as string[] - let count = 0 - ids.forEach((id) => { - const target = deadletterList.find((item) => item.id === id) - if (target) { - target.status = 'discarded' - target.nextRetryTime = null - target.failReason = '批量永久丢弃,不再自动重投' - count += 1 - } - }) - return ok(`已永久丢弃 ${count} 条记录`) -} - -registerMocks([ - { method: 'POST', url: '/admin/device/distribute/deadletter/list', handler: listDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/info', handler: infoDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/stat', handler: statDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/retry', handler: retryDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/batchRetry', handler: batchRetryDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/discard', handler: discardDeadletter }, - { method: 'POST', url: '/admin/device/distribute/deadletter/batchDiscard', handler: batchDiscardDeadletter }, -]) diff --git a/src/axios/handlers/device-center/distribute/record.ts b/src/axios/handlers/device-center/distribute/record.ts index acee172..0aec8c7 100644 --- a/src/axios/handlers/device-center/distribute/record.ts +++ b/src/axios/handlers/device-center/distribute/record.ts @@ -4,8 +4,8 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 分发记录 mock 数据 * - 一条上报数据 × 一个下游 = 一条分发记录 - * - 含 HTTP 状态码、响应耗时、重试次数、状态、失败原因 - * - 详情包含请求体 / 响应体 + * - 列表/统计支持按 downstreamId 过滤(从分发配置跳转携带) + * - 详情包含请求体 / 响应体(成功)或失败报错日志(失败/重投中) */ interface RecordItem { @@ -51,7 +51,7 @@ const sampleRequestBody = (dataType: string, deviceCode: string): string => JSON.stringify({ deviceCode, dataType, - timestamp: '2026-07-02T10:30:00Z', + timestamp: '2026-07-09T10:30:00Z', payload: { weight: dataType === 'weight' ? 65.3 : undefined, bodyFat: dataType === 'bodyFat' ? 18.5 : undefined, @@ -64,61 +64,61 @@ const sampleResponseBody = (httpStatus: number, status: string): string => JSON.stringify({ code: httpStatus, message: status === 'success' ? 'OK' : 'Internal Server Error', - receivedAt: '2026-07-02T10:30:01Z', + receivedAt: '2026-07-09T10:30:01Z', }) const recordList: RecordItem[] = [ { id: 'DPR-001', deviceCode: 'WD-SCALE-0001', deviceName: '总部体重秤 A', dataType: 'weight', downstreamId: 'DS-001', downstreamName: '体重管理系统', - dispatchTime: '2026-07-02 09:12:30', httpStatus: 200, responseTime: 85, retryCount: 0, + dispatchTime: '2026-07-09 09:12:30', httpStatus: 200, responseTime: 85, retryCount: 0, status: 'success', }, { id: 'DPR-002', deviceCode: 'WD-INBODY-0012', deviceName: '园区体脂仪', dataType: 'bodyFat', downstreamId: 'DS-001', downstreamName: '体重管理系统', - dispatchTime: '2026-07-02 09:13:12', httpStatus: 500, responseTime: 320, retryCount: 3, + dispatchTime: '2026-07-09 09:13:12', httpStatus: 500, responseTime: 320, retryCount: 3, status: 'failed', failureReason: '下游服务 500 内部错误,已耗尽 3 次重试', }, { id: 'DPR-003', deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', dataType: 'heartRate', downstreamId: 'DS-002', downstreamName: '健康管理系统', - dispatchTime: '2026-07-02 09:14:48', httpStatus: 200, responseTime: 102, retryCount: 0, + dispatchTime: '2026-07-09 09:14:48', httpStatus: 200, responseTime: 102, retryCount: 0, status: 'success', }, { id: 'DPR-004', deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', dataType: 'sleep', downstreamId: 'DS-002', downstreamName: '健康管理系统', - dispatchTime: '2026-07-02 09:15:20', httpStatus: 408, responseTime: 5000, retryCount: 1, - status: 'retrying', failureReason: '请求超时,进入重投队列', + dispatchTime: '2026-07-09 09:15:20', httpStatus: 408, responseTime: 5000, retryCount: 1, + status: 'retrying', failureReason: '请求超时 408,进入重投队列等待下一次回调', }, { id: 'DPR-005', deviceCode: 'WD-BIKE-0008', deviceName: '健身房 08 号健身车', dataType: 'training', downstreamId: 'DS-003', downstreamName: '运动管理系统', - dispatchTime: '2026-07-02 09:16:05', httpStatus: 200, responseTime: 145, retryCount: 0, + dispatchTime: '2026-07-09 09:16:05', httpStatus: 200, responseTime: 145, retryCount: 0, status: 'success', }, { id: 'DPR-006', deviceCode: 'WD-HEALTH-0023', deviceName: '体检一体机 23', dataType: 'healthCheck', downstreamId: 'DS-002', downstreamName: '健康管理系统', - dispatchTime: '2026-07-02 09:17:42', httpStatus: 200, responseTime: 220, retryCount: 0, + dispatchTime: '2026-07-09 09:17:42', httpStatus: 200, responseTime: 220, retryCount: 0, status: 'success', }, { id: 'DPR-007', deviceCode: 'WD-SCALE-0001', deviceName: '总部体重秤 A', dataType: 'weight', downstreamId: 'DS-001', downstreamName: '体重管理系统', - dispatchTime: '2026-07-02 09:18:33', httpStatus: 200, responseTime: 78, retryCount: 0, + dispatchTime: '2026-07-09 09:18:33', httpStatus: 200, responseTime: 78, retryCount: 0, status: 'success', }, { id: 'DPR-008', deviceCode: 'WD-WATCH-0042', deviceName: '李四的华为手表', dataType: 'stepCount', downstreamId: 'DS-002', downstreamName: '健康管理系统', - dispatchTime: '2026-07-02 09:19:18', httpStatus: 503, responseTime: 280, retryCount: 2, + dispatchTime: '2026-07-09 09:19:18', httpStatus: 503, responseTime: 280, retryCount: 2, status: 'failed', failureReason: '下游服务不可用 503,已耗尽重试', }, ] -/** 列表筛选 + 分页 */ +/** 列表筛选 + 分页(支持 downstreamId 维度过滤) */ const listRecord = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = recordList.filter((item) => { @@ -150,21 +150,29 @@ const infoRecord = (ctx: MockContext): ApiResponse => { if (!target) return ok('记录不存在', null) return ok('查询成功', { ...target, - requestBody: sampleRequestBody(target.dataType, target.deviceCode), - responseBody: sampleResponseBody(target.httpStatus, target.status), + requestBody: target.status === 'success' ? sampleRequestBody(target.dataType, target.deviceCode) : '', + responseBody: target.status === 'success' ? sampleResponseBody(target.httpStatus, target.status) : '', authKey: 'ak_xxx_' + target.downstreamId.toLowerCase() + '_dev', }) } -/** 顶部统计卡:今日分发总数 / 成功率 / 平均响应耗时 / 失败待重投数 */ -const statRecord = (): ApiResponse<{ todayTotal: number; successRate: number; avgResponseTime: number; failedPending: number }> => { - const total = recordList.length - const successCount = recordList.filter((i) => i.status === 'success').length - const failedPending = recordList.filter((i) => i.status !== 'success').length - const avgTime = Math.round(recordList.reduce((sum, i) => sum + i.responseTime, 0) / total) +/** 顶部统计卡(支持按 downstreamId 维度统计) */ +const statRecord = (ctx: MockContext): ApiResponse<{ todayTotal: number; successRate: number; avgResponseTime: number; failedPending: number }> => { + const params = getParams(ctx.params) + const scoped = params.downstreamId + ? recordList.filter((i) => i.downstreamId === params.downstreamId) + : recordList + if (scoped.length === 0) { + return ok('查询成功', { todayTotal: 0, successRate: 0, avgResponseTime: 0, failedPending: 0 }) + } + const successCount = scoped.filter((i) => i.status === 'success').length + const failedPending = scoped.filter((i) => i.status !== 'success').length + const avgTime = Math.round(scoped.reduce((sum, i) => sum + i.responseTime, 0) / scoped.length) + /** todayTotal 用 mock 倍数模拟(实际场景由后端聚合今日分发总数) */ + const todayTotal = scoped.length * 160 return ok('查询成功', { - todayTotal: 1280, - successRate: Math.round((successCount / total) * 1000) / 10, + todayTotal, + successRate: Math.round((successCount / scoped.length) * 1000) / 10, avgResponseTime: avgTime, failedPending, }) diff --git a/src/axios/handlers/device-center/distribute/report.ts b/src/axios/handlers/device-center/distribute/report.ts new file mode 100644 index 0000000..e7d5ca8 --- /dev/null +++ b/src/axios/handlers/device-center/distribute/report.ts @@ -0,0 +1,310 @@ +import { registerMocks } from '@axios/mockBus' +import type { ApiResponse, MockContext, RequestParameter } from '@axios' + +/** + * 上报配置 mock 数据 + * - 单列表 CRUD(无 Tab) + * - 字段:上报通道名称 / 通道编码 / 所属客户 / 布点场所 / 鉴权开关 / 鉴权账号 / 鉴权秘钥 / + * 鉴权接口地址 / 状态 / MQ 通讯地址 / 服务网关地址 / 覆盖设备(设备分类多选) / + * 备注 / 创建时间 + * - 客户下拉复用 /admin/device/customer/archive/page(由 customer/archive.ts 注册) + * - 场所下拉复用 /admin/device/site/list(由 customer/site.ts 注册,支持 customerId 联动过滤) + * - 设备分类树复用 /admin/device/category/tree(由 asset/category.ts 注册) + */ + +interface ReportItem { + id: string + channelName: string + channelCode: string + customerId: string + customerName: string + siteId: string + siteName: string + authEnabled: boolean + authAccount?: string + authSecret?: string + authApiUrl?: string + status: 'enabled' | 'disabled' + mqAddress: string + gatewayUrl: string + deviceCategoryIds: string[] + deviceCategoryNames: string[] + remark?: string + createTime?: string +} + +interface ReportParams { + id?: string + channelName?: string + channelCode?: string + customerId?: string + siteId?: string + name?: string + status?: string + authEnabled?: boolean + mqAddress?: string + gatewayUrl?: string + deviceCategoryIds?: string[] + remark?: string + column?: string + order?: 'ascend' | 'descend' | null + pageNum?: number + pageSize?: number + [key: string]: unknown +} + +const getP = (params: RequestParameter): ReportParams => (params ?? {}) as ReportParams +const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) + +/** 设备分类 ID → 名称映射(与 asset/category mock 真实树节点保持一致) */ +const categoryDict: Record = { + 'cat-body-bt-scale': '蓝牙秤', + 'cat-body-fat': '体脂仪', + 'cat-body-all-in-one': '一体机', + 'cat-wearable-hw5': '华为-watch5', + 'cat-wearable-hw4': '华为-watch4', + 'cat-wearable-ljf01': '罗杰夫-LJF01', + 'cat-fitness-bike': '健身车', + 'cat-fitness-treadmill': '跑步机', + 'cat-fitness-elliptical': '椭圆机', + 'cat-monitor-sugar': '血糖监测仪', + 'cat-monitor-pressure': '高血压治疗仪', + 'cat-monitor-all-in-one': '智能健康一体机', +} + +const categoryName = (id: string): string => categoryDict[id] ?? id + +/** 客户字典(与 customer/archive mock 同源) */ +const customerDict: Record = { + 'cust-001': '国庆能源总部食堂', + 'cust-002': '第一采油厂', + 'cust-003': '炼化分公司', + 'cust-004': '健康示范园区', +} + +/** 场所字典(与 customer/site mock 同源,用于 siteId → siteName 反查) */ +const siteDict: Record = { + 'site-001': { name: '国庆总部一食堂', customerId: 'cust-001' }, + 'site-002': { name: '采油一厂食堂', customerId: 'cust-002' }, + 'site-003': { name: '炼化员工餐厅', customerId: 'cust-003' }, + 'site-004': { name: '健康园区体检站', customerId: 'cust-004' }, + 'site-005': { name: '总部体重测量点 A', customerId: 'cust-001' }, + 'site-006': { name: '园区员工健身房', customerId: 'cust-004' }, + 'site-007': { name: '采油一厂社区站', customerId: 'cust-002' }, + 'site-008': { name: '炼化二食堂', customerId: 'cust-003' }, +} + +const reportList: ReportItem[] = [ + { + id: 'RP-001', channelName: '一厂食堂前置服务器', channelCode: 'CQ-CANTEEN-01', + customerId: 'cust-001', customerName: '国庆能源总部食堂', + siteId: 'site-001', siteName: '国庆总部一食堂', + authEnabled: true, + authAccount: 'cq_report_001', authSecret: 'ak_xxx_cq_secret', authApiUrl: 'https://auth.cq-energy.com/v1/token', + status: 'enabled', + mqAddress: 'mq://10.10.1.21:61616', + gatewayUrl: 'https://gateway.cq-energy.com/api/v1/iot/report', + deviceCategoryIds: ['cat-body-bt-scale', 'cat-body-fat'], deviceCategoryNames: ['蓝牙秤', '体脂仪'], + remark: '总部一食堂体重体脂数据上报通道', + createTime: '2026-06-10 10:30:00', + }, + { + id: 'RP-002', channelName: '阿里云中转服务器', channelCode: 'ALI-RELAY-EAST', + customerId: 'cust-004', customerName: '健康示范园区', + siteId: 'site-004', siteName: '健康园区体检站', + authEnabled: true, + authAccount: 'hp_relay_002', authSecret: 'hmac_xxx_hp_secret', authApiUrl: 'https://auth.health-park.gov/v1/token', + status: 'enabled', + mqAddress: 'mq://10.30.3.31:61616', + gatewayUrl: 'https://gateway.health-park.gov/api/v1/iot/report', + deviceCategoryIds: ['cat-wearable-hw5', 'cat-monitor-all-in-one', 'cat-monitor-pressure'], deviceCategoryNames: ['华为-watch5', '智能健康一体机', '高血压治疗仪'], + remark: '园区体检 + 手表数据经阿里云中转上报', + createTime: '2026-06-12 11:20:00', + }, + { + id: 'RP-003', channelName: '二厂食堂前置机', channelCode: 'OILF-CTN-02', + customerId: 'cust-002', customerName: '第一采油厂', + siteId: 'site-002', siteName: '采油一厂食堂', + authEnabled: false, + status: 'enabled', + mqAddress: 'mq://10.20.2.11:61616', + gatewayUrl: 'https://gateway.oilfield-01.com/api/v1/iot/report', + deviceCategoryIds: ['cat-body-bt-scale'], deviceCategoryNames: ['蓝牙秤'], + remark: '采油厂食堂体重数据上报(免鉴权通道)', + createTime: '2026-06-15 09:00:00', + }, + { + id: 'RP-004', channelName: '炼化餐厅前置服务器', channelCode: 'REF-DIN-01', + customerId: 'cust-003', customerName: '炼化分公司', + siteId: 'site-003', siteName: '炼化员工餐厅', + authEnabled: true, + authAccount: 'ref_din_004', authSecret: 'ak_xxx_ref_secret', authApiUrl: 'https://auth.refining.com/v1/token', + status: 'disabled', + mqAddress: 'mq://10.40.4.11:61616', + gatewayUrl: 'https://gateway.refining.com/api/v1/iot/report', + deviceCategoryIds: ['cat-body-fat', 'cat-monitor-sugar'], deviceCategoryNames: ['体脂仪', '血糖监测仪'], + remark: '炼化员工餐厅体脂血糖上报(停用中)', + createTime: '2026-06-20 14:00:00', + }, + { + id: 'RP-005', channelName: '总部测量点上报', channelCode: 'CQ-MSR-01', + customerId: 'cust-001', customerName: '国庆能源总部食堂', + siteId: 'site-005', siteName: '总部体重测量点 A', + authEnabled: false, + status: 'enabled', + mqAddress: 'mq://10.10.1.31:61616', + gatewayUrl: 'https://gateway.cq-energy.com/api/v1/iot/measure', + deviceCategoryIds: ['cat-body-bt-scale'], deviceCategoryNames: ['蓝牙秤'], + remark: '总部测量点 A 蓝牙秤数据上报', + createTime: '2026-06-25 16:30:00', + }, + { + id: 'RP-006', channelName: '园区健身房网关', channelCode: 'HP-GYM-01', + customerId: 'cust-004', customerName: '健康示范园区', + siteId: 'site-006', siteName: '园区员工健身房', + authEnabled: true, + authAccount: 'hp_gym_006', authSecret: 'hmac_xxx_gym_secret', authApiUrl: 'https://auth.health-park.gov/v1/token', + status: 'enabled', + mqAddress: 'mq://10.30.3.41:61616', + gatewayUrl: 'https://gateway.health-park.gov/api/v1/iot/fitness', + deviceCategoryIds: ['cat-fitness-bike', 'cat-fitness-treadmill', 'cat-wearable-hw5'], deviceCategoryNames: ['健身车', '跑步机', '华为-watch5'], + remark: '健身房运动数据上报通道', + createTime: '2026-06-28 10:00:00', + }, + { + id: 'RP-007', channelName: '采油社区站前置', channelCode: 'OILF-COM-01', + customerId: 'cust-002', customerName: '第一采油厂', + siteId: 'site-007', siteName: '采油一厂社区站', + authEnabled: false, + status: 'enabled', + mqAddress: 'mq://10.20.4.21:61616', + gatewayUrl: 'https://gateway.oilfield-01.com/api/v1/iot/community', + deviceCategoryIds: ['cat-wearable-hw5', 'cat-monitor-pressure'], deviceCategoryNames: ['华为-watch5', '高血压治疗仪'], + remark: '社区站老人健康数据上报(免鉴权)', + createTime: '2026-07-02 09:00:00', + }, + { + id: 'RP-008', channelName: '炼化二食堂网关', channelCode: 'REF-CTN-02', + customerId: 'cust-003', customerName: '炼化分公司', + siteId: 'site-008', siteName: '炼化二食堂', + authEnabled: true, + authAccount: 'ref_ctn_008', authSecret: 'ak_xxx_ref2_secret', authApiUrl: 'https://auth.refining.com/v1/token', + status: 'enabled', + mqAddress: 'mq://10.40.4.31:61616', + gatewayUrl: 'https://gateway.refining.com/api/v1/iot/canteen2', + deviceCategoryIds: ['cat-body-all-in-one'], deviceCategoryNames: ['一体机'], + remark: '炼化二食堂一体机数据上报', + createTime: '2026-07-05 14:30:00', + }, +] + +/** 列表筛选 */ +const listReport = (ctx: MockContext): ApiResponse => { + const p = getP(ctx.params) + const filtered = reportList.filter((item) => { + if (p.customerId && item.customerId !== p.customerId) return false + if (p.siteId && item.siteId !== p.siteId) return false + /** 兼容 BaseSearchForm.name(前端搜索字段命名为 name) */ + const kw = (p.name ?? p.channelName) as string | undefined + if (kw && !item.channelName.toLowerCase().includes(kw.toLowerCase())) return false + if (p.status && item.status !== p.status) return false + if (p.authEnabled !== undefined && p.authEnabled !== null && item.authEnabled !== p.authEnabled) return false + return true + }) + + if (p.column === 'createTime' && p.order) { + filtered.sort((a, b) => { + const cmp = (a.createTime ?? '').localeCompare(b.createTime ?? '') + return p.order === 'ascend' ? cmp : -cmp + }) + } + + return { code: '00000', msg: '查询成功', data: filtered, total: filtered.length } +} + +/** 详情 */ +const infoReport = (ctx: MockContext): ApiResponse => { + const p = getP(ctx.params) + const target = reportList.find((i) => i.id === p.id) + if (!target) return ok('记录不存在', null) + return ok('查询成功', target) +} + +/** 新增 */ +const addReport = (ctx: MockContext): ApiResponse => { + const p = getP(ctx.params) + const id = 'RP-' + String(reportList.length + 1).padStart(3, '0') + const ids = (p.deviceCategoryIds ?? []) as string[] + const customerId = String(p.customerId ?? '') + const siteId = String(p.siteId ?? '') + const site = siteDict[siteId] + reportList.push({ + id, + channelName: String(p.channelName ?? ''), + channelCode: String(p.channelCode ?? ''), + customerId, + customerName: customerDict[customerId] ?? '', + siteId, + siteName: site?.name ?? '', + authEnabled: Boolean(p.authEnabled), + authAccount: p.authEnabled ? String(p.authAccount ?? '') : '', + authSecret: p.authEnabled ? String(p.authSecret ?? '') : '', + authApiUrl: p.authEnabled ? String(p.authApiUrl ?? '') : '', + status: (p.status as ReportItem['status']) ?? 'enabled', + mqAddress: String(p.mqAddress ?? ''), + gatewayUrl: String(p.gatewayUrl ?? ''), + deviceCategoryIds: ids, + deviceCategoryNames: ids.map(categoryName), + remark: p.remark ?? '', + createTime: '2026-07-09 10:00:00', + }) + return ok('新增成功') +} + +/** 编辑 */ +const updateReport = (ctx: MockContext): ApiResponse => { + const p = getP(ctx.params) + const id = String(p.id ?? '') + const target = reportList.find((i) => i.id === id) + if (!target) return ok('记录不存在', null) + if (p.channelName !== undefined) target.channelName = String(p.channelName) + if (p.customerId !== undefined) { + const cid = String(p.customerId) + target.customerId = cid + target.customerName = customerDict[cid] ?? '' + } + if (p.siteId !== undefined) { + const sid = String(p.siteId) + target.siteId = sid + target.siteName = siteDict[sid]?.name ?? '' + } + if (p.authEnabled !== undefined) { + target.authEnabled = Boolean(p.authEnabled) + if (!target.authEnabled) { + target.authAccount = '' + target.authSecret = '' + target.authApiUrl = '' + } else { + target.authAccount = String(p.authAccount ?? '') + target.authSecret = String(p.authSecret ?? '') + target.authApiUrl = String(p.authApiUrl ?? '') + } + } + if (p.status !== undefined) target.status = p.status as ReportItem['status'] + if (p.mqAddress !== undefined) target.mqAddress = String(p.mqAddress) + if (p.gatewayUrl !== undefined) target.gatewayUrl = String(p.gatewayUrl) + if (p.deviceCategoryIds !== undefined) { + const ids = p.deviceCategoryIds as string[] + target.deviceCategoryIds = ids + target.deviceCategoryNames = ids.map(categoryName) + } + if (p.remark !== undefined) target.remark = p.remark + return ok('更新成功') +} + +registerMocks([ + { method: 'POST', url: '/admin/device/distribute/report/list', handler: listReport }, + { method: 'POST', url: '/admin/device/distribute/report/info', handler: infoReport }, + { method: 'POST', url: '/admin/device/distribute/report/add', handler: addReport }, + { method: 'POST', url: '/admin/device/distribute/report/update', handler: updateReport }, +]) diff --git a/src/axios/handlers/device-center/ops/alarm.ts b/src/axios/handlers/device-center/ops/alarm.ts index 4857492..81d9911 100644 --- a/src/axios/handlers/device-center/ops/alarm.ts +++ b/src/axios/handlers/device-center/ops/alarm.ts @@ -3,14 +3,14 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 设备运维 / 告警管理 mock - * - 告警处置:转工单 / 忽略 / 批量处理 - * - 业务日志:全量操作流水视图 + * - 告警列表:转工单 / 忽略 / 批量处理 + * - 告警详情:基础信息 + 报错日志(代码风格多行文本) + * - 设备分类下拉:末级分类名(与设备监测模块对齐) */ type AlarmSource = 'runLog' | 'selfCheck' | 'inspection' | 'sla' | 'ota' | 'dataDistribute' type AlarmLevel = 'info' | 'warn' | 'critical' type AlarmStatus = 'pending' | 'processing' | 'ignored' | 'workorder' -type BusinessLogType = 'operate' | 'system' | 'audit' | 'data' interface AlarmRecord { id: string @@ -19,29 +19,29 @@ interface AlarmRecord { level: AlarmLevel deviceCode: string deviceName: string + deviceCategory: string content: string triggerTime: string status: AlarmStatus handler: string | null -} - -interface BusinessLogRecord { - id: string - logType: BusinessLogType + /** 触发规则 */ + triggerRule: string + /** 所属租户 */ + tenantName: string + /** 所属场所 */ siteName: string - module: string - content: string - operator: string - operateTime: string + /** 处理建议 */ + handleSuggestion: string + /** 报错日志(代码风格多行文本) */ + errorLog: string } interface AlarmParams { - keyword?: string + deviceCode?: string + deviceCategory?: string source?: string level?: string status?: string - logType?: string - siteName?: string id?: string ids?: string[] type?: string @@ -50,113 +50,183 @@ interface AlarmParams { const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) const getParams = (params: RequestParameter): AlarmParams => (params ?? {}) as AlarmParams -const includes = (value: unknown, keyword: unknown): boolean => { - if (!keyword) return true - return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) -} const alarmList: AlarmRecord[] = [ { id: 'alm-001', alarmCode: 'ALM20260703001', source: 'selfCheck', level: 'critical', - deviceCode: 'CAB-SH-001', deviceName: '1号净菜柜(上海浦东)', + deviceCode: 'CAB-SH-001', deviceName: '1号净菜柜(上海浦东)', deviceCategory: '智能货柜', content: '自检失败:压缩机停转 3 次/小时,疑似硬件故障', triggerTime: '2026-07-03 09:24:11', status: 'pending', handler: null, + triggerRule: '压缩机停转次数 ≥ 3 次/小时(critical)', + tenantName: '上海智养医疗科技有限公司', siteName: '上海浦东 1号净菜柜', + handleSuggestion: '现场检查压缩机供电与控制板,必要时更换压缩机;修复后重新自检', + errorLog: `[2026-07-03 09:24:11.243] ERROR [self-check-thread-3] c.yx.device.SelfCheckTask - +>>> 自检失败:压缩机停转次数超阈值(当前值=3,阈值=3) + at com.yx.device.SelfCheckTask.checkCompressor(SelfCheckTask.java:142) + at com.yx.device.SelfCheckTask.run(SelfCheckTask.java:88) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) + at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:317) + at java.base/java.util.concurrent.ThreadPoolExecutor.runWorker(ThreadPoolExecutor.java:1144) + at java.base/java.util.concurrent.ThreadPoolExecutor$Worker.run(ThreadPoolExecutor.java:642) +Caused by: com.yx.device.HardwareException: 压缩机响应超时 (3000ms) + at com.yx.device.CompressorClient.ping(CompressorClient.java:67) + at com.yx.device.CompressorClient.getStatus(CompressorClient.java:42) + at com.yx.device.SelfCheckTask.checkCompressor(SelfCheckTask.java:138) + ... 6 more +`, }, { id: 'alm-002', alarmCode: 'ALM20260703002', source: 'ota', level: 'warn', - deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', + deviceCode: 'WATCH-BJ-014', deviceName: '14号健康手表(北京朝阳)', deviceCategory: '智能手表', content: 'OTA 升级失败:设备位于 v1.0.4,目标 v1.1.0,已回滚', triggerTime: '2026-07-03 10:12:48', status: 'processing', handler: '李工程师', + triggerRule: 'OTA 任务执行结果 = fail(warn)', + tenantName: '北京康养科技有限公司', siteName: '北京朝阳 14号健康手表', + handleSuggestion: '检查设备网络与电量,确认目标版本号无误后重试 OTA;必要时人工升级', + errorLog: `[2026-07-03 10:12:48.108] WARN [ota-push-worker-5] c.yx.device.ota.OtaPushService - +>>> OTA 升级失败:设备 WATCH-BJ-014 从 v1.0.4 升级到 v1.1.0 失败,已自动回滚 + at com.yx.device.ota.OtaPushService.executeUpgrade(OtaPushService.java:215) + at com.yx.device.ota.OtaPushService.handle(OtaPushService.java:96) + at com.yx.device.ota.OtaScheduler$ScheduledJob.run(OtaScheduler.java:54) +Caused by: com.yx.device.ota.FirmwareVerifyException: 固件签名校验失败:expected=sha256:8f3a...e1d2, actual=sha256:9c2b...7f4a + at com.yx.device.ota.FirmwareVerifier.verify(FirmwareVerifier.java:78) + at com.yx.device.ota.OtaPushService.executeUpgrade(OtaPushService.java:198) + ... 2 more +`, }, { id: 'alm-003', alarmCode: 'ALM20260702018', source: 'inspection', level: 'warn', - deviceCode: 'CAB-GZ-007', deviceName: '7号净菜柜(广州天河)', + deviceCode: 'CAB-GZ-007', deviceName: '7号净菜柜(广州天河)', deviceCategory: '智能货柜', content: '巡检异常:货道 5 错位,建议现场复位', triggerTime: '2026-07-02 16:30:22', status: 'workorder', handler: '王工程师', + triggerRule: '货道错位传感器触发(warn)', + tenantName: '广州智养餐饮有限公司', siteName: '广州天河 7号净菜柜', + handleSuggestion: '现场复位货道 5 传感器,重新校准后完成巡检', + errorLog: `[2026-07-02 16:30:22.571] WARN [inspection-task-1] c.yx.device.inspection.InspectionExecutor - +>>> 巡检异常:货道 5 错位(laneOffset=12mm,阈值=5mm) + at com.yx.device.inspection.InspectionExecutor.checkLane(InspectionExecutor.java:118) + at com.yx.device.inspection.InspectionExecutor.checkAllLanes(InspectionExecutor.java:64) + at com.yx.device.inspection.InspectionTask.run(InspectionTask.java:35) +[2026-07-02 16:30:23.104] INFO [inspection-task-1] c.yx.device.inspection.InspectionExecutor - + 巡检任务 INS20260702001 完成,异常项=1,已自动生成巡检异常记录 +`, }, { id: 'alm-004', alarmCode: 'ALM20260702015', source: 'sla', level: 'critical', - deviceCode: 'CHECK-SZ-003', deviceName: '3号体检一体机(深圳南山)', + deviceCode: 'CHECK-SZ-003', deviceName: '3号体检一体机(深圳南山)', deviceCategory: '智能健康一体机', content: 'SLA 超时:连续 3 次心跳未上报,已离线 25 分钟', triggerTime: '2026-07-02 14:05:09', status: 'ignored', handler: '赵工程师', + triggerRule: '心跳连续缺失次数 ≥ 3(critical)', + tenantName: '深圳南山区医疗集团', siteName: '深圳南山 3号体检一体机', + handleSuggestion: '现场排查网络与电源;如设备损坏需走维修流程', + errorLog: `[2026-07-02 14:05:09.821] ERROR [sla-monitor-2] c.yx.device.sla.SlaMonitor - +>>> SLA 超时:设备 CHECK-SZ-003 已离线 25 分钟(连续 3 次心跳未上报) + at com.yx.device.sla.SlaMonitor.checkHeartbeat(SlaMonitor.java:92) + at com.yx.device.sla.SlaMonitor.scan(SlaMonitor.java:54) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) +[2026-07-02 13:40:09.103] WARN [sla-monitor-2] c.yx.device.sla.SlaMonitor - + 设备 CHECK-SZ-003 心跳缺失(第 1 次),最后心跳时间=2026-07-02 13:39:45 +[2026-07-02 13:50:09.105] WARN [sla-monitor-2] c.yx.device.sla.SlaMonitor - + 设备 CHECK-SZ-003 心跳缺失(第 2 次),最后心跳时间=2026-07-02 13:39:45 +[2026-07-02 14:00:09.107] WARN [sla-monitor-2] c.yx.device.sla.SlaMonitor - + 设备 CHECK-SZ-003 心跳缺失(第 3 次),最后心跳时间=2026-07-02 13:39:45 +`, }, { id: 'alm-005', alarmCode: 'ALM20260702009', source: 'dataDistribute', level: 'warn', - deviceCode: 'CAB-SH-002', deviceName: '2号净菜柜(上海闵行)', + deviceCode: 'CAB-SH-002', deviceName: '2号净菜柜(上海闵行)', deviceCategory: '智能货柜', content: '数据分发失败:营养系统订阅通道 4 次重试失败', triggerTime: '2026-07-02 11:48:33', status: 'workorder', handler: '李工程师', + triggerRule: '分发重试次数 ≥ 4(warn)', + tenantName: '上海智养医疗科技有限公司', siteName: '上海闵行 2号净菜柜', + handleSuggestion: '检查营养系统订阅服务可用性;必要时人工补偿数据', + errorLog: `[2026-07-02 11:48:33.412] WARN [distribute-worker-7] c.yx.device.distribute.DataDistributeService - +>>> 数据分发失败:目标=营养系统订阅通道,重试次数=4/4(已达上限) + at com.yx.device.distribute.DataDistributeService.publish(DataDistributeService.java:156) + at com.yx.device.distribute.DataDistributeService.handle(DataDistributeService.java:73) + at com.yx.device.distribute.DataDistributeScheduler$Job.run(DataDistributeScheduler.java:42) +Caused by: com.yx.device.distribute.SubscribeChannelException: 订阅通道连接被拒绝(connection refused) + at com.yx.device.distribute.SubscribeClient.connect(SubscribeClient.java:88) + at com.yx.device.distribute.SubscribeClient.publish(SubscribeClient.java:54) + at com.yx.device.distribute.DataDistributeService.publish(DataDistributeService.java:148) + ... 2 more +[2026-07-02 11:48:33.412] INFO [distribute-worker-7] c.yx.device.distribute.DataDistributeService - + 分发任务已转入死信队列,任务ID=DT-20260702-114833-002 +`, }, { id: 'alm-006', alarmCode: 'ALM20260701022', source: 'runLog', level: 'info', - deviceCode: 'BODYFAT-HZ-005', deviceName: '5号体脂仪(杭州西湖)', + deviceCode: 'BODYFAT-HZ-005', deviceName: '5号体脂仪(杭州西湖)', deviceCategory: '体脂仪', content: '运行日志:电池电量低于 15%,建议安排更换', triggerTime: '2026-07-01 17:20:00', status: 'ignored', handler: null, + triggerRule: '电池电量 < 15%(info)', + tenantName: '杭州西湖区医疗中心', siteName: '杭州西湖 5号体脂仪', + handleSuggestion: '安排运维人员更换电池', + errorLog: `[2026-07-01 17:20:00.000] INFO [run-log-collector-1] c.yx.device.runlog.RunLogCollector - +>>> 运行日志:设备 BODYFAT-HZ-005 电池电量=12%(阈值=15%) + at com.yx.device.runlog.RunLogCollector.collectBattery(RunLogCollector.java:64) + at com.yx.device.runlog.RunLogCollector.collect(RunLogCollector.java:38) + at com.yx.device.runlog.RunLogScheduler$ScheduledJob.run(RunLogScheduler.java:29) +[2026-07-01 17:20:00.000] INFO [run-log-collector-1] c.yx.device.runlog.RunLogCollector - + 建议处理动作:通知运维人员更换电池 +`, }, { id: 'alm-007', alarmCode: 'ALM20260701008', source: 'selfCheck', level: 'warn', - deviceCode: 'CAB-BJ-003', deviceName: '3号净菜柜(北京海淀)', + deviceCode: 'CAB-BJ-003', deviceName: '3号净菜柜(北京海淀)', deviceCategory: '智能货柜', content: '自检告警:温度传感器读数漂移 2.4℃,建议校准', triggerTime: '2026-07-01 09:15:42', status: 'workorder', handler: '王工程师', + triggerRule: '温度传感器读数漂移 ≥ 2℃(warn)', + tenantName: '北京海淀餐饮集团', siteName: '北京海淀 3号净菜柜', + handleSuggestion: '现场校准温度传感器;如漂移持续需更换传感器', + errorLog: `[2026-07-01 09:15:42.221] WARN [self-check-thread-1] c.yx.device.SelfCheckTask - +>>> 自检告警:温度传感器读数漂移(drift=2.4℃,阈值=2.0℃) + at com.yx.device.SelfCheckTask.checkTempSensor(SelfCheckTask.java:176) + at com.yx.device.SelfCheckTask.run(SelfCheckTask.java:85) + at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:572) +[2026-07-01 09:15:42.221] INFO [self-check-thread-1] c.yx.device.SelfCheckTask - + 温度传感器读数:currentValue=-3.2℃,standardValue=-0.8℃ +`, }, { id: 'alm-008', alarmCode: 'ALM20260630031', source: 'ota', level: 'critical', - deviceCode: 'CHECK-SH-008', deviceName: '8号体检一体机(上海徐汇)', + deviceCode: 'CHECK-SH-008', deviceName: '8号体检一体机(上海徐汇)', deviceCategory: '智能健康一体机', content: 'OTA 升级失败:固件签名校验未通过,已拦截', triggerTime: '2026-06-30 22:08:55', status: 'processing', handler: '赵工程师', + triggerRule: 'OTA 固件签名校验失败(critical)', + tenantName: '上海徐汇区医疗集团', siteName: '上海徐汇 8号体检一体机', + handleSuggestion: '联系固件提供方核对签名公钥;确认固件完整性后重新推送', + errorLog: `[2026-06-30 22:08:55.642] ERROR [ota-push-worker-2] c.yx.device.ota.OtaPushService - +>>> OTA 升级失败:设备 CHECK-SH-008 固件签名校验未通过,已拦截升级 + at com.yx.device.ota.FirmwareVerifier.verify(FirmwareVerifier.java:81) + at com.yx.device.ota.OtaPushService.executeUpgrade(OtaPushService.java:198) + at com.yx.device.ota.OtaPushService.handle(OtaPushService.java:96) +Caused by: java.security.SignatureException: Signature does not match + at java.base/java.security.Signature.verify(Signature.java:610) + at com.yx.device.ota.FirmwareVerifier.verify(FirmwareVerifier.java:75) + ... 2 more +[2026-06-30 22:08:55.642] WARN [ota-push-worker-2] c.yx.device.ota.OtaPushService - + 已自动拦截升级任务,原版本=v1.1.0,目标版本=v1.2.0 +`, }, ] -const logList: BusinessLogRecord[] = [ - { - id: 'log-001', logType: 'operate', siteName: '上海浦东 1号净菜柜', - module: '工单管理', content: '创建工单 WO20260703001(基于告警 ALM20260703001)', - operator: '李工程师', operateTime: '2026-07-03 09:30:14', - }, - { - id: 'log-002', logType: 'system', siteName: '全局', - module: '设备初始化', content: '设备 CAB-SH-001 完成证书烧录,进入待出库状态', - operator: '系统', operateTime: '2026-07-03 08:50:01', - }, - { - id: 'log-003', logType: 'audit', siteName: '北京朝阳 14号健康手表', - module: '告警管理', content: '用户 [李工程师] 处理告警 ALM20260703002:标记为处理中', - operator: '李工程师', operateTime: '2026-07-03 10:15:00', - }, - { - id: 'log-004', logType: 'data', siteName: '广州天河 7号净菜柜', - module: '数据分发', content: '推送销售数据到营养系统成功(耗时 220ms)', - operator: '系统', operateTime: '2026-07-02 18:00:11', - }, - { - id: 'log-005', logType: 'operate', siteName: '深圳南山 3号体检一体机', - module: '版本管理', content: '固件版本 V3.0.0 灰度推送至 30% 设备', - operator: '赵工程师', operateTime: '2026-07-02 15:42:30', - }, - { - id: 'log-006', logType: 'audit', siteName: '上海闵行 2号净菜柜', - module: '告警管理', content: '用户 [王工程师] 转工单 ALM20260702009', - operator: '王工程师', operateTime: '2026-07-02 14:00:55', - }, - { - id: 'log-007', logType: 'system', siteName: '全局', - module: '设备监测', content: '触发巡检任务 INS20260702001(覆盖上海区域 12 台设备)', - operator: '系统', operateTime: '2026-07-02 09:00:00', - }, - { - id: 'log-008', logType: 'operate', siteName: '杭州西湖 5号体脂仪', - module: '告警管理', content: '用户 [孙工程师] 忽略告警 ALM20260701022', - operator: '孙工程师', operateTime: '2026-07-01 17:35:20', - }, +const CATEGORY_OPTIONS: { name: string }[] = [ + { name: '智能货柜' }, + { name: '智能手表' }, + { name: '智能健康一体机' }, + { name: '体脂仪' }, + { name: '蓝牙秤' }, + { name: '健身车' }, + { name: '带鱼屏' }, + { name: '入库秤' }, ] const listAlarm = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = alarmList.filter((item) => { - if (params.keyword) { - const kw = params.keyword - if (!includes(item.alarmCode, kw) && !includes(item.deviceCode, kw) && - !includes(item.deviceName, kw) && !includes(item.content, kw)) return false - } + if (params.deviceCode && !item.deviceCode.toLowerCase().includes(params.deviceCode.toLowerCase())) return false + if (params.deviceCategory && item.deviceCategory !== params.deviceCategory) return false if (params.source && item.source !== params.source) return false if (params.level && item.level !== params.level) return false if (params.status && item.status !== params.status) return false @@ -169,6 +239,13 @@ const statsAlarm = (): ApiResponse<{ todayNew: number; pending: number; critical return ok('查询成功', { todayNew: 2, pending: 3, critical: 2, weekTrend: 11 }) } +const infoAlarm = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const target = alarmList.find((i) => i.id === String(params.id ?? '')) + if (!target) return ok('告警不存在') + return ok('查询成功', target) +} + const toWorkOrder = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const target = alarmList.find((i) => i.id === String(params.id ?? '')) @@ -204,25 +281,14 @@ const batchHandle = (ctx: MockContext): ApiResponse => { return ok(type === 'workorder' ? '批量转工单成功' : '批量忽略成功') } -const listBusinessLog = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const filtered = logList.filter((item) => { - if (params.keyword) { - const kw = params.keyword - if (!includes(item.module, kw) && !includes(item.content, kw)) return false - } - if (params.logType && item.logType !== params.logType) return false - if (params.siteName && !includes(item.siteName, params.siteName)) return false - return true - }) - return ok('查询成功', filtered) -} +const categoryOpts = (): ApiResponse<{ name: string }[]> => ok('查询成功', CATEGORY_OPTIONS) registerMocks([ { method: 'POST', url: '/admin/device/ops/alarm/list', handler: listAlarm }, { method: 'POST', url: '/admin/device/ops/alarm/stats', handler: statsAlarm }, + { method: 'POST', url: '/admin/device/ops/alarm/info', handler: infoAlarm }, { method: 'POST', url: '/admin/device/ops/alarm/to-work-order', handler: toWorkOrder }, { method: 'POST', url: '/admin/device/ops/alarm/ignore', handler: ignoreAlarm }, { method: 'POST', url: '/admin/device/ops/alarm/batch', handler: batchHandle }, - { method: 'POST', url: '/admin/device/ops/alarm/log-list', handler: listBusinessLog }, + { method: 'POST', url: '/admin/device/ops/alarm/category/options', handler: categoryOpts }, ]) diff --git a/src/axios/handlers/device-center/ops/config.ts b/src/axios/handlers/device-center/ops/config.ts deleted file mode 100644 index 6b69879..0000000 --- a/src/axios/handlers/device-center/ops/config.ts +++ /dev/null @@ -1,203 +0,0 @@ -import { registerMocks } from '@axios/mockBus' -import type { ApiResponse, MockContext, RequestParameter } from '@axios' - -/** - * 设备运维 / 系统配置 mock - * - 5 大分组的全局参数(推送通道 / 限流 / 巡检自检 / SLA / OTA) - * - 编辑配置(写历史) - * - 变更历史列表 - */ - -type ConfigGroup = 'push' | 'rateLimit' | 'inspect' | 'sla' | 'ota' -type ConfigValueType = 'number' | 'string' | 'boolean' | 'json' - -interface ConfigRecord { - id: string - group: ConfigGroup - configKey: string - configName: string - configValue: string - valueType: ConfigValueType - unit?: string - description: string - updater: string - updateTime: string -} - -interface HistoryRecord { - id: string - configId: string - oldValue: string - newValue: string - operator: string - operateTime: string - reason: string -} - -interface ConfigParams { - keyword?: string - group?: string - id?: string - configId?: string - configValue?: string - reason?: string - [key: string]: unknown -} - -const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) -const getParams = (params: RequestParameter): ConfigParams => (params ?? {}) as ConfigParams -const includes = (value: unknown, keyword: unknown): boolean => { - if (!keyword) return true - return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) -} -const now = (): string => new Date().toISOString().replace('T', ' ').slice(0, 19) - -const configList: ConfigRecord[] = [ - // ===== 推送通道 ===== - { - id: 'cfg-push-01', group: 'push', configKey: 'push.callback.timeout', - configName: '回调超时时间', configValue: '5', valueType: 'number', unit: '秒', - description: 'HTTP 回调等待响应的最大时长,超过则判定为推送失败并重试', - updater: '李工程师', updateTime: '2026-06-22 15:30:00', - }, - { - id: 'cfg-push-02', group: 'push', configKey: 'push.callback.retry', - configName: '回调重试次数', configValue: '4', valueType: 'number', unit: '次', - description: '推送失败后最大重试次数(指数退避:5s / 30s / 2min / 10min)', - updater: '李工程师', updateTime: '2026-06-22 15:30:00', - }, - { - id: 'cfg-push-03', group: 'push', configKey: 'push.callback.concurrency', - configName: '回调并发数', configValue: '20', valueType: 'number', unit: '路', - description: '推送通道全局并发请求数,超过将排队等待', - updater: '李工程师', updateTime: '2026-06-22 15:30:00', - }, - // ===== 限流 / 黑白名单 ===== - { - id: 'cfg-rl-01', group: 'rateLimit', configKey: 'rateLimit.qps', - configName: '全局限流 QPS', configValue: '500', valueType: 'number', unit: '次/秒', - description: '租户调用设备数据查询接口的全局 QPS 上限', - updater: '赵工程师', updateTime: '2026-06-15 10:20:00', - }, - { - id: 'cfg-rl-02', group: 'rateLimit', configKey: 'rateLimit.ip.whitelist', - configName: 'IP 白名单', configValue: '10.0.0.0/8,172.16.0.0/12', valueType: 'string', - description: '设备数据查询接口的 IP 白名单(CIDR 逗号分隔)', - updater: '赵工程师', updateTime: '2026-06-15 10:20:00', - }, - // ===== 巡检 / 自检 ===== - { - id: 'cfg-ins-01', group: 'inspect', configKey: 'inspection.defaultPeriod', - configName: '巡检默认周期', configValue: '24', valueType: 'number', unit: '小时', - description: '新建设备的默认巡检周期,超过未上报视为巡检异常', - updater: '王工程师', updateTime: '2026-06-10 14:00:00', - }, - { - id: 'cfg-ins-02', group: 'inspect', configKey: 'selfCheck.failThreshold', - configName: '自检告警阈值', configValue: '2', valueType: 'number', unit: '次', - description: '设备连续自检失败次数达到阈值即产生告警', - updater: '王工程师', updateTime: '2026-06-10 14:00:00', - }, - // ===== SLA ===== - { - id: 'cfg-sla-01', group: 'sla', configKey: 'sla.urgent.responseMinutes', - configName: '紧急工单响应时长', configValue: '30', valueType: 'number', unit: '分钟', - description: '紧急级别工单必须在此时长内有人接单', - updater: '孙工程师', updateTime: '2026-05-28 09:00:00', - }, - { - id: 'cfg-sla-02', group: 'sla', configKey: 'sla.urgent.resolveHours', - configName: '紧急工单处理时长', configValue: '4', valueType: 'number', unit: '小时', - description: '紧急级别工单从接单到完成的处理时长上限', - updater: '孙工程师', updateTime: '2026-05-28 09:00:00', - }, - // ===== OTA ===== - { - id: 'cfg-ota-01', group: 'ota', configKey: 'ota.defaultGrayPercent', - configName: '默认灰度比例', configValue: '10', valueType: 'number', unit: '%', - description: '新建版本默认的灰度推送比例,可后续逐级提升', - updater: '李工程师', updateTime: '2026-06-25 17:00:00', - }, - { - id: 'cfg-ota-02', group: 'ota', configKey: 'ota.rollback.failRate', - configName: 'OTA 自动回滚阈值', configValue: '5', valueType: 'number', unit: '%', - description: '灰度阶段失败率超过此阈值将自动触发回滚', - updater: '李工程师', updateTime: '2026-06-25 17:00:00', - }, -] - -const historyList: HistoryRecord[] = [ - { - id: 'his-001', configId: 'cfg-push-01', oldValue: '3', newValue: '5', - operator: '李工程师', operateTime: '2026-06-22 15:30:00', - reason: '部分下游系统回调耗时较高,超时时间从 3 秒调整为 5 秒以降低误报', - }, - { - id: 'his-002', configId: 'cfg-push-02', oldValue: '3', newValue: '4', - operator: '李工程师', operateTime: '2026-06-22 15:30:00', - reason: '配合超时调整,将重试次数从 3 次提升到 4 次以提高送达率', - }, - { - id: 'his-003', configId: 'cfg-ota-01', oldValue: '5', newValue: '10', - operator: '李工程师', operateTime: '2026-06-25 17:00:00', - reason: 'V1.2.0 灰度验证通过,将默认灰度比例从 5% 提升至 10% 加快推广', - }, - { - id: 'his-004', configId: 'cfg-sla-01', oldValue: '60', newValue: '30', - operator: '孙工程师', operateTime: '2026-05-28 09:00:00', - reason: '紧急工单响应时长从 60 分钟缩短到 30 分钟,对齐 SLA 协议升级', - }, -] - -const listConfig = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const filtered = configList.filter((item) => { - if (params.keyword) { - const kw = params.keyword - if (!includes(item.configKey, kw) && !includes(item.configName, kw) && !includes(item.description, kw)) return false - } - if (params.group && item.group !== params.group) return false - return true - }) - return ok('查询成功', filtered) -} - -const infoConfig = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - return ok('查询成功', configList.find((i) => i.id === params.id) ?? null) -} - -const updateConfig = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const target = configList.find((i) => i.id === String(params.id ?? '')) - if (!target) return ok('配置不存在') - const oldValue = target.configValue - const newValue = String(params.configValue ?? '') - if (oldValue === newValue) return ok('配置值未变化') - target.configValue = newValue - target.updater = '当前用户' - target.updateTime = now() - historyList.unshift({ - id: `his-${String(historyList.length + 1).padStart(3, '0')}`, - configId: target.id, - oldValue, - newValue, - operator: '当前用户', - operateTime: now(), - reason: String(params.reason ?? ''), - }) - return ok('配置已更新') -} - -const listHistory = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const filtered = historyList.filter((i) => i.configId === String(params.configId ?? '')) - return ok('查询成功', filtered) -} - -registerMocks([ - { method: 'POST', url: '/admin/device/ops/config/list', handler: listConfig }, - { method: 'POST', url: '/admin/device/ops/config/info', handler: infoConfig }, - { method: 'POST', url: '/admin/device/ops/config/update', handler: updateConfig }, - { method: 'POST', url: '/admin/device/ops/config/history', handler: listHistory }, -]) diff --git a/src/axios/handlers/device-center/ops/init.ts b/src/axios/handlers/device-center/ops/init.ts index 28cc324..d8067ae 100644 --- a/src/axios/handlers/device-center/ops/init.ts +++ b/src/axios/handlers/device-center/ops/init.ts @@ -1,287 +1,378 @@ +/** + * 设备运维 / 初始化设置 mock + * + * 业务定位:IOT 设备出库投用前的软件版本设置 + * - list 列表查询(设备视角) + * - info 详情(含类别 / 数据分发 / 软件版本详情) + * - set-version / batch-set-version 版本设置 + * - export 异步导出 + * - app/version/category/customer/site 下拉 + */ import { registerMocks } from '@axios/mockBus' import type { ApiResponse, MockContext, RequestParameter } from '@axios' +import type { + AppOption, + CustomerOption, + DeviceCategoryOption, + InitDetail, + InitItem, + SiteOption, + VersionOption, +} from '../../../pages/admin-portal/device-center/ops/init/types' -/** - * 设备运维 / 设备初始化 mock - * - 出库装机前的激活 / 烧录证书 / 预置场所配置 / 固件基线 - */ +const BASE = '/admin/device/ops/init' -interface InitRecord { - id: string - taskCode: string - deviceSn: string - deviceName: string - deviceCategory: 'weightScale' | 'bodyFat' | 'watch' | 'healthCheck' | 'cabinet' | 'other' - targetSite: string - baselineFw: string - baselineApp: string - activateCode: string - status: 'pending' | 'processing' | 'success' | 'failed' - operator: string - initTime: string | null - failReason?: string - outboundOrderNo?: string - createTime: string - certPayload: string - siteConfig: string - strategy: string - steps: { id: string; stepName: string; status: 'pending' | 'processing' | 'success' | 'failed'; costSeconds: number; finishTime: string | null; remark?: string }[] -} - -interface InitParams { +type ListParams = { keyword?: string - deviceSn?: string + deviceCode?: string deviceCategory?: string - status?: string - id?: string - [key: string]: unknown + initStatus?: string + deployStatus?: string + customerId?: string + siteId?: string + outboundOrderNo?: string } -const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) -const getParams = (params: RequestParameter): InitParams => (params ?? {}) as InitParams -const includes = (value: unknown, keyword: unknown): boolean => { - if (!keyword) return true - return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) -} +const getParams = (params: RequestParameter): T => (params ?? {}) as T -const initList: InitRecord[] = [ +const ok = (msg: string, data: T = null as T): ApiResponse => ({ + code: '00000', msg, data, +}) + +/** 应用名称 mock */ +const APP_DATASET: AppOption[] = [ + { id: 'app-001', name: '智养秤终端 App', description: '体重秤/体脂仪终端 App' }, + { id: 'app-002', name: '智能手表 App', description: '智能手表终端 App' }, + { id: 'app-003', name: '体检一体机 App', description: '体检一体机终端 App' }, + { id: 'app-004', name: '净菜柜固件', description: '净菜柜屏固件 App' }, +] + +/** 版本号 mock(按应用联动) */ +const VERSION_DATASET: VersionOption[] = [ + { id: 'ver-001', appId: 'app-001', version: 'V1.2.0', description: '体重秤/体脂仪终端 App', upgradePoints: '修复数据上报丢包问题', packageUrl: 'https://mock.example.com/pkg/app-001/v1.2.0.bin' }, + { id: 'ver-002', appId: 'app-001', version: 'V1.3.0', description: '体重秤/体脂仪终端 App', upgradePoints: '新增蓝牙配网引导流程', packageUrl: 'https://mock.example.com/pkg/app-001/v1.3.0.bin' }, + { id: 'ver-003', appId: 'app-002', version: 'V2.0.1', description: '智能手表终端 App', upgradePoints: '心率异常阈值调整', packageUrl: 'https://mock.example.com/pkg/app-002/v2.0.1.bin' }, + { id: 'ver-004', appId: 'app-002', version: 'V2.1.0', description: '智能手表终端 App', upgradePoints: '新增 OTA 静默升级模式', packageUrl: 'https://mock.example.com/pkg/app-002/v2.1.0.bin' }, + { id: 'ver-005', appId: 'app-003', version: 'V3.0.0', description: '体检一体机终端 App', upgradePoints: '新增身份识别多模态', packageUrl: 'https://mock.example.com/pkg/app-003/v3.0.0.bin' }, + { id: 'ver-006', appId: 'app-004', version: 'V0.9.5', description: '净菜柜屏固件 App', upgradePoints: '柜门传感器识别优化', packageUrl: 'https://mock.example.com/pkg/app-004/v0.9.5.bin' }, +] + +/** 设备类别 mock(二级分类名) */ +const CATEGORY_DATASET: DeviceCategoryOption[] = [ + { name: '体重秤' }, + { name: '体脂仪' }, + { name: '智能手表' }, + { name: '体检一体机' }, + { name: '净菜柜' }, +] + +/** 客户 mock */ +const CUSTOMER_DATASET: CustomerOption[] = [ + { id: 'cus-001', name: '阳光养老院' }, + { id: 'cus-002', name: '康乐社区卫生服务中心' }, + { id: 'cus-003', name: '福寿康养集团' }, +] + +/** 场所 mock(按客户联动) */ +const SITE_DATASET: SiteOption[] = [ + { id: 'site-001', name: '阳光养老院 · 1号楼', customerId: 'cus-001' }, + { id: 'site-002', name: '阳光养老院 · 食堂', customerId: 'cus-001' }, + { id: 'site-003', name: '康乐社区 · 健康小屋', customerId: 'cus-002' }, + { id: 'site-004', name: '福寿康养 · 总部', customerId: 'cus-003' }, +] + +/** 初始化设备 mock dataset(8 条,符合原型展示上限) */ +const INIT_DATASET: InitItem[] = [ { - id: 'init-001', taskCode: 'INIT-20260620-001', deviceSn: 'CAB-A1-0001', deviceName: '总部食堂货柜A1', - deviceCategory: 'cabinet', targetSite: '长庆总部一食堂', baselineFw: 'V1.2.0', baselineApp: 'V1.0.5', - activateCode: 'ACT-20260620-A1B2C3D4', status: 'success', operator: '李工程师', initTime: '2026-06-20 14:30:00', - outboundOrderNo: 'OUT-20260620-001', createTime: '2026-06-20 13:45:12', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"CQ-HQ-CANTEEN-01","mqTopic":"device.cabinet","heartbeat":30}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 12, finishTime: '2026-06-20 14:25:30' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 4, finishTime: '2026-06-20 14:25:34' }, - { id: 's3', stepName: '固件基线烧录', status: 'success', costSeconds: 180, finishTime: '2026-06-20 14:28:34' }, - { id: 's4', stepName: 'APP 基线烧录', status: 'success', costSeconds: 90, finishTime: '2026-06-20 14:30:04' }, - ], + id: 'dev-001', + deviceCode: 'WT-2025-0001', + deviceCategory: '体重秤', + deviceName: '智养体重秤', + serialNumber: 'SN-WT-2025-0001', + initStatus: 'pending', + packageName: '', + softwareVersion: '', + deployStatus: 'in_stock', + customerId: 'cus-001', + customerName: '阳光养老院', + siteId: 'site-001', + siteName: '阳光养老院 · 1号楼', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250701-001', + onlineTime: '2025-07-01 09:30:00', + deployTime: '', + initTime: '', }, { - id: 'init-002', taskCode: 'INIT-20260622-002', deviceSn: 'WAT-W1-0001', deviceName: '健康站01手表', - deviceCategory: 'watch', targetSite: '健康园区体检站', baselineFw: 'V2.0.1', baselineApp: 'V1.3.0', - activateCode: 'ACT-20260622-E5F6G7H8', status: 'failed', operator: '王工程师', initTime: null, - failReason: '固件烧录超时,设备未在 180s 内完成 OTA', outboundOrderNo: 'OUT-20260622-002', createTime: '2026-06-22 09:10:00', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"HP-CHKUP-01","mqTopic":"device.watch","heartbeat":60}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 10, finishTime: '2026-06-22 09:15:00' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 3, finishTime: '2026-06-22 09:15:03' }, - { id: 's3', stepName: '固件基线烧录', status: 'failed', costSeconds: 180, finishTime: null, remark: 'OTA 超时' }, - ], + id: 'dev-002', + deviceCode: 'BF-2025-0002', + deviceCategory: '体脂仪', + deviceName: '智养体脂仪', + serialNumber: 'SN-BF-2025-0002', + initStatus: 'success', + packageName: '智养秤终端 App', + softwareVersion: 'V1.3.0', + deployStatus: 'deployed', + customerId: 'cus-001', + customerName: '阳光养老院', + siteId: 'site-002', + siteName: '阳光养老院 · 食堂', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250701-001', + onlineTime: '2025-07-01 10:15:00', + deployTime: '2025-07-02 14:00:00', + initTime: '2025-07-02 13:50:00', }, { - id: 'init-003', taskCode: 'INIT-20260624-003', deviceSn: 'CHK-M1-0001', deviceName: '体检站一体机', - deviceCategory: 'healthCheck', targetSite: '东区体检站', baselineFw: 'V3.0.0', baselineApp: 'V2.0.0', - activateCode: 'ACT-20260624-I9J0K1L2', status: 'processing', operator: '赵工程师', initTime: null, - outboundOrderNo: 'OUT-20260624-003', createTime: '2026-06-24 10:00:00', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"HP-CHKUP-01","mqTopic":"device.healthcheck","heartbeat":120}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 11, finishTime: '2026-06-24 10:05:00' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 4, finishTime: '2026-06-24 10:05:04' }, - { id: 's3', stepName: '固件基线烧录', status: 'processing', costSeconds: 90, finishTime: null, remark: '升级中…' }, - ], + id: 'dev-003', + deviceCode: 'WT-2025-0003', + deviceCategory: '智能手表', + deviceName: '智养手表', + serialNumber: 'SN-WT-2025-0003', + initStatus: 'pending', + packageName: '', + softwareVersion: '', + deployStatus: 'in_stock', + customerId: 'cus-002', + customerName: '康乐社区卫生服务中心', + siteId: 'site-003', + siteName: '康乐社区 · 健康小屋', + dataTransit: false, + dataReport: true, + outboundOrderNo: 'OUT-20250703-008', + onlineTime: '2025-07-03 11:00:00', + deployTime: '', + initTime: '', }, { - id: 'init-004', taskCode: 'INIT-20260625-004', deviceSn: 'FAT-INBODY-0001', deviceName: '体检站体脂秤', - deviceCategory: 'bodyFat', targetSite: '东区体检站', baselineFw: 'V1.5.0', baselineApp: 'V1.2.0', - activateCode: 'ACT-20260625-M3N4O5P6', status: 'success', operator: '孙工程师', initTime: '2026-06-25 16:00:00', - createTime: '2026-06-25 15:30:00', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"HP-CHKUP-01","mqTopic":"device.bodyfat","heartbeat":60}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 9, finishTime: '2026-06-25 15:55:00' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 3, finishTime: '2026-06-25 15:55:03' }, - { id: 's3', stepName: '固件基线烧录', status: 'success', costSeconds: 150, finishTime: '2026-06-25 15:57:33' }, - { id: 's4', stepName: 'APP 基线烧录', status: 'success', costSeconds: 80, finishTime: '2026-06-25 15:58:53' }, - ], + id: 'dev-004', + deviceCode: 'HC-2025-0004', + deviceCategory: '体检一体机', + deviceName: '智养体检一体机', + serialNumber: 'SN-HC-2025-0004', + initStatus: 'pending', + packageName: '', + softwareVersion: '', + deployStatus: 'in_stock', + customerId: 'cus-003', + customerName: '福寿康养集团', + siteId: 'site-004', + siteName: '福寿康养 · 总部', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250705-012', + onlineTime: '2025-07-05 16:20:00', + deployTime: '', + initTime: '', }, { - id: 'init-005', taskCode: 'INIT-20260626-005', deviceSn: 'SCL-W-0001', deviceName: '健康站体重秤', - deviceCategory: 'weightScale', targetSite: '健康园区体检站', baselineFw: 'V1.0.3', baselineApp: 'V1.0.0', - activateCode: 'ACT-20260626-Q7R8S9T0', status: 'pending', operator: '—', initTime: null, - createTime: '2026-06-26 08:30:00', - certPayload: '—', siteConfig: '{}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [], + id: 'dev-005', + deviceCode: 'CB-2025-0005', + deviceCategory: '净菜柜', + deviceName: '智养净菜柜', + serialNumber: 'SN-CB-2025-0005', + initStatus: 'pending', + packageName: '', + softwareVersion: '', + deployStatus: 'in_stock', + customerId: 'cus-001', + customerName: '阳光养老院', + siteId: 'site-002', + siteName: '阳光养老院 · 食堂', + dataTransit: true, + dataReport: false, + outboundOrderNo: 'OUT-20250706-015', + onlineTime: '2025-07-06 09:00:00', + deployTime: '', + initTime: '', }, { - id: 'init-006', taskCode: 'INIT-20260627-006', deviceSn: 'FIT-S1-0001', deviceName: '健身房顿米车', - deviceCategory: 'other', targetSite: '园区员工健身房', baselineFw: 'V2.1.0', baselineApp: 'V1.0.5', - activateCode: 'ACT-20260627-U1V2W3X4', status: 'success', operator: '李工程师', initTime: '2026-06-27 11:00:00', - createTime: '2026-06-27 10:30:00', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"HP-GYM-01","mqTopic":"device.fitness","heartbeat":120}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 12, finishTime: '2026-06-27 10:50:00' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 4, finishTime: '2026-06-27 10:50:04' }, - { id: 's3', stepName: '固件基线烧录', status: 'success', costSeconds: 180, finishTime: '2026-06-27 10:53:04' }, - { id: 's4', stepName: 'APP 基线烧录', status: 'success', costSeconds: 90, finishTime: '2026-06-27 10:54:34' }, - ], + id: 'dev-006', + deviceCode: 'WT-2025-0006', + deviceCategory: '体重秤', + deviceName: '智养体重秤', + serialNumber: 'SN-WT-2025-0006', + initStatus: 'success', + packageName: '智养秤终端 App', + softwareVersion: 'V1.2.0', + deployStatus: 'deployed', + customerId: 'cus-002', + customerName: '康乐社区卫生服务中心', + siteId: 'site-003', + siteName: '康乐社区 · 健康小屋', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250707-020', + onlineTime: '2025-07-07 10:30:00', + deployTime: '2025-07-08 09:00:00', + initTime: '2025-07-08 08:50:00', }, { - id: 'init-007', taskCode: 'INIT-20260628-007', deviceSn: 'WAT-W1-0002', deviceName: '健康站02手表', - deviceCategory: 'watch', targetSite: '总部体重测量点 A', baselineFw: 'V2.0.1', baselineApp: 'V1.3.0', - activateCode: 'ACT-20260628-Y5Z6A7B8', status: 'failed', operator: '王工程师', initTime: null, - failReason: '证书签名校验失败,请检查激活码', createTime: '2026-06-28 14:00:00', - certPayload: '—', - siteConfig: '{"siteCode":"CQ-HQ-MEASURE-A","mqTopic":"device.watch"}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'failed', costSeconds: 5, finishTime: null, remark: '签名校验失败' }, - ], + id: 'dev-007', + deviceCode: 'BF-2025-0007', + deviceCategory: '体脂仪', + deviceName: '智养体脂仪', + serialNumber: 'SN-BF-2025-0007', + initStatus: 'pending', + packageName: '', + softwareVersion: '', + deployStatus: 'in_stock', + customerId: 'cus-003', + customerName: '福寿康养集团', + siteId: 'site-004', + siteName: '福寿康养 · 总部', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250708-025', + onlineTime: '2025-07-08 14:00:00', + deployTime: '', + initTime: '', }, { - id: 'init-008', taskCode: 'INIT-20260629-008', deviceSn: 'CHK-M1-0002', deviceName: '社区站一体机', - deviceCategory: 'healthCheck', targetSite: '采油一厂社区站', baselineFw: 'V3.0.0', baselineApp: 'V2.0.0', - activateCode: 'ACT-20260629-C9D0E1F2', status: 'success', operator: '赵工程师', initTime: '2026-06-29 17:30:00', - createTime: '2026-06-29 16:45:00', - certPayload: '-----BEGIN CERTIFICATE-----\nMIIBxTCCAW8CFA==\n-----END CERTIFICATE-----', - siteConfig: '{"siteCode":"OILF-01-COMMUNITY","mqTopic":"device.healthcheck"}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [ - { id: 's1', stepName: '设备激活与证书烧录', status: 'success', costSeconds: 11, finishTime: '2026-06-29 17:20:00' }, - { id: 's2', stepName: '场所配置预置', status: 'success', costSeconds: 4, finishTime: '2026-06-29 17:20:04' }, - { id: 's3', stepName: '固件基线烧录', status: 'success', costSeconds: 160, finishTime: '2026-06-29 17:22:44' }, - { id: 's4', stepName: 'APP 基线烧录', status: 'success', costSeconds: 85, finishTime: '2026-06-29 17:24:09' }, - ], + id: 'dev-008', + deviceCode: 'WT-2025-0008', + deviceCategory: '体重秤', + deviceName: '智养体重秤', + serialNumber: 'SN-WT-2025-0008', + initStatus: 'success', + packageName: '智养秤终端 App', + softwareVersion: 'V1.3.0', + deployStatus: 'deployed', + customerId: 'cus-001', + customerName: '阳光养老院', + siteId: 'site-001', + siteName: '阳光养老院 · 1号楼', + dataTransit: true, + dataReport: true, + outboundOrderNo: 'OUT-20250709-030', + onlineTime: '2025-07-09 08:00:00', + deployTime: '2025-07-09 10:30:00', + initTime: '2025-07-09 10:20:00', }, ] -const matchKeyword = (item: InitRecord, keyword?: string): boolean => { - if (!keyword) return true - return includes(item.taskCode, keyword) || includes(item.deviceSn, keyword) || includes(item.deviceName, keyword) -} - -const listInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const filtered = initList.filter((item) => { - if (!matchKeyword(item, params.keyword)) return false - if (params.deviceSn && !includes(item.deviceSn, params.deviceSn)) return false - if (params.deviceCategory && item.deviceCategory !== params.deviceCategory) return false - if (params.status && item.status !== params.status) return false - return true - }) - return ok('查询成功', filtered) -} - -const infoInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const target = initList.find((item) => item.id === params.id) - return ok('查询成功', target ?? null) -} - -/** 编辑回填版(剔除 certPayload / steps 等大字段) */ -const infoForEdit = (ctx: MockContext): ApiResponse | null> => { - const params = getParams(ctx.params) - const target = initList.find((item) => item.id === params.id) - if (!target) return ok('查询成功', null) - return ok('查询成功', { - id: target.id, - taskCode: target.taskCode, - deviceSn: target.deviceSn, - deviceName: target.deviceName, - deviceCategory: target.deviceCategory, - targetSite: target.targetSite, - baselineFw: target.baselineFw, - baselineApp: target.baselineApp, - activateCode: target.activateCode, - status: target.status, - outboundOrderNo: target.outboundOrderNo, - }) -} - -const genTaskCode = (): string => { - const ts = new Date() - const ymd = `${ts.getFullYear()}${String(ts.getMonth() + 1).padStart(2, '0')}${String(ts.getDate()).padStart(2, '0')}` - const seq = String(initList.length + 1).padStart(3, '0') - return `INIT-${ymd}-${seq}` -} - -const genActivateCode = (): string => { - const random = Math.random().toString(36).slice(2, 10).toUpperCase() - const ts = Date.now().toString(36).toUpperCase().slice(-6) - return `ACT-${ts}-${random}` -} - -const addInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = `init-${String(initList.length + 1).padStart(3, '0')}` - const now = new Date().toISOString().replace('T', ' ').slice(0, 19) - const record: InitRecord = { - id, - taskCode: genTaskCode(), - deviceSn: String(params.deviceSn ?? ''), - deviceName: String(params.deviceName ?? ''), - deviceCategory: params.deviceCategory as InitRecord['deviceCategory'], - targetSite: String(params.targetSite ?? ''), - baselineFw: String(params.baselineFw ?? 'V1.0.0'), - baselineApp: String(params.baselineApp ?? 'V1.0.0'), - activateCode: String(params.activateCode ?? genActivateCode()), - status: (params.status as InitRecord['status']) ?? 'pending', - operator: '当前用户', - initTime: null, - outboundOrderNo: params.outboundOrderNo ? String(params.outboundOrderNo) : undefined, - createTime: now, - certPayload: '—', - siteConfig: '{}', - strategy: '标准烧录策略:证书 → 场所配置 → 固件 → APP → 自检', - steps: [], +/** 详情构造器:在列表项基础上扩展类别 / 数据分发 / 软件版本详情 */ +const buildDetail = (item: InitItem): InitDetail => { + const versionMeta = VERSION_DATASET.find((v) => v.version === item.softwareVersion) + const base: InitDetail = { + ...item, + isIot: true, + networkMode: 'wifi', + manufacturer: '智养科技', + model: `${item.deviceCategory}-PRO`, + appDescription: versionMeta?.description ?? '', + packageUrl: versionMeta?.packageUrl ?? '', } - initList.unshift(record) - return ok('新增成功') + if (item.dataTransit) { + base.downstreamName = '营养管理中台' + base.downstreamCode = 'NUTRITION-HUB' + base.callbackUrl = 'https://api.mock.example.com/v1/device/data' + base.dispatchMode = 'realtime' + base.dispatchStatus = 'enabled' + } + if (item.dataReport) { + base.reportChannelName = '设备数据上报通道' + base.reportChannelCode = 'DEV-REPORT' + base.mqAddress = 'rabbitmq://mock.example.com:5672/dev-report' + base.gatewayUrl = 'https://gateway.mock.example.com/device/report' + base.reportStatus = 'enabled' + } + return base } -const updateInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const target = initList.find((item) => item.id === id) - if (!target) return ok('记录不存在') - if (params.deviceSn !== undefined) target.deviceSn = String(params.deviceSn) - if (params.deviceName !== undefined) target.deviceName = String(params.deviceName) - if (params.deviceCategory !== undefined) target.deviceCategory = params.deviceCategory as InitRecord['deviceCategory'] - if (params.targetSite !== undefined) target.targetSite = String(params.targetSite) - if (params.baselineFw !== undefined) target.baselineFw = String(params.baselineFw) - if (params.baselineApp !== undefined) target.baselineApp = String(params.baselineApp) - if (params.activateCode !== undefined) target.activateCode = String(params.activateCode) - if (params.outboundOrderNo !== undefined) target.outboundOrderNo = String(params.outboundOrderNo) - return ok('更新成功') +/** 列表查询(按筛选条件过滤) */ +const listInit = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + let list = INIT_DATASET.slice() + if (params.deviceCode) list = list.filter((i) => i.deviceCode.includes(params.deviceCode as string)) + if (params.deviceCategory) list = list.filter((i) => i.deviceCategory === params.deviceCategory) + if (params.initStatus) list = list.filter((i) => i.initStatus === params.initStatus) + if (params.deployStatus) list = list.filter((i) => i.deployStatus === params.deployStatus) + if (params.customerId) list = list.filter((i) => i.customerId === params.customerId) + if (params.siteId) list = list.filter((i) => i.siteId === params.siteId) + if (params.outboundOrderNo) list = list.filter((i) => i.outboundOrderNo.includes(params.outboundOrderNo as string)) + if (params.keyword) list = list.filter((i) => i.deviceCode.includes(params.keyword as string) || i.serialNumber.includes(params.keyword as string)) + return ok('查询成功', list) } -const deleteInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const idx = initList.findIndex((item) => item.id === id) - if (idx >= 0) initList.splice(idx, 1) - return ok('删除成功') +/** 详情查询 */ +const infoInit = (ctx: MockContext): ApiResponse => { + const params = getParams<{ id?: string }>(ctx.params) + const item = INIT_DATASET.find((i) => i.id === params.id) + if (!item) return ok('查询成功', null) + return ok('查询成功', buildDetail(item)) } -/** 重试:把失败步骤重新置为 pending,整条任务进入 processing */ -const retryInit = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const id = String(params.id ?? '') - const target = initList.find((item) => item.id === id) - if (!target) return ok('记录不存在') - target.status = 'processing' - target.failReason = undefined - target.steps = target.steps.map((s) => s.status === 'failed' ? { ...s, status: 'processing', finishTime: null, remark: '重试中…' } : s) - return ok('已触发重新初始化') +/** 单条 / 批量版本设置(共用逻辑) */ +const applyVersionSet = (ids: string[], versionId?: string): number => { + const version = VERSION_DATASET.find((v) => v.id === versionId) + if (!version) return -1 + const appName = APP_DATASET.find((a) => a.id === version.appId)?.name ?? '' + const now = new Date().toISOString().replace('T', ' ').substring(0, 19) + INIT_DATASET.forEach((item) => { + if (ids.includes(item.id)) { + item.packageName = appName + item.softwareVersion = version.version + item.initStatus = 'success' + item.initTime = now + } + }) + return ids.length +} + +/** 单条版本设置 */ +const setVersion = (ctx: MockContext): ApiResponse => { + const params = getParams<{ id?: string; ids?: string[]; versionId?: string }>(ctx.params) + const ids = params.ids ?? (params.id ? [params.id] : []) + const n = applyVersionSet(ids, params.versionId) + if (n < 0) return ok('版本不存在') + return ok('版本设置成功') +} + +/** 批量版本设置 */ +const batchSetVersion = (ctx: MockContext): ApiResponse => { + const params = getParams<{ ids?: string[]; versionId?: string }>(ctx.params) + const ids = params.ids ?? [] + const n = applyVersionSet(ids, params.versionId) + if (n < 0) return ok('版本不存在') + return ok(`已成功为 ${ids.length} 台设备设置版本`) +} + +/** 异步导出(仅创建任务) */ +const exportInit = (): ApiResponse => ok('导出任务已创建,请稍后到导出中心查看') + +/** 应用名称下拉 */ +const appOpts = (): ApiResponse => ok('ok', APP_DATASET) + +/** 版本号下拉(按应用联动) */ +const versionOpts = (ctx: MockContext): ApiResponse => { + const params = getParams<{ appId?: string }>(ctx.params) + return ok('ok', VERSION_DATASET.filter((v) => v.appId === params.appId)) +} + +/** 设备类别下拉 */ +const categoryOpts = (): ApiResponse => ok('ok', CATEGORY_DATASET) + +/** 客户下拉 */ +const customerOpts = (): ApiResponse => ok('ok', CUSTOMER_DATASET) + +/** 场所下拉(按客户联动) */ +const siteOpts = (ctx: MockContext): ApiResponse => { + const params = getParams<{ customerId?: string }>(ctx.params) + return ok('ok', SITE_DATASET.filter((s) => s.customerId === params.customerId)) } registerMocks([ - { method: 'POST', url: '/admin/device/ops/init/list', handler: listInit }, - { method: 'POST', url: '/admin/device/ops/init/info', handler: infoInit }, - { method: 'POST', url: '/admin/device/ops/init/info-for-edit', handler: infoForEdit }, - { method: 'POST', url: '/admin/device/ops/init/add', handler: addInit }, - { method: 'POST', url: '/admin/device/ops/init/update', handler: updateInit }, - { method: 'POST', url: '/admin/device/ops/init/delete', handler: deleteInit }, - { method: 'POST', url: '/admin/device/ops/init/retry', handler: retryInit }, + { method: 'POST', url: BASE + '/list', handler: listInit }, + { method: 'POST', url: BASE + '/info', handler: infoInit }, + { method: 'POST', url: BASE + '/set-version', handler: setVersion }, + { method: 'POST', url: BASE + '/batch-set-version', handler: batchSetVersion }, + { method: 'POST', url: BASE + '/export', handler: exportInit }, + { method: 'POST', url: BASE + '/app/options', handler: appOpts }, + { method: 'POST', url: BASE + '/version/options', handler: versionOpts }, + { method: 'POST', url: BASE + '/category/options', handler: categoryOpts }, + { method: 'POST', url: BASE + '/customer/options', handler: customerOpts }, + { method: 'POST', url: BASE + '/site/options', handler: siteOpts }, ]) diff --git a/src/axios/handlers/device-center/ops/monitor.ts b/src/axios/handlers/device-center/ops/monitor.ts index 2e7dcfc..8b52a8a 100644 --- a/src/axios/handlers/device-center/ops/monitor.ts +++ b/src/axios/handlers/device-center/ops/monitor.ts @@ -3,30 +3,35 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 设备运维 / 设备监测 mock - * - 主列表:设备运行状态总览 + * - 主列表:设备运行状态总览(软件版本 = 应用名 + 版本号) * - 详情 6 Tab:实施/自检/巡检/运行日志/更新记录/维修记录 + * - 远程下发:重启 */ interface MonitorRecord { id: string deviceCode: string deviceName: string - deviceCategory: 'catering' | 'body' | 'wearable' | 'fitness' | 'monitor' + /** 设备分类(末级分类名,中文展示,与设备初始化模块对齐) */ + deviceCategory: string tenantName: string siteName: string runStatus: 'online' | 'offline' | 'repairing' | 'scrapped' - firmwareVersion: string + /** 应用名称(与设备初始化模块对齐) */ + packageName: string + /** 软件版本号 */ + softwareVersion: string lastHeartbeat: string onlineRate: number alertCount: number - dataTransit: boolean onlineTime: string isIot: boolean implements: { id: string; implementType: 'newInstall' | 'migration' | 'removal' | 'reset'; implementer: string; implementTime: string; signedBy: string; remark: string }[] selfChecks: { id: string; checkType: 'powerOn' | 'scheduled' | 'remote'; checkItems: { name: string; category: 'hardware' | 'network' | 'sensor' | 'firmware'; result: 'pass' | 'fail'; detail?: string }[]; result: 'pass' | 'fail'; checkTime: string; triggerWorkOrder?: string }[] inspections: { id: string; planName: string; inspector: string; inspectTime: string; result: 'pass' | 'fail'; itemsCount: number; exceptionCount: number; handleType?: 'close' | 'workorder'; workOrderNo?: string }[] runLogs: { id: string; eventType: string; content: string; occurTime: string; level: 'info' | 'warn' | 'error' }[] - updates: { id: string; targetVersion: string; beforeVersion: string; pushTask: string; result: 'success' | 'fail' | 'rollback'; retryCount: number; finishTime: string }[] + /** 更新记录(每条含 packageName 应用名 + 目标/原版本号) */ + updates: { id: string; packageName: string; targetVersion: string; beforeVersion: string; pushTask: string; result: 'success' | 'fail' | 'rollback'; retryCount: number; finishTime: string }[] repairs: { id: string; workOrderNo: string; faultType: string; repairMethod: string; repairer: string; workHours: number; partsConsumed?: string; finishTime: string }[] } @@ -51,9 +56,10 @@ const includes = (value: unknown, keyword: unknown): boolean => { const monitorList: MonitorRecord[] = [ { - id: 'mon-001', deviceCode: 'CAB-A1-0001', deviceName: '总部食堂货柜A1', deviceCategory: 'catering', - tenantName: '长庆能源总部食堂', siteName: '长庆总部一食堂', runStatus: 'online', firmwareVersion: 'V1.2.0', - lastHeartbeat: '2026-07-02 14:50:12', onlineRate: 99, alertCount: 0, dataTransit: false, onlineTime: '2026-06-20 14:30:00', isIot: true, + id: 'mon-001', deviceCode: 'CAB-A1-0001', deviceName: '总部食堂货柜A1', deviceCategory: '智能货柜', + tenantName: '国庆能源总部食堂', siteName: '国庆总部一食堂', runStatus: 'online', + packageName: '智养货柜 App', softwareVersion: 'V1.2.0', + lastHeartbeat: '2026-07-02 14:50:12', onlineRate: 99, alertCount: 0, onlineTime: '2026-06-20 14:30:00', isIot: true, implements: [ { id: 'i1', implementType: 'newInstall', implementer: '李工程师', implementTime: '2026-06-20 14:00:00', signedBy: '王经理', remark: '首次装机,A1 货架第二层' }, ], @@ -69,16 +75,17 @@ const monitorList: MonitorRecord[] = [ { id: 'rl2', eventType: '开机', content: '设备开机', occurTime: '2026-07-02 06:00:00', level: 'info' }, ], updates: [ - { id: 'u1', targetVersion: 'V1.2.0', beforeVersion: 'V1.1.5', pushTask: 'OTA-20260625-003', result: 'success', retryCount: 0, finishTime: '2026-06-25 03:15:00' }, + { id: 'u1', packageName: '智养货柜 App', targetVersion: 'V1.2.0', beforeVersion: 'V1.1.5', pushTask: 'OTA-20260625-003', result: 'success', retryCount: 0, finishTime: '2026-06-25 03:15:00' }, ], repairs: [ { id: 'r1', workOrderNo: 'WO-20260620-001', faultType: '锁具故障', repairMethod: '更换锁芯', repairer: '李工程师', workHours: 1.5, partsConsumed: '锁芯×1', finishTime: '2026-06-20 17:30:00' }, ], }, { - id: 'mon-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', deviceCategory: 'wearable', - tenantName: '健康管理机构', siteName: '健康园区体检站', runStatus: 'repairing', firmwareVersion: 'V2.0.0', - lastHeartbeat: '2026-07-02 13:20:00', onlineRate: 78, alertCount: 2, dataTransit: true, onlineTime: '2026-06-22 09:30:00', isIot: true, + id: 'mon-002', deviceCode: 'WAT-W1-0001', deviceName: '健康站01手表', deviceCategory: '智能手表', + tenantName: '健康管理机构', siteName: '健康园区体检站', runStatus: 'repairing', + packageName: '智能手表 App', softwareVersion: 'V2.0.0', + lastHeartbeat: '2026-07-02 13:20:00', onlineRate: 78, alertCount: 2, onlineTime: '2026-06-22 09:30:00', isIot: true, implements: [ { id: 'i1', implementType: 'newInstall', implementer: '王工程师', implementTime: '2026-06-22 09:30:00', signedBy: '张医生', remark: '绑定用户 u-001' }, ], @@ -91,14 +98,15 @@ const monitorList: MonitorRecord[] = [ { id: 'rl2', eventType: '故障', content: '心率数据采集异常', occurTime: '2026-07-02 12:50:00', level: 'error' }, ], updates: [ - { id: 'u1', targetVersion: 'V2.0.0', beforeVersion: 'V1.9.0', pushTask: 'OTA-20260620-002', result: 'success', retryCount: 0, finishTime: '2026-06-20 22:00:00' }, + { id: 'u1', packageName: '智能手表 App', targetVersion: 'V2.0.0', beforeVersion: 'V1.9.0', pushTask: 'OTA-20260620-002', result: 'success', retryCount: 0, finishTime: '2026-06-20 22:00:00' }, ], repairs: [], }, { - id: 'mon-003', deviceCode: 'CHK-M1-0001', deviceName: '体检站一体机', deviceCategory: 'monitor', - tenantName: '健康管理机构', siteName: '东区体检站', runStatus: 'offline', firmwareVersion: 'V3.0.0', - lastHeartbeat: '2026-07-02 02:00:00', onlineRate: 45, alertCount: 3, dataTransit: true, onlineTime: '2026-06-24 10:30:00', isIot: true, + id: 'mon-003', deviceCode: 'CHK-M1-0001', deviceName: '体检站一体机', deviceCategory: '智能健康一体机', + tenantName: '健康管理机构', siteName: '东区体检站', runStatus: 'offline', + packageName: '体检一体机 App', softwareVersion: 'V3.0.0', + lastHeartbeat: '2026-07-02 02:00:00', onlineRate: 45, alertCount: 3, onlineTime: '2026-06-24 10:30:00', isIot: true, implements: [ { id: 'i1', implementType: 'newInstall', implementer: '赵工程师', implementTime: '2026-06-24 10:30:00', signedBy: '张医生', remark: '东区体检站主力机' }, ], @@ -111,9 +119,10 @@ const monitorList: MonitorRecord[] = [ repairs: [], }, { - id: 'mon-004', deviceCode: 'FAT-INBODY-0001', deviceName: '体检站体脂秤', deviceCategory: 'body', - tenantName: '健康管理机构', siteName: '东区体检站', runStatus: 'online', firmwareVersion: 'V1.5.0', - lastHeartbeat: '2026-07-02 14:48:00', onlineRate: 96, alertCount: 0, dataTransit: true, onlineTime: '2026-06-25 16:00:00', isIot: true, + id: 'mon-004', deviceCode: 'FAT-INBODY-0001', deviceName: '体检站体脂秤', deviceCategory: '体脂仪', + tenantName: '健康管理机构', siteName: '东区体检站', runStatus: 'online', + packageName: '智养秤终端 App', softwareVersion: 'V1.5.0', + lastHeartbeat: '2026-07-02 14:48:00', onlineRate: 96, alertCount: 0, onlineTime: '2026-06-25 16:00:00', isIot: true, implements: [{ id: 'i1', implementType: 'newInstall', implementer: '孙工程师', implementTime: '2026-06-25 16:00:00', signedBy: '张医生', remark: '—' }], selfChecks: [{ id: 'sc1', checkType: 'powerOn', checkItems: [{ name: '网络', category: 'network', result: 'pass' }], result: 'pass', checkTime: '2026-07-02 08:00:00' }], inspections: [{ id: 'ins1', planName: '体检站周巡检', inspector: '王工程师', inspectTime: '2026-06-29 14:00:00', result: 'pass', itemsCount: 8, exceptionCount: 0, handleType: 'close' }], @@ -122,9 +131,10 @@ const monitorList: MonitorRecord[] = [ repairs: [], }, { - id: 'mon-005', deviceCode: 'SCL-W-0001', deviceName: '健康站体重秤', deviceCategory: 'body', - tenantName: '健康管理机构', siteName: '健康园区体检站', runStatus: 'online', firmwareVersion: 'V1.0.3', - lastHeartbeat: '2026-07-02 14:45:00', onlineRate: 92, alertCount: 1, dataTransit: true, onlineTime: '2026-06-26 09:00:00', isIot: true, + id: 'mon-005', deviceCode: 'SCL-W-0001', deviceName: '健康站体重秤', deviceCategory: '蓝牙秤', + tenantName: '健康管理机构', siteName: '健康园区体检站', runStatus: 'online', + packageName: '智养秤终端 App', softwareVersion: 'V1.0.3', + lastHeartbeat: '2026-07-02 14:45:00', onlineRate: 92, alertCount: 1, onlineTime: '2026-06-26 09:00:00', isIot: true, implements: [{ id: 'i1', implementType: 'newInstall', implementer: '李工程师', implementTime: '2026-06-26 09:00:00', signedBy: '张医生', remark: '—' }], selfChecks: [], inspections: [], @@ -133,9 +143,10 @@ const monitorList: MonitorRecord[] = [ repairs: [{ id: 'r1', workOrderNo: 'WO-20260629-008', faultType: '蓝牙异常', repairMethod: '更换蓝牙模块', repairer: '王工程师', workHours: 1.0, partsConsumed: '蓝牙模块×1', finishTime: '2026-06-30 16:00:00' }], }, { - id: 'mon-006', deviceCode: 'FIT-S1-0001', deviceName: '健身房顿米车', deviceCategory: 'fitness', - tenantName: '智养餐饮集团', siteName: '园区员工健身房', runStatus: 'repairing', firmwareVersion: 'V2.1.0', - lastHeartbeat: '2026-07-02 12:00:00', onlineRate: 65, alertCount: 1, dataTransit: true, onlineTime: '2026-06-27 11:00:00', isIot: true, + id: 'mon-006', deviceCode: 'FIT-S1-0001', deviceName: '健身房顿米车', deviceCategory: '健身车', + tenantName: '智养餐饮集团', siteName: '园区员工健身房', runStatus: 'repairing', + packageName: '智养健身 App', softwareVersion: 'V2.1.0', + lastHeartbeat: '2026-07-02 12:00:00', onlineRate: 65, alertCount: 1, onlineTime: '2026-06-27 11:00:00', isIot: true, implements: [{ id: 'i1', implementType: 'newInstall', implementer: '李工程师', implementTime: '2026-06-27 11:00:00', signedBy: '吴教练', remark: '—' }], selfChecks: [], inspections: [], @@ -144,9 +155,10 @@ const monitorList: MonitorRecord[] = [ repairs: [], }, { - id: 'mon-007', deviceCode: 'SCR-P1-0001', deviceName: '总部大屏', deviceCategory: 'catering', - tenantName: '智养餐饮集团', siteName: '长庆总部一食堂', runStatus: 'online', firmwareVersion: 'V2.5.0', - lastHeartbeat: '2026-07-02 14:50:00', onlineRate: 99, alertCount: 0, dataTransit: false, onlineTime: '2026-05-01 10:00:00', isIot: false, + id: 'mon-007', deviceCode: 'SCR-P1-0001', deviceName: '总部大屏', deviceCategory: '带鱼屏', + tenantName: '智养餐饮集团', siteName: '国庆总部一食堂', runStatus: 'online', + packageName: '智养大屏 App', softwareVersion: 'V2.5.0', + lastHeartbeat: '2026-07-02 14:50:00', onlineRate: 99, alertCount: 0, onlineTime: '2026-05-01 10:00:00', isIot: false, implements: [{ id: 'i1', implementType: 'newInstall', implementer: '赵工程师', implementTime: '2026-05-01 10:00:00', signedBy: '王经理', remark: '主餐厅入口' }], selfChecks: [], inspections: [{ id: 'ins1', planName: '食堂周巡检', inspector: '赵工程师', inspectTime: '2026-06-28 10:30:00', result: 'pass', itemsCount: 6, exceptionCount: 0, handleType: 'close' }], @@ -155,14 +167,15 @@ const monitorList: MonitorRecord[] = [ repairs: [], }, { - id: 'mon-008', deviceCode: 'SCL-X1-0001', deviceName: '总部入库秤', deviceCategory: 'catering', - tenantName: '智养餐饮集团', siteName: '长庆总部一食堂', runStatus: 'online', firmwareVersion: 'V1.3.0', - lastHeartbeat: '2026-07-02 14:49:30', onlineRate: 98, alertCount: 0, dataTransit: false, onlineTime: '2026-04-15 09:00:00', isIot: true, + id: 'mon-008', deviceCode: 'SCL-X1-0001', deviceName: '总部入库秤', deviceCategory: '入库秤', + tenantName: '智养餐饮集团', siteName: '国庆总部一食堂', runStatus: 'online', + packageName: '智养秤终端 App', softwareVersion: 'V1.3.0', + lastHeartbeat: '2026-07-02 14:49:30', onlineRate: 98, alertCount: 0, onlineTime: '2026-04-15 09:00:00', isIot: true, implements: [{ id: 'i1', implementType: 'newInstall', implementer: '李工程师', implementTime: '2026-04-15 09:00:00', signedBy: '王经理', remark: '后厨入库区' }], selfChecks: [{ id: 'sc1', checkType: 'scheduled', checkItems: [{ name: '称重校准', category: 'sensor', result: 'pass' }], result: 'pass', checkTime: '2026-07-02 06:00:00' }], inspections: [], runLogs: [], - updates: [{ id: 'u1', targetVersion: 'V1.3.0', beforeVersion: 'V1.2.0', pushTask: 'OTA-20260620-001', result: 'success', retryCount: 0, finishTime: '2026-06-20 22:00:00' }], + updates: [{ id: 'u1', packageName: '智养秤终端 App', targetVersion: 'V1.3.0', beforeVersion: 'V1.2.0', pushTask: 'OTA-20260620-001', result: 'success', retryCount: 0, finishTime: '2026-06-20 22:00:00' }], repairs: [{ id: 'r1', workOrderNo: 'WO-20260625-004', faultType: '校准异常', repairMethod: '重新校准', repairer: '赵工程师', workHours: 0.5, finishTime: '2026-06-26 10:00:00' }], }, ] @@ -209,20 +222,34 @@ const remoteMonitor = (ctx: MockContext): ApiResponse => { if (!target) return ok('设备不存在') /** 在运行日志追加一条记录 */ const now = new Date().toISOString().replace('T', ' ').slice(0, 19) - const actionText = params.type === 'self-check' ? '远程自检' : params.type === 'restart' ? '远程重启' : 'OTA 升级' target.runLogs.unshift({ id: `rl-${Date.now()}`, - eventType: actionText, - content: `${actionText}指令已下发`, + eventType: '远程重启', + content: '远程重启指令已下发', occurTime: now, level: 'info', }) - return ok(`${actionText}指令已下发`) + return ok('远程重启指令已下发') } +/** 设备分类下拉(末级分类名,与设备初始化模块对齐) */ +const CATEGORY_OPTIONS: { name: string }[] = [ + { name: '智能货柜' }, + { name: '智能手表' }, + { name: '智能健康一体机' }, + { name: '体脂仪' }, + { name: '蓝牙秤' }, + { name: '健身车' }, + { name: '带鱼屏' }, + { name: '入库秤' }, +] + +const categoryOpts = (): ApiResponse<{ name: string }[]> => ok('查询成功', CATEGORY_OPTIONS) + registerMocks([ { method: 'POST', url: '/admin/device/ops/monitor/list', handler: listMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/stats', handler: statsMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/info', handler: infoMonitor }, { method: 'POST', url: '/admin/device/ops/monitor/remote', handler: remoteMonitor }, + { method: 'POST', url: '/admin/device/ops/monitor/category/options', handler: categoryOpts }, ]) diff --git a/src/axios/handlers/device-center/ops/version.ts b/src/axios/handlers/device-center/ops/version.ts index 2b10dc1..d9ac302 100644 --- a/src/axios/handlers/device-center/ops/version.ts +++ b/src/axios/handlers/device-center/ops/version.ts @@ -3,106 +3,235 @@ import type { ApiResponse, MockContext, RequestParameter } from '@axios' /** * 设备运维 / 版本管理 mock - * - 固件 + APP 双轨 - * - 状态流转:草稿 → 灰度 → 全量 → 停用 + * - 两级结构:应用(App)→ 版本(Version) + * - 应用列表:从版本数据汇总「最新版本 / 历史版本数 / 最后更新日期」 + * - 版本列表:按 appId 过滤历史版本 + * - 应用状态:启用 / 停用;版本状态:有效 / 停用(均支持 toggle) + * - 研发主体为公司名称(字符串),不再使用枚举 + * - 版本记录含软件包字段:packageName / packageSize */ +type AppStatus = 'enabled' | 'disabled' +type VersionStatus = 'active' | 'disabled' + interface VersionRecord { id: string + appId: string + /** 版本号,如 V1.2.0 */ version: string - track: 'firmware' | 'app' - packageUrl: string - upgradePoints: string - grayPercent: number - rollbackFlag: boolean - publishTime: string | null - status: 'draft' | 'gray' | 'full' | 'deprecated' - upgradedCount: number - creator: string + /** 版本名称,如 首版 / 灰度版 / 稳定版 */ + versionName: string + /** 更新说明 */ + releaseNote: string + /** 是否强制更新 */ + forceUpdate: boolean + status: VersionStatus + /** 再用设备数(mock 静态) */ + usingDeviceCount: number + /** 适配类目:设备分类 code 数组 */ + deviceCategories: string[] + /** 软件包文件名 */ + packageName: string + /** 软件包大小(字节) */ + packageSize: number + updateTime: string createTime: string + creator: string } -interface VersionParams { +interface AppRecord { + id: string + /** 应用名称 */ + name: string + /** 研发主体(公司名称) */ + devSubject: string + /** 应用描述 */ + description: string + status: AppStatus +} + +interface CommonParams { keyword?: string - track?: string - status?: string id?: string + appId?: string toStatus?: string [key: string]: unknown } const ok = (msg: string, data: T = null as T): ApiResponse => ({ code: '00000', msg, data }) -const getParams = (params: RequestParameter): VersionParams => (params ?? {}) as VersionParams +const getParams = (params: RequestParameter): CommonParams => (params ?? {}) as CommonParams const includes = (value: unknown, keyword: unknown): boolean => { if (!keyword) return true return String(value ?? '').toLowerCase().includes(String(keyword).toLowerCase()) } +const now = (): string => new Date().toISOString().replace('T', ' ').slice(0, 19) + +/** 设备分类 code → 名称(与主页/二级页 types 中的 DEVICE_CATEGORY_OPTIONS 保持一致) */ +const deviceCategoryText: Record = { + cabinet: '净菜柜', + watch: '智能手表', + healthCheck: '体检一体机', + bodyFat: '体脂仪', + scale: '健康量表', +} + +const appList: AppRecord[] = [ + { id: 'app-cabinet-fw', name: '净菜柜固件', devSubject: '寰汐科技(上海)有限公司', description: '净菜柜主控板固件,负责开门控制、温控、 OTA 升级', status: 'enabled' }, + { id: 'app-cabinet-app', name: '净菜柜客户端 APP', devSubject: '寰汐科技(上海)有限公司', description: '净菜柜前端交互应用,提供扫码开门、菜品展示、订单查询', status: 'enabled' }, + { id: 'app-watch-fw', name: '智能手表固件', devSubject: '深圳瑞康智能科技有限公司', description: '智能健康手表底层固件,由供应商提供底包 + 自研算法层', status: 'enabled' }, + { id: 'app-healthcheck-fw', name: '体检一体机固件', devSubject: '寰汐科技(上海)有限公司', description: '体检一体机主控固件,集成血压 / 血尿酸 / 体脂多项检测', status: 'disabled' }, + { id: 'app-bodyfat-fw', name: '体脂仪固件', devSubject: '江苏康体电子科技有限公司', description: '体脂仪底层固件,第三方厂商定制开发', status: 'enabled' }, + { id: 'app-scale-app', name: '健康量表 APP', devSubject: '寰汐科技(上海)有限公司', description: '客户终端健康量表自评 APP(暂未发布任何版本)', status: 'enabled' }, +] const versionList: VersionRecord[] = [ { - id: 'ver-001', version: 'V1.2.0', track: 'firmware', - packageUrl: 'https://ota.yx-canteen.com/fw/cabinet/v1.2.0.bin', - upgradePoints: '优化货柜开门响应速度\n修复温度传感器偶发漂移\n新增离线缓存支持', - grayPercent: 100, rollbackFlag: true, publishTime: '2026-06-25 03:00:00', - status: 'full', upgradedCount: 12, creator: '李工程师', createTime: '2026-06-22 15:00:00', + id: 'ver-001', appId: 'app-cabinet-fw', version: 'V1.2.0', versionName: '稳定版', + releaseNote: '优化货柜开门响应速度\n修复温度传感器偶发漂移\n新增离线缓存支持', + forceUpdate: false, status: 'active', usingDeviceCount: 12, + deviceCategories: ['cabinet'], + packageName: 'cabinet-fw-V1.2.0.bin', packageSize: 3 * 1024 * 1024 + 240 * 1024, + updateTime: '2026-06-25 03:00:00', createTime: '2026-06-22 15:00:00', creator: '李工程师', }, { - id: 'ver-002', version: 'V1.1.5', track: 'firmware', - packageUrl: 'https://ota.yx-canteen.com/fw/cabinet/v1.1.5.bin', - upgradePoints: '修复开门指令偶发丢失问题', - grayPercent: 0, rollbackFlag: false, publishTime: '2026-05-10 10:00:00', - status: 'deprecated', upgradedCount: 0, creator: '李工程师', createTime: '2026-05-08 14:00:00', + id: 'ver-002', appId: 'app-cabinet-fw', version: 'V1.1.5', versionName: '稳定版', + releaseNote: '修复开门指令偶发丢失问题', + forceUpdate: true, status: 'disabled', usingDeviceCount: 0, + deviceCategories: ['cabinet'], + packageName: 'cabinet-fw-V1.1.5.bin', packageSize: 2 * 1024 * 1024 + 768 * 1024, + updateTime: '2026-05-10 10:00:00', createTime: '2026-05-08 14:00:00', creator: '李工程师', }, { - id: 'ver-003', version: 'V2.0.1', track: 'firmware', - packageUrl: 'https://ota.yx-canteen.com/fw/watch/v2.0.1.bin', - upgradePoints: '心率监测算法升级\n新增睡眠分析\n优化电池续航', - grayPercent: 30, rollbackFlag: true, publishTime: '2026-06-28 22:00:00', - status: 'gray', upgradedCount: 8, creator: '王工程师', createTime: '2026-06-26 09:30:00', + id: 'ver-003', appId: 'app-cabinet-fw', version: 'V1.1.0', versionName: '稳定版', + releaseNote: '首版支持远程开门\n新增温度上报', + forceUpdate: false, status: 'active', usingDeviceCount: 3, + deviceCategories: ['cabinet'], + packageName: 'cabinet-fw-V1.1.0.bin', packageSize: 2 * 1024 * 1024 + 100 * 1024, + updateTime: '2026-04-20 16:00:00', createTime: '2026-04-18 09:00:00', creator: '李工程师', }, { - id: 'ver-004', version: 'V1.0.0', track: 'app', - packageUrl: 'https://ota.yx-canteen.com/app/cabinet/v1.0.0.apk', - upgradePoints: '货柜客户端首版\n支持扫码开门、菜品展示、订单查询', - grayPercent: 100, rollbackFlag: true, publishTime: '2026-04-15 09:00:00', - status: 'full', upgradedCount: 12, creator: '张产品', createTime: '2026-04-10 14:00:00', + id: 'ver-004', appId: 'app-cabinet-app', version: 'V1.1.0', versionName: '灰度版', + releaseNote: '新增会员登录\n优化 UI 加载速度\n修复部分小屏适配问题', + forceUpdate: false, status: 'active', usingDeviceCount: 6, + deviceCategories: ['cabinet'], + packageName: 'cabinet-app-V1.1.0.apk', packageSize: 18 * 1024 * 1024 + 500 * 1024, + updateTime: '2026-06-15 14:00:00', createTime: '2026-06-12 10:00:00', creator: '张产品', }, { - id: 'ver-005', version: 'V1.1.0', track: 'app', - packageUrl: 'https://ota.yx-canteen.com/app/cabinet/v1.1.0.apk', - upgradePoints: '新增会员登录\n优化 UI 加载速度\n修复部分小屏适配问题', - grayPercent: 50, rollbackFlag: true, publishTime: '2026-06-15 14:00:00', - status: 'gray', upgradedCount: 6, creator: '张产品', createTime: '2026-06-12 10:00:00', + id: 'ver-005', appId: 'app-cabinet-app', version: 'V1.0.0', versionName: '首版', + releaseNote: '货柜客户端首版\n支持扫码开门、菜品展示、订单查询', + forceUpdate: true, status: 'disabled', usingDeviceCount: 0, + deviceCategories: ['cabinet'], + packageName: 'cabinet-app-V1.0.0.apk', packageSize: 16 * 1024 * 1024 + 200 * 1024, + updateTime: '2026-04-15 09:00:00', createTime: '2026-04-10 14:00:00', creator: '张产品', }, { - id: 'ver-006', version: 'V1.2.0-beta', track: 'app', - packageUrl: 'https://ota.yx-canteen.com/app/cabinet/v1.2.0-beta.apk', - upgradePoints: '新增健康菜品推荐(灰度验证)', - grayPercent: 0, rollbackFlag: true, publishTime: null, - status: 'draft', upgradedCount: 0, creator: '张产品', createTime: '2026-07-01 16:00:00', + id: 'ver-006', appId: 'app-watch-fw', version: 'V2.0.1', versionName: '灰度版', + releaseNote: '心率监测算法升级\n新增睡眠分析\n优化电池续航', + forceUpdate: true, status: 'active', usingDeviceCount: 8, + deviceCategories: ['watch'], + packageName: 'watch-fw-V2.0.1.bin', packageSize: 5 * 1024 * 1024 + 120 * 1024, + updateTime: '2026-06-28 22:00:00', createTime: '2026-06-26 09:30:00', creator: '王工程师', }, { - id: 'ver-007', version: 'V3.0.0', track: 'firmware', - packageUrl: 'https://ota.yx-canteen.com/fw/healthcheck/v3.0.0.bin', - upgradePoints: '体检一体机 V3 主版本\n新增血尿酸检测\n优化血压采样算法', - grayPercent: 100, rollbackFlag: false, publishTime: '2026-05-20 10:00:00', - status: 'full', upgradedCount: 4, creator: '赵工程师', createTime: '2026-05-15 14:00:00', + id: 'ver-007', appId: 'app-healthcheck-fw', version: 'V3.0.0', versionName: '稳定版', + releaseNote: '体检一体机 V3 主版本\n新增血尿酸检测\n优化血压采样算法', + forceUpdate: false, status: 'disabled', usingDeviceCount: 0, + deviceCategories: ['healthCheck'], + packageName: 'healthcheck-fw-V3.0.0.img', packageSize: 32 * 1024 * 1024 + 768 * 1024, + updateTime: '2026-05-20 10:00:00', createTime: '2026-05-15 14:00:00', creator: '赵工程师', }, { - id: 'ver-008', version: 'V1.5.0', track: 'firmware', - packageUrl: 'https://ota.yx-canteen.com/fw/bodyfat/v1.5.0.bin', - upgradePoints: '体脂算法同步 InBody 最新公式\n阻抗采样精度提升', - grayPercent: 100, rollbackFlag: true, publishTime: '2026-06-25 17:00:00', - status: 'full', upgradedCount: 2, creator: '孙工程师', createTime: '2026-06-22 11:00:00', + id: 'ver-008', appId: 'app-bodyfat-fw', version: 'V1.5.0', versionName: '稳定版', + releaseNote: '体脂算法同步 InBody 最新公式\n阻抗采样精度提升', + forceUpdate: false, status: 'active', usingDeviceCount: 2, + deviceCategories: ['bodyFat'], + packageName: 'bodyfat-fw-V1.5.0.hex', packageSize: 1 * 1024 * 1024 + 512 * 1024, + updateTime: '2026-06-25 17:00:00', createTime: '2026-06-22 11:00:00', creator: '孙工程师', }, ] +/** 从版本记录汇总应用展示字段 */ +const buildAppSummary = (app: AppRecord) => { + const versions = versionList + .filter((v) => v.appId === app.id) + .sort((a, b) => (a.updateTime > b.updateTime ? -1 : 1)) + const latestVersion = versions[0]?.version ?? '' + const versionCount = versions.length + const updateTime = versions[0]?.updateTime ?? '' + return { + id: app.id, + name: app.name, + devSubject: app.devSubject, + description: app.description, + status: app.status, + latestVersion, + versionCount, + updateTime, + } +} + +// ============ 应用相关 mock ============ + +const listApp = (ctx: MockContext): ApiResponse[]> => { + const params = getParams(ctx.params) + const filtered = appList.filter((item) => { + if (params.keyword) { + const kw = params.keyword + if (!includes(item.name, kw) && !includes(item.description, kw)) return false + } + return true + }) + return ok('查询成功', filtered.map(buildAppSummary)) +} + +const infoAppForEdit = (ctx: MockContext): ApiResponse | null> => { + const params = getParams(ctx.params) + const target = appList.find((i) => i.id === params.id) + if (!target) return ok('查询成功', null) + return ok('查询成功', { + id: target.id, name: target.name, devSubject: target.devSubject, + description: target.description, status: target.status, + }) +} + +const addApp = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const id = `app-${String(appList.length + 1).padStart(3, '0')}` + appList.unshift({ + id, + name: String(params.name ?? ''), + devSubject: String(params.devSubject ?? ''), + description: String(params.description ?? ''), + status: (params.status as AppStatus) ?? 'enabled', + }) + return ok('新增成功') +} + +const updateApp = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const target = appList.find((i) => i.id === String(params.id ?? '')) + if (!target) return ok('记录不存在') + if (params.name !== undefined) target.name = String(params.name) + if (params.devSubject !== undefined) target.devSubject = String(params.devSubject) + if (params.description !== undefined) target.description = String(params.description) + return ok('更新成功') +} + +const toggleApp = (ctx: MockContext): ApiResponse => { + const params = getParams(ctx.params) + const target = appList.find((i) => i.id === String(params.id ?? '')) + if (!target) return ok('记录不存在') + target.status = target.status === 'enabled' ? 'disabled' : 'enabled' + return ok('状态已更新') +} + +// ============ 版本相关 mock ============ + const listVersion = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const filtered = versionList.filter((item) => { - if (params.keyword && !includes(item.version, params.keyword) && !includes(item.upgradePoints, params.keyword)) return false - if (params.track && item.track !== params.track) return false - if (params.status && item.status !== params.status) return false + if (params.appId && item.appId !== params.appId) return false return true }) return ok('查询成功', filtered) @@ -113,79 +242,48 @@ const infoVersion = (ctx: MockContext): ApiResponse => { return ok('查询成功', versionList.find((i) => i.id === params.id) ?? null) } -/** 编辑回填版 */ -const infoForEdit = (ctx: MockContext): ApiResponse | null> => { - const params = getParams(ctx.params) - const target = versionList.find((i) => i.id === params.id) - if (!target) return ok('查询成功', null) - return ok('查询成功', { - id: target.id, version: target.version, track: target.track, - packageUrl: target.packageUrl, upgradePoints: target.upgradePoints, - grayPercent: target.grayPercent, rollbackFlag: target.rollbackFlag, status: target.status, - }) -} - const addVersion = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const id = `ver-${String(versionList.length + 1).padStart(3, '0')}` - const now = new Date().toISOString().replace('T', ' ').slice(0, 19) - const record: VersionRecord = { + const ts = now() + versionList.unshift({ id, + appId: String(params.appId ?? ''), version: String(params.version ?? ''), - track: params.track as VersionRecord['track'], - packageUrl: String(params.packageUrl ?? ''), - upgradePoints: String(params.upgradePoints ?? ''), - grayPercent: Number(params.grayPercent ?? 0), - rollbackFlag: Boolean(params.rollbackFlag), - publishTime: null, - status: (params.status as VersionRecord['status']) ?? 'draft', - upgradedCount: 0, + versionName: String(params.versionName ?? ''), + releaseNote: String(params.releaseNote ?? ''), + forceUpdate: Boolean(params.forceUpdate), + status: (params.status as VersionStatus) ?? 'active', + usingDeviceCount: 0, + deviceCategories: Array.isArray(params.deviceCategories) ? (params.deviceCategories as string[]) : [], + packageName: String(params.packageName ?? ''), + packageSize: Number(params.packageSize) || 0, + updateTime: ts, + createTime: ts, creator: '当前用户', - createTime: now, - } - versionList.unshift(record) + }) return ok('新增成功') } -const updateVersion = (ctx: MockContext): ApiResponse => { +const toggleVersion = (ctx: MockContext): ApiResponse => { const params = getParams(ctx.params) const target = versionList.find((i) => i.id === String(params.id ?? '')) if (!target) return ok('记录不存在') - if (params.version !== undefined) target.version = String(params.version) - if (params.track !== undefined) target.track = params.track as VersionRecord['track'] - if (params.packageUrl !== undefined) target.packageUrl = String(params.packageUrl) - if (params.upgradePoints !== undefined) target.upgradePoints = String(params.upgradePoints) - if (params.grayPercent !== undefined) target.grayPercent = Number(params.grayPercent) - if (params.rollbackFlag !== undefined) target.rollbackFlag = Boolean(params.rollbackFlag) - return ok('更新成功') -} - -const deleteVersion = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const idx = versionList.findIndex((i) => i.id === String(params.id ?? '')) - if (idx >= 0) versionList.splice(idx, 1) - return ok('删除成功') -} - -const transitionVersion = (ctx: MockContext): ApiResponse => { - const params = getParams(ctx.params) - const target = versionList.find((i) => i.id === String(params.id ?? '')) - if (!target) return ok('记录不存在') - const to = params.toStatus as VersionRecord['status'] - if (!to) return ok('目标状态不能为空') - target.status = to - if (to === 'gray' || to === 'full') { - target.publishTime = new Date().toISOString().replace('T', ' ').slice(0, 19) - } + target.status = target.status === 'active' ? 'disabled' : 'active' + target.updateTime = now() return ok('状态已更新') } registerMocks([ + // 应用 + { method: 'POST', url: '/admin/device/ops/version/app/list', handler: listApp }, + { method: 'POST', url: '/admin/device/ops/version/app/info-for-edit', handler: infoAppForEdit }, + { method: 'POST', url: '/admin/device/ops/version/app/add', handler: addApp }, + { method: 'POST', url: '/admin/device/ops/version/app/update', handler: updateApp }, + { method: 'POST', url: '/admin/device/ops/version/app/toggle', handler: toggleApp }, + // 版本 { method: 'POST', url: '/admin/device/ops/version/list', handler: listVersion }, { method: 'POST', url: '/admin/device/ops/version/info', handler: infoVersion }, - { method: 'POST', url: '/admin/device/ops/version/info-for-edit', handler: infoForEdit }, { method: 'POST', url: '/admin/device/ops/version/add', handler: addVersion }, - { method: 'POST', url: '/admin/device/ops/version/update', handler: updateVersion }, - { method: 'POST', url: '/admin/device/ops/version/delete', handler: deleteVersion }, - { method: 'POST', url: '/admin/device/ops/version/transition', handler: transitionVersion }, + { method: 'POST', url: '/admin/device/ops/version/toggle', handler: toggleVersion }, ]) diff --git a/src/axios/handlers/device-center/third-auth/amap.ts b/src/axios/handlers/device-center/third-auth/amap.ts index 99388a5..4d3b6bd 100644 --- a/src/axios/handlers/device-center/third-auth/amap.ts +++ b/src/axios/handlers/device-center/third-auth/amap.ts @@ -56,16 +56,40 @@ 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', 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' }, + { id: 'amap-001', name: '华为watch-5', 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: '华为watch-4', apiKey: 'b2c3d4e5f6a7b8c9d0e1f2a3b4c5d6e7', securityCode: 'yx-sec-2026-002', authenticatedCompany: '数字味道', usage: '货柜/体检设备 GPS 定位回传', status: true, createTime: '2026-03-12 14:22:45' }, + { id: 'amap-003', name: '终端N合一APP', 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' }, ] +/** + * 应用软件下拉选项 + * - 业务模型:先有高德 Key/秘钥,再选择性地配置给某个应用软件使用 + * - 此处为平台已注册的可选应用软件清单(独立于 amapList 维护) + */ +interface AppOption { + label: string + value: string +} + +const appOptionList: AppOption[] = [ + { label: '华为watch-5', value: '华为watch-5' }, + { label: '华为watch-4', value: '华为watch-4' }, + { label: '终端N合一APP', value: '终端N合一APP' }, + { label: '小程序地图组件', value: '小程序地图组件' }, + { label: '食堂数据大屏地图', value: '食堂数据大屏地图' }, + { label: '巡检路径追踪服务', value: '巡检路径追踪服务' }, + { label: '行政区域查询服务', value: '行政区域查询服务' }, + { label: '健康园区打卡围栏', value: '健康园区打卡围栏' }, +] + +/** 应用软件下拉选项查询 */ +const listAmapAppOptions = (): ApiResponse => ok('查询成功', appOptionList) + /** 高德 ApiKey 模糊匹配 */ const matchApiKey = (item: AmapRecord, apiKey?: string): boolean => includes(item.apiKey, apiKey) @@ -145,4 +169,5 @@ registerMocks([ { method: 'POST', url: '/admin/device/third-auth/amap/add', handler: addAmap }, { method: 'POST', url: '/admin/device/third-auth/amap/update', handler: updateAmap }, { method: 'POST', url: '/admin/device/third-auth/amap/delete', handler: deleteAmap }, + { method: 'POST', url: '/admin/device/third-auth/amap/app-options', handler: listAmapAppOptions }, ]) diff --git a/src/axios/handlers/index.ts b/src/axios/handlers/index.ts index 8ff74f0..e34911f 100644 --- a/src/axios/handlers/index.ts +++ b/src/axios/handlers/index.ts @@ -60,18 +60,16 @@ import './device-center/ops/monitor' import './device-center/ops/version' // 设备中心 - 设备运维 / 告警管理(告警处置 + 业务日志双 Tab) import './device-center/ops/alarm' -// 设备中心 - 设备运维 / 系统配置(5 分组 + 变更历史) -import './device-center/ops/config' // 设备中心 - 客户管理 / 客户档案(CRUD + 详情抽屉) import './device-center/customer/archive' // 设备中心 - 客户管理 / 布点场所(CRUD,含心跳状态展示) import './device-center/customer/site' -// 设备中心 - 数据分发 / 分发配置(下游系统 + 订阅映射,双 Tab CRUD) +// 设备中心 - 数据分发 / 分发配置(单列表 CRUD + 鉴权联动 + 设备分类树多选) import './device-center/distribute/config' -// 设备中心 - 数据分发 / 分发记录(含重投、批量重投、导出) +// 设备中心 - 数据分发 / 上报配置(单列表 CRUD + 客户→场所联动 + 鉴权联动 + 设备分类树多选) +import './device-center/distribute/report' +// 设备中心 - 数据分发 / 分发记录(分发配置二级子页面,含 downstreamId 过滤、重投、导出) import './device-center/distribute/record' -// 设备中心 - 数据分发 / 失败重投(死信队列,含手动重投、永久丢弃) -import './device-center/distribute/deadletter' // 设备中心 - 第三方授权 / 虹软人脸授权(按设备绑定激活码) import './device-center/third-auth/arcsoft' // 设备中心 - 第三方授权 / 高德地图应用(API Key + 安全密钥) diff --git a/src/components/FilterBar.vue b/src/components/FilterBar.vue index 777ff99..1d62435 100644 --- a/src/components/FilterBar.vue +++ b/src/components/FilterBar.vue @@ -178,17 +178,17 @@ const onResetClick = (): void => { } &__form { - .ant-form-item { - margin-bottom: 12px; + :deep(.ant-form-item) { + margin-bottom: 16px; } } &__item { - margin-bottom: 12px; + margin-bottom: 16px; } &__actions { - margin-bottom: 12px; + margin-bottom: 16px; } } diff --git a/src/layouts/components/MenuTreeNode.vue b/src/layouts/components/MenuTreeNode.vue index 4b368fe..f0670fe 100644 --- a/src/layouts/components/MenuTreeNode.vue +++ b/src/layouts/components/MenuTreeNode.vue @@ -6,7 +6,7 @@ {{ item.label }} { return icon ?? 'FileOutlined' }) +/** NavGroup 的可见子节点(过滤 hidden leaf) */ +const visibleChildren = computed(() => { + if (!isGroup.value) return [] + return (props.item as NavGroup).children.filter((child) => { + const leaf = child as NavLeaf + return !('hidden' in leaf) || !leaf.hidden + }) +}) + const onLeafClick = (): void => { if (!isGroup.value) { emit('navigate', props.item as NavLeaf) diff --git a/src/layouts/components/Sidebar.vue b/src/layouts/components/Sidebar.vue index 49742af..93ffd04 100644 --- a/src/layouts/components/Sidebar.vue +++ b/src/layouts/components/Sidebar.vue @@ -10,7 +10,7 @@ @openChange="onOpenChange" > () +/** 侧边栏仅渲染未隐藏的菜单(hidden 的 leaf 仍参与路由,但不显示) */ +const visibleChildren = computed(() => { + if (!props.section) return [] + return props.section.children.filter((item) => { + const leaf = item as NavLeaf + return !('hidden' in leaf) || !leaf.hidden + }) +}) + /** 递归查找激活叶子所在的所有祖先分组 key(用于受控展开) */ const activeGroupKeys = computed(() => { if (!props.section) return [] diff --git a/src/meta/nav.ts b/src/meta/nav.ts index fe34710..847e832 100644 --- a/src/meta/nav.ts +++ b/src/meta/nav.ts @@ -49,6 +49,8 @@ export interface NavLeaf { status?: 'draft' | 'review' | 'ready' /** 受众限制,'designer' 仅设计可见;默认 'all' */ audience?: 'designer' | 'all' + /** 是否在侧边栏隐藏(仍参与路由注册,用于"操作按钮跳转"的二级子页面) */ + hidden?: boolean } /** 二级分组(侧边栏 SubMenu,里面装三级叶子) */ @@ -441,7 +443,7 @@ export const adminSections: NavSection[] = [ }, ], }, - // 5. 设备运维(重组:原"故障与维修"+ 原"系统设置"迁入,6 个二级菜单) + // 5. 设备运维(重组:原"故障与维修"+ 原"系统设置"迁入,5 个二级菜单) { key: 'admin-device-ops', label: '设备运维', @@ -449,7 +451,7 @@ export const adminSections: NavSection[] = [ children: [ { key: 'admin-device-ops-init', - label: '设备初始化', + label: '初始化设置', path: 'ops/init', status: 'ready', component: () => import('@pages/admin-portal/device-center/ops/init/init.vue'), @@ -475,6 +477,14 @@ export const adminSections: NavSection[] = [ status: 'ready', component: () => import('@pages/admin-portal/device-center/ops/version/version.vue'), }, + { + key: 'admin-device-ops-version-list', + label: '版本列表', + path: 'ops/version-list', + status: 'ready', + hidden: true, + component: () => import('@pages/admin-portal/device-center/ops/version/component/versionList/versionList.vue'), + }, { key: 'admin-device-ops-alarm', label: '告警管理', @@ -482,16 +492,9 @@ export const adminSections: NavSection[] = [ status: 'ready', component: () => import('@pages/admin-portal/device-center/ops/alarm/alarm.vue'), }, - { - key: 'admin-device-ops-config', - label: '系统配置', - path: 'ops/config', - status: 'ready', - component: () => import('@pages/admin-portal/device-center/ops/config/config.vue'), - }, ], }, - // 6. 数据分发(原"数据中转"更名,本期仅支持接口回调,3 个二级菜单) + // 6. 数据分发(2 个可见菜单:分发配置 / 上报配置;分发记录作为分发配置的二级子页面,hidden 但可路由) { key: 'admin-device-distribute', label: '数据分发', @@ -504,19 +507,20 @@ export const adminSections: NavSection[] = [ status: 'ready', component: () => import('@pages/admin-portal/device-center/distribute/config/config.vue'), }, + { + key: 'admin-device-distribute-report', + label: '上报配置', + path: 'distribute/report', + status: 'ready', + component: () => import('@pages/admin-portal/device-center/distribute/report/report.vue'), + }, { key: 'admin-device-distribute-record', label: '分发记录', path: 'distribute/record', status: 'ready', - component: () => import('@pages/admin-portal/device-center/distribute/record/record.vue'), - }, - { - key: 'admin-device-distribute-deadletter', - label: '失败重投', - path: 'distribute/deadletter', - status: 'ready', - component: () => import('@pages/admin-portal/device-center/distribute/deadletter/deadletter.vue'), + hidden: true, + component: () => import('@pages/admin-portal/device-center/distribute/config/component/record/record.vue'), }, ], }, diff --git a/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/info.vue index 01bd926..acf3a7d 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/info.vue +++ b/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/info.vue @@ -21,7 +21,13 @@ {{ deployStatusText(detail.deployStatus) }} - {{ detail.softwareVersion ?? '—' }} + + + + {{ detail.customerName ?? '—' }} {{ detail.siteName ?? '—' }} {{ detail.onlineTime ?? '—' }} @@ -114,4 +120,15 @@ const deployStatusText = (status: DeployStatus | undefined): string => { .info-drawer__section { margin-top: 16px; } +.info-drawer__pkg { + color: #8c8c8c; + font-size: 12px; + line-height: 1.6; +} +.info-drawer__ver { + color: #262626; + font-size: 13px; + font-weight: 500; + line-height: 1.6; +} diff --git a/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/types/index.ts index 3703f68..565c346 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/types/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/component/drawer/info/types/index.ts @@ -45,7 +45,9 @@ export interface DetailInfo { siteName: string /** 投用状态 */ deployStatus: DeployStatus - /** 软件版本 */ + /** 软件包名(初始化设置时写入) */ + packageName: string + /** 软件版本(初始化设置时写入) */ softwareVersion: string /** 入网时间 */ onlineTime: string diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue index 0c96b8c..c57ca3d 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/addOrEdit.vue @@ -101,17 +101,6 @@ - - - - - @@ -120,9 +109,6 @@ 由客户 / 场所自动判定 - - - - + + + + ({ siteId: '', siteName: '', deployStatus: 'in_stock', - softwareVersion: '', deployTime: '', onlineTime: '', arcLicense: '', @@ -126,7 +127,7 @@ export const usePage = (emit: (e: 'load') => void) => { const rules: Record = { deviceCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }], deviceCategory: [{ required: true, message: '请选择设备类别', trigger: 'change' }], - // 设备名称 / 序列号 / 客户 / 场所 / 软件版本 / 虹软授权码 / SIM 卡 均非必填 + // 设备名称 / 序列号 / 客户 / 场所 / 虹软授权码 / SIM 卡 均非必填 } const resetForm = (): void => { diff --git a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts index 2c328fc..4da845d 100644 --- a/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/component/modal/addOrEdit/types/index.ts @@ -6,6 +6,7 @@ * - activeTab 由 openModal 上下文传入,仅用于切换设备类别下拉数据源 * - 所属客户 / 所属场所 非必填:留空代表在库未投用,由 usePage 联动计算 deployStatus * - 绑定 SIM 卡字段仅当所选类别 networkMode === 'sim' 时展示 + * - 软件包名 / 软件版本 不在弹框采集(由后续"初始化设置"流程写入),编辑回填时仅做透传 */ import type { SelectProps } from 'ant-design-vue' @@ -46,8 +47,6 @@ export interface Form { siteName: string /** 投用状态(依据客户 / 场所自动判定) */ deployStatus: DeployStatus - /** 软件版本 */ - softwareVersion: string /** 投用时间(投用后才存在,编辑回填) */ deployTime: string /** 入网时间(编辑回填,新增时由后端写入) */ diff --git a/src/pages/admin-portal/device-center/asset/manage/init/useTable.ts b/src/pages/admin-portal/device-center/asset/manage/init/useTable.ts index 96b679f..3867475 100644 --- a/src/pages/admin-portal/device-center/asset/manage/init/useTable.ts +++ b/src/pages/admin-portal/device-center/asset/manage/init/useTable.ts @@ -37,7 +37,23 @@ const buildCommonColumns = (): TableColumnsType => [ }, { title: '所属客户', dataIndex: 'customerName', align: 'left', width: 160, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, { title: '所属场所', dataIndex: 'siteName', align: 'left', width: 160, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, - { title: '软件版本', dataIndex: 'softwareVersion', align: 'center', width: 110, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, + { + title: '软件版本', + dataIndex: 'softwareVersion', + align: 'center', + width: 130, + resizable: true, + ellipsis: true, + customRender: ({ record }: { record: DeviceItem }) => { + const pkg = record.packageName ?? '' + const ver = record.softwareVersion ?? '' + if (!pkg && !ver) return '—' + return h('div', { style: 'line-height: 1.4; text-align: center' }, [ + pkg ? h('div', { style: 'color: #8c8c8c; font-size: 12px' }, pkg) : null, + ver ? h('div', { style: 'color: #262626; font-size: 13px; font-weight: 500' }, ver) : null, + ]) + }, + }, { title: '入库单号', dataIndex: 'inboundCode', align: 'left', width: 170, resizable: true, ellipsis: true, customRender: ({ value }: { value?: string }) => value ?? '—' }, { title: '入库时间', dataIndex: 'onlineTime', align: 'center', width: 170, sorter: true, resizable: true }, { title: '投用时间', dataIndex: 'deployTime', align: 'center', width: 170, resizable: true, customRender: ({ value }: { value: string }) => value ?? '—' }, diff --git a/src/pages/admin-portal/device-center/asset/manage/types/index.ts b/src/pages/admin-portal/device-center/asset/manage/types/index.ts index 32fad09..9f9b0ff 100644 --- a/src/pages/admin-portal/device-center/asset/manage/types/index.ts +++ b/src/pages/admin-portal/device-center/asset/manage/types/index.ts @@ -41,7 +41,9 @@ export interface DeviceItem extends BaseRecord { siteName: string /** 投用状态:在库 / 投用(依据客户与场所自动判定) */ deployStatus: DeployStatus - /** 软件版本 */ + /** 软件包名(初始化设置时写入,与 softwareVersion 同时产生) */ + packageName: string + /** 软件版本(初始化设置时写入,新增/编辑弹框不采集) */ softwareVersion: string /** 入网时间(即入库时间) */ onlineTime: string diff --git a/src/pages/admin-portal/device-center/customer/site/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/customer/site/component/modal/addOrEdit/addOrEdit.vue index 4486c1e..3dd4357 100644 --- a/src/pages/admin-portal/device-center/customer/site/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/customer/site/component/modal/addOrEdit/addOrEdit.vue @@ -21,7 +21,7 @@ v-no-space allow-clear :maxlength="60" - placeholder="如 长庆总部一食堂" + placeholder="如 国庆总部一食堂" /> diff --git a/src/pages/admin-portal/device-center/distribute/config/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/api/index.ts new file mode 100644 index 0000000..f9cdb2e --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/api/index.ts @@ -0,0 +1,37 @@ +/** + * 数据分发 - 分发配置 接口层 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { CustomerOption } from '../types' +import type { ConfigItem, ConfigPayload, SearchForm } from '../types' + +const BASE = '/admin/device/distribute/config' + +/** 列表查询 */ +export const list = (params: SearchForm): Promise> => + postRequest('axiosRequest', BASE + '/list', params) + +/** 详情查询 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) + +/** 新增 */ +export const add = (params: ConfigPayload): Promise> => + postRequest('axiosRequest', BASE + '/add', params) + +/** 编辑 */ +export const update = (params: ConfigPayload): Promise> => + postRequest('axiosRequest', BASE + '/update', params) + +/** 切换启用状态 */ +export const toggleStatus = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/toggleStatus', { id }) + +/** + * 客户下拉选项(复用客户档案列表接口,仅取 id/name) + * - 路径与 @pages/admin-portal/device-center/customer/archive/api 一致 + * - 研发接手时该接口保持不变,本处无需修改 + */ +export const customerOptions = (): Promise> => + postRequest('axiosRequest', '/admin/device/customer/archive/page', {}) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/api/index.ts new file mode 100644 index 0000000..acce0f9 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/api/index.ts @@ -0,0 +1,12 @@ +/** + * 分发配置 - 详情抽屉 - 接口层 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { DetailInfo } from '../types' + +const BASE = '/admin/device/distribute/config' + +/** 详情查询 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/info.vue new file mode 100644 index 0000000..e6af3ed --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/info.vue @@ -0,0 +1,64 @@ + + + diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/init/usePage.ts similarity index 58% rename from src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/init/usePage.ts rename to src/pages/admin-portal/device-center/distribute/config/component/drawer/info/init/usePage.ts index 58f7bbc..efaf0bd 100644 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/init/usePage.ts +++ b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/init/usePage.ts @@ -1,44 +1,47 @@ -import { reactive, ref } from 'vue' -import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import * as api from '../api' +/** + * 分发配置 - 详情抽屉 - 主逻辑 + */ +import { info } from '../api' import type { DetailInfo, OpenContext, PageInfo } from '../types' export const usePage = () => { - const { message } = useAntdStaticMethods() - const pageInfo = reactive({ visible: false, - title: '下游系统详情', - width: 720, + title: '分发配置详情', + width: 760, spin: false, }) const detail = ref(null) + const closeDrawer = (): void => { + pageInfo.visible = false + detail.value = null + } + const openDrawer = (ctx: OpenContext): void => { - pageInfo.title = ctx.title ?? '下游系统详情' pageInfo.visible = true pageInfo.spin = true detail.value = null - api.getInfo(ctx.id) + + info(ctx.id) .then((res) => { if (res.code === '00000' && res.data) { detail.value = res.data - } else { - void message.error(res.msg || '查询失败') } }) .catch((err: unknown) => { - console.error('查询失败:', err) + console.error('分发配置详情查询失败:', err) }) .finally(() => { pageInfo.spin = false }) } - const closeDrawer = (): void => { - pageInfo.visible = false + return { + pageInfo, + detail, + openDrawer, + closeDrawer, } - - return { pageInfo, detail, openDrawer, closeDrawer } } diff --git a/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/types/index.ts new file mode 100644 index 0000000..a081212 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/drawer/info/types/index.ts @@ -0,0 +1,17 @@ +import type { ConfigItem } from '../../../types' + +/** 抽屉页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 打开抽屉上下文 */ +export interface OpenContext { + id: string +} + +/** 详情数据(与列表项 ConfigItem 同构) */ +export type DetailInfo = ConfigItem diff --git a/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/addOrEdit.vue new file mode 100644 index 0000000..ca1aed2 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/addOrEdit.vue @@ -0,0 +1,203 @@ + + + diff --git a/src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/api/index.ts similarity index 64% rename from src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/api/index.ts rename to src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/api/index.ts index 6375d0a..0f4d183 100644 --- a/src/pages/admin-portal/device-center/ops/init/component/modal/addOrEdit/api/index.ts +++ b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/api/index.ts @@ -1,18 +1,21 @@ /** - * 设备初始化 - 新增/编辑弹框 - 接口层 - * - 复用主页 add/update/info 接口 + * 分发配置 - 新增/编辑 弹框 - 接口层 + * - 复用主页的 info / add / update */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' import type { Form } from '../types' -const BASE = '/admin/device/ops/init' +const BASE = '/admin/device/distribute/config' +/** 详情查询(编辑回填) */ export const info = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/info-for-edit', { id }) + postRequest('axiosRequest', BASE + '/info', { id }) +/** 新增 */ export const add = (params: Record): Promise> => postRequest('axiosRequest', BASE + '/add', params) +/** 编辑 */ export const edit = (params: Record): Promise> => postRequest('axiosRequest', BASE + '/update', params) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/init/usePage.ts new file mode 100644 index 0000000..8a09709 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/init/usePage.ts @@ -0,0 +1,236 @@ +/** + * 分发配置 - 新增/编辑 弹框 - 主逻辑 + * + * 严格遵循 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 { onMounted, ref } from 'vue' +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { deviceCategoryTree } from '@api' +import type { DeviceCategoryTreeNode } from '@api' +import { customerOptions as fetchCustomerOptions } from '../../../../api' +import { add, edit, info } from '../api' +import type { CustomerOption, Form, ModalType, OpenContext, PageInfo } from '../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + id: undefined, + name: '', + code: '', + customerId: undefined, + callbackUrl: '', + dispatchMode: 'realtime', + authEnabled: false, + authAccount: '', + authSecret: '', + authApiUrl: '', + status: 'enabled', + deviceCategoryIds: [], + maxRetry: 5, + remark: '', +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + type: 'add', + title: '', + width: 820, + spin: false, + }) + + const formRef = ref() + const form = ref
(createInitForm()) + + /** 客户下拉选项 */ + const customerOptions = ref([]) + /** 设备分类树(a-tree-select 数据源) */ + const categoryTree = ref([]) + /** a-tree-select 字段映射(与设备分类树节点字段对齐) */ + const treeFieldNames = { label: 'name', value: 'id', children: 'children' } + + const rules: Record = { + name: [{ required: true, message: '请输入下游系统名称', trigger: 'blur' }], + code: [{ required: true, message: '请输入系统标识', trigger: 'blur' }], + customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }], + callbackUrl: [{ required: true, message: '请输入数据分发地址', trigger: 'blur' }], + dispatchMode: [{ required: true, message: '请选择分发模式', trigger: 'change' }], + status: [{ required: true, message: '请选择状态', trigger: 'change' }], + maxRetry: [{ required: true, message: '请输入最大重试次数', trigger: 'blur' }], + } + + /** 鉴权开关联动:开启时鉴权字段必填 */ + const authFieldRules = (field: 'authAccount' | 'authSecret' | 'authApiUrl'): Rule[] => { + return [{ + required: true, + message: `鉴权开启时必填`, + trigger: 'blur', + validator: (_rule: unknown, value: string) => { + if (form.value.authEnabled && !value) return Promise.reject(new Error('鉴权开启时必填')) + return Promise.resolve() + }, + }] as Rule[] + } + + const rulesWithAuth: Record = { + ...rules, + authAccount: authFieldRules('authAccount'), + authSecret: authFieldRules('authSecret'), + authApiUrl: authFieldRules('authApiUrl'), + } + + /** 拉取客户下拉 */ + const loadCustomerOptions = (): void => { + fetchCustomerOptions() + .then((res) => { + if (res.code === '00000') { + customerOptions.value = (res.data ?? []).map((c) => ({ label: c.name, value: c.id })) + } + }) + .catch((err: unknown) => { + console.error('客户下拉加载失败:', err) + }) + } + + /** 拉取设备分类树 */ + const loadCategoryTree = (): void => { + deviceCategoryTree() + .then((res) => { + if (res.code === '00000') { + categoryTree.value = res.data ?? [] + } + }) + .catch((err: unknown) => { + console.error('加载设备分类树失败:', err) + }) + } + + 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' ? '新增分发配置' : '编辑分发配置' + + /** 客户下拉 / 设备分类树为空时补拉一次 */ + if (!customerOptions.value.length) loadCustomerOptions() + if (!categoryTree.value.length) loadCategoryTree() + + 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 + /** 鉴权关闭时清空鉴权字段,避免脏数据 */ + if (!params.authEnabled) { + params.authAccount = '' + params.authSecret = '' + params.authApiUrl = '' + } + 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 => { + if (!params.authEnabled) { + params.authAccount = '' + params.authSecret = '' + params.authApiUrl = '' + } + 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) + }) + } + + onMounted(() => { + loadCustomerOptions() + loadCategoryTree() + }) + + return { + pageInfo, + formRef, + form, + rules: rulesWithAuth, + customerOptions, + categoryTree, + treeFieldNames, + openModal, + closeModal, + submit, + } +} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/types/index.ts new file mode 100644 index 0000000..0062ab5 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/modal/addOrEdit/types/index.ts @@ -0,0 +1,59 @@ +import type { FormInstance } from 'ant-design-vue/es/form' +import type { DeviceCategoryTreeNode } from '@api' +import type { DispatchMode, Status } from '../../../types' +import type { CustomerOption } 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 +} + +/** 表单字段(对齐 ConfigPayload) */ +export interface Form { + id: string | undefined + /** 下游系统名称 */ + name: string + /** 系统标识(编辑时禁用) */ + code: string + /** 所属客户 ID */ + customerId: string | undefined + /** 数据分发地址 */ + callbackUrl: string + /** 分发模式 */ + dispatchMode: DispatchMode + /** 鉴权开关 */ + authEnabled: boolean + /** 鉴权账号 */ + authAccount: string + /** 鉴权秘钥 */ + authSecret: string + /** 鉴权接口地址 */ + authApiUrl: string + /** 状态 */ + status: Status + /** 覆盖设备(设备分类 ID 多选) */ + deviceCategoryIds: string[] + /** 最大重试次数 */ + maxRetry: number + /** 备注 */ + remark: string +} + +/** 客户下拉选项(与主页共享类型) */ +export type { CustomerOption } + +/** 设备分类树节点(与 @api 对齐) */ +export type CategoryTreeNode = DeviceCategoryTreeNode diff --git a/src/pages/admin-portal/device-center/distribute/record/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/record/api/index.ts similarity index 56% rename from src/pages/admin-portal/device-center/distribute/record/api/index.ts rename to src/pages/admin-portal/device-center/distribute/config/component/record/api/index.ts index 211b197..0a69126 100644 --- a/src/pages/admin-portal/device-center/distribute/record/api/index.ts +++ b/src/pages/admin-portal/device-center/distribute/config/component/record/api/index.ts @@ -1,31 +1,32 @@ /** * 分发记录 - 接口层 */ - import type { ApiResponse } from '@axios' import { postRequest } from '@axios' import type { DetailInfo, RecordItem, SearchForm, StatCards } from '../types' +const BASE = '/admin/device/distribute/record' + /** 列表查询 */ export const list = (params: SearchForm): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/list', params) + postRequest('axiosRequest', BASE + '/list', params) /** 详情查询 */ export const info = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/info', { id }) + postRequest('axiosRequest', BASE + '/info', { id }) /** 顶部统计卡 */ -export const stat = (): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/stat', {}) +export const stat = (params: { downstreamId?: string }): Promise> => + postRequest('axiosRequest', BASE + '/stat', params) /** 手动重投(单条) */ export const retry = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/retry', { id }) + postRequest('axiosRequest', BASE + '/retry', { id }) /** 批量重投 */ export const batchRetry = (ids: string[]): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/batchRetry', { ids }) + postRequest('axiosRequest', BASE + '/batchRetry', { ids }) /** 导出 */ export const exportList = (params: SearchForm): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/export', params) + postRequest('axiosRequest', BASE + '/export', params) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/api/index.ts new file mode 100644 index 0000000..ce236db --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/api/index.ts @@ -0,0 +1,12 @@ +/** + * 分发记录 - 详情抽屉 - 接口层 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { DetailInfo } from '../types' + +const BASE = '/admin/device/distribute/record' + +/** 详情查询 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/info.vue similarity index 65% rename from src/pages/admin-portal/device-center/distribute/record/component/drawer/info/info.vue rename to src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/info.vue index 184ad87..a862323 100644 --- a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/info.vue +++ b/src/pages/admin-portal/device-center/distribute/config/component/record/component/drawer/info/info.vue @@ -6,29 +6,42 @@ :destroy-on-close="true" > - + + {{ detail.dispatchTime ?? '—' }} + + {{ dispatchStatusText(detail.status) }} + {{ detail.deviceCode ?? '—' }} {{ detail.deviceName ?? '—' }} {{ dataTypeText(detail.dataType) }} {{ detail.downstreamName ?? '—' }} - {{ detail.dispatchTime ?? '—' }} {{ detail.httpStatus }} {{ detail.responseTime ?? 0 }} ms {{ detail.retryCount ?? 0 }} - - {{ dispatchStatusText(detail.status) }} - {{ detail.authKey ?? '—' }} - {{ detail.failureReason ?? '—' }} - -
{{ formatJson(detail.requestBody) }}
-
- -
{{ formatJson(detail.responseBody) }}
+ + + + + +
{{ detail.failureReason || '—' }}
@@ -37,7 +50,8 @@ @@ -98,6 +119,19 @@ const { gap: 12px; } +.page-back-bar { + display: flex; + align-items: center; + height: 32px; + padding: 0 4px; + + .bar-title { + font-size: 14px; + color: rgba(0, 0, 0, 0.85); + font-weight: 500; + } +} + .record-stat { margin: 0 !important; } diff --git a/src/pages/admin-portal/device-center/distribute/record/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/record/types/index.ts similarity index 92% rename from src/pages/admin-portal/device-center/distribute/record/types/index.ts rename to src/pages/admin-portal/device-center/distribute/config/component/record/types/index.ts index 4f12119..cbaf82b 100644 --- a/src/pages/admin-portal/device-center/distribute/record/types/index.ts +++ b/src/pages/admin-portal/device-center/distribute/config/component/record/types/index.ts @@ -33,11 +33,11 @@ export interface RecordItem { failureReason?: string } -/** 详情数据(含请求体/响应体) */ +/** 详情数据(含请求体/响应体/失败日志) */ export interface DetailInfo extends RecordItem { - /** 请求体(JSON 字符串) */ + /** 请求体(分发数据格式,JSON 字符串) */ requestBody: string - /** 响应体(JSON 字符串) */ + /** 响应体(响应数据格式,JSON 字符串) */ responseBody: string /** 鉴权密钥(脱敏展示) */ authKey: string @@ -59,7 +59,7 @@ export interface StatCards { export interface SearchForm extends BaseSearchForm { /** 设备编码 */ deviceCode?: string - /** 下游系统 ID */ + /** 下游系统 ID(从分发配置跳转携带) */ downstreamId?: string /** 数据类型 */ dataType?: DataType diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/api/index.ts deleted file mode 100644 index b1b7c88..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/api/index.ts +++ /dev/null @@ -1,32 +0,0 @@ -/** - * 分发配置 - 下游系统 接口 - */ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { DownstreamForm, DownstreamItem, SearchForm } from '../types' - -const BASE = '/admin/device/distribute/config/downstream' - -/** 列表 */ -export const list = (params: SearchForm): Promise> => - postRequest('axiosRequest', BASE + '/list', params) - -/** 详情 */ -export const info = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/info', { id }) - -/** 新增 */ -export const add = (params: DownstreamForm): Promise> => - postRequest('axiosRequest', BASE + '/add', params) - -/** 编辑 */ -export const update = (params: DownstreamForm): Promise> => - postRequest('axiosRequest', BASE + '/update', params) - -/** 删除 */ -export const remove = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/delete', { id }) - -/** 启用/禁用 */ -export const toggleStatus = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/toggleStatus', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/api/index.ts deleted file mode 100644 index a747993..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/api/index.ts +++ /dev/null @@ -1,8 +0,0 @@ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { DetailInfo } from '../types' - -const BASE = '/admin/device/distribute/config/downstream' - -export const getInfo = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/info', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/info.vue deleted file mode 100644 index a27d432..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/info.vue +++ /dev/null @@ -1,53 +0,0 @@ - - - diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/types/index.ts deleted file mode 100644 index a2959b4..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/drawer/info/types/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { DownstreamItem } from '../../../../types' - -export interface PageInfo { - visible: boolean - title: string - width: number - spin: boolean -} - -export interface OpenContext { - id: string - title?: string -} - -export type DetailInfo = DownstreamItem diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/addOrEdit.vue deleted file mode 100644 index 41bdbcd..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/addOrEdit.vue +++ /dev/null @@ -1,108 +0,0 @@ - - - diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/api/index.ts deleted file mode 100644 index 9da7297..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/api/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { FormModel } from '../types' - -const BASE = '/admin/device/distribute/config/downstream' - -export const add = (params: FormModel): Promise> => - postRequest('axiosRequest', BASE + '/add', params) - -export const update = (params: FormModel): Promise> => - postRequest('axiosRequest', BASE + '/update', params) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/init/usePage.ts deleted file mode 100644 index ee9579e..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/init/usePage.ts +++ /dev/null @@ -1,85 +0,0 @@ -import { reactive } from 'vue' -import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import * as api from '../api' -import type { DetailRecord, FormModel, PageInfo } from '../types' - -/** 新增的默认模型 */ -const createFormModel = (): FormModel => ({ - id: undefined, - name: '', - code: '', - callbackUrl: '', - authType: 'apikey', - authKey: '', - dataTypes: [], - retryStrategy: 'exponential', - maxRetry: 5, - status: 'enabled', - remark: '', -}) - -export const usePage = () => { - const { message } = useAntdStaticMethods() - - const pageInfo = reactive({ - visible: false, - title: '新增下游系统', - width: 720, - spin: false, - }) - - const formModel = reactive(createFormModel()) - - /** 新增 */ - const openAdd = (): void => { - Object.assign(formModel, createFormModel()) - pageInfo.title = '新增下游系统' - pageInfo.visible = true - } - - /** 编辑(携带行数据) */ - const openEdit = (record: DetailRecord): void => { - Object.assign(formModel, createFormModel(), { - id: record.id, - name: record.name, - code: record.code, - callbackUrl: record.callbackUrl, - authType: record.authType, - authKey: record.authKey, - dataTypes: [...record.dataTypes], - retryStrategy: record.retryStrategy, - maxRetry: record.maxRetry, - status: record.status, - remark: record.remark ?? '', - }) - pageInfo.title = '编辑下游系统' - pageInfo.visible = true - } - - const close = (): void => { - pageInfo.visible = false - } - - /** 提交 */ - const submit = (emit: (event: 'load') => void): void => { - pageInfo.spin = true - const params = { ...formModel } - const request = formModel.id ? api.update(params) : api.add({ ...params, id: undefined }) - request - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - emit('load') - close() - } - }) - .catch((err: unknown) => { - console.error('提交失败:', err) - }) - .finally(() => { - pageInfo.spin = false - }) - } - - return { pageInfo, formModel, openAdd, openEdit, close, submit } -} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/types/index.ts deleted file mode 100644 index c21aee3..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/component/modal/addOrEdit/types/index.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { DownstreamForm, DownstreamItem } from '../../../../types' - -export interface PageInfo { - visible: boolean - title: string - width: number - spin: boolean -} - -export type FormModel = DownstreamForm - -export type DetailRecord = DownstreamItem diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/downstream.vue b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/downstream.vue deleted file mode 100644 index f646b30..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/downstream.vue +++ /dev/null @@ -1,78 +0,0 @@ - - - - - diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useTable.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useTable.ts deleted file mode 100644 index 1380806..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useTable.ts +++ /dev/null @@ -1,59 +0,0 @@ -import type { TableColumnsType } from 'ant-design-vue' -import { h } from 'vue' -import { Tag } from 'ant-design-vue' -import type { AuthType, DownstreamItem, RetryStrategy, Status } from '../types' -import { - authTypeColor, - authTypeText, - dataTypeText, - retryStrategyColor, - retryStrategyText, - statusColor, - statusText, -} from '../types' - -/** 下游系统列定义 */ -export const tableColumns: TableColumnsType = [ - { title: '下游ID', dataIndex: 'id', align: 'left', fixed: 'left', width: 100 }, - { title: '系统名称', dataIndex: 'name', align: 'left', width: 160, resizable: true, ellipsis: true }, - { title: '业务编码', dataIndex: 'code', align: 'left', width: 130, ellipsis: true }, - { title: '回调地址', dataIndex: 'callbackUrl', align: 'left', width: 220, resizable: true, ellipsis: true }, - { - title: '鉴权方式', - dataIndex: 'authType', - align: 'center', - width: 110, - customRender: ({ value }: { value: AuthType }) => - h(Tag, { color: authTypeColor[value] }, () => authTypeText(value)), - }, - { title: '鉴权密钥', dataIndex: 'authKey', align: 'left', width: 160, ellipsis: true }, - { - title: '订阅数据类型', - dataIndex: 'dataTypes', - align: 'left', - width: 200, - customRender: ({ value }: { value: DownstreamItem['dataTypes'] }) => { - if (!value || value.length === 0) return '—' - return value.map((v) => h(Tag, { color: 'blue', style: 'margin-bottom:2px' }, () => dataTypeText(v))) - }, - }, - { - title: '重投策略', - dataIndex: 'retryStrategy', - align: 'center', - width: 110, - customRender: ({ value }: { value: RetryStrategy }) => - h(Tag, { color: retryStrategyColor[value] }, () => retryStrategyText(value)), - }, - { title: '最大重试', dataIndex: 'maxRetry', align: 'center', width: 90 }, - { - title: '状态', - dataIndex: 'status', - align: 'center', - width: 90, - customRender: ({ value }: { value: Status }) => - h(Tag, { color: statusColor[value] }, () => statusText(value)), - }, - { title: '创建时间', dataIndex: 'createdAt', align: 'center', width: 170 }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 220 }, -] diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/types/index.ts deleted file mode 100644 index 9902d5f..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/types/index.ts +++ /dev/null @@ -1,110 +0,0 @@ -/** - * 分发配置 - 下游系统 类型 - */ -import type { BaseSearchForm } from '@pages/admin-portal/shared/types' -import type { DataType } from '../../../../../record/types' - -/** 鉴权方式 */ -export type AuthType = 'none' | 'apikey' | 'hmac' | 'token' - -/** 重投策略 */ -export type RetryStrategy = 'exponential' | 'fixed' | 'none' - -/** 启用状态 */ -export type Status = 'enabled' | 'disabled' - -/** 下游系统列表项 */ -export interface DownstreamItem { - id: string - name: string - code: string - callbackUrl: string - authType: AuthType - authKey: string - dataTypes: DataType[] - retryStrategy: RetryStrategy - maxRetry: number - status: Status - remark?: string - createdAt: string -} - -/** 表单(新增/编辑) */ -export interface DownstreamForm { - id?: string - name: string - code: string - callbackUrl: string - authType: AuthType - authKey: string - dataTypes: DataType[] - retryStrategy: RetryStrategy - maxRetry: number - status: Status - remark?: string -} - -/** 搜索表单 */ -export interface SearchForm extends BaseSearchForm { - name?: string - authType?: AuthType - retryStrategy?: RetryStrategy - status?: Status -} - -/** 鉴权方式选项 */ -export const AUTH_TYPE_OPTIONS: { label: string; value: AuthType }[] = [ - { label: '无鉴权', value: 'none' }, - { label: 'API Key', value: 'apikey' }, - { label: 'HMAC 签名', value: 'hmac' }, - { label: 'Bearer Token', value: 'token' }, -] - -/** 重投策略选项 */ -export const RETRY_STRATEGY_OPTIONS: { label: string; value: RetryStrategy }[] = [ - { label: '指数退避', value: 'exponential' }, - { label: '固定间隔', value: 'fixed' }, - { label: '不重投', value: 'none' }, -] - -/** 启用状态选项 */ -export const STATUS_OPTIONS: { label: string; value: Status }[] = [ - { label: '启用', value: 'enabled' }, - { label: '禁用', value: 'disabled' }, -] - -/** 鉴权方式 → 中文 */ -export const authTypeText = (value: AuthType | string | undefined): string => - AUTH_TYPE_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' - -/** 重投策略 → 中文 */ -export const retryStrategyText = (value: RetryStrategy | string | undefined): string => - RETRY_STRATEGY_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' - -/** 鉴权方式 → Tag 颜色 */ -export const authTypeColor: Record = { - none: 'default', - apikey: 'blue', - hmac: 'geekblue', - token: 'cyan', -} - -/** 重投策略 → Tag 颜色 */ -export const retryStrategyColor: Record = { - exponential: 'processing', - fixed: 'warning', - none: 'default', -} - -/** 启用状态 → Tag 颜色 */ -export const statusColor: Record = { - enabled: 'success', - disabled: 'default', -} - -/** 启用状态 → 中文 */ -export const statusText = (value: Status | string | undefined): string => - STATUS_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' - -/** 数据类型选项(复用 record) */ -export { DATA_TYPE_OPTIONS, dataTypeText } from '../../../../../record/types' diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/api/index.ts deleted file mode 100644 index 9c3cac2..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/api/index.ts +++ /dev/null @@ -1,29 +0,0 @@ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { SearchForm, SubscribeForm, SubscribeItem } from '../types' - -const BASE = '/admin/device/distribute/config/subscribe' - -/** 列表 */ -export const list = (params: SearchForm): Promise> => - postRequest('axiosRequest', BASE + '/list', params) - -/** 新增 */ -export const add = (params: SubscribeForm): Promise> => - postRequest('axiosRequest', BASE + '/add', params) - -/** 编辑 */ -export const update = (params: SubscribeForm): Promise> => - postRequest('axiosRequest', BASE + '/update', params) - -/** 删除 */ -export const remove = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/delete', { id }) - -/** 启用/禁用 */ -export const toggleStatus = (id: string): Promise> => - postRequest('axiosRequest', BASE + '/toggleStatus', { id }) - -/** 下游下拉选项 */ -export const options = (): Promise> => - postRequest('axiosRequest', BASE + '/options', {}) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/addOrEdit.vue deleted file mode 100644 index e41e721..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/addOrEdit.vue +++ /dev/null @@ -1,52 +0,0 @@ - - - diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/api/index.ts deleted file mode 100644 index 04a06b0..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/api/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { FormModel } from '../types' - -const BASE = '/admin/device/distribute/config/subscribe' - -export const add = (params: FormModel): Promise> => - postRequest('axiosRequest', BASE + '/add', params) - -export const update = (params: FormModel): Promise> => - postRequest('axiosRequest', BASE + '/update', params) diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/init/usePage.ts deleted file mode 100644 index 8599a1c..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/init/usePage.ts +++ /dev/null @@ -1,74 +0,0 @@ -import { reactive, ref } from 'vue' -import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import * as api from '../api' -import type { DetailRecord, DownstreamOptions, FormModel, PageInfo } from '../types' - -const createFormModel = (): FormModel => ({ - id: undefined, - downstreamId: '', - deviceCode: '', - dataType: 'weight', - filterRule: '', - status: 'enabled', -}) - -export const usePage = () => { - const { message } = useAntdStaticMethods() - - const pageInfo = reactive({ - visible: false, - title: '新增订阅映射', - width: 600, - spin: false, - }) - - const formModel = reactive(createFormModel()) - const downstreamOptions = ref([]) - - const openAdd = (options: DownstreamOptions): void => { - Object.assign(formModel, createFormModel()) - downstreamOptions.value = options - pageInfo.title = '新增订阅映射' - pageInfo.visible = true - } - - const openEdit = (record: DetailRecord, options: DownstreamOptions): void => { - Object.assign(formModel, createFormModel(), { - id: record.id, - downstreamId: record.downstreamId, - deviceCode: record.deviceCode, - dataType: record.dataType, - filterRule: record.filterRule, - status: record.status, - }) - downstreamOptions.value = options - pageInfo.title = '编辑订阅映射' - pageInfo.visible = true - } - - const close = (): void => { - pageInfo.visible = false - } - - const submit = (emit: (event: 'load') => void): void => { - pageInfo.spin = true - const params = { ...formModel } - const request = formModel.id ? api.update(params) : api.add({ ...params, id: undefined }) - request - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - emit('load') - close() - } - }) - .catch((err: unknown) => { - console.error('提交失败:', err) - }) - .finally(() => { - pageInfo.spin = false - }) - } - - return { pageInfo, formModel, downstreamOptions, openAdd, openEdit, close, submit } -} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/types/index.ts deleted file mode 100644 index 8db5f6d..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/component/modal/addOrEdit/types/index.ts +++ /dev/null @@ -1,15 +0,0 @@ -import type { DownstreamOption } from '../../../../types' -import type { SubscribeForm, SubscribeItem } from '../../../../types' - -export interface PageInfo { - visible: boolean - title: string - width: number - spin: boolean -} - -export type FormModel = SubscribeForm - -export type DetailRecord = SubscribeItem - -export type DownstreamOptions = DownstreamOption[] diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/usePage.ts deleted file mode 100644 index 84a0a23..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/usePage.ts +++ /dev/null @@ -1,161 +0,0 @@ -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 * as api from '../api' -import type { DownstreamOption, SearchForm, SubscribeItem } from '../types' -import { createSearchKey } from './useSearch' -import { tableColumns } from './useTable' - -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 downstreamOptions = ref([]) - - const addOrEditRef = ref | null>(null) - - const listRequest = (): void => { - pageLoading.value = true - api.list(search) - .then((res) => { - if (res.code === '00000') { - table.dataSource = res.data ?? [] - table.pagination.total = table.dataSource.length - } - }) - .catch((err: unknown) => { - console.error('订阅映射列表请求失败:', err) - }) - .finally(() => { - pageLoading.value = false - }) - } - - /** 拉下游选项(首次进入时调用) */ - const loadOptions = (): void => { - api.options() - .then((res) => { - if (res.code === '00000' && res.data?.downstream) { - downstreamOptions.value = res.data.downstream - } - }) - .catch((err: unknown) => { - console.error('下游选项拉取失败:', err) - }) - } - - 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 openAdd = (): void => { - if (downstreamOptions.value.length === 0) { - void message.warning('请先在「下游系统」Tab 创建至少一条下游系统') - return - } - addOrEditRef.value?.openAdd(downstreamOptions.value) - } - - const openEdit = (record: SubscribeItem): void => { - addOrEditRef.value?.openEdit(record, downstreamOptions.value) - } - - const removeRecord = (record: SubscribeItem): void => { - Modal.confirm({ - title: '确认删除?', - content: `将删除映射 [${record.id}],确认继续?`, - okText: '确认删除', - okType: 'danger', - cancelText: '取消', - onOk: () => { - api.remove(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - } - }) - .catch((err: unknown) => { - console.error('删除失败:', err) - }) - }, - }) - } - - const toggleStatus = (record: SubscribeItem): void => { - const next = record.status === 'enabled' ? '禁用' : '启用' - Modal.confirm({ - title: `确认${next}?`, - content: `将${next}映射 [${record.id}],确认继续?`, - okText: `确认${next}`, - okType: 'danger', - cancelText: '取消', - onOk: () => { - api.toggleStatus(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - } - }) - .catch((err: unknown) => { - console.error('状态切换失败:', err) - }) - }, - }) - } - - const reload = (): void => { - listRequest() - } - - const handleAction = (type: string, record?: SubscribeItem): void => { - if (!record) return - if (type === 'edit') openEdit(record) - if (type === 'delete') removeRecord(record) - if (type === 'toggle') toggleStatus(record) - } - - onMounted(() => { - loadOptions() - listRequest() - }) - - return { - pageLoading, - search, - table, - downstreamOptions, - addOrEditRef, - searchQuery, - resetQuery, - dataSourceChange, - handleAction, - openAdd, - reload, - } -} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useSearch.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useSearch.ts deleted file mode 100644 index 628b330..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useSearch.ts +++ /dev/null @@ -1,12 +0,0 @@ -import type { SearchForm } from '../types' - -export const createSearchKey = (): SearchForm => ({ - downstreamId: undefined, - deviceCode: undefined, - dataType: undefined, - status: undefined, -}) - -export const useSearch = () => { - return { createSearchKey } -} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useTable.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useTable.ts deleted file mode 100644 index 3df5d97..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/init/useTable.ts +++ /dev/null @@ -1,31 +0,0 @@ -import type { TableColumnsType } from 'ant-design-vue' -import { h } from 'vue' -import { Tag } from 'ant-design-vue' -import type { DataType } from '../../../../../record/types' -import { dataTypeText } from '../../../../../record/types' -import type { Status } from '../../downstream/types' -import { statusColor, statusText } from '../../downstream/types' - -export const tableColumns: TableColumnsType = [ - { title: '映射ID', dataIndex: 'id', align: 'left', fixed: 'left', width: 110 }, - { title: '下游系统', dataIndex: 'downstreamName', align: 'left', width: 160, resizable: true, ellipsis: true }, - { title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 140, resizable: true, ellipsis: true }, - { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 160, resizable: true, ellipsis: true }, - { - title: '数据类型', - dataIndex: 'dataType', - align: 'center', - width: 100, - customRender: ({ value }: { value: DataType }) => h(Tag, { color: 'blue' }, () => dataTypeText(value)), - }, - { title: '过滤规则', dataIndex: 'filterRule', align: 'left', width: 220, resizable: true, ellipsis: true, customRender: ({ value }: { value: string }) => value ?? '—' }, - { - title: '状态', - dataIndex: 'status', - align: 'center', - width: 90, - customRender: ({ value }: { value: Status }) => h(Tag, { color: statusColor[value] }, () => statusText(value)), - }, - { title: '创建时间', dataIndex: 'createdAt', align: 'center', width: 170 }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, -] diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/subscribe.vue b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/subscribe.vue deleted file mode 100644 index 395c260..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/subscribe.vue +++ /dev/null @@ -1,76 +0,0 @@ - - - - - diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/types/index.ts deleted file mode 100644 index ff1b0f1..0000000 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/subscribe/types/index.ts +++ /dev/null @@ -1,43 +0,0 @@ -import type { BaseSearchForm } from '@pages/admin-portal/shared/types' -import type { DataType } from '../../../../../record/types' -import type { Status } from '../../downstream/types' - -/** 订阅映射列表项 */ -export interface SubscribeItem { - id: string - downstreamId: string - downstreamName: string - deviceCode: string - deviceName: string - dataType: DataType - filterRule: string - status: Status - createdAt: string -} - -/** 表单(新增/编辑) */ -export interface SubscribeForm { - id?: string - downstreamId: string - deviceCode: string - dataType: DataType - filterRule: string - status: Status -} - -/** 搜索表单 */ -export interface SearchForm extends BaseSearchForm { - downstreamId?: string - deviceCode?: string - dataType?: DataType - status?: Status -} - -/** 下游选项(用于搜索/表单的下拉) */ -export interface DownstreamOption { - label: string - value: string -} - -export { STATUS_OPTIONS, statusColor, statusText } from '../../downstream/types' -export { DATA_TYPE_OPTIONS, dataTypeText } from '../../../../../record/types' diff --git a/src/pages/admin-portal/device-center/distribute/config/config.vue b/src/pages/admin-portal/device-center/distribute/config/config.vue index 6afe451..320eca5 100644 --- a/src/pages/admin-portal/device-center/distribute/config/config.vue +++ b/src/pages/admin-portal/device-center/distribute/config/config.vue @@ -1,44 +1,113 @@ diff --git a/src/pages/admin-portal/device-center/distribute/config/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/config/init/usePage.ts new file mode 100644 index 0000000..81b0070 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/init/usePage.ts @@ -0,0 +1,172 @@ +import { onMounted, reactive, ref } from 'vue' +import type { TablePaginationConfig } from 'ant-design-vue' +import { useRouter } from 'vue-router' +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 InfoDrawer from '../component/drawer/info/info.vue' +import { customerOptions as fetchCustomerOptions, list } from '../api' +import type { ConfigItem, CustomerOption, SearchForm } from '../types' +import { createSearchKey } from './useSearch' +import { tableColumns } from './useTable' + +/** + * 分发配置主编排: + * - 标准列表 + 新增/编辑弹框 + 详情抽屉 + * - 所属客户下拉(首屏拉取一次,弹框共用) + * - 「分发记录」操作按钮跳转到 hidden 子页面 distribute/record?downstreamId=xxx + */ +export const usePage = () => { + const router = useRouter() + const { 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 customerOptions = ref([]) + const customerLoading = ref(false) + + const addOrEditRef = ref | null>(null) + const infoDrawerRef = ref | null>(null) + + /** 拉取客户下拉(复用客户档案列表接口,仅取 id/name) */ + const loadCustomerOptions = (): void => { + customerLoading.value = true + fetchCustomerOptions() + .then((res) => { + if (res.code === '00000') { + customerOptions.value = (res.data ?? []).map((c) => ({ label: c.name, value: c.id })) + } + }) + .catch((err: unknown) => { + console.error('客户下拉加载失败:', err) + }) + .finally(() => { + customerLoading.value = false + }) + } + + /** 搜索参数序列化(Dayjs → 字符串) */ + const buildSearchParams = (form: SearchForm): Record => { + const { communicateTimeRange, ...rest } = form + const params: Record = { ...rest } + if (communicateTimeRange && communicateTimeRange.length === 2) { + params.lastCommunicateTimeStart = communicateTimeRange[0]?.format('YYYY-MM-DD') + params.lastCommunicateTimeEnd = communicateTimeRange[1]?.format('YYYY-MM-DD') + } + return params + } + + const listRequest = (): void => { + pageLoading.value = true + list(buildSearchParams(search) as SearchForm) + .then((res) => { + if (res.code === '00000') { + table.dataSource = res.data ?? [] + table.pagination.total = table.dataSource.length + } + }) + .catch((err: unknown) => { + console.error('分发配置列表请求失败:', 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 openAddModal = (): void => { + addOrEditRef.value?.openModal('add') + } + + /** 打开编辑弹框 */ + const openEditModal = (record: ConfigItem): void => { + addOrEditRef.value?.openModal('edit', { id: record.id }) + } + + /** 打开详情抽屉 */ + const openInfoDrawer = (record: ConfigItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id }) + } + + /** 跳转分发记录子页面(携带下游系统维度过滤参数) */ + const openRecordPage = (record: ConfigItem): void => { + router.push({ + path: '/admin-portal/device-center/distribute/record', + query: { + downstreamId: record.id, + downstreamName: record.name, + }, + }) + } + + /** 切换启用状态 */ + const toggleStatus = (record: ConfigItem): void => { + const next = record.status === 'enabled' ? '停用' : '启用' + Modal.confirm({ + title: `确认${next}?`, + content: `将${next}下游系统 [${record.name}],确认继续?`, + okText: `确认${next}`, + okType: 'danger', + cancelText: '取消', + }) + } + + const handleAction = (type: string, record?: ConfigItem): void => { + if (type === 'add') { + openAddModal() + return + } + if (!record) return + if (type === 'edit') openEditModal(record) + if (type === 'detail') openInfoDrawer(record) + if (type === 'record') openRecordPage(record) + if (type === 'toggle') toggleStatus(record) + } + + onMounted(() => { + loadCustomerOptions() + listRequest() + }) + + return { + pageLoading, + search, + table, + customerOptions, + customerLoading, + addOrEditRef, + infoDrawerRef, + searchQuery, + resetQuery, + dataSourceChange, + handleAction, + listRequest, + } +} diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useSearch.ts b/src/pages/admin-portal/device-center/distribute/config/init/useSearch.ts similarity index 61% rename from src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useSearch.ts rename to src/pages/admin-portal/device-center/distribute/config/init/useSearch.ts index 3578eed..aab75a5 100644 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/distribute/config/init/useSearch.ts @@ -2,12 +2,9 @@ import type { SearchForm } from '../types' /** 搜索表单工厂:统一重置入口 */ export const createSearchKey = (): SearchForm => ({ + customerId: undefined, name: undefined, - authType: undefined, - retryStrategy: undefined, status: undefined, + authEnabled: undefined, + communicateTimeRange: undefined, }) - -export const useSearch = () => { - return { createSearchKey } -} diff --git a/src/pages/admin-portal/device-center/distribute/config/init/useTable.ts b/src/pages/admin-portal/device-center/distribute/config/init/useTable.ts new file mode 100644 index 0000000..2f48fa1 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/init/useTable.ts @@ -0,0 +1,47 @@ +import type { TableColumnsType } from 'ant-design-vue' +import { h } from 'vue' +import { Tag } from 'ant-design-vue' +import type { DispatchMode, Status } from '../types' +import { dispatchModeColor, dispatchModeText, statusColor, statusText } from '../types' + +/** 分发配置列定义 */ +export const tableColumns: TableColumnsType = [ + { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, + { title: '下游系统名称', dataIndex: 'name', align: 'left', width: 160, resizable: true, ellipsis: true }, + { title: '系统标识', dataIndex: 'code', align: 'left', width: 140, ellipsis: true }, + { title: '所属客户', dataIndex: 'customerName', align: 'left', width: 140, ellipsis: true }, + { title: '数据分发地址', dataIndex: 'callbackUrl', align: 'left', width: 240, resizable: true, ellipsis: true }, + { + title: '分发模式', + dataIndex: 'dispatchMode', + align: 'center', + width: 100, + customRender: ({ value }: { value: DispatchMode }) => + h(Tag, { color: dispatchModeColor[value] }, () => dispatchModeText(value)), + }, + { + title: '鉴权开关', + dataIndex: 'authEnabled', + align: 'center', + width: 100, + customRender: ({ value }: { value: boolean }) => + h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已开启' : '已关闭')), + }, + { + title: '状态', + dataIndex: 'status', + align: 'center', + width: 90, + customRender: ({ value }: { value: Status }) => + h(Tag, { color: statusColor[value] }, () => statusText(value)), + }, + { + title: '覆盖设备', + dataIndex: 'deviceCategoryNames', + align: 'left', + width: 220, + }, + { title: '最大重试', dataIndex: 'maxRetry', align: 'center', width: 90 }, + { title: '最新通讯时间', dataIndex: 'lastCommunicateTime', 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/distribute/config/types/index.ts b/src/pages/admin-portal/device-center/distribute/config/types/index.ts new file mode 100644 index 0000000..4d69b44 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/config/types/index.ts @@ -0,0 +1,124 @@ +/** + * 数据分发 - 分发配置 类型 + */ +import type { Dayjs } from 'dayjs' +import type { BaseSearchForm } from '@pages/admin-portal/shared/types' +import type { DeviceCategoryTreeNode } from '@api' + +/** 分发模式:实时 / 轮询 */ +export type DispatchMode = 'realtime' | 'polling' + +/** 启用状态 */ +export type Status = 'enabled' | 'disabled' + +/** 列表项 */ +export interface ConfigItem { + id: string + /** 下游系统名称 */ + name: string + /** 系统标识 */ + code: string + /** 所属客户 ID */ + customerId: string + /** 所属客户名称 */ + customerName: string + /** 数据分发地址 */ + callbackUrl: string + /** 分发模式 */ + dispatchMode: DispatchMode + /** 鉴权开关 */ + authEnabled: boolean + /** 鉴权账号(authEnabled=true 时必填) */ + authAccount?: string + /** 鉴权秘钥(authEnabled=true 时必填) */ + authSecret?: string + /** 鉴权接口地址(authEnabled=true 时必填) */ + authApiUrl?: string + /** 状态 */ + status: Status + /** 覆盖设备(设备分类 ID 多选) */ + deviceCategoryIds: string[] + /** 覆盖设备名称(列表展示用,由后端 join) */ + deviceCategoryNames?: string[] + /** 最大重试次数 */ + maxRetry: number + /** 最新通讯时间 */ + lastCommunicateTime?: string + /** 备注 */ + remark?: string + /** 创建时间 */ + createTime?: string +} + +/** 新增/编辑提交载荷 */ +export interface ConfigPayload { + id?: string + name: string + code: string + customerId: string + callbackUrl: string + dispatchMode: DispatchMode + authEnabled: boolean + authAccount?: string + authSecret?: string + authApiUrl?: string + status: Status + deviceCategoryIds: string[] + maxRetry: number + remark?: string +} + +/** 搜索表单 */ +export interface SearchForm extends BaseSearchForm { + /** 所属客户 */ + customerId?: string + /** 下游系统名称 */ + name?: string + /** 状态 */ + status?: Status + /** 鉴权开关(undefined=全部) */ + authEnabled?: boolean + /** 最新通讯时间范围(Dayjs) */ + communicateTimeRange?: [Dayjs, Dayjs] +} + +/** 分发模式下拉选项 */ +export const DISPATCH_MODE_OPTIONS: { label: string; value: DispatchMode }[] = [ + { label: '实时', value: 'realtime' }, + { label: '轮询', value: 'polling' }, +] + +/** 启用状态下拉选项 */ +export const STATUS_OPTIONS: { label: string; value: Status }[] = [ + { label: '启用', value: 'enabled' }, + { label: '停用', value: 'disabled' }, +] + +/** 分发模式 → 中文 */ +export const dispatchModeText = (value: DispatchMode | string | undefined): string => + DISPATCH_MODE_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' + +/** 启用状态 → 中文 */ +export const statusText = (value: Status | string | undefined): string => + STATUS_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' + +/** 分发模式 → Tag 颜色 */ +export const dispatchModeColor: Record = { + realtime: 'processing', + polling: 'warning', +} + +/** 启用状态 → Tag 颜色 */ +export const statusColor: Record = { + enabled: 'success', + disabled: 'default', +} + +/** 客户下拉选项(列表页 / 弹框共用) */ +export interface CustomerOption { + label: string + value: string +} + +/** 设备分类树节点(与 @api 对齐) */ +export type CategoryTreeNode = DeviceCategoryTreeNode diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/api/index.ts b/src/pages/admin-portal/device-center/distribute/deadletter/api/index.ts deleted file mode 100644 index e263e75..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/api/index.ts +++ /dev/null @@ -1,34 +0,0 @@ -/** - * 失败重投 - 接口层 - */ -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { DeadletterDetail, DeadletterItem, SearchForm, StatCards } from '../types' - -/** 列表查询 */ -export const list = (params: SearchForm): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/list', params) - -/** 详情查询 */ -export const info = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/info', { id }) - -/** 顶部统计卡 */ -export const stat = (): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/stat', {}) - -/** 手动重投(单条) */ -export const retry = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/retry', { id }) - -/** 批量重投 */ -export const batchRetry = (ids: string[]): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/batchRetry', { ids }) - -/** 永久丢弃(单条) */ -export const discard = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/discard', { id }) - -/** 批量丢弃 */ -export const batchDiscard = (ids: string[]): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/batchDiscard', { ids }) diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/info.vue deleted file mode 100644 index 72e5089..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/info.vue +++ /dev/null @@ -1,99 +0,0 @@ - - - - - diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/init/usePage.ts deleted file mode 100644 index 9e9cc70..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/init/usePage.ts +++ /dev/null @@ -1,43 +0,0 @@ -/** - * 死信详情抽屉 - hook - */ -import { reactive, ref } from 'vue' -import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import * as api from '../api' -import type { DetailInfo, OpenContext, PageInfo } from '../types' - -export const usePage = () => { - const { message } = useAntdStaticMethods() - - const pageInfo = reactive({ - visible: false, - title: '死信详情', - width: 720, - spin: false, - }) - - const detail = ref(null) - - const openDrawer = (ctx: OpenContext) => { - pageInfo.title = ctx.title ?? '死信详情' - pageInfo.visible = true - pageInfo.spin = true - detail.value = null - api.getInfo(ctx.id) - .then((res) => { - if (res.code === '00000') { - detail.value = res.data - } else { - message.error(res.msg || '查询失败') - } - }) - .catch(() => { message.error('查询失败') }) - .finally(() => { pageInfo.spin = false }) - } - - const closeDrawer = () => { - pageInfo.visible = false - } - - return { pageInfo, detail, openDrawer, closeDrawer } -} diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/types/index.ts deleted file mode 100644 index 90d2c18..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/types/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 死信详情抽屉 - 类型 - */ -import type { DeadletterDetail } from '../../../../types' - -export interface PageInfo { - visible: boolean - title: string - width: number - spin: boolean -} - -export interface OpenContext { - id: string - title?: string -} - -export type DetailInfo = DeadletterDetail diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/deadletter.vue b/src/pages/admin-portal/device-center/distribute/deadletter/deadletter.vue deleted file mode 100644 index e0ba582..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/deadletter.vue +++ /dev/null @@ -1,134 +0,0 @@ - - - - - diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/deadletter/init/usePage.ts deleted file mode 100644 index eda7044..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/init/usePage.ts +++ /dev/null @@ -1,252 +0,0 @@ -/** - * 失败重投 - 主 hook - * - 列表查询 / 分页 / 排序 - * - 详情抽屉 - * - 手动重投(单条/批量) - * - 永久丢弃(单条/批量,二次确认) - */ -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 InfoDrawer from '../component/drawer/info/info.vue' -import { batchDiscard as batchDiscardApi, batchRetry as batchRetryApi, discard, list, retry, stat } from '../api' -import type { DeadletterItem, SearchForm, StatCards } from '../types' -import { createSearchKey } from './useSearch' -import { tableColumns } from './useTable' - -/** - * 失败重投页主编排: - * - 顶部统计卡(待重投 / 重投中 / 已恢复 / 已丢弃) - * - 策略说明条(指数退避) - * - 标准列表 + 详情抽屉(请求体/响应体) - * - 手动重投 / 永久丢弃(单条/批量) - */ -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)[]>([]) - - /** 顶部统计卡数据 */ - const statCards = ref({ - pendingCount: 0, - retryingCount: 0, - recoveredCount: 0, - discardedCount: 0, - }) - - /** 详情抽屉 ref */ - const infoDrawerRef = 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('死信队列列表请求失败:', err) - }) - .finally(() => { - pageLoading.value = false - }) - } - - const statRequest = (): void => { - stat() - .then((res) => { - if (res.code === '00000' && res.data) { - statCards.value = res.data - } - }) - .catch((err: unknown) => { - console.error('死信统计请求失败:', err) - }) - } - - 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 openInfoDrawer = (record: DeadletterItem): void => { - infoDrawerRef.value?.openDrawer({ id: record.id, title: '死信详情' }) - } - - /** 手动重投(单条) */ - const retryRecord = (record: DeadletterItem): void => { - Modal.confirm({ - title: '确认重投?', - content: `死信 [${record.id}] 将立即触发一次手动重投,确认继续?`, - okText: '确认重投', - okType: 'danger', - cancelText: '取消', - onOk: () => { - retry(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - statRequest() - } - }) - .catch((err: unknown) => { - console.error('重投失败:', err) - }) - }, - }) - } - - /** 永久丢弃(单条) */ - const discardRecord = (record: DeadletterItem): void => { - Modal.confirm({ - title: '永久丢弃?', - content: `丢弃后死信 [${record.id}] 将不再自动重投,且无法恢复,确认继续?`, - okText: '确认丢弃', - okType: 'danger', - cancelText: '取消', - onOk: () => { - discard(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - statRequest() - } - }) - .catch((err: unknown) => { - console.error('丢弃失败:', err) - }) - }, - }) - } - - /** 批量重投 */ - const batchRetry = (): void => { - if (selectedRowKeys.value.length === 0) { - void message.warning('请先勾选要重投的死信') - return - } - const ids = selectedRowKeys.value.map((key) => String(key)) - Modal.confirm({ - title: '确认批量重投?', - content: `将立即重投 ${ids.length} 条死信,确认继续?`, - okText: '确认重投', - okType: 'danger', - cancelText: '取消', - onOk: () => { - batchRetryApi(ids) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - selectedRowKeys.value = [] - listRequest() - statRequest() - } - }) - .catch((err: unknown) => { - console.error('批量重投失败:', err) - }) - }, - }) - } - - /** 批量丢弃 */ - const batchDiscard = (): void => { - if (selectedRowKeys.value.length === 0) { - void message.warning('请先勾选要丢弃的死信') - return - } - const ids = selectedRowKeys.value.map((key) => String(key)) - Modal.confirm({ - title: '确认批量丢弃?', - content: `将永久丢弃 ${ids.length} 条死信,此操作不可恢复,确认继续?`, - okText: '确认丢弃', - okType: 'danger', - cancelText: '取消', - onOk: () => { - batchDiscardApi(ids) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - selectedRowKeys.value = [] - listRequest() - statRequest() - } - }) - .catch((err: unknown) => { - console.error('批量丢弃失败:', err) - }) - }, - }) - } - - const handleAction = (type: string, record?: DeadletterItem): void => { - if (type === 'detail' && record) { - openInfoDrawer(record) - return - } - if (type === 'retry' && record) { - retryRecord(record) - return - } - if (type === 'discard' && record) { - discardRecord(record) - } - } - - onMounted(() => { - listRequest() - statRequest() - }) - - return { - pageLoading, - search, - table, - selectedRowKeys, - rowSelection, - statCards, - infoDrawerRef, - searchQuery, - resetQuery, - dataSourceChange, - handleAction, - listRequest, - batchRetry, - batchDiscard, - } -} diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/init/useSearch.ts b/src/pages/admin-portal/device-center/distribute/deadletter/init/useSearch.ts deleted file mode 100644 index 0033ef8..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/init/useSearch.ts +++ /dev/null @@ -1,13 +0,0 @@ -import type { SearchForm } from '../types' - -/** 搜索表单工厂:统一重置入口 */ -export const createSearchKey = (): SearchForm => ({ - deviceCode: undefined, - downstreamId: undefined, - dataType: undefined, - status: undefined, -}) - -export const useSearch = () => { - return { createSearchKey } -} diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/init/useTable.ts b/src/pages/admin-portal/device-center/distribute/deadletter/init/useTable.ts deleted file mode 100644 index 26a82d2..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/init/useTable.ts +++ /dev/null @@ -1,46 +0,0 @@ -import type { TableColumnsType } from 'ant-design-vue' -import { h } from 'vue' -import { Tag } from 'ant-design-vue' -import type { DataType } from '../../record/types' -import { dataTypeText } from '../../record/types' -import type { DeadletterStatus } from '../types' -import { deadletterStatusColor, deadletterStatusText } from '../types' - -/** 死信队列列定义 */ -export const tableColumns: TableColumnsType = [ - { title: '死信ID', dataIndex: 'id', align: 'left', fixed: 'left', width: 110 }, - { title: '原分发记录', dataIndex: 'originalRecordId', align: 'left', width: 120, ellipsis: true }, - { title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 140, resizable: true, ellipsis: true }, - { title: '设备名称', dataIndex: 'deviceName', align: 'left', width: 150, resizable: true, ellipsis: true }, - { - title: '数据类型', - dataIndex: 'dataType', - align: 'center', - width: 100, - customRender: ({ value }: { value: DataType }) => h(Tag, { color: 'blue' }, () => dataTypeText(value)), - }, - { title: '下游系统', dataIndex: 'downstreamName', align: 'left', width: 140, resizable: true, ellipsis: true }, - { title: '首次失败', dataIndex: 'firstFailTime', align: 'center', width: 170, sorter: true }, - { title: '最近重投', dataIndex: 'lastRetryTime', align: 'center', width: 170, customRender: ({ value }: { value: string | null }) => value ?? '—' }, - { - title: '重投次数', - dataIndex: 'retryCount', - align: 'center', - width: 90, - customRender: ({ value }: { value: number }) => { - const color = value >= 4 ? 'red' : value >= 2 ? 'orange' : 'default' - return h(Tag, { color }, () => `${value} / 5`) - }, - }, - { title: '下次重投', dataIndex: 'nextRetryTime', align: 'center', width: 170, customRender: ({ value }: { value: string | null }) => value ?? '—' }, - { title: '失败原因', dataIndex: 'failReason', align: 'left', width: 200, resizable: true, ellipsis: true }, - { - title: '状态', - dataIndex: 'status', - align: 'center', - width: 100, - customRender: ({ value }: { value: DeadletterStatus }) => - h(Tag, { color: deadletterStatusColor[value] }, () => deadletterStatusText(value)), - }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 240 }, -] diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/types/index.ts b/src/pages/admin-portal/device-center/distribute/deadletter/types/index.ts deleted file mode 100644 index 317d58c..0000000 --- a/src/pages/admin-portal/device-center/distribute/deadletter/types/index.ts +++ /dev/null @@ -1,86 +0,0 @@ -/** - * 失败重投 - 死信队列类型定义 - * - 记录分发失败进入死信队列的样本 - * - 自动重投策略:指数退避(1min / 5min / 30min / 2h / 12h),最多 5 次后转人工 - * - 状态机:pending(待重投)→ retrying(重投中)→ recovered(已恢复)/ discarded(已丢弃) - */ -import type { BaseSearchForm } from '@pages/admin-portal/shared/types' -import type { DataType } from '../../record/types' - -/** 死信状态 */ -export type DeadletterStatus = 'pending' | 'retrying' | 'recovered' | 'discarded' - -/** 死信记录(列表项) */ -export interface DeadletterItem { - id: string - originalRecordId: string - deviceCode: string - deviceName: string - dataType: DataType - downstreamId: string - downstreamName: string - firstFailTime: string - lastRetryTime: string | null - retryCount: number - nextRetryTime: string | null - failReason: string - status: DeadletterStatus -} - -/** 死信详情(含请求体/响应体) */ -export interface DeadletterDetail extends DeadletterItem { - requestBody: string - responseBody: string - authKey: string - strategy: string - thresholdWarn: string -} - -/** 顶部统计卡 */ -export interface StatCards { - pendingCount: number - retryingCount: number - recoveredCount: number - discardedCount: number -} - -/** 搜索表单 */ -export interface SearchForm extends BaseSearchForm { - deviceCode?: string - dataType?: DataType - downstreamId?: string - status?: DeadletterStatus -} - -/** 死信状态选项 */ -export const DEADLETTER_STATUS_OPTIONS = [ - { label: '待重投', value: 'pending' }, - { label: '重投中', value: 'retrying' }, - { label: '已恢复', value: 'recovered' }, - { label: '已丢弃', value: 'discarded' }, -] as const - -/** 复用 record 的数据类型选项 + 文本字典 */ -export { DATA_TYPE_OPTIONS, dataTypeText } from '../../record/types' - -/** 死信状态 → Tag 颜色 */ -export const deadletterStatusColor: Record = { - pending: 'warning', - retrying: 'processing', - recovered: 'success', - discarded: 'default', -} - -/** 死信状态 → 中文文本 */ -export const deadletterStatusText = (status: DeadletterStatus): string => { - const map: Record = { - pending: '待重投', - retrying: '重投中', - recovered: '已恢复', - discarded: '已丢弃', - } - return map[status] ?? '—' -} - -/** 指数退避策略说明(展示用) */ -export const RETRY_STRATEGY_TEXT = '指数退避:1min → 5min → 30min → 2h → 12h,最多 5 次后转人工' diff --git a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/api/index.ts deleted file mode 100644 index 5980b07..0000000 --- a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/api/index.ts +++ /dev/null @@ -1,11 +0,0 @@ -/** - * 分发记录 - 详情抽屉 - 接口层 - */ - -import type { ApiResponse } from '@axios' -import { postRequest } from '@axios' -import type { DetailInfo } from '../../../../types' - -/** 详情查询(含请求体/响应体) */ -export const getInfo = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/record/info', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/report/api/index.ts b/src/pages/admin-portal/device-center/distribute/report/api/index.ts new file mode 100644 index 0000000..14f6866 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/api/index.ts @@ -0,0 +1,39 @@ +/** + * 数据分发 - 上报配置 接口层 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { CustomerOption, ReportItem, ReportPayload, SearchForm, SiteOption } from '../types' + +const BASE = '/admin/device/distribute/report' + +/** 列表查询 */ +export const list = (params: SearchForm): Promise> => + postRequest('axiosRequest', BASE + '/list', params) + +/** 详情查询 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) + +/** 新增 */ +export const add = (params: ReportPayload): Promise> => + postRequest('axiosRequest', BASE + '/add', params) + +/** 编辑 */ +export const update = (params: ReportPayload): Promise> => + postRequest('axiosRequest', BASE + '/update', params) + +/** + * 客户下拉选项(复用客户档案列表接口,仅取 id/name) + * - 路径与 @pages/admin-portal/device-center/customer/archive/api 一致 + */ +export const customerOptions = (): Promise> => + postRequest('axiosRequest', '/admin/device/customer/archive/page', {}) + +/** + * 布点场所下拉选项(复用布点场所列表接口,可按 customerId 联动过滤) + * - 路径与 @pages/admin-portal/device-center/customer/site/api 一致 + * - 传 customerId 时仅返回该客户的场所;不传时返回全部 + */ +export const siteOptions = (customerId?: string): Promise> => + postRequest('axiosRequest', '/admin/device/site/list', { customerId, pageNum: 1, pageSize: 200 }) diff --git a/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/api/index.ts new file mode 100644 index 0000000..d4d8903 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/api/index.ts @@ -0,0 +1,12 @@ +/** + * 上报配置 - 详情抽屉 - 接口层 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { DetailInfo } from '../types' + +const BASE = '/admin/device/distribute/report' + +/** 详情查询 */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) diff --git a/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/info.vue new file mode 100644 index 0000000..379a3bc --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/info.vue @@ -0,0 +1,57 @@ + + + diff --git a/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/init/usePage.ts new file mode 100644 index 0000000..28318bd --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/init/usePage.ts @@ -0,0 +1,47 @@ +/** + * 上报配置 - 详情抽屉 - 主逻辑 + */ +import { info } from '../api' +import type { DetailInfo, OpenContext, PageInfo } from '../types' + +export const usePage = () => { + const pageInfo = reactive({ + visible: false, + title: '上报配置详情', + width: 760, + spin: false, + }) + + const detail = ref(null) + + const closeDrawer = (): void => { + pageInfo.visible = false + detail.value = null + } + + const openDrawer = (ctx: OpenContext): void => { + pageInfo.visible = true + pageInfo.spin = true + detail.value = null + + info(ctx.id) + .then((res) => { + if (res.code === '00000' && res.data) { + detail.value = res.data + } + }) + .catch((err: unknown) => { + console.error('上报配置详情查询失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + return { + pageInfo, + detail, + openDrawer, + closeDrawer, + } +} diff --git a/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/types/index.ts new file mode 100644 index 0000000..1ee71bf --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/drawer/info/types/index.ts @@ -0,0 +1,17 @@ +import type { ReportItem } from '../../../types' + +/** 抽屉页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 打开抽屉上下文 */ +export interface OpenContext { + id: string +} + +/** 详情数据(与列表项 ReportItem 同构) */ +export type DetailInfo = ReportItem diff --git a/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/addOrEdit.vue new file mode 100644 index 0000000..6d94180 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/addOrEdit.vue @@ -0,0 +1,213 @@ + + + diff --git a/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/api/index.ts new file mode 100644 index 0000000..3cabb5f --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/api/index.ts @@ -0,0 +1,21 @@ +/** + * 上报配置 - 新增/编辑 弹框 - 接口层 + * - 复用主页的 info / add / update + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { Form } from '../types' + +const BASE = '/admin/device/distribute/report' + +/** 详情查询(编辑回填) */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) + +/** 新增 */ +export const add = (params: Record): Promise> => + postRequest('axiosRequest', BASE + '/add', params) + +/** 编辑 */ +export const edit = (params: Record): Promise> => + postRequest('axiosRequest', BASE + '/update', params) diff --git a/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/init/usePage.ts new file mode 100644 index 0000000..372b756 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/init/usePage.ts @@ -0,0 +1,273 @@ +/** + * 上报配置 - 新增/编辑 弹框 - 主逻辑 + * + * 严格遵循 modal-spec.md: + * - createInitForm 工厂函数 + * - openModal 用 Promise.all 拉取详情 + 客户下拉 + 设备分类树 + * - addRequest 必须 delete params.id + * - submit 开头立即 pageInfo.spin = true + * - 用 params.id 判断新增/编辑 + * - 客户 → 场所联动:选客户后加载该客户场所;切换客户清空 siteId + * - 鉴权开关联动:关闭时清空 鉴权账号/秘钥/接口地址 且不校验 + */ +import type { FormInstance, Rule } from 'ant-design-vue/es/form' +import { onMounted, ref } from 'vue' +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { deviceCategoryTree } from '@api' +import type { DeviceCategoryTreeNode } from '@api' +import { customerOptions as fetchCustomerOptions, siteOptions as fetchSiteOptions } from '../../../../api' +import { add, edit, info } from '../api' +import type { CustomerOption, Form, ModalType, OpenContext, PageInfo, SiteOption } from '../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + id: undefined, + channelName: '', + channelCode: '', + customerId: undefined, + siteId: undefined, + authEnabled: false, + authAccount: '', + authSecret: '', + authApiUrl: '', + status: 'enabled', + mqAddress: '', + gatewayUrl: '', + deviceCategoryIds: [], + remark: '', +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + type: 'add', + title: '', + width: 820, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + + /** 客户下拉选项 */ + const customerOptions = ref([]) + /** 场所下拉选项(按当前 customerId 联动) */ + const siteOptions = ref([]) + const siteLoading = ref(false) + /** 设备分类树(a-tree-select 数据源) */ + const categoryTree = ref([]) + /** a-tree-select 字段映射(与设备分类树节点字段对齐) */ + const treeFieldNames = { label: 'name', value: 'id', children: 'children' } + + const rules: Record = { + channelName: [{ required: true, message: '请输入上报通道名称', trigger: 'blur' }], + channelCode: [{ required: true, message: '请输入通道编码', trigger: 'blur' }], + customerId: [{ required: true, message: '请选择所属客户', trigger: 'change' }], + siteId: [{ required: true, message: '请选择布点场所', trigger: 'change' }], + status: [{ required: true, message: '请选择状态', trigger: 'change' }], + mqAddress: [{ required: true, message: '请输入 MQ 通讯地址', trigger: 'blur' }], + gatewayUrl: [{ required: true, message: '请输入服务网关地址', trigger: 'blur' }], + } + + /** 鉴权开关联动:开启时鉴权字段必填 */ + const authFieldRules = (): Rule[] => + [{ + required: true, + message: '鉴权开启时必填', + trigger: 'blur', + validator: (_rule: unknown, value: string) => { + if (form.value.authEnabled && !value) return Promise.reject(new Error('鉴权开启时必填')) + return Promise.resolve() + }, + }] as Rule[] + + const rulesWithAuth: Record = { + ...rules, + authAccount: authFieldRules(), + authSecret: authFieldRules(), + authApiUrl: authFieldRules(), + } + + /** 拉取客户下拉 */ + const loadCustomerOptions = (): void => { + fetchCustomerOptions() + .then((res) => { + if (res.code === '00000') { + customerOptions.value = (res.data ?? []).map((c) => ({ label: c.name, value: c.id })) + } + }) + .catch((err: unknown) => { + console.error('客户下拉加载失败:', err) + }) + } + + /** 拉取场所下拉(按 customerId 联动) */ + const loadSiteOptions = (customerId?: string): void => { + if (!customerId) { + siteOptions.value = [] + return + } + siteLoading.value = true + fetchSiteOptions(customerId) + .then((res) => { + if (res.code === '00000') { + siteOptions.value = (res.data ?? []).map((s) => ({ label: s.name, value: s.id, customerId: s.customerId })) + } + }) + .catch((err: unknown) => { + console.error('场所下拉加载失败:', err) + }) + .finally(() => { + siteLoading.value = false + }) + } + + /** 表单客户切换:清空 siteId 并重拉场所 */ + const onCustomerChange = (value: string | undefined): void => { + form.value.siteId = undefined + loadSiteOptions(value) + } + + /** 拉取设备分类树 */ + const loadCategoryTree = (): void => { + deviceCategoryTree() + .then((res) => { + if (res.code === '00000') { + categoryTree.value = res.data ?? [] + } + }) + .catch((err: unknown) => { + console.error('加载设备分类树失败:', err) + }) + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + siteOptions.value = [] + } + + 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' ? '新增上报配置' : '编辑上报配置' + + /** 客户下拉 / 设备分类树为空时补拉一次 */ + if (!customerOptions.value.length) loadCustomerOptions() + if (!categoryTree.value.length) loadCategoryTree() + + 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 } + /** 编辑回填后按当前 customerId 拉一次场所下拉,确保 siteId 能正常展示 */ + if (form.value.customerId) loadSiteOptions(form.value.customerId) + } + } + }) + .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 + /** 鉴权关闭时清空鉴权字段,避免脏数据 */ + if (!params.authEnabled) { + params.authAccount = '' + params.authSecret = '' + params.authApiUrl = '' + } + 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 => { + if (!params.authEnabled) { + params.authAccount = '' + params.authSecret = '' + params.authApiUrl = '' + } + 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) + }) + } + + onMounted(() => { + loadCustomerOptions() + loadCategoryTree() + }) + + return { + pageInfo, + formRef, + form, + rules: rulesWithAuth, + customerOptions, + siteOptions, + siteLoading, + categoryTree, + treeFieldNames, + openModal, + closeModal, + submit, + onCustomerChange, + } +} diff --git a/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/types/index.ts new file mode 100644 index 0000000..906cbed --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/component/modal/addOrEdit/types/index.ts @@ -0,0 +1,58 @@ +import type { DeviceCategoryTreeNode } from '@api' +import type { Status } from '../../../types' +import type { CustomerOption, SiteOption } 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 +} + +/** 表单字段(对齐 ReportPayload) */ +export interface Form { + id: string | undefined + /** 上报通道名称 */ + channelName: string + /** 通道编码(编辑时禁用) */ + channelCode: string + /** 所属客户 ID */ + customerId: string | undefined + /** 布点场所 ID */ + siteId: string | undefined + /** 鉴权开关 */ + authEnabled: boolean + /** 鉴权账号 */ + authAccount: string + /** 鉴权秘钥 */ + authSecret: string + /** 鉴权接口地址 */ + authApiUrl: string + /** 状态 */ + status: Status + /** MQ 通讯地址 */ + mqAddress: string + /** 服务网关地址 */ + gatewayUrl: string + /** 覆盖设备(设备分类 ID 多选) */ + deviceCategoryIds: string[] + /** 备注 */ + remark: string +} + +/** 客户下拉选项(与主页共享类型) */ +export type { CustomerOption, SiteOption } + +/** 设备分类树节点(与 @api 对齐) */ +export type CategoryTreeNode = DeviceCategoryTreeNode diff --git a/src/pages/admin-portal/device-center/distribute/report/init/usePage.ts b/src/pages/admin-portal/device-center/distribute/report/init/usePage.ts new file mode 100644 index 0000000..44add87 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/init/usePage.ts @@ -0,0 +1,166 @@ +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 InfoDrawer from '../component/drawer/info/info.vue' +import { customerOptions as fetchCustomerOptions, list, siteOptions as fetchSiteOptions } from '../api' +import type { CustomerOption, ReportItem, SearchForm, SiteOption } from '../types' +import { createSearchKey } from './useSearch' +import { tableColumns } from './useTable' + +/** + * 上报配置主编排: + * - 标准列表 + 新增/编辑弹框 + 详情抽屉 + * - 客户下拉首屏拉一次,场所下拉随客户联动(列表搜索 + 弹框共用本 hook 的下拉数据) + * - 切换客户时清空已选 siteId,避免脏数据 + */ +export const usePage = () => { + const { 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 customerOptions = ref([]) + const customerLoading = ref(false) + /** 场所下拉选项(按当前 customerId 联动) */ + const siteOptions = ref([]) + const siteLoading = ref(false) + + const addOrEditRef = ref | null>(null) + const infoDrawerRef = ref | null>(null) + + /** 拉取客户下拉(复用客户档案列表接口,仅取 id/name) */ + const loadCustomerOptions = (): void => { + customerLoading.value = true + fetchCustomerOptions() + .then((res) => { + if (res.code === '00000') { + customerOptions.value = (res.data ?? []).map((c) => ({ label: c.name, value: c.id })) + } + }) + .catch((err: unknown) => { + console.error('客户下拉加载失败:', err) + }) + .finally(() => { + customerLoading.value = false + }) + } + + /** 拉取场所下拉(按 customerId 联动;不传时返回全部场所) */ + const loadSiteOptions = (customerId?: string): void => { + siteLoading.value = true + fetchSiteOptions(customerId) + .then((res) => { + if (res.code === '00000') { + siteOptions.value = (res.data ?? []).map((s) => ({ label: s.name, value: s.id, customerId: s.customerId })) + } + }) + .catch((err: unknown) => { + console.error('场所下拉加载失败:', err) + }) + .finally(() => { + siteLoading.value = false + }) + } + + /** 搜索区客户切换:联动重拉场所 + 清空已选 siteId */ + const onCustomerChange = (value: string | undefined): void => { + search.siteId = undefined + loadSiteOptions(value) + } + + 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('上报配置列表请求失败:', err) + }) + .finally(() => { + pageLoading.value = false + }) + } + + const searchQuery = (): void => { + table.pagination.current = 1 + listRequest() + } + + const resetQuery = (): void => { + Object.assign(search, createSearchKey()) + table.pagination.current = 1 + /** 重置后场所下拉回到全量 */ + loadSiteOptions() + listRequest() + } + + const dataSourceChange = (pagination: TablePaginationConfig): void => { + table.pagination.current = pagination.current ?? 1 + table.pagination.pageSize = pagination.pageSize ?? 30 + } + + /** 打开新增弹框 */ + const openAddModal = (): void => { + addOrEditRef.value?.openModal('add') + } + + /** 打开编辑弹框 */ + const openEditModal = (record: ReportItem): void => { + addOrEditRef.value?.openModal('edit', { id: record.id }) + } + + /** 打开详情抽屉 */ + const openInfoDrawer = (record: ReportItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id }) + } + + const handleAction = (type: string, record?: ReportItem): void => { + if (type === 'add') { + openAddModal() + return + } + if (!record) return + if (type === 'edit') openEditModal(record) + if (type === 'detail') openInfoDrawer(record) + } + + onMounted(() => { + loadCustomerOptions() + loadSiteOptions() + listRequest() + }) + + return { + pageLoading, + search, + table, + customerOptions, + customerLoading, + siteOptions, + siteLoading, + addOrEditRef, + infoDrawerRef, + searchQuery, + resetQuery, + dataSourceChange, + onCustomerChange, + handleAction, + listRequest, + } +} diff --git a/src/pages/admin-portal/device-center/distribute/record/init/useSearch.ts b/src/pages/admin-portal/device-center/distribute/report/init/useSearch.ts similarity index 64% rename from src/pages/admin-portal/device-center/distribute/record/init/useSearch.ts rename to src/pages/admin-portal/device-center/distribute/report/init/useSearch.ts index 69a4a88..2c8452d 100644 --- a/src/pages/admin-portal/device-center/distribute/record/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/distribute/report/init/useSearch.ts @@ -2,8 +2,9 @@ import type { SearchForm } from '../types' /** 搜索表单工厂:统一重置入口 */ export const createSearchKey = (): SearchForm => ({ - deviceCode: undefined, - downstreamId: undefined, - dataType: undefined, + customerId: undefined, + siteId: undefined, + name: undefined, status: undefined, + authEnabled: undefined, }) diff --git a/src/pages/admin-portal/device-center/distribute/report/init/useTable.ts b/src/pages/admin-portal/device-center/distribute/report/init/useTable.ts new file mode 100644 index 0000000..fd727b4 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/init/useTable.ts @@ -0,0 +1,44 @@ +import type { TableColumnsType } from 'ant-design-vue' +import { h } from 'vue' +import { Tag } from 'ant-design-vue' +import type { Status } from '../types' +import { statusColor, statusText } from '../types' + +/** 上报配置列定义(字段顺序对齐产品需求) */ +export const tableColumns: TableColumnsType = [ + { title: '序号', dataIndex: 'index', align: 'center', fixed: 'left', width: 70, customRender: ({ index }: { index: number }) => index + 1 }, + { title: '上报通道名称', dataIndex: 'channelName', align: 'left', width: 180, resizable: true, ellipsis: true }, + { title: '通道编码', dataIndex: 'channelCode', align: 'left', width: 140, ellipsis: true }, + { title: '所属客户', dataIndex: 'customerName', align: 'left', width: 160, ellipsis: true }, + { title: '布点场所', dataIndex: 'siteName', align: 'left', width: 160, ellipsis: true }, + { + title: '鉴权开关', + dataIndex: 'authEnabled', + align: 'center', + width: 100, + customRender: ({ value }: { value: boolean }) => + h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '已开启' : '已关闭')), + }, + { + title: '状态', + dataIndex: 'status', + align: 'center', + width: 90, + customRender: ({ value }: { value: Status }) => + h(Tag, { color: statusColor[value] }, () => statusText(value)), + }, + { title: 'MQ 通讯地址', dataIndex: 'mqAddress', align: 'left', width: 220, ellipsis: true }, + { title: '服务网关地址', dataIndex: 'gatewayUrl', align: 'left', width: 240, resizable: true, ellipsis: true }, + { + title: '覆盖设备', + dataIndex: 'deviceCategoryNames', + align: 'left', + width: 220, + customRender: ({ value }: { value: string[] | undefined }) => { + if (!value || value.length === 0) return '—' + return h('div', value.map((name) => h('div', { key: name, style: 'line-height: 1.6' }, name))) + }, + }, + { title: '创建时间', dataIndex: 'createTime', align: 'center', width: 170, sorter: true, resizable: true }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 160 }, +] diff --git a/src/pages/admin-portal/device-center/distribute/report/report.vue b/src/pages/admin-portal/device-center/distribute/report/report.vue new file mode 100644 index 0000000..34a160b --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/report.vue @@ -0,0 +1,113 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/distribute/report/types/index.ts b/src/pages/admin-portal/device-center/distribute/report/types/index.ts new file mode 100644 index 0000000..d48b937 --- /dev/null +++ b/src/pages/admin-portal/device-center/distribute/report/types/index.ts @@ -0,0 +1,116 @@ +/** + * 数据分发 - 上报配置 类型 + * + * 业务定位:管理「设备数据上报到外部下游系统」的通道配置 + * - 一条上报配置 = 一个上报通道(含 MQ 地址 + 服务网关地址 + 鉴权信息 + 覆盖设备) + * - 与「分发配置」相互独立,分发配置面向"下游系统订阅",上报配置面向"主动推送上报" + */ +import type { BaseSearchForm } from '@pages/admin-portal/shared/types' +import type { DeviceCategoryTreeNode } from '@api' + +/** 启用状态 */ +export type Status = 'enabled' | 'disabled' + +/** 列表项 / 详情 */ +export interface ReportItem { + id: string + /** 上报通道名称 */ + channelName: string + /** 通道编码 */ + channelCode: string + /** 所属客户 ID */ + customerId: string + /** 所属客户名称(联表回填) */ + customerName: string + /** 布点场所 ID */ + siteId: string + /** 布点场所名称(联表回填) */ + siteName: string + /** 鉴权开关 */ + authEnabled: boolean + /** 鉴权账号(authEnabled=true 时必填) */ + authAccount?: string + /** 鉴权秘钥(authEnabled=true 时必填) */ + authSecret?: string + /** 鉴权接口地址(authEnabled=true 时必填) */ + authApiUrl?: string + /** 状态 */ + status: Status + /** MQ 通讯地址 */ + mqAddress: string + /** 服务网关地址 */ + gatewayUrl: string + /** 覆盖设备(设备分类 ID 多选) */ + deviceCategoryIds: string[] + /** 覆盖设备名称(列表展示用,由后端 join) */ + deviceCategoryNames?: string[] + /** 备注 */ + remark?: string + /** 创建时间 */ + createTime?: string +} + +/** 新增/编辑提交载荷 */ +export interface ReportPayload { + id?: string + channelName: string + channelCode: string + customerId: string + siteId: string + authEnabled: boolean + authAccount?: string + authSecret?: string + authApiUrl?: string + status: Status + mqAddress: string + gatewayUrl: string + deviceCategoryIds: string[] + remark?: string +} + +/** 搜索表单 */ +export interface SearchForm extends BaseSearchForm { + /** 所属客户 */ + customerId?: string + /** 布点场所 */ + siteId?: string + /** 上报通道名称(复用 BaseSearchForm.name) */ + name?: string + /** 状态 */ + status?: Status + /** 鉴权开关(undefined=全部) */ + authEnabled?: boolean +} + +/** 启用状态下拉选项 */ +export const STATUS_OPTIONS: { label: string; value: Status }[] = [ + { label: '启用', value: 'enabled' }, + { label: '停用', value: 'disabled' }, +] + +/** 启用状态 → 中文 */ +export const statusText = (value: Status | string | undefined): string => + STATUS_OPTIONS.find((i) => i.value === value)?.label ?? value ?? '—' + +/** 启用状态 → Tag 颜色 */ +export const statusColor: Record = { + enabled: 'success', + disabled: 'default', +} + +/** 客户下拉选项(列表页 / 弹框共用) */ +export interface CustomerOption { + label: string + value: string +} + +/** 布点场所下拉选项(列表页 / 弹框共用) */ +export interface SiteOption { + label: string + value: string + /** 携带 customerId 供联动校验 */ + customerId: string +} + +/** 设备分类树节点(与 @api 对齐) */ +export type CategoryTreeNode = DeviceCategoryTreeNode diff --git a/src/pages/admin-portal/device-center/ops/alarm/alarm.vue b/src/pages/admin-portal/device-center/ops/alarm/alarm.vue index 66d7569..4336256 100644 --- a/src/pages/admin-portal/device-center/ops/alarm/alarm.vue +++ b/src/pages/admin-portal/device-center/ops/alarm/alarm.vue @@ -1,129 +1,126 @@ @@ -76,12 +74,15 @@ * - 主列表:设备运行状态总览 * - 详情页(多 Tab 聚合):实施/自检/巡检/运行日志/更新记录/维修记录 */ +import { onMounted, ref } from 'vue' import FilterBar from '@components/FilterBar.vue' import TableCard from '@components/TableCard.vue' import StatCard from '@components/StatCard.vue' import InfoDrawer from './component/drawer/info/info.vue' -import { DEVICE_CATEGORY_OPTIONS, RUN_STATUS_OPTIONS } from './types' +import { RUN_STATUS_OPTIONS, type DeviceCategoryOption } from './types' import { usePage } from './init/usePage' +import { filterOption } from '@utils/antDesign/select' +import * as api from './api' const { pageLoading, @@ -94,6 +95,23 @@ const { dataSourceChange, handleAction, } = usePage() + +/** 设备分类下拉(末级分类名,与设备初始化模块对齐) */ +const categoryOpts = ref<{ label: string; value: string }[]>([]) + +const loadCategoryOpts = (): void => { + api.categoryOptions() + .then((res) => { + if (res.code === '00000') { + categoryOpts.value = (res.data ?? []).map((i: DeviceCategoryOption) => ({ label: i.name, value: i.name })) + } + }) + .catch((err: unknown) => console.error('设备分类下拉失败:', err)) +} + +onMounted(() => { + loadCategoryOpts() +}) diff --git a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/ops/version/component/drawer/info/types/index.ts deleted file mode 100644 index 1324910..0000000 --- a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/types/index.ts +++ /dev/null @@ -1,18 +0,0 @@ -/** - * 版本管理 - 详情抽屉 - 类型 - */ -import type { VersionItem } from '../../../../types' - -export interface PageInfo { - visible: boolean - title: string - spin: boolean - width: number -} - -export interface OpenContext { - id: string - title?: string -} - -export type { VersionItem } diff --git a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/addOrEdit.vue index ea2bdf7..0cbe93d 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/addOrEdit.vue +++ b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/addOrEdit.vue @@ -10,46 +10,29 @@ - - + + - - + + - - - - - + - - - - - - - - - - - - - - + diff --git a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/api/index.ts index 4c607d8..b75fb56 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/api/index.ts @@ -1,13 +1,13 @@ /** - * 版本管理 - 新增/编辑弹框 - 接口 + * 版本管理 - 应用新增/编辑弹框 - 接口 */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' import type { Form } from '../types' -const BASE = '/admin/device/ops/version' +const BASE = '/admin/device/ops/version/app' -export const info = (id: string): Promise> => +export const info = (id: string): Promise>> => postRequest('axiosRequest', BASE + '/info-for-edit', { id }) export const add = (params: Record): Promise> => diff --git a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/init/usePage.ts index 36dc846..85cfc9c 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/init/usePage.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/init/usePage.ts @@ -1,26 +1,22 @@ /** - * 版本管理 - 新增/编辑弹框 - 主逻辑 + * 版本管理 - 应用新增/编辑弹框 - 主逻辑 */ import type { FormInstance, Rule } from 'ant-design-vue/es/form' import { useAntdStaticMethods } from '@utils/antDesign/popUp' import { add, edit, info } from '../api' -import { VERSION_STATUS_OPTIONS, VERSION_TRACK_OPTIONS } from '../../../../types' +import { APP_STATUS_OPTIONS } from '../../../../types' import type { Form, FormOptions, ModalType, OpenContext, PageInfo } from '../types' const createInitForm = (): Form => ({ id: undefined, - version: '', - track: 'firmware', - packageUrl: '', - upgradePoints: '', - grayPercent: 10, - rollbackFlag: true, - status: 'draft', + name: '', + devSubject: '', + description: '', + status: 'enabled', }) const createInitOptions = (): FormOptions => ({ - track: VERSION_TRACK_OPTIONS, - status: VERSION_STATUS_OPTIONS, + status: APP_STATUS_OPTIONS, }) export const usePage = (emit: (e: 'load') => void) => { @@ -30,7 +26,7 @@ export const usePage = (emit: (e: 'load') => void) => { visible: false, type: 'add', title: '', - width: 720, + width: 640, spin: false, }) @@ -39,10 +35,9 @@ export const usePage = (emit: (e: 'load') => void) => { const formOptions = ref(createInitOptions()) const rules: Record = { - version: [{ required: true, message: '请输入版本号', trigger: 'blur' }], - track: [{ required: true, message: '请选择轨道', trigger: 'change' }], - packageUrl: [{ required: true, message: '请输入升级包地址', trigger: 'blur' }], - upgradePoints: [{ required: true, message: '请输入升级点说明', trigger: 'blur' }], + name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }], + devSubject: [{ required: true, message: '请输入研发主体(公司名称)', trigger: 'blur' }], + status: [{ required: true, message: '请选择状态', trigger: 'change' }], } const resetForm = (): void => { @@ -60,7 +55,7 @@ export const usePage = (emit: (e: 'load') => void) => { pageInfo.spin = true pageInfo.type = type resetForm() - pageInfo.title = type === 'add' ? '新增版本' : '编辑版本' + pageInfo.title = type === 'add' ? '新增应用' : '编辑应用' const requests: Promise[] = [] if (type === 'edit' && ctx?.id) requests.push(info(ctx.id)) @@ -68,8 +63,8 @@ export const usePage = (emit: (e: 'load') => void) => { Promise.all(requests) .then((results) => { if (type === 'edit' && ctx?.id) { - const infoRes = results[0] as { code: string; data: Form } - if (infoRes?.code === '00000') { + const infoRes = results[0] as { code: string; data: Partial } + if (infoRes?.code === '00000' && infoRes.data) { form.value = { ...createInitForm(), ...infoRes.data } } } diff --git a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/types/index.ts index 4cfd870..269249a 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/modal/addOrEdit/types/index.ts @@ -1,8 +1,8 @@ /** - * 版本管理 - 新增/编辑弹框 - 类型 + * 版本管理 - 应用新增/编辑弹框 - 类型 */ import type { SelectProps } from 'ant-design-vue' -import type { VersionStatus, VersionTrack } from '../../../../types' +import type { AppStatus } from '../../../../types' export type ModalType = 'add' | 'edit' @@ -16,17 +16,17 @@ export interface PageInfo { export interface Form { id?: string - version: string - track: VersionTrack - packageUrl: string - upgradePoints: string - grayPercent: number - rollbackFlag: boolean - status: VersionStatus + /** 应用名称 */ + name: string + /** 研发主体(公司名称) */ + devSubject: string + /** 应用描述 */ + description: string + /** 状态 */ + status: AppStatus } export interface FormOptions { - track: SelectProps['options'] status: SelectProps['options'] } diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/api/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/api/index.ts new file mode 100644 index 0000000..8ddb726 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/api/index.ts @@ -0,0 +1,26 @@ +/** + * 版本管理 - 二级页接口 + * - list 按 appId 过滤 + * - info 详情(抽屉用) + * - add 新增版本 + * - toggle 切换状态 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { VersionItem } from '../../types' +import type { VersionSearchForm } from '../types' + +const BASE = '/admin/device/ops/version' + +export const list = (params: VersionSearchForm): Promise> => + postRequest('axiosRequest', BASE + '/list', params) + +export const info = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/info', { id }) + +export const add = (params: Record): Promise> => + postRequest('axiosRequest', BASE + '/add', params) + +/** 切换有效 / 停用 */ +export const toggle = (id: string): Promise> => + postRequest('axiosRequest', BASE + '/toggle', { id }) diff --git a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/api/index.ts similarity index 64% rename from src/pages/admin-portal/device-center/ops/version/component/drawer/info/api/index.ts rename to src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/api/index.ts index 6ba50cb..edb9567 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/api/index.ts @@ -1,6 +1,9 @@ +/** + * 版本管理 - 二级页 / 版本详情抽屉 - 接口 + */ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { VersionItem } from '../../../../types' +import type { VersionItem } from '../../../../../types' export const getInfo = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/ops/version/info', { id }) diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/info.vue new file mode 100644 index 0000000..b7d6976 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/info.vue @@ -0,0 +1,90 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/init/usePage.ts similarity index 88% rename from src/pages/admin-portal/device-center/ops/version/component/drawer/info/init/usePage.ts rename to src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/init/usePage.ts index 6f6c4fc..2c60c71 100644 --- a/src/pages/admin-portal/device-center/ops/version/component/drawer/info/init/usePage.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/init/usePage.ts @@ -1,6 +1,9 @@ +/** + * 版本管理 - 二级页 / 版本详情抽屉 - 主逻辑 + */ import { reactive, ref } from 'vue' import { getInfo } from '../api' -import type { VersionItem } from '../../../../types' +import type { VersionItem } from '../../../../../../types' import type { OpenContext, PageInfo } from '../types' export const usePage = () => { diff --git a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/types/index.ts similarity index 63% rename from src/pages/admin-portal/device-center/distribute/record/component/drawer/info/types/index.ts rename to src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/types/index.ts index 7ef9b3d..511f3da 100644 --- a/src/pages/admin-portal/device-center/distribute/record/component/drawer/info/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/drawer/info/types/index.ts @@ -1,13 +1,14 @@ -/** 抽屉页面状态 */ +/** + * 版本管理 - 二级页 / 版本详情抽屉 - 类型 + */ export interface PageInfo { visible: boolean title: string - width: number spin: boolean + width: number } -/** 打开抽屉上下文 */ export interface OpenContext { - /** 必传 */ id: string + title?: string } diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/addOrEdit.vue new file mode 100644 index 0000000..351b31f --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/addOrEdit.vue @@ -0,0 +1,126 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/api/index.ts new file mode 100644 index 0000000..97a9124 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/api/index.ts @@ -0,0 +1,10 @@ +/** + * 版本管理 - 二级页 / 新增版本弹框 - 接口 + */ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' + +const BASE = '/admin/device/ops/version' + +export const add = (params: Record): Promise> => + postRequest('axiosRequest', BASE + '/add', params) diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/init/usePage.ts new file mode 100644 index 0000000..22df430 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/init/usePage.ts @@ -0,0 +1,152 @@ +/** + * 版本管理 - 二级页 / 新增版本弹框 - 主逻辑 + * - 接收 appId(来自 URL query)作为关联字段 + */ +import type { FormInstance, Rule } from 'ant-design-vue/es/form' +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { add } from '../api' +import { DEVICE_CATEGORY_OPTIONS, VERSION_STATUS_OPTIONS } from '../../../../../../types' +import type { Form, FormOptions, ModalType, OpenContext, PackageFile, PageInfo } from '../types' + +const createInitForm = (appId?: string): Form => ({ + id: undefined, + appId, + version: '', + versionName: '', + releaseNote: '', + forceUpdate: false, + deviceCategories: [], + packageFile: null, + status: 'active', +}) + +const createInitOptions = (): FormOptions => ({ + deviceCategories: DEVICE_CATEGORY_OPTIONS, + status: VERSION_STATUS_OPTIONS, +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + type: 'add', + title: '', + width: 640, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + const formOptions = ref(createInitOptions()) + + /** a-upload 受控文件列表(始终保持 0 或 1 条) */ + const fileList = ref([]) + + const rules: Record = { + version: [{ required: true, message: '请输入版本号', trigger: 'blur' }], + versionName: [{ required: true, message: '请输入版本名称', trigger: 'blur' }], + releaseNote: [{ required: true, message: '请输入更新说明', trigger: 'blur' }], + deviceCategories: [{ required: true, type: 'array', message: '请选择适配类目', trigger: 'change' }], + packageFile: [{ required: true, message: '请上传软件包', trigger: 'change' }], + status: [{ required: true, message: '请选择状态', trigger: 'change' }], + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + fileList.value = [] + } + + const closeModal = (): void => { + resetForm() + pageInfo.visible = false + } + + const openModal = (type: ModalType, ctx?: OpenContext): void => { + pageInfo.visible = true + pageInfo.spin = false + pageInfo.type = type + resetForm() + pageInfo.title = '新增版本' + if (ctx?.appId) form.value.appId = ctx.appId + } + + /** 阻断真实上传,由 customRequest 接管 */ + const beforeUpload = (): boolean => false + + /** 模拟上传请求(原型阶段用 setTimeout 模拟成功) */ + const customRequest = (options: { + file: File + onSuccess?: (arg: unknown) => void + onError?: (err: Error) => void + }): void => { + setTimeout(() => { + const file = options.file + const pkg: PackageFile = { + uid: `pkg-${Date.now()}`, + name: file.name, + size: file.size, + status: 'done', + } + fileList.value = [pkg] + form.value.packageFile = pkg + formRef.value?.clearValidate('packageFile') + options.onSuccess?.({ url: '' }) + }, 300) + } + + /** 移除软件包 */ + const handleRemove = (): void => { + fileList.value = [] + form.value.packageFile = null + } + + const submit = (): void => { + pageInfo.spin = true + formRef.value + ?.validate() + .then(() => { + const params = JSON.parse(JSON.stringify(form.value)) as Record + delete params.id + // 软件包拆字段:packageFile → packageName / packageSize + const pkg = params.packageFile as PackageFile | null + params.packageName = pkg?.name ?? '' + params.packageSize = pkg?.size ?? 0 + delete params.packageFile + add(params) + .then((res) => { + if (res.code === '00000') { + void message.success(res.msg) + emit('load') + closeModal() + } + }) + .catch((err: unknown) => { + console.error('新增版本失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + }) + .catch((err: unknown) => { + pageInfo.spin = false + console.error('表单验证失败:', err) + }) + } + + return { + pageInfo, + formRef, + form, + formOptions, + rules, + fileList, + openModal, + closeModal, + submit, + beforeUpload, + customRequest, + handleRemove, + } +} diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/types/index.ts new file mode 100644 index 0000000..dba3e5f --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/component/modal/addOrEdit/types/index.ts @@ -0,0 +1,53 @@ +/** + * 版本管理 - 二级页 / 新增版本弹框 - 类型 + */ +import type { SelectProps } from 'ant-design-vue' +import type { DeviceCategory, VersionStatus } from '../../../../../types' + +export type ModalType = 'add' + +/** 软件包文件信息(上传后的本地状态) */ +export interface PackageFile { + uid: string + name: string + size: number + status: 'done' +} + +export interface PageInfo { + visible: boolean + type: ModalType + title: string + width: number + spin: boolean +} + +export interface Form { + id?: string + appId?: string + /** 版本号 */ + version: string + /** 版本名称 */ + versionName: string + /** 更新说明 */ + releaseNote: string + /** 强制更新 */ + forceUpdate: boolean + /** 适配类目 */ + deviceCategories: DeviceCategory[] + /** 软件包(新增时为本地文件信息,提交时拆为 packageName / packageSize) */ + packageFile: PackageFile | null + /** 状态 */ + status: VersionStatus +} + +export interface FormOptions { + deviceCategories: SelectProps['options'] + status: SelectProps['options'] +} + +export interface OpenContext { + appId?: string +} + +export const LABEL_COL = { style: { width: '120px', minWidth: '120px' } } as const diff --git a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/usePage.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/usePage.ts similarity index 53% rename from src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/usePage.ts rename to src/pages/admin-portal/device-center/ops/version/component/versionList/init/usePage.ts index 82d5475..0ac1f73 100644 --- a/src/pages/admin-portal/device-center/distribute/config/component/tabs/downstream/init/usePage.ts +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/usePage.ts @@ -1,24 +1,34 @@ +/** + * 版本管理 - 二级页(版本列表)编排 + * - 入口:从主页「版本详情」按钮跳转,URL 携带 appId / appName + * - 列表查询 / 新增版本 / 状态切换(二次确认)/ 详情抽屉 + */ import { onMounted, reactive, ref } from 'vue' import type { TablePaginationConfig } from 'ant-design-vue' +import { useRoute, useRouter } from 'vue-router' 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 InfoDrawer from '../component/drawer/info/info.vue' import * as api from '../api' -import type { DownstreamItem, SearchForm } from '../types' +import type { VersionItem } from '../../types' +import type { VersionSearchForm } from '../types' import { createSearchKey } from './useSearch' import { tableColumns } from './useTable' -/** - * 下游系统 Tab 编排 - */ export const usePage = () => { + const route = useRoute() + const router = useRouter() const { message, Modal } = useAntdStaticMethods() - const search = reactive(createSearchKey()) + /** 从 URL query 注入 appId / appName(重置时不清除) */ + const appId = (route.query.appId as string | undefined) ?? undefined + const appName = (route.query.appName as string | undefined) ?? undefined - const table = reactive>({ + const search = reactive(createSearchKey(appId)) + + const table = reactive>({ columns: tableColumns, dataSource: [], sort: { field: '', order: null }, @@ -26,8 +36,6 @@ export const usePage = () => { }) const pageLoading = ref(false) - const selectedRowKeys = ref<(string | number)[]>([]) - const addOrEditRef = ref | null>(null) const infoDrawerRef = ref | null>(null) @@ -41,7 +49,7 @@ export const usePage = () => { } }) .catch((err: unknown) => { - console.error('下游系统列表请求失败:', err) + console.error('版本列表请求失败:', err) }) .finally(() => { pageLoading.value = false @@ -54,7 +62,7 @@ export const usePage = () => { } const resetQuery = (): void => { - Object.assign(search, createSearchKey()) + Object.assign(search, createSearchKey(appId)) table.pagination.current = 1 listRequest() } @@ -64,62 +72,30 @@ export const usePage = () => { table.pagination.pageSize = pagination.pageSize ?? 30 } - const rowSelection = { - selectedRowKeys, - onChange: (keys: (string | number)[]): void => { - selectedRowKeys.value = keys - }, + /** 返回版本管理主页 */ + const goBackToApp = (): void => { + router.push('/admin-portal/device-center/ops/version') } - /** 新增 */ const openAdd = (): void => { - addOrEditRef.value?.openAdd() + addOrEditRef.value?.openModal('add', { appId }) } - /** 编辑 */ - const openEdit = (record: DownstreamItem): void => { - addOrEditRef.value?.openEdit(record) + const openDetail = (record: VersionItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id, title: `版本详情 · ${record.version}` }) } - /** 详情 */ - const openDetail = (record: DownstreamItem): void => { - infoDrawerRef.value?.openDrawer({ id: record.id, title: '下游系统详情' }) - } - - /** 删除(单条) */ - const removeRecord = (record: DownstreamItem): void => { - Modal.confirm({ - title: '确认删除?', - content: `将删除下游系统 [${record.name}],删除后相关订阅映射将失效,确认继续?`, - okText: '确认删除', - okType: 'danger', - cancelText: '取消', - onOk: () => { - api.remove(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - } - }) - .catch((err: unknown) => { - console.error('删除失败:', err) - }) - }, - }) - } - - /** 切换启用状态 */ - const toggleStatus = (record: DownstreamItem): void => { - const next = record.status === 'enabled' ? '禁用' : '启用' + /** 切换有效 / 停用(二次确认) */ + const toggleStatus = (record: VersionItem): void => { + const next = record.status === 'active' ? '停用' : '启用' Modal.confirm({ title: `确认${next}?`, - content: `将${next}下游系统 [${record.name}],确认继续?`, + content: `版本 [${record.version}] 将被${next},确认继续?`, okText: `确认${next}`, - okType: 'danger', + okType: record.status === 'active' ? 'danger' : 'primary', cancelText: '取消', onOk: () => { - api.toggleStatus(record.id) + api.toggle(record.id) .then((res) => { if (res.code === '00000') { void message.success(res.msg) @@ -127,22 +103,19 @@ export const usePage = () => { } }) .catch((err: unknown) => { - console.error('状态切换失败:', err) + console.error('版本状态切换失败:', err) }) }, }) } - /** 刷新(弹框 ok 后回调) */ - const reload = (): void => { - listRequest() - } - - const handleAction = (type: string, record?: DownstreamItem): void => { + const handleAction = (type: string, record?: VersionItem): void => { + if (type === 'add') { + openAdd() + return + } if (!record) return if (type === 'detail') openDetail(record) - if (type === 'edit') openEdit(record) - if (type === 'delete') removeRecord(record) if (type === 'toggle') toggleStatus(record) } @@ -154,15 +127,14 @@ export const usePage = () => { pageLoading, search, table, - selectedRowKeys, - rowSelection, + appName, addOrEditRef, infoDrawerRef, searchQuery, resetQuery, dataSourceChange, handleAction, - openAdd, - reload, + listRequest, + goBackToApp, } } diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useSearch.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useSearch.ts new file mode 100644 index 0000000..9b275b3 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useSearch.ts @@ -0,0 +1,10 @@ +import type { VersionSearchForm } from '../types' + +/** 搜索表单工厂:appId 由 URL query 注入,重置时保留 */ +export const createSearchKey = (appId?: string): VersionSearchForm => ({ + appId, + keyword: undefined, + status: undefined, +}) + +export const useSearch = () => ({ createSearchKey }) diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useTable.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useTable.ts new file mode 100644 index 0000000..360fcaf --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/init/useTable.ts @@ -0,0 +1,18 @@ +import type { TableColumnsType } from 'ant-design-vue' + +/** + * 版本管理 - 二级页(版本列表)列定义 + * - 状态列由 bodyCell 用 a-switch 渲染 + * - 适配类目 / 强制更新 / 操作列同样由 bodyCell 渲染 + */ +export const tableColumns: TableColumnsType = [ + { title: '版本号', dataIndex: 'version', align: 'left', fixed: 'left', width: 130, resizable: true, ellipsis: true }, + { title: '版本名称', dataIndex: 'versionName', align: 'left', width: 130, ellipsis: true }, + { title: '更新说明', dataIndex: 'releaseNote', align: 'left', width: 280, resizable: true, ellipsis: true }, + { title: '强制更新', dataIndex: 'forceUpdate', align: 'center', width: 100 }, + { title: '状态', dataIndex: 'status', align: 'center', width: 110 }, + { title: '再用设备数', dataIndex: 'usingDeviceCount', align: 'center', width: 110 }, + { title: '适配类目', dataIndex: 'deviceCategories', align: 'left', width: 180 }, + { title: '更新时间', dataIndex: 'updateTime', align: 'center', width: 170 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 100 }, +] diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/types/index.ts b/src/pages/admin-portal/device-center/ops/version/component/versionList/types/index.ts new file mode 100644 index 0000000..64bf085 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/types/index.ts @@ -0,0 +1,70 @@ +/** + * 版本管理 - 二级页(版本列表)类型 + * - VersionItem / SearchForm 从主页 types 复用 + * - 弹框 / 抽屉本地类型 + */ +import type { BaseSearchForm } from '@pages/admin-portal/shared/types' +import type { + DeviceCategory, + VersionItem, + VersionStatus, +} from '../../types' + +export type { VersionItem, VersionStatus } + +/** 二级页搜索表单(appId 由 URL query 注入) */ +export interface VersionSearchForm extends BaseSearchForm { + appId?: string + keyword?: string + status?: VersionStatus +} + +// ============ 版本新增弹框 ============ + +export type ModalType = 'add' + +export interface PageInfo { + visible: boolean + type: ModalType + title: string + width: number + spin: boolean +} + +export interface Form { + id?: string + appId?: string + /** 版本号 */ + version: string + /** 版本名称 */ + versionName: string + /** 更新说明 */ + releaseNote: string + /** 强制更新 */ + forceUpdate: boolean + /** 适配类目 */ + deviceCategories: DeviceCategory[] + /** 状态 */ + status: VersionStatus +} + +export interface FormOptions { + deviceCategories: { label: string; value: DeviceCategory }[] + status: { label: string; value: VersionStatus }[] +} + +export const LABEL_COL = { style: { width: '120px', minWidth: '120px' } } as const + +// ============ 版本详情抽屉 ============ + +export interface DrawerPageInfo { + visible: boolean + title: string + spin: boolean + width: number +} + +export interface DrawerOpenContext { + id: string + title?: string +} diff --git a/src/pages/admin-portal/device-center/ops/version/component/versionList/versionList.vue b/src/pages/admin-portal/device-center/ops/version/component/versionList/versionList.vue new file mode 100644 index 0000000..fa06bd6 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/version/component/versionList/versionList.vue @@ -0,0 +1,117 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/ops/version/init/usePage.ts b/src/pages/admin-portal/device-center/ops/version/init/usePage.ts index cbe4411..d559b7a 100644 --- a/src/pages/admin-portal/device-center/ops/version/init/usePage.ts +++ b/src/pages/admin-portal/device-center/ops/version/init/usePage.ts @@ -1,27 +1,27 @@ /** - * 版本管理 - 页面主编排 - * - 列表查询 / 新增 / 编辑 / 删除 - * - 状态流转:草稿 → 灰度 → 全量 → 停用 + * 版本管理 - 主页(应用列表)编排 + * - 列表查询 / 新增 / 编辑 / 状态切换(二次确认)/ 跳转版本列表子页面 */ import { onMounted, reactive, ref } from 'vue' import type { TablePaginationConfig } from 'ant-design-vue' +import { useRouter } from 'vue-router' 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 InfoDrawer from '../component/drawer/info/info.vue' import * as api from '../api' -import type { SearchForm, VersionItem, VersionStatus } from '../types' -import { NEXT_STATUS_MAP, transitionText } from '../types' +import type { AppItem, AppSearchForm } from '../types' +import { appStatusText } from '../types' import { createSearchKey } from './useSearch' import { tableColumns } from './useTable' export const usePage = () => { + const router = useRouter() const { message, Modal } = useAntdStaticMethods() - const search = reactive(createSearchKey()) + const search = reactive(createSearchKey()) - const table = reactive>({ + const table = reactive>({ columns: tableColumns, dataSource: [], sort: { field: '', order: null }, @@ -30,7 +30,6 @@ export const usePage = () => { const pageLoading = ref(false) const addOrEditRef = ref | null>(null) - const infoDrawerRef = ref | null>(null) const listRequest = (): void => { pageLoading.value = true @@ -42,7 +41,7 @@ export const usePage = () => { } }) .catch((err: unknown) => { - console.error('版本管理列表请求失败:', err) + console.error('应用列表请求失败:', err) }) .finally(() => { pageLoading.value = false @@ -69,25 +68,29 @@ export const usePage = () => { addOrEditRef.value?.openModal('add') } - const openEdit = (record: VersionItem): void => { + const openEdit = (record: AppItem): void => { addOrEditRef.value?.openModal('edit', { id: record.id }) } - const openDetail = (record: VersionItem): void => { - infoDrawerRef.value?.openDrawer({ id: record.id, title: `版本详情 · ${record.version}` }) + /** 跳转版本列表子页面 */ + const openVersionList = (record: AppItem): void => { + router.push({ + path: '/admin-portal/device-center/ops/version-list', + query: { appId: record.id, appName: record.name }, + }) } - /** 状态流转 */ - const transition = (record: VersionItem, to: VersionStatus): void => { - const text = transitionText(to) + /** 切换启用 / 停用(二次确认) */ + const toggleStatus = (record: AppItem): void => { + const next = record.status === 'enabled' ? '停用' : '启用' Modal.confirm({ - title: `确认${text}?`, - content: `版本号:${record.version}`, - okType: to === 'deprecated' ? 'danger' : 'primary', - okText: `确认${text}`, + title: `确认${next}?`, + content: `应用 [${record.name}] 将被${next},确认继续?`, + okText: `确认${next}`, + okType: record.status === 'enabled' ? 'danger' : 'primary', cancelText: '取消', onOk: () => { - api.transition(record.id, to) + api.toggle(record.id) .then((res) => { if (res.code === '00000') { void message.success(res.msg) @@ -95,50 +98,21 @@ export const usePage = () => { } }) .catch((err: unknown) => { - console.error('版本状态流转失败:', err) + console.error('状态切换失败:', err) }) }, }) } - const deleteRecord = (record: VersionItem): void => { - Modal.confirm({ - title: '确认删除?', - content: `版本 [${record.version}] 删除后不可恢复。`, - okType: 'danger', - okText: '确认删除', - cancelText: '取消', - onOk: () => { - api.remove(record.id) - .then((res) => { - if (res.code === '00000') { - void message.success(res.msg) - listRequest() - } - }) - .catch((err: unknown) => { - console.error('删除失败:', err) - }) - }, - }) - } - - /** 流转按钮文案 */ - const nextStatus = (status: VersionStatus): VersionStatus | null => NEXT_STATUS_MAP[status] - - const handleAction = (type: string, record?: VersionItem): void => { + const handleAction = (type: string, record?: AppItem): void => { if (type === 'add') { openAdd() return } if (!record) return - if (type === 'detail') openDetail(record) if (type === 'edit') openEdit(record) - if (type === 'delete') deleteRecord(record) - if (type === 'transition') { - const next = nextStatus(record.status) - if (next) transition(record, next) - } + if (type === 'versionList') openVersionList(record) + if (type === 'toggle') toggleStatus(record) } onMounted(() => { @@ -150,12 +124,11 @@ export const usePage = () => { search, table, addOrEditRef, - infoDrawerRef, searchQuery, resetQuery, dataSourceChange, handleAction, - nextStatus, listRequest, + appStatusText, } } diff --git a/src/pages/admin-portal/device-center/ops/version/init/useSearch.ts b/src/pages/admin-portal/device-center/ops/version/init/useSearch.ts index 3f7fb11..fa3d523 100644 --- a/src/pages/admin-portal/device-center/ops/version/init/useSearch.ts +++ b/src/pages/admin-portal/device-center/ops/version/init/useSearch.ts @@ -1,9 +1,7 @@ -import type { SearchForm } from '../types' +import type { AppSearchForm } from '../types' -export const createSearchKey = (): SearchForm => ({ +export const createSearchKey = (): AppSearchForm => ({ keyword: undefined, - track: undefined, - status: undefined, }) export const useSearch = () => ({ createSearchKey }) diff --git a/src/pages/admin-portal/device-center/ops/version/init/useTable.ts b/src/pages/admin-portal/device-center/ops/version/init/useTable.ts index 7368d85..0efe4cd 100644 --- a/src/pages/admin-portal/device-center/ops/version/init/useTable.ts +++ b/src/pages/admin-portal/device-center/ops/version/init/useTable.ts @@ -1,55 +1,17 @@ import type { TableColumnsType } from 'ant-design-vue' -import { h } from 'vue' -import { Progress, Tag } from 'ant-design-vue' -import type { VersionItem, VersionStatus, VersionTrack } from '../types' -import { versionStatusColor, versionStatusText, versionTrackText } from '../types' +/** + * 版本管理 - 主页(应用列表)列定义 + * - 状态列由模板 bodyCell 用 a-switch 渲染(胶囊按钮 + 二次确认) + * - 操作列:版本详情(跳转)/ 编辑 + */ export const tableColumns: TableColumnsType = [ - { title: '版本号', dataIndex: 'version', align: 'left', fixed: 'left', width: 130, resizable: true, ellipsis: true }, - { - title: '轨道', - dataIndex: 'track', - align: 'center', - width: 110, - customRender: ({ value }: { value: VersionTrack }) => - h(Tag, { color: value === 'firmware' ? 'geekblue' : 'purple' }, () => versionTrackText(value)), - }, - { title: '升级包地址', dataIndex: 'packageUrl', align: 'left', width: 220, resizable: true, ellipsis: true }, - { title: '升级点', dataIndex: 'upgradePoints', align: 'left', width: 240, resizable: true, ellipsis: true }, - { - title: '灰度', - dataIndex: 'grayPercent', - align: 'center', - width: 120, - customRender: ({ value, record }: { value: number; record: VersionItem }) => { - if (record.status === 'full') return h(Tag, { color: 'success' }, () => '已全量') - if (record.status === 'draft') return '—' - return h(Progress, { percent: value ?? 0, size: 'small' }) - }, - }, - { - title: '可回滚', - dataIndex: 'rollbackFlag', - align: 'center', - width: 90, - customRender: ({ value }: { value: boolean }) => - h(Tag, { color: value ? 'green' : 'default' }, () => (value ? '是' : '否')), - }, - { - title: '已升级', - dataIndex: 'upgradedCount', - align: 'center', - width: 100, - customRender: ({ value }: { value: number }) => `${value ?? 0} 台`, - }, - { title: '发布时间', dataIndex: 'publishTime', align: 'center', width: 170, customRender: ({ value }: { value: string | null }) => value ?? '—' }, - { - title: '状态', - dataIndex: 'status', - align: 'center', - width: 100, - customRender: ({ value }: { value: VersionStatus }) => - h(Tag, { color: versionStatusColor[value] }, () => versionStatusText(value)), - }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 220 }, + { title: '应用名称', dataIndex: 'name', align: 'left', fixed: 'left', width: 180, resizable: true, ellipsis: true }, + { title: '研发主体', dataIndex: 'devSubject', align: 'left', width: 180, resizable: true, ellipsis: true }, + { title: '应用描述', dataIndex: 'description', align: 'left', width: 280, resizable: true, ellipsis: true }, + { title: '最新版本', dataIndex: 'latestVersion', align: 'center', width: 120 }, + { title: '历史版本', dataIndex: 'versionCount', align: 'center', width: 100 }, + { title: '状态', dataIndex: 'status', align: 'center', width: 110 }, + { title: '最后更新日期', dataIndex: 'updateTime', align: 'center', width: 170 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 180 }, ] diff --git a/src/pages/admin-portal/device-center/ops/version/types/index.ts b/src/pages/admin-portal/device-center/ops/version/types/index.ts index c813b41..d2bdca1 100644 --- a/src/pages/admin-portal/device-center/ops/version/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/version/types/index.ts @@ -1,98 +1,111 @@ /** - * 版本管理 - 类型 - * - 固件版本(硬件 OTA)+ APP 版本(客户端 APP)双轨 - * - 状态流转:草稿 → 灰度 → 全量 → 已停用 + * 版本管理 - 类型定义(主页 + 二级页共享) + * - 主页:应用列表(AppItem) + * - 二级页:版本列表(VersionItem) + * - 主页状态:enabled / disabled;二级页状态:active / disabled */ import type { BaseSearchForm } from '@pages/admin-portal/shared/types' -/** 版本轨道 */ -export type VersionTrack = 'firmware' | 'app' +// ============ 研发主体 ============ +// 说明:研发主体为公司名称(输入框填写),不再使用枚举。 -/** 版本状态 */ -export type VersionStatus = 'draft' | 'gray' | 'full' | 'deprecated' +// ============ 设备分类(适配类目) ============ + +export type DeviceCategory = 'cabinet' | 'watch' | 'healthCheck' | 'bodyFat' | 'scale' + +export const DEVICE_CATEGORY_OPTIONS: { label: string; value: DeviceCategory }[] = [ + { label: '净菜柜', value: 'cabinet' }, + { label: '智能手表', value: 'watch' }, + { label: '体检一体机', value: 'healthCheck' }, + { label: '体脂仪', value: 'bodyFat' }, + { label: '健康量表', value: 'scale' }, +] + +export const deviceCategoryText = (v: DeviceCategory | string | undefined): string => { + if (!v) return '—' + return DEVICE_CATEGORY_OPTIONS.find((i) => i.value === v)?.label ?? v +} + +// ============ 应用(主页) ============ + +export type AppStatus = 'enabled' | 'disabled' + +export interface AppItem { + id: string + /** 应用名称 */ + name: string + /** 研发主体(公司名称) */ + devSubject: string + /** 应用描述 */ + description: string + /** 状态 */ + status: AppStatus + /** 最新版本号(汇总) */ + latestVersion: string + /** 历史版本数(含最新) */ + versionCount: number + /** 最后更新日期(取最新版本的 updateTime) */ + updateTime: string +} + +export const APP_STATUS_OPTIONS: { label: string; value: AppStatus }[] = [ + { label: '启用', value: 'enabled' }, + { label: '停用', value: 'disabled' }, +] + +export const appStatusText = (v: AppStatus | string | undefined): string => { + if (!v) return '—' + return APP_STATUS_OPTIONS.find((i) => i.value === v)?.label ?? v +} + +/** 主页搜索表单 */ +export interface AppSearchForm extends BaseSearchForm { + keyword?: string +} + +// ============ 版本(二级页) ============ + +export type VersionStatus = 'active' | 'disabled' -/** 列表项 */ export interface VersionItem { id: string + /** 所属应用 ID */ + appId: string /** 版本号 */ version: string - /** 轨道 */ - track: VersionTrack - /** 升级包地址 */ - packageUrl: string - /** 升级点说明 */ - upgradePoints: string - /** 灰度策略(百分比) */ - grayPercent: number - /** 是否可回滚 */ - rollbackFlag: boolean - /** 发布时间 */ - publishTime: string | null + /** 版本名称 */ + versionName: string + /** 更新说明 */ + releaseNote: string + /** 强制更新 */ + forceUpdate: boolean /** 状态 */ status: VersionStatus - /** 已升级设备数 */ - upgradedCount: number - /** 创建人 */ - creator: string - /** 创建时间 */ - createTime: string + /** 再用设备数 */ + usingDeviceCount: number + /** 适配类目 */ + deviceCategories: DeviceCategory[] + /** 软件包文件名(含扩展名) */ + packageName: string + /** 软件包大小(字节) */ + packageSize: number + /** 更新时间 */ + updateTime: string } -/** 查询表单 */ -export interface SearchForm extends BaseSearchForm { - track?: VersionTrack - status?: VersionStatus +/** 软件包大小格式化(字节 → KB/MB) */ +export const formatPackageSize = (bytes: number | undefined): string => { + if (!bytes || bytes <= 0) return '—' + if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB` + return `${(bytes / (1024 * 1024)).toFixed(2)} MB` } -/** 轨道下拉 */ -export const VERSION_TRACK_OPTIONS: { label: string; value: VersionTrack }[] = [ - { label: '固件版本', value: 'firmware' }, - { label: 'APP 版本', value: 'app' }, -] - -/** 状态下拉 */ export const VERSION_STATUS_OPTIONS: { label: string; value: VersionStatus }[] = [ - { label: '草稿', value: 'draft' }, - { label: '灰度', value: 'gray' }, - { label: '全量', value: 'full' }, - { label: '已停用', value: 'deprecated' }, + { label: '有效', value: 'active' }, + { label: '停用', value: 'disabled' }, ] -/** 状态 → Tag 颜色 */ -export const versionStatusColor: Record = { - draft: 'default', - gray: 'warning', - full: 'success', - deprecated: 'error', -} - -/** 状态 → 中文 */ -export const versionStatusText = (status: VersionStatus | string | undefined): string => { - if (!status) return '—' - return VERSION_STATUS_OPTIONS.find((i) => i.value === status)?.label ?? status -} - -/** 轨道 → 中文 */ -export const versionTrackText = (track: VersionTrack | string | undefined): string => { - if (!track) return '—' - return VERSION_TRACK_OPTIONS.find((i) => i.value === track)?.label ?? track -} - -/** 可流转的下一个状态 */ -export const NEXT_STATUS_MAP: Record = { - draft: 'gray', - gray: 'full', - full: 'deprecated', - deprecated: null, -} - -/** 流转动作名 */ -export const transitionText = (to: VersionStatus): string => { - const map: Record = { - draft: '设为草稿', - gray: '发布灰度', - full: '发布全量', - deprecated: '停用', - } - return map[to] +export const versionStatusText = (v: VersionStatus | string | undefined): string => { + if (!v) return '—' + return VERSION_STATUS_OPTIONS.find((i) => i.value === v)?.label ?? v } diff --git a/src/pages/admin-portal/device-center/ops/version/version.vue b/src/pages/admin-portal/device-center/ops/version/version.vue index 0c0bfb5..48e1c08 100644 --- a/src/pages/admin-portal/device-center/ops/version/version.vue +++ b/src/pages/admin-portal/device-center/ops/version/version.vue @@ -2,13 +2,7 @@
- - - - - - - + @@ -16,42 +10,46 @@ @@ -76,5 +72,6 @@ const { display: flex; flex-direction: column; height: 100%; + gap: 12px; } diff --git a/src/pages/admin-portal/device-center/ops/workOrder/api/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/api/index.ts index eaeced5..d271998 100644 --- a/src/pages/admin-portal/device-center/ops/workOrder/api/index.ts +++ b/src/pages/admin-portal/device-center/ops/workOrder/api/index.ts @@ -1,13 +1,27 @@ import type { ApiResponse } from '@axios' import { postRequest } from '@axios' -import type { ListItem, ListParams, WorkOrderStatus } from '../types' +import type { DetailInfo, ListItem, ListParams } from '../types' +/** 工单分页查询 */ export const list = (params: ListParams): Promise> => postRequest('axiosRequest', '/admin/device/repair/work-order/page', params) -/** 工单流转:接单 / 派单 / 关单 */ -export const transition = (id: string, toStatus: WorkOrderStatus, assignee?: string): Promise> => - postRequest('axiosRequest', '/admin/device/repair/work-order/transition', { id, toStatus, assignee }) +/** 工单详情(含处理记录) */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/info', { id }) +/** 新增工单 */ +export const add = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/add', params) + +/** 派单 / 改派 */ +export const dispatch = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/dispatch', params) + +/** 处理(自动按当前状态推进:pending/assigned → processing;processing → closed) */ +export const process = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/process', params) + +/** 删除 */ export const remove = (id: string): Promise> => postRequest('axiosRequest', '/admin/device/repair/work-order/delete', { id }) diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/api/index.ts new file mode 100644 index 0000000..0d8b1fd --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/api/index.ts @@ -0,0 +1,7 @@ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' +import type { DetailInfo } from '../../../types' + +/** 工单详情(含处理记录时间线) */ +export const info = (id: string): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/info', { id }) diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/info.vue new file mode 100644 index 0000000..da19c3d --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/info.vue @@ -0,0 +1,102 @@ + + + diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/init/usePage.ts new file mode 100644 index 0000000..5176919 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/init/usePage.ts @@ -0,0 +1,49 @@ +/** + * 工单详情抽屉 - 主逻辑 + * - 加载详情(含基本信息 + 处理记录时间线) + */ + +import { info } from '../api' +import type { DetailInfo, OpenContext, PageInfo } from '../types' + +export const usePage = () => { + const pageInfo = reactive({ + visible: false, + title: '工单详情', + width: 760, + spin: false, + }) + + const detail = ref(null) + + const closeDrawer = (): void => { + pageInfo.visible = false + detail.value = null + } + + const openDrawer = (ctx: OpenContext): void => { + pageInfo.visible = true + pageInfo.spin = true + detail.value = null + + info(ctx.id) + .then((res) => { + if (res.code === '00000' && res.data) { + detail.value = res.data + } + }) + .catch((err: unknown) => { + console.error('工单详情查询失败:', err) + }) + .finally(() => { + pageInfo.spin = false + }) + } + + return { + pageInfo, + detail, + openDrawer, + closeDrawer, + } +} diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/types/index.ts new file mode 100644 index 0000000..862eca7 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/drawer/info/types/index.ts @@ -0,0 +1,23 @@ +import type { DetailInfo } from '../../../types' + +/** 详情抽屉页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 打开抽屉上下文 */ +export interface OpenContext { + id: string +} + +/** 处理记录时间线节点(带颜色映射) */ +export type ActionTagColor = 'green' | 'blue' | 'orange' | 'purple' + +/** 暴露给父组件的 ref 类型 */ +export type DrawerRef = InstanceType + +/** 导出详情类型供组件使用 */ +export type { DetailInfo } diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/addOrEdit.vue b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/addOrEdit.vue new file mode 100644 index 0000000..0209090 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/addOrEdit.vue @@ -0,0 +1,147 @@ + + + diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/api/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/api/index.ts new file mode 100644 index 0000000..4f9325e --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/api/index.ts @@ -0,0 +1,6 @@ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' + +/** 新增工单 */ +export const add = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/add', params) diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/init/usePage.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/init/usePage.ts new file mode 100644 index 0000000..2a50345 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/init/usePage.ts @@ -0,0 +1,104 @@ +/** + * 新增工单弹框 - 主逻辑 + */ + +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { add } from '../api' +import type { Form, PageInfo } from '../types' +import { FAULT_LEVEL_OPTIONS, FAULT_TYPE_OPTIONS, ORDER_SOURCE_OPTIONS } from '../../../../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + deviceCode: '', + deviceName: '', + customerName: '', + siteName: '', + source: undefined, + faultLevel: undefined, + faultType: undefined, + faultDesc: '', + slaDeadline: '', + images: [], +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + title: '新增工单', + width: 720, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + + const rules = { + deviceCode: [{ required: true, message: '请输入设备编码', trigger: 'blur' }], + deviceName: [{ required: true, message: '请输入设备名称', trigger: 'blur' }], + customerName: [{ required: true, message: '请选择所属客户', trigger: 'change' }], + siteName: [{ required: true, message: '请选择所属场所', trigger: 'change' }], + source: [{ required: true, message: '请选择工单来源', trigger: 'change' }], + faultLevel: [{ required: true, message: '请选择故障等级', trigger: 'change' }], + faultType: [{ required: true, message: '请选择故障类型', trigger: 'change' }], + faultDesc: [{ required: true, message: '请输入故障描述', trigger: 'blur' }], + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + } + + const closeModal = (): void => { + resetForm() + pageInfo.visible = false + } + + const openModal = (): void => { + pageInfo.visible = true + resetForm() + } + + const successRequest = (msg: string): void => { + void message.success(msg) + emit('load') + closeModal() + } + + const submit = (): void => { + pageInfo.spin = true + formRef.value + ?.validate() + .then(() => { + const params = { ...form.value } as Record + add(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, + FAULT_LEVEL_OPTIONS, + FAULT_TYPE_OPTIONS, + ORDER_SOURCE_OPTIONS, + } +} diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/types/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/types/index.ts new file mode 100644 index 0000000..0b458a3 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/addOrEdit/types/index.ts @@ -0,0 +1,27 @@ +import type { FaultLevel, FaultType, OrderSource } from '../../../../types' + +/** 弹框页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 表单字段 */ +export interface Form { + deviceCode: string + deviceName: string + customerName: string + siteName: string + source: OrderSource | undefined + faultLevel: FaultLevel | undefined + faultType: FaultType | undefined + faultDesc: string + slaDeadline: string + /** 故障图片(原型阶段用 URL 文本占位) */ + images: string[] +} + +/** 暴露给父组件的 ref 类型 */ +export type ModalRef = InstanceType diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/api/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/api/index.ts new file mode 100644 index 0000000..04abcbd --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/api/index.ts @@ -0,0 +1,6 @@ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' + +/** 派单 / 改派 */ +export const dispatch = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/dispatch', params) diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/dispatch.vue b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/dispatch.vue new file mode 100644 index 0000000..4bee29e --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/dispatch.vue @@ -0,0 +1,43 @@ + + + diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/init/usePage.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/init/usePage.ts new file mode 100644 index 0000000..14eed3b --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/init/usePage.ts @@ -0,0 +1,87 @@ +/** + * 派单弹框 - 主逻辑 + */ + +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { dispatch } from '../api' +import type { Form, PageInfo } from '../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + id: '', + assignee: '', + remark: '', +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + title: '派单', + width: 480, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + + const rules = { + assignee: [{ required: true, message: '请选择责任人', trigger: 'change' }], + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + } + + const closeModal = (): void => { + resetForm() + pageInfo.visible = false + } + + const openModal = (id: string): void => { + pageInfo.visible = true + resetForm() + form.value.id = id + } + + const successRequest = (msg: string): void => { + void message.success(msg) + emit('load') + closeModal() + } + + const submit = (): void => { + pageInfo.spin = true + formRef.value + ?.validate() + .then(() => { + const params = { ...form.value } as Record + dispatch(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/ops/workOrder/component/modal/dispatch/types/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/types/index.ts new file mode 100644 index 0000000..e355cbd --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/dispatch/types/index.ts @@ -0,0 +1,17 @@ +/** 弹框页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 表单字段 */ +export interface Form { + id: string + assignee: string + remark: string +} + +/** 暴露给父组件的 ref 类型 */ +export type ModalRef = InstanceType diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/api/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/api/index.ts new file mode 100644 index 0000000..4f80828 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/api/index.ts @@ -0,0 +1,6 @@ +import type { ApiResponse } from '@axios' +import { postRequest } from '@axios' + +/** 处理(自动按当前状态推进) */ +export const process = (params: Record): Promise> => + postRequest('axiosRequest', '/admin/device/repair/work-order/process', params) diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/init/usePage.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/init/usePage.ts new file mode 100644 index 0000000..52fda27 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/init/usePage.ts @@ -0,0 +1,90 @@ +/** + * 处理弹框 - 主逻辑 + * - 仅填写处理说明,后端自动按当前状态推进 + */ + +import { useAntdStaticMethods } from '@utils/antDesign/popUp' +import { process } from '../api' +import type { Form, PageInfo } from '../types' +import type { WorkOrderStatus } from '../../../../types' + +/** 初始表单工厂 */ +const createInitForm = (): Form => ({ + id: '', + remark: '', + currentStatus: 'pending', +}) + +export const usePage = (emit: (e: 'load') => void) => { + const { message } = useAntdStaticMethods() + + const pageInfo = reactive({ + visible: false, + title: '处理工单', + width: 480, + spin: false, + }) + + const formRef = ref() + const form = ref(createInitForm()) + + const rules = { + remark: [{ required: true, message: '请输入处理说明', trigger: 'blur' }], + } + + const resetForm = (): void => { + formRef.value?.clearValidate() + form.value = createInitForm() + } + + const closeModal = (): void => { + resetForm() + pageInfo.visible = false + } + + const openModal = (id: string, currentStatus: WorkOrderStatus): void => { + pageInfo.visible = true + resetForm() + form.value.id = id + form.value.currentStatus = currentStatus + } + + const successRequest = (msg: string): void => { + void message.success(msg) + emit('load') + closeModal() + } + + const submit = (): void => { + pageInfo.spin = true + formRef.value + ?.validate() + .then(() => { + const params = { id: form.value.id, remark: form.value.remark } as Record + process(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/ops/workOrder/component/modal/process/process.vue b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/process.vue new file mode 100644 index 0000000..738f9b0 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/process.vue @@ -0,0 +1,70 @@ + + + diff --git a/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/types/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/types/index.ts new file mode 100644 index 0000000..fc9d933 --- /dev/null +++ b/src/pages/admin-portal/device-center/ops/workOrder/component/modal/process/types/index.ts @@ -0,0 +1,19 @@ +import type { WorkOrderStatus } from '../../../../types' + +/** 弹框页面状态 */ +export interface PageInfo { + visible: boolean + title: string + width: number + spin: boolean +} + +/** 表单字段 */ +export interface Form { + id: string + remark: string + currentStatus: WorkOrderStatus +} + +/** 暴露给父组件的 ref 类型 */ +export type ModalRef = InstanceType diff --git a/src/pages/admin-portal/device-center/ops/workOrder/init/usePage.ts b/src/pages/admin-portal/device-center/ops/workOrder/init/usePage.ts index b08a93c..6e2a61c 100644 --- a/src/pages/admin-portal/device-center/ops/workOrder/init/usePage.ts +++ b/src/pages/admin-portal/device-center/ops/workOrder/init/usePage.ts @@ -1,60 +1,159 @@ +import { onMounted, reactive, ref } from 'vue' +import type { TablePaginationConfig } from 'ant-design-vue' import { useAntdStaticMethods } from '@utils/antDesign/popUp' -import { useAdminPage } from '@pages/admin-portal/shared/useAdminPage' -import { list, remove, transition } from '../api' -import { tableColumns } from './useTable' -import { createSearchKey } from './useSearch' +import { createPaginationConfig } from '@utils/antDesign/table' +import type { TableState } from '@utils/antDesign/table' +import AddOrEdit from '../component/modal/addOrEdit/addOrEdit.vue' +import Dispatch from '../component/modal/dispatch/dispatch.vue' +import ProcessModal from '../component/modal/process/process.vue' +import InfoDrawer from '../component/drawer/info/info.vue' +import { list, remove } from '../api' import type { ListItem, SearchForm, WorkOrderStatus } from '../types' +import { createSearchKey } from './useSearch' +import { tableColumns } from './useTable' -/** 工单管理页:搜索 + 新增 + 工单流转(接单/关单) + 删除 */ +/** + * 工单管理页 - 主编排 + * - 标准列表 + 新增 / 派单 / 处理 / 详情 / 删除 5 个交互 + * - 操作列按状态展示按钮(未完成 4 个 / 已完成 2 个) + */ export const usePage = () => { const { message, Modal } = useAntdStaticMethods() - const adminPage = useAdminPage( - { - pageName: '工单管理', - addButtonText: '新增工单', - columns: tableColumns, - actions: ['edit', 'delete'], - listApi: list, - deleteApi: remove, - }, - createSearchKey, - ) + const search = reactive(createSearchKey()) - /** 工单流转:接单 / 关单 */ - const handleTransition = (record: ListItem, toStatus: WorkOrderStatus): void => { - const actionText = toStatus === 'assigned' ? '接单' : toStatus === 'processing' ? '开始处理' : '关单' + const table = reactive>({ + columns: tableColumns, + dataSource: [], + sort: { field: '', order: null }, + pagination: createPaginationConfig() as TablePaginationConfig, + }) + + const pageLoading = ref(false) + + const addOrEditRef = ref | null>(null) + const dispatchRef = ref | null>(null) + const processRef = ref | null>(null) + const infoDrawerRef = ref | null>(null) + + const buildParams = (): Record => ({ + ...search, + pageNum: table.pagination.current ?? 1, + pageSize: table.pagination.pageSize ?? 30, + order: table.sort.order, + column: table.sort.field, + }) + + const listRequest = (): void => { + pageLoading.value = true + list(buildParams() as Parameters[0]) + .then((res) => { + if (res.code === '00000') { + table.dataSource = res.data ?? [] + table.pagination.total = res.total ?? 0 + } + }) + .catch((err: unknown) => { + console.error('工单列表请求失败:', 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 + listRequest() + } + + const deleteRecord = (record: ListItem): void => { Modal.confirm({ - title: `确认${actionText}?`, - content: `工单号:${record.orderCode}`, - okType: toStatus === 'closed' ? 'danger' : 'primary', - okText: `确认${actionText}`, + title: '确认删除?', + content: `工单「${record.orderCode}」删除后不可恢复,请谨慎操作。`, + okType: 'danger', + okText: '确认删除', cancelText: '取消', onOk: () => { - transition(record.id, toStatus, toStatus === 'closed' ? undefined : '当前用户') + remove(record.id) .then((res) => { if (res.code === '00000') { void message.success(res.msg) - adminPage.listRequest() + listRequest() } }) .catch((err: unknown) => { - console.error('工单流转失败:', err) + console.error('工单删除失败:', err) }) }, }) } - /** 是否允许接单(仅 pending 状态) */ - const canAccept = (status: WorkOrderStatus): boolean => status === 'pending' + const openAddModal = (): void => { + addOrEditRef.value?.openModal() + } - /** 是否允许关单(仅 processing 状态) */ - const canClose = (status: WorkOrderStatus): boolean => status === 'processing' || status === 'assigned' + const openDispatchModal = (record: ListItem): void => { + dispatchRef.value?.openModal(record.id) + } + + const openProcessModal = (record: ListItem): void => { + processRef.value?.openModal(record.id, record.status as WorkOrderStatus) + } + + const openInfoDrawer = (record: ListItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id }) + } + + const handleAction = (type: string, record?: ListItem): void => { + if (type === 'add') { + openAddModal() + return + } + if (type === 'dispatch' && record) { + openDispatchModal(record) + return + } + if (type === 'process' && record) { + openProcessModal(record) + return + } + if (type === 'detail' && record) { + openInfoDrawer(record) + return + } + if (type === 'delete' && record) { + deleteRecord(record) + } + } + + onMounted(() => { + listRequest() + }) return { - ...adminPage, - handleTransition, - canAccept, - canClose, + pageLoading, + search, + table, + addOrEditRef, + dispatchRef, + processRef, + infoDrawerRef, + dataSourceChange, + searchQuery, + resetQuery, + handleAction, + listRequest, } } diff --git a/src/pages/admin-portal/device-center/ops/workOrder/init/useTable.ts b/src/pages/admin-portal/device-center/ops/workOrder/init/useTable.ts index 627bac3..cd0277e 100644 --- a/src/pages/admin-portal/device-center/ops/workOrder/init/useTable.ts +++ b/src/pages/admin-portal/device-center/ops/workOrder/init/useTable.ts @@ -68,5 +68,5 @@ export const tableColumns: TableColumnsType = [ { title: '责任人', dataIndex: 'assignee', align: 'center', width: 100, resizable: true, ellipsis: true }, { title: '报障时间', dataIndex: 'reportTime', align: 'center', width: 170, sorter: true, resizable: true }, { title: 'SLA 截止', dataIndex: 'slaDeadline', align: 'center', width: 170, resizable: true }, - { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 200 }, + { title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 280 }, ] diff --git a/src/pages/admin-portal/device-center/ops/workOrder/types/index.ts b/src/pages/admin-portal/device-center/ops/workOrder/types/index.ts index 42a6095..c22aa6d 100644 --- a/src/pages/admin-portal/device-center/ops/workOrder/types/index.ts +++ b/src/pages/admin-portal/device-center/ops/workOrder/types/index.ts @@ -1,6 +1,6 @@ import type { BaseListParams, BaseRecord, BaseSearchForm } from '@pages/admin-portal/shared/types' -/** 工单状态:接单 → 派单 → 处理中 → 关单 */ +/** 工单状态:待派单 → 已派单 → 处理中 → 已关单 */ export type WorkOrderStatus = 'pending' | 'assigned' | 'processing' | 'closed' /** 工单来源 */ @@ -9,6 +9,12 @@ export type OrderSource = 'customer' | 'inspection' | 'self-check' | 'after-sale /** 故障等级 */ export type FaultLevel = 'low' | 'medium' | 'high' | 'critical' +/** 故障类型 */ +export type FaultType = 'hardware' | 'software' | 'network' | 'mechanical' | 'electrical' | 'other' + +/** 工单流转动作(处理记录中的操作类型) */ +export type ProcessAction = 'create' | 'dispatch' | 'start' | 'progress' | 'close' + export interface SearchForm extends BaseSearchForm { keyword?: string status?: string @@ -30,8 +36,12 @@ export interface ListItem extends BaseRecord { source: OrderSource /** 故障等级 */ faultLevel: FaultLevel + /** 故障类型 */ + faultType: FaultType /** 故障描述 */ faultDesc: string + /** 故障图片 URL 列表 */ + images?: string[] /** 工单状态 */ status: WorkOrderStatus /** 责任人 */ @@ -46,8 +56,27 @@ export interface ListItem extends BaseRecord { export interface ListParams extends BaseListParams, SearchForm {} +/** 工单详情(含处理记录) */ +export interface DetailInfo extends ListItem { + /** 处理记录时间线 */ + processLogs: ProcessLog[] +} + +/** 处理记录 */ +export interface ProcessLog { + id: string + /** 操作类型 */ + action: ProcessAction + /** 操作人 */ + operator: string + /** 操作时间 */ + operateTime: string + /** 操作说明 */ + remark: string +} + export const WORK_ORDER_STATUS_OPTIONS = [ - { value: 'pending', label: '待接单' }, + { value: 'pending', label: '待派单' }, { value: 'assigned', label: '已派单' }, { value: 'processing', label: '处理中' }, { value: 'closed', label: '已关单' }, @@ -66,3 +95,57 @@ export const FAULT_LEVEL_OPTIONS = [ { value: 'high', label: '高' }, { value: 'critical', label: '紧急' }, ] + +export const FAULT_TYPE_OPTIONS = [ + { value: 'hardware', label: '硬件故障' }, + { value: 'software', label: '软件故障' }, + { value: 'network', label: '网络故障' }, + { value: 'mechanical', label: '机械故障' }, + { value: 'electrical', label: '电气故障' }, + { value: 'other', label: '其他' }, +] + +/** 故障类型 → 文本 */ +export const faultTypeText = (type: string): string => + FAULT_TYPE_OPTIONS.find((item) => item.value === type)?.label ?? '—' + +/** 工单状态 → 文本 */ +export const statusText = (status: string): string => + WORK_ORDER_STATUS_OPTIONS.find((item) => item.value === status)?.label ?? '—' + +/** 工单来源 → 文本 */ +export const sourceText = (source: string): string => + ORDER_SOURCE_OPTIONS.find((item) => item.value === source)?.label ?? '—' + +/** 故障等级 → 文本 */ +export const faultLevelText = (level: string): string => + FAULT_LEVEL_OPTIONS.find((item) => item.value === level)?.label ?? '—' + +/** 责任人候选(原型阶段固定枚举) */ +export const ASSIGNEE_OPTIONS = [ + { value: '李工程师', label: '李工程师' }, + { value: '王工程师', label: '王工程师' }, + { value: '赵工程师', label: '赵工程师' }, + { value: '孙工程师', label: '孙工程师' }, +] + +/** 客户候选(与设备中心客户档案 mock 数据对齐) */ +export const CUSTOMER_OPTIONS = [ + { value: '智养餐饮集团', label: '智养餐饮集团' }, + { value: '健康管理机构', label: '健康管理机构' }, + { value: '国庆能源总部食堂', label: '国庆能源总部食堂' }, + { value: '第一采油厂', label: '第一采油厂' }, + { value: '炼化分公司', label: '炼化分公司' }, + { value: '健康示范园区', label: '健康示范园区' }, + { value: '江南大学后勤处', label: '江南大学后勤处' }, + { value: '市第一人民医院', label: '市第一人民医院' }, +] + +/** 场所候选(与设备中心场所 mock 数据对齐) */ +export const SITE_OPTIONS = [ + { value: '总部食堂', label: '总部食堂' }, + { value: '总部健身房', label: '总部健身房' }, + { value: '健康站01', label: '健康站01' }, + { value: '东区体检站', label: '东区体检站' }, + { value: '健康站02', label: '健康站02' }, +] diff --git a/src/pages/admin-portal/device-center/ops/workOrder/workOrder.vue b/src/pages/admin-portal/device-center/ops/workOrder/workOrder.vue index f3daeef..c067057 100644 --- a/src/pages/admin-portal/device-center/ops/workOrder/workOrder.vue +++ b/src/pages/admin-portal/device-center/ops/workOrder/workOrder.vue @@ -33,23 +33,37 @@ - + + + +
diff --git a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/api/index.ts b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/api/index.ts similarity index 63% rename from src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/api/index.ts rename to src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/api/index.ts index 5770f4e..b6fda8e 100644 --- a/src/pages/admin-portal/device-center/distribute/deadletter/component/drawer/info/api/index.ts +++ b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/api/index.ts @@ -1,9 +1,10 @@ /** - * 死信详情抽屉 - 接口 + * 入库管理 - 详情抽屉 - 接口层 */ + import type { ApiResponse } from '@axios' import { postRequest } from '@axios' import type { DetailInfo } from '../types' export const getInfo = (id: string): Promise> => - postRequest('axiosRequest', '/admin/device/distribute/deadletter/info', { id }) + postRequest('axiosRequest', '/admin/device/stock/inbound/info', { id }) diff --git a/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/info.vue new file mode 100644 index 0000000..80631d1 --- /dev/null +++ b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/info.vue @@ -0,0 +1,124 @@ + + + + + diff --git a/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/init/usePage.ts b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/init/usePage.ts new file mode 100644 index 0000000..6ca2005 --- /dev/null +++ b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/init/usePage.ts @@ -0,0 +1,53 @@ +/** + * 入库管理 - 详情抽屉 - 主逻辑(只读) + * - 展示入库单全字段 + 设备明细列表(items) + */ + +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: 760, + }) + + 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/inbound/component/drawer/info/types/index.ts b/src/pages/admin-portal/device-center/stock/inbound/component/drawer/info/types/index.ts new file mode 100644 index 0000000..f038fd1 --- /dev/null +++ b/src/pages/admin-portal/device-center/stock/inbound/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/inbound/inbound.vue b/src/pages/admin-portal/device-center/stock/inbound/inbound.vue index eefbe7d..9407e51 100644 --- a/src/pages/admin-portal/device-center/stock/inbound/inbound.vue +++ b/src/pages/admin-portal/device-center/stock/inbound/inbound.vue @@ -54,7 +54,7 @@ @click="handleAction('audit', record)" >审核 查看台账 + + 查看详情 + @@ -92,6 +99,7 @@ import DeviceCategoryPicker from '@components/DeviceCategoryPicker.vue' import WarehouseAreaPicker from '@components/WarehouseAreaPicker.vue' import ExportTaskDrawer from '@components/ExportTaskDrawer.vue' import AddOrEdit from './component/modal/addOrEdit/addOrEdit.vue' +import InfoDrawer from './component/drawer/info/info.vue' import { INBOUND_STATUS_OPTIONS, INBOUND_TYPE_OPTIONS } from './types' import { usePage } from './init/usePage' @@ -100,6 +108,7 @@ const { search, table, addOrEditRef, + infoDrawerRef, exportTaskRef, dataSourceChange, searchQuery, 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 4306e8c..a0b3bbc 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 @@ -7,11 +7,12 @@ 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 InfoDrawer from '../component/drawer/info/info.vue' import type ExportTaskDrawer from '@components/ExportTaskDrawer.vue' /** * 入库管理页主编排: - * - 列表 + 新增 + 编辑 + 删除 + 导出 + * - 列表 + 新增 + 编辑 + 删除 + 导出 + 查看详情 * - 完整表单弹框(含入库类型 / 批次号 / 采购价等设计文档字段) */ export const usePage = () => { @@ -21,6 +22,7 @@ export const usePage = () => { const pageLoading = ref(false) const addOrEditRef = ref | null>(null) + const infoDrawerRef = ref | null>(null) const exportTaskRef = ref | null>(null) const buildParams = () => ({ @@ -77,6 +79,11 @@ export const usePage = () => { addOrEditRef.value?.openModal('edit', { id: record.id }) } + /** 打开详情抽屉(只读,含设备明细) */ + const openInfoDrawer = (record: ListItem): void => { + infoDrawerRef.value?.openDrawer({ id: record.id, title: record.inboundCode }) + } + const deleteRecord = (record: ListItem): void => { Modal.confirm({ title: '确认删除?', @@ -144,6 +151,7 @@ export const usePage = () => { 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 === 'audit' && record) auditRecord(record) else if (type === 'ledger' && record) viewDeviceLedger(record) @@ -165,6 +173,7 @@ export const usePage = () => { selectedRowKeys, rowSelection, addOrEditRef, + infoDrawerRef, exportTaskRef, dataSourceChange, searchQuery, diff --git a/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/info.vue b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/info.vue index 4415489..d68fd6f 100644 --- a/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/info.vue +++ b/src/pages/admin-portal/device-center/stock/outbound/component/drawer/info/info.vue @@ -34,6 +34,27 @@ {{ detail.cancelledTime ?? '—' }} + + +
+
+ 设备明细 + 共 {{ detail.items.length }} 条 +
+ + + +