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

422 lines
32 KiB
HTML
Raw Normal View History

2026-08-10 13:33:06 +08:00
<!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>