任务监控界面接入任务进度还有workname

This commit is contained in:
mmc
2026-11-06 11:03:18 +08:00
parent d43d44c502
commit 25bdaa521f
5 changed files with 329 additions and 19 deletions

BIN
.tmp/采集表_new.doc Normal file

Binary file not shown.

View File

@@ -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`。

240
scripts/fill_collect.py Normal file
View File

@@ -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))

View File

@@ -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);

View File

@@ -79,6 +79,8 @@ const TaskMonitorPage: React.FC = () => {
const [realtimeMap, setRealtimeMap] = useState<any>({});
// 当前选中的车辆:列表高亮与地图信息浮窗共用此状态
const [selectedDeviceSerial, setSelectedDeviceSerial] = useState<string | null>(null);
// 实时画面视角:与 DeviceStatusPage 保持一致(front/back/left/right)
const [videoView, setVideoView] = useState<'front' | 'back' | 'left' | 'right'>('front');
const [focusLocation, setFocusLocation] = useState<any>(null);
const [messageApi, contextHolder] = message.useMessage();
const [taskPoolList, setTaskPoolList] = useState([]);
@@ -87,10 +89,40 @@ const TaskMonitorPage: React.FC = () => {
const controllerRef = useRef<DeviceController | null>(null);
const mapRef = useRef<any>(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<string, number> = {};
@@ -388,7 +431,7 @@ const TaskMonitorPage: React.FC = () => {
{[
{ label: '机器总数', value: deviceStats.total, online: deviceStats.online, offline: deviceStats.total - deviceStats.online, icon: <Box color="#1890ff" />, bg: '#e6f7ff' },
{ label: '在线总数', value: deviceStats.online, subLeft: { label: '空闲', value: deviceStats.idle, color: '#1890ff' }, subRight: { label: '工作中', value: deviceStats.working, color: '#52c41a' }, icon: <Activity color="#52c41a" />, bg: '#f6ffed' },
{ label: '告警车辆', value: deviceStats.alarm, online: deviceStats.alarm, offline: 0, icon: <AlertCircle color="#ff4d4f" />, bg: '#fff1f0' }
{ label: '告警车辆', value: deviceStats.alarm, online: deviceStats.online, offline: deviceStats.alarmOffline, icon: <AlertCircle color="#ff4d4f" />, bg: '#fff1f0' }
].map((item, index) => (
<div key={index} style={{ display: 'flex', alignItems: 'center', gap: 16 }}>
<div style={{ width: 48, height: 48, borderRadius: 12, background: item.bg, display: 'flex', alignItems: 'center', justifyContent: 'center' }}>
@@ -442,14 +485,16 @@ const TaskMonitorPage: React.FC = () => {
<Button type="text" size="small" aria-label="关闭车辆详情" icon={<X size={18} />} onClick={() => setSelectedDeviceSerial(null)} />
</div>
<div className="vehicle-map-popup__body">
<div className="vehicle-map-popup__row"><span>当前状态</span><strong className={selectedRealtime.status === 'EXECUTING' ? 'is-running' : ''}>{selectedRealtime.status === 'EXECUTING' ? '执行中' : '待命'}</strong></div>
<div className="vehicle-map-popup__row"><span>剩余电量</span><strong className={(selectedRealtime.battery ?? 85) < 20 ? 'is-low-battery' : ''}>{selectedRealtime.battery ?? 85}%</strong></div>
<div className="vehicle-map-popup__row"><span>当前任务</span><strong>{selectedRealtime.currentTaskName || '常规巡检'}</strong></div>
<div className="vehicle-map-popup__row"><span>设备类型</span><strong>{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}</strong></div>
<div className="vehicle-map-popup__row"><span>任务进度</span><strong>{selectedRealtime.taskProgress ?? 0}%</strong></div>
<div className="vehicle-map-popup__row"><span>当前状态</span><strong className={selectedRealtime.status === '1' ? 'is-running' : ''}>{selectedRealtime.status === '1' ? '进行中' : selectedRealtime.status === '2' ? '待命' : '离线'}</strong></div>
<div className="vehicle-map-popup__row"><span>剩余电量</span><strong className={(selectedBattery ?? 85) < 20 ? 'is-low-battery' : ''}>{selectedBattery ?? 85}%</strong></div>
<div className="vehicle-map-popup__row"><span>当前任务</span><strong>{selectedRealtime.workName || "无"}</strong></div>
{/*<div className="vehicle-map-popup__row"><span>设备类型</span><strong>{selectedDevice.deviceTypeName || selectedDevice.deviceType || '车辆'}</strong></div>*/}
<div className="vehicle-map-popup__row"><span>任务进度</span><strong>{selectedRealtime.progress ?? 0}%</strong></div>
</div>
</div>
)}
{/*{rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'}*/}
{/* 地图上的图例 */}
<div style={{ position: 'absolute', bottom: 20, left: 20, display: 'flex', gap: 12, zIndex: 10 }}>
@@ -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 (
<List.Item
@@ -507,9 +553,13 @@ const TaskMonitorPage: React.FC = () => {
<div style={{ flex: 1 }}>
<div style={{ display: 'flex', justifyContent: 'space-between', alignItems: 'center', marginBottom: 4 }}>
<span style={{ fontWeight: 600, fontSize: 14, color: '#262626' }}>{item.deviceAlias || item.deviceName}</span>
<Tag color={rt.status === 'EXECUTING' ? 'green' : 'default'} style={{ margin: 0, borderRadius: 4 }}>
{rt.status === 'EXECUTING' ? '进行中' : '待命'}
<Tag
color={rt.status === '1' ? 'blue' : rt.status === '2' ? 'green' : 'default'}
style={{ margin: 0, borderRadius: 4 }}
>
{rt.status == '1' ? '进行中' : rt.status == '2' ? '待命' : '离线'}
</Tag>
</div>
<div
style={{
@@ -521,10 +571,11 @@ const TaskMonitorPage: React.FC = () => {
color: '#8c8c8c',
minWidth: 0,
}}
> <span>任务: {rt.currentTaskName || '常规巡检'}</span>
>
<span>任务: {rt.workName || '无'}</span>
<span
style={{
color: rt.battery < 20 ? '#ff4d4f' : '#faad14',
color: (battery ?? 85) < 20 ? '#ff4d4f' : '#faad14',
display: 'inline-flex',
alignItems: 'center',
gap: 2,
@@ -534,10 +585,11 @@ const TaskMonitorPage: React.FC = () => {
}}
>
<Zap size={10} style={{ flexShrink: 0 }} />
{rt.battery ?? 85}%
{battery ?? 85}%
</span>
</div>
<Progress percent={rt.taskProgress || Math.floor(Math.random() * 100)} size="small" showInfo={false} strokeColor="#52c41a" style={{ marginTop: 6 }} />
<Progress percent={rt.progress || 0} size="small" showInfo={false} strokeColor="#52c41a" style={{ marginTop: 6 }} />
</div>
</div>
</List.Item>
@@ -557,14 +609,19 @@ const TaskMonitorPage: React.FC = () => {
}}>
<VideoPlayer
deviceId={devices.find(d => 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"
/>
</div>
<div style={{ display: 'grid', gridTemplateColumns: 'repeat(4, 1fr)', gap: 8 }}>
{['前视', '后视', '左视', '右视'].map((v, i) => (
<Button key={i} size="small" type={i === 0 ? 'primary' : 'default'} style={{ fontSize: 11, height: 28 }}>{v}</Button>
{[
{ key: 'front', label: '前视' },
{ key: 'back', label: '后视' },
{ key: 'left', label: '左视' },
{ key: 'right', label: '右视' },
].map((v) => (
<Button key={v.key} size="small" type={videoView === v.key ? 'primary' : 'default'} style={{ fontSize: 11, height: 28 }} onClick={() => setVideoView(v.key as 'front' | 'back' | 'left' | 'right')}>{v.label}</Button>
))}
</div>
</MonitorCard>