Files
web-Iot/docs/系统架构设计文档.html
2026-08-14 09:02:52 +08:00

985 lines
35 KiB
HTML
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>光伏机器人环境感知服务平台 · 架构设计文档</title>
<style>
:root {
--bg: #ffffff;
--surface: #f7f9fc;
--surface2: #eef3f9;
--text: #1a2333;
--text2: #5a6577;
--text3: #8a93a3;
--border: #e3e8ef;
--border2: #cdd5e0;
--blue: #185FA5;
--blue-d: #0C447C;
--blue-l: #E6F1FB;
--teal: #0F6E56;
--teal-d: #085041;
--teal-l: #E1F5EE;
--coral: #D85A30;
--coral-d: #993C1D;
--amber: #BA7517;
--radius: 12px;
--radius-s: 8px;
--sans: -apple-system, BlinkMacSystemFont, "Segoe UI", "PingFang SC", "Microsoft YaHei", sans-serif;
--mono: "SFMono-Regular", Consolas, "Liberation Mono", monospace;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0
}
body {
font-family: var(--sans);
background: var(--bg);
color: var(--text);
line-height: 1.7;
font-size: 15px
}
.wrap {
max-width: 920px;
margin: 0 auto;
padding: 40px 28px 80px
}
header {
border-bottom: 2px solid var(--blue);
padding-bottom: 24px;
margin-bottom: 8px
}
header h1 {
font-size: 26px;
font-weight: 600;
color: var(--blue-d);
margin-bottom: 6px
}
header .sub {
font-size: 15px;
color: var(--text2)
}
header .meta {
font-size: 13px;
color: var(--text3);
margin-top: 10px
}
header .meta span {
display: inline-block;
margin-right: 18px
}
.tag {
display: inline-block;
font-size: 12px;
padding: 2px 10px;
border-radius: 20px;
background: var(--blue-l);
color: var(--blue-d);
font-weight: 500;
margin-right: 6px
}
.tag.teal {
background: var(--teal-l);
color: var(--teal-d)
}
h2 {
font-size: 20px;
font-weight: 600;
color: var(--blue-d);
margin: 40px 0 14px;
padding-bottom: 8px;
border-bottom: 1px solid var(--border);
display: flex;
align-items: center;
gap: 10px
}
h2 .num {
display: inline-flex;
align-items: center;
justify-content: center;
width: 26px;
height: 26px;
border-radius: 50%;
background: var(--blue);
color: #fff;
font-size: 13px;
font-weight: 600
}
h3 {
font-size: 16px;
font-weight: 600;
color: var(--text);
margin: 24px 0 10px
}
p {
margin-bottom: 12px;
color: var(--text)
}
p.lead {
font-size: 16px;
color: var(--text2)
}
ul,
ol {
margin: 0 0 14px 20px
}
li {
margin-bottom: 6px
}
code {
font-family: var(--mono);
font-size: 13px;
background: var(--surface2);
padding: 1px 6px;
border-radius: 4px;
color: var(--blue-d)
}
pre {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-s);
padding: 14px 16px;
overflow-x: auto;
margin-bottom: 16px
}
pre code {
background: none;
padding: 0;
font-size: 13px;
color: var(--text)
}
table {
width: 100%;
border-collapse: collapse;
margin-bottom: 16px;
font-size: 14px
}
th,
td {
padding: 10px 12px;
text-align: left;
border-bottom: 1px solid var(--border);
vertical-align: top
}
th {
background: var(--surface);
color: var(--blue-d);
font-weight: 600;
font-size: 13px
}
tr:hover td {
background: var(--surface)
}
.card {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px 20px;
margin-bottom: 16px
}
.card.blue {
border-left: 3px solid var(--blue)
}
.card.teal {
border-left: 3px solid var(--teal)
}
.card.coral {
border-left: 3px solid var(--coral)
}
.card h3 {
margin-top: 0
}
.grid {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
gap: 12px;
margin-bottom: 16px
}
.stat {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius-s);
padding: 14px
}
.stat .v {
font-size: 22px;
font-weight: 600;
color: var(--blue-d)
}
.stat .l {
font-size: 12px;
color: var(--text3);
margin-top: 2px
}
.diagram {
background: #fff;
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 20px;
margin: 16px 0
}
.diagram svg {
width: 100%;
height: auto;
display: block
}
.cap {
font-size: 13px;
color: var(--text3);
text-align: center;
margin-top: 8px;
font-style: italic
}
.flow {
display: flex;
gap: 0;
align-items: stretch;
margin: 14px 0;
flex-wrap: wrap
}
.flow .step {
flex: 1;
min-width: 90px;
background: var(--blue-l);
border: 1px solid #B5D4F4;
border-radius: var(--radius-s);
padding: 10px;
text-align: center;
font-size: 13px
}
.flow .step.t {
background: var(--teal-l);
border-color: #9FE1CB
}
.flow .arr {
display: flex;
align-items: center;
color: var(--blue);
font-size: 18px;
padding: 0 4px
}
.note {
font-size: 13px;
color: var(--text2);
background: var(--blue-l);
border-radius: var(--radius-s);
padding: 10px 14px;
margin-bottom: 14px
}
.note.warn {
background: #FAECE7;
color: var(--coral-d)
}
.toc {
background: var(--surface);
border: 1px solid var(--border);
border-radius: var(--radius);
padding: 18px 22px;
margin-bottom: 30px
}
.toc h3 {
margin: 0 0 10px;
color: var(--blue-d)
}
.toc ol {
margin: 0 0 0 22px
}
.toc a {
color: var(--blue);
text-decoration: none
}
.toc a:hover {
text-decoration: underline
}
footer {
margin-top: 50px;
padding-top: 20px;
border-top: 1px solid var(--border);
font-size: 13px;
color: var(--text3);
text-align: center
}
</style>
</head>
<body>
<div class="wrap">
<header>
<div><span class="tag">架构设计文档</span><span class="tag teal">React 19</span><span class="tag teal">Vite
6</span></div>
<h1>光伏机器人环境感知服务平台</h1>
<div class="sub">机器人智慧服务平台 · 前端系统架构与设计说明</div>
<div class="meta">
<span>项目代号:机器人智慧服务系统</span>
<span>文档版本:v1.0</span>
<span>生成日期:2026-08-10</span>
</div>
</header>
<div class="toc">
<h3>目录</h3>
<ol>
<li><a href="#s1">项目概述</a></li>
<li><a href="#s2">技术栈选型</a></li>
<li><a href="#s3">系统分层架构</a></li>
<li><a href="#s4">核心模块详解</a></li>
<li><a href="#s5">实时通信层设计</a></li>
<li><a href="#s6">数据流设计</a></li>
<li><a href="#s7">目录结构</a></li>
<li><a href="#s8">关键设计决策与特点</a></li>
</ol>
</div>
<section id="s1">
<h2><span class="num">1</span>项目概述</h2>
<p>本项目(metadata 名:<code>光伏机器人环境感知服务平台</code>)是一个面向光伏电站场景的<strong>机器人智能管理平台</strong>。它将巡检机器人 /
无人机、视频监控、告警处理与运维工单整合到单一 Web 控制台,提供设备管理、实时监控、视频回传、状态预警、AI 诊断分析、清洗优化、报表中心与视频下载等一站式能力。</p>
<div class="grid">
<div class="stat">
<div class="v">4</div>
<div class="l">通信协议通道</div>
</div>
<div class="stat">
<div class="v">17</div>
<div class="l">业务页面</div>
</div>
<div class="stat">
<div class="v">全懒加载</div>
<div class="l">路由按需加载</div>
</div>
<div class="stat">
<div class="v">RBAC</div>
<div class="l">三级角色权限</div>
</div>
</div>
<p class="lead">系统定位是<strong>前端 SPA</strong>,通过四条独立通信通道对接后端的 API、MQTT、WebSocket 与 GeoServer 服务,实现“低延迟控制 +
高吞吐遥测 + 实时视频”的多协议协同。</p>
</section>
<section id="s2">
<h2><span class="num">2</span>技术栈选型</h2>
<table>
<thead>
<tr>
<th>层级</th>
<th>技术</th>
<th>用途</th>
</tr>
</thead>
<tbody>
<tr>
<td>框架</td>
<td>React 19 + TypeScript</td>
<td>UI 渲染与类型安全</td>
</tr>
<tr>
<td>构建</td>
<td>Vite 6 + @vitejs/plugin-react</td>
<td>极速 HMR 与打包,含 CSS 代码分割</td>
</tr>
<tr>
<td>UI 组件</td>
<td>Ant Design 5 + TailwindCSS 4</td>
<td>企业级组件库 + 原子化样式</td>
</tr>
<tr>
<td>状态管理</td>
<td>Redux Toolkit + react-redux</td>
<td>user / station 全局状态切片</td>
</tr>
<tr>
<td>路由</td>
<td>React Router DOM 7</td>
<td>全路由懒加载 + ProtectedRoute 鉴权</td>
</tr>
<tr>
<td>3D / GIS</td>
<td>Cesium + vite-plugin-cesium</td>
<td>三维地图、设备轨迹与场站可视化</td>
</tr>
<tr>
<td>图表</td>
<td>ECharts 6 / Recharts / @ant-design/charts</td>
<td>告警统计、运行数据可视化</td>
</tr>
<tr>
<td>HTTP</td>
<td>Axios</td>
<td>统一请求实例 + 拦截器</td>
</tr>
<tr>
<td>实时通信</td>
<td>MQTT / WebSocket / WebRTC / Socket.io</td>
<td>遥测 / 控制 / 视频 / 实时推送</td>
</tr>
<tr>
<td>音视频</td>
<td>@volcengine/rtc · agora-rtc-sdk-ng · JSWebrtc</td>
<td>多路无人机/机器人视频回传</td>
</tr>
<tr>
<td>其它</td>
<td>js-cookie · qrcode · lodash · motion · react-draggable</td>
<td>会话/二维码/工具/动画/拖拽</td>
</tr>
</tbody>
</table>
</section>
<section id="s3">
<h2><span class="num">3</span>系统分层架构</h2>
<p>系统自上而下分为四层。应用层内部进一步拆为路由、页面、组件、状态四个子层;通信层以四条并行通道桥接前后端。</p>
<div class="diagram">
<svg viewBox="0 0 680 492" width="100%" role="img" aria-label="系统分层架构图">
<text x="340" y="26" text-anchor="middle" font-family="var(--sans)" font-size="15" font-weight="500"
fill="#0C447C">光伏机器人环境感知服务平台 · 分层架构</text>
<rect x="200" y="52" width="280" height="44" rx="8" fill="#F1EFE8" stroke="#5F5E5A"
stroke-width="0.5" />
<text x="340" y="74" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#444441">用户浏览器 / Web Client</text>
<path d="M340 98 L340 118" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)" />
<defs>
<marker id="ar" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</marker>
</defs>
<rect x="40" y="120" width="600" height="132" rx="12" fill="#E6F1FB" stroke="#185FA5"
stroke-width="0.5" />
<text x="56" y="138" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">React 19 + TypeScript · Vite 6 (SPA 前端应用)</text>
<rect x="60" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="126" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">路由层</text>
<text x="126" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">Router7 · 懒加载 · 鉴权</text>
<rect x="204" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="270" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">页面层</text>
<text x="270" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">Home·设备·视频·告警·AI</text>
<rect x="348" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="414" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">组件层</text>
<text x="414" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">Layout·devices·alerts</text>
<rect x="492" y="156" width="132" height="84" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="558" y="182" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">状态管理</text>
<text x="558" y="206" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">Redux (user·station)</text>
<path d="M340 252 L340 274" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)" />
<rect x="40" y="276" width="600" height="86" rx="10" fill="#E6F1FB" stroke="#185FA5"
stroke-width="0.5" />
<text x="56" y="294" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">实时通信层(四通道并行)</text>
<rect x="60" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="126" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">HTTP · Axios</text>
<text x="126" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">API 请求 · 401 拦截</text>
<rect x="204" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="270" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">MQTT</text>
<text x="270" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">设备遥测 useMqtt</text>
<rect x="348" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="414" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">WebSocket</text>
<text x="414" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">设备控制 wsManager</text>
<rect x="492" y="310" width="132" height="44" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="558" y="324" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">WebRTC</text>
<text x="558" y="342" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">视频流 JSWebrtc</text>
<path d="M340 362 L340 384" fill="none" stroke="#888780" stroke-width="1" marker-end="url(#ar)" />
<rect x="40" y="386" width="600" height="86" rx="10" fill="#E1F5EE" stroke="#0F6E56"
stroke-width="0.5" />
<text x="56" y="404" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">后端服务层(1.95.137.212)</text>
<rect x="60" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="126" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">API server</text>
<text x="126" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">:8081 REST 业务</text>
<rect x="204" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="270" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">MQTT broker</text>
<text x="270" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">:8083 消息</text>
<rect x="348" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="414" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">WS server</text>
<text x="414" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">:9002 设备控制</text>
<rect x="492" y="420" width="132" height="44" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="558" y="434" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">GeoServer</text>
<text x="558" y="452" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">:59018 GIS 地图</text>
</svg>
<div class="cap">图 1 · 系统四层分层架构</div>
</div>
</section>
<section id="s4">
<h2><span class="num">4</span>核心模块详解</h2>
<h3>4.1 路由层 <code>src/router</code></h3>
<p>采用 <code>createBrowserRouter</code> + 全量 <code>lazy()</code> 懒加载,首屏只加载必要代码。路由通过 <code>routeMeta</code>
声明每个路径的 <strong>角色白名单</strong>(admin / operator / viewer)与
<code>requiresAuth</code>,<code>ProtectedRoute</code> 在进入受保护路由前校验登录态与权限,未登录跳转 <code>/login</code>。</p>
<h3>4.2 页面层 <code>src/pages</code></h3>
<table>
<thead>
<tr>
<th>页面</th>
<th>职责</th>
</tr>
</thead>
<tbody>
<tr>
<td>Home / overview</td>
<td>总览首页,聚合设备状态、告警、作业统计概览</td>
</tr>
<tr>
<td>Devices 相关</td>
<td>设备总览 / 状态 / 航线任务 / 机器人任务</td>
</tr>
<tr>
<td>Video / VideoMonitorPage</td>
<td>在线视频与视频监控,多路 WebRTC 拉流</td>
</tr>
<tr>
<td>RealTimeMonitor</td>
<td>实时监控,Cesium 地图 + 设备轨迹</td>
</tr>
<tr>
<td>Alerts</td>
<td>告警中心:实时 / 历史 / 统计 / 规则 / 订阅</td>
</tr>
<tr>
<td>AIAnalysis</td>
<td>AI 诊断分析(热成像分析)</td>
</tr>
<tr>
<td>CleanOptimization</td>
<td>清洗优化</td>
</tr>
<tr>
<td>WorkOrder</td>
<td>工单任务管理</td>
</tr>
<tr>
<td>ReportCenter / Downloads</td>
<td>报表中心 / 视频下载</td>
</tr>
<tr>
<td>SystemSetting(嵌套子路由)</td>
<td>系统设置:基础设置、用户、菜单、组织、场站、设备接入、系统维护等</td>
</tr>
<tr>
<td>Login / Profile / Users / Roles</td>
<td>登录 / 个人中心 / 用户管理 / 角色管理</td>
</tr>
</tbody>
</table>
<h3>4.3 组件层 <code>src/components</code></h3>
<table>
<thead>
<tr>
<th>目录/组件</th>
<th>职责</th>
</tr>
</thead>
<tbody>
<tr>
<td>Layout.tsx</td>
<td>应用外壳:侧边栏菜单(动态构建菜单树)、顶栏、场站切换器、全屏控制</td>
</tr>
<tr>
<td>ProtectedRoute</td>
<td>路由级鉴权守卫</td>
</tr>
<tr>
<td>devices/</td>
<td>设备控制核心:DeviceControl、DeviceController(控制类)、DroneVideoPlayer、WayLinePage、RobotTaskPage、XboxController
手柄</td>
</tr>
<tr>
<td>alerts/</td>
<td>告警六件套:实时 / 历史 / 概览 / 统计 / 规则 / 订阅</td>
</tr>
<tr>
<td>SystemSetting/</td>
<td>系统设置全部子页面</td>
</tr>
<tr>
<td>Map.jsx</td>
<td>Cesium 三维地图封装</td>
</tr>
<tr>
<td>VideoPlayer / VideoCanvas / AgoraRtc / VolcRtcPlayer / useWebRTCStream</td>
<td>多策略视频播放组件</td>
</tr>
<tr>
<td>WebSocketManager.ts</td>
<td>WebSocket 单例管理器(见 5.3)</td>
</tr>
<tr>
<td>Weather/</td>
<td>天气动效组件(晴/云/雨)</td>
</tr>
</tbody>
</table>
<h3>4.4 状态管理 <code>src/store</code></h3>
<div class="card blue">
<p>Redux Toolkit,仅两个切片,均<strong>持久化到 localStorage</strong>,刷新自动恢复:</p>
<ul>
<li><code>userSlice</code>:token、userInfo、isAuthenticated;含 <code>userLogin</code> 异步 thunk(调登录 API
→ 存 token / cookie / userInfo)与 <code>logout</code>、<code>setUserInfo</code> 同步 action。</li>
<li><code>stationSlice</code>:currentStation、stationId;场站切换后所有页面据此过滤数据。</li>
</ul>
</div>
<h3>4.5 API 服务层 <code>src/api</code></h3>
<p><code>request.ts</code> 创建全局唯一 Axios 实例:<code>baseURL</code> 来自 <code>env.js</code>,超时
100s,<code>withCredentials</code>。请求拦截器注入 <code>Authorization: Bearer &lt;token&gt;</code>;响应拦截器对
401(业务码或 HTTP 状态)触发<strong>带全局锁的统一登出</strong>(<code>isLoggingOut</code> 保证并发 401 只执行一次),清存储 → dispatch
logout → 跳转 /login。</p>
<p>业务 API
按域拆分:<code>api.ts</code>(设备任务/作业/热成像)、<code>device.ts</code>、<code>aiAnalysis.ts</code>、<code>alarmApi.js</code>、<code>workOrder.ts</code>、<code>reportDownloader.ts</code>、<code>systemSetting.ts</code>,以及
login / mune / organization / role / stationManage / user 子模块。</p>
</section>
<section id="s5">
<h2><span class="num">5</span>实时通信层设计</h2>
<p>这是系统区别于普通 CRUD 后台的核心。四条通道各司其职、并行工作:</p>
<table>
<thead>
<tr>
<th>通道</th>
<th>协议</th>
<th>方向</th>
<th>用途</th>
<th>前端实现</th>
</tr>
</thead>
<tbody>
<tr>
<td>业务接口</td>
<td>HTTP/REST</td>
<td>请求-响应</td>
<td>用户/设备/工单/报表等增删改查</td>
<td>Axios 实例 + 拦截器</td>
</tr>
<tr>
<td>设备遥测</td>
<td>MQTT over WS</td>
<td>发布-订阅(上行)</td>
<td>机器人实时位置、状态、传感器数据广播</td>
<td><code>useMqtt</code> Hook(自动重连 3s)</td>
</tr>
<tr>
<td>设备控制</td>
<td>WebSocket</td>
<td>双向</td>
<td>下发手柄/触控指令、获取控制权、保活</td>
<td><code>WebSocketManager</code> 单例 + <code>DeviceController</code></td>
</tr>
<tr>
<td>视频回传</td>
<td>WebRTC</td>
<td>单向拉流</td>
<td>无人机/机器人摄像头实时画面</td>
<td>JSWebrtc.Player / Agora / Volcengine RTC</td>
</tr>
</tbody>
</table>
<div class="card teal">
<h3>WebSocketManager 单例设计要点</h3>
<ul>
<li><strong>单例</strong>:全局唯一连接,按设备序列号 <code>serialNumber</code> 绑定,切换设备先关旧连新。</li>
<li><strong>AUTH 握手</strong>:连接 onopen 后立即发送 <code>{type:'AUTH', token, userName, deviceId}</code>
认证包。</li>
<li><strong>自动重连</strong>:非主动关闭时,onclose 后 5s 自动重连;切后台关连接、切前台自动恢复。</li>
<li><strong>观察者模式</strong>:<code>onMessage</code> / <code>onStatusChange</code> 注册回调,返回取消订阅函数。</li>
</ul>
</div>
<div class="card coral">
<h3>DeviceController 控制类</h3>
<p>封装设备控制全流程:申请控制权(<code>getControlRight</code>)→ 握手连接 → <code>sendGamepad</code> 经 lodash
<code>throttle(50ms)</code> 节流下发手柄数据 → 通过回调上抛设备信息 / 控制状态 / WS 状态变化。手柄输入 50ms 一发,兼顾流畅与带宽。</p>
</div>
</section>
<section id="s6">
<h2><span class="num">6</span>数据流设计</h2>
<h3>6.1 认证与会话流</h3>
<div class="flow">
<div class="step">Login 页</div>
<div class="arr">→</div>
<div class="step">userLogin thunk</div>
<div class="arr">→</div>
<div class="step">/login API</div>
<div class="arr">→</div>
<div class="step">存 token/cookie/userInfo</div>
<div class="arr">→</div>
<div class="step">ProtectedRoute 放行</div>
</div>
<p>登录成功后 token 写入 localStorage 与 cookie(含跨域 sharetoken),Layout 据用户 ID
拉取<strong>菜单列表</strong>(动态构建侧边栏)与<strong>场站列表</strong>(默认选第一个)。后续所有请求由拦截器自动带 token;401 时统一登出回登录页。</p>
<h3>6.2 实时设备控制环(核心链路)</h3>
<div class="diagram">
<svg viewBox="0 0 680 440" width="100%" role="img" aria-label="实时设备控制与遥测数据流">
<defs>
<marker id="ar2" viewBox="0 0 10 10" refX="8" refY="5" markerWidth="6" markerHeight="6"
orient="auto-start-reverse">
<path d="M2 1L8 5L2 9" fill="none" stroke="context-stroke" stroke-width="1.5"
stroke-linecap="round" stroke-linejoin="round" />
</marker>
</defs>
<text x="340" y="26" text-anchor="middle" font-family="var(--sans)" font-size="15" font-weight="500"
fill="#0C447C">实时设备控制与遥测数据流</text>
<rect x="230" y="50" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="340" y="70" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">操作员</text>
<text x="340" y="88" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">手柄 / 触控指令</text>
<path d="M340 102 L340 128" fill="none" stroke="#185FA5" stroke-width="1.2"
marker-end="url(#ar2)" />
<rect x="230" y="128" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="340" y="148" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">DeviceController</text>
<text x="340" y="166" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">设备控制类 · 50ms 节流</text>
<path d="M340 180 L340 206" fill="none" stroke="#185FA5" stroke-width="1.2"
marker-end="url(#ar2)" />
<rect x="230" y="206" width="220" height="52" rx="8" fill="#B5D4F4" stroke="#185FA5"
stroke-width="0.5" />
<text x="340" y="226" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#0C447C">WebSocketManager</text>
<text x="340" y="244" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#185FA5">单例 · AUTH 握手 · 自动重连</text>
<path d="M340 258 L340 284" fill="none" stroke="#185FA5" stroke-width="1.2"
marker-end="url(#ar2)" />
<rect x="230" y="284" width="220" height="52" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="340" y="304" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">WS server :9002</text>
<text x="340" y="322" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">设备控制服务</text>
<path d="M340 336 L340 362" fill="none" stroke="#0F6E56" stroke-width="1.2"
marker-end="url(#ar2)" />
<rect x="230" y="362" width="220" height="52" rx="8" fill="#9FE1CB" stroke="#0F6E56"
stroke-width="0.5" />
<text x="340" y="382" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="13" font-weight="500" fill="#085041">机器人 / 无人机</text>
<text x="340" y="400" text-anchor="middle" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#0F6E56">执行动作 · 采集状态</text>
<path d="M450 388 L492 388 L492 76 L450 76" fill="none" stroke="#D85A30" stroke-width="1.2"
stroke-dasharray="5 4" marker-end="url(#ar2)" />
<text x="500" y="226" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="12" font-weight="500" fill="#993C1D">MQTT</text>
<text x="500" y="244" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#D85A30">遥测上行</text>
<text x="500" y="262" text-anchor="start" dominant-baseline="central" font-family="var(--sans)"
font-size="12" fill="#D85A30">→ 监控页</text>
</svg>
<div class="cap">图 2 · 控制指令下行(蓝色实线 WebSocket)与遥测上行(橙色虚线 MQTT)走两条独立通道</div>
</div>
<p>关键设计:<strong>控制下行用 WebSocket(低延迟、可靠、双向),状态上行用
MQTT(发布订阅、一对多广播)</strong>。两条通道互不阻塞——即便视频/遥测流量大,也不会影响控制指令的实时性。手柄输入经 50ms 节流,避免高频刷爆带宽。</p>
<h3>6.3 实时监控遥测流</h3>
<div class="flow">
<div class="step t">机器人设备</div>
<div class="arr">→</div>
<div class="step t">MQTT broker :8083</div>
<div class="arr">→</div>
<div class="step t">useMqtt 订阅</div>
<div class="arr">→</div>
<div class="step t">监控页/Cesium 地图</div>
</div>
<p>设备按 topic 发布状态(位置、电量、传感器等),前端 <code>useMqtt</code> 订阅后驱动 Cesium 地图更新设备图标位置与实时监控面板。</p>
<h3>6.4 视频回传流</h3>
<div class="flow">
<div class="step">机器人摄像头</div>
<div class="arr">→</div>
<div class="step">WebRTC/流媒体服务</div>
<div class="arr">→</div>
<div class="step">JSWebrtc.Player</div>
<div class="arr">→</div>
<div class="step">&lt;video&gt; 元素</div>
</div>
<p><code>useWebRTCPlayer(deviceId, url)</code> 以 deviceId+url 为唯一键创建独立 video 元素并拉流,组件卸载时统一销毁
player,防止内存泄漏。同时支持 Agora RTC 与火山引擎 RTC 两套备选方案。</p>
<h3>6.5 HTTP 业务数据流</h3>
<div class="flow">
<div class="step">页面/组件</div>
<div class="arr">→</div>
<div class="step">api/*.ts</div>
<div class="arr">→</div>
<div class="step">request.ts (拦截器)</div>
<div class="arr">→</div>
<div class="step">API server :8081</div>
</div>
<p>所有 REST 请求经统一 Axios 实例:请求拦截器注入 Bearer token,响应拦截器解包 <code>response.data</code> 并统一处理 401 登出。</p>
</section>
<section id="s7">
<h2><span class="num">7</span>目录结构</h2>
<pre><code>机器人智慧服务系统/
├── env.js # 环境配置(baseURL / WS_URL / mqtt_url / geoserver_url)
├── vite.config.ts # 构建配置(别名 @、Cesium 插件、CSS 分割)
├── index.html # 入口 HTML
├── metadata.json # 项目元信息(真实名称:光伏机器人环境感知服务平台)
└── src/
├── api/ # API 服务层
│ ├── request.ts # Axios 实例 + 拦截器(401 统一登出)
│ ├── api.ts # 设备任务/作业/热成像等业务接口
│ ├── device.ts / aiAnalysis.ts / alarmApi.js / workOrder.ts ...
│ └── login/ mune/ organization/ role/ stationManage/ user/
├── components/ # 组件层
│ ├── Layout.tsx / ProtectedRoute.tsx
│ ├── devices/ # 设备控制(DeviceController / DroneVideoPlayer / XboxController ...)
│ ├── alerts/ # 告警六件套
│ ├── SystemSetting/ # 系统设置子页面
│ ├── WebSocketManager.ts # WS 单例
│ ├── useWebRTCStream.tsx # WebRTC 拉流 Hook
│ ├── Map.jsx / VideoPlayer / AgoraRtc / VolcRtcPlayer
│ └── Weather/
├── hooks/useMqtt.ts # MQTT 订阅 Hook
├── pages/ # 17 个业务页面
├── router/index.tsx # 全懒加载路由 + routeMeta 角色白名单
├── store/ # Redux(userSlice / stationSlice / types)
├── lib/TracePoint/ # 工具库(轨迹点)
├── assets/ / types/ # 静态资源 / 类型定义
└── App / main 入口</code></pre>
</section>
<section id="s8">
<h2><span class="num">8</span>关键设计决策与特点</h2>
<div class="card blue">
<h3>多协议并行,职责分离</h3>
<p>控制(WS)、遥测(MQTT)、视频(WebRTC)、业务(HTTP)各走独立通道。这是物联网实时控制系统的成熟模式:避免单通道拥塞互相拖累,也便于后端按特性独立伸缩。</p>
</div>
<div class="card teal">
<h3>全路由懒加载 + RBAC 元数据声明</h3>
<p>所有页面 <code>lazy()</code> 按需加载,首屏轻量;权限不写死在组件里,而是在 <code>routeMeta</code>
集中声明角色白名单,<code>ProtectedRoute</code> 统一校验,新增页面只改路由表即可获得权限控制。</p>
</div>
<div class="card blue">
<h3>状态最小化 + 本地持久化</h3>
<p>Redux 仅保留 user / station 两个跨页全局状态,其余页面数据就地管理;两者都持久化到 localStorage,刷新即恢复,体验接近原生应用。</p>
</div>
<div class="card coral">
<h3>连接健壮性</h3>
<p>WebSocket 单例 + AUTH 握手 + 5s 自动重连 + 页面可见性监听(切后台关、切前台重连);MQTT 3s 自动重连;Axios 401
全局锁防并发重复登出。多重保障弱网/切屏场景下的连接稳定。</p>
</div>
<div class="note warn">
<strong>待优化项(建议):</strong>
<ul style="margin-top:6px">
<li><code>useMqtt.ts</code> 中 MQTT 凭据硬编码在前端(username/password),建议改为后端动态下发 token。</li>
<li><code>useWebRTCStream.tsx</code> 中视频 token 硬编码,应从登录态/接口动态获取。</li>
<li><code>vite.config.ts</code> 的 manualChunks 分包策略被注释掉,Cesium 等大体积依赖建议启用分包以优化加载。</li>
<li>Redux store 仅两个切片,部分跨组件共享的设备/告警实时数据可考虑纳入统一管理或用 SWR/缓存层。</li>
</ul>
</div>
</section>
<footer>
光伏机器人环境感知服务平台 · 架构设计文档 · 由 WorkBuddy 基于源码静态分析生成
</footer>
</div>
</body>
</html>