ota管理界面

This commit is contained in:
mmc
2026-09-22 19:37:13 +08:00
parent 300e2c221d
commit 8a7e239bd6
13 changed files with 2325 additions and 0 deletions

View File

@@ -0,0 +1,166 @@
# OTA 管理界面 — 实施方案
## Context
系统设置模块目前缺少固件/版本管理入口。运维人员需要在一个统一界面里:
- 查看所有机器的当前固件版本
- 上传新的固件包(.bin/.zip)
- 对单台或批量选中机器下发升级
- 每台机器记住上一次的版本号,支持按机器批量回退
后端 OTA 接口尚未实现。本方案先以 Mock 数据把页面与交互跑通,API 函数集中在 `src/api/ota.ts`,后续后端接口就绪时只需把 mock 实现替换为真实 `request` 调用即可,页面无需改动。
## 关键参考文件
- [routes.tsx](file:///d:/code/机器人智慧服务系统/src/router/routes.tsx) — 路由 + 懒加载注册位置(L35-L42 导入区,L160-L177 systemSetting.children)
- [SystemSetting.tsx](file:///d:/code/机器人智慧服务系统/src/pages/SystemSetting.tsx) — 系统设置页 Outlet 容器
- [AllDeviceManagement.tsx](file:///d:/code/机器人智慧服务系统/src/components/SystemSetting/AllDeviceManagement.tsx) — 表格 + 搜索 + 操作列模板
- [DeviceManagement.tsx](file:///d:/code/机器人智慧服务系统/src/components/SystemSetting/DeviceManagement.tsx) — Tabs 子页面模板
- [device.ts](file:///d:/code/机器人智慧服务系统/src/api/device.ts) — API 封装范例(`request`、`getAllDevice`、`importBindDevices` 上传 FormData 范例)
- [request.ts](file:///d:/code/机器人智慧服务系统/src/api/request.ts) — axios 实例 + token/FormData 拦截器
- [Layout.tsx](file:///d:/code/机器人智慧服务系统/src/components/Layout.tsx) — 侧边栏菜单构建(`buildMenuTree`、`renderMenuItems`),动态菜单来自 `getMenuList`
- [DeviceOverviewPage.tsx#L811-L814](file:///d:/code/机器人智慧服务系统/src/components/devices/DeviceOverviewPage.tsx) — 设备已有 `firmwareVersion` 字段
- [zh/index.ts](file:///d:/code/机器人智慧服务系统/src/locales/zh/index.ts) — 中文 i18n(`systemSetting` 节 L2433、`router` 节 L3562)
- [en/index.ts](file:///d:/code/机器人智慧服务系统/src/locales/en/index.ts) — 英文 i18n
## 实施步骤
### 1. 新建 Mock API 层 `src/api/ota.ts`
定义以下函数,内部用 `setTimeout` 模拟网络延迟,数据保存在模块级内存变量中(页面刷新即重置)。
```ts
// 数据结构
interface MachineVersion {
id: number; // 机器ID
code: string; // 编号
name: string; // 名称
type: string; // 类型 ROBOT/DRONE/CAMERA/...
online: 'ONLINE' | 'OFFLINE';
currentVersion: string; // 当前固件版本
previousVersion: string | null; // 上一版本(首次升级前为 null)
lastUpgradeTime: string | null;
status: 'IDLE' | 'UPGRADING' | 'FAILED' | 'SUCCESS';
}
interface FirmwarePackage {
id: number;
version: string; // 版本号,如 v1.2.3
fileName: string;
fileSize: number; // bytes
description?: string;
uploadTime: string;
}
```
导出函数:
- `getMachineVersionList()` → 返回 8~10 条 Mock 机器数据
- `getFirmwarePackageList()` → 返回已上传的固件包列表(初始内置 2 条历史版本)
- `uploadFirmwarePackage({ file, version, description })` → 把新固件加入列表并返回
- `upgradeMachines({ machineIds, targetVersion })` → 更新对应机器:`previousVersion = currentVersion`,`currentVersion = targetVersion`,`status` 流转,`lastUpgradeTime = now`
- `rollbackMachines({ machineIds })` → 对每台机器:若 `previousVersion` 存在则交换 current/previous;否则报错
### 2. 新建页面 `src/components/SystemSetting/OTAManagement.tsx`
单页三段式布局(参考 [AllDeviceManagement.tsx](file:///d:/code/机器人智慧服务系统/src/components/SystemSetting/AllDeviceManagement.tsx) 的表格范式):
**A. 顶部 — 固件包上传卡(Card)**
- `Upload` 组件(antd)`accept=".bin,.zip"` `beforeUpload` 返回 `false` 阻止自动上传
- `Input` 版本号(必填,校验 `v\d+\.\d+\.\d+` 格式)
- `Input.TextArea` 描述
- 「上传」按钮 → 调用 `uploadFirmwarePackage`,成功后刷新固件列表
**B. 中部 — 已上传固件包列表(Table)**
- 列:版本号 / 文件名 / 文件大小(格式化为 KB/MB)/ 描述 / 上传时间 / 操作(删除)
- `rowKey="id"`,分页 pageSize=10
**C. 底部 — 机器版本表(Table + rowSelection)**
- 列:机器编号 / 名称 / 类型 / 在线状态 / 当前版本 / 上一版本 / 最近升级时间 / 升级状态 / 操作(单台升级 / 单台回退)
- `rowSelection={{ type: 'checkbox' }}` 支持多选
- 表格上方工具栏:「批量升级」「批量回退」「刷新」按钮
- 「批量升级」弹出 `Modal.select` 让用户选择目标固件版本(从固件包列表)
- 「批量回退」对每台选中机器调用 `rollbackMachines`,对 `previousVersion` 为空的机器用 `message.warning` 跳过
- 操作期间 `loading` 状态,操作完成后 `message.success` 并刷新表格
整页使用 `useTranslation` + `App.useApp().modal/message` 保持与 [AllDeviceManagement.tsx](file:///d:/code/机器人智慧服务系统/src/components/SystemSetting/AllDeviceManagement.tsx) 一致的交互风格。
### 3. 注册路由 `src/router/routes.tsx`
- L35-L42 区添加:`const OTAManagementPage = lazy(() => import('../components/SystemSetting/OTAManagement'));`
- `systemSetting.children` 数组追加:`{ path: 'ota', element: withSuspense(OTAManagementPage) },`
### 4. 侧边栏入口 — `src/components/Layout.tsx` 加静态 fallback
`getMenu()` 拉到后端菜单后,遍历 `menuList` 找 `path === '/systemSetting'` 的节点,在其 `children` 末尾追加一项(若不存在):
```ts
{
key: '/systemSetting/ota',
label: t('router.otaManagement'),
icon: null, // 子项无图标,与现有子菜单保持一致
}
```
这样后端菜单数据未配置 OTA 时,前端也能立即在侧边栏看到入口;后端配置好之后该项会被原生菜单项覆盖或共存(key 相同不会重复渲染)。可通过 `t('router.otaManagement')` 做开关:前端 fallback 项的 label 与后端配置时使用同一个 i18n key。
### 5. i18n 文案
**`src/locales/zh/index.ts`**
`router` 节新增:
```ts
otaManagement: 'OTA 管理',
```
`systemSetting` 节新增:
```ts
ota: {
title: 'OTA 固件管理',
uploadCard: '上传固件包',
versionPlaceholder: '例如 v1.2.3',
description: '描述',
uploadBtn: '上传',
firmwareList: '已上传固件包',
machineVersionList: '机器版本列表',
currentVersion: '当前版本',
previousVersion: '上一版本',
lastUpgradeTime: '最近升级时间',
upgradeStatus: '升级状态',
batchUpgrade: '批量升级',
batchRollback: '批量回退',
selectTargetVersion: '选择目标固件版本',
confirmUpgrade: '确认对选中的 {count} 台机器升级到 {version}?',
confirmRollback: '确认对选中的 {count} 台机器回退到上一版本?',
noPreviousVersion: '机器 {name} 没有上一版本,无法回退',
uploadSuccess: '固件包上传成功',
upgradeSuccess: '升级指令已下发',
rollbackSuccess: '回退指令已下发',
deleteConfirm: '确认删除固件包 {version}?',
statusIdle: '空闲',
statusUpgrading: '升级中',
statusFailed: '失败',
statusSuccess: '成功',
},
```
**`src/locales/en/index.ts`** 同步补全英文翻译。
## 验证步骤
1. `npm run dev` 启动开发服务器
2. 登录后从侧边栏「系统设置 → OTA 管理」进入,或直接访问 `/systemSetting/ota`
3. 验证流程:
- 顶部上传一个 `.zip` 测试文件 + 填版本号 `v1.0.0` → 中部固件列表出现新条目
- 底部机器表勾选 2-3 台机器 → 点「批量升级」→ 弹窗选 `v1.0.0` → 确认 → 选中机器 `currentVersion` 变为 `v1.0.0`、`previousVersion` 记录原版本、`lastUpgradeTime` 刷新
- 再次勾选同样机器 → 点「批量回退」→ 确认 → `currentVersion` 与 `previousVersion` 互换
- 单台机器「操作」列的「升级」/「回退」按钮单独验证
- 取消勾选时「批量升级/回退」按钮置灰
- 切换语言到 English,所有文案正常
4. 浏览器 Network 面板无需真实请求(Mock 在前端完成);控制台无报错
5. `npm run build` 通过,无 TS 报错
## 范围边界
- 仅做前端 + Mock 数据,不修改后端代码
- 不改 `getMenuList` 接口契约,仅前端 Layout 加 fallback
- 不引入新依赖,全部使用项目已有的 antd 组件
- Mock 数据保存在模块内存,刷新即重置;后续接后端时仅替换 `src/api/ota.ts` 内部实现

View File

@@ -0,0 +1,653 @@
# -*- coding: utf-8 -*-
"""
Generate August 2026 Monthly Report Word Document for 葛昀.
Audience: 老板 - 通俗易懂、图文并茂
"""
import os
from docx import Document
from docx.shared import Inches, Pt, Cm, RGBColor, Emu
from docx.enum.text import WD_ALIGN_PARAGRAPH, WD_BREAK
from docx.enum.table import WD_TABLE_ALIGNMENT, WD_ALIGN_VERTICAL
from docx.oxml.ns import qn
from docx.oxml import OxmlElement
# ---- config ----
SCREENSHOT_DIR = r"D:\code\机器人智慧服务系统\.workbuddy\screenshots"
OUTPUT_PATH = r"D:\code\机器人智慧服务系统\2026年8月工作月度报告-葛昀.docx"
NAME = "葛昀"
MONTH = "2026年8月"
DEPT = "智慧运维研发组"
PERIOD = "2026.08.01 — 2026.08.31"
# ---- helpers ----
def set_cell_shading(cell, color):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
shading = OxmlElement('w:shd')
shading.set(qn('w:fill'), color)
shading.set(qn('w:val'), 'clear')
tcPr.append(shading)
def set_cell_border(cell, color="BFBFBF", sz=4):
tc = cell._tc
tcPr = tc.get_or_add_tcPr()
tcBorders = OxmlElement('w:tcBorders')
for border_name in ('top', 'left', 'bottom', 'right'):
border_el = OxmlElement(f'w:{border_name}')
border_el.set(qn('w:val'), 'single')
border_el.set(qn('w:sz'), str(sz))
border_el.set(qn('w:color'), color)
tcBorders.append(border_el)
tcPr.append(tcBorders)
def add_page_break(doc):
p = doc.add_paragraph()
run = p.add_run()
run.add_break(WD_BREAK.PAGE)
def add_p(doc, text, size=10.5, bold=False, color=None, align=None, indent_first=False, space_after=4):
p = doc.add_paragraph()
if align == "center":
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
elif align == "right":
p.alignment = WD_ALIGN_PARAGRAPH.RIGHT
elif align == "justify":
p.alignment = WD_ALIGN_PARAGRAPH.JUSTIFY
else:
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
if indent_first:
p.paragraph_format.first_line_indent = Pt(21) # 2字符
run = p.add_run(text)
run.font.size = Pt(size)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
run.font.bold = bold
if color:
run.font.color.rgb = RGBColor(*color)
p.paragraph_format.space_after = Pt(space_after)
return p
def add_h1(doc, text):
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.LEFT
run = p.add_run(text)
run.font.size = Pt(18)
run.font.bold = True
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
run.font.color.rgb = RGBColor(31, 73, 125)
p.paragraph_format.space_before = Pt(14)
p.paragraph_format.space_after = Pt(8)
# 下划线效果:加一条浅色横线
pPr = p._element.get_or_add_pPr()
pBdr = OxmlElement('w:pBdr')
bottom = OxmlElement('w:bottom')
bottom.set(qn('w:val'), 'single')
bottom.set(qn('w:sz'), '12')
bottom.set(qn('w:color'), '1F497D')
bottom.set(qn('w:space'), '4')
pBdr.append(bottom)
pPr.append(pBdr)
return p
def add_h2(doc, text):
p = doc.add_paragraph()
run = p.add_run(text)
run.font.size = Pt(14)
run.font.bold = True
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
run.font.color.rgb = RGBColor(46, 116, 181)
p.paragraph_format.space_before = Pt(10)
p.paragraph_format.space_after = Pt(4)
return p
def add_h3(doc, text):
p = doc.add_paragraph()
run = p.add_run(text)
run.font.size = Pt(12)
run.font.bold = True
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
run.font.color.rgb = RGBColor(31, 73, 125)
p.paragraph_format.space_before = Pt(6)
p.paragraph_format.space_after = Pt(2)
return p
def add_bullet(doc, text, size=10.5):
p = doc.add_paragraph(style='List Bullet')
run = p.add_run(text)
run.font.size = Pt(size)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
p.paragraph_format.space_after = Pt(2)
return p
def add_numbered(doc, text, size=10.5):
p = doc.add_paragraph(style='List Number')
run = p.add_run(text)
run.font.size = Pt(size)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
p.paragraph_format.space_after = Pt(2)
return p
def add_image(doc, name, max_width_inches=5.5, caption=None):
"""Insert a PNG screenshot with optional caption."""
fp = os.path.join(SCREENSHOT_DIR, name)
if not os.path.exists(fp):
add_p(doc, f"[图片缺失: {name}]", size=9, color=(150, 150, 150))
return None
p = doc.add_paragraph()
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run()
run.add_picture(fp, width=Inches(max_width_inches))
p.paragraph_format.space_before = Pt(6)
p.paragraph_format.space_after = Pt(2)
if caption:
cp = doc.add_paragraph()
cp.alignment = WD_ALIGN_PARAGRAPH.CENTER
cr = cp.add_run(f"图 {caption}")
cr.font.size = Pt(9)
cr.font.italic = True
cr.font.name = 'Microsoft YaHei'
cr._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
cr.font.color.rgb = RGBColor(89, 89, 89)
cp.paragraph_format.space_after = Pt(10)
return p
def add_kv_table(doc, rows, col_widths=(1.5, 5.0)):
"""Add a simple two-column key-value table."""
table = doc.add_table(rows=len(rows), cols=2)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
for i, (k, v) in enumerate(rows):
for j, val in enumerate((k, v)):
cell = table.cell(i, j)
cell.width = Inches(col_widths[j])
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
set_cell_border(cell, "BFBFBF")
if j == 0:
set_cell_shading(cell, "EAF1FA")
cell.text = ""
p = cell.paragraphs[0]
run = p.add_run(val)
run.font.size = Pt(10.5)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
if j == 0:
run.font.bold = True
return table
def add_data_table(doc, headers, rows, widths=None, header_color="1F497D"):
n = len(rows) + 1
cols = len(headers)
table = doc.add_table(rows=n, cols=cols)
table.alignment = WD_TABLE_ALIGNMENT.CENTER
table.autofit = False
if widths:
for j, w in enumerate(widths):
for i in range(n):
table.cell(i, j).width = Inches(w)
# header
for j, h in enumerate(headers):
cell = table.cell(0, j)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
set_cell_border(cell, "BFBFBF")
set_cell_shading(cell, header_color)
cell.text = ""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(h)
run.font.size = Pt(10.5)
run.font.bold = True
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
run.font.color.rgb = RGBColor(255, 255, 255)
# rows
for i, row in enumerate(rows, start=1):
for j, v in enumerate(row):
cell = table.cell(i, j)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
set_cell_border(cell, "BFBFBF")
if i % 2 == 0:
set_cell_shading(cell, "F2F6FB")
cell.text = ""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.LEFT if j > 0 else WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(v)
run.font.size = Pt(10)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
return table
# ---- 文档主体 ----
doc = Document()
# 默认样式
style = doc.styles['Normal']
style.font.name = 'Microsoft YaHei'
style.element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
style.font.size = Pt(10.5)
# 页边距
for section in doc.sections:
section.top_margin = Cm(2.2)
section.bottom_margin = Cm(2.2)
section.left_margin = Cm(2.2)
section.right_margin = Cm(2.2)
# ============================================================
# 封面
# ============================================================
cover_p = doc.add_paragraph()
cover_p.alignment = WD_ALIGN_PARAGRAPH.CENTER
cover_p.paragraph_format.space_before = Pt(120)
cr = cover_p.add_run("月度工作报告")
cr.font.size = Pt(36)
cr.font.bold = True
cr.font.name = 'Microsoft YaHei'
cr._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
cr.font.color.rgb = RGBColor(31, 73, 125)
cover_p2 = doc.add_paragraph()
cover_p2.alignment = WD_ALIGN_PARAGRAPH.CENTER
cr2 = cover_p2.add_run(MONTH)
cr2.font.size = Pt(28)
cr2.font.bold = True
cr2.font.name = 'Microsoft YaHei'
cr2._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
cr2.font.color.rgb = RGBColor(89, 89, 89)
# 间隔
for _ in range(2):
doc.add_paragraph()
# 封面信息表
cover_table = doc.add_table(rows=4, cols=2)
cover_table.alignment = WD_TABLE_ALIGNMENT.CENTER
cover_table.autofit = False
info = [
("报 告 人", f"{NAME}({DEPT})"),
("报告周期", PERIOD),
("汇报对象", "部门负责人"),
("报告日期", "2026 年 9 月"),
]
for i, (k, v) in enumerate(info):
for jj, val in enumerate((k, v)):
cell = cover_table.cell(i, jj)
cell.width = Inches(1.4 if jj == 0 else 2.4)
cell.vertical_alignment = WD_ALIGN_VERTICAL.CENTER
set_cell_border(cell, "1F497D", sz=8)
if jj == 0:
set_cell_shading(cell, "1F497D")
cell.text = ""
p = cell.paragraphs[0]
p.alignment = WD_ALIGN_PARAGRAPH.CENTER
run = p.add_run(val)
run.font.size = Pt(13)
run.font.bold = (jj == 0)
run.font.name = 'Microsoft YaHei'
run._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
if jj == 0:
run.font.color.rgb = RGBColor(255, 255, 255)
# 底部
for _ in range(6):
doc.add_paragraph()
foot = doc.add_paragraph()
foot.alignment = WD_ALIGN_PARAGRAPH.CENTER
fr = foot.add_run("机器人智慧服务系统 · 葛昀")
fr.font.size = Pt(11)
fr.font.name = 'Microsoft YaHei'
fr._element.rPr.rFonts.set(qn('w:eastAsia'), 'Microsoft YaHei')
fr.font.color.rgb = RGBColor(120, 120, 120)
add_page_break(doc)
# ============================================================
# 一、本月工作总览
# ============================================================
add_h1(doc, "一、本月工作总览")
add_p(doc,
f"本月({PERIOD}),本人主要围绕「光伏机器人智慧运维系统」的稳定性提升、功能迭代、跨端联调 "
f"与文档沉淀四条主线开展工作,累计完成代码提交 58 次,涵盖设备控制权限重构、WebSocket 通信优化、"
f"视频与日志管理模块建设、iOS/Android 双端协同、AI 大模型版本升级、软件著作权申报等多项重点工作。",
indent_first=True, align="justify")
add_h2(doc, "1.1 本月核心数据")
data_rows = [
("代码提交", "58 次", "git 提交,覆盖 8 个主要迭代方向"),
("工作主线", "4 条", "稳定性 / 功能迭代 / 跨端联调 / 文档沉淀"),
("周报输出", "5 篇", "覆盖 8 月完整工作节奏"),
("关键里程碑", "6 项", "WS 重构、视频模型升权、二级路由权开等"),
("跨团队联调", "1 项", "苏州算法团队路径规划全流程联调"),
("联调演示", "2 次", "现场联调保障、展会演示支持"),
("文档输出", "3 份", "用户手册、软著采集表、SOP 操作文档"),
]
add_data_table(doc, ["指标", "数值", "说明"], data_rows, widths=(1.4, 1.4, 3.6))
doc.add_paragraph()
add_h2(doc, "1.2 本月整体工作亮点")
highlights = [
"【控制权限】重构设备控制权限体系,由「单用户单设备」升级为「单设备单 Token」,彻底避免权限混乱、残留占用问题;"
"并完成 Web/APP 跨端抢占与释放的全场景闭环测试。",
"【联调突破】与苏州算法团队完成路径规划全流程联调——打点 → 数据保存 → 规划指令下发 → 算法回执 → "
"地图可视化展示规划路径与实时轨迹,业务链路完整跑通。",
"【架构升级】完成系统路由从一级到二级架构改造,页面跳转更清晰;同时将视频下载从 Node 脚本重构为 Java 接口,"
"统一后端技术栈,提升服务稳定性与可维护性。",
"【国际化】完成平台核心业务界面中英文双语切换,覆盖日常高频使用页面;为多语言场景落地提供基础。",
"【文档产出】独立输出 13 章 ~15 页的用户操作手册(含 16 张页面截图)、软著采集表(20 个字段全覆盖)、"
"以及标准 SOP 操作文档,便于后续交接与维护。",
]
for h in highlights:
add_bullet(doc, h)
add_page_break(doc)
# ============================================================
# 二、各周工作详情
# ============================================================
add_h1(doc, "二、各周工作详情")
# ---------- 第 1 周 ----------
add_h2(doc, "第 1 周(8/1 – 8/7):控制权限重构 + WS 通信优化")
add_h3(doc, "工作主题")
add_p(doc, "智慧运维系统平台底层通信机制优化、设备控制权限架构升级、跨团队算法联调启动。", indent_first=True)
add_h3(doc, "具体工作")
items = [
"优化 WebSocket 连接机制:实现单页面仅维持一次 WS 连接,路由切换自动销毁,避免多页面重复连接、"
"消息错乱与后台心跳堆积。",
"重构设备控制权限体系:从「单用户单设备控制」升级为「单设备单 Token 唯一占用」机制,"
"保证同一时间一台设备仅被一个有效连接控制。",
"新增主动释放权限功能,并完成闭环测试:Web 不同账号切换、Web 同账号页面切换、"
"Web 与 APP 跨端权限交替抢占与释放。",
"与苏州算法团队深度联调路径规划功能:完成设备打点、路径数据保存、规划指令下发、算法回执接收等"
"完整交互流程;地图可视化绘制规划轨迹与实时行走路径。",
"全程指导 APP 端完成路径规划全套业务流程适配与调试,配合团队完成苏州现场机器演示调试与保障。",
]
for i, it in enumerate(items, 1):
add_numbered(doc, it)
add_h3(doc, "本周产出")
add_image(doc, "00_login.png", 4.5, "系统登录界面 — 8 月初持续优化登录态")
# ---------- 第 2 周 ----------
add_h2(doc, "第 2 周(8/8 – 8/14):联调推进 + 组件迭代 + 文档沉淀")
add_h3(doc, "工作主题")
add_p(doc, "持续推进路径规划联调、平台基础功能完善、性能优化组件迭代,并沉淀 SOP 操作文档。", indent_first=True)
add_h3(doc, "具体工作")
items = [
"苏州路径规划联调持续推进,本周完成设备状态界面打点功能调试:机器在线状态、经纬度数据正常上报,"
"实时轨迹打点运行正常。",
"控制权限逻辑重构优化:基于新权限架构重构平台控制代码,彻底移除老旧权限逻辑;"
"优化 Web 端路由切换时的控制权限释放机制,解决权限滞留。",
"性能优化与组件迭代:完成路由组件拆分优化、精简冗余代码、优化加载逻辑,提升路由跳转速度;"
"修复 WebSocket 切换时序差导致的历史数据残留。",
"基础功能模块完善:完成系统基本设置模块迭代,新增「用户角色概览」「角色权限概览」;"
"完成电站地址功能接入,支持自定义修改场站地址。",
"文档沉淀:梳理 Web 端路径规划完整操作流程,编写标准化 SOP 操作文档,统一操作规范。",
"现场演示调试:配合机器展会演示调试与保障工作,确保现场演示顺利开展。",
]
for i, it in enumerate(items, 1):
add_numbered(doc, it)
add_h3(doc, "本周产出")
add_image(doc, "14_system.png", 2.6, "系统设置 — 用户/角色模块优化")
add_image(doc, "02_device_overview.png", 5.5, "设备总览 — 电站地址接入与多场站切换")
# ---------- 第 3 周 ----------
add_h2(doc, "第 3 周(8/15 – 8/21):视频日志改造 + 国际化 + 软著推进")
add_h3(doc, "工作主题")
add_p(doc, "完成视频/日志管理模块建设、iOS 适配指导、系统中英文国际化、APP 端优化及软著申报材料撰写。", indent_first=True)
add_h3(doc, "具体工作")
items = [
"视频管理模块开发:支持批量下载设备本地存储视频文件,将原有 Node 脚本实现的下载逻辑重构为 Java 接口,"
"统一后端技术栈。",
"日志管理模块开发:实现权限区分展示,支持管理员查看服务端运行日志,便于后续问题排查与系统运维。",
"iOS 适配指导与技术文档沉淀:协助指导 APP 苹果端相关功能实现,独立整理并输出 ios.md 技术文档,"
"统一调试规范与适配细节。",
"系统中英文国际化适配:完成平台核心业务界面的中英文双语切换功能开发,覆盖日常使用高频页面,"
"满足多语言使用场景。",
"APP 端功能优化:指导 APP 端完成视频推拉流控制功能开发与调试;优化无人机实时数据展示逻辑,"
"提升数据刷新实时性与页面展示效果。",
"软件著作权申报:结合平台现有功能、业务架构与技术实现,独立完成软著申报材料撰写,"
"梳理系统创新点、功能架构与应用场景。",
"AI 大模型版本升级:根据项目智能化迭代要求,将原有模型统一更新为 GPT-5.6 版本,"
"进一步提升平台智能化服务能力。",
]
for i, it in enumerate(items, 1):
add_numbered(doc, it)
add_h3(doc, "本周产出")
add_image(doc, "06_video.png", 5.5, "视频管理模块 — 批量下载 + 日志管理界面")
add_image(doc, "15_users.png", 4.0, "用户与角色管理 — 国际化覆盖")
# ---------- 第 4 周 ----------
add_h2(doc, "第 4 周(8/22 – 8/28):路由重构 + 设备参数编辑 + 定时任务")
add_h3(doc, "工作主题")
add_p(doc, "完成系统路由架构从一级到二级改造、设备参数编辑功能迭代开发与测试、定时任务功能落实。", indent_first=True)
add_h3(doc, "具体工作")
items = [
"路由架构整体优化升级:将原有一级路由改造为二级路由体系,梳理规整页面层级结构;"
"针对性修复更换路由框架带来的适配问题。",
"设备参数编辑功能迭代:完成 Web 端与 APP 端设备参数修改功能开发;新增授权机制管控参数修改权限,"
"Web 端可自定义配置设备参数可修改时长。",
"双端授权与权限控制:根据授权信息自动判断用户操作权限,实现参数编辑功能的可用性精准控制。",
"参数滑块联动与组件轻量化:实现参数滑块联动展示效果;对设备参数组件进行抽离重构,"
"新增 loading 加载状态,优化页面加载性能。",
"APP 端蓝牙模式适配:完成 APP 端蓝牙模式修改设备参数功能开发与测试,适配不同设备操作场景。",
"定时任务功能测试:完成 Web 端定时任务功能全流程测试,校验任务创建、执行、启停、周期运行等核心逻辑。",
"APP 端优化指导与测试验收:协助 APP 端完成扫码绑定设备、蓝牙/非蓝牙模式适配、"
"授权管控设备参数修改等核心功能优化与全面测试。",
]
for i, it in enumerate(items, 1):
add_numbered(doc, it)
add_h3(doc, "本周产出")
add_image(doc, "01_home.png", 5.5, "系统首页 — 二级路由重构后加载更顺畅")
add_image(doc, "03_device_status.png", 5.5, "设备状态详情 — 参数滑块 + 授权控制")
add_image(doc, "04_wayline.png", 5.0, "航线任务 — 二级路由结构下跳转清晰")
# ---------- 第 5 周 ----------
add_h2(doc, "第 5 周(8/29 – 8/31):全链路闭环测试 + 工单派发 + TodoList")
add_h3(doc, "工作主题")
add_p(doc, "完成系统全业务链路闭环测试、工单派发功能开发、TodoList 页面建设,APP 端收尾验证。", indent_first=True)
add_h3(doc, "具体工作")
items = [
"全业务链路闭环测试:完成告警、工单、任务全流程链路逻辑测试——告警触发后自动生成工单、"
"关联告警的工单完成后自动消除、设备异常可重新触发告警。",
"告警钉钉推送:支持各场站独立配置开启/关闭钉钉推送权限,实现告警消息灵活推送。",
"工单派发功能开发:新增平台工单手动派发逻辑,支持管理员将各类运维工单定向指派给指定用户,"
"完善工单分配管理机制。",
"设备参数配置功能测试:支持自定义配置设备轮胎增益系数、运行速度等核心参数,"
"页面配置逻辑与参数保存功能运行正常(待下位机联调收尾)。",
"TodoList 页面建设:完成平台 TodoList 页面搭建,统一录入待优化、待开发、待测试任务点,"
"实现任务可视化、条理化管理。",
"APP 端收尾:APP 端绝大部分测试点已完成验证,整体功能基本达标;"
"剩余约 10% 测试内容仍存在轻微问题,持续跟进排查与修复。",
]
for i, it in enumerate(items, 1):
add_numbered(doc, it)
add_h3(doc, "本周产出")
add_image(doc, "09_alerts.png", 5.5, "告警中心 — 钉钉推送 + 自动闭环")
add_image(doc, "12_workorder.png", 5.5, "工单任务 — 手动派发 + 全流程闭环")
add_image(doc, "05_robot_task.png", 4.5, "机器人任务 — 实时路径与规划路径展示")
add_page_break(doc)
# ============================================================
# 三、重点项目成果
# ============================================================
add_h1(doc, "三、重点项目成果")
add_h2(doc, "3.1 设备控制权限体系重构(核心项目)")
add_p(doc, "原「单用户单设备控制」模式存在多端抢占、权限残留、消息错乱等隐患,"
"本月将权限模型重构为「单设备单 Token 唯一占用」,从底层保证设备控制权的唯一性。",
indent_first=True, align="justify")
ach_rows = [
("权限模型", "单用户单设备", "单设备单 Token 唯一占用"),
("多端抢占", "存在冲突", "彻底解决"),
("权限残留", "偶发滞留", "路由切换即释放"),
("跨端测试", "未覆盖", "Web/APP 跨端 4 场景全闭环"),
("WS 连接", "多页面重复连接", "单页面单连接 + 自动销毁"),
("心跳堆积", "偶发", "已消除"),
]
add_data_table(doc, ["对比项", "重构前", "重构后"], ach_rows, widths=(1.8, 2.0, 2.5))
doc.add_paragraph()
add_h2(doc, "3.2 视频与日志管理模块(新建项目)")
add_p(doc, "原视频下载采用 Node 脚本实现,与平台 Java 后端技术栈不统一;日志管理缺失管理员视角。"
"本月将视频下载重构为 Java 接口,统一后端技术栈;新增日志管理模块,按权限区分展示。",
indent_first=True, align="justify")
add_bullet(doc, "视频管理:支持批量下载设备本地存储视频文件,由 Java 接口实现,统一后端技术栈。")
add_bullet(doc, "日志管理:支持管理员查看服务端运行日志,按权限区分展示,便于问题定位与运维排查。")
add_h2(doc, "3.3 路由架构二级改造(平台升级)")
add_p(doc, "为解决页面层级不清、跳转混乱的问题,本月完成路由从一级到二级架构的整体升级。",
indent_first=True, align="justify")
add_bullet(doc, "梳理页面层级结构,整体架构更清晰规范;")
add_bullet(doc, "重点优化设备总览、设备状态详情页面的路由跳转逻辑,解决跳转异常、定位不准等问题;")
add_bullet(doc, "页面访问体验与可维护性显著提升。")
add_h2(doc, "3.4 软件著作权申报")
add_p(doc, "本月独立完成软件著作权申报材料撰写,覆盖系统创新点、功能架构与应用场景,"
"并依据审核反馈持续完善。",
indent_first=True, align="justify")
ach2 = [
("软件名称", "光伏机器人环境感知服务平台"),
("软件简称", "光伏智能运维系统"),
("版本号", "V1.0"),
("源程序量", "约 53700 行"),
("主要功能", "10 大模块,701 字详述"),
("文档产出", "用户操作手册(13 章 / 16 张截图)+ 软著采集表 + SOP"),
]
add_kv_table(doc, ach2)
add_page_break(doc)
# ============================================================
# 四、代码与文档产出
# ============================================================
add_h1(doc, "四、代码与文档产出")
add_h2(doc, "4.1 月度代码提交概览")
git_rows = [
("8 月上旬", "中英文切换 / 二级路由 / 设备参数滑块 / 派单优化 / 推拉流", "17 次"),
("8 月中旬", "二级菜单 / 状态机解耦 / 文档产出 / iOS 适配 / 软著推进", "12 次"),
("8 月下旬", "MQTT 优化 / WS 时序 / 控制权限重构 / 路径联调", "13 次"),
("8 月底", "告警闭环 / 工单派发 / 钉钉推送 / TodoList / Bug 修复", "16 次"),
("合计", "—", "58 次"),
]
add_data_table(doc, ["时段", "主要方向", "提交次数"], git_rows, widths=(1.2, 4.6, 1.0))
doc.add_paragraph()
add_h2(doc, "4.2 主要文档清单")
add_bullet(doc, "《用户操作手册》——13 章 ~15 页,含 16 张页面截图(路径 / .workbuddy/gen_doc.py)")
add_bullet(doc, "《软件著作权采集表》——20 个字段全覆盖(路径 / 软著/采集表(已填写).doc)")
add_bullet(doc, "《SOP 操作文档》——Web 端路径规划完整操作流程,便于人员交接与业务落地")
add_bullet(doc, "《ios.md 技术文档》——iOS 端调试要点、适配细节、问题复盘")
add_bullet(doc, "《声网视频接入说明》——Agora 接入方式、Token 安全风险与改造建议")
add_bullet(doc, "《系统架构设计文档》——html 版,便于浏览与跨部门评审")
doc.add_paragraph()
add_h2(doc, "4.3 关键界面截图")
add_image(doc, "07_realtime.png", 5.5, "实时监控中心 — 设备控制 + 视频播放")
add_image(doc, "08_monitor.png", 5.5, "实时监控地图 — 规划路径与实时轨迹")
add_image(doc, "10_ai.png", 4.5, "AI 诊断分析 — GPT-5.6 模型支撑")
add_image(doc, "11_clean.png", 4.5, "清洗优化模块 — 业务模块适配")
add_image(doc, "13_report.png", 4.5, "报表中心 — 数据可视化")
add_image(doc, "16_roles.png", 4.5, "角色管理 — 权限精细化管控")
add_page_break(doc)
# ============================================================
# 五、工作总结与反思
# ============================================================
add_h1(doc, "五、工作总结与反思")
add_h2(doc, "5.1 本月做得好的方面")
good = [
"持续推进苏州算法联调:从 8 月初启动到 8 月底收尾,完整跑通路径规划全流程,"
"并完成现场演示保障,业务链路闭合度较高。",
"控制权限重构扎实:模型从设计、迁移到全场景测试均严格闭环,覆盖 Web/APP 双端 4 种跨端场景。",
"代码质量与可维护性提升:完成路由组件拆分、设备参数组件抽离、视频接口统一(Java 化),"
"为后续迭代打好基础。",
"文档与材料齐头并进:在迭代开发的同时,同步输出用户手册、软著材料、SOP 等关键文档,"
"沉淀团队知识资产。",
"跨端协同高效:与 APP 端保持紧密指导与测试验收节奏,确保双端业务逻辑统一、交互闭环。",
]
for g in good:
add_bullet(doc, g)
add_h2(doc, "5.2 待提升与反思")
todo = [
"设备参数配置模块:因下位机未完成全量测试,整体联调暂未完成,需在下月优先推进。",
"国际化覆盖:尚有约 1070 个 t() key 在中英文 locale 中缺失、69 个文件约 1403 行硬编码中文待处理,"
"需要分批推进。",
"APP 端收尾:仍有约 10% 测试点存在轻微问题,需持续跟进排查与修复。",
"工单自动派发:当前仅完成手动派发逻辑,自动派发策略仍需后续迭代。",
"二级路由后路径仍有抖动,地图地址接入在切换场站时偶发小问题,需进一步打磨。",
]
for t in todo:
add_bullet(doc, t)
add_page_break(doc)
# ============================================================
# 六、9 月工作计划
# ============================================================
add_h1(doc, "六、9 月工作计划")
add_p(doc, "结合本月遗留问题与项目下一阶段目标,初步拟定 9 月工作计划如下:", indent_first=True)
sep_rows = [
("设备参数全量联调", "高", "9 月上旬", "配合下位机完成设备参数配置模块全量测试闭环"),
("国际化深水区推进", "高", "9 月全月", "批量补齐约 1070 个 t() key 与 1403 行硬编码中文"),
("APP 端 10% 问题修复", "高", "9 月上旬", "完成 APP 端剩余测试点闭环,达成全量验收"),
("工单自动派发", "中", "9 月中旬", "上线自动派发策略,提升运维自动化效率"),
("视频流稳定性优化", "中", "9 月中旬", "声网重连、订阅回收、推拉流优化持续打磨"),
("软著申报推进", "中", "9 月", "根据审核意见持续完善软著材料"),
("TodoList 持续运营", "低", "9 月全月", "持续沉淀任务点,每周迭代"),
]
add_data_table(doc, ["工作计划", "优先级", "时间", "目标"], sep_rows, widths=(1.6, 0.8, 1.0, 2.9))
doc.add_paragraph()
add_p(doc, "以上为本人 2026 年 8 月工作月度报告的全部内容,请审阅。", indent_first=True, align="right",
space_after=2)
add_p(doc, "报告人:葛昀", indent_first=False, align="right", space_after=2)
add_p(doc, "日期:2026 年 9 月", indent_first=False, align="right", space_after=2)
# 保存
doc.save(OUTPUT_PATH)
print(f"OK: 生成成功 -> {OUTPUT_PATH}")
print(f"大小: {os.path.getsize(OUTPUT_PATH)} bytes")

View File

@@ -0,0 +1,8 @@
# 2026-09-22
### 月度报告 Word 生成
- 用户基于 5 篇 8 月周报 + 项目 git 记录,要求生成给老板看的月度报告 Word
- 输出:`D:\code\机器人智慧服务系统\2026年8月工作月度报告-葛昀.docx`(约 7 MB,含 16 张系统截图)
- 结构:封面 / 6 大章节(总览 / 各周详情 / 重点项目成果 / 代码与文档产出 / 总结反思 / 9 月计划)
- 风格:通俗、表格化、图文混排,蓝色主调(1F497D)
- 工具:python-docx 1.2.0(sopenv venv)+ .workbuddy/screenshots 截图素材

Binary file not shown.

675
generate_monthly_report.cjs Normal file
View File

@@ -0,0 +1,675 @@
const fs = require('fs');
const path = require('path');
const {
Document, Packer, Paragraph, TextRun, ImageRun,
HeadingLevel, AlignmentType, LevelFormat,
PageBreak, PageNumber, Header, Footer,
Table, TableRow, TableCell, WidthType,
BorderStyle, ShadingType, VerticalAlign,
} = require('docx');
const SCREENSHOT_DIR = path.join(__dirname, '.workbuddy', 'screenshots');
function getPngDimensions(filePath) {
const buf = fs.readFileSync(filePath);
const width = buf.readUInt32BE(16);
const height = buf.readUInt32BE(20);
return { width, height };
}
function createImage(screenshotName, maxWidth = 480) {
const filePath = path.join(SCREENSHOT_DIR, screenshotName);
if (!fs.existsSync(filePath)) return null;
const { width, height } = getPngDimensions(filePath);
const ratio = height / width;
const w = Math.min(maxWidth, width);
const h = Math.round(w * ratio);
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { before: 120, after: 80 },
children: [new ImageRun({
type: 'png',
data: fs.readFileSync(filePath),
transformation: { width: w, height: h },
altText: { title: screenshotName, description: screenshotName, name: screenshotName },
})],
});
}
function h1(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_1, children: [new TextRun({ text, bold: true })] });
}
function h2(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_2, children: [new TextRun({ text, bold: true })] });
}
function h3(text) {
return new Paragraph({ heading: HeadingLevel.HEADING_3, children: [new TextRun({ text, bold: true })] });
}
function p(text) {
return new Paragraph({ spacing: { after: 120, line: 360 }, children: [new TextRun({ text, size: 22 })] });
}
function bullet(text) {
return new Paragraph({
numbering: { reference: 'bullets', level: 0 },
spacing: { after: 60, line: 360 },
children: [new TextRun({ text, size: 22 })],
});
}
function caption(text) {
return new Paragraph({
alignment: AlignmentType.CENTER,
spacing: { after: 200 },
children: [new TextRun({ text, italics: true, size: 18, color: '888888' })],
});
}
function callout(label, text) {
const border = { style: BorderStyle.SINGLE, size: 1, color: '0969DA' };
return new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
columnWidths: [9026],
rows: [new TableRow({
cantSplit: true,
children: [new TableCell({
borders: { top: border, bottom: border, left: border, right: border },
width: { size: 9026, type: WidthType.DXA },
shading: { fill: 'EAF2FC', type: ShadingType.CLEAR },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [
new Paragraph({ spacing: { after: 40 }, children: [new TextRun({ text: label, bold: true, size: 22, color: '0969DA' })] }),
new Paragraph({ children: [new TextRun({ text, size: 22 })] }),
],
})],
})],
});
}
// 数据统计卡片
function statCard(label, value, color) {
const border = { style: BorderStyle.SINGLE, size: 4, color: color || '0969DA' };
return new TableCell({
borders: { top: border, bottom: border, left: border, right: border },
width: { size: 2256, type: WidthType.DXA },
shading: { fill: 'F5F8FC', type: ShadingType.CLEAR },
margins: { top: 200, bottom: 200, left: 120, right: 120 },
verticalAlign: VerticalAlign.CENTER,
children: [
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 60 }, children: [new TextRun({ text: value, bold: true, size: 40, color: color || '0969DA' })] }),
new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: label, size: 18, color: '6B7785' })] }),
],
});
}
const cjkFont = 'Microsoft YaHei';
const asciiFont = 'Arial';
const fontConfig = { ascii: asciiFont, hAnsi: asciiFont, eastAsia: cjkFont };
const doc = new Document({
styles: {
default: { document: { run: { font: fontConfig, size: 22 } } },
paragraphStyles: [
{ id: 'Heading1', name: 'Heading 1', basedOn: 'Normal', next: 'Normal', quickFormat: true,
run: { size: 32, bold: true, color: '0969DA', font: fontConfig },
paragraph: { spacing: { before: 360, after: 200 }, outlineLevel: 0 } },
{ id: 'Heading2', name: 'Heading 2', basedOn: 'Normal', next: 'Normal', quickFormat: true,
run: { size: 28, bold: true, color: '1B232E', font: fontConfig },
paragraph: { spacing: { before: 280, after: 160 }, outlineLevel: 1 } },
{ id: 'Heading3', name: 'Heading 3', basedOn: 'Normal', next: 'Normal', quickFormat: true,
run: { size: 24, bold: true, color: '3D4D5C', font: fontConfig },
paragraph: { spacing: { before: 200, after: 120 }, outlineLevel: 2 } },
],
},
numbering: {
config: [
{ reference: 'bullets', levels: [{ level: 0, format: LevelFormat.BULLET, text: '\u2022', alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 720, hanging: 360 } } } }] },
{ reference: 'bullets2', levels: [{ level: 0, format: LevelFormat.BULLET, text: '\u25E6', alignment: AlignmentType.LEFT, style: { paragraph: { indent: { left: 1080, hanging: 360 } } } }] },
],
},
sections: [{
properties: {
page: {
size: { width: 11906, height: 16838 },
margin: { top: 1440, right: 1440, bottom: 1440, left: 1440 },
},
},
headers: {
default: new Header({
children: [new Paragraph({
alignment: AlignmentType.RIGHT,
children: [new TextRun({ text: '智慧运维系统 · 月度工作总结报告', size: 16, color: '999999', font: fontConfig })],
})],
}),
},
footers: {
default: new Footer({
children: [new Paragraph({
alignment: AlignmentType.CENTER,
children: [
new TextRun({ text: '第 ', size: 16, color: '999999', font: fontConfig }),
new TextRun({ children: [PageNumber.CURRENT], size: 16, color: '999999' }),
new TextRun({ text: ' 页', size: 16, color: '999999', font: fontConfig }),
],
})],
}),
},
children: [
// === 封面 ===
new Paragraph({ spacing: { before: 2400 } }),
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 240 },
children: [new TextRun({ text: '智慧运维系统平台', size: 56, bold: true, color: '0969DA', font: fontConfig })] }),
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 320 },
children: [new TextRun({ text: '月度工作总结报告', size: 44, bold: true, color: '3D4D5C', font: fontConfig })] }),
new Paragraph({ alignment: AlignmentType.CENTER, spacing: { after: 200 },
children: [new TextRun({ text: '( 2026年8月 )', size: 28, color: '6B7785', font: fontConfig })] }),
new Paragraph({ spacing: { before: 1600 } }),
// 信息表
new Table({
width: { size: 60, type: WidthType.PERCENTAGE },
alignment: AlignmentType.CENTER,
columnWidths: [2250, 4050],
rows: [
new TableRow({
children: [
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
shading: { fill: 'F5F8FC', type: ShadingType.CLEAR },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '汇 报 人', bold: true, size: 24, color: '3D4D5C', font: fontConfig })] })],
}),
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '葌 昀', size: 24, color: '1B232E', font: fontConfig })] })],
}),
],
}),
new TableRow({
children: [
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
shading: { fill: 'F5F8FC', type: ShadingType.CLEAR },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '汇报周期', bold: true, size: 24, color: '3D4D5C', font: fontConfig })] })],
}),
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '2026年8月1日 — 2026年8月31日', size: 24, color: '1B232E', font: fontConfig })] })],
}),
],
}),
new TableRow({
children: [
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
shading: { fill: 'F5F8FC', type: ShadingType.CLEAR },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '所属项目', bold: true, size: 24, color: '3D4D5C', font: fontConfig })] })],
}),
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '机器人智慧服务系统', size: 24, color: '1B232E', font: fontConfig })] })],
}),
],
}),
new TableRow({
children: [
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
shading: { fill: 'F5F8FC', type: ShadingType.CLEAR },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '提交日期', bold: true, size: 24, color: '3D4D5C', font: fontConfig })] })],
}),
new TableCell({
borders: { top: { style: BorderStyle.NONE }, bottom: { style: BorderStyle.SINGLE, size: 4, color: 'D6DEE8' }, left: { style: BorderStyle.NONE }, right: { style: BorderStyle.NONE } },
margins: { top: 120, bottom: 120, left: 200, right: 200 },
children: [new Paragraph({ alignment: AlignmentType.CENTER, children: [new TextRun({ text: '2026年9月1日', size: 24, color: '1B232E', font: fontConfig })] })],
}),
],
}),
],
}),
new Paragraph({ spacing: { before: 1600 } }),
new Paragraph({ alignment: AlignmentType.CENTER,
children: [new TextRun({ text: '二〇二六年九月', size: 22, color: '999999', font: fontConfig })] }),
new Paragraph({ children: [new PageBreak()] }),
// === 目录 ===
h1('目 录'),
p('本文档为智慧运维系统平台 2026 年 8 月份工作总结月报,汇总当月在 WebSocket 通信机制优化、控制权限重构、路径规划联调、路由架构升级、设备参数开发、国际化适配、AI 模型升级、APP 端指导测试、软著申报等方面的核心工作成果,并附系统功能截图。'),
new Paragraph({ spacing: { after: 200 } }),
// 目录列表
(() => {
function tocItem(num, title) {
return new Paragraph({
spacing: { after: 120, line: 360 },
indent: { left: 200 },
children: [
new TextRun({ text: num + ' ', bold: true, size: 24, color: '0969DA', font: fontConfig }),
new TextRun({ text: title, size: 24, color: '1B232E', font: fontConfig }),
],
});
}
return [
tocItem('第一章', '本月工作总览'),
tocItem('第二章', '智慧运维平台底层机制优化'),
tocItem('第三章', '苏州算法联调与路径规划'),
tocItem('第四章', '控制权限体系重构与闭环'),
tocItem('第五章', '路由架构升级与组件迭代'),
tocItem('第六章', '设备参数与定时任务功能'),
tocItem('第七章', '视频与日志管理模块'),
tocItem('第八章', '国际化适配与 AI 大模型升级'),
tocItem('第九章', '基础功能模块完善'),
tocItem('第十章', 'APP 端优化指导与测试'),
tocItem('第十一章', '全业务链路闭环测试'),
tocItem('第十二章', '软著申报与文档沉淀'),
tocItem('第十三章', 'Git 提交记录统计'),
tocItem('第十四章', '本月小结与下月计划'),
];
})().reduce((acc, cur) => acc.concat([cur]), []),
new Paragraph({ children: [new PageBreak()] }),
// === 第一章 本月工作总览 ===
h1('第一章 本月工作总览'),
p('2026 年 8 月,围绕智慧运维系统平台稳定性、控制安全性、业务完整性与移动端联调适配四大方向开展系统性工作,持续推进苏州算法团队的路径规划联调,完成 WebSocket 通信机制与控制权限架构的底层重构,落地路由体系升级、设备参数功能、视频日志模块、中英文国际化、AI 模型升级等多项功能迭代,同步指导 APP 端核心功能开发与测试验收,并完成软件著作权申报材料撰写。'),
h2('1.1 核心数据概览'),
new Table({
width: { size: 100, type: WidthType.PERCENTAGE },
columnWidths: [2256, 2256, 2256, 2258],
rows: [new TableRow({
cantSplit: true,
children: [
statCard('Git 提交次数', '58', '0969DA'),
statCard('功能模块迭代', '10+', '28A745'),
statCard('核心联调里程碑', '3', 'FF8C00'),
statCard('系统界面截图', '18', '8B5CF6'),
],
})],
}),
new Paragraph({ spacing: { after: 200 } }),
h2('1.2 本月工作主线'),
bullet('底层通信机制优化:WebSocket 单页面单连接 + 路由切换自动销毁,彻底解决多页面重复连接、消息错乱、后台心跳堆积'),
bullet('控制权限体系重构:单设备单 Token 唯一占用机制 + 主动释放权限功能,完成 Web/APP 跨端权限交替抢占与释放闭环测试'),
bullet('苏州算法联调:路径规划全流程打通,覆盖设备打点、路径保存、后台规划指令下发、算法回执、地图可视化轨迹绘制'),
bullet('路由架构升级:一级路由整体改造为二级路由体系,完成路由框架适配与跳转逻辑修复'),
bullet('设备参数与定时任务:双端参数修改功能开发与测试、APP 蓝牙模式参数修改、Web 定时任务全流程测试'),
bullet('视频与日志模块:视频批量下载 Node 脚本重构为 Java 接口、日志权限区分展示'),
bullet('国际化与 AI 升级:中英文双语切换覆盖高频业务页面、AI 大模型统一升级为 GPT-5.6'),
bullet('APP 端指导与测试:扫码绑定、蓝牙/非蓝牙模式、参数修改授权管控、视频推拉流、无人机数据展示'),
bullet('软著与文档沉淀:软件著作权申报材料撰写与迭代、iOS 适配技术文档、Web 端路径规划 SOP 文档'),
new Paragraph({ children: [new PageBreak()] }),
// === 第二章 底层机制优化 ===
h1('第二章 智慧运维平台底层机制优化'),
h2('2.1 WebSocket 连接机制优化'),
p('针对系统原有 WebSocket 连接逻辑存在多页面重复连接、消息错乱、后台心跳堆积等问题,本月对 WS 连接逻辑进行深度优化:'),
bullet('实现单页面仅维持一次 WS 连接,避免多页面重复创建连接'),
bullet('页面路由切换时自动销毁当前 WS 连接,杜绝旧连接残留'),
bullet('解决 WS 切换时序差导致的历史数据残留与数据展示异常问题'),
bullet('退出路由时不订阅消息,提升系统整体通信稳定性与性能'),
callout('优化价值', 'WebSocket 机制优化后,系统通信链路更加稳定,消息推送实时性与准确性显著提升,为多端协同、实时监控、告警推送等业务场景提供可靠的底层通信支撑。'),
h2('2.2 实时监控与消息订阅'),
p('结合 WebSocket 机制优化,实时监控页面能够稳定接收设备状态、告警事件、机器位置等实时数据,为运维人员提供准确的一线态势感知。'),
createImage('07_realtime.png'),
caption('图 2-1 实时监控页面 - 实时功率与设备在线率'),
new Paragraph({ children: [new PageBreak()] }),
// === 第三章 苏州算法联调 ===
h1('第三章 苏州算法联调与路径规划'),
h2('3.1 联调背景与目标'),
p('本月持续对接苏州算法团队,开展机器路径规划功能的全流程联调工作。受展会演示设备资源短缺影响,优先完成核心基础联调内容,并支撑苏州现场机器演示调试与保障。'),
h2('3.2 联调里程碑'),
h3('3.2.1 第一步:设备状态界面打点联调成功'),
p('与算法团队完成设备状态界面机器打点功能调试,实现:'),
bullet('机器在线状态正常上报'),
bullet('经纬度数据正常上报与展示'),
bullet('机器实时轨迹打点功能运行正常'),
bullet('设备位置状态精准展示,保障联调基础能力闭环'),
createImage('03_device_status.png'),
caption('图 3-1 设备状态详情页 - 机器经纬度与打点数据'),
h3('3.2.2 第二步:路径规划全链路打通'),
p('完整打通业务链路,实现路径规划功能全流程:'),
bullet('设备打点:在地图上完成机器位置打点与清除打点标志'),
bullet('路径数据保存:将打点数据上传至后台保存'),
bullet('后台规划指令下发:平台向算法端下发路径规划指令'),
bullet('接收算法端回执:接收算法端规划结果回执'),
bullet('地图可视化绘制:在地图上同时绘制规划轨迹与机器实时行走路径'),
p('指令下发成功后,地图上可清晰看到规划路径与机器实时行走轨迹,路径规划功能整体流程跑通、状态正常。'),
h3('3.2.3 第三步:模式适配与显示优化'),
p('根据不同模式显示区域或线段,优化一加载时显示的地图内容,并支持在地图上鼠标悬浮显示机器信息、优化登录用户角色显示等细节体验。'),
createImage('04_wayline.png'),
caption('图 3-2 航线任务页 - 路径规划与可视化'),
callout('联调成果', '路径规划全链路打通,覆盖设备打点、数据保存、指令下发、回执接收、地图可视化绘制五大环节,并支撑苏州现场机器演示顺利落地。'),
new Paragraph({ children: [new PageBreak()] }),
// === 第四章 控制权限体系重构 ===
h1('第四章 控制权限体系重构与闭环'),
h2('4.1 权限架构升级背景'),
p('原有"单用户单设备控制"模式存在多端抢占、权限混乱、权限滞留等问题,无法满足跨端协同控制与设备安全管控需求。本月重构设备抢占控制逻辑,从底层保证设备控制唯一性与安全性。'),
h2('4.2 单设备单 Token 唯一占用机制'),
p('将原有控制模式升级为单设备单 Token 唯一占用机制:'),
bullet('一台设备同一时间仅被一个有效连接控制,彻底解决多端抢占问题'),
bullet('控制权限修改不再需要判断 Web 是否有权限,简化权限判定逻辑'),
bullet('新增主动释放权限功能,支持用户主动让出设备控制权'),
bullet('页面切换/杀死页面时自动释放控制权限,避免权限滞留'),
h2('4.3 跨端闭环测试'),
p('完成权限逻辑的全场景闭环测试,验证权限释放与抢占逻辑准确、稳定、无残留占用:'),
bullet('Web 不同账号切换场景:权限正确释放与抢占'),
bullet('Web 同账号页面切换场景:权限随路由切换自动释放'),
bullet('Web 与 APP 跨端权限交替抢占与释放场景:跨端协同正常'),
p('控制权限重构为后续与苏州团队深度联调、实现跨端权限联动与设备协同控制筑牢了基础。'),
createImage('14_system.png'),
caption('图 4-1 系统设置 - 权限管控与基础配置'),
new Paragraph({ children: [new PageBreak()] }),
// === 第五章 路由架构升级与组件迭代 ===
h1('第五章 路由架构升级与组件迭代'),
h2('5.1 路由架构整体优化升级'),
p('根据团队优化建议,完成系统路由结构重构:'),
bullet('将原有一级路由改造为二级路由体系,页面层级结构更加清晰规范'),
bullet('完成二级菜单初步实现、配置与机器人路由适配'),
bullet('针对性修复更换路由框架带来的适配问题'),
bullet('重点优化设备总览、设备状态详情页面路由跳转逻辑,解决跳转异常、页面定位不准等问题'),
createImage('02_device_overview.png'),
caption('图 5-1 设备总览页 - 二级路由体系下的设备管理'),
h2('5.2 路由组件拆分与性能优化'),
p('完成路由组件拆分优化,精简冗余代码、优化加载逻辑:'),
bullet('路由组件按需拆分,提升页面路由跳转速度'),
bullet('优化整体页面访问流畅度'),
bullet('登录默认选中机制,提升首次访问体验'),
bullet('优化设备管理界面,将 5 个子页面抽离为独立组件,实现组件轻量化'),
bullet('优化设备参数弹窗 loading 效果与两个组件内弹窗的数据影响'),
h2('5.3 设备管理子模块重构'),
p('完成设备管理 tab 重构,针对状态界面视频停止与状态信息解耦进行优化,让数据展示更清晰,业务解耦更彻底。'),
createImage('05_robot_task.png'),
caption('图 5-2 机器人任务页 - 设备任务调度与管理'),
new Paragraph({ children: [new PageBreak()] }),
// === 第六章 设备参数与定时任务 ===
h1('第六章 设备参数与定时任务功能'),
h2('6.1 设备参数编辑功能迭代开发与测试'),
p('完成 Web 端与 APP 端设备参数修改功能开发及测试工作:'),
h3('6.1.1 权限管控机制'),
bullet('双端新增设备参数修改能力,支持通过授权机制管控参数修改权限'),
bullet('Web 端可自定义配置设备参数可修改时长'),
bullet('双端根据授权信息自动判断用户操作权限,精准控制参数编辑功能可用性'),
bullet('根据身份判断能不能选中相关参数项,权限不足时不可编辑'),
h3('6.1.2 交互体验优化'),
bullet('参数滑块联动展示效果,操作更直观'),
bullet('设备参数组件抽离重构,实现组件轻量化,优化页面加载性能'),
bullet('新增 loading 加载状态,完善交互体验'),
bullet('派单流程优化:派单时选择接收人员,根据身份判断能不能选中'),
h3('6.1.3 APP 端蓝牙模式适配'),
bullet('完成 APP 端蓝牙模式修改设备参数功能开发与测试'),
bullet('适配不同设备操作场景,保障无网络/弱网环境下的设备管控能力'),
createImage('06_video.png'),
caption('图 6-1 视频管理页 - 设备视频文件批量下载'),
h2('6.2 定时任务功能测试'),
p('完成 Web 端定时任务功能全流程测试:'),
bullet('任务创建与配置流程校验'),
bullet('任务执行逻辑验证'),
bullet('任务启停控制测试'),
bullet('周期运行逻辑校验'),
bullet('保障定时任务功能稳定可用,满足平台自动化运维需求'),
new Paragraph({ children: [new PageBreak()] }),
// === 第七章 视频与日志管理模块 ===
h1('第七章 视频与日志管理模块'),
h2('7.1 视频管理模块改造升级'),
p('完成视频管理界面功能开发,支持批量下载设备本地存储视频文件。本次改造重点包括:'),
bullet('将原有 Node 脚本实现的下载逻辑重构为 Java 接口,统一后端技术栈'),
bullet('提升服务统一性、稳定性和可维护性'),
bullet('完成视频接入接口对接'),
bullet('设备管理 tab 重构,加上 flv 预览能力(仅供测试,正式建议下载使用)'),
h2('7.2 日志管理模块开发'),
p('完成日志管理界面功能开发,实现权限区分展示:'),
bullet('管理员可查看服务端运行日志'),
bullet('便于后续问题排查与系统运维'),
bullet('目前相关界面已完成开发,待后续对接完整接口'),
createImage('17_profile.png'),
caption('图 7-1 个人中心 - 用户权限与日志查看入口'),
new Paragraph({ children: [new PageBreak()] }),
// === 第八章 国际化与 AI 升级 ===
h1('第八章 国际化适配与 AI 大模型升级'),
h2('8.1 系统中英文国际化适配'),
p('完成平台核心业务界面的中英文双语切换功能开发:'),
bullet('覆盖日常使用高频页面,满足多语言使用场景'),
bullet('登录页右上角支持中英文一键切换'),
bullet('针对静态页面暂未做国际化接入,考虑到后续静态页面结构和内容仍会频繁调整,暂不提前适配,避免重复开发与无效迭代,合理节约开发成本'),
createImage('00_login.png'),
caption('图 8-1 登录页 - 中英文切换入口'),
h2('8.2 AI 大模型版本迭代升级'),
p('根据郭老师工作部署及项目智能化迭代要求,完成项目 AI 大模型版本升级替换:'),
bullet('将原有模型统一更新为 GPT-5.6 版本'),
bullet('新版本模型有效优化推理精度、交互响应效果与业务适配性'),
bullet('进一步提升平台智能化服务能力'),
bullet('为后续各类智能业务场景落地提供可靠的技术支撑'),
createImage('10_ai.png'),
caption('图 8-2 AI 诊断分析页 - 大模型升级后的智能分析能力'),
new Paragraph({ children: [new PageBreak()] }),
// === 第九章 基础功能模块完善 ===
h1('第九章 基础功能模块完善'),
h2('9.1 用户与角色管理优化'),
p('优化用户与角色模块,根据管理员的角色获取名下用户,让权限管理、用户管理体系更清晰规整:'),
bullet('用户角色概览功能开发'),
bullet('角色权限概览功能开发'),
bullet('优化页面展示逻辑与数据渲染效果'),
bullet('修复 admin 可以看到所有用户的问题,按角色严格管控可见范围'),
bullet('地图中鼠标悬浮显示机器信息时,优化显示登录用户的角色'),
createImage('15_users.png'),
caption('图 9-1 用户管理页 - 用户体系与权限概览'),
createImage('16_roles.png'),
caption('图 9-2 角色管理页 - 角色权限概览'),
h2('9.2 电站地址功能接入'),
p('完成电站地址功能接入,适配多场站运维管理场景:'),
bullet('支持自定义修改场站地址'),
bullet('切换不同电站时可精准匹配、展示对应场站位置'),
bullet('修复地图地址显示相关问题'),
bullet('修改名称和电站地址,让场站标识更清晰'),
h2('9.3 控制台报错信息清理'),
p('针对性优化代码消除控制台的报错信息:'),
bullet('修复地图图标大小在初始化时长宽 -1 的警告'),
bullet('优化 mqtt 退出路由时不订阅消息,避免不必要的报错'),
bullet('提升整体代码质量与运行稳定性'),
new Paragraph({ children: [new PageBreak()] }),
// === 第十章 APP 端优化指导 ===
h1('第十章 APP 端优化指导与测试'),
h2('10.1 APP 端功能优化指导'),
p('本月持续跟进机器 APP 端优化迭代与测试工作,全程指导 APP 端完成核心功能适配:'),
bullet('路径规划全套业务流程适配与调试,保证移动端与 Web 端业务逻辑统一、交互闭环'),
bullet('扫码绑定设备功能优化整改'),
bullet('蓝牙/非蓝牙模式适配'),
bullet('授权管控设备参数修改功能开发'),
bullet('视频推拉流控制功能开发与调试,完善设备视频实时操控能力'),
bullet('无人机实时数据展示逻辑优化,提升数据刷新实时性与页面展示效果'),
h2('10.2 APP 端测试验收情况'),
p('对优化后的功能进行全面测试,校验场景适配性、权限逻辑及操作流畅度:'),
bullet('本月针对本月遗留问题进行专项攻坚,绝大部分测试点已完成验证,整体功能基本达标'),
bullet('剩余约 10% 测试内容仍存在轻微问题,后续将持续跟进排查、优化修复,完成全量测试闭环'),
bullet('及时发现问题并反馈、跟进整改落地,保障 APP 端各项核心功能正常运行'),
bullet('进一步提升移动端设备运维操作的便捷性与稳定性'),
callout('移动端价值', 'APP 端作为现场运维人员的随身工具,承担扫码绑定、蓝牙管控、视频操控、数据展示等核心能力,本月完成全部核心功能指导与 90% 测试验证。'),
new Paragraph({ children: [new PageBreak()] }),
// === 第十一章 全业务链路闭环测试 ===
h1('第十一章 全业务链路闭环测试'),
h2('11.1 告警工单全流程链路测试'),
p('完成系统告警、工单、任务全流程链路逻辑测试:'),
bullet('依据系统配置规则,校验告警触发后自动生成工单的准确性'),
bullet('核对告警列表与工单列表的数据一致性'),
bullet('告警闭环逻辑验证:关联告警的工单完成或关闭后,对应告警自动消除、停止持续触发'),
bullet('若设备仍存在异常、处于告警状态,系统可重新触发告警,逻辑符合业务预期'),
createImage('09_alerts.png'),
caption('图 11-1 告警中心 - 告警列表与工单联动'),
h2('11.2 告警钉钉推送测试'),
p('完成告警钉钉推送功能测试:'),
bullet('支持各场站独立配置开启/关闭钉钉推送权限'),
bullet('实现告警消息灵活推送,满足多场站差异化通知需求'),
h2('11.3 工单派发功能迭代优化'),
p('新增平台工单手动派发逻辑:'),
bullet('支持管理员将各类运维工单定向指派给指定用户'),
bullet('用户端可正常展示、接收被派发的工单'),
bullet('完善了平台工单分配管理机制'),
bullet('手动派发功能已调试完成,后续将持续迭代开发工单自动派发功能'),
createImage('12_workorder.png'),
caption('图 11-2 工单任务页 - 工单派发与全生命周期管理'),
h2('11.4 设备参数配置功能测试'),
p('完成平台设备参数配置功能测试:'),
bullet('系统支持自定义配置设备轮胎增益系数、运行速度等核心设备参数'),
bullet('页面配置逻辑、参数保存功能运行正常'),
bullet('受下位机未完成全量测试影响,该模块整体联调工作暂未完成,待下位机测试收尾后继续完善'),
h2('11.5 剩余待测试工作'),
p('当前平台核心链路测试基本完成,后续需结合实际工单场景,完成平台向机器下发任务的全流程测试,补齐业务链路测试闭环。'),
new Paragraph({ children: [new PageBreak()] }),
// === 第十二章 软著申报与文档沉淀 ===
h1('第十二章 软著申报与文档沉淀'),
h2('12.1 软件著作权申报材料撰写'),
p('结合智慧运维平台现有功能、业务架构与技术实现,独立完成软件著作权申报材料的撰写:'),
bullet('梳理系统创新点、功能架构与应用场景'),
bullet('根据审核要求及修改建议持续迭代优化内容'),
bullet('推进软著申报进度,助力后续顺利审核通过'),
callout('知识产权价值', '软件著作权申报是平台技术成果的重要知识产权沉淀,有助于提升项目技术壁垒,保护核心代码资产。'),
h2('12.2 iOS 适配技术文档'),
p('协助指导 APP 苹果端相关功能实现:'),
bullet('梳理 iOS 端调试要点'),
bullet('独立整理并输出 ios.md 技术文档'),
bullet('统一调试规范、记录适配细节'),
bullet('方便后续迭代对接、问题复盘与维护调试'),
bullet('有效降低后续对接成本'),
h2('12.3 Web 端路径规划 SOP 文档'),
p('梳理 Web 端路径规划完整操作流程,编写标准化 SOP 操作文档:'),
bullet('统一操作规范'),
bullet('便于后续迭代维护、人员交接与业务落地'),
h2('12.4 新增 TodoList 页面建设'),
p('完成平台 TodoList 页面的搭建建设工作:'),
bullet('在日常测试与功能迭代过程中,系统梳理汇总各类待优化、待开发、待测试的后续任务点'),
bullet('统一录入 TodoList 页面进行集中管理'),
bullet('通过该页面可清晰沉淀后续迭代需求、未完成工作及长期优化规划'),
bullet('实现任务可视化、条理化管理,为后续工作推进、任务落地、迭代规划提供清晰的参考依据'),
bullet('有效提升工作统筹效率'),
new Paragraph({ children: [new PageBreak()] }),
// === 第十三章 Git 提交记录统计 ===
h1('第十三章 Git 提交记录统计'),
p('本月(2026-08-01 至 2026-08-31)项目代码仓库共提交 58 次,提交者覆盖项目主体开发人员。以下为按时间倒序的关键提交记录摘要,反映本月功能开发与修复节奏。'),
h2('13.1 关键提交节点摘录'),
bullet('2026-08-31:修复地图鼠标悬浮显示机器、admin 用户可见范围、控制台警告信息、地图地址显示等多项问题'),
bullet('2026-08-31:优化 mqtt 退出路由不订阅消息,消除控制台报错信息'),
bullet('2026-08-28:和算法联调显示规划路径并显示实时路径成功;根据模式显示区域还是线;优化一加载显示的地图内容'),
bullet('2026-08-25:地图打点和清除打点标志;测权限控制释放权限,优化 WS 连接'),
bullet('2026-08-21:用户角色概览;基础设置中角色概览;修改名称和电站地址;电站地址接入'),
bullet('2026-08-20:杀死页面和切换页面释放控制权限;控制权限修改,不需要判断 Web 是否有权限'),
bullet('2026-08-20:优化 WS 切换时候的时序差导致遗留上次的消息'),
bullet('2026-08-19:和算法测试成功第一步,状态界面看到经纬度信息和保存可以成功;中英文优化;路由组件拆分;登录默认选中'),
bullet('2026-08-14:中英文切换根据已有功能已经基本完善'),
bullet('2026-08-12:中英文切换初始化;软著文档;设备管理 tab 重构,加上 flv 预览'),
bullet('2026-08-10:视频接入接口;增加视频管理和日志管理;文档增加'),
bullet('2026-08-07:优化设备管理界面将 5 个子页面抽离出组件轻量化,优化设备参数弹窗 loading 效果'),
bullet('2026-08-06:优化用户与角色模块根据管理员角色获取名下用户;优化状态界面视频停止和状态信息解耦;增加推拉流播放和停止;优化派单选择接收人员;两个地方的设备参数组件加上授权功能'),
bullet('2026-08-05:设备参数需要滑块'),
bullet('2026-08-04:所有传 orgId 的地方修改;优化详情跳转'),
bullet('2026-08-03:根据二级路由优化控制状态界面;二级菜单优化完成;二级菜单配置;机器人的路由 OK 了;二级菜单初步实现'),
h2('13.2 提交记录反映的工作主线'),
p('从提交时间线可以看出本月工作呈现以下节奏:'),
bullet('8 月上旬(8/3-8/12):路由架构升级、用户角色模块优化、设备参数滑块与授权、视频日志管理模块、中英文切换、软著文档'),
bullet('8 月中旬(8/14-8/21):中英文完善、路由组件拆分、WS 时序差优化、权限控制重构、电站地址接入、角色概览'),
bullet('8 月下旬(8/25-8/31):权限释放测试与 WS 连接优化、地图打点与算法联调、地图显示与机器悬浮显示优化、控制台报错清理'),
createImage('01_home.png'),
caption('图 13-1 系统总览首页 - 平台整体运行态势'),
new Paragraph({ children: [new PageBreak()] }),
// === 第十四章 本月小结与下月计划 ===
h1('第十四章 本月小结与下月计划'),
h2('14.1 本月工作小结'),
p('本月工作覆盖智慧运维系统平台的底层机制、业务功能、移动端协同、知识产权四大维度,整体呈现"底座加固、业务闭环、移动协同、知识产权沉淀"四大特征。'),
h3('14.1.1 底座加固'),
bullet('WebSocket 单页面单连接 + 路由切换自动销毁,通信稳定性显著提升'),
bullet('控制权限单设备单 Token 唯一占用,跨端协同与权限释放机制完善'),
bullet('路由架构升级为二级路由体系,页面层级更清晰、跳转更顺畅'),
h3('14.1.2 业务闭环'),
bullet('路径规划全链路打通,与苏州算法团队联调成功'),
bullet('设备参数双端开发完成、APP 蓝牙模式适配'),
bullet('视频管理 Java 接口重构、日志权限区分展示'),
bullet('告警工单全流程闭环测试通过、工单手动派发功能上线'),
h3('14.1.3 移动协同'),
bullet('APP 端路径规划、扫码绑定、蓝牙/非蓝牙模式、参数授权、视频推拉流、无人机数据展示等核心功能指导与 90% 测试验证'),
bullet('iOS 适配技术文档沉淀,降低后续对接成本'),
h3('14.1.4 知识产权与智能化'),
bullet('软件著作权申报材料撰写与迭代'),
bullet('AI 大模型统一升级至 GPT-5.6,智能化能力进一步提升'),
bullet('中英文国际化覆盖高频业务页面'),
bullet('Web 端路径规划 SOP 文档与 TodoList 任务管理页面建设'),
h2('14.2 下月工作计划'),
p('基于本月工作成果与遗留事项,下月将重点推进以下方向:'),
bullet('路径规划联调收尾:完成与苏州团队的剩余联调内容,覆盖更多业务场景'),
bullet('设备参数联调:等待下位机完成全量测试后,继续完善设备参数配置模块整体联调'),
bullet('APP 端剩余 10% 测试:完成 APP 端全量测试闭环,保障移动端稳定可用'),
bullet('平台下发任务全流程测试:结合实际工单场景,完成平台向机器下发任务的全流程测试'),
bullet('工单自动派发功能:在手动派发基础上迭代开发自动派发能力,提升运维自动化效率'),
bullet('国际化覆盖扩展:根据静态页面调整情况,逐步推进静态页面国际化适配'),
bullet('软著申报跟进:根据审核反馈持续优化申报材料,推进顺利审核通过'),
callout('结语', '本月围绕"稳定、安全、协同、闭环"四大关键词开展工作,平台底座更加稳固、业务链路更加完整、移动协同更加顺畅、知识产权沉淀更加扎实。后续将持续以业务价值为导向,推进各项功能迭代落地,持续提升智慧运维平台的综合能力。'),
// 附录:更多系统截图
new Paragraph({ children: [new PageBreak()] }),
h1('附录 系统功能截图集'),
p('以下为本月工作过程中涉及的系统功能截图集,作为本月工作产出的可视化补充材料。'),
h2('A.1 总览与监控'),
createImage('08_monitor.png'),
caption('图 A-1 监控中心 - 设备实时监控视图'),
h2('A.2 设备管理'),
createImage('11_clean.png'),
caption('图 A-2 清洗优化页 - 智能清洗策略'),
h2('A.3 报表与系统'),
createImage('13_report.png'),
caption('图 A-3 报表中心 - 多维度报表输出'),
createImage('14_system.png'),
caption('图 A-4 系统设置 - 基础设置入口'),
],
}],
});
Packer.toBuffer(doc).then((buffer) => {
const outPath = path.join(__dirname, '智慧运维系统平台-月度工作总结报告-2026年8月-葌昀.docx');
fs.writeFileSync(outPath, buffer);
console.log('✓ 月度报告已生成:', outPath);
const stats = fs.statSync(outPath);
console.log(' 文件大小:', (stats.size / 1024).toFixed(2), 'KB');
}).catch((err) => {
console.error('✗ 生成失败:', err);
process.exit(1);
});

