feat: 设备版本管理 OTA 升级任务抽屉落地 + 适配类目上移至应用级

- 新增版本详情页 OTA 升级任务抽屉子模块(列表 + 新建任务弹框,遵循列表页 6 文件结构)
- 适配类目 deviceCategories 由 VersionItem 上移至 AppItem(应用级共享,所有版本复用)
- VersionItem 新增 pendingUpdateCount 字段(待更新设备数),原 deviceCategories 列移除
- 应用列表新增适配类目列展示(a-tag 多类目渲染),编辑弹框同步调整
- 同步 mock handler 与设备管理服务器功能现状文档

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
fengpu
2026-07-10 10:02:36 +08:00
co-authored by Claude Opus 4.7
parent 68ea2a1167
commit 6d2e885fd0
25 changed files with 1532 additions and 225 deletions
+120 -25
View File
@@ -28,8 +28,8 @@ interface VersionRecord {
status: VersionStatus
/** 再用设备数(mock 静态) */
usingDeviceCount: number
/** 适配类目:设备分类 code 数组 */
deviceCategories: string[]
/** 待更新设备数(尚未升级到本版本的设备数) */
pendingUpdateCount: number
/** 软件包文件名 */
packageName: string
/** 软件包大小(字节) */
@@ -48,6 +48,8 @@ interface AppRecord {
/** 应用描述 */
description: string
status: AppStatus
/** 适配类目(应用级配置,所有版本共享) */
deviceCategories: string[]
}
interface CommonParams {
@@ -76,76 +78,68 @@ const deviceCategoryText: Record<string, string> = {
}
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' },
{ id: 'app-cabinet-fw', name: '净菜柜固件', devSubject: '寰汐科技(上海)有限公司', description: '净菜柜主控板固件,负责开门控制、温控、 OTA 升级', status: 'enabled', deviceCategories: ['cabinet'] },
{ id: 'app-cabinet-app', name: '净菜柜客户端 APP', devSubject: '寰汐科技(上海)有限公司', description: '净菜柜前端交互应用,提供扫码开门、菜品展示、订单查询', status: 'enabled', deviceCategories: ['cabinet'] },
{ id: 'app-watch-fw', name: '智能手表固件', devSubject: '深圳瑞康智能科技有限公司', description: '智能健康手表底层固件,由供应商提供底包 + 自研算法层', status: 'enabled', deviceCategories: ['watch'] },
{ id: 'app-healthcheck-fw', name: '体检一体机固件', devSubject: '寰汐科技(上海)有限公司', description: '体检一体机主控固件,集成血压 / 血尿酸 / 体脂多项检测', status: 'disabled', deviceCategories: ['healthCheck'] },
{ id: 'app-bodyfat-fw', name: '体脂仪固件', devSubject: '江苏康体电子科技有限公司', description: '体脂仪底层固件,第三方厂商定制开发', status: 'enabled', deviceCategories: ['bodyFat'] },
{ id: 'app-scale-app', name: '健康量表 APP', devSubject: '寰汐科技(上海)有限公司', description: '客户终端健康量表自评 APP(暂未发布任何版本)', status: 'enabled', deviceCategories: ['scale'] },
]
const versionList: VersionRecord[] = [
{
id: 'ver-001', appId: 'app-cabinet-fw', version: 'V1.2.0', versionName: '稳定版',
releaseNote: '优化货柜开门响应速度\n修复温度传感器偶发漂移\n新增离线缓存支持',
forceUpdate: false, status: 'active', usingDeviceCount: 12,
deviceCategories: ['cabinet'],
forceUpdate: false, status: 'active', usingDeviceCount: 12, pendingUpdateCount: 5,
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', appId: 'app-cabinet-fw', version: 'V1.1.5', versionName: '稳定版',
releaseNote: '修复开门指令偶发丢失问题',
forceUpdate: true, status: 'disabled', usingDeviceCount: 0,
deviceCategories: ['cabinet'],
forceUpdate: true, status: 'disabled', usingDeviceCount: 0, pendingUpdateCount: 0,
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', appId: 'app-cabinet-fw', version: 'V1.1.0', versionName: '稳定版',
releaseNote: '首版支持远程开门\n新增温度上报',
forceUpdate: false, status: 'active', usingDeviceCount: 3,
deviceCategories: ['cabinet'],
forceUpdate: false, status: 'active', usingDeviceCount: 3, pendingUpdateCount: 14,
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', appId: 'app-cabinet-app', version: 'V1.1.0', versionName: '灰度版',
releaseNote: '新增会员登录\n优化 UI 加载速度\n修复部分小屏适配问题',
forceUpdate: false, status: 'active', usingDeviceCount: 6,
deviceCategories: ['cabinet'],
forceUpdate: false, status: 'active', usingDeviceCount: 6, pendingUpdateCount: 8,
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', appId: 'app-cabinet-app', version: 'V1.0.0', versionName: '首版',
releaseNote: '货柜客户端首版\n支持扫码开门、菜品展示、订单查询',
forceUpdate: true, status: 'disabled', usingDeviceCount: 0,
deviceCategories: ['cabinet'],
forceUpdate: true, status: 'disabled', usingDeviceCount: 0, pendingUpdateCount: 0,
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', appId: 'app-watch-fw', version: 'V2.0.1', versionName: '灰度版',
releaseNote: '心率监测算法升级\n新增睡眠分析\n优化电池续航',
forceUpdate: true, status: 'active', usingDeviceCount: 8,
deviceCategories: ['watch'],
forceUpdate: true, status: 'active', usingDeviceCount: 8, pendingUpdateCount: 3,
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', appId: 'app-healthcheck-fw', version: 'V3.0.0', versionName: '稳定版',
releaseNote: '体检一体机 V3 主版本\n新增血尿酸检测\n优化血压采样算法',
forceUpdate: false, status: 'disabled', usingDeviceCount: 0,
deviceCategories: ['healthCheck'],
forceUpdate: false, status: 'disabled', usingDeviceCount: 0, pendingUpdateCount: 0,
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', appId: 'app-bodyfat-fw', version: 'V1.5.0', versionName: '稳定版',
releaseNote: '体脂算法同步 InBody 最新公式\n阻抗采样精度提升',
forceUpdate: false, status: 'active', usingDeviceCount: 2,
deviceCategories: ['bodyFat'],
forceUpdate: false, status: 'active', usingDeviceCount: 2, pendingUpdateCount: 6,
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: '孙工程师',
},
@@ -165,6 +159,7 @@ const buildAppSummary = (app: AppRecord) => {
devSubject: app.devSubject,
description: app.description,
status: app.status,
deviceCategories: app.deviceCategories,
latestVersion,
versionCount,
updateTime,
@@ -192,6 +187,7 @@ const infoAppForEdit = (ctx: MockContext): ApiResponse<Partial<AppRecord> | null
return ok('查询成功', {
id: target.id, name: target.name, devSubject: target.devSubject,
description: target.description, status: target.status,
deviceCategories: target.deviceCategories,
})
}
@@ -204,6 +200,7 @@ const addApp = (ctx: MockContext): ApiResponse<null> => {
devSubject: String(params.devSubject ?? ''),
description: String(params.description ?? ''),
status: (params.status as AppStatus) ?? 'enabled',
deviceCategories: Array.isArray(params.deviceCategories) ? (params.deviceCategories as string[]) : [],
})
return ok('新增成功')
}
@@ -215,6 +212,7 @@ const updateApp = (ctx: MockContext): ApiResponse<null> => {
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)
if (params.deviceCategories !== undefined) target.deviceCategories = Array.isArray(params.deviceCategories) ? (params.deviceCategories as string[]) : []
return ok('更新成功')
}
@@ -255,7 +253,7 @@ const addVersion = (ctx: MockContext): ApiResponse<null> => {
forceUpdate: Boolean(params.forceUpdate),
status: (params.status as VersionStatus) ?? 'active',
usingDeviceCount: 0,
deviceCategories: Array.isArray(params.deviceCategories) ? (params.deviceCategories as string[]) : [],
pendingUpdateCount: 0,
packageName: String(params.packageName ?? ''),
packageSize: Number(params.packageSize) || 0,
updateTime: ts,
@@ -274,6 +272,99 @@ const toggleVersion = (ctx: MockContext): ApiResponse<null> => {
return ok('状态已更新')
}
// ============ OTA 升级相关 mock ============
type OtaUpdateStatus = 'updated' | 'pending'
interface OtaDeviceRecord {
id: string
deviceCode: string
customerId: string
customerName: string
siteId: string
siteName: string
updateStatus: OtaUpdateStatus
previousVersion: string
updateTime: string
}
interface SiteTreeNode {
id: string
name: string
customerId: string
customerName: string
deviceCount: number
}
interface CustomerTreeNode {
id: string
name: string
sites: SiteTreeNode[]
}
/** OTA 设备列表(所有版本共享同一份 mock 数据) */
const otaDeviceList: OtaDeviceRecord[] = [
{ id: 'd-001', deviceCode: 'CAB-SH-001', customerId: 'c-001', customerName: '上海禧月餐饮', siteId: 's-001', siteName: '浦东旗舰店', updateStatus: 'updated', previousVersion: 'V1.1.0', updateTime: '2026-06-26 10:00:00' },
{ id: 'd-002', deviceCode: 'CAB-SH-002', customerId: 'c-001', customerName: '上海禧月餐饮', siteId: 's-001', siteName: '浦东旗舰店', updateStatus: 'updated', previousVersion: 'V1.1.0', updateTime: '2026-06-26 11:30:00' },
{ id: 'd-003', deviceCode: 'CAB-SH-003', customerId: 'c-001', customerName: '上海禧月餐饮', siteId: 's-002', siteName: '徐汇社区店', updateStatus: 'pending', previousVersion: 'V1.1.0', updateTime: '' },
{ id: 'd-004', deviceCode: 'CAB-SH-004', customerId: 'c-001', customerName: '上海禧月餐饮', siteId: 's-002', siteName: '徐汇社区店', updateStatus: 'pending', previousVersion: 'V1.1.0', updateTime: '' },
{ id: 'd-005', deviceCode: 'CAB-SZ-001', customerId: 'c-002', customerName: '苏州颐康养老', siteId: 's-003', siteName: '姑苏照护中心', updateStatus: 'pending', previousVersion: 'V1.1.0', updateTime: '' },
{ id: 'd-006', deviceCode: 'CAB-SZ-002', customerId: 'c-002', customerName: '苏州颐康养老', siteId: 's-004', siteName: '工业园便利点', updateStatus: 'updated', previousVersion: 'V1.1.0', updateTime: '2026-06-27 09:15:00' },
{ id: 'd-007', deviceCode: 'CAB-HZ-001', customerId: 'c-003', customerName: '杭州悦食食堂', siteId: 's-005', siteName: '西湖餐厅', updateStatus: 'pending', previousVersion: 'V1.1.0', updateTime: '' },
{ id: 'd-008', deviceCode: 'CAB-HZ-002', customerId: 'c-003', customerName: '杭州悦食食堂', siteId: 's-005', siteName: '西湖餐厅', updateStatus: 'updated', previousVersion: 'V1.1.0', updateTime: '2026-06-28 14:20:00' },
]
/** 客户场所树(含场所下未更新设备数) */
const otaCustomerTree: CustomerTreeNode[] = [
{
id: 'c-001', name: '上海禧月餐饮', sites: [
{ id: 's-001', name: '浦东旗舰店', customerId: 'c-001', customerName: '上海禧月餐饮', deviceCount: 0 },
{ id: 's-002', name: '徐汇社区店', customerId: 'c-001', customerName: '上海禧月餐饮', deviceCount: 2 },
],
},
{
id: 'c-002', name: '苏州颐康养老', sites: [
{ id: 's-003', name: '姑苏照护中心', customerId: 'c-002', customerName: '苏州颐康养老', deviceCount: 1 },
{ id: 's-004', name: '工业园便利点', customerId: 'c-002', customerName: '苏州颐康养老', deviceCount: 0 },
],
},
{
id: 'c-003', name: '杭州悦食食堂', sites: [
{ id: 's-005', name: '西湖餐厅', customerId: 'c-003', customerName: '杭州悦食食堂', deviceCount: 1 },
],
},
]
const listOtaDevice = (ctx: MockContext): ApiResponse<OtaDeviceRecord[]> => {
const params = getParams(ctx.params)
const filtered = otaDeviceList.filter((item) => {
if (params.keyword && !includes(item.deviceCode, params.keyword)) return false
if (params.customerId && item.customerId !== params.customerId) return false
if (params.siteId && item.siteId !== params.siteId) return false
if (params.updateStatus && item.updateStatus !== params.updateStatus) return false
return true
})
return ok('查询成功', filtered)
}
const getOtaCustomerTree = (ctx: MockContext): ApiResponse<CustomerTreeNode[]> => {
const params = getParams(ctx.params)
void params
return ok('查询成功', otaCustomerTree)
}
const addOtaTask = (ctx: MockContext): ApiResponse<null> => {
const params = getParams(ctx.params)
const forceUpdate = Boolean(params.forceUpdate)
const scope = String(params.scope ?? 'site')
if (forceUpdate || scope === 'all') {
return ok('已发起全量 OTA 升级任务,将覆盖应用适用的所有类目下的全部设备')
}
const siteIds = Array.isArray(params.siteIds) ? (params.siteIds as string[]) : []
if (!siteIds.length) return ok('未选择任何场所,任务未创建')
return ok(`已发起 OTA 升级任务,覆盖 ${siteIds.length} 个场所(共 ${Number(params.deviceCount) || 0} 台设备)`)
}
registerMocks([
// 应用
{ method: 'POST', url: '/admin/device/ops/version/app/list', handler: listApp },
@@ -286,4 +377,8 @@ registerMocks([
{ method: 'POST', url: '/admin/device/ops/version/info', handler: infoVersion },
{ method: 'POST', url: '/admin/device/ops/version/add', handler: addVersion },
{ method: 'POST', url: '/admin/device/ops/version/toggle', handler: toggleVersion },
// OTA 升级
{ method: 'POST', url: '/admin/device/ops/version/ota/device/list', handler: listOtaDevice },
{ method: 'POST', url: '/admin/device/ops/version/ota/customer-tree', handler: getOtaCustomerTree },
{ method: 'POST', url: '/admin/device/ops/version/ota/task/add', handler: addOtaTask },
])
@@ -4,22 +4,23 @@
>
> 老系统参考后台地址:https://device.shuziweidao.com/canteen/home,登录账号:admin,登录密码:Q4Zz64285886@629
>
> _Last updated: 2026-07-03_
> _Last updated: 2026-07-09_
---
## 一、系统定位与业务边界
### 1.1 系统定位(大职能)
### 1.1 系统定位(大职能)
本系统具有**大职能**
本系统具有**大职能**
| 职能 | 服务对象 | 业务范围 |
|------|---------|---------|
| **设备资产中台** | 所有硬件设备(食堂 + 体重管理) | 设备台账、库存、配置——只管资产,不管业务数据 |
| **设备租售履约** | 租售给客户的设备 | 订单、租约、售后——把设备作为商品进行全生命周期履约 |
| **设备租售履约** | 租售给客户的设备 | 订单、租约、售后——把设备作为商品进行全生命周期履约**本期不做** |
| **设备运维巡检** | 所有现场在用设备 | 实施装机、自检、巡检、运行监控——保障设备健康度 |
| **体重管理数据分发** | 仅体重管理类设备 | 数据采集、归档、分发到下游业务系统 |
| **数据分发** | 所有开启数据分发的 IOT 设备 | 上报通道配置(设备→平台)+ 分发规则配置(平台→下游)+ 分发记录可查可重投 |
| **第三方授权中台** | 所有依赖外部授权资源的设备 | 虹软人脸激活码、高德地图 ApiKey、SIM 卡生命周期——外部授权资源集中管理 |
**核心边界澄清**
- 食堂类设备(大屏、餐盘柜、货架、入库秤、配比秤)的业务数据**不过本平台**,只在食堂本地系统跑
@@ -52,7 +53,7 @@
## 二、菜单分类规划
6 大业务域重组菜单,替代原"3 子应用平铺"结构。**菜单层级最多 3 级**(一级业务域 → 二级菜单 → 三级叶子),不再出现 4 级嵌套。
**7 大业务域**重组菜单,替代原"3 子应用平铺"结构。**菜单层级最多 3 级**(一级业务域 → 二级菜单 → 三级叶子),不再出现 4 级嵌套。
### 2.1 菜单总览
@@ -60,8 +61,7 @@
设备管理服务器
├─ 1. 工作台
实时监控大屏设备在线率 / 数据吞吐 / 异常告警
│ └─ 数据概览 ← 关键指标卡片
数据概览 KPI 卡片 + 资产结构环形图 + 运行状态 + 租户排行(实时监控大屏能力合入)
├─ 2. 设备资产 ← 设备分类与设备实例统一管理
│ ├─ 设备分类 ← 左树 + 右侧"类别详情";分类层级携带"是否 IOT 设备""是否启用数据分发"标识
@@ -76,39 +76,44 @@
│ └─ 库存盘点 ← 盘点单管理(生成 / 执行 / 审核),自动比对盘盈盘亏
├─ 4. 客户管理 ← 原"租户与场所"更名;客户档案 + 布点场所 + 供应商档案
│ ├─ 客户档案 ← 只读复用运营中心 tenantList无新增/编辑/删除等操作按钮
│ ├─ 客户档案 ← 只读复用运营中心 tenantList支持查看详情 + 下钻查看该客户布点场所概览
│ ├─ 布点场所 ← N:1 客户;档案含"自定义标识"字段
│ └─ 供应商管理 ← 供应商档案(从库存管理迁入)
├─ 5. 设备运维 ← 主动运维 + 被动维修 + 系统配置合一,承接原"系统设置"域
│ ├─ 设备初始化 ← 出库装机前的激活 / 预配置 / 入网
│ ├─ 设备监测 ← 主列表:设备运行状态总览;详情页多 Tab 聚合实施/自检/巡检/运行日志/更新/维修记录
│ ├─ 工单管理 ← 工单接派 / 处理 / 关单(原"报修工单"更名
│ ├─ 版本管理 ← 固件 + APP 双轨版本(原系统设置迁入
─ 告警管理 ← 运行/自检/巡检/SLA 超时等告警统一处置 + 业务日志视图
│ └─ 系统配置 ← 全局参数(原系统设置迁入)
├─ 5. 设备运维 ← 主动运维 + 被动维修合一,承接原"系统设置"域中的版本/告警(系统配置不再独立菜单)
│ ├─ 初始化设置IOT 设备出库投用前的软件版本设置;设备由出库流程产生,不支持新增
│ ├─ 设备监测 ← 主列表:设备运行状态总览(含多维度筛选);详情页多 Tab 聚合实施/自检/巡检/运行日志/更新/维修记录
│ ├─ 工单管理 ← 工单生命周期(派/接/处理/关单),含维修上报(关单前必填
│ ├─ 版本管理 ← 应用软件维度管理(一级:应用档案 + 状态启停;二级:版本列表,含固件/APP/应用软件
─ 告警管理 ← 运行/自检/巡检/SLA 超时/OTA 失败/数据分发失败等告警统一处置
─ 6. 数据分发 ← 原"数据中转"更名,本期仅支持接口回调方式
├─ 分发配置 ← 下游系统注册 + 订阅映射关系融合(原"下游系统 + 订阅关系"合并
├─ 分发记录每条分发状态(成功/失败/重投中)+ HTTP 响应码 + 手动重投
└─ 失败重投 ← 死信队列集中重投
─ 6. 数据分发 ← 原"数据中转"更名,本期仅支持接口回调方式;上下游闭环
├─ 分发配置 ← 下游系统注册 + 订阅映射关系融合 + 内侧"分发记录"子页面(hidden
└─ 上报配置设备 → 平台的上报通道配置(客户/场所/通道名称/鉴权开关/状态)
└─ 7. 第三方授权 ← 外部授权资源中台化管理(原"暂未归属"的分项统一收纳)
├─ 虹软人脸授权 ← 按设备绑定激活码 + AppID;支持批量导入激活码
├─ 高德地图授权 ← 按应用软件管理 ApiKey + 启用/停用
└─ SIM 卡管理 ← SIM 卡生命周期管理(新增/批量导入/导出/绑定设备/解绑/绑定记录)
```
> **菜单调整说明(2026-07-02 修订**
> **菜单调整说明(最终原型,截至 2026-07-09**
> 1. **拍平 4 级嵌套**:原"设备资产 → 设备库存 → 5 个子菜单""设备资产 → 故障与维修 → 2 个子菜单""数据中转 → 下游分发 → 3 个子菜单""系统设置 → 设备版本管理 → 2 个子菜单"均为 4 级,本期全部拍平到 3 级。
> 2. **设备库存更名 + 菜单重组**:原"设备库存"更名为"库存管理"5 个二级菜单——"出库单"与"出库管理"融合(出库单作为出库管理内的视图/详情,不再单列菜单);"库存明细"更名为"在库库存"以突出"在库状态"视角;新增"仓库区域"承载多仓库档案;新增"库存盘点"承载盘点单管理;供应商管理迁出到客户管理域(详见第 6 条)。
> 3. **设备运维扩域 + 承接系统设置**:原"系统设置"一级域取消,其 4 个二级菜单全部迁入"设备运维"域——版本管理直接迁入;更新记录迁入"设备监测"详情页 Tab系统配置直接迁入业务日志并入"告警管理"作为日志视图。运维域共 6 个二级菜单(设备初始化 / 设备监测 / 工单管理 / 版本管理 / 告警管理 / 系统配置)。
> 4. **设备监测聚合多视角**:原"实施记录 / 自检记录 / 巡检记录 / 运行日志 / 维修记录"5 个独立二级菜单收敛为"设备监测"1 个二级菜单——主列表页统一展示设备运行状态总览,详情页以 Tab 形式聚合 5 个视角 + 更新记录共 6 个 Tab
> 5. **数据中转更名 + 简化为接口回调**:原"数据中转"更名为"数据分发",本期仅支持"接口回调"模式(平台收到设备上报数据后按规则 HTTP 回调下游)。原 6 个子菜单(采集设备/采集监控/订阅关系/投递记录/死信重投/数据归档)精简为 3 个——"下游系统 + 订阅关系"融合为"分发配置""投递记录"更名为"分发记录""死信重投"更名为"失败重投""采集设备/采集监控"并入设备资产/设备监测(不再重复建表)"数据归档"后置 P1。
> 6. **客户管理扩域 + 更名**:原"客户与场所"先改为"租户与场所",本期再次改为"客户管理"(与运营中心"租户"概念区分);"租户管理"改为"客户档案"且只读复用运营中心 tenantList;供应商管理从库存管理迁入(属于"商业合作伙伴"档案而非"仓储资产")。
> 7. **设备资产瘦身**:仅保留"设备分类 / 设备管理 / 设备统计"3 个二级菜单原"设备配置 / 大屏内容管理"**暂未归属,后续讨论**其归宿
> 8. **设备租售本期不做**:原"设备租售"域本期不建设,相关章节标注"本期不做"保留设计思路供未来参考
> 3. **设备运维扩域 + 承接系统设置(精简)**:原"系统设置"一级域取消——版本管理直接迁入"设备运维"(改为"应用软件 → 版本列表"两级结构);更新记录迁入"设备监测"详情页 Tab告警管理迁入业务日志作为告警管理日志视图);**系统配置被移除**(原型落地时判定全局参数独立建页价值有限,相关阈值/策略分散到设备分类、告警管理、初始化设置各自承载)。运维域共 5可见二级菜单 + 1 个隐藏子菜单(版本列表)。
> 4. **设备监测聚合多视角**:原"实施记录 / 自检记录 / 巡检记录 / 运行日志 / 维修记录"5 个独立二级菜单收敛为"设备监测"1 个二级菜单——主列表页统一展示设备运行状态总览,详情页以 Tab 形式聚合 6 个视角(实施记录 / 自检记录 / 巡检记录 / 运行日志 / 更新记录 / 维修记录)
> 5. **数据中转更名 + 重构为上下游闭环**:原"数据中转"更名为"数据分发",本期仅支持"接口回调"模式。原 6 个子菜单精简为 2 个——"下游系统 + 订阅关系"融合为"分发配置"(内侧"分发记录"作为 hidden 子页面);**"失败重投"独立菜单取消**(失败记录在分发记录中筛选 + 详情页手动重投承载);**新增"上报配置"**单独建二级菜单,配置设备数据上报到平台的通道(客户/场所/鉴权/状态),与"分发配置"形成设备→平台 vs 平台→下游的闭环。原"采集设备/采集监控"并入设备资产/设备监测;"数据归档"后置 P1。
> 6. **客户管理扩域 + 更名**:原"客户与场所"先改为"租户与场所",本期再次改为"客户管理"(与运营中心"租户"概念区分);"租户管理"改为"客户档案"且只读复用运营中心 tenantList,支持查看详情 + 下钻查看该客户下布点场所概览(siteBrief 子组件);供应商管理从库存管理迁入(属于"商业合作伙伴"档案而非"仓储资产")。
> 7. **设备资产瘦身 + 暂未归属项归宿明确**:仅保留"设备分类 / 设备管理 / 设备统计"3 个二级菜单原"设备配置 / 大屏内容管理"的归宿——虹软 SDK 授权迁入**新增的"第三方授权"**域(与高德地图授权、SIM 卡管理共同构成"外部授权资源中台");大屏内容管理**不做**(本期取下面向大屏的内容编排功能)
> 8. **新增第三方授权域**:虹软人脸授权(按设备绑定激活码 + 批量导入)、高德地图授权(ApiKey + 启停管理)、SIM 卡管理(生命周期 + 绑定记录)三者共同构成新增的"第三方授权"一级业务域,覆盖原"设备配置"下的人脸 SDK 授权,并扩展了地图 Key 管理和 SIM 卡生命周期管理。该域不再属于"设备运维 → 系统配置"的子项或"暂未归属"
> 9. **工作台瘦身**:原规划的"实时监控大屏 + 数据概览"两个二级菜单收敛为 1 个"数据概览"——设备在线率、数据吞吐、租户排行等关键指标通过 KPI 卡 + 环形图 + Top 5 表格内聚在一个页面展示,实时监控大屏能力合入数据概览而非独立建页。
> 10. **设备租售本期不做**:销售/租约/售后履约不在本期建设范围,相关章节标注"本期不做"保留设计思路供未来参考。
### 2.2 与原菜单的映射关系
| 原页面 | 新位置 |
|--------|-------|
| 食堂数字化 home / 智慧餐厅 home / 体重管理 home | 工作台 → 实时监控大屏 / 数据概览 |
| 食堂数字化 home / 智慧餐厅 home / 体重管理 home | 工作台 → 数据概览(实时监控能力内聚) |
| 食堂设备(大屏 / 餐盘柜 / 货架 / 入库秤 / 配比秤) | 设备资产 → 设备管理("智养餐饮设备" Tab |
| 配比秤 / 智能货架 / 自动入库 / 智能餐盘柜 | 设备资产 → 设备管理("智养餐饮设备" Tab |
| 设备新增编辑 | 设备资产 → 设备管理(按 Tab 切换) |
@@ -120,8 +125,8 @@
| 仓库档案 | 库存管理 → 仓库区域 |
| 盘点单 | 库存管理 → 库存盘点 |
| 供应商 | 客户管理 → 供应商管理(从库存管理迁入) |
| 大屏设备列表 | 暂未归属,后续讨论(原"大屏内容管理" |
| 虹软 SDK 授权 | 暂未归属,后续讨论(原"设备配置" |
| 大屏设备列表 / 大屏内容管理 | **本期不做**(面向大屏内容编排功能本期不建设 |
| 虹软 SDK 授权 | 第三方授权 → 虹软人脸授权(从"暂未归属"纳入新域 |
| 客户列表 | 客户管理 → 客户档案(只读复用运营中心 tenantList |
| 食堂列表 / 同步 | 客户管理 → 布点场所 |
| 小型 / 大型体重秤 | 设备资产 → 设备管理("体重测量设备" Tab);数据分发按分类订阅 |
@@ -129,16 +134,22 @@
| 顿米健身器材 | 设备资产 → 设备管理("健身器材设备" Tab);数据分发按分类订阅 |
| 华盛一体机 | 设备资产 → 设备管理("健康监测设备" Tab);数据分发按分类订阅 |
| 鹏升 / 聚伟 / 金亿帝手表 | 设备资产 → 设备管理("智能穿戴设备" Tab);数据分发按分类订阅 |
| APP 版本管理 | 设备运维 → 版本管理 |
| APP 版本管理 | 设备运维 → 版本管理(改为"应用软件 → 版本列表"两级) |
| 业务日志列表 | 设备运维 → 告警管理(日志视图) |
| **(新增)设备统计** | 设备资产 → 设备统计 |
| **(新增)装机 / 自检 / 巡检 / 运行日志 / 维修记录** | 设备运维 → 设备监测(详情页 Tab 聚合,不再独立二级菜单) |
| **(新增)设备激活预配置** | 设备运维 → 设备初始化 |
| **(新增)设备预配置** | 设备运维 → 初始化设置(软件版本设置维度,非全流程初始化 |
| **(新增)OTA 推送记录** | 设备运维 → 设备监测.详情.更新记录 Tab |
| **(新增)告警统一处置** | 设备运维 → 告警管理 |
| **(新增)下游系统 + 订阅关系(融合)** | 数据分发 → 分发配置 |
| **(新增)分发记录 / 失败重投** | 数据分发 → 分发记录 / 失败重投 |
| **(新增)设备上报通道配置** | 数据分发 → 上报配置(新增概念,平台→下游 vs 设备→平台 闭环) |
| **(新增)分发记录(子页面)** | 数据分发 → 分发配置.内侧"分发记录"子页(hidden 路由) |
| **(新增)虹软人脸授权激活码管理** | 第三方授权 → 虹软人脸授权 |
| **(新增)高德地图 ApiKey 管理** | 第三方授权 → 高德地图授权 |
| **(新增)SIM 卡生命周期管理** | 第三方授权 → SIM 卡管理 |
| ~~订单 / 租约 / 售后~~ | 设备租售 → 本期不做(保留设计思路供未来参考) |
| ~~失败重投独立菜单~~ | **取消独立菜单**;失败记录在分发记录中筛选 + 详情页手动重投承载 |
| ~~系统配置独立菜单~~ | **取消独立菜单**;全局阈值/策略分散到分类/告警/初始化设置各自承载 |
### 2.3 菜单设计原则
@@ -147,14 +158,15 @@
3. **设备资产统一管理**,不再按食堂 / 体重管理分库
4. **设备管理按顶级分类 Tab 切换**:5 个顶级分类作为顶部 Tab 呈现,每类设备字段差异在各自 Tab 内承载,避免一张表硬塞所有字段
5. **数据分发标识上提到分类层**:是否 IOT 设备、是否启用数据分发,统一在"设备分类"层配置;设备管理不再为单台设备单独配置数据分发开关(详见第五章)
6. **数据分发独立成域**,与资产台账解耦;本期仅支持"接口回调"一种分发模式,下游注册回调 URL + 鉴权密钥,平台按"分发配置"规则 HTTP 回调
7. **客户管理分层 + 扩域**:客户与布点场所两层结构,场所类型可扩展(不绑定"食堂"概念);客户档案只读复用运营中心,不在设备中心重复维护;本期供应商管理从库存管理迁入,与客户档案、布点场所共同构成"业务伙伴"层(供应商属于商业合作伙伴而非仓储资产)
8. **库存管理独立二级 + 扩域**:库存与资产台账关注角色不同(仓储 vs 资产管理),独立二级菜单后查询不互相污染;出库单与出库管理融合,避免重复;本期扩域加入"仓库区域"承载多仓库档案、加入"库存盘点"承载盘点单管理;供应商管理迁出到客户管理域(详见第 7 条)
9. **设备运维扩域承接系统设置**:运维域既管主动运维(监测/巡检/自检/运行日志)也管被动维修(工单/维修记录),同时承接原系统设置的版本/告警/系统配置,统一由运维班组负责,避免故障工单与系统设置在两个域间跳转
10. **设备监测多视角聚合**:原"实施记录 / 自检记录 / 巡检记录 / 运行日志 / 维修记录"5 个独立二级菜单收敛为"设备监测"1 个二级菜单——主列表页看设备运行状态总览,详情页 Tab 聚合视角,避免同一台设备在 5 个菜单间来回跳转
11. **告警统一处置**:运行异常、自检异常、巡检异常、SLA 超时、OTA 失败等告警统一进"告警管理"处置;业务日志作为告警管理下的日志视图,避免业务日志单独建菜单
6. **数据分发独立成域**,与资产台账解耦;本期仅支持"接口回调"一种分发模式;数据分发形成"上报配置(设备→平台)+ 分发配置(平台→下游)"的上下游闭环;失败记录在分发记录中筛选 + 详情页手动重投,不再单列"失败重投"菜单
7. **客户管理分层 + 扩域**:客户与布点场所两层结构,场所类型可扩展(不绑定"食堂"概念);客户档案只读复用运营中心,不在设备中心重复维护;供应商管理从库存管理迁入,与客户档案、布点场所共同构成"业务伙伴"层
8. **库存管理独立二级 + 扩域**:库存与资产台账关注角色不同(仓储 vs 资产管理),独立二级菜单后查询不互相污染;出库单与出库管理融合,避免重复;扩域加入"仓库区域"承载多仓库档案、加入"库存盘点"承载盘点单管理;供应商管理迁出到客户管理域
9. **设备运维扩域精简承接系统设置**:运维域既管主动运维(监测/巡检/自检/运行日志)也管被动维修(工单/维修记录);版本管理和告警管理从原系统设置迁入;系统配置独立菜单取消(全局阈值/策略分散到各业务点各自承载),避免运维人员在不同域间跳转
10. **设备监测多视角聚合**:原 5 个独立二级菜单(实施/自检/巡检/运行日志/维修)收敛为"设备监测"1 个二级菜单——主列表页看设备运行状态总览,详情页 Tab 聚合 6 个视角,避免同一台设备在个菜单间来回跳转
11. **告警统一处置**:运行异常、自检异常、巡检异常、SLA 超时、OTA 失败、数据分发失败等告警统一进"告警管理"处置;业务日志作为告警管理下的日志视图
12. **同事件多视角呈现**:原"客户报障"在"售后服务"和"故障与维修"各有一条记录的思路本期收敛——售后服务随设备租售域一并本期不做,故障与维修的工单 ID 在"工单管理"与"设备监测.详情.维修记录 Tab"间关联即可
13. **设备租售本期不做**:销售/租约/售后履约不在本期建设范围,相关需求暂以线下流程或运营中心既有能力承接
14. **第三方授权独立成域**:虹软人脸授权(按设备绑定激活码 + 批量导入)、高德地图授权(ApiKey + 启停管理)、SIM 卡管理(生命周期 + 绑定记录)三者共同构成"外部授权资源中台",不再作为"暂未归属"项或"系统配置"子项分散存在
---
@@ -162,13 +174,20 @@
### 3.1 工作台
**实时监控大屏**:平台级实时看板
- 设备在线率(基于心跳实时计算)
- 数据吞吐量 / 平均分发延迟 / 异常率
- 下游订阅者健康状态
- 死信告警
原规划"实时监控大屏 + 数据概览"两个二级菜单收敛为 **1 个"数据概览"**——实时监控能力(设备在线率、数据吞吐、租户排行等)内聚到数据概览页面,通过 KPI 卡 + 环形图 + Top 5 表格一屏展示,不再独立建大屏页。
**数据概览**:关键指标卡片(设备总数、采集设备数、今日数据量、下游租户数等)
**数据概览**(单页四段式布局):
- **段 1 核心 KPI 卡**(8 张,白底 + 左色条 + 大数字 + 趋势箭头):设备总数 / 在线数 / 离线数 / 告警中 / 维修中 / 今日数据量 / 下游租户数 / 死信累积数等关键指标;每张卡含图标、数值、单位、趋势箭头(▲/▼)和同比/环比提示
- **段 2 资产总览**(左右两张环形图卡):
- 左:**设备资产结构**——按顶级分类占比的环形图(中心显示设备总数),下方洞察行展示主力分类名称 + 占比 + 台数
- 右:**设备运行状态**——在用/离线/维修中/报废占比的环形图(中心显示设备总数),下方洞察行展示在用率 + 离线率
- **段 3 业务洞察**(左右两块):
- 左:**实时业务状态**——2×N 迷你卡矩阵(1 分钟刷新快照),含当前告警数、待处理工单数、今日待巡检数、待初始化设备数等现场状态指标
- 右:**租户设备 Top 5**——按设备数排行的表格,含排名徽章(1/2/3 名彩色高亮)、租户名、设备数、故障率(≥10% 标红)
- **刷新策略**:段 3 实时业务状态 1 分钟自动刷新;其余段为静态快照(进入页面时拉取)
> **设计理由**:原"实时监控大屏"独立建页的目的是给运维值班人员一个全屏看板,但实际使用中运维人员更习惯从"数据概览"入口快速判断整体健康度,再下钻到设备监测/告警管理。将大屏能力内聚到数据概览后,一屏即可完成"总览 → 异常定位 → 下钻处置"的动线,减少页面跳转。
### 3.2 设备资产
@@ -308,6 +327,7 @@
- 列表展示客户基本信息(名称、联系人、地址、应用授权、状态)
- **不在设备中心重复维护客户档案**;新增/编辑/删除/授权应用/重置密码等操作按钮**全部隐藏**
- 设备中心只读消费客户数据,用于设备归属、布点场所关联
- **查看详情 + 下钻场所概览**:操作列支持"查看详情"打开抽屉展示客户完整档案;同时支持打开"场所概览(siteBrief)"子组件,下钻查看该客户下所有布点场所的汇总信息(场所数、场所列表、各场所设备数),便于从客户维度快速了解其场所布局,无需跳转到"布点场所"菜单重新按客户筛选
**布点场所**
- 场所档案(名称、编号、所属客户、部门、地址、联系人、场所类型)
@@ -336,14 +356,16 @@
### 3.6 设备运维
把设备当作**运行体**进行运维巡检 + 维修全流程 + 系统配置管理,关注角色是运维班组、巡检工程师、维修工程师。**本期吸收原"故障与维修"域 + 原"系统设置"域**,统一由运维班组负责6 个二级菜单:设备初始化 / 设备监测 / 工单管理 / 版本管理 / 告警管理 / 系统配置。
把设备当作**运行体**进行运维巡检 + 维修全流程管理,关注角色是运维班组、巡检工程师、维修工程师。**本期吸收原"故障与维修"域 + 原"系统设置"域中的版本/告警**,统一由运维班组负责**5 个可见二级菜单 + 1 个隐藏子菜单**初始化设置 / 设备监测 / 工单管理 / 版本管理+隐藏"版本列表"子页)/ 告警管理。**系统配置独立菜单取消**——全局阈值/策略分散到设备分类、告警管理、初始化设置各自承载
**设备初始化**
- 设备出库装机前的激活 / 预配置 / 入网
- 初始化任务(设备绑定密钥、烧录证书、预置场所配置、固件基线版本)
- 初始化结果归档(成功 / 失败、操作人、时间
- 批量初始化(按批次号 / 出库单
- 与"库存管理 → 出库管理"衔接:出库后设备进入初始化阶段;初始化完成后转为设备资产实例
**初始化设置**(原"设备初始化"更名 + 定位收窄)
- **业务定位**:IOT 设备出库投用前的**软件版本设置**,不再承载激活/烧录证书/预置场所配置等全流程初始化动作(这些动作由实施运维小程序现场完成,详见第八章)
- 设备来源:**由出库流程产生**,本页不支持新增设备,仅对出库产生的待初始化设备做版本配置
- 字段:设备编码、设备类别、所属客户、布点场所、出库单号、初始化状态(待初始化/已初始化/失败)、投用状态(未投用/已投用)、当前版本、目标版本、操作人、操作时间
- 筛选:设备类别 / 设备编码 / 初始化状态 / 投用状态 / 所属客户 / 布点场所 / 出库单
- 操作:**批量设置版本**(选中多台设备统一设置目标版本)、**版本设置**(单台设备配置目标版本)、**配置详情**(查看配置档案)、**导出**
- 与"库存管理 → 出库管理"衔接:出库后设备进入初始化设置阶段,配置目标版本后由实施运维小程序现场执行 OTA 升级完成激活
- 与"版本管理"衔接:初始化设置选用的目标版本来自"版本管理 → 版本列表"中该设备型号的可用版本
**设备监测**(核心重构,原 5 个独立菜单聚合为 1 个):
- **主列表页**:设备运行状态总览
@@ -367,11 +389,18 @@
- 顶部红点:超 SLA 工单
- 关单前必填维修上报(故障类型 / 维修方式 / 工时 / 备件 / 图片)
**版本管理**(原系统设置 → 版本管理迁入):
- 固件版本(硬件 OTA)+ APP 版本(客户端 APP)双轨
- 字段:版本号、升级包地址、升级点、灰度策略、回滚标识、发布时间、状态(草稿 / 灰度 / 全量 / 已停用)
- 操作:新增版本、编辑、停用、查看详情
- 与"设备监测.详情.更新记录 Tab"关系:版本管理管"版本档案",更新记录 Tab 管"单台设备的升级结果"
**版本管理**(原系统设置 → 版本管理迁入,**改为"应用软件 → 版本列表"两级结构**):
- **一级页面(应用列表)**:以"应用软件"维度组织版本档案
- 字段:应用名称、研发主体(devSubject)、描述、版本数(versionCount)、状态(启用/停用)
- 筛选:关键字(应用名称 / 描述)
- 操作:**新增应用**、**版本详情**(跳转二级页面查看该应用历史版本列表)、**编辑**、**状态切换**(胶囊开关 + 二次确认)
- **二级页面(版本列表,hidden 路由 `ops/version-list`**:某一应用下的历史版本清单
- 字段:版本号、升级包地址、升级点说明、灰度策略、回滚标识、发布时间、状态(草稿/灰度/全量/已停用)
- 操作:新增版本、编辑、停用、查看详情
- **设计理由**:原"固件 + APP 双轨"平铺在一张表里,导致版本记录混杂、查询体验差。改为"应用 → 版本"两级后,每个应用(如"智养餐饮终端 APP""体重秤固件")有独立版本空间,研发可按应用维度管理版本演进,运维可按应用维度查询历史版本
- 与"设备监测.详情.更新记录 Tab"关系:版本管理管"版本档案"(应用→版本两级),更新记录 Tab 管"单台设备的升级结果"
- 与"初始化设置"关系:初始化设置选用的目标版本来自版本列表中该设备型号对应应用的可用版本
**告警管理**(新):
- 告警统一处置中心,承接所有来源的异常告警
@@ -381,16 +410,16 @@
- 顶部统计卡:今日新增告警 / 待处理告警 / 严重告警数 / 本周告警趋势
- **业务日志视图**(原系统设置 → 业务日志迁入):作为告警管理内的子 Tab,按场所 / 日志类型 / 日志数据归档查询;与告警互补——告警是异常事件视图,业务日志是全量操作流水视图
**系统配置**原系统设置 → 系统配置迁入):
- 全局参数管理
- 配置项分组:
- 推送通道全局配置(数据分发回调超时、重试次数、并发数)
- 限流 / 黑白名单策略
- 巡检 / 自检全局策略(默认周期、告警阈值)
- SLA 策略(不同级别工单的响应/处理时长)
- OTA 全局策略(默认灰度比例、回滚阈值)
- 操作:编辑配置项、查看变更历史
- 注:原"各设备类型的默认数据分发开关"已上提到"设备资产 → 设备分类"层配置,不再放在系统配置(详见第五章)
**系统配置****独立菜单取消**):
- 原规划的"系统配置"独立二级菜单在原型落地时取消
- **取消理由**:原型落地时判定全局参数独立建页价值有限——实际使用中运维人员很少主动去"系统配置"页调整全局阈值,相关参数在各自业务点就地配置更高效;独立建页反而增加跳转成本
- **原规划配置项的归宿**(分散到各业务点承载):
- 推送通道全局配置(数据分发回调超时、重试次数、并发数)→ "数据分发 → 分发配置"下游系统详情中承载
- 巡检 / 自检全局策略(默认周期、告警阈值)→ "设备运维 → 告警管理"的告警规则配置中承载
- SLA 策略(不同级别工单的响应/处理时长)→ "设备运维 → 工单管理"的工单类型配置中承载
- OTA 全局策略(默认灰度比例、回滚阈值)→ "设备运维 → 版本管理"的版本发布配置中承载
- 限流 / 黑白名单策略 → 后端配置项承载(不暴露前端页面)
- 注:原"各设备类型的默认数据分发开关"已上提到"设备资产 → 设备分类"层配置(详见第五章)
> **域内菜单边界**
> - 设备初始化管"出厂首次烧录基线版本",版本管理管"在用设备的持续 OTA 升级"
@@ -399,47 +428,94 @@
### 3.7 数据分发
原"数据中转"更名,本期仅支持"接口回调"一种分发模式:平台收到设备上报数据后,按"分发配置"规则,以 HTTP POST 方式回调下游系统注册的 URL,并携带鉴权密钥。不再维护消息队列、Webhook 等多种通道——简化为单一接口回调,本期足够覆盖业务。
原"数据中转"更名,本期仅支持"接口回调"一种分发模式。数据分发形成**上下游闭环**
- **上游(设备 → 平台)**:通过"上报配置"管理设备数据上报到平台的通道(客户/场所/鉴权/状态)
- **下游(平台 → 下游业务系统)**:通过"分发配置"管理平台按规则 HTTP POST 回调下游系统
**分发配置**(融合原"下游系统 + 订阅关系"):
**2 个可见二级菜单 + 1 个隐藏子页面**:上报配置 / 分发配置(+内侧"分发记录"hidden 子页面)。不再维护消息队列、Webhook 等多种通道——简化为单一接口回调,本期足够覆盖业务。
**上报配置**(新增概念,**设备 → 平台**的上报通道配置):
- **业务定位**:管理 IOT 设备数据上报到平台的通道档案,明确"哪些设备的哪类数据允许上报到平台",与"分发配置"(平台→下游)形成闭环
- 字段:所属客户、布点场所、上报通道名称、通道编码、鉴权开关(是否启用上报鉴权)、鉴权密钥(启用时必填)、状态(启用/停用)、备注
- 筛选:所属客户 / 布点场所 / 上报通道名称 / 状态 / 鉴权开关
- 操作:新增上报配置、编辑、查看详情
- **与"设备分类"关系**:上报配置管"通道档案"(客户/场所/鉴权),设备分类的"是否启用数据分发"开关管"分类级数据边界";二者协同——分类层开关决定"该类设备数据是否过平台",上报配置决定"过平台时走哪个通道"
- **与"分发配置"关系**:上报配置是数据进入平台的入口,分发配置是数据流出平台的出口;一条设备数据从上报配置进入平台,按分发配置规则分发到下游
**分发配置**(融合原"下游系统 + 订阅关系",**平台 → 下游**的分发规则):
- **下游系统注册**:注册下游业务系统档案
- 字段:下游系统名称、回调 URL、鉴权密钥(用于签名校验)、联系人、启停状态、备注
- 操作:新增 / 编辑 / 删除 / 启停 / 测试连通性(一键发送测试数据验证回调可达)
- **内侧"分发记录"子页面入口**:每个下游系统支持查看该下游的分发记录(hidden 路由 `distribute/record`
- **订阅映射关系**:按"设备分类 / 数据类型"配置分发给哪些下游
- 字段:源(设备顶级分类 + 二级型号 + 数据类型 = 如体重/体脂/心率/训练数据)、目标下游系统(可多选)、过滤条件(可选,如仅工作时段、仅异常值)、启用状态
- 操作:新增规则、编辑、删除、启停
- 一个源可分发给多个下游;一个下游可订阅多个源(多对多)
- **设计意图**:原"下游系统"和"订阅关系"分两个菜单,但二者强耦合(订阅关系本质是"下游系统 + 订阅源"的映射),融合后一个菜单内同时管理"接收方"和"订阅什么",减少跨菜单跳转
**分发记录**
**分发记录**hidden 子页面,挂在"分发配置"内侧,路由 `distribute/record`
- 每条数据分发的执行记录(一条上报数据 × 一个下游 = 一条分发记录)
- 字段:分发记录 ID、设备编码、数据类型、下游系统、分发时间、HTTP 状态码、响应耗时、重试次数、状态(成功 / 失败 / 重投中)、失败原因
- 筛选:按下游系统 / 设备 / 数据类型 / 状态 / 时间范围
- 操作:查看详情(含请求体 / 响应体)、手动重投、批量重投(按筛选条件)、导出
- 字段:分发记录 ID、设备编码、数据类型、下游系统、分发时间、HTTP 状态码、响应耗时、重试次数、状态(成功 / 失败 / 重投中 / 已丢弃)、失败原因
- 筛选:按下游系统 / 设备 / 数据类型 / 状态(含"失败"筛选)/ 时间范围
- 操作:查看详情(含请求体 / 响应体)、手动重投(单条)、批量重投(按筛选条件)、永久丢弃(需二次确认)、导出
- 顶部统计卡:今日分发总数 / 成功率 / 平均响应耗时 / 失败待重投数
- 数据保留策略:默认保留 30 天,超期归档(归档查询后置 P1)
**失败重投**
- 死信队列:分发失败且自动重试耗尽(默认 3 次)后进入死信
- 字段:死信记录 ID、原分发记录 ID、设备编码、数据类型、下游系统、首次失败时间、最近重投时间、重投次数、失败原因、状态(待重投 / 重投中 / 已恢复 / 已丢弃)
- 操作:手动重投(单条 / 批量)、查看详情、永久丢弃(需二次确认)
- 自动重投策略:指数退避(1min / 5min / 30min / 2h / 12h),最多 5 次后转人工
- **失败处置能力内聚**:原"失败重投"独立菜单的能力(手动重投 / 批量重投 / 永久丢弃)全部并入分发记录——通过"状态=失败"筛选进入失败记录视图,详情页或批量操作栏触发重投
- 自动重投策略:指数退避(1min / 5min / 30min / 2h / 12h),最多 5 次后转人工(在分发记录中标记"重试耗尽"状态)
- 死信累积超阈值自动告警到"设备运维 → 告警管理"
- 与"分发记录"关系:分发记录是全量流水视图,失败重投是失败子集的集中处置视图
- 数据保留策略:默认保留 30 天,超期归档(归档查询后置 P1)
- **入口路径**:从"分发配置"下游系统列表 → 点击"分发记录"进入;也可在"分发配置"内侧按需查看全量分发流水
> **失败重投取消独立菜单的理由**:原"失败重投"本质是"分发记录按状态=失败的筛选视图",独立建菜单会导致同一批数据在两个菜单间重复展示,运营人员需在两个菜单间切换判断"哪些失败记录该重投"。并入分发记录后,通过筛选条件即可进入失败处置视图,操作动线更顺畅,也避免了数据重复展示。
> **本期不做**:消息队列通道、Webhook 多通道、下游自主申请订阅(订阅由我方运营在"分发配置"中维护)、数据归档独立查询页(分发记录可查 30 天,超期归档后置 P1)
### 3.8 系统设置(已合并至设备运维)
### 3.8 系统设置(已合并至设备运维 / 系统配置取消
> **本期取消"系统设置"一级菜单**,其下 4 个二级菜单全部迁入"设备运维"域
> - 版本管理 → 设备运维 → 版本管理(直接迁入
> **本期取消"系统设置"一级菜单**,其下 4 个二级菜单的归宿
> - 版本管理 → 设备运维 → 版本管理(改为"应用软件 → 版本列表"两级结构
> - 更新记录 → 设备运维 → 设备监测.详情.更新记录 Tab(按设备维度查看 OTA 结果)
> - 业务日志 → 设备运维 → 告警管理(作为告警管理内的日志视图 Tab)
> - 系统配置 → 设备运维 → 系统配置(直接迁入
> - 系统配置 → **取消独立菜单**(全局阈值/策略分散到设备分类、告警管理、初始化设置、版本管理、分发配置各自承载,详见 §3.6 系统配置段
>
> 迁移动因:版本管理、系统配置本质服务于设备运维场景(OTA 升级、巡检/自检策略、SLA 策略),与设备运维强耦合;更新记录是"按设备看升级结果",归到设备监测详情页更符合运维人员的查询路径;业务日志是"全量操作流水",与告警(异常事件视图)互补,并入告警管理做日志视图。合并后运维班组在一个域内即可完成所有运维 + 系统配置操作,避免在"系统设置"和"设备运维"两个域间跳转
> 迁移动因:版本管理本质服务于设备运维场景(OTA 升级),与设备运维强耦合;更新记录是"按设备看升级结果",归到设备监测详情页更符合运维人员的查询路径;业务日志是"全量操作流水",与告警(异常事件视图)互补,并入告警管理做日志视图;系统配置独立建页价值有限,分散到各业务点就地配置更高效
> **暂未归属的页面**:原"设备配置"(含人脸 SDK 授权、客户端 IP、所属轨道等)和"大屏内容管理"(大屏 HTML 内容)本期从设备资产移除后暂未归属,**后续讨论**其归宿。候选位置:设备运维 → 系统配置下新增"设备配置"子项,或独立新建"设备配置"域。
> **原"暂未归属"项的归宿已明确**:
> - **虹软 SDK 授权** → 迁入新增的"第三方授权 → 虹软人脸授权"(详见 §3.9)
> - **大屏内容管理** → **本期不做**(面向大屏的内容编排功能本期不建设)
> - **客户端 IP / 所属轨道等设备级配置** → 已并入"设备资产 → 设备管理"各 Tab 的差异字段中承载
### 3.9 第三方授权(新增业务域)
> **新增一级业务域**,承载设备依赖的外部授权资源中台化管理。原"虹软 SDK 授权"在 §2 菜单调整中标注为"暂未归属",本期明确归宿——与高德地图授权、SIM 卡管理共同构成"第三方授权"域。该域不再属于"设备运维 → 系统配置"的子项或"暂未归属"项。
**虹软人脸授权**(原"设备配置 → 虹软 SDK 授权"迁入 + 扩展):
- **业务定位**:管理虹软人脸识别 SDK 的激活码档案,按设备绑定激活码 + AppID;支持批量导入激活码
- 字段:激活码、AppID、设备类别(树形选择,关联设备分类树)、设备编码(绑定到具体设备实例)、状态(已绑定/未绑定/已停用)、激活时间、备注
- 筛选:激活码 / 设备类别 / 设备编码 / AppID / 状态
- 操作:新增授权、**批量导入激活码**(Excel 模板下载 + 上传)、查看详情、编辑、删除、**导出列表数据**、**查看导出任务**(异步导出任务列表)
- **与设备资产关系**:设备类别来自"设备资产 → 设备分类"树;设备编码来自"设备资产 → 设备管理";本页只管"激活码档案 + 绑定关系",不重复维护设备档案
- **设计理由**:虹软 SDK 激活码是按设备发放的(一码一机),需要按设备维度管理激活状态;批量导入支持运营一次性录入采购批次的所有激活码
**高德地图授权**(新增):
- **业务定位**:管理高德地图 ApiKey 档案,按应用软件维度管理 ApiKey 的启停
- 字段:高德 ApiKey、应用软件名称(使用该 Key 的应用)、状态(启用/停用)、创建时间、备注
- 筛选:高德 ApiKey / 应用软件名称 / 状态
- 操作:新增授权、查看详情、编辑、删除
- **设计理由**:设备端 / 小程序端调用高德地图服务时需要 ApiKey,多个应用可能共用或独立 Key;集中管理避免 Key 散落在各应用配置中难以轮换
**SIM 卡管理**(新增):
- **业务定位**:管理 IOT 设备配套 SIM 卡的全生命周期——从入库、绑定设备、解绑到绑定记录追溯
- 字段:ICCID(SIM 卡唯一标识)、手机号、运营商(移动/联通/电信/虚拟运营商)、设备 SN(绑定的设备编码)、状态(在库/已绑定/已停用/已注销)、入库时间、绑定时间、备注
- 筛选:关键字(ICCID / 手机号 / 设备 SN)/ 运营商 / 状态
- 操作:新增 SIM 卡、**批量导入**(Excel 模板)、**导出**、**查看导出任务**、**绑定设备**(未绑定时)、**解绑设备**(已绑定时,二次确认)、**绑定记录**(查看该 SIM 卡的历史绑定/解绑流水)、编辑、删除
- **与设备资产关系**:SIM 卡绑定到"设备资产 → 设备管理"中"智能穿戴设备" Tab 的设备实例(穿戴类设备含 SIM 卡号字段);本页管 SIM 卡档案 + 绑定关系,设备档案在设备管理维护
- **设计理由**:穿戴类设备(手表等)依赖 SIM 卡联网,SIM 卡的生命周期与设备生命周期解耦——一张 SIM 卡可能从设备 A 解绑后绑到设备 B;独立管理 SIM 卡档案可避免 SIM 卡信息散落在设备档案中难以独立查询和回收
> **域内菜单边界**
> - 虹软人脸授权管"激活码档案 + 设备绑定"
> - 高德地图授权管"ApiKey 档案 + 应用启停"
> - SIM 卡管理管"SIM 卡档案 + 设备绑定 + 绑定记录"
> - 三者共同特征:管理设备依赖的外部授权资源(SDK 激活码 / 地图 Key / 通信卡),与设备资产解耦但通过设备编码/类别关联
---
@@ -522,11 +598,37 @@
- 是否启用数据回调(启用数据分发时可细化控制)
- 默认下游订阅关系(可选,启用数据分发时可预置默认订阅到"数据分发 → 分发配置")
### 5.4 业务价值
### 5.4 数据分发开关与上报配置/分发配置的闭环关系
数据分发开关(分类层)+ 上报配置(设备→平台通道)+ 分发配置(平台→下游规则)三者协同构成完整的数据分发链路:
```
设备数据 ──(分类层开关:是否过平台)──> 上报配置(设备→平台通道) ──> 平台 ──(分发配置:平台→下游规则)──> 下游业务系统
```
| 配置项 | 配置位置 | 管什么 | 决策粒度 |
|--------|---------|--------|---------|
| **数据分发开关** | 设备资产 → 设备分类 → 类别详情 | 该分类的设备数据是否过平台 | 分类级(粗粒度,决定边界) |
| **上报配置** | 数据分发 → 上报配置 | 设备数据过平台时走哪个通道(客户/场所/鉴权) | 通道级(中粒度,决定入口) |
| **分发配置** | 数据分发 → 分发配置 | 平台收到数据后分发给哪些下游系统 | 规则级(细粒度,决定出口) |
**三者关系**
- 数据分发开关是"总闸"——分类层开关关闭的设备数据根本不进入平台
- 上报配置是"入口"——开关开启的设备数据通过上报配置的通道进入平台
- 分发配置是"出口"——进入平台的数据按分发配置规则分发给下游
**为何不合并**
- 数据分发开关在分类层(设备资产域),因为它本质是设备属性
- 上报配置在数据分发域,因为它管的是通道档案(客户/场所/鉴权)
- 分发配置在数据分发域,因为它管的是分发规则(源→目标映射)
- 三者配置角色不同:分类开关由资产管理员配置,上报配置由数据运维配置,分发配置由数据运维配置
### 5.5 业务价值
- **灵活性**:新增设备分类时无需改代码,运营自主配置
- **可审计**:分类层一眼可见哪些分类走数据分发,全局审计清晰
- **可演进**:未来食堂设备如果要走平台,只需在分类层打开开关
- **闭环清晰**:开关(边界)+ 上报配置(入口)+ 分发配置(出口)三者职责分离又协同,数据流向一目了然
---
@@ -538,12 +640,18 @@
┌──────────────────────────────────────────────────────────┐
│ 上游 硬件采集 │
│ ┌─ 过平台:体重秤 / 体脂秤 / 手表 / 健身器材 / 体检机 │
│ │ (分类层"是否启用数据分发"= 开启 的设备) │
│ └─ 不过平台:食堂设备(大屏 / 货架 / 秤等) │
│ (分类层"是否启用数据分发"= 关闭) │
├──────────────────────────────────────────────────────────┤
分发 本平台(仅针对"分类层开启数据分发"的设备)
│ ├─ 数据接收(设备上报)
入口 上报配置(设备 → 平台)
│ ├─ 通道档案:所属客户 / 布点场所 / 鉴权开关 / 状态
│ └─ 设备数据通过上报通道进入平台 │
├──────────────────────────────────────────────────────────┤
│ 出口 分发配置(平台 → 下游) │
│ ├─ 分发配置匹配(按分类 + 数据类型 → 下游系统) │
│ └─ 接口回调(HTTP POST + 签名鉴权 → 下游回调 URL) │
│ ├─ 分发记录(每条回调可查 + 失败手动重投) │
├──────────────────────────────────────────────────────────┤
│ 下游 租户业务系统 │
│ 餐饮管理系统 / 健康管理系统 / 体重管理系统 │
@@ -557,30 +665,36 @@
**库存链**:入库 → 在库库存 ↔ 出库管理(含出库单视图);维修挂到设备实例(设备监测.详情.维修记录 Tab)
**分发链**:设备上报 → 分发配置匹配 → 下游系统回调 URL(带鉴权密钥)
**分发链**:设备上报 → 上报配置(通道鉴权)→ 平台接收 → 分发配置匹配 → 下游系统回调 URL(带鉴权密钥)→ 分发记录(可查可重投)
**授权链**:设备 → 虹软激活码绑定 / SIM 卡绑定(第三方授权域管档案 + 绑定关系,设备资产域管设备实例)
**用户链**:用户 ↔ 用户设备 ↔ 体重秤数据 / 人脸档案
### 6.3 下游分发模式(本期简化方案)
**本期方案(接口回调)**
- 下游系统在"数据分发 → 分发配置"注册回调 URL + 鉴权密钥
**本期方案(接口回调 + 上下游闭环**
- 设备端:通过"数据分发 → 上报配置"管理上报通道(客户/场所/鉴权/状态)
- 下游端:在"数据分发 → 分发配置"注册回调 URL + 鉴权密钥
- 运营在"分发配置"内维护订阅映射关系(按设备分类 / 数据类型 → 下游系统)
- 平台收到设备上报数据后,按订阅映射规则,以 HTTP POST 方式回调下游 URL
- 每次回调生成一条"分发记录",可查可重投
- 失败自动重试 3 次(指数退避),仍失败进入"失败重投"死信队列
- 每次回调生成一条"分发记录"(挂在分发配置内侧 hidden 子页面),可查可重投
- 失败自动重试 3 次(指数退避),仍失败在分发记录中标记"重试耗尽"状态
- 运营在分发记录中按"状态=失败"筛选进入失败处置视图,手动重投或永久丢弃
- 死信累积超阈值自动告警到"设备运维 → 告警管理"
**业务边界**
- 下游不能自主订阅,订阅关系由我方运营维护(本期不做下游自助订阅门户)
- 仅支持接口回调,不支持消息队列、Webhook 多通道
- 分发记录保留 30 天,超期归档(归档查询后置 P1)
- "失败重投"不独立建菜单,失败处置能力内聚在分发记录中
**业务影响**
- 上下游闭环(上报配置 + 分发配置)后,数据流向一目了然
- 简化为单一通道后,运维与对接成本降低,本期可快速落地
- 数据是否送达下游可核查(分发记录 + HTTP 状态码),客诉可定位
- 失败数据不丢(死信兜底 + 手动重投)
- 下游鉴权 + 限流(在系统配置中全局管理),数据串户风险消除
- 失败数据不丢(重试耗尽后保留在分发记录中 + 手动重投)
- 下游鉴权 + 限流(在分发配置的下游系统详情中管理),数据串户风险消除
### 6.4 设备全生命周期跨域映射
@@ -590,8 +704,11 @@
|------------|----------|----------------|---------|
| 采购入库 | 库存管理 → 入库管理 | 入库单一条 | 入库登记 |
| 在库待用 | 库存管理 → 在库库存 | 在库台账一条 | 库存状态 = 在库 |
| 出库装机前预配置 | 设备运维 → 设备初始化 | 初始化任务一条 | 激活 / 烧录证书 |
| SIM 卡入库(穿戴类) | 第三方授权 → SIM 卡管理 | SIM 卡档案一条 | SIM 卡入库(未绑定设备) |
| 出库装机前预配置 | 设备运维 → 初始化设置 | 初始化设置一条 | 软件版本设置(目标版本配置) |
| 现场装机 | 设备运维 → 设备监测.详情.实施记录 Tab | 实施记录一条 | 装机 / 租户签收 |
| SIM 卡绑定(穿戴类) | 第三方授权 → SIM 卡管理 | SIM 卡绑定记录一条 | SIM 卡绑定到设备 |
| 虹软激活码绑定(餐饮类) | 第三方授权 → 虹软人脸授权 | 激活码绑定记录一条 | 激活码绑定到设备 |
| 入网建档 | 设备资产 → 设备管理 | 设备实例一条(按 Tab 落到对应顶级分类) | 设备编码分配、固件基线版本 |
| 在用运行 | 设备运维 → 设备监测.详情.运行日志 Tab | 时序日志持续写入 | 心跳、关键事件 |
| 定期巡检 | 设备运维 → 设备监测.详情.巡检记录 Tab | 巡检记录按周期产生 | 巡检任务完成 |
@@ -599,6 +716,7 @@
| 异常报修 | 设备运维 → 工单管理 | 工单一条 | 工单接派 |
| 维修处置 | 设备运维 → 设备监测.详情.维修记录 Tab | 维修记录归档 | 工时、备件、关单 |
| 固件升级 | 设备运维 → 设备监测.详情.更新记录 Tab | OTA 升级记录一条 | 升级成功/失败 |
| SIM 卡解绑(穿戴类) | 第三方授权 → SIM 卡管理 | SIM 卡绑定记录一条 | SIM 卡解绑(可回库或绑其他设备) |
| 回库 | 库存管理 → 在库库存 | 库存回库一条 | 设备状态变更为在库 |
| 报废 | 设备资产 → 设备管理 | 设备状态变更为报废 | 资产核销 |
@@ -608,8 +726,9 @@
- **运维发现异常**:设备监测.详情.自检/巡检/运行日志 Tab ⇄ 工单管理(1:0..1,自动派单)
- **报修与维修**:工单管理 ⇄ 设备监测.详情.维修记录 Tab(1:1,工单 ID 关联,前者关注流转过程,后者关注维修细节)
- **设备与场所**:设备管理 ⇄ 布点场所(N:1,设备当前所在场所)
- **固件升级与运维**:版本管理 ⇄ 设备初始化(基线版本 vs 持续升级);版本管理 ⇄ 设备监测.详情.更新记录 Tab(版本档案 vs 单设备升级结果)
- **固件升级与运维**:版本管理 ⇄ 初始化设置(初始化设置选用目标版本,版本管理管版本档案);版本管理 ⇄ 设备监测.详情.更新记录 Tab(版本档案 vs 单设备升级结果)
- **告警与运维**:告警管理 ⇄ 工单管理(异常告警自动转工单);告警管理 ⇄ 设备监测.详情.运行日志 Tab(告警是异常事件视图,运行日志是全量流水视图)
- **设备与授权资源**:设备管理 ⇄ 第三方授权(虹软激活码 / SIM 卡按设备编码绑定,设备档案在设备管理,授权档案在第三方授权域)
---
@@ -635,78 +754,108 @@
| ~~租售履约缺位~~ | ~~设备采购后到设备出库之间无订单/租约概念~~ | 本期不做(详见 §3.5),暂以线下流程承接 |
| ~~售后闭环缺位~~ | ~~客户报障无 SLA 跟进,无满意度回访~~ | 本期不做(随设备租售域一并延后) |
> **原型落地情况(截至 2026-07-09**
> 上述短板在当前原型中**绝大多数已落地补齐**,按短板逐项标注:
>
> | 短板维度 | 原型落地状态 | 落地位置 |
> |---------|------------|---------|
> | 菜单组织 | ✅ 已落地 | 7 大业务域 + 3 级菜单(详见第二章) |
> | 设备数据边界 | ✅ 已落地 | 设备分类层"是否启用数据分发"开关(详见第五章) |
> | 下游订阅能力 | ⏳ 本期不做 | 下游自主订阅本期不做,订阅关系由运营在分发配置维护 |
> | 分发可追溯性 | ✅ 已落地 | 数据分发 → 分发配置 → 内侧"分发记录"子页面 |
> | 失败兜底 | ✅ 已落地 | 分发记录中"状态=失败"筛选 + 手动重投 + 永久丢弃 |
> | 采集品牌碎片化 | ⏳ 后置 P1 | 采集能力组件化后置 P1,原型未承载 |
> | 设备模型层级 | ✅ 已落地 | 顶级分类 + 二级型号 + 设备实例三层(详见 §3.2) |
> | 实时监控 | ✅ 已落地 | 工作台 → 数据概览(KPI + 环形图 + Top 5,实时监控大屏能力合入) |
> | 多租户隔离 | ✅ 已落地 | 上报配置鉴权 + 分发配置下游鉴权密钥 |
> | 运维巡检缺位 | ✅ 已落地 | 设备监测.详情页 6 Tab 聚合(实施/自检/巡检/运行日志/更新/维修) |
> | 运维菜单分散 | ✅ 已落地 | 5 个独立菜单收敛为设备监测 1 个二级菜单 |
> | 告警无统一处置 | ✅ 已落地 | 设备运维 → 告警管理(统一处置 + 业务日志视图) |
> | 固件升级黑盒 | ✅ 已落地 | 设备运维 → 版本管理(应用→版本列表两级)+ 设备监测.详情.更新记录 Tab |
> | 第三方授权散落 | ✅ 已落地(新增) | 第三方授权域(虹软 / 高德 / SIM 卡) |
### 7.2 重构方向
#### (1)菜单按业务能力域重组(本次重点)
- 6 大业务域:工作台 / 设备资产 / 库存管理 / 客户管理 / 设备运维 / 数据分发
#### (1)菜单按业务能力域重组(本次重点)✅ 已落地
- **7 大业务域**:工作台 / 设备资产 / 库存管理 / 客户管理 / 设备运维 / 数据分发 / 第三方授权
- 食堂数字化、智慧餐厅、体重管理 3 子应用打散重组
- 设备资产统一管理,数据分发独立成域
- 原系统设置域取消,迁入设备运维域
- 设备资产统一管理,数据分发独立成域,第三方授权独立成域
- 原系统设置域取消——版本管理 + 告警管理迁入设备运维域,系统配置独立菜单取消(分散到各业务点承载)
- **菜单层级最多 3 级**,禁止 4 级嵌套
#### (2)补齐下游分发能力(最关键,本期简化)
#### (2)补齐下游分发能力(最关键,本期简化)✅ 已落地
- 上报配置(设备→平台通道)+ 分发配置(平台→下游规则)形成上下游闭环
- 下游系统注册回调 URL + 鉴权密钥
- 运营在"分发配置"内维护订阅映射关系(按设备分类 / 数据类型 → 下游系统)
- 接口回调方式分发,每条分发记录可查可重投
- 失败入死信,后台手动重投
- 失败在分发记录中筛选 + 手动重投(不独立建"失败重投"菜单)
- 分发状态实时可查
- 下游鉴权 + 限流(系统配置中全局管理
- 下游鉴权 + 限流(在分发配置的下游系统详情中管理,系统配置独立菜单取消
#### 3)采集能力组件化
#### 3)采集能力组件化 ⏳ 后置 P1
- 每个品牌一个采集适配器,新增品牌只加适配器
- 新品牌接入不再依赖核心研发排期
- 原型未承载,后置 P1
#### (4)数据分发开关分类级单级配置
#### (4)数据分发开关分类级单级配置 ✅ 已落地
- 数据分发开关收敛到"设备资产 → 设备分类"层单级配置
- 设备实例继承所属分类配置,不再单独覆盖
- 运营自主配置,无需研发改代码
#### 5首页改为实时监控大屏
- 接入设备数 / 在线率
- 数据吞吐 / 平均分发延迟 / 异常率
- 下游订阅者健康状态
- 死信告警
#### 5工作台改为数据概览(实时监控能力合入)✅ 已落地
- KPI 卡片 + 资产结构环形图 + 运行状态环形图 + 租户设备 Top 5
- 实时业务状态 1 分钟刷新
- 原规划的"实时监控大屏"独立菜单合入数据概览,不再单独建页
#### (6)设备运维多视角聚合 + 告警统一处置
- 原 5 个独立二级菜单(实施/自检/巡检/运行日志/维修)收敛为"设备监测"1 个二级菜单,详情页 Tab 聚合
#### (6)设备运维多视角聚合 + 告警统一处置 ✅ 已落地
- 原 5 个独立二级菜单(实施/自检/巡检/运行日志/维修)收敛为"设备监测"1 个二级菜单,详情页 6 Tab 聚合
- 告警统一进"告警管理"处置,业务日志作为告警管理内的日志视图
- 工单管理承接所有来源的工单流转
#### 7)设备模型三层解耦
#### 7)设备模型三层解耦 ✅ 已落地
- 顶级分类(智养餐饮 / 体重测量 / 智能穿戴 / 健身器材 / 健康监测)
- 二级型号(具体 SKU + 采集适配器引用 + IOT 标识 + 数据分发标识)
- 设备实例(单台资产 + 在线状态 + 各 Tab 差异字段)
### 7.3 重构优先级
#### (8)第三方授权资源中台化管理 ✅ 已落地(新增)
- 虹软人脸授权(激活码 + 设备绑定 + 批量导入)
- 高德地图授权(ApiKey + 应用启停)
- SIM 卡管理(生命周期 + 绑定记录)
- 三者共同构成"外部授权资源中台"独立业务域
| 优先级 | 事项 | 业务价值 |
|-------|------|---------|
| P0 | 菜单按业务能力域重组(拍平 3 级,合并系统设置) | 系统骨架清晰,职责解耦,操作效率提升 |
| P0 | 分发记录 + 死信兜底(接口回调模式) | 数据可靠性:失败数据不丢、可追溯、可重投 |
| P0 | 下游系统注册 + 鉴权 + 订阅映射 | 分发链路规范化、数据串户风险消除 |
| P0 | 数据分发开关分类级单级配置 | 设备数据边界可运营自主调整,全局可审计 |
| P0 | 设备运维多视角聚合 + 告警统一处置 🆕 | 设备监测详情 Tab 聚合,告警中心统一处置,主动运维 |
| P1 | 采集能力组件化 | 新品牌接入不再卡研发排期 |
| P1 | 首页实时监控 | 运营可视:在线率 / 吞吐 / 异常实时掌握 |
| P1 | 固件 OTA 版本管理 🆕 | 双轨版本(APP + 固件)+ 升级记录可审计 |
| P1 | 实施运维小程序 🆕 | 现场作业移动化:扫码直达、巡检打卡、维修上报、离线缓存 |
| P2 | 设备模型三层解耦 | 设备台账维护成本下降 |
| ~~P0~~ | ~~设备租售履约补齐~~ | 本期不做(详见 §3.5 |
### 7.3 重构优先级与原型落地状态
| 优先级 | 事项 | 业务价值 | 原型落地状态 |
|-------|------|---------|------------|
| P0 | 菜单按业务能力域重组(拍平 3 级,7 大业务域) | 系统骨架清晰,职责解耦,操作效率提升 | ✅ 已落地 |
| P0 | 分发记录 + 失败重投(接口回调模式) | 数据可靠性:失败数据不丢、可追溯、可重投 | ✅ 已落地(失败重投并入分发记录) |
| P0 | 上报配置 + 下游系统注册 + 鉴权 + 订阅映射 | 上下游闭环 + 分发链路规范化 + 数据串户风险消除 | ✅ 已落地 |
| P0 | 数据分发开关分类级单级配置 | 设备数据边界可运营自主调整,全局可审计 | ✅ 已落地 |
| P0 | 设备运维多视角聚合 + 告警统一处置 | 设备监测详情 Tab 聚合,告警中心统一处置,主动运维 | ✅ 已落地 |
| P0 | 第三方授权资源中台化管理 | 虹软/高德/SIM 卡集中管理,不再散落 | ✅ 已落地(新增) |
| P1 | 采集能力组件化 | 新品牌接入不再卡研发排期 | ⏳ 后置 P1 |
| P1 | 工作台数据概览(实时监控能力合入) | 运营可视:在线率 / 吞吐 / 异常实时掌握 | ✅ 已落地 |
| P1 | 版本管理(应用→版本列表两级) | 版本档案 + 升级记录可审计 | ✅ 已落地 |
| P1 | 实施运维小程序 | 现场作业移动化:扫码直达、巡检打卡、维修上报、离线缓存 | ✅ 已落地(详见第八章) |
| P2 | 设备模型三层解耦 | 设备台账维护成本下降 | ✅ 已落地 |
| ~~P0~~ | ~~设备租售履约补齐~~ | 本期不做(详见 §3.5) | ⏳ 本期不做 |
### 7.4 一个关键判断
**如果只做一件事**:补齐"分发记录 + 失败重投(死信兜底)"。
> **原型落地状态**:✅ 已落地。分发记录挂在"数据分发 → 分发配置"内侧 hidden 子页面,失败重投能力内聚在分发记录中(状态=失败 筛选 + 手动重投 + 永久丢弃),不再独立建"失败重投"菜单。
理由:这是当前最大的业务风险点——失败数据永久丢失且无法追溯,是**数据可靠性**问题。比菜单重组(骨架清晰)影响更深远,比首页改版(可观测性)更紧迫。先补可靠性,再做菜单重组,最后做可观测性。
### 7.5 重构迁移路径(分 4 期)
### 7.5 重构迁移路径(分 4 期)与原型落地状态
| 期 | 目标 | 业务价值 |
|----|------|---------|
| 一期 | 菜单重组(拍平 3 级,合并系统设置至设备运维)+ 分发记录 + 死信 + 下游注册 + 数据分发分类级开关 | 骨架清晰 + 数据可靠 + 下游分发规范化 |
| 二期 | 设备运维多视角聚合(设备监测详情 Tab)+ 告警管理 + 工单管理 🆕 | 补齐"设备作为运行体"主线,主动运维 + 被动维修合一,告警统一处置 |
| 三期 | 采集能力组件化 + 首页实时监控 + 固件 OTA 版本管理 🆕 | 新品牌快接入 + 运营可视 + 升级可审计 |
| 四期 | 实施运维小程序 🆕 + 设备模型三层解耦 | 现场作业移动化 + 台账维护成本下降 |
| 期 | 目标 | 业务价值 | 原型落地状态 |
|----|------|---------|------------|
| 一期 | 菜单重组(拍平 3 级,7 大业务域)+ 分发记录 + 失败重投 + 上报配置 + 下游注册 + 数据分发分类级开关 | 骨架清晰 + 数据可靠 + 上下游闭环 + 下游分发规范化 | ✅ 已落地 |
| 二期 | 设备运维多视角聚合(设备监测详情 6 Tab)+ 告警管理 + 工单管理 | 补齐"设备作为运行体"主线,主动运维 + 被动维修合一,告警统一处置 | ✅ 已落地 |
| 三期 | 采集能力组件化 + 工作台数据概览 + 版本管理(应用→版本列表两级) | 新品牌快接入 + 运营可视 + 升级可审计 | ✅ 部分落地(采集组件化后置 P1) |
| 四期 | 实施运维小程序 + 设备模型三层解耦 + 第三方授权域 | 现场作业移动化 + 台账维护成本下降 + 外部授权中台化 | ✅ 已落地 |
每期可独立交付业务价值,不需要一次性重构。
@@ -714,6 +863,8 @@
> **小程序与四期解耦**:实施运维小程序强依赖二期(运维监测/告警/工单域)和三期(OTA 版本管理)的功能落地,因此放在四期。若需提前验证一线体验,可在二期结束时做最小可用版本(仅扫码 + 工单 + 巡检,无 OTA/离线缓存),随二期灰度。
> **原型落地总结**:截至 2026-07-09,一期 / 二期 / 四期已全部落地,三期除"采集能力组件化"后置 P1 外均已落地。原型已覆盖 7 大业务域全部菜单页面,可进入研发接手阶段。
---
## 八、实施运维小程序
@@ -993,3 +1144,76 @@
6. **权限隔离**:仅实施运维工程师角色可登录小程序,管理员 / 客户经理 / 销售等角色无小程序权限
7. **告警与工单互补**:告警是被动接收的异常事件视图(在工作台入口),工单是主动处置的流转中心;告警可一键转工单衔接流转,避免在两个域间重复操作
8. **设备监测只读简化**:小程序端不做全量历史台账查看(实施/自检/巡检记录 3 Tab 不放),只保留运维现场最高频的 5 个视角(概览/运行日志/告警/维修记录/更新记录),完整版在 PC 端
---
## 九、附录:原型演进记录
> 本附录记录从最初功能规划(2026-07-02 版)到当前原型(2026-07-09 版)的关键变更点,保留设计演进脉络供未来参考。
### 9.1 演进时间线
| 日期 | 版本 | 关键变更 |
|------|------|---------|
| 2026-07-02 | 初版规划 | 6 大业务域 / 4 级嵌套拍平 / 系统设置迁入设备运维 / 数据中转更名数据分发 |
| 2026-07-03 | 文档初稿 | 补充设备租售本期不做 / 实施运维小程序设计 / 数据分发开关分类级单级配置 |
| 2026-07-09 | 原型落地 | 7 大业务域(新增第三方授权)/ 工作台瘦身 / 设备运维精简(系统配置取消)/ 数据分发上下游闭环(新增上报配置,失败重投取消)/ 版本管理两级结构 / 初始化设置定位收窄 |
### 9.2 关键变更点(按业务域)
#### 工作台
- **原规划**:实时监控大屏 + 数据概览(2 个二级菜单)
- **原型落地**:数据概览(1 个二级菜单),实时监控大屏能力合入数据概览
- **变更理由**:运维人员实际使用中更习惯从数据概览入口快速判断整体健康度,独立大屏页使用频率低;合入后一屏完成"总览→异常定位→下钻处置"动线
#### 设备运维
- **原规划**:6 个二级菜单(设备初始化 / 设备监测 / 工单管理 / 版本管理 / 告警管理 / 系统配置)
- **原型落地**:5 个可见二级菜单 + 1 个隐藏子菜单(初始化设置 / 设备监测 / 工单管理 / 版本管理 + 隐藏版本列表 / 告警管理)
- **关键变更**
1. "设备初始化"更名"初始化设置",定位从"全流程初始化(激活/烧录证书/预置配置/固件基线版本)"收窄为"软件版本设置"——全流程初始化动作由实施运维小程序现场完成
2. "系统配置"独立菜单取消——全局阈值/策略分散到设备分类、告警管理、初始化设置、版本管理、分发配置各自承载
3. "版本管理"从"固件+APP 双轨平铺"改为"应用软件→版本列表"两级结构(一级管应用档案,二级管历史版本)
#### 数据分发
- **原规划**:3 个二级菜单(分发配置 / 分发记录 / 失败重投)
- **原型落地**:2 个可见二级菜单 + 1 个隐藏子页面(分发配置 + 内侧分发记录 / 上报配置)
- **关键变更**
1. 新增"上报配置"概念——配置设备→平台的上报通道(客户/场所/鉴权/状态),与"分发配置"(平台→下游)形成上下游闭环
2. "失败重投"独立菜单取消——失败处置能力内聚到分发记录中(状态=失败 筛选 + 手动重投 + 永久丢弃)
3. "分发记录"从独立二级菜单降级为分发配置内侧的 hidden 子页面
#### 第三方授权(新增业务域)
- **原规划**:无此域;虹软 SDK 授权标注"暂未归属,后续讨论"
- **原型落地**:新增一级业务域,含 3 个二级菜单(虹软人脸授权 / 高德地图授权 / SIM 卡管理)
- **变更理由**:虹软 SDK 授权、高德 ApiKey、SIM 卡三者共同特征是"管理设备依赖的外部授权资源",独立成域后不再作为"暂未归属"项或"系统配置"子项分散存在
#### 客户管理
- **原规划**:客户档案只读复用运营中心 tenantList
- **原型落地**:补充 siteBrief 子组件——从客户档案可下钻查看该客户下布点场所概览(场所数/场所列表/各场所设备数),无需跳转到布点场所菜单重新筛选
### 9.3 取消的功能项及理由
| 取消项 | 原规划位置 | 取消理由 |
|--------|---------|---------|
| 实时监控大屏独立菜单 | 工作台 → 实时监控大屏 | 使用频率低,能力合入数据概览更高效 |
| 系统配置独立菜单 | 设备运维 → 系统配置 | 全局参数独立建页价值有限,分散到各业务点就地配置更高效 |
| 失败重投独立菜单 | 数据分发 → 失败重投 | 本质是分发记录按状态=失败的筛选视图,独立建菜单导致数据重复展示 |
| 设备初始化全流程 | 设备运维 → 设备初始化(激活/烧录证书/预置配置) | 全流程初始化由实施运维小程序现场完成更合理,PC 端只管软件版本设置 |
| 版本管理双轨平铺 | 设备运维 → 版本管理(固件+APP 一张表) | 双轨平铺导致版本记录混杂,改为应用→版本两级结构更清晰 |
| 大屏内容管理 | 设备资产 → 大屏内容管理(暂未归属) | 面向大屏的内容编排功能本期不建设 |
| 设备租售域 | 设备租售(一级业务域) | 本期不做,相关需求暂以线下流程或运营中心既有能力承接 |
| 下游自主订阅门户 | 数据分发 → 分发配置(下游自助订阅) | 本期不做,订阅关系由我方运营维护 |
| 采集能力组件化 | 设备运维 → 采集适配器 | 后置 P1,原型未承载 |
| 数据归档独立查询页 | 数据分发 → 数据归档 | 后置 P1,分发记录可查 30 天,超期归档 |
### 9.4 新增的功能项及理由
| 新增项 | 落地位置 | 新增理由 |
|--------|---------|---------|
| 第三方授权域(虹软/高德/SIM) | 一级业务域 | 外部授权资源中台化管理,不再散落 |
| 上报配置 | 数据分发 → 上报配置 | 形成设备→平台 vs 平台→下游的上下游闭环 |
| 客户档案下钻场所概览(siteBrief) | 客户管理 → 客户档案 | 从客户维度快速了解场所布局,无需跳转 |
| 版本列表子页面(hidden) | 设备运维 → 版本管理 → 版本列表 | 应用→版本两级结构,按应用维度管理版本演进 |
| 初始化设置批量设置版本 | 设备运维 → 初始化设置 | 支持选中多台设备统一设置目标版本 |
| SIM 卡绑定记录 | 第三方授权 → SIM 卡管理 → 绑定记录 | 追溯 SIM 卡历史绑定/解绑流水 |
| 虹软批量导入激活码 | 第三方授权 → 虹软人脸授权 → 批量导入 | 支持一次性录入采购批次的所有激活码 |
@@ -31,6 +31,18 @@
/>
</a-form-item>
<a-form-item label="适配类目" name="deviceCategories">
<a-select
v-model:value="form.deviceCategories"
:options="formOptions.deviceCategories"
mode="multiple"
allow-clear
placeholder="请选择设备分类"
style="width: 100%"
/>
<div class="category-hint">该应用支持的设备分类所有版本共享此配置</div>
</a-form-item>
<a-form-item label="状态" name="status">
<a-select v-model:value="form.status" :options="formOptions.status" placeholder="请选择" style="width: 100%" />
</a-form-item>
@@ -64,4 +76,10 @@ const {
defineExpose({ openModal })
</script>
<style scoped lang="less"></style>
<style scoped lang="less">
.category-hint {
margin-top: 4px;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
</style>
@@ -4,7 +4,7 @@
import type { FormInstance, Rule } from 'ant-design-vue/es/form'
import { useAntdStaticMethods } from '@utils/antDesign/popUp'
import { add, edit, info } from '../api'
import { APP_STATUS_OPTIONS } from '../../../../types'
import { APP_STATUS_OPTIONS, DEVICE_CATEGORY_OPTIONS } from '../../../../types'
import type { Form, FormOptions, ModalType, OpenContext, PageInfo } from '../types'
const createInitForm = (): Form => ({
@@ -12,10 +12,12 @@ const createInitForm = (): Form => ({
name: '',
devSubject: '',
description: '',
deviceCategories: [],
status: 'enabled',
})
const createInitOptions = (): FormOptions => ({
deviceCategories: DEVICE_CATEGORY_OPTIONS,
status: APP_STATUS_OPTIONS,
})
@@ -37,6 +39,7 @@ export const usePage = (emit: (e: 'load') => void) => {
const rules: Record<string, Rule[]> = {
name: [{ required: true, message: '请输入应用名称', trigger: 'blur' }],
devSubject: [{ required: true, message: '请输入研发主体(公司名称)', trigger: 'blur' }],
deviceCategories: [{ required: true, type: 'array', message: '请选择适配类目', trigger: 'change' }],
status: [{ required: true, message: '请选择状态', trigger: 'change' }],
}
@@ -2,7 +2,7 @@
* 版本管理 - 应用新增/编辑弹框 - 类型
*/
import type { SelectProps } from 'ant-design-vue'
import type { AppStatus } from '../../../../types'
import type { AppStatus, DeviceCategory } from '../../../../types'
export type ModalType = 'add' | 'edit'
@@ -22,11 +22,14 @@ export interface Form {
devSubject: string
/** 应用描述 */
description: string
/** 适配类目(应用级配置) */
deviceCategories: DeviceCategory[]
/** 状态 */
status: AppStatus
}
export interface FormOptions {
deviceCategories: SelectProps['options']
status: SelectProps['options']
}
@@ -19,6 +19,7 @@
<a-tag :color="detail.forceUpdate ? 'red' : 'default'">{{ detail.forceUpdate ? '是' : '否' }}</a-tag>
</a-descriptions-item>
<a-descriptions-item label="再用设备数">{{ detail.usingDeviceCount ?? 0 }} </a-descriptions-item>
<a-descriptions-item label="待更新设备数">{{ detail.pendingUpdateCount ?? 0 }} </a-descriptions-item>
<a-descriptions-item label="更新时间">{{ detail.updateTime ?? '—' }}</a-descriptions-item>
<a-descriptions-item label="软件包" :span="2">
<a-space>
@@ -28,14 +29,6 @@
<a-button v-if="detail.packageName" type="link" size="small" @click="handleDownload">下载</a-button>
</a-space>
</a-descriptions-item>
<a-descriptions-item label="适配类目" :span="2">
<template v-if="detail.deviceCategories && detail.deviceCategories.length">
<a-tag v-for="cat in detail.deviceCategories" :key="cat" color="blue" style="margin-right: 4px; margin-bottom: 2px">
{{ deviceCategoryText(cat) }}
</a-tag>
</template>
<span v-else></span>
</a-descriptions-item>
<a-descriptions-item label="更新说明" :span="2">
<pre class="info-drawer__code">{{ detail.releaseNote ?? '—' }}</pre>
</a-descriptions-item>
@@ -51,7 +44,7 @@
<script setup lang="ts">
import { PaperClipOutlined } from '@ant-design/icons-vue'
import { deviceCategoryText, formatPackageSize, versionStatusText } from '../../../../../types'
import { formatPackageSize, versionStatusText } from '../../../../../types'
import { usePage } from './init/usePage'
const {
@@ -0,0 +1,20 @@
/**
* 版本管理 - 二级页 / OTA 升级抽屉 - 接口
* - listDevice:查询某版本下设备升级状态列表
* - customerTree:查询客户场所树(含场所下未更新设备数,用于新增任务弹窗)
* - addTask:新增 OTA 升级任务(强制更新 / 范围更新)
*/
import type { ApiResponse } from '@axios'
import { postRequest } from '@axios'
import type { CustomerNode, OtaDeviceItem, OtaSearchForm } from '../types'
const BASE = '/admin/device/ops/version/ota'
export const listDevice = (params: OtaSearchForm & { versionId: string }): Promise<ApiResponse<OtaDeviceItem[]>> =>
postRequest('axiosRequest', BASE + '/device/list', params)
export const customerTree = (versionId: string): Promise<ApiResponse<CustomerNode[]>> =>
postRequest('axiosRequest', BASE + '/customer-tree', { versionId })
export const addTask = (params: Record<string, unknown>): Promise<ApiResponse<null>> =>
postRequest('axiosRequest', BASE + '/task/add', params)
@@ -0,0 +1,195 @@
<template>
<a-modal
v-model:open="pageInfo.visible"
:title="pageInfo.title"
:width="pageInfo.width"
:keyboard="false"
:mask-closable="false"
>
<a-spin :spinning="pageInfo.spin">
<div class="add-task">
<a-row :gutter="16">
<!-- 左侧选择面板 -->
<a-col :span="12">
<div class="panel">
<div class="panel-title">选择更新范围</div>
<a-form :label-col="LABEL_COL">
<a-form-item label="客户">
<a-select
v-model:value="selectedCustomerIds"
:options="customerOptions"
mode="multiple"
allow-clear
show-search
placeholder="选择客户(可多选,过滤场所)"
style="width: 100%"
:max-tag-count="2"
/>
</a-form-item>
<a-form-item label="场所">
<a-select
v-model:value="pendingSiteIds"
:options="siteOptions"
mode="multiple"
allow-clear
show-search
placeholder="选择场所(可多选)"
style="width: 100%"
:max-tag-count="2"
/>
</a-form-item>
<a-form-item :wrapper-col="{ offset: 4 }">
<a-button type="primary" ghost :disabled="!pendingSiteIds.length" @click="addSelectedSites">
<template #icon><PlusOutlined /></template>
添加到已选
</a-button>
</a-form-item>
</a-form>
</div>
</a-col>
<!-- 右侧已选场所列表 -->
<a-col :span="12">
<div class="panel">
<div class="panel-title">
<span>已选场所</span>
<span class="panel-title__extra">
{{ selectedSites.length }} 个场所 · {{ totalDeviceCount }} 台设备
</span>
</div>
<div class="selected-list">
<a-empty v-if="!selectedSites.length" description="暂无已选场所" :image-style="{ height: '60px' }" />
<div v-for="item in selectedSites" :key="item.siteId" class="selected-item">
<div class="selected-item__main">
<div class="selected-item__site">{{ item.siteName }}</div>
<div class="selected-item__customer">{{ item.customerName }}</div>
</div>
<div class="selected-item__count">{{ item.deviceCount }} </div>
<a-button type="link" size="small" danger @click="removeSelectedSite(item.siteId)">移除</a-button>
</div>
</div>
</div>
</a-col>
</a-row>
</div>
</a-spin>
<template #footer>
<a-button @click="closeModal">取消</a-button>
<a-button type="primary" :disabled="pageInfo.spin" :loading="pageInfo.spin" @click="submit">确定</a-button>
</template>
</a-modal>
</template>
<script setup lang="ts">
/**
* 新增 OTA 任务弹窗
* - 左侧:客户多选 → 场所多选 → 添加到已选
* - 右侧:已选场所列表(场所 + 客户 + 设备数 + 移除)
* - 仅用于非强制更新版本;强制更新走抽屉内的全量推送 confirm
*/
import { PlusOutlined } from '@ant-design/icons-vue'
import { usePage } from './init/usePage'
import { LABEL_COL } from './types'
const emit = defineEmits<{ (e: 'ok'): void }>()
const {
pageInfo,
selectedCustomerIds,
pendingSiteIds,
selectedSites,
customerOptions,
siteOptions,
rules,
totalDeviceCount,
addSelectedSites,
removeSelectedSite,
openModal,
closeModal,
submit,
} = usePage(emit)
// 规则对象供模板未来扩展使用(避免 lint 警告)
void rules
defineExpose({ openModal })
</script>
<style scoped lang="less">
.add-task {
.panel {
border: 1px solid #f0f0f0;
border-radius: 4px;
height: 360px;
display: flex;
flex-direction: column;
}
.panel-title {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #fafafa;
border-bottom: 1px solid #f0f0f0;
font-size: 13px;
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
&__extra {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
font-weight: normal;
}
}
.selected-list {
flex: 1;
overflow-y: auto;
padding: 8px 12px;
}
.selected-item {
display: flex;
align-items: center;
padding: 8px 0;
border-bottom: 1px dashed #f0f0f0;
&:last-child {
border-bottom: none;
}
&__main {
flex: 1;
min-width: 0;
}
&__site {
font-size: 13px;
color: rgba(0, 0, 0, 0.85);
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
&__customer {
font-size: 12px;
color: rgba(0, 0, 0, 0.45);
margin-top: 2px;
}
&__count {
margin: 0 8px;
font-size: 13px;
color: #fa8c16;
font-weight: 500;
white-space: nowrap;
}
}
:deep(.ant-form-item) {
margin-bottom: 12px;
}
}
</style>
@@ -0,0 +1,8 @@
/**
* 版本管理 - OTA 升级抽屉 / 新增任务弹窗 - 接口
*/
import type { ApiResponse } from '@axios'
import { postRequest } from '@axios'
export const addTask = (params: Record<string, unknown>): Promise<ApiResponse<null>> =>
postRequest('axiosRequest', '/admin/device/ops/version/ota/task/add', params)
@@ -0,0 +1,167 @@
/**
* 版本管理 - OTA 升级抽屉 / 新增任务弹窗 - 主逻辑
* - 左侧:客户多选 → 场所多选(场所按客户过滤,选完场所后追加到右侧已选列表)
* - 右侧:已选场所列表(场所 + 客户 + 设备数 + 移除)
* - 提交:scope=site / siteIds=[...]
*/
import type { Rule } from 'ant-design-vue/es/form'
import { useAntdStaticMethods } from '@utils/antDesign/popUp'
import { addTask } from '../api'
import type { CustomerNode, SelectedSite } from '../../../../types'
import type { OpenContext, PageInfo } from '../types'
export const usePage = (emit: (e: 'ok') => void) => {
const { message } = useAntdStaticMethods()
const pageInfo = reactive<PageInfo>({
visible: false,
title: '新增 OTA 任务',
spin: false,
width: 880,
})
/** 版本上下文 */
const versionId = ref<string>('')
const version = ref<string>('')
/** 客户场所树原始数据 */
const customerTree = ref<CustomerNode[]>([])
/** 客户多选(用于场所下拉过滤) */
const selectedCustomerIds = ref<string[]>([])
/** 场所多选(待添加到右侧) */
const pendingSiteIds = ref<string[]>([])
/** 已选场所列表 */
const selectedSites = ref<SelectedSite[]>([])
/** 客户下拉选项 */
const customerOptions = computed(() =>
customerTree.value.map((c) => ({ label: c.name, value: c.id })),
)
/** 场所下拉选项(按所选客户过滤) */
const siteOptions = computed(() => {
const customerIds = selectedCustomerIds.value
if (!customerIds.length) return customerTree.value.flatMap((c) => c.sites)
return customerTree.value
.filter((c) => customerIds.includes(c.id))
.flatMap((c) => c.sites)
})
const rules: Record<string, Rule[]> = {
selectedSites: [
{
required: true,
type: 'array',
validator: (_rule: Rule, value: SelectedSite[]) => {
if (!value || !value.length) return Promise.reject('请至少选择一个场所')
return Promise.resolve()
},
trigger: 'change',
},
],
}
/** 场所下拉选择后追加到右侧已选列表(去重) */
const addSelectedSites = (): void => {
if (!pendingSiteIds.value.length) return
const existingIds = new Set(selectedSites.value.map((s) => s.siteId))
const toAdd: SelectedSite[] = []
customerTree.value.forEach((c) => {
c.sites.forEach((s) => {
if (pendingSiteIds.value.includes(s.id) && !existingIds.has(s.id)) {
toAdd.push({
siteId: s.id,
siteName: s.name,
customerId: c.id,
customerName: c.name,
deviceCount: s.deviceCount,
})
}
})
})
if (toAdd.length) {
selectedSites.value = [...selectedSites.value, ...toAdd]
}
pendingSiteIds.value = []
}
const removeSelectedSite = (siteId: string): void => {
selectedSites.value = selectedSites.value.filter((s) => s.siteId !== siteId)
}
/** 已选场所合计设备数 */
const totalDeviceCount = computed(() =>
selectedSites.value.reduce((sum, s) => sum + (s.deviceCount || 0), 0),
)
const resetForm = (): void => {
versionId.value = ''
version.value = ''
customerTree.value = []
selectedCustomerIds.value = []
pendingSiteIds.value = []
selectedSites.value = []
}
const closeModal = (): void => {
resetForm()
pageInfo.visible = false
}
const openModal = (ctx: OpenContext): void => {
resetForm()
pageInfo.visible = true
pageInfo.spin = false
pageInfo.title = `新增 OTA 任务 · ${ctx.version}`
versionId.value = ctx.versionId
version.value = ctx.version
customerTree.value = ctx.customerTree ?? []
}
const submit = (): void => {
if (!selectedSites.value.length) {
void message.warning('请至少选择一个场所')
return
}
pageInfo.spin = true
const params = {
versionId: versionId.value,
forceUpdate: false,
scope: 'site',
siteIds: selectedSites.value.map((s) => s.siteId),
deviceCount: totalDeviceCount.value,
}
addTask(params)
.then((res) => {
if (res.code === '00000') {
void message.success(res.msg)
emit('ok')
closeModal()
}
})
.catch((err: unknown) => {
console.error('新增 OTA 任务失败:', err)
})
.finally(() => {
pageInfo.spin = false
})
}
return {
pageInfo,
selectedCustomerIds,
pendingSiteIds,
selectedSites,
customerOptions,
siteOptions,
rules,
totalDeviceCount,
addSelectedSites,
removeSelectedSite,
openModal,
closeModal,
submit,
}
}
@@ -0,0 +1,31 @@
/**
* 版本管理 - OTA 升级抽屉 / 新增任务弹窗 - 类型
* - 仅非强制更新版本使用,用于按客户 / 场所配置更新范围
* - 强制更新版本直接走 usePage 中的全量推送 confirm
*/
import type { CustomerNode } from '../../../../types'
/** 已选场所项 */
export interface SelectedSite {
siteId: string
siteName: string
customerId: string
customerName: string
/** 该场所下未更新设备数 */
deviceCount: number
}
export interface OpenContext {
versionId: string
version: string
customerTree: CustomerNode[]
}
export interface PageInfo {
visible: boolean
title: string
spin: boolean
width: number
}
export const LABEL_COL = { style: { width: '100px', minWidth: '100px' } } as const
@@ -0,0 +1,207 @@
/**
* 版本管理 - 二级页 / OTA 升级抽屉 - 主逻辑
* - 入口:版本列表「OTA」按钮
* - 抽屉内:搜索 + 设备列表 + 分页 + 新增任务
* - 新增任务按钮行为:
* 强制更新 → Modal.confirm 全量推送
* 非强制更新 → 打开 addTask 弹窗(按客户 / 场所配置更新范围)
*/
import { 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 AddTask from '../component/modal/addTask/addTask.vue'
import * as api from '../api'
import type { CustomerNode, OtaDeviceItem, OpenContext, OtaSearchForm, PageInfo } from '../types'
import { createSearchKey } from './useSearch'
import { tableColumns } from './useTable'
interface DrawerContext {
versionId: string
version: string
appId?: string
appName?: string
forceUpdate: boolean
pendingUpdateCount: number
}
export const usePage = (emit: (e: 'load') => void) => {
const { message, Modal } = useAntdStaticMethods()
const pageInfo = reactive<PageInfo>({
visible: false,
title: 'OTA 升级',
spin: false,
width: 980,
})
/** 抽屉打开时携带的版本上下文 */
const ctx = ref<DrawerContext>({
versionId: '',
version: '',
appId: undefined,
appName: undefined,
forceUpdate: false,
pendingUpdateCount: 0,
})
const search = reactive<OtaSearchForm>(createSearchKey(''))
const table = reactive<TableState<OtaDeviceItem>>({
columns: tableColumns,
dataSource: [],
sort: { field: '', order: null },
pagination: createPaginationConfig() as TablePaginationConfig,
})
const pageLoading = ref<boolean>(false)
const addTaskRef = ref<InstanceType<typeof AddTask> | null>(null)
/** 客户下拉选项(从 customerTree 接口拉取后扁平化) */
const customerOptions = ref<{ label: string; value: string }[]>([])
/** 场所下拉选项(按所选客户过滤) */
const siteOptions = ref<{ label: string; value: string; customerId: string }[]>([])
/** 客户场所树原始数据(addTask 弹窗用) */
const customerTreeData = ref<CustomerNode[]>([])
/** 拉取客户场所树(含场所下未更新设备数) */
const loadCustomerTree = (): void => {
api.customerTree(ctx.value.versionId)
.then((res) => {
if (res.code === '00000' && res.data) {
customerTreeData.value = res.data
customerOptions.value = res.data.map((c) => ({ label: c.name, value: c.id }))
siteOptions.value = res.data.flatMap((c) =>
c.sites.map((s) => ({ label: s.name, value: s.id, customerId: c.id })),
)
}
})
.catch((err: unknown) => {
console.error('客户场所树查询失败:', err)
})
}
/** 场所下拉按所选客户过滤 */
const filteredSiteOptions = (): { label: string; value: string; customerId: string }[] => {
if (!search.customerId) return siteOptions.value
return siteOptions.value.filter((s) => s.customerId === search.customerId)
}
const listRequest = (): void => {
pageLoading.value = true
api.listDevice({ ...search, versionId: ctx.value.versionId })
.then((res) => {
if (res.code === '00000') {
table.dataSource = res.data ?? []
table.pagination.total = table.dataSource.length
}
})
.catch((err: unknown) => {
console.error('OTA 设备列表查询失败:', err)
})
.finally(() => {
pageLoading.value = false
})
}
const searchQuery = (): void => {
table.pagination.current = 1
listRequest()
}
const resetQuery = (): void => {
Object.assign(search, createSearchKey(ctx.value.versionId))
table.pagination.current = 1
listRequest()
}
const dataSourceChange = (pagination: TablePaginationConfig): void => {
table.pagination.current = pagination.current ?? 1
table.pagination.pageSize = pagination.pageSize ?? 30
}
/** 强制更新 - 全量推送二次确认 */
const confirmForceUpdate = (): void => {
Modal.confirm({
title: '确认发起全量 OTA 升级?',
content: `版本 [${ctx.value.version}] 已开启强制更新,任务将自动覆盖应用适用的所有类目下的全部设备(共 ${ctx.value.pendingUpdateCount} 台)。确认继续?`,
okText: '确认发起',
okType: 'danger',
cancelText: '取消',
onOk: () => {
api.addTask({
versionId: ctx.value.versionId,
forceUpdate: true,
scope: 'all',
})
.then((res) => {
if (res.code === '00000') {
void message.success(res.msg)
listRequest()
emit('load')
}
})
.catch((err: unknown) => {
console.error('强制更新任务创建失败:', err)
})
},
})
}
const openAddTask = (): void => {
if (ctx.value.forceUpdate) {
confirmForceUpdate()
return
}
addTaskRef.value?.openModal({
versionId: ctx.value.versionId,
version: ctx.value.version,
customerTree: customerTreeData.value,
})
}
const handleAction = (type: string): void => {
if (type === 'addTask') openAddTask()
}
const openDrawer = (openCtx: OpenContext): void => {
ctx.value = {
versionId: openCtx.versionId,
version: openCtx.version,
appId: openCtx.appId,
appName: openCtx.appName,
forceUpdate: openCtx.forceUpdate,
pendingUpdateCount: openCtx.pendingUpdateCount,
}
pageInfo.visible = true
pageInfo.title = `OTA 升级 · ${openCtx.version}`
Object.assign(search, createSearchKey(openCtx.versionId))
table.pagination.current = 1
listRequest()
loadCustomerTree()
}
const closeDrawer = (): void => {
pageInfo.visible = false
}
return {
pageInfo,
ctx,
search,
table,
pageLoading,
addTaskRef,
customerOptions,
siteOptions,
filteredSiteOptions,
searchQuery,
resetQuery,
dataSourceChange,
handleAction,
listRequest,
openDrawer,
closeDrawer,
}
}
@@ -0,0 +1,12 @@
/**
* 版本管理 - 二级页 / OTA 升级抽屉 - 搜索状态
*/
import type { OtaSearchForm } from '../types'
export const createSearchKey = (versionId: string): OtaSearchForm => ({
versionId,
keyword: undefined,
customerId: undefined,
siteId: undefined,
updateStatus: undefined,
})
@@ -0,0 +1,14 @@
import type { TableColumnsType } from 'ant-design-vue'
/**
* 版本管理 - OTA 升级抽屉 / 设备列表列定义
* - 更新状态由 bodyCell 用 a-tag 渲染
*/
export const tableColumns: TableColumnsType = [
{ title: '设备编码', dataIndex: 'deviceCode', align: 'left', width: 180, ellipsis: true },
{ title: '客户', dataIndex: 'customerName', align: 'left', width: 160, ellipsis: true },
{ title: '场所', dataIndex: 'siteName', align: 'left', width: 160, ellipsis: true },
{ title: '更新状态', dataIndex: 'updateStatus', align: 'center', width: 110 },
{ title: '升级前版本', dataIndex: 'previousVersion', align: 'center', width: 130, ellipsis: true },
{ title: '更新时间', dataIndex: 'updateTime', align: 'center', width: 170 },
]
@@ -0,0 +1,126 @@
<template>
<a-drawer
v-model:open="pageInfo.visible"
:title="pageInfo.title"
:width="pageInfo.width"
:keyboard="false"
:mask-closable="false"
destroy-on-close
@close="closeDrawer"
>
<div class="ota-drawer">
<!-- 强制更新提示 -->
<a-alert
v-if="ctx.forceUpdate"
type="warning"
show-icon
message="该版本已开启强制更新"
:description="`OTA 任务将自动覆盖应用适用的所有类目下的全部设备(待更新 ${ctx.pendingUpdateCount} 台),无需手动配置更新范围。`"
style="margin-bottom: 12px"
/>
<FilterBar v-model="search" @search="searchQuery" @reset="resetQuery">
<a-form-item label="设备编码">
<a-input v-model:value="search.keyword" v-no-space allow-clear placeholder="请输入设备编码" style="width: 200px" />
</a-form-item>
<a-form-item label="客户">
<a-select
v-model:value="search.customerId"
:options="customerOptions"
allow-clear
show-search
placeholder="全部客户"
style="width: 180px"
@change="search.siteId = undefined"
/>
</a-form-item>
<a-form-item label="场所">
<a-select
v-model:value="search.siteId"
:options="filteredSiteOptions()"
allow-clear
show-search
placeholder="全部场所"
style="width: 180px"
/>
</a-form-item>
<a-form-item label="更新状态">
<a-select v-model:value="search.updateStatus" :options="OTA_STATUS_OPTIONS" allow-clear placeholder="全部状态" style="width: 140px" />
</a-form-item>
</FilterBar>
<TableCard :table="table" :loading="pageLoading" row-key="id" @change="dataSourceChange">
<template #toolbar>
<a-button type="primary" @click="handleAction('addTask')">
<template #icon><PlusOutlined /></template>
新增任务
</a-button>
</template>
<template #bodyCell="{ column, record }">
<template v-if="column.dataIndex === 'updateStatus'">
<a-tag :color="record.updateStatus === 'updated' ? 'success' : 'warning'">
{{ otaStatusText(record.updateStatus) }}
</a-tag>
</template>
<template v-else-if="column.dataIndex === 'updateTime'">
{{ record.updateTime ?? '' }}
</template>
<template v-else-if="column.dataIndex === 'previousVersion'">
{{ record.previousVersion ?? '' }}
</template>
</template>
</TableCard>
</div>
<AddTask ref="addTaskRef" @ok="listRequest" />
<template #footer>
<a-button @click="closeDrawer">关闭</a-button>
</template>
</a-drawer>
</template>
<script setup lang="ts">
/**
* 版本管理 - 二级页 / OTA 升级抽屉
* - 设备列表:设备编码 / 客户 / 场所 / 更新状态 / 升级前版本 / 更新时间
* - toolbar:新增任务(强制更新 → 全量推送;非强制更新 → 弹窗配置更新范围)
*/
import { PlusOutlined } from '@ant-design/icons-vue'
import FilterBar from '@components/FilterBar.vue'
import TableCard from '@components/TableCard.vue'
import AddTask from './component/modal/addTask/addTask.vue'
import { OTA_STATUS_OPTIONS, otaStatusText } from './types'
import { usePage } from './init/usePage'
const emit = defineEmits<{ (e: 'load'): void }>()
const {
pageInfo,
ctx,
search,
table,
pageLoading,
addTaskRef,
customerOptions,
filteredSiteOptions,
searchQuery,
resetQuery,
dataSourceChange,
handleAction,
listRequest,
openDrawer,
closeDrawer,
} = usePage(emit)
defineExpose({ openDrawer })
</script>
<style scoped lang="less">
.ota-drawer {
display: flex;
flex-direction: column;
height: 100%;
gap: 12px;
}
</style>
@@ -0,0 +1,83 @@
/**
* 版本管理 - 二级页 / OTA 升级抽屉 - 类型
* - 抽屉入口由版本列表「OTA」按钮触发,携带版本信息
* - 抽屉内表格展示该版本下的设备升级状态
*/
import type { BaseSearchForm } from '@pages/admin-portal/shared/types'
/** 设备升级状态:已更新 / 未更新 */
export type OtaUpdateStatus = 'updated' | 'pending'
export interface OtaDeviceItem {
id: string
/** 设备编码 */
deviceCode: string
/** 客户 ID */
customerId: string
/** 客户名称 */
customerName: string
/** 场所 ID */
siteId: string
/** 场所名称 */
siteName: string
/** 升级状态 */
updateStatus: OtaUpdateStatus
/** 升级前版本 */
previousVersion: string
/** 更新时间(已更新才有) */
updateTime: string
}
/** 客户节点(含场所列表,用于新增任务弹窗的客户场所树) */
export interface CustomerNode {
id: string
name: string
sites: SiteNode[]
}
/** 场所节点(含未更新设备数) */
export interface SiteNode {
id: string
name: string
customerId: string
customerName: string
/** 该场所下未更新到本版本的设备数 */
deviceCount: number
}
/** 抽屉打开上下文 */
export interface OpenContext {
versionId: string
version: string
appId?: string
appName?: string
/** 是否强制更新(决定新增任务按钮的行为) */
forceUpdate: boolean
/** 待更新设备数 */
pendingUpdateCount: number
}
export interface PageInfo {
visible: boolean
title: string
spin: boolean
width: number
}
/** 搜索表单 */
export interface OtaSearchForm extends BaseSearchForm {
keyword?: string
customerId?: string
siteId?: string
updateStatus?: OtaUpdateStatus
}
export const OTA_STATUS_OPTIONS: { label: string; value: OtaUpdateStatus }[] = [
{ label: '已更新', value: 'updated' },
{ label: '未更新', value: 'pending' },
]
export const otaStatusText = (v: OtaUpdateStatus | string | undefined): string => {
if (!v) return '—'
return OTA_STATUS_OPTIONS.find((i) => i.value === v)?.label ?? v
}
@@ -49,29 +49,24 @@
</a-form-item>
<a-row :gutter="16">
<a-col :span="12">
<a-form-item label="适配类目" name="deviceCategories">
<a-select
v-model:value="form.deviceCategories"
:options="formOptions.deviceCategories"
mode="multiple"
allow-clear
placeholder="请选择设备分类"
style="width: 100%"
/>
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="状态" name="status">
<a-select v-model:value="form.status" :options="formOptions.status" placeholder="请选择" style="width: 100%" />
</a-form-item>
</a-col>
<a-col :span="12">
<a-form-item label="强制更新" name="forceUpdate">
<a-switch v-model:checked="form.forceUpdate" checked-children="" un-checked-children="" />
</a-form-item>
</a-col>
</a-row>
<a-form-item label="强制更新" name="forceUpdate">
<a-switch v-model:checked="form.forceUpdate" checked-children="" un-checked-children="" />
<span class="force-hint">开启后使用旧版本的设备必须升级才能继续使用</span>
</a-form-item>
<div v-if="form.forceUpdate" class="force-hint-block">
开启强制更新后使用旧版本的设备必须升级才能继续使用OTA 任务将自动覆盖应用适用的所有类目
</div>
<div v-else class="force-hint-block">
未开启强制更新时可在版本列表OTA中手动配置更新范围按客户 / 场所
</div>
</a-form>
</a-spin>
@@ -85,7 +80,8 @@
<script setup lang="ts">
/**
* 新增版本弹框
* - 字段:版本号 / 版本名称 / 更新说明 / 软件包上传 / 适配类目(多选)/ 状态 / 强制更新
* - 字段:版本号 / 版本名称 / 更新说明 / 软件包上传 / 状态 / 强制更新
* - 适配类目已上移到应用管理(应用级配置)
*/
import { UploadOutlined } from '@ant-design/icons-vue'
import { usePage } from './init/usePage'
@@ -112,15 +108,19 @@ defineExpose({ openModal })
</script>
<style scoped lang="less">
.force-hint {
margin-left: 12px;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
.package-hint {
margin-top: 4px;
color: rgba(0, 0, 0, 0.45);
font-size: 12px;
}
.force-hint-block {
margin: -8px 0 8px 120px;
padding: 8px 12px;
background: #f5f5f5;
border-radius: 4px;
color: rgba(0, 0, 0, 0.65);
font-size: 12px;
line-height: 1.6;
}
</style>
@@ -5,7 +5,7 @@
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 { VERSION_STATUS_OPTIONS } from '../../../../../../types'
import type { Form, FormOptions, ModalType, OpenContext, PackageFile, PageInfo } from '../types'
const createInitForm = (appId?: string): Form => ({
@@ -15,13 +15,11 @@ const createInitForm = (appId?: string): Form => ({
versionName: '',
releaseNote: '',
forceUpdate: false,
deviceCategories: [],
packageFile: null,
status: 'active',
})
const createInitOptions = (): FormOptions => ({
deviceCategories: DEVICE_CATEGORY_OPTIONS,
status: VERSION_STATUS_OPTIONS,
})
@@ -47,7 +45,6 @@ export const usePage = (emit: (e: 'load') => void) => {
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' }],
}
@@ -2,7 +2,7 @@
* 版本管理 - 二级页 / 新增版本弹框 - 类型
*/
import type { SelectProps } from 'ant-design-vue'
import type { DeviceCategory, VersionStatus } from '../../../../../types'
import type { VersionStatus } from '../../../../../types'
export type ModalType = 'add'
@@ -33,8 +33,6 @@ export interface Form {
releaseNote: string
/** 强制更新 */
forceUpdate: boolean
/** 适配类目 */
deviceCategories: DeviceCategory[]
/** 软件包(新增时为本地文件信息,提交时拆为 packageName / packageSize */
packageFile: PackageFile | null
/** 状态 */
@@ -42,7 +40,6 @@ export interface Form {
}
export interface FormOptions {
deviceCategories: SelectProps['options']
status: SelectProps['options']
}
@@ -11,6 +11,7 @@ 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 OtaDrawer from '../component/drawer/ota/ota.vue'
import * as api from '../api'
import type { VersionItem } from '../../types'
import type { VersionSearchForm } from '../types'
@@ -38,6 +39,7 @@ export const usePage = () => {
const pageLoading = ref<boolean>(false)
const addOrEditRef = ref<InstanceType<typeof AddOrEdit> | null>(null)
const infoDrawerRef = ref<InstanceType<typeof InfoDrawer> | null>(null)
const otaDrawerRef = ref<InstanceType<typeof OtaDrawer> | null>(null)
const listRequest = (): void => {
pageLoading.value = true
@@ -85,6 +87,18 @@ export const usePage = () => {
infoDrawerRef.value?.openDrawer({ id: record.id, title: `版本详情 · ${record.version}` })
}
/** 打开 OTA 升级抽屉(携带版本信息以判断是否强制更新) */
const openOta = (record: VersionItem): void => {
otaDrawerRef.value?.openDrawer({
versionId: record.id,
version: record.version,
appId,
appName,
forceUpdate: record.forceUpdate,
pendingUpdateCount: record.pendingUpdateCount,
})
}
/** 切换有效 / 停用(二次确认) */
const toggleStatus = (record: VersionItem): void => {
const next = record.status === 'active' ? '停用' : '启用'
@@ -116,6 +130,7 @@ export const usePage = () => {
}
if (!record) return
if (type === 'detail') openDetail(record)
if (type === 'ota') openOta(record)
if (type === 'toggle') toggleStatus(record)
}
@@ -130,6 +145,7 @@ export const usePage = () => {
appName,
addOrEditRef,
infoDrawerRef,
otaDrawerRef,
searchQuery,
resetQuery,
dataSourceChange,
@@ -3,16 +3,17 @@ import type { TableColumnsType } from 'ant-design-vue'
/**
* 版本管理 - 二级页(版本列表)列定义
* - 状态列由 bodyCell 用 a-switch 渲染
* - 适配类目 / 强制更新 / 操作列同样由 bodyCell 渲染
* - 待更新设备数 / 强制更新 / 操作列同样由 bodyCell 渲染
* - 操作列:详情 / OTA(OTA 触发升级抽屉)
*/
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: 'releaseNote', align: 'left', width: 260, 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: 'pendingUpdateCount', align: 'center', width: 120 },
{ title: '更新时间', dataIndex: 'updateTime', align: 'center', width: 170 },
{ title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 100 },
{ title: '操作', dataIndex: 'action', align: 'center', fixed: 'right', width: 160 },
]
@@ -13,6 +13,37 @@
</span>
</div>
<!-- 原型备注升级规则 -->
<a-alert type="info" show-icon class="rule-alert">
<template #message>
<span class="rule-alert__title">升级规则说明</span>
</template>
<template #description>
<div class="rule-alert__body">
<div class="rule-line">
<span class="rule-line__index">1.</span>
<div class="rule-line__content">
<div class="rule-line__title">软件包更新流程</div>
<div class="rule-line__item">a. 应用启动时检查是否有新版本若有新版本则提示更新或强制更新根据后台设置</div>
<div class="rule-line__item">b. 定时检查更新查到有待执行的 OTA 任务则自动更新</div>
</div>
</div>
<div class="rule-line">
<span class="rule-line__index">2.</span>
<div class="rule-line__content">
若此版本的软件包设置了强制更新 OTA 任务自动覆盖应用适用的所有类目若不设置强制更新可手动配置更新范围按客户 / 场所
</div>
</div>
<div class="rule-line">
<span class="rule-line__index">3.</span>
<div class="rule-line__content">
升级完成后需要向平台上报更新结果
</div>
</div>
</div>
</template>
</a-alert>
<FilterBar v-model="search" @search="searchQuery" @reset="resetQuery">
<a-form-item label="关键字">
<a-input v-model:value="search.keyword" v-no-space allow-clear placeholder="版本号 / 版本名称" style="width: 220px" />
@@ -41,22 +72,22 @@
@click="handleAction('toggle', record)"
/>
</template>
<template v-else-if="column.dataIndex === 'deviceCategories'">
<template v-if="record.deviceCategories && record.deviceCategories.length">
<a-tag v-for="cat in record.deviceCategories" :key="cat" color="blue" style="margin-bottom: 2px">
{{ deviceCategoryText(cat) }}
</a-tag>
</template>
<span v-else></span>
<template v-else-if="column.dataIndex === 'pendingUpdateCount'">
<span :class="{ 'pending-count--active': record.pendingUpdateCount > 0 }">
{{ record.pendingUpdateCount ? `${record.pendingUpdateCount}` : '—' }}
</span>
</template>
<template v-else-if="column.dataIndex === 'action'">
<a-button type="link" size="small" @click="handleAction('detail', record)">详情</a-button>
<a-divider type="vertical" />
<a-button type="link" size="small" @click="handleAction('ota', record)">OTA</a-button>
</template>
</template>
</TableCard>
<AddOrEdit ref="addOrEditRef" @load="listRequest" />
<InfoDrawer ref="infoDrawerRef" />
<OtaDrawer ref="otaDrawerRef" @load="listRequest" />
</div>
</template>
@@ -64,8 +95,8 @@
/**
* 版本管理 - 二级页(版本列表)
* - 入口:主页应用列表「版本详情」按钮跳转,URL 携带 appId / appName
* - 列表:版本号 / 版本名称 / 更新说明 / 强制更新 / 状态 / 再用设备数 / 适配类目 / 更新时间
* - 操作:详情(抽屉)/ 状态胶囊切换(二次确认)
* - 列表:版本号 / 版本名称 / 更新说明 / 强制更新 / 状态 / 再用设备数 / 待更新设备数 / 更新时间
* - 操作:详情(抽屉)/ OTA(升级抽屉)/ 状态胶囊切换(二次确认)
* - toolbar:新增版本
*/
import { ArrowLeftOutlined, PlusOutlined } from '@ant-design/icons-vue'
@@ -73,7 +104,8 @@ import FilterBar from '@components/FilterBar.vue'
import TableCard from '@components/TableCard.vue'
import AddOrEdit from './component/modal/addOrEdit/addOrEdit.vue'
import InfoDrawer from './component/drawer/info/info.vue'
import { VERSION_STATUS_OPTIONS, deviceCategoryText } from '../../types'
import OtaDrawer from './component/drawer/ota/ota.vue'
import { VERSION_STATUS_OPTIONS } from '../../types'
import { usePage } from './init/usePage'
const {
@@ -83,6 +115,7 @@ const {
appName,
addOrEditRef,
infoDrawerRef,
otaDrawerRef,
searchQuery,
resetQuery,
dataSourceChange,
@@ -114,4 +147,56 @@ const {
font-weight: 500;
}
}
.pending-count--active {
color: #fa8c16;
font-weight: 500;
}
.rule-alert {
:deep(.ant-alert-message) {
margin-bottom: 0;
}
&__title {
font-size: 13px;
font-weight: 500;
color: rgba(0, 0, 0, 0.85);
}
&__body {
font-size: 12px;
color: rgba(0, 0, 0, 0.65);
line-height: 1.7;
}
.rule-line {
display: flex;
align-items: flex-start;
& + .rule-line {
margin-top: 6px;
}
&__index {
flex-shrink: 0;
margin-right: 4px;
color: rgba(0, 0, 0, 0.45);
}
&__content {
flex: 1;
}
&__title {
color: rgba(0, 0, 0, 0.85);
font-weight: 500;
margin-bottom: 2px;
}
&__item {
color: rgba(0, 0, 0, 0.65);
}
}
}
</style>
@@ -3,13 +3,14 @@ import type { TableColumnsType } from 'ant-design-vue'
/**
* 版本管理 - 主页(应用列表)列定义
* - 状态列由模板 bodyCell 用 a-switch 渲染(胶囊按钮 + 二次确认)
* - 操作列:版本详情(跳转)/ 编辑
* - 适配类目 / 操作列同样由 bodyCell 渲染
*/
export const tableColumns: TableColumnsType = [
{ 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: 'description', align: 'left', width: 240, resizable: true, ellipsis: true },
{ title: '适配类目', dataIndex: 'deviceCategories', align: 'left', width: 160 },
{ title: '最新版本', dataIndex: 'latestVersion', align: 'center', width: 110 },
{ title: '历史版本', dataIndex: 'versionCount', align: 'center', width: 100 },
{ title: '状态', dataIndex: 'status', align: 'center', width: 110 },
{ title: '最后更新日期', dataIndex: 'updateTime', align: 'center', width: 170 },
@@ -40,6 +40,8 @@ export interface AppItem {
description: string
/** 状态 */
status: AppStatus
/** 适配类目(应用级配置,所有版本共享) */
deviceCategories: DeviceCategory[]
/** 最新版本号(汇总) */
latestVersion: string
/** 历史版本数(含最新) */
@@ -83,8 +85,8 @@ export interface VersionItem {
status: VersionStatus
/** 再用设备数 */
usingDeviceCount: number
/** 适配类目 */
deviceCategories: DeviceCategory[]
/** 待更新设备数(尚未升级到本版本的设备数) */
pendingUpdateCount: number
/** 软件包文件名(含扩展名) */
packageName: string
/** 软件包大小(字节) */
@@ -17,6 +17,14 @@
<template v-if="column.dataIndex === 'devSubject'">
{{ record.devSubject ?? '' }}
</template>
<template v-else-if="column.dataIndex === 'deviceCategories'">
<template v-if="record.deviceCategories && record.deviceCategories.length">
<a-tag v-for="cat in record.deviceCategories" :key="cat" color="blue" style="margin-bottom: 2px">
{{ deviceCategoryText(cat) }}
</a-tag>
</template>
<span v-else></span>
</template>
<template v-else-if="column.dataIndex === 'versionCount'">
{{ record.versionCount ? `${record.versionCount} ` : '' }}
</template>
@@ -43,13 +51,14 @@
<script setup lang="ts">
/**
* 版本管理 - 主页(应用列表)
* - 一级:维护应用软件(应用名称 / 研发主体 / 描述 / 状态)
* - 一级:维护应用软件(应用名称 / 研发主体 / 描述 / 适配类目 / 状态)
* - 操作:版本详情(跳转二级页查询历史版本)/ 编辑 / 状态胶囊切换(二次确认)
*/
import { PlusOutlined } from '@ant-design/icons-vue'
import FilterBar from '@components/FilterBar.vue'
import TableCard from '@components/TableCard.vue'
import AddOrEdit from './component/modal/addOrEdit/addOrEdit.vue'
import { deviceCategoryText } from './types'
import { usePage } from './init/usePage'
const {