ota管理界面
This commit is contained in:
166
.trae/documents/ota-management-interface.md
Normal file
166
.trae/documents/ota-management-interface.md
Normal 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` 内部实现
|
||||
653
.workbuddy/gen_monthly_report.py
Normal file
653
.workbuddy/gen_monthly_report.py
Normal 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")
|
||||
8
.workbuddy/memory/2026-09-22.md
Normal file
8
.workbuddy/memory/2026-09-22.md
Normal 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 截图素材
|
||||
BIN
2026年8月工作月度报告-葛昀.docx
Normal file
BIN
2026年8月工作月度报告-葛昀.docx
Normal file
Binary file not shown.
675
generate_monthly_report.cjs
Normal file
675
generate_monthly_report.cjs
Normal 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
184
src/api/ota.ts
Normal 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);
|
||||
}
|
||||
553
src/components/SystemSetting/OTAManagement.tsx
Normal file
553
src/components/SystemSetting/OTAManagement.tsx
Normal 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>
|
||||
);
|
||||
}
|
||||
@@ -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',
|
||||
|
||||
@@ -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: '角色管理',
|
||||
|
||||
@@ -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) },
|
||||
]
|
||||
},
|
||||
{
|
||||
|
||||
BIN
~$26年8月工作月度报告-葛昀.docx
Normal file
BIN
~$26年8月工作月度报告-葛昀.docx
Normal file
Binary file not shown.
BIN
~$运维系统平台-月度工作总结报告-2026年8月-葌昀.docx
Normal file
BIN
~$运维系统平台-月度工作总结报告-2026年8月-葌昀.docx
Normal file
Binary file not shown.
BIN
智慧运维系统平台-月度工作总结报告-2026年8月-葌昀.docx
Normal file
BIN
智慧运维系统平台-月度工作总结报告-2026年8月-葌昀.docx
Normal file
Binary file not shown.
Reference in New Issue
Block a user