Files
web-Iot/docs/系统架构设计文档.html

985 lines
35 KiB
HTML
Raw Permalink Normal View History

2026-08-10 13:33:06 +08:00
<!DOCTYPE html>
<html lang="zh-CN">
2026-08-14 09:02:52 +08:00
2026-08-10 13:33:06 +08:00
<head>
2026-08-14 09:02:52 +08:00
<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>
2026-08-10 13:33:06 +08:00
</head>
2026-08-14 09:02:52 +08:00
2026-08-10 13:33:06 +08:00
<body>
2026-08-14 09:02:52 +08:00
<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>机器人智慧服务系统/
2026-08-10 13:33:06 +08:00
├── env.js # 环境配置(baseURL / WS_URL / mqtt_url / geoserver_url)
├── vite.config.ts # 构建配置(别名 @、Cesium 插件、CSS 分割)
├── index.html # 入口 HTML
2026-08-14 09:02:52 +08:00
├── metadata.json # 项目元信息(真实名称:光伏机器人环境感知服务平台)
2026-08-10 13:33:06 +08:00
└── 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>
2026-08-14 09:02:52 +08:00
</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>
2026-08-10 13:33:06 +08:00
</body>
2026-08-14 09:02:52 +08:00
2026-08-10 13:33:06 +08:00
</html>