diff --git a/.trae/documents/ota-management-interface.md b/.trae/documents/ota-management-interface.md new file mode 100644 index 0000000..b73ccd3 --- /dev/null +++ b/.trae/documents/ota-management-interface.md @@ -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` 内部实现 diff --git a/.workbuddy/gen_monthly_report.py b/.workbuddy/gen_monthly_report.py new file mode 100644 index 0000000..2f50e64 --- /dev/null +++ b/.workbuddy/gen_monthly_report.py @@ -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") \ No newline at end of file diff --git a/.workbuddy/memory/2026-09-22.md b/.workbuddy/memory/2026-09-22.md new file mode 100644 index 0000000..763a424 --- /dev/null +++ b/.workbuddy/memory/2026-09-22.md @@ -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 截图素材 \ No newline at end of file diff --git a/2026年8月工作月度报告-葛昀.docx b/2026年8月工作月度报告-葛昀.docx new file mode 100644 index 0000000..d2fa749 Binary files /dev/null and b/2026年8月工作月度报告-葛昀.docx differ diff --git a/generate_monthly_report.cjs b/generate_monthly_report.cjs new file mode 100644 index 0000000..ebb27f8 --- /dev/null +++ b/generate_monthly_report.cjs @@ -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); +}); diff --git a/src/api/ota.ts b/src/api/ota.ts new file mode 100644 index 0000000..8781a07 --- /dev/null +++ b/src/api/ota.ts @@ -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 { + code: number; + msg: string; + data: T; +} + +function mockResponse(data: T, delay = 300): Promise> { + 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> { + return mockResponse([...machineStore]); +} + +export function getFirmwarePackageList(): Promise> { + return mockResponse([...firmwareStore]); +} + +export function uploadFirmwarePackage(params: { + file: File; + version: string; + description?: string; +}): Promise> { + 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> { + 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> { + 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> { + 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); +} diff --git a/src/components/SystemSetting/OTAManagement.tsx b/src/components/SystemSetting/OTAManagement.tsx new file mode 100644 index 0000000..03dcc8d --- /dev/null +++ b/src/components/SystemSetting/OTAManagement.tsx @@ -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([]); + const [firmwareList, setFirmwareList] = useState([]); + const [loadingMachines, setLoadingMachines] = useState(false); + const [loadingFirmwares, setLoadingFirmwares] = useState(false); + const [submitting, setSubmitting] = useState(false); + const [selectedMachineIds, setSelectedMachineIds] = useState([]); + const [pendingFile, setPendingFile] = useState(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 = ( +
+ + ({ label: `${f.version} (${f.fileName})`, value: f.version }))} + defaultValue={selectedVersion} + onChange={(v) => (selectedVersion = v)} + style={{ width: '100%' }} + /> + +
+ ), + 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 = [ + { 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) => ( + handleDeleteFirmware(record)} + okText={t('common.confirm')} + cancelText={t('common.cancel')} + > + + + ), + }, + ]; + + const machineColumns: ColumnsType = [ + { 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) => {v}, + }, + { + title: t('systemSetting.ota.currentVersion'), + dataIndex: 'currentVersion', + key: 'currentVersion', + width: 130, + render: (v: string) => {v}, + }, + { + title: t('systemSetting.ota.previousVersion'), + dataIndex: 'previousVersion', + key: 'previousVersion', + width: 130, + render: (v: string | null) => (v ? {v} : —), + }, + { + title: t('systemSetting.ota.lastUpgradeTime'), + dataIndex: 'lastUpgradeTime', + key: 'lastUpgradeTime', + width: 180, + render: (v: string | null) => v || —, + }, + { + title: t('systemSetting.ota.upgradeStatus'), + dataIndex: 'status', + key: 'status', + width: 110, + render: (v: MachineVersion['status']) => { + const map: Record = { + 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 {t(`systemSetting.ota.${cfg.key}`)}; + }, + }, + { + title: t('roles.actions'), + key: 'action', + width: 160, + render: (_: any, record: MachineVersion) => ( + + + + + ), + }, + ]; + + const machineRowSelection = { + selectedRowKeys: selectedMachineIds, + onChange: (keys: React.Key[]) => setSelectedMachineIds(keys), + }; + + const uploadCard = ( + + + {t('systemSetting.ota.uploadCard')} + +
+ + + + +