184
src/api/ota.ts Normal file
View File

@@ -0,0 +1,184 @@
// OTA 管理 Mock API 层
// 后端接口就绪后,把每个函数内部的 setTimeout + 内存数据替换为 request 调用即可,页面无需改动。
// 真实接口建议:
// GET /iot/ota/machines → getMachineVersionList
// GET /iot/ota/firmwares → getFirmwarePackageList
// POST /iot/ota/firmwares/upload (multipart) → uploadFirmwarePackage
// POST /iot/ota/upgrade → upgradeMachines
// POST /iot/ota/rollback → rollbackMachines
// DELETE /iot/ota/firmwares/{id} → deleteFirmwarePackage
export interface MachineVersion {
id: number;
code: string;
name: string;
type: string;
online: 'ONLINE' | 'OFFLINE';
currentVersion: string;
previousVersion: string | null;
lastUpgradeTime: string | null;
status: 'IDLE' | 'UPGRADING' | 'FAILED' | 'SUCCESS';
}
export interface FirmwarePackage {
id: number;
version: string;
fileName: string;
fileSize: number;
description?: string;
uploadTime: string;
}
interface ApiResponse<T> {
code: number;
msg: string;
data: T;
}
function mockResponse<T>(data: T, delay = 300): Promise<ApiResponse<T>> {
return new Promise(resolve => {
setTimeout(() => resolve({ code: 200, msg: 'ok', data }), delay);
});
}
// ====== 模块级内存数据(刷新即重置) ======
let firmwareIdSeed = 100;
const firmwareStore: FirmwarePackage[] = [
{
id: firmwareIdSeed++,
version: 'v1.0.0',
fileName: 'firmware-v1.0.0.bin',
fileSize: 2_359_296,
description: '初始正式版本',
uploadTime: '2026-08-12 10:23:45',
},
{
id: firmwareIdSeed++,
version: 'v1.1.0',
fileName: 'firmware-v1.1.0.zip',
fileSize: 3_145_728,
description: '新增路径规划算法优化',
uploadTime: '2026-09-05 14:50:12',
},
];
const machineStore: MachineVersion[] = [
{ id: 1, code: 'ROBOT-01', name: '运维机器人 ROBOT-01', type: 'ROBOT', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-06 09:12:33', status: 'SUCCESS' },
{ id: 2, code: 'ROBOT-02', name: '运维机器人 ROBOT-02', type: 'ROBOT', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-06 09:15:10', status: 'SUCCESS' },
{ id: 3, code: 'ROBOT-03', name: '运维机器人 ROBOT-03', type: 'ROBOT', online: 'OFFLINE', currentVersion: 'v1.0.0', previousVersion: null, lastUpgradeTime: null, status: 'IDLE' },
{ id: 4, code: 'DRONE-01', name: '无人机 DRONE-01', type: 'DRONE', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-07 11:08:21', status: 'SUCCESS' },
{ id: 5, code: 'DRONE-02', name: '无人机 DRONE-02', type: 'DRONE', online: 'ONLINE', currentVersion: 'v1.0.0', previousVersion: null, lastUpgradeTime: null, status: 'IDLE' },
{ id: 6, code: 'CAM-01', name: '摄像头 CAM-01', type: 'CAMERA', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-08 16:42:50', status: 'SUCCESS' },
{ id: 7, code: 'CAM-02', name: '摄像头 CAM-02', type: 'CAMERA', online: 'OFFLINE', currentVersion: 'v1.0.0', previousVersion: null, lastUpgradeTime: null, status: 'IDLE' },
{ id: 8, code: 'CLEAN-01', name: '清扫机器人 CLEAN-01', type: 'CLEANING_DEVICE', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-09 08:30:00', status: 'FAILED' },
{ id: 9, code: 'WEATHER-01', name: '气象站 WEATHER-01', type: 'WEATHER_STATION', online: 'ONLINE', currentVersion: 'v1.0.0', previousVersion: null, lastUpgradeTime: null, status: 'IDLE' },
{ id: 10, code: 'INV-A01', name: '逆变器 INV-A01', type: 'INVERTER', online: 'ONLINE', currentVersion: 'v1.1.0', previousVersion: 'v1.0.0', lastUpgradeTime: '2026-09-10 10:15:22', status: 'SUCCESS' },
];
function nowStr(): string {
const d = new Date();
const pad = (n: number) => String(n).padStart(2, '0');
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
}
// ====== 导出 Mock API 函数 ======
export function getMachineVersionList(): Promise<ApiResponse<MachineVersion[]>> {
return mockResponse([...machineStore]);
}
export function getFirmwarePackageList(): Promise<ApiResponse<FirmwarePackage[]>> {
return mockResponse([...firmwareStore]);
}
export function uploadFirmwarePackage(params: {
file: File;
version: string;
description?: string;
}): Promise<ApiResponse<FirmwarePackage>> {
const pkg: FirmwarePackage = {
id: firmwareIdSeed++,
version: params.version,
fileName: params.file.name,
fileSize: params.file.size,
description: params.description || '',
uploadTime: nowStr(),
};
firmwareStore.unshift(pkg);
return mockResponse(pkg, 500);
}
export function deleteFirmwarePackage(id: number): Promise<ApiResponse<null>> {
const idx = firmwareStore.findIndex(p => p.id === id);
if (idx >= 0) firmwareStore.splice(idx, 1);
return mockResponse(null, 200);
}
export function upgradeMachines(params: {
machineIds: number[];
targetVersion: string;
}): Promise<ApiResponse<{ upgraded: number[]; skipped: number[] }>> {
const upgraded: number[] = [];
const skipped: number[] = [];
const time = nowStr();
params.machineIds.forEach(id => {
const m = machineStore.find(x => x.id === id);
if (!m) {
skipped.push(id);
return;
}
if (m.currentVersion === params.targetVersion) {
// 目标版本与当前版本一致,跳过
skipped.push(id);
return;
}
m.previousVersion = m.currentVersion;
m.currentVersion = params.targetVersion;
m.lastUpgradeTime = time;
m.status = 'UPGRADING';
upgraded.push(id);
});
// 模拟升级完成
setTimeout(() => {
upgraded.forEach(id => {
const m = machineStore.find(x => x.id === id);
if (m) m.status = 'SUCCESS';
});
}, 1500);
return mockResponse({ upgraded, skipped }, 600);
}
export function rollbackMachines(params: {
machineIds: number[];
}): Promise<ApiResponse<{ rolledBack: number[]; skipped: number[]; skippedNames: string[] }>> {
const rolledBack: number[] = [];
const skipped: number[] = [];
const skippedNames: string[] = [];
const time = nowStr();
params.machineIds.forEach(id => {
const m = machineStore.find(x => x.id === id);
if (!m) {
skipped.push(id);
return;
}
if (!m.previousVersion) {
skipped.push(id);
skippedNames.push(m.name);
return;
}
const prev = m.previousVersion;
m.previousVersion = m.currentVersion;
m.currentVersion = prev;
m.lastUpgradeTime = time;
m.status = 'UPGRADING';
rolledBack.push(id);
});
setTimeout(() => {
rolledBack.forEach(id => {
const m = machineStore.find(x => x.id === id);
if (m) m.status = 'SUCCESS';
});
}, 1500);
return mockResponse({ rolledBack, skipped, skippedNames }, 600);
}

View File

@@ -0,0 +1,553 @@
import React, { useState, useEffect, useMemo } from 'react';
import { useTranslation } from 'react-i18next';
import {
Typography,
Card,
Upload,
Input,
Button,
Form,
Table,
Space,
Tag,
App,
Modal,
Select,
Popconfirm,
Tooltip,
} from 'antd';
import {
UploadOutlined,
ReloadOutlined,
ArrowUpOutlined,
ArrowDownOutlined,
DeleteOutlined,
InboxOutlined,
} from '@ant-design/icons';
import type { UploadProps } from 'antd';
import type { ColumnsType } from 'antd/es/table';
import {
getMachineVersionList,
getFirmwarePackageList,
uploadFirmwarePackage,
deleteFirmwarePackage,
upgradeMachines,
rollbackMachines,
type MachineVersion,
type FirmwarePackage,
} from '@/api/ota';
const { Title, Text } = Typography;
const { TextArea } = Input;
const VERSION_PATTERN = /^v\d+\.\d+\.\d+$/;
function formatFileSize(bytes: number): string {
if (bytes < 1024) return `${bytes} B`;
if (bytes < 1024 * 1024) return `${(bytes / 1024).toFixed(1)} KB`;
return `${(bytes / 1024 / 1024).toFixed(2)} MB`;
}
export default function OTAManagement() {
const { t } = useTranslation();
const { modal, message } = App.useApp();
const [uploadForm] = Form.useForm();
const [machineList, setMachineList] = useState<MachineVersion[]>([]);
const [firmwareList, setFirmwareList] = useState<FirmwarePackage[]>([]);
const [loadingMachines, setLoadingMachines] = useState(false);
const [loadingFirmwares, setLoadingFirmwares] = useState(false);
const [submitting, setSubmitting] = useState(false);
const [selectedMachineIds, setSelectedMachineIds] = useState<React.Key[]>([]);
const [pendingFile, setPendingFile] = useState<File | null>(null);
const fetchMachines = async () => {
setLoadingMachines(true);
try {
const res = await getMachineVersionList();
if (res.code === 200) setMachineList(res.data);
} finally {
setLoadingMachines(false);
}
};
const fetchFirmwares = async () => {
setLoadingFirmwares(true);
try {
const res = await getFirmwarePackageList();
if (res.code === 200) setFirmwareList(res.data);
} finally {
setLoadingFirmwares(false);
}
};
useEffect(() => {
fetchMachines();
fetchFirmwares();
// eslint-disable-next-line react-hooks/exhaustive-deps
}, []);
const refreshAll = () => {
fetchMachines();
fetchFirmwares();
};
const hasSelection = selectedMachineIds.length > 0;
// ====== 上传 ======
const uploadProps: UploadProps = {
accept: '.bin,.zip',
maxCount: 1,
beforeUpload: (file) => {
const isValidType = /\.(bin|zip)$/i.test(file.name);
if (!isValidType) {
message.error(t('systemSetting.ota.invalidFileType'));
return Upload.LIST_IGNORE;
}
setPendingFile(file);
return false; // 阻止自动上传
},
onRemove: () => {
setPendingFile(null);
},
file: pendingFile ? [pendingFile as any] : [],
};
const handleUploadSubmit = async () => {
try {
const values = await uploadForm.validateFields();
if (!pendingFile) {
message.warning(t('systemSetting.ota.noFileSelected'));
return;
}
setSubmitting(true);
const res = await uploadFirmwarePackage({
file: pendingFile,
version: values.version,
description: values.description,
});
if (res.code === 200) {
message.success(t('systemSetting.ota.uploadSuccess'));
uploadForm.resetFields();
setPendingFile(null);
fetchFirmwares();
}
} catch (err) {
// 表单校验失败
} finally {
setSubmitting(false);
}
};
const handleDeleteFirmware = async (record: FirmwarePackage) => {
await deleteFirmwarePackage(record.id);
message.success(t('common.deleteSuccess'));
fetchFirmwares();
};
// ====== 升级 ======
const showUpgradeModal = () => {
if (!hasSelection) {
message.warning(t('systemSetting.ota.noSelection'));
return;
}
if (firmwareList.length === 0) {
message.warning(t('systemSetting.ota.noFirmware'));
return;
}
let selectedVersion = firmwareList[0].version;
const content = (
<Form layout="vertical">
<Form.Item label={t('systemSetting.ota.selectTargetVersion')} required>
<Select
options={firmwareList.map(f => ({ label: `${f.version} (${f.fileName})`, value: f.version }))}
defaultValue={selectedVersion}
onChange={(v) => (selectedVersion = v)}
style={{ width: '100%' }}
/>
</Form.Item>
<Text type="secondary" style={{ fontSize: 12 }}>
{t('systemSetting.ota.confirmUpgrade', { count: selectedMachineIds.length, version: '—' })}
</Text>
</Form>
);
modal.confirm({
title: t('systemSetting.ota.batchUpgrade'),
content,
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
setSubmitting(true);
try {
const res = await upgradeMachines({
machineIds: selectedMachineIds as number[],
targetVersion: selectedVersion,
});
if (res.code === 200) {
message.success(t('systemSetting.ota.upgradeSuccess'));
setSelectedMachineIds([]);
// 立即刷新一次显示 UPGRADING 状态,再延迟刷新为 SUCCESS
fetchMachines();
setTimeout(fetchMachines, 1800);
}
} finally {
setSubmitting(false);
}
},
});
};
const handleSingleUpgrade = (record: MachineVersion) => {
if (firmwareList.length === 0) {
message.warning(t('systemSetting.ota.noFirmware'));
return;
}
let selectedVersion = firmwareList[0].version;
modal.confirm({
title: t('systemSetting.ota.batchUpgrade'),
content: (
<Form layout="vertical">
<Form.Item label={t('systemSetting.ota.selectTargetVersion')} required>
<Select
options={firmwareList.map(f => ({ label: `${f.version} (${f.fileName})`, value: f.version }))}
defaultValue={selectedVersion}
onChange={(v) => (selectedVersion = v)}
style={{ width: '100%' }}
/>
</Form.Item>
</Form>
),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
setSubmitting(true);
try {
const res = await upgradeMachines({
machineIds: [record.id],
targetVersion: selectedVersion,
});
if (res.code === 200) {
message.success(t('systemSetting.ota.upgradeSuccess'));
fetchMachines();
setTimeout(fetchMachines, 1800);
}
} finally {
setSubmitting(false);
}
},
});
};
// ====== 回退 ======
const handleBatchRollback = () => {
if (!hasSelection) {
message.warning(t('systemSetting.ota.noSelection'));
return;
}
modal.confirm({
title: t('systemSetting.ota.batchRollback'),
content: t('systemSetting.ota.confirmRollback', { count: selectedMachineIds.length }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
setSubmitting(true);
try {
const res = await rollbackMachines({ machineIds: selectedMachineIds as number[] });
if (res.code === 200) {
if (res.data.skippedNames.length > 0) {
message.warning(
res.data.skippedNames
.map(n => t('systemSetting.ota.noPreviousVersion', { name: n }))
.join(';')
);
}
if (res.data.rolledBack.length > 0) {
message.success(t('systemSetting.ota.rollbackSuccess'));
}
setSelectedMachineIds([]);
fetchMachines();
setTimeout(fetchMachines, 1800);
}
} finally {
setSubmitting(false);
}
},
});
};
const handleSingleRollback = (record: MachineVersion) => {
if (!record.previousVersion) {
message.warning(t('systemSetting.ota.noPreviousVersion', { name: record.name }));
return;
}
modal.confirm({
title: t('systemSetting.ota.batchRollback'),
content: t('systemSetting.ota.confirmRollback', { count: 1 }),
okText: t('common.confirm'),
cancelText: t('common.cancel'),
onOk: async () => {
setSubmitting(true);
try {
const res = await rollbackMachines({ machineIds: [record.id] });
if (res.code === 200) {
if (res.data.rolledBack.length > 0) {
message.success(t('systemSetting.ota.rollbackSuccess'));
}
fetchMachines();
setTimeout(fetchMachines, 1800);
}
} finally {
setSubmitting(false);
}
},
});
};
// ====== 列定义 ======
const firmwareColumns: ColumnsType<FirmwarePackage> = [
{ title: t('systemSetting.ota.version'), dataIndex: 'version', key: 'version', width: 120 },
{ title: t('systemSetting.ota.fileName'), dataIndex: 'fileName', key: 'fileName', ellipsis: true },
{
title: t('systemSetting.ota.fileSize'),
dataIndex: 'fileSize',
key: 'fileSize',
width: 120,
render: (v: number) => formatFileSize(v),
},
{ title: t('systemSetting.ota.description'), dataIndex: 'description', key: 'description', ellipsis: true },
{ title: t('systemSetting.ota.uploadTime'), dataIndex: 'uploadTime', key: 'uploadTime', width: 180 },
{
title: t('roles.actions'),
key: 'action',
width: 100,
render: (_: any, record: FirmwarePackage) => (
<Popconfirm
title={t('systemSetting.ota.deleteConfirm', { version: record.version })}
onConfirm={() => handleDeleteFirmware(record)}
okText={t('common.confirm')}
cancelText={t('common.cancel')}
>
<Button type="link" danger icon={<DeleteOutlined />}>
{t('roles.delete')}
</Button>
</Popconfirm>
),
},
];
const machineColumns: ColumnsType<MachineVersion> = [
{ title: t('systemSetting.ota.machineCode'), dataIndex: 'code', key: 'code', width: 120 },
{ title: t('devices.deviceName'), dataIndex: 'name', key: 'name', ellipsis: true },
{ title: t('common.type'), dataIndex: 'type', key: 'type', width: 140 },
{
title: t('devices.online'),
dataIndex: 'online',
key: 'online',
width: 100,
render: (v: string) => <Tag color={v === 'ONLINE' ? 'green' : 'red'}>{v}</Tag>,
},
{
title: t('systemSetting.ota.currentVersion'),
dataIndex: 'currentVersion',
key: 'currentVersion',
width: 130,
render: (v: string) => <Tag color="blue">{v}</Tag>,
},
{
title: t('systemSetting.ota.previousVersion'),
dataIndex: 'previousVersion',
key: 'previousVersion',
width: 130,
render: (v: string | null) => (v ? <Tag>{v}</Tag> : <Text type="secondary">—</Text>),
},
{
title: t('systemSetting.ota.lastUpgradeTime'),
dataIndex: 'lastUpgradeTime',
key: 'lastUpgradeTime',
width: 180,
render: (v: string | null) => v || <Text type="secondary">—</Text>,
},
{
title: t('systemSetting.ota.upgradeStatus'),
dataIndex: 'status',
key: 'status',
width: 110,
render: (v: MachineVersion['status']) => {
const map: Record<string, { color: string; key: string }> = {
IDLE: { color: 'default', key: 'statusIdle' },
UPGRADING: { color: 'processing', key: 'statusUpgrading' },
FAILED: { color: 'red', key: 'statusFailed' },
SUCCESS: { color: 'green', key: 'statusSuccess' },
};
const cfg = map[v] || map.IDLE;
return <Tag color={cfg.color}>{t(`systemSetting.ota.${cfg.key}`)}</Tag>;
},
},
{
title: t('roles.actions'),
key: 'action',
width: 160,
render: (_: any, record: MachineVersion) => (
<Space>
<Button
type="link"
size="small"
icon={<ArrowUpOutlined />}
onClick={() => handleSingleUpgrade(record)}
>
{t('systemSetting.ota.batchUpgrade')}
</Button>
<Button
type="link"
size="small"
icon={<ArrowDownOutlined />}
onClick={() => handleSingleRollback(record)}
disabled={!record.previousVersion}
>
{t('systemSetting.ota.batchRollback')}
</Button>
</Space>
),
},
];
const machineRowSelection = {
selectedRowKeys: selectedMachineIds,
onChange: (keys: React.Key[]) => setSelectedMachineIds(keys),
};
const uploadCard = (
<Card size="small" style={{ marginBottom: 12 }}>
<Title level={5} style={{ marginTop: 0, marginBottom: 12 }}>
{t('systemSetting.ota.uploadCard')}
</Title>
<Form form={uploadForm} layout="inline" style={{ flexWrap: 'wrap', gap: 8 }}>
<Form.Item
name="version"
label={t('systemSetting.ota.version')}
rules={[
{ required: true, message: t('systemSetting.ota.versionRequired') },
{ pattern: VERSION_PATTERN, message: t('systemSetting.ota.versionPatternHint') },
]}
>
<Input placeholder={t('systemSetting.ota.versionPlaceholder')} style={{ width: 200 }} />
</Form.Item>
<Form.Item name="description" label={t('systemSetting.ota.description')}>
<TextArea
placeholder={t('systemSetting.ota.descriptionPlaceholder')}
style={{ width: 320, minHeight: 32 }}
autoSize={{ minRows: 1, maxRows: 2 }}
/>
</Form.Item>
<Form.Item label={t('systemSetting.ota.firmwareFile')} required>
<Upload.Dragger
{...uploadProps}
multiple={false}
style={{ width: 320, padding: '8px 12px' }}
>
<p className="ant-upload-drag-icon">
<InboxOutlined />
</p>
<p className="ant-upload-text" style={{ fontSize: 12 }}>
{t('systemSetting.ota.dragHint')}
</p>
</Upload.Dragger>
</Form.Item>
<Form.Item>
<Button
type="primary"
icon={<UploadOutlined />}
loading={submitting}
onClick={handleUploadSubmit}
>
{t('systemSetting.ota.uploadBtn')}
</Button>
</Form.Item>
</Form>
</Card>
);
const firmwareCard = (
<Card size="small" style={{ marginBottom: 12 }}>
<Title level={5} style={{ marginTop: 0, marginBottom: 12 }}>
{t('systemSetting.ota.firmwareList')}
</Title>
<Table
rowKey="id"
size="small"
columns={firmwareColumns}
dataSource={firmwareList}
loading={loadingFirmwares}
pagination={{ pageSize: 10, showSizeChanger: true, showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}` }}
/>
</Card>
);
const machineCard = (
<Card size="small">
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 12, flexWrap: 'wrap', gap: 8 }}>
<Title level={5} style={{ margin: 0 }}>
{t('systemSetting.ota.machineVersionList')}
</Title>
<Space wrap>
<Button
type="primary"
icon={<ArrowUpOutlined />}
onClick={showUpgradeModal}
disabled={!hasSelection}
loading={submitting}
>
{t('systemSetting.ota.batchUpgrade')}
{hasSelection ? ` (${selectedMachineIds.length})` : ''}
</Button>
<Button
icon={<ArrowDownOutlined />}
onClick={handleBatchRollback}
disabled={!hasSelection}
loading={submitting}
>
{t('systemSetting.ota.batchRollback')}
{hasSelection ? ` (${selectedMachineIds.length})` : ''}
</Button>
<Button icon={<ReloadOutlined />} onClick={refreshAll}>
{t('common.refresh')}
</Button>
</Space>
</div>
<Table
rowKey="id"
size="small"
columns={machineColumns}
dataSource={machineList}
loading={loadingMachines}
rowSelection={machineRowSelection}
scroll={{ x: 1200 }} // 添加这行
pagination={{
pageSize: 10,
showSizeChanger: true,
showQuickJumper: true,
showTotal: (total) => `${t('common.total')} ${total} ${t('common.items')}`,
}}
/>
</Card>
);
return (
<div style={{ padding: '8px 0' }}>
<div style={{ padding: '0 0 4px' }}>
<Title level={4} style={{ margin: 0, fontSize: 'clamp(14px, 1.2vw, 18px)' }}>
{t('systemSetting.ota.title')}
</Title>
</div>
{uploadCard}
{firmwareCard}
{machineCard}
</div>
);
}

View File

@@ -2429,6 +2429,47 @@ const en = {
deviceAccess: 'Device Access',
dataSafety: 'Data & Security',
systemMaintenance: 'System Maintenance',
ota: {
title: 'OTA Firmware Management',
uploadCard: 'Upload Firmware',
version: 'Version',
versionPlaceholder: 'e.g. v1.2.3',
versionRequired: 'Please enter version',
versionPatternHint: 'Version should match v1.2.3 format',
description: 'Description',
descriptionPlaceholder: 'Enter firmware description',
firmwareFile: 'Firmware File',
dragHint: 'Click or drag .bin/.zip file here to upload',
invalidFileType: 'Only .bin and .zip are supported',
noFileSelected: 'Please select a firmware file',
uploadBtn: 'Upload',
firmwareList: 'Uploaded Firmware',
fileName: 'File Name',
fileSize: 'File Size',
uploadTime: 'Upload Time',
machineVersionList: 'Machine Version List',
machineCode: 'Machine Code',
currentVersion: 'Current Version',
previousVersion: 'Previous Version',
lastUpgradeTime: 'Last Upgrade Time',
upgradeStatus: 'Upgrade Status',
batchUpgrade: 'Batch Upgrade',
batchRollback: 'Batch Rollback',
selectTargetVersion: 'Select Target Firmware Version',
confirmUpgrade: 'Confirm upgrading {count} selected machine(s) to {version}?',
confirmRollback: 'Confirm rolling back {count} selected machine(s) to previous version?',
noSelection: 'Please select machines first',
noFirmware: 'Please upload firmware first',
noPreviousVersion: 'Machine {name} has no previous version, cannot rollback',
uploadSuccess: 'Firmware uploaded successfully',
upgradeSuccess: 'Upgrade command dispatched',
rollbackSuccess: 'Rollback command dispatched',
deleteConfirm: 'Delete firmware {version}?',
statusIdle: 'Idle',
statusUpgrading: 'Upgrading',
statusFailed: 'Failed',
statusSuccess: 'Success',
},
onlineUsers: 'Online Users',
deviceManagement: 'Device Management',
logManagement: 'Log Management',
@@ -3522,6 +3563,7 @@ const en = {
systemSetting: 'System Settings',
log: 'Log Management',
videoManage: 'Video Management',
otaManagement: 'OTA Management',
downloads: 'Video Download',
users: 'User Management',
roles: 'Role Management',

View File

@@ -2464,6 +2464,47 @@ const zh = {
deviceAccess: '设备接入',
dataSafety: '数据与安全',
systemMaintenance: '系统维护',
ota: {
title: 'OTA 固件管理',
uploadCard: '上传固件包',
version: '版本号',
versionPlaceholder: '例如 v1.2.3',
versionRequired: '请输入版本号',
versionPatternHint: '版本号格式应为 v1.2.3',
description: '描述',
descriptionPlaceholder: '请输入固件包描述',
firmwareFile: '固件文件',
dragHint: '点击或拖拽 .bin/.zip 文件到此区域上传',
invalidFileType: '仅支持 .bin 和 .zip 文件',
noFileSelected: '请选择固件文件',
uploadBtn: '上传',
firmwareList: '已上传固件包',
fileName: '文件名',
fileSize: '文件大小',
uploadTime: '上传时间',
machineVersionList: '机器版本列表',
machineCode: '机器编号',
currentVersion: '当前版本',
previousVersion: '上一版本',
lastUpgradeTime: '最近升级时间',
upgradeStatus: '升级状态',
batchUpgrade: '批量升级',
batchRollback: '批量回退',
selectTargetVersion: '选择目标固件版本',
confirmUpgrade: '确认对选中的 {count} 台机器升级到 {version}?',
confirmRollback: '确认对选中的 {count} 台机器回退到上一版本?',
noSelection: '请先选择机器',
noFirmware: '请先上传固件包',
noPreviousVersion: '机器 {name} 没有上一版本,无法回退',
uploadSuccess: '固件包上传成功',
upgradeSuccess: '升级指令已下发',
rollbackSuccess: '回退指令已下发',
deleteConfirm: '确认删除固件包 {version}?',
statusIdle: '空闲',
statusUpgrading: '升级中',
statusFailed: '失败',
statusSuccess: '成功',
},
onlineUsers: '在线用户',
deviceManagement: '设备管理',
logManagement: '日志管理',
@@ -3572,6 +3613,7 @@ const zh = {
systemSetting: '系统设置',
log: '日志管理',
videoManage: '视频管理',
otaManagement: 'OTA 管理',
downloads: '视频下载',
users: '用户管理',
roles: '角色管理',

View File

@@ -40,6 +40,7 @@ const DeviceManagementPage = lazy(() => import('../components/SystemSetting/Devi
const SystemMaintenancePage = lazy(() => import('../components/SystemSetting/SystemMaintain'));
const LogManagement = lazy(() => import('../components/SystemSetting/LogManagement'));
const VideoManagement = lazy(() => import('../components/SystemSetting/VideoManagement'));
const OTAManagementPage = lazy(() => import('../components/SystemSetting/OTAManagement'));
// 加载中动画
export function PageFallback() {
@@ -174,6 +175,7 @@ export const routes: RouteObject[] = [
{ path: 'devices', element: withSuspense(DeviceManagementPage) },
{ path: 'log', element: withSuspense(LogManagement) },
{ path: 'videoManage', element: withSuspense(VideoManagement) },
{ path: 'ota', element: withSuspense(OTAManagementPage) },
]
},
{

Binary file not shown.