From 25bdaa521ff3337445a9794c1db2f41a5c159c74 Mon Sep 17 00:00:00 2001 From: mmc <1556375442@qq.com> Date: Fri, 6 Nov 2026 11:03:18 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BB=BB=E5=8A=A1=E7=9B=91=E6=8E=A7=E7=95=8C?= =?UTF-8?q?=E9=9D=A2=E6=8E=A5=E5=85=A5=E4=BB=BB=E5=8A=A1=E8=BF=9B=E5=BA=A6?= =?UTF-8?q?=E8=BF=98=E6=9C=89workname?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .tmp/采集表_new.doc | Bin 0 -> 17887 bytes .workbuddy/memory/2026-11-06.md | 10 + scripts/fill_collect.py | 240 +++++++++++++++++++++ src/components/devices/deviceController.js | 3 + src/pages/TaskMonitorPage.tsx | 95 ++++++-- 5 files changed, 329 insertions(+), 19 deletions(-) create mode 100644 .tmp/采集表_new.doc create mode 100644 .workbuddy/memory/2026-11-06.md create mode 100644 scripts/fill_collect.py diff --git a/.tmp/采集表_new.doc b/.tmp/采集表_new.doc new file mode 100644 index 0000000000000000000000000000000000000000..dc21d8d6e9ff83e7d50a43af300c6514c9b87a0a GIT binary patch literal 17887 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 Z&zX~z0R8KkMf{B6f%`P(cD~<${|Aam$*ceX literal 0 HcmV?d00001 diff --git a/.workbuddy/memory/2026-11-06.md b/.workbuddy/memory/2026-11-06.md new file mode 100644 index 0000000..deadae7 --- /dev/null +++ b/.workbuddy/memory/2026-11-06.md @@ -0,0 +1,10 @@ +# 2026-11-06 + +## 软著《采集表》填写(机器人智慧服务系统) + +- 文件:`C:/Users/15563/Desktop/软著/软著-机器人智慧服务系统/采集表.doc`(实为 docx 内容、扩展名 .doc),两列表格共 20 行项目。 +- 任务:依据《用户操作手册-机器人智慧服务系统-完整版.docx》重写「软件的主要功能」「软件的主要特点」两栏。 +- 结果:功能栏 2252 字(8 条:登录与权限/装备总览/机器人任务/机器人控制/地图编辑/告警中心/实时监控与数据统计/工单联动);特点栏 818 字(8 条:一张图指挥/四通道实时通信/任务全流程闭环/双模遥控/多重安全/告警工单闭环/权限与数据隔离/跨平台免安装)。 +- 原表「软件的主要功能」是广口径描述(含清洗优化、报表中心等手册未展开的模块),本次改为严格对齐手册章节。 +- 脚本:`scripts/fill_collect.py`(ET 解析 document.xml,按「表格行 → 第2个 tc」定位,沿用单元格首个带 rPr 的 run 作字体模板,小标题加粗;可重复执行)。 +- 注意:源文件被 Word 占用时写入 Permission denied,会回退输出到 `.tmp/采集表_new.doc` 并复制为同目录 `采集表-已完善.doc`。 diff --git a/scripts/fill_collect.py b/scripts/fill_collect.py new file mode 100644 index 0000000..1871c2e --- /dev/null +++ b/scripts/fill_collect.py @@ -0,0 +1,240 @@ +# -*- coding: utf-8 -*- +"""填充《采集表》中「软件的主要功能」「软件的主要特点」两栏。 +源文件:C:/Users/15563/Desktop/软著/软著-机器人智慧服务系统/采集表.doc +内容依据:《用户操作手册-机器人智慧服务系统-完整版.docx》 +""" +import zipfile, os, shutil, copy, re, time +from xml.etree import ElementTree as ET + +W = 'http://schemas.openxmlformats.org/wordprocessingml/2006/main' +def w(t): return '{%s}%s' % (W, t) +ET.register_namespace('w', W) +for pfx, uri in ( + ('w14', 'http://schemas.microsoft.com/office/word/2010/wordml'), + ('mc', 'http://schemas.openxmlformats.org/markup-compatibility/2006'), + ('v', 'urn:schemas-microsoft-com:vml'), + ('wp', 'http://schemas.openxmlformats.org/drawingml/2006/wordprocessingDrawing'), + ('r', 'http://schemas.openxmlformats.org/officeDocument/2006/relationships'), + ('m', 'http://schemas.openxmlformats.org/officeDocument/2006/math'), + ('wps', 'http://schemas.microsoft.com/office/word/2010/wordprocessingShape'), + ('wpg', 'http://schemas.microsoft.com/office/word/2010/wordprocessingGroup'), + ('wp14', 'http://schemas.microsoft.com/office/word/2010/wordprocessingDrawing'), + ('w10', 'urn:schemas-microsoft-com:office:word'), + ('w15', 'http://schemas.microsoft.com/office/word/2012/wordml'), +): + try: ET.register_namespace(pfx, uri) + except Exception: pass + +SRC = r'C:/Users/15563/Desktop/软著/软著-机器人智慧服务系统/采集表.doc' +TMP = r'D:/code/机器人智慧服务系统/.tmp/采集表_new.doc' +os.makedirs(os.path.dirname(TMP), exist_ok=True) + +# ---------------- 内容 ---------------- +FUNC_INTRO = ("本系统是面向光伏电站场景的机器人智能运维平台,采用前后端分离的 B/S 架构," + "以三维电子地图为统一空间底座,把巡检机器人、除草机器人、无人机机场、清洗无人机等" + "智能装备的接入、调度、控制、作业与告警处置集中到统一的 Web 控制台," + "实现“一张图指挥、一套流程闭环”。主要功能包括:") + +FUNC = [ + ("(1)系统登录与权限管理:", + "支持账号密码登录与用户协议确认,未登录访问任意业务页面自动重定向至登录页," + "登录态超时后提示并跳回登录页,重新登录可回到原页面。系统采用 RBAC 三级角色权限" + "(管理员、操作员、观察者),权限差异同时体现在菜单是否可见与按钮是否可操作两个层面;" + "并支持按组织与场站限定数据可见范围,跨场站数据不在列表中呈现。顶部栏提供场站切换、" + "全屏控制、中英文切换、用户头像与退出登录入口。"), + ("(2)装备总览:", + "以五张状态统计卡(无人机机场状态、巡检机器人、除草机器人、清洗无人机、今日任务执行)" + "按场站汇总装备数量、在线台数与任务完成率;提供基于 Cesium 三维地图与场站正射影像的" + "装备协同态势图,叠加设备位置、作业路径、分区、障碍物、返航点等图层,支持图层开关与" + "“定位到机器人”一键聚焦;装备调度列表展示装备名称、类型、当前状态、电量、位置与通讯状态," + "支持刷新、编辑设备 SN(可一键复制)、设备类型与设备别名,并可打开设备参数设置弹窗;" + "设备控制面板展示在线状态、电量、产品类型、SN、激活时间、固件版本,提供控制、回充、返航、" + "生成任务等快捷操作,无人机展示风速/温度/降雨等飞行条件,地面机器人展示累计作业里程、" + "面积与时长;另提供 AI 分析与操作建议、控制日志与告警记录、任务执行时间线,便于回溯与研判。"), + ("(3)机器人任务:", + "顶部六张任务统计卡展示当日作业指标并给出较昨日的变化趋势;机器人路径规划与监控地图" + "实时显示机器人位置、规划路线与历史轨迹,支持控制模式(本地/自动任务/远程)显示与切换、" + "定位图层与视频开关、寻车与返航快捷按钮,以及清空实时轨迹、清空规划区域等视图清理操作;" + "任务下发支持按路线下发(单选,点击路线地图自动飞至该路线)与按区域下发(可多选," + "多条路径可拖拽调节执行顺序)两种方式,任务类型分为立即执行与定时任务,定时任务可按天、" + "按周、按月重复并设置执行日期、执行时间与周期范围,提交前自动校验路径、执行机器人与" + "定时时间;任务列表分任务池、任务执行计划、历史任务三个页签,支持按状态、机器人与日期" + "范围筛选,行内提供暂停、恢复、取消(普通取消/强制取消)、删除与继续作业操作," + "任务状态覆盖待执行、执行中、已暂停、已完成、执行失败、已取消六种。"), + ("(4)机器人控制:", + "设备状态面板以列表形式实时刷新电机温度、电流、电压、电量、速度、航向与姿态等十四项" + "运行参数,每项标注单位与含义;实时视频监控通过 WebRTC 拉取机载摄像头画面,支持前视角、" + "左视角、右视角、后视角与全景五个视角切换、播放与停止控制,后视角自动叠加倒车辅助线;" + "设备态势地图实时显示航向角、横滚角与俯仰角,支持清空轨迹与定位到机器人;手柄控制支持" + "游戏手柄按键映射、控制器连接状态显示以及请求控制、释放控制与权限状态标签;方向盘控制" + "提供前进、后退、左转、右转与中键停止的屏幕虚拟方向盘,行驶速度可在 0–100% 之间调节," + "控制指令以 100 毫秒为周期下发,并集成点火/熄火、底盘升起/下降、割刀加速/减速等执行机构" + "按钮;功能控制区提供五项辅助开关与红色急停按钮,急停触发后立即下发停车指令并自动补发" + "复位帧。"), + ("(5)地图编辑:", + "提供返航点、分区、障碍物点、路径生成四种绘制模式,以及点选工具、机器人选择、撤销与清空" + "操作,地图区实时显示操作提示与已选点位数量;返航点支持在地图上标注多点、核对坐标后保存," + "并可在已配置列表中编辑与删除;分区需不少于三个点位闭合边界,保存时可填写分区名称、" + "分区编码、默认线路与绑定线路(可多选),用于按区域编排任务;障碍物点用于标记支架基础、" + "水塘、电杆等固定障碍,标记后路径规划与自动作业自动绕开;路径生成可配置路径名称、" + "规划类型与作业模式,保存后进入预设路线列表,供任务下发页面直接引用。"), + ("(6)告警中心:", + "顶部四张统计卡展示今日总告警、今日严重告警、今日待处理与今日已关闭数量并附昨日对比与" + "升降趋势,下方以折线图、饼图与柱状图分别展示告警趋势、告警类型比例与告警状态统计;" + "筛选栏支持按告警级别、处理状态、设备 ID 与时间范围组合查询,并提供重置、查询与结果导出;" + "告警列表展示告警编号、标题、级别、类型、来源设备、发生时间、处理状态等字段;" + "点击查看可从右侧展开告警详情抽屉,展示告警完整描述并提供联系处理人与一键生成工单入口," + "填写处理状态、处理结果、根原因与处理备注后确认处理,告警由待处理流转为已关闭;" + "告警分提示、一般、重要、严重四级,支持修改告警标题、级别与类型并保留操作记录。"), + ("(7)实时监控与数据统计:", + "基于 Cesium 三维地图与 MQTT 遥测通道,对设备位置、状态与运动轨迹进行实时渲染与回放," + "支持电子围栏与场站可视化监控;历史任务、控制日志与告警记录长期留存并支持多维查询与" + "趋势分析,运营数据可导出为报表,为作业计划编排、绩效考核与管理决策提供数据支撑。"), + ("(8)工单联动处置:", + "对需要现场处置的告警可一键转为运维工单,系统自动带出告警编号、设备与告警描述," + "补充处理人与期望完成时间后提交;工单按待处理、处理中、待验收、已完成状态流转," + "验收关闭后对应告警同步标记为已关闭,形成“发现—派单—处置—验收”的完整闭环。"), +] + +FEAT_INTRO = "本系统在架构设计与人机交互上具备以下特点:" +FEAT = [ + ("(1)一张图指挥:", + "以 Cesium 三维地图与场站正射影像为统一空间底座,将所有可动作装备、航线、分区、障碍物与" + "实时位置叠加在同一张态势图上,图层可独立开关、目标可一键定位,运维人员无需在多系统间" + "切换即可掌握全场站态势。"), + ("(2)四通道实时通信:", + "HTTP/REST 承载业务接口,MQTT over WebSocket 上行设备遥测,WebSocket 下行控制指令," + "WebRTC 回传机载视频,四条通道职责清晰、互不阻塞,兼顾业务可靠性与遥控低延迟。"), + ("(3)任务全流程闭环:", + "从地图要素配置、路线与区域编排、立即或定时下发,到执行监控、异常处置与统计回溯," + "作业全链路可追溯;失败或取消的任务可基于原编排“继续作业”,无需重新规划路径。"), + ("(4)双模遥控与低延迟指令:", + "同时支持游戏手柄与屏幕虚拟方向盘两种操控方式,控制指令以 100 毫秒为周期打包下发," + "结合速度分级调节,既满足精细作业的稳定操控,也支持突发情况下的快速接管。"), + ("(5)多重安全保护机制:", + "提供急停(自动补发复位帧)、控制权独占、管理员强制抢占、会话保活、避障开关、倾斜保护、" + "低电量自动返航等多重保护,控制权申请与释放过程全程可见,避免多操作员指令冲突与设备" + "被长期占用在远程模式。"), + ("(6)告警与工单联动闭环:", + "告警处理结果与工单状态双向同步,告警可一键转工单,工单验收后告警自动关闭," + "使异常处置从“被动发现”转为“流程闭环、责任到人”。"), + ("(7)精细化权限与数据隔离:", + "三级 RBAC 角色权限同时约束菜单可见性与按钮可用性,并按组织与场站隔离数据可见范围," + "满足多场站、多班组协同运维的管理要求。"), + ("(8)跨平台免安装部署:", + "前后端分离的 B/S 架构,前端基于 React 19 + TypeScript + Vite 6 构建单页应用," + "服务端可在 Windows/Linux 环境部署,客户端仅需现代浏览器即可访问,无需安装任何插件;" + "界面支持中英文双语切换。"), +] + +# ---------------- 写入 ---------------- +zin = zipfile.ZipFile(SRC) +doc = ET.fromstring(zin.read('word/document.xml')) + + +def cell_text(tc): + return ''.join(t.text or '' for t in tc.iter(w('t'))) + + +def fill_cell(tc, intro, items): + """用 intro + 分条内容重写单元格,沿用原单元格字体与段落格式。""" + paras = tc.findall(w('p')) + tpl_p = paras[0] + pPr = tpl_p.find(w('pPr')) + # 取第一个带文字 run 的 rPr 作为字体模板 + rPr = None + for r in tpl_p.findall(w('r')): + rp = r.find(w('rPr')) + if rp is not None: + rPr = rp + break + if rPr is None: + for r in tpl_p.iter(w('r')): + rp = r.find(w('rPr')) + if rp is not None: + rPr = rp + break + # 清空原有段落 + for p in paras: + tc.remove(p) + + def add(text, bold=False): + p = ET.SubElement(tc, w('p')) + if pPr is not None: + newpPr = copy.deepcopy(pPr) + # 去重:段落级段落标记 rPr 保留 + p.append(newpPr) + r = ET.SubElement(p, w('r')) + if rPr is not None: + newrPr = copy.deepcopy(rPr) + if bold: + b = ET.SubElement(newrPr, w('b')) + r.append(newrPr) + t = ET.SubElement(r, w('t')) + t.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') + t.text = text + return p + + add(intro) + for head, body in items: + # 每条一段:小标题加粗 + 正文 + p = ET.SubElement(tc, w('p')) + if pPr is not None: + p.append(copy.deepcopy(pPr)) + r1 = ET.SubElement(p, w('r')) + if rPr is not None: + rp = copy.deepcopy(rPr) + ET.SubElement(rp, w('b')) + r1.append(rp) + t1 = ET.SubElement(r1, w('t')) + t1.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') + t1.text = head + r2 = ET.SubElement(p, w('r')) + if rPr is not None: + r2.append(copy.deepcopy(rPr)) + t2 = ET.SubElement(r2, w('t')) + t2.set('{http://www.w3.org/XML/1998/namespace}space', 'preserve') + t2.text = body + + +done = [] +for tbl in doc.iter(w('tbl')): + for tr in tbl.findall(w('tr')): + tcs = tr.findall(w('tc')) + if len(tcs) < 2: + continue + label = cell_text(tcs[0]).strip().replace(' ', '') + if label == '软件的主要功能': + fill_cell(tcs[1], FUNC_INTRO, FUNC) + done.append('功能') + elif label == '软件的主要特点': + fill_cell(tcs[1], FEAT_INTRO, FEAT) + done.append('特点') + +print('已填充:', done) +assert len(done) == 2, '未找到目标单元格' + +new_doc = ET.tostring(doc, encoding='UTF-8', xml_declaration=True) + +zout = zipfile.ZipFile(TMP, 'w', zipfile.ZIP_DEFLATED) +for item in zin.namelist(): + if item == 'word/document.xml': + continue + zout.writestr(item, zin.read(item)) +zout.writestr('word/document.xml', new_doc) +zout.close() +zin.close() +print('生成:', TMP, os.path.getsize(TMP)) + +# 尝试覆盖源文件 +ok = False +for i in range(3): + try: + shutil.copyfile(TMP, SRC) + ok = True + break + except Exception as e: + print('写入失败', i, e) + time.sleep(1.5) +print('覆盖源文件:', '成功' if ok else '源文件被占用,已保留新文件') +print('OUT=' + (SRC if ok else TMP)) diff --git a/src/components/devices/deviceController.js b/src/components/devices/deviceController.js index aa889f9..6d74e34 100644 --- a/src/components/devices/deviceController.js +++ b/src/components/devices/deviceController.js @@ -144,6 +144,9 @@ export default class DeviceController { hasAlarm: !!it?.ha || !!it?.hasAlarm, updateTime: dayjs().format('YYYY-MM-DD HH:mm:ss'), controlMode: it?.cm ?? it?.controlMode, + progress: it?.p ?? it?.progress, + workName: it?.wk ?? null, + }; }); console.log('[DeviceController] 收到全量设备状态', next); diff --git a/src/pages/TaskMonitorPage.tsx b/src/pages/TaskMonitorPage.tsx index 0e4d8e1..45a6db2 100644 --- a/src/pages/TaskMonitorPage.tsx +++ b/src/pages/TaskMonitorPage.tsx @@ -79,6 +79,8 @@ const TaskMonitorPage: React.FC = () => { const [realtimeMap, setRealtimeMap] = useState({}); // 当前选中的车辆:列表高亮与地图信息浮窗共用此状态 const [selectedDeviceSerial, setSelectedDeviceSerial] = useState(null); + // 实时画面视角:与 DeviceStatusPage 保持一致(front/back/left/right) + const [videoView, setVideoView] = useState<'front' | 'back' | 'left' | 'right'>('front'); const [focusLocation, setFocusLocation] = useState(null); const [messageApi, contextHolder] = message.useMessage(); const [taskPoolList, setTaskPoolList] = useState([]); @@ -87,10 +89,40 @@ const TaskMonitorPage: React.FC = () => { const controllerRef = useRef(null); const mapRef = useRef(null); + // 从全量实时数据(mapPush)里按设备标识匹配记录,realtimeMap 结构为 { [设备ID]: 记录 } + const pickRealtimeRecord = (rtMap: any, device: any) => { + for (const key of [device?.serialNumber, device?.device_sn, device?.sn, device?.id, device?.deviceId]) { + if (key == null || String(key) === '') continue; + const hit = rtMap?.[String(key)]; + if (hit) return hit; + } + return null; + }; + + // 电量优先取实时数据,实时没有时回退到设备结构里的 lastRunningStatus / locationMessage + const getDeviceBattery = (device: any): number | undefined => { + const rt = pickRealtimeRecord(realtimeMap, device); + const candidates = [rt?.battery, device?.lastRunningStatus?.batteryLevel, device?.lastRunningStatus?.battery, device?.locationMessage?.batteryLevel, device?.locationMessage?.battery]; + for (const value of candidates) { + if (value === undefined || value === null || value === '') continue; + const num = Number(value); + if (Number.isFinite(num)) return num; + } + return undefined; + }; + + // 供 WebSocket 回调始终调用最新的 fetchData + const fetchDataRef = useRef<() => any>(() => { }); + // 初始化实时数据控制器 useEffect(() => { if (!userInfo || !stationId) return; const controller = new DeviceController(userInfo, messageApi, stationId); + // 与 RobotTaskPage 保持一致:任务状态发生变化后重新拉取接口数据 + const refreshData = () => fetchDataRef.current?.(); + controller.onIsFinished = refreshData; + controller.onIsExecuting = refreshData; + controller.deviceStatusChangedMsg = refreshData; controller.initMapPush((rtMap: any) => setRealtimeMap(rtMap || {})); controllerRef.current = controller; return () => { @@ -136,6 +168,8 @@ const TaskMonitorPage: React.FC = () => { } }; + fetchDataRef.current = fetchData; + useEffect(() => { @@ -203,13 +237,21 @@ const TaskMonitorPage: React.FC = () => { const deviceStats = useMemo(() => { const total = devices.length; - const online = devices.filter(d => d.onlineStatus === 1).length; + // 在线数以实时数据为准:当前设备列表中存在实时记录的即在线 + const online = devices.filter(d => !!pickRealtimeRecord(realtimeMap, d)).length; // feStatus: 1=作业中, 2=空闲 const working = devices.filter(d => String(d.feStatus) === '1').length; const idle = devices.filter(d => String(d.feStatus) === '2').length; - const alarm = alarmList.length; - return { total, online, working, idle, alarm }; - }, [devices, realtimeMap, alarmList]); + console.log(realtimeMap, "realtimeMap") + // 告警车辆数量以实时数据里的 hasAlarm 为准 + const alarmDevices = devices.filter(d => !!pickRealtimeRecord(realtimeMap, d)?.hasAlarm); + console.log(alarmDevices, "alarmDevices") + const alarm = alarmDevices.length; + // 与 DeviceStatusPage 一致:有实时数据即在线,兜底接口 onlineStatus + const alarmOnline = alarmDevices.filter(d => pickRealtimeRecord(realtimeMap, d) || d?.onlineStatus == 1).length; + const alarmOffline = total - online; + return { total, online, working, idle, alarm, alarmOnline, alarmOffline }; + }, [devices, realtimeMap]); @@ -218,6 +260,7 @@ const TaskMonitorPage: React.FC = () => { [devices, selectedDeviceSerial] ); const selectedRealtime: any = selectedDevice ? (realtimeMap[selectedDevice.serialNumber] || {}) : {}; + const selectedBattery = getDeviceBattery(selectedDevice); const trendData = useMemo(() => { const countMap: Record = {}; @@ -388,7 +431,7 @@ const TaskMonitorPage: React.FC = () => { {[ { label: '机器总数', value: deviceStats.total, online: deviceStats.online, offline: deviceStats.total - deviceStats.online, icon: , bg: '#e6f7ff' }, { label: '在线总数', value: deviceStats.online, subLeft: { label: '空闲', value: deviceStats.idle, color: '#1890ff' }, subRight: { label: '工作中', value: deviceStats.working, color: '#52c41a' }, icon: , bg: '#f6ffed' }, - { label: '告警车辆', value: deviceStats.alarm, online: deviceStats.alarm, offline: 0, icon: , bg: '#fff1f0' } + { label: '告警车辆', value: deviceStats.alarm, online: deviceStats.online, offline: deviceStats.alarmOffline, icon: , bg: '#fff1f0' } ].map((item, index) => (
@@ -442,14 +485,16 @@ const TaskMonitorPage: React.FC = () => {
-
当前状态{selectedRealtime.status === 'EXECUTING' ? '执行中' : '待命'}
-
剩余电量{selectedRealtime.battery ?? 85}%
-
当前任务{selectedRealtime.currentTaskName || '常规巡检'}
-
设备类型{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}
-
任务进度{selectedRealtime.taskProgress ?? 0}%
+
当前状态{selectedRealtime.status === '1' ? '进行中' : selectedRealtime.status === '2' ? '待命' : '离线'}
+
剩余电量{selectedBattery ?? 85}%
+
当前任务{selectedRealtime.workName || "无"}
+ {/*
设备类型{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}
*/} +
任务进度{selectedRealtime.progress ?? 0}%
)} + {/*{rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'}*/} + {/* 地图上的图例 */}
@@ -477,6 +522,7 @@ const TaskMonitorPage: React.FC = () => { dataSource={devices.filter(d => d.deviceType !== 'CAMERA')} renderItem={(item: any) => { const rt = realtimeMap[item.serialNumber] || {}; + const battery = getDeviceBattery(item); return ( {
{item.deviceAlias || item.deviceName} - - {rt.status === 'EXECUTING' ? '进行中' : '待命'} + + {rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'} +
{ color: '#8c8c8c', minWidth: 0, }} - > 任务: {rt.currentTaskName || '常规巡检'} + > + 任务: {rt.workName || '无'} { }} > - {rt.battery ?? 85}% + {battery ?? 85}%
- + +
@@ -557,14 +609,19 @@ const TaskMonitorPage: React.FC = () => { }}> d.deviceType === 'ROBOT' || d.deviceType === 'CLEANING')?.serialNumber} - idx={1} + idx={videoView === 'back' ? 2 : videoView === 'left' ? 3 : videoView === 'right' ? 4 : 1} className="w-full h-full object-cover" />
- {['前视', '后视', '左视', '右视'].map((v, i) => ( - + {[ + { key: 'front', label: '前视' }, + { key: 'back', label: '后视' }, + { key: 'left', label: '左视' }, + { key: 'right', label: '右视' }, + ].map((v) => ( + ))}