重构模拟器工作台与日志浮层
This commit is contained in:
108
doc/debug/模拟器控制面板重构方案.md
Normal file
108
doc/debug/模拟器控制面板重构方案.md
Normal file
@@ -0,0 +1,108 @@
|
|||||||
|
# 模拟器控制面板重构方案
|
||||||
|
|
||||||
|
## 目标
|
||||||
|
|
||||||
|
在不破坏现有老版面板的前提下,新增一套新版控制面板,用于承接更复杂的开发调试工作流。
|
||||||
|
|
||||||
|
重构目标:
|
||||||
|
|
||||||
|
- 保留老版入口,确保已有使用习惯不受影响
|
||||||
|
- 新增工作台式面板,提升连接、控制、观察、排障效率
|
||||||
|
- 继续复用现有模拟器脚本和 websocket 协议,避免维护两套逻辑
|
||||||
|
|
||||||
|
## 设计原则
|
||||||
|
|
||||||
|
1. 新旧并行
|
||||||
|
- 新版入口使用 `/`
|
||||||
|
- 旧版入口保留在 `/v1/`
|
||||||
|
2. 逻辑复用
|
||||||
|
- 两个页面共用 `simulator.js`
|
||||||
|
- 只通过不同 HTML 布局和 CSS 风格区分
|
||||||
|
3. 面向调试流程
|
||||||
|
- 连接优先
|
||||||
|
- 控制第二
|
||||||
|
- 观察第三
|
||||||
|
- 日志独立
|
||||||
|
|
||||||
|
## 新版布局
|
||||||
|
|
||||||
|
新版面板采用工作台布局:
|
||||||
|
|
||||||
|
- 顶部:连接状态条
|
||||||
|
- 左侧:控制区
|
||||||
|
- 中间:地图与路径预览
|
||||||
|
- 右侧:状态摘要与快捷观察
|
||||||
|
- 右下:调试日志浮层
|
||||||
|
|
||||||
|
## 功能分区
|
||||||
|
|
||||||
|
### 1. 顶部连接条
|
||||||
|
|
||||||
|
包含:
|
||||||
|
|
||||||
|
- 定位模拟连接状态
|
||||||
|
- 心率模拟连接状态
|
||||||
|
- 调试日志连接状态
|
||||||
|
- 一键连接开发调试源
|
||||||
|
- 新旧面板切换入口
|
||||||
|
|
||||||
|
### 2. 左侧控制区
|
||||||
|
|
||||||
|
包含:
|
||||||
|
|
||||||
|
- 资源加载
|
||||||
|
- 定位实时发送
|
||||||
|
- 路径回放
|
||||||
|
- 心率模拟
|
||||||
|
- 新网关桥接
|
||||||
|
|
||||||
|
采用折叠分组,默认展开高频项。
|
||||||
|
|
||||||
|
### 3. 中间地图区
|
||||||
|
|
||||||
|
保留现有 Leaflet 地图和轨迹预览能力,作为核心观察区。
|
||||||
|
|
||||||
|
### 4. 右侧状态摘要
|
||||||
|
|
||||||
|
包含:
|
||||||
|
|
||||||
|
- 当前经纬度
|
||||||
|
- 当前航向
|
||||||
|
- 当前路径点数
|
||||||
|
- 最近发送状态
|
||||||
|
- 最近心率发送状态
|
||||||
|
- 资源加载摘要
|
||||||
|
- 网关桥接摘要
|
||||||
|
|
||||||
|
### 5. 日志区
|
||||||
|
|
||||||
|
日志继续做成浮层:
|
||||||
|
|
||||||
|
- 默认悬浮在地图右下
|
||||||
|
- 可清空
|
||||||
|
- 面积更大
|
||||||
|
- 便于边看地图边看日志
|
||||||
|
|
||||||
|
## 与旧版的关系
|
||||||
|
|
||||||
|
旧版和新版应同时可用:
|
||||||
|
|
||||||
|
- 新版作为默认工作台
|
||||||
|
- 旧版继续作为稳定基线
|
||||||
|
- 问题排查时可快速回退旧版
|
||||||
|
|
||||||
|
## 实施顺序
|
||||||
|
|
||||||
|
1. 根路径切换到新版工作台
|
||||||
|
2. 新增新版样式 `workbench.css`
|
||||||
|
3. 复用现有 `simulator.js`
|
||||||
|
4. 旧版页面迁移到 `/v1/`
|
||||||
|
5. 在旧版和新版之间互相添加跳转入口
|
||||||
|
6. 更新 README 和调试文档索引
|
||||||
|
|
||||||
|
## 验收标准
|
||||||
|
|
||||||
|
- 老版页面继续正常工作
|
||||||
|
- 新版页面可完整使用现有 GPS、心率、日志、路径、网关能力
|
||||||
|
- 两个页面共用同一套 websocket 协议和数据逻辑
|
||||||
|
- 用户可以在两个版本之间切换
|
||||||
@@ -9,6 +9,8 @@
|
|||||||
|
|
||||||
## 当前主文档
|
## 当前主文档
|
||||||
|
|
||||||
|
- [模拟器控制面板重构方案](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
||||||
|
用于说明新版模拟器工作台布局、新旧并行策略和重构目标。
|
||||||
- [平台能力说明](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
- [平台能力说明](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
||||||
用于记录主体能力、`web-view`、传感器等平台边界。
|
用于记录主体能力、`web-view`、传感器等平台边界。
|
||||||
- [模拟器调试日志方案](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
- [模拟器调试日志方案](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
||||||
@@ -21,9 +23,10 @@
|
|||||||
## 推荐阅读顺序
|
## 推荐阅读顺序
|
||||||
|
|
||||||
1. [platform-capability-notes.md](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
1. [platform-capability-notes.md](/D:/dev/cmr-mini/doc/debug/平台能力说明.md)
|
||||||
2. [sensor-current-summary.md](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
2. [mock-simulator-control-panel-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
||||||
3. [mock-simulator-debug-log-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
3. [sensor-current-summary.md](/D:/dev/cmr-mini/doc/debug/传感器现状总结.md)
|
||||||
4. [compass-debugging-notes.md](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
4. [mock-simulator-debug-log-proposal.md](/D:/dev/cmr-mini/doc/debug/模拟器调试日志方案.md)
|
||||||
|
5. [compass-debugging-notes.md](/D:/dev/cmr-mini/doc/debug/罗盘排障记录.md)
|
||||||
|
|
||||||
## 使用建议
|
## 使用建议
|
||||||
|
|
||||||
@@ -31,4 +34,3 @@
|
|||||||
- 看“现在系统是什么状态”,优先看传感器现状总结。
|
- 看“现在系统是什么状态”,优先看传感器现状总结。
|
||||||
- 看“以后日志怎么打”,优先看模拟器日志方案。
|
- 看“以后日志怎么打”,优先看模拟器日志方案。
|
||||||
- 看“为什么罗盘以前坏过”,再去看罗盘问题记录。
|
- 看“为什么罗盘以前坏过”,再去看罗盘问题记录。
|
||||||
|
|
||||||
|
|||||||
@@ -26,6 +26,7 @@
|
|||||||
## 调试
|
## 调试
|
||||||
|
|
||||||
- [调试文档索引](/D:/dev/cmr-mini/doc/debug/调试文档索引.md)
|
- [调试文档索引](/D:/dev/cmr-mini/doc/debug/调试文档索引.md)
|
||||||
|
- [模拟器控制面板重构方案](/D:/dev/cmr-mini/doc/debug/模拟器控制面板重构方案.md)
|
||||||
|
|
||||||
## 网关
|
## 网关
|
||||||
|
|
||||||
@@ -40,4 +41,3 @@
|
|||||||
- 长期保留的少量工作便签见 [notes](/D:/dev/cmr-mini/doc/notes)。
|
- 长期保留的少量工作便签见 [notes](/D:/dev/cmr-mini/doc/notes)。
|
||||||
- 历史方案稿和阶段性讨论稿已移到 [archive](/D:/dev/cmr-mini/doc/archive/归档索引.md)。
|
- 历史方案稿和阶段性讨论稿已移到 [archive](/D:/dev/cmr-mini/doc/archive/归档索引.md)。
|
||||||
- 正式阅读建议优先从本页和配置索引进入,不再直接平铺浏览全部文档。
|
- 正式阅读建议优先从本页和配置索引进入,不再直接平铺浏览全部文档。
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,8 @@ npm run mock-gps-sim
|
|||||||
|
|
||||||
启动后:
|
启动后:
|
||||||
|
|
||||||
- 控制台页面: `http://127.0.0.1:17865/`
|
- 新版工作台: `http://127.0.0.1:17865/`
|
||||||
|
- 旧版面板: `http://127.0.0.1:17865/v1/`
|
||||||
- 小程序定位模拟地址: `ws://127.0.0.1:17865/mock-gps`
|
- 小程序定位模拟地址: `ws://127.0.0.1:17865/mock-gps`
|
||||||
- 小程序心率模拟地址: `ws://127.0.0.1:17865/mock-hr`
|
- 小程序心率模拟地址: `ws://127.0.0.1:17865/mock-hr`
|
||||||
- 小程序调试日志地址: `ws://127.0.0.1:17865/debug-log`
|
- 小程序调试日志地址: `ws://127.0.0.1:17865/debug-log`
|
||||||
@@ -88,6 +89,12 @@ ws://127.0.0.1:17865/debug-log
|
|||||||
http://127.0.0.1:17865/
|
http://127.0.0.1:17865/
|
||||||
```
|
```
|
||||||
|
|
||||||
|
如果需要旧版稳定界面,打开:
|
||||||
|
|
||||||
|
```text
|
||||||
|
http://127.0.0.1:17865/v1/
|
||||||
|
```
|
||||||
|
|
||||||
在“新网关桥接”区域可以直接配置:
|
在“新网关桥接”区域可以直接配置:
|
||||||
|
|
||||||
- 是否启用桥接
|
- 是否启用桥接
|
||||||
|
|||||||
@@ -3,21 +3,65 @@
|
|||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
<title>Mock GPS Simulator</title>
|
<title>Mock GPS Simulator Workbench</title>
|
||||||
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
<link rel="stylesheet" href="./style.css">
|
<link rel="stylesheet" href="./workbench.css">
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="layout">
|
<div class="wb-shell">
|
||||||
<aside class="panel">
|
<header class="wb-topbar">
|
||||||
<div class="panel__header">
|
<div class="wb-topbar__brand">
|
||||||
<div class="panel__eyebrow">MOCK GPS SIM</div>
|
<div class="wb-topbar__eyebrow">MOCK GPS SIM</div>
|
||||||
<h1>外部模拟器</h1>
|
<h1>模拟器工作台</h1>
|
||||||
|
<div class="wb-topbar__links">
|
||||||
|
<a href="/v1/">打开旧版面板</a>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div class="wb-topbar__status">
|
||||||
|
<div class="wb-connection-bar">
|
||||||
|
<div class="wb-connection-pill">
|
||||||
|
<span class="wb-connection-pill__label">定位模拟</span>
|
||||||
|
<strong id="topGpsStatus" class="wb-connection-pill__value">未连接</strong>
|
||||||
|
</div>
|
||||||
|
<div class="wb-connection-pill">
|
||||||
|
<span class="wb-connection-pill__label">心率模拟</span>
|
||||||
|
<strong id="topHrStatus" class="wb-connection-pill__value">未连接</strong>
|
||||||
|
</div>
|
||||||
|
<div class="wb-connection-pill">
|
||||||
|
<span class="wb-connection-pill__label">调试日志</span>
|
||||||
|
<strong id="topLoggerStatus" class="wb-connection-pill__value">未连接</strong>
|
||||||
|
</div>
|
||||||
|
<div class="wb-connection-pill">
|
||||||
|
<span class="wb-connection-pill__label">网关桥接</span>
|
||||||
|
<strong id="topGatewayStatus" class="wb-connection-pill__value">未启用</strong>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
||||||
</div>
|
</div>
|
||||||
|
</header>
|
||||||
|
|
||||||
<section class="group">
|
<div class="wb-layout">
|
||||||
<div class="group__title">资源加载</div>
|
<aside class="wb-sidebar">
|
||||||
|
<section class="wb-card">
|
||||||
|
<div class="wb-card__title">运行摘要</div>
|
||||||
|
<div class="stat"><span>资源状态</span><strong id="summaryResourceText">未载入</strong></div>
|
||||||
|
<div class="stat"><span>定位发送</span><strong id="summaryGpsSendText">待命</strong></div>
|
||||||
|
<div class="stat"><span>心率发送</span><strong id="summaryHrSendText">待命</strong></div>
|
||||||
|
<div class="stat"><span>路径状态</span><strong id="summaryPathText">待命</strong></div>
|
||||||
|
<div class="stat"><span>网关桥接</span><strong id="summaryGatewayText">未启用</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="wb-card">
|
||||||
|
<div class="wb-card__title">当前位置</div>
|
||||||
|
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
||||||
|
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
||||||
|
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
||||||
|
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<details class="wb-section" open>
|
||||||
|
<summary>资源加载</summary>
|
||||||
|
<div class="wb-section__body">
|
||||||
<label class="field">
|
<label class="field">
|
||||||
<span>游戏配置 URL</span>
|
<span>游戏配置 URL</span>
|
||||||
<input id="configUrlInput" type="text" value="https://oss-mbh5.colormaprun.com/wxmini/test/game.json">
|
<input id="configUrlInput" type="text" value="https://oss-mbh5.colormaprun.com/wxmini/test/game.json">
|
||||||
@@ -45,10 +89,12 @@
|
|||||||
<div id="resourceStatus" class="hint">支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。</div>
|
<div id="resourceStatus" class="hint">支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。</div>
|
||||||
<div id="resourceDetail" class="group__status">尚未载入资源</div>
|
<div id="resourceDetail" class="group__status">尚未载入资源</div>
|
||||||
<div id="courseJumpList" class="jump-list"></div>
|
<div id="courseJumpList" class="jump-list"></div>
|
||||||
</section>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<section class="group">
|
<details class="wb-section" open>
|
||||||
<div class="group__title">实时发送</div>
|
<summary>定位发送</summary>
|
||||||
|
<div class="wb-section__body">
|
||||||
<div id="realtimeStatus" class="group__status">桥接未连接</div>
|
<div id="realtimeStatus" class="group__status">桥接未连接</div>
|
||||||
<div id="lastSendStatus" class="group__status">最近发送: --</div>
|
<div id="lastSendStatus" class="group__status">最近发送: --</div>
|
||||||
<div class="row">
|
<div class="row">
|
||||||
@@ -71,10 +117,81 @@
|
|||||||
<span>精度 (m)</span>
|
<span>精度 (m)</span>
|
||||||
<input id="accuracyInput" type="number" min="1" max="100" value="6">
|
<input id="accuracyInput" type="number" min="1" max="100" value="6">
|
||||||
</label>
|
</label>
|
||||||
</section>
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
<section class="group">
|
<details class="wb-section" open>
|
||||||
<div class="group__title">新网关桥接</div>
|
<summary>路径回放</summary>
|
||||||
|
<div class="wb-section__body">
|
||||||
|
<div id="playbackStatus" class="group__status">路径待命</div>
|
||||||
|
<input id="trackFileInput" class="file-input-hidden" type="file" accept=".gpx,.kml,.geojson,.json,application/json,application/gpx+xml,application/vnd.google-earth.kml+xml">
|
||||||
|
<div class="row">
|
||||||
|
<button id="importTrackBtn" class="btn">导入轨迹文件</button>
|
||||||
|
<button id="togglePathModeBtn" class="btn">开启路径编辑</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="clearPathBtn" class="btn">清空路径</button>
|
||||||
|
<button id="fitPathBtn" class="btn">适配路径</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="playPathBtn" class="btn btn--accent">开始回放</button>
|
||||||
|
<button id="pausePathBtn" class="btn">暂停回放</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>移动速度 (km/h)</span>
|
||||||
|
<input id="speedInput" type="number" min="1" max="25" step="0.1" value="6">
|
||||||
|
</label>
|
||||||
|
<label class="field field--check">
|
||||||
|
<input id="loopPathInput" type="checkbox" checked>
|
||||||
|
<span>循环回放</span>
|
||||||
|
</label>
|
||||||
|
<div id="pathHint" class="hint">点击“开启路径编辑”后,在地图上逐点添加路径。</div>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="wb-section">
|
||||||
|
<summary>心率模拟</summary>
|
||||||
|
<div class="wb-section__body">
|
||||||
|
<div id="heartRateStatus" class="group__status">心率模拟待命</div>
|
||||||
|
<div id="lastHeartRateStatus" class="group__status">最近发送: --</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="sendHeartRateOnceBtn" class="btn">发送一次</button>
|
||||||
|
<button id="startHeartRateStreamBtn" class="btn btn--accent">开始连续发送</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="stopHeartRateStreamBtn" class="btn">停止发送</button>
|
||||||
|
<button id="applyHeartRatePresetBtn" class="btn">应用分区样本</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="toggleHeartRateSampleBtn" class="btn">模拟真实样本</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>心率值 (bpm)</span>
|
||||||
|
<input id="heartRateInput" type="number" min="40" max="220" value="120">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>发送频率</span>
|
||||||
|
<select id="heartRateHzSelect">
|
||||||
|
<option value="1" selected>1 Hz</option>
|
||||||
|
<option value="2">2 Hz</option>
|
||||||
|
<option value="4">4 Hz</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>样本模板</span>
|
||||||
|
<select id="heartRateSampleTemplateSelect">
|
||||||
|
<option value="jog" selected>慢跑样本</option>
|
||||||
|
<option value="tempo">节奏跑样本</option>
|
||||||
|
<option value="interval">间歇跑样本</option>
|
||||||
|
<option value="recovery">恢复走样本</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
</details>
|
||||||
|
|
||||||
|
<details class="wb-section">
|
||||||
|
<summary>新网关桥接</summary>
|
||||||
|
<div class="wb-section__body">
|
||||||
<div id="gatewayBridgeStatus" class="group__status">未启用</div>
|
<div id="gatewayBridgeStatus" class="group__status">未启用</div>
|
||||||
<div id="gatewayBridgeTarget" class="group__status">目标设备: --</div>
|
<div id="gatewayBridgeTarget" class="group__status">目标设备: --</div>
|
||||||
<div id="gatewayBridgeLast" class="group__status">最近状态: --</div>
|
<div id="gatewayBridgeLast" class="group__status">最近状态: --</div>
|
||||||
@@ -131,99 +248,40 @@
|
|||||||
<button id="applyGatewayBridgeConfigBtn" class="btn btn--primary">应用桥接配置</button>
|
<button id="applyGatewayBridgeConfigBtn" class="btn btn--primary">应用桥接配置</button>
|
||||||
<button id="reloadGatewayBridgeConfigBtn" class="btn">重新读取</button>
|
<button id="reloadGatewayBridgeConfigBtn" class="btn">重新读取</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">心率模拟</div>
|
|
||||||
<div id="heartRateStatus" class="group__status">心率模拟待命</div>
|
|
||||||
<div id="lastHeartRateStatus" class="group__status">最近发送: --</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="sendHeartRateOnceBtn" class="btn">发送一次</button>
|
|
||||||
<button id="startHeartRateStreamBtn" class="btn btn--accent">开始连续发送</button>
|
|
||||||
</div>
|
</div>
|
||||||
<div class="row">
|
</details>
|
||||||
<button id="stopHeartRateStreamBtn" class="btn">停止发送</button>
|
|
||||||
<button id="applyHeartRatePresetBtn" class="btn">应用分区样本</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="toggleHeartRateSampleBtn" class="btn">模拟真实样本</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>心率值 (bpm)</span>
|
|
||||||
<input id="heartRateInput" type="number" min="40" max="220" value="120">
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>发送频率</span>
|
|
||||||
<select id="heartRateHzSelect">
|
|
||||||
<option value="1" selected>1 Hz</option>
|
|
||||||
<option value="2">2 Hz</option>
|
|
||||||
<option value="4">4 Hz</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
<label class="field">
|
|
||||||
<span>样本模板</span>
|
|
||||||
<select id="heartRateSampleTemplateSelect">
|
|
||||||
<option value="jog" selected>慢跑样本</option>
|
|
||||||
<option value="tempo">节奏跑样本</option>
|
|
||||||
<option value="interval">间歇跑样本</option>
|
|
||||||
<option value="recovery">恢复走样本</option>
|
|
||||||
</select>
|
|
||||||
</label>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">路径回放</div>
|
|
||||||
<div id="playbackStatus" class="group__status">路径待命</div>
|
|
||||||
<input id="trackFileInput" class="file-input-hidden" type="file" accept=".gpx,.kml,.geojson,.json,application/json,application/gpx+xml,application/vnd.google-earth.kml+xml">
|
|
||||||
<div class="row">
|
|
||||||
<button id="importTrackBtn" class="btn">导入轨迹文件</button>
|
|
||||||
<button id="togglePathModeBtn" class="btn">开启路径编辑</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="clearPathBtn" class="btn">清空路径</button>
|
|
||||||
<button id="fitPathBtn" class="btn">适配路径</button>
|
|
||||||
</div>
|
|
||||||
<div class="row">
|
|
||||||
<button id="playPathBtn" class="btn btn--accent">开始回放</button>
|
|
||||||
<button id="pausePathBtn" class="btn">暂停回放</button>
|
|
||||||
</div>
|
|
||||||
<label class="field">
|
|
||||||
<span>移动速度 (km/h)</span>
|
|
||||||
<input id="speedInput" type="number" min="1" max="25" step="0.1" value="6">
|
|
||||||
</label>
|
|
||||||
<label class="field field--check">
|
|
||||||
<input id="loopPathInput" type="checkbox" checked>
|
|
||||||
<span>循环回放</span>
|
|
||||||
</label>
|
|
||||||
<div id="pathHint" class="hint">点击“开启路径编辑”后,在地图上逐点添加路径。</div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">当前位置</div>
|
|
||||||
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
|
||||||
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
|
||||||
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
|
||||||
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
<section class="group">
|
|
||||||
<div class="group__title">日志</div>
|
|
||||||
<div id="log" class="log"></div>
|
|
||||||
</section>
|
|
||||||
|
|
||||||
</aside>
|
</aside>
|
||||||
|
|
||||||
<main class="map-shell">
|
<main class="wb-stage">
|
||||||
<div id="map"></div>
|
<div id="map"></div>
|
||||||
<section class="floating-debug-log">
|
<section id="floatingDebugLogPanel" class="floating-debug-log">
|
||||||
<div class="floating-debug-log__header">
|
<div class="floating-debug-log__header">
|
||||||
|
<div class="floating-debug-log__title-wrap">
|
||||||
<div class="floating-debug-log__title">调试日志</div>
|
<div class="floating-debug-log__title">调试日志</div>
|
||||||
|
<div id="debugLogMeta" class="floating-debug-log__meta">全部 · 0 条</div>
|
||||||
|
</div>
|
||||||
|
<div class="floating-debug-log__actions">
|
||||||
|
<label class="floating-debug-log__filter">
|
||||||
|
<span>范围</span>
|
||||||
|
<select id="debugLogScopeFilter">
|
||||||
|
<option value="all">全部</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<button id="toggleDebugLogPanelBtn" class="floating-debug-log__toggle" type="button">缩小</button>
|
||||||
<button id="clearDebugLogBtn" class="floating-debug-log__clear" type="button">清空</button>
|
<button id="clearDebugLogBtn" class="floating-debug-log__clear" type="button">清空</button>
|
||||||
</div>
|
</div>
|
||||||
|
</div>
|
||||||
<div id="debugLog" class="log log--debug log--floating"></div>
|
<div id="debugLog" class="log log--debug log--floating"></div>
|
||||||
</section>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
</div>
|
</div>
|
||||||
|
<section class="wb-bottom-strip">
|
||||||
|
<section class="wb-card wb-card--bottom">
|
||||||
|
<div class="wb-card__title">最近事件</div>
|
||||||
|
<div id="log" class="log"></div>
|
||||||
|
</section>
|
||||||
|
</section>
|
||||||
|
</div>
|
||||||
|
|
||||||
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
<script src="./simulator.js"></script>
|
<script src="./simulator.js"></script>
|
||||||
|
|||||||
@@ -44,6 +44,7 @@
|
|||||||
debugSocket: null,
|
debugSocket: null,
|
||||||
connected: false,
|
connected: false,
|
||||||
heartRateConnected: false,
|
heartRateConnected: false,
|
||||||
|
debugConnected: false,
|
||||||
socketConnecting: false,
|
socketConnecting: false,
|
||||||
heartRateSocketConnecting: false,
|
heartRateSocketConnecting: false,
|
||||||
debugSocketConnecting: false,
|
debugSocketConnecting: false,
|
||||||
@@ -74,6 +75,9 @@
|
|||||||
bridgeLastStatusText: '--',
|
bridgeLastStatusText: '--',
|
||||||
bridgeConfigSaving: false,
|
bridgeConfigSaving: false,
|
||||||
bridgePresets: [],
|
bridgePresets: [],
|
||||||
|
debugLogEntries: [],
|
||||||
|
debugLogScopeFilter: 'all',
|
||||||
|
debugLogPanelMinimized: false,
|
||||||
}
|
}
|
||||||
|
|
||||||
const elements = {
|
const elements = {
|
||||||
@@ -143,7 +147,20 @@
|
|||||||
pathCountText: document.getElementById('pathCountText'),
|
pathCountText: document.getElementById('pathCountText'),
|
||||||
log: document.getElementById('log'),
|
log: document.getElementById('log'),
|
||||||
debugLog: document.getElementById('debugLog'),
|
debugLog: document.getElementById('debugLog'),
|
||||||
|
debugLogMeta: document.getElementById('debugLogMeta'),
|
||||||
clearDebugLogBtn: document.getElementById('clearDebugLogBtn'),
|
clearDebugLogBtn: document.getElementById('clearDebugLogBtn'),
|
||||||
|
debugLogScopeFilter: document.getElementById('debugLogScopeFilter'),
|
||||||
|
floatingDebugLogPanel: document.getElementById('floatingDebugLogPanel'),
|
||||||
|
toggleDebugLogPanelBtn: document.getElementById('toggleDebugLogPanelBtn'),
|
||||||
|
topGpsStatus: document.getElementById('topGpsStatus'),
|
||||||
|
topHrStatus: document.getElementById('topHrStatus'),
|
||||||
|
topLoggerStatus: document.getElementById('topLoggerStatus'),
|
||||||
|
topGatewayStatus: document.getElementById('topGatewayStatus'),
|
||||||
|
summaryResourceText: document.getElementById('summaryResourceText'),
|
||||||
|
summaryGpsSendText: document.getElementById('summaryGpsSendText'),
|
||||||
|
summaryHrSendText: document.getElementById('summaryHrSendText'),
|
||||||
|
summaryPathText: document.getElementById('summaryPathText'),
|
||||||
|
summaryGatewayText: document.getElementById('summaryGatewayText'),
|
||||||
}
|
}
|
||||||
|
|
||||||
elements.configUrlInput.value = DEFAULT_CONFIG_URL
|
elements.configUrlInput.value = DEFAULT_CONFIG_URL
|
||||||
@@ -165,22 +182,79 @@
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
|
const normalized = {
|
||||||
const scope = String(entry.scope || 'app')
|
timestamp: entry.timestamp || Date.now(),
|
||||||
const level = String(entry.level || 'info').toUpperCase()
|
scope: String(entry.scope || 'app'),
|
||||||
const message = String(entry.message || '')
|
level: String(entry.level || 'info'),
|
||||||
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
|
message: String(entry.message || ''),
|
||||||
const nextText = `[${time}] [${scope}] [${level}] ${message}${payloadText}\n${elements.debugLog.textContent || ''}`
|
payload: entry.payload && typeof entry.payload === 'object' ? entry.payload : null,
|
||||||
elements.debugLog.textContent = nextText
|
}
|
||||||
.split('\n')
|
state.debugLogEntries.unshift(normalized)
|
||||||
.slice(0, MAX_DEBUG_LOG_LINES)
|
if (state.debugLogEntries.length > MAX_DEBUG_LOG_LINES) {
|
||||||
.join('\n')
|
state.debugLogEntries = state.debugLogEntries.slice(0, MAX_DEBUG_LOG_LINES)
|
||||||
|
}
|
||||||
|
renderDebugScopeOptions()
|
||||||
|
renderDebugLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
function clearDebugLog() {
|
function clearDebugLog() {
|
||||||
if (elements.debugLog) {
|
state.debugLogEntries = []
|
||||||
elements.debugLog.textContent = ''
|
renderDebugScopeOptions()
|
||||||
|
renderDebugLog()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function renderDebugScopeOptions() {
|
||||||
|
if (!elements.debugLogScopeFilter) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const staticOptions = ['all', 'logger', 'gps-logo', 'gps', 'heart-rate', 'track', 'compass', 'h5', 'content-card', 'gateway']
|
||||||
|
const seenScopes = new Set(staticOptions)
|
||||||
|
state.debugLogEntries.forEach((entry) => {
|
||||||
|
if (entry.scope) {
|
||||||
|
seenScopes.add(entry.scope)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const options = Array.from(seenScopes)
|
||||||
|
const currentValue = options.includes(state.debugLogScopeFilter) ? state.debugLogScopeFilter : 'all'
|
||||||
|
elements.debugLogScopeFilter.innerHTML = options
|
||||||
|
.map((scope) => `<option value="${scope}">${scope === 'all' ? '全部' : scope}</option>`)
|
||||||
|
.join('')
|
||||||
|
elements.debugLogScopeFilter.value = currentValue
|
||||||
|
state.debugLogScopeFilter = currentValue
|
||||||
|
}
|
||||||
|
|
||||||
|
function renderDebugLog() {
|
||||||
|
if (!elements.debugLog) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const filteredEntries = state.debugLogEntries.filter((entry) => {
|
||||||
|
return state.debugLogScopeFilter === 'all' || entry.scope === state.debugLogScopeFilter
|
||||||
|
})
|
||||||
|
|
||||||
|
if (elements.debugLogMeta) {
|
||||||
|
const scopeLabel = state.debugLogScopeFilter === 'all' ? '全部' : state.debugLogScopeFilter
|
||||||
|
elements.debugLogMeta.textContent = `${scopeLabel} · ${filteredEntries.length} 条`
|
||||||
|
}
|
||||||
|
|
||||||
|
elements.debugLog.textContent = filteredEntries
|
||||||
|
.map((entry) => {
|
||||||
|
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
|
||||||
|
const level = String(entry.level || 'info').toUpperCase()
|
||||||
|
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
|
||||||
|
return `[${time}] [${entry.scope}] [${level}] ${entry.message}${payloadText}`
|
||||||
|
})
|
||||||
|
.join('\n')
|
||||||
|
}
|
||||||
|
|
||||||
|
function updateDebugLogPanelState() {
|
||||||
|
if (!elements.floatingDebugLogPanel || !elements.toggleDebugLogPanelBtn) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
elements.floatingDebugLogPanel.classList.toggle('is-minimized', state.debugLogPanelMinimized)
|
||||||
|
elements.toggleDebugLogPanelBtn.textContent = state.debugLogPanelMinimized ? '展开' : '缩小'
|
||||||
}
|
}
|
||||||
|
|
||||||
function setResourceStatus(message, tone) {
|
function setResourceStatus(message, tone) {
|
||||||
@@ -206,6 +280,19 @@
|
|||||||
elements.socketStatus.className = connected ? 'badge badge--ok' : 'badge badge--muted'
|
elements.socketStatus.className = connected ? 'badge badge--ok' : 'badge badge--muted'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function setConnectionValue(element, text, tone) {
|
||||||
|
if (!element) {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
element.textContent = text
|
||||||
|
element.classList.remove('is-ok', 'is-warn')
|
||||||
|
if (tone === 'ok') {
|
||||||
|
element.classList.add('is-ok')
|
||||||
|
} else if (tone === 'warn') {
|
||||||
|
element.classList.add('is-warn')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
function formatClockTime(timestamp) {
|
function formatClockTime(timestamp) {
|
||||||
if (!timestamp) {
|
if (!timestamp) {
|
||||||
return '--'
|
return '--'
|
||||||
@@ -304,6 +391,43 @@
|
|||||||
} else {
|
} else {
|
||||||
elements.playbackStatus.textContent = '路径待命'
|
elements.playbackStatus.textContent = '路径待命'
|
||||||
}
|
}
|
||||||
|
|
||||||
|
setConnectionValue(
|
||||||
|
elements.topGpsStatus,
|
||||||
|
state.connected ? (state.streaming ? '发送中' : '已连接') : state.socketConnecting ? '连接中' : '未连接',
|
||||||
|
state.connected ? 'ok' : state.socketConnecting ? 'warn' : null
|
||||||
|
)
|
||||||
|
setConnectionValue(
|
||||||
|
elements.topHrStatus,
|
||||||
|
state.heartRateConnected ? (state.heartRateStreaming ? '发送中' : '已连接') : state.heartRateSocketConnecting ? '连接中' : '未连接',
|
||||||
|
state.heartRateConnected ? 'ok' : state.heartRateSocketConnecting ? 'warn' : null
|
||||||
|
)
|
||||||
|
setConnectionValue(
|
||||||
|
elements.topLoggerStatus,
|
||||||
|
state.debugConnected ? '已连接' : state.debugSocketConnecting ? '连接中' : '未连接',
|
||||||
|
state.debugConnected ? 'ok' : state.debugSocketConnecting ? 'warn' : null
|
||||||
|
)
|
||||||
|
setConnectionValue(
|
||||||
|
elements.topGatewayStatus,
|
||||||
|
!state.bridgeEnabled ? '未启用' : state.bridgeConnected && state.bridgeAuthenticated ? '已认证' : state.bridgeConnected ? '待认证' : '未连接',
|
||||||
|
state.bridgeConnected && state.bridgeAuthenticated ? 'ok' : state.bridgeEnabled ? 'warn' : null
|
||||||
|
)
|
||||||
|
|
||||||
|
if (elements.summaryResourceText) {
|
||||||
|
elements.summaryResourceText.textContent = state.resourceLoading ? '载入中' : state.loadedCourse ? '已载入' : '未载入'
|
||||||
|
}
|
||||||
|
if (elements.summaryGpsSendText) {
|
||||||
|
elements.summaryGpsSendText.textContent = state.connected ? (state.streaming ? `${elements.hzSelect.value} Hz` : '待命') : '未连接'
|
||||||
|
}
|
||||||
|
if (elements.summaryHrSendText) {
|
||||||
|
elements.summaryHrSendText.textContent = state.heartRateConnected ? (state.heartRateStreaming ? `${elements.heartRateHzSelect.value} Hz` : '待命') : '未连接'
|
||||||
|
}
|
||||||
|
if (elements.summaryPathText) {
|
||||||
|
elements.summaryPathText.textContent = state.playbackRunning ? '回放中' : state.pathEditMode ? '编辑中' : pathPoints.length >= 2 ? `${pathPoints.length} 点` : '待命'
|
||||||
|
}
|
||||||
|
if (elements.summaryGatewayText) {
|
||||||
|
elements.summaryGatewayText.textContent = !state.bridgeEnabled ? '未启用' : state.bridgeConnected && state.bridgeAuthenticated ? '已认证' : state.bridgeConnected ? '待认证' : '未连接'
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function bridgeConfigFromServerPayload(payload) {
|
function bridgeConfigFromServerPayload(payload) {
|
||||||
@@ -587,6 +711,7 @@
|
|||||||
|
|
||||||
const socket = new WebSocket(DEBUG_LOG_WS_URL)
|
const socket = new WebSocket(DEBUG_LOG_WS_URL)
|
||||||
state.debugSocket = socket
|
state.debugSocket = socket
|
||||||
|
state.debugConnected = false
|
||||||
state.debugSocketConnecting = true
|
state.debugSocketConnecting = true
|
||||||
log(`连接日志通道 ${DEBUG_LOG_WS_URL}`)
|
log(`连接日志通道 ${DEBUG_LOG_WS_URL}`)
|
||||||
|
|
||||||
@@ -605,20 +730,27 @@
|
|||||||
|
|
||||||
socket.addEventListener('open', () => {
|
socket.addEventListener('open', () => {
|
||||||
state.debugSocketConnecting = false
|
state.debugSocketConnecting = false
|
||||||
|
state.debugSocket = socket
|
||||||
|
state.debugConnected = true
|
||||||
log('日志通道已连接')
|
log('日志通道已连接')
|
||||||
|
updateUiState()
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.addEventListener('close', () => {
|
socket.addEventListener('close', () => {
|
||||||
state.debugSocketConnecting = false
|
state.debugSocketConnecting = false
|
||||||
state.debugSocket = null
|
state.debugSocket = null
|
||||||
|
state.debugConnected = false
|
||||||
log('日志通道已断开')
|
log('日志通道已断开')
|
||||||
|
updateUiState()
|
||||||
window.setTimeout(connectDebugSocket, 1500)
|
window.setTimeout(connectDebugSocket, 1500)
|
||||||
})
|
})
|
||||||
|
|
||||||
socket.addEventListener('error', () => {
|
socket.addEventListener('error', () => {
|
||||||
state.debugSocketConnecting = false
|
state.debugSocketConnecting = false
|
||||||
state.debugSocket = null
|
state.debugSocket = null
|
||||||
|
state.debugConnected = false
|
||||||
log('日志通道连接失败')
|
log('日志通道连接失败')
|
||||||
|
updateUiState()
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -1803,9 +1935,24 @@
|
|||||||
if (elements.clearDebugLogBtn) {
|
if (elements.clearDebugLogBtn) {
|
||||||
elements.clearDebugLogBtn.addEventListener('click', clearDebugLog)
|
elements.clearDebugLogBtn.addEventListener('click', clearDebugLog)
|
||||||
}
|
}
|
||||||
|
if (elements.toggleDebugLogPanelBtn) {
|
||||||
|
elements.toggleDebugLogPanelBtn.addEventListener('click', () => {
|
||||||
|
state.debugLogPanelMinimized = !state.debugLogPanelMinimized
|
||||||
|
updateDebugLogPanelState()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
if (elements.debugLogScopeFilter) {
|
||||||
|
elements.debugLogScopeFilter.addEventListener('change', () => {
|
||||||
|
state.debugLogScopeFilter = elements.debugLogScopeFilter.value || 'all'
|
||||||
|
renderDebugLog()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
updateReadout()
|
updateReadout()
|
||||||
setSocketBadge(false)
|
setSocketBadge(false)
|
||||||
|
renderDebugScopeOptions()
|
||||||
|
renderDebugLog()
|
||||||
|
updateDebugLogPanelState()
|
||||||
setResourceStatus('支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。', null)
|
setResourceStatus('支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。', null)
|
||||||
state.bridgePresets = loadBridgePresets()
|
state.bridgePresets = loadBridgePresets()
|
||||||
renderBridgePresetOptions('')
|
renderBridgePresetOptions('')
|
||||||
|
|||||||
@@ -33,6 +33,17 @@ body {
|
|||||||
font-size: 28px;
|
font-size: 28px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.panel__links {
|
||||||
|
margin: 0 0 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.panel__links a {
|
||||||
|
color: #24523e;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
.panel__eyebrow {
|
.panel__eyebrow {
|
||||||
font-weight: 800;
|
font-weight: 800;
|
||||||
letter-spacing: 0.18em;
|
letter-spacing: 0.18em;
|
||||||
|
|||||||
231
tools/mock-gps-sim/public/v1/index.html
Normal file
231
tools/mock-gps-sim/public/v1/index.html
Normal file
@@ -0,0 +1,231 @@
|
|||||||
|
<!doctype html>
|
||||||
|
<html lang="zh-CN">
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
|
<title>Mock GPS Simulator v1</title>
|
||||||
|
<link rel="stylesheet" href="https://unpkg.com/leaflet@1.9.4/dist/leaflet.css">
|
||||||
|
<link rel="stylesheet" href="../style.css">
|
||||||
|
</head>
|
||||||
|
<body>
|
||||||
|
<div class="layout">
|
||||||
|
<aside class="panel">
|
||||||
|
<div class="panel__header">
|
||||||
|
<div class="panel__eyebrow">MOCK GPS SIM</div>
|
||||||
|
<h1>外部模拟器</h1>
|
||||||
|
<div class="panel__links"><a href="/">打开新版工作台</a></div>
|
||||||
|
<div id="socketStatus" class="badge badge--muted">未连接</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">资源加载</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>游戏配置 URL</span>
|
||||||
|
<input id="configUrlInput" type="text" value="https://oss-mbh5.colormaprun.com/wxmini/test/game.json">
|
||||||
|
</label>
|
||||||
|
<div class="row">
|
||||||
|
<button id="loadConfigBtn" class="btn btn--primary">载入配置</button>
|
||||||
|
<button id="fitCourseBtn" class="btn">适配视野</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>瓦片模板</span>
|
||||||
|
<input id="tileUrlInput" type="text" placeholder="https://host/tiles/{z}/{x}/{y}.webp">
|
||||||
|
</label>
|
||||||
|
<div class="row">
|
||||||
|
<button id="applyTilesBtn" class="btn">应用瓦片</button>
|
||||||
|
<button id="resetTilesBtn" class="btn">恢复 OSM</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>KML URL</span>
|
||||||
|
<input id="courseUrlInput" type="text" placeholder="https://host/course/c01.kml">
|
||||||
|
</label>
|
||||||
|
<div class="row">
|
||||||
|
<button id="loadCourseBtn" class="btn">载入控制点</button>
|
||||||
|
<button id="clearCourseBtn" class="btn">清空控制点</button>
|
||||||
|
</div>
|
||||||
|
<div id="resourceStatus" class="hint">支持直接载入 game.json,也支持单独填瓦片模板和 KML 地址。</div>
|
||||||
|
<div id="resourceDetail" class="group__status">尚未载入资源</div>
|
||||||
|
<div id="courseJumpList" class="jump-list"></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">实时发送</div>
|
||||||
|
<div id="realtimeStatus" class="group__status">桥接未连接</div>
|
||||||
|
<div id="lastSendStatus" class="group__status">最近发送: --</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="connectBtn" class="btn btn--primary">连接桥接</button>
|
||||||
|
<button id="sendOnceBtn" class="btn">发送一次</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="streamBtn" class="btn btn--accent">开始连续发送</button>
|
||||||
|
<button id="stopStreamBtn" class="btn">停止发送</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>发送频率</span>
|
||||||
|
<select id="hzSelect">
|
||||||
|
<option value="2">2 Hz</option>
|
||||||
|
<option value="5" selected>5 Hz</option>
|
||||||
|
<option value="10">10 Hz</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>精度 (m)</span>
|
||||||
|
<input id="accuracyInput" type="number" min="1" max="100" value="6">
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">新网关桥接</div>
|
||||||
|
<div id="gatewayBridgeStatus" class="group__status">未启用</div>
|
||||||
|
<div id="gatewayBridgeTarget" class="group__status">目标设备: --</div>
|
||||||
|
<div id="gatewayBridgeLast" class="group__status">最近状态: --</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>桥接预设</span>
|
||||||
|
<select id="gatewayBridgePresetSelect">
|
||||||
|
<option value="">选择预设</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>预设名称</span>
|
||||||
|
<input id="gatewayBridgePresetNameInput" type="text" placeholder="例如:家长端-A / 场控-B">
|
||||||
|
</label>
|
||||||
|
<div class="row">
|
||||||
|
<button id="applyGatewayBridgePresetBtn" class="btn">套用预设</button>
|
||||||
|
<button id="saveGatewayBridgePresetBtn" class="btn">保存预设</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="deleteGatewayBridgePresetBtn" class="btn">删除预设</button>
|
||||||
|
</div>
|
||||||
|
<label class="field field--check">
|
||||||
|
<input id="gatewayBridgeEnabledInput" type="checkbox">
|
||||||
|
<span>启用新网关桥接</span>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>网关地址</span>
|
||||||
|
<input id="gatewayBridgeUrlInput" type="text" placeholder="ws://127.0.0.1:18080/ws">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Producer Token / Channel Token</span>
|
||||||
|
<input id="gatewayBridgeTokenInput" type="text" placeholder="producerToken 或 dev-producer-token">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Channel ID</span>
|
||||||
|
<input id="gatewayBridgeChannelIdInput" type="text" placeholder="ch-xxxx">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>目标 Device ID</span>
|
||||||
|
<input id="gatewayBridgeDeviceIdInput" type="text" placeholder="child-001">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>目标 Group ID</span>
|
||||||
|
<input id="gatewayBridgeGroupIdInput" type="text" placeholder="class-a">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Source ID</span>
|
||||||
|
<input id="gatewayBridgeSourceIdInput" type="text" placeholder="mock-gps-sim">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>Source Mode</span>
|
||||||
|
<input id="gatewayBridgeSourceModeInput" type="text" placeholder="mock">
|
||||||
|
</label>
|
||||||
|
<div class="row">
|
||||||
|
<button id="applyGatewayBridgeConfigBtn" class="btn btn--primary">应用桥接配置</button>
|
||||||
|
<button id="reloadGatewayBridgeConfigBtn" class="btn">重新读取</button>
|
||||||
|
</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">心率模拟</div>
|
||||||
|
<div id="heartRateStatus" class="group__status">心率模拟待命</div>
|
||||||
|
<div id="lastHeartRateStatus" class="group__status">最近发送: --</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="sendHeartRateOnceBtn" class="btn">发送一次</button>
|
||||||
|
<button id="startHeartRateStreamBtn" class="btn btn--accent">开始连续发送</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="stopHeartRateStreamBtn" class="btn">停止发送</button>
|
||||||
|
<button id="applyHeartRatePresetBtn" class="btn">应用分区样本</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="toggleHeartRateSampleBtn" class="btn">模拟真实样本</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>心率值 (bpm)</span>
|
||||||
|
<input id="heartRateInput" type="number" min="40" max="220" value="120">
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>发送频率</span>
|
||||||
|
<select id="heartRateHzSelect">
|
||||||
|
<option value="1" selected>1 Hz</option>
|
||||||
|
<option value="2">2 Hz</option>
|
||||||
|
<option value="4">4 Hz</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
<label class="field">
|
||||||
|
<span>样本模板</span>
|
||||||
|
<select id="heartRateSampleTemplateSelect">
|
||||||
|
<option value="jog" selected>慢跑样本</option>
|
||||||
|
<option value="tempo">节奏跑样本</option>
|
||||||
|
<option value="interval">间歇跑样本</option>
|
||||||
|
<option value="recovery">恢复走样本</option>
|
||||||
|
</select>
|
||||||
|
</label>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">路径回放</div>
|
||||||
|
<div id="playbackStatus" class="group__status">路径待命</div>
|
||||||
|
<input id="trackFileInput" class="file-input-hidden" type="file" accept=".gpx,.kml,.geojson,.json,application/json,application/gpx+xml,application/vnd.google-earth.kml+xml">
|
||||||
|
<div class="row">
|
||||||
|
<button id="importTrackBtn" class="btn">导入轨迹文件</button>
|
||||||
|
<button id="togglePathModeBtn" class="btn">开启路径编辑</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="clearPathBtn" class="btn">清空路径</button>
|
||||||
|
<button id="fitPathBtn" class="btn">适配路径</button>
|
||||||
|
</div>
|
||||||
|
<div class="row">
|
||||||
|
<button id="playPathBtn" class="btn btn--accent">开始回放</button>
|
||||||
|
<button id="pausePathBtn" class="btn">暂停回放</button>
|
||||||
|
</div>
|
||||||
|
<label class="field">
|
||||||
|
<span>移动速度 (km/h)</span>
|
||||||
|
<input id="speedInput" type="number" min="1" max="25" step="0.1" value="6">
|
||||||
|
</label>
|
||||||
|
<label class="field field--check">
|
||||||
|
<input id="loopPathInput" type="checkbox" checked>
|
||||||
|
<span>循环回放</span>
|
||||||
|
</label>
|
||||||
|
<div id="pathHint" class="hint">点击“开启路径编辑”后,在地图上逐点添加路径。</div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">当前位置</div>
|
||||||
|
<div class="stat"><span>纬度</span><strong id="latText">--</strong></div>
|
||||||
|
<div class="stat"><span>经度</span><strong id="lonText">--</strong></div>
|
||||||
|
<div class="stat"><span>航向</span><strong id="headingText">--</strong></div>
|
||||||
|
<div class="stat"><span>路径点</span><strong id="pathCountText">0</strong></div>
|
||||||
|
</section>
|
||||||
|
|
||||||
|
<section class="group">
|
||||||
|
<div class="group__title">日志</div>
|
||||||
|
<div id="log" class="log"></div>
|
||||||
|
</section>
|
||||||
|
</aside>
|
||||||
|
|
||||||
|
<main class="map-shell">
|
||||||
|
<div id="map"></div>
|
||||||
|
<section class="floating-debug-log">
|
||||||
|
<div class="floating-debug-log__header">
|
||||||
|
<div class="floating-debug-log__title">调试日志</div>
|
||||||
|
<button id="clearDebugLogBtn" class="floating-debug-log__clear" type="button">清空</button>
|
||||||
|
</div>
|
||||||
|
<div id="debugLog" class="log log--debug log--floating"></div>
|
||||||
|
</section>
|
||||||
|
</main>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<script src="https://unpkg.com/leaflet@1.9.4/dist/leaflet.js"></script>
|
||||||
|
<script src="../simulator.js"></script>
|
||||||
|
</body>
|
||||||
|
</html>
|
||||||
555
tools/mock-gps-sim/public/workbench.css
Normal file
555
tools/mock-gps-sim/public/workbench.css
Normal file
@@ -0,0 +1,555 @@
|
|||||||
|
* {
|
||||||
|
box-sizing: border-box;
|
||||||
|
}
|
||||||
|
|
||||||
|
html,
|
||||||
|
body {
|
||||||
|
height: 100%;
|
||||||
|
margin: 0;
|
||||||
|
font-family: "Segoe UI", "PingFang SC", sans-serif;
|
||||||
|
background:
|
||||||
|
radial-gradient(circle at top left, rgba(68, 161, 124, 0.16), transparent 34%),
|
||||||
|
linear-gradient(160deg, #edf5ef 0%, #dbe8df 100%);
|
||||||
|
color: #163126;
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-shell {
|
||||||
|
height: 100vh;
|
||||||
|
display: grid;
|
||||||
|
grid-template-rows: auto 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 20px;
|
||||||
|
padding: 18px 24px;
|
||||||
|
background: rgba(248, 251, 247, 0.92);
|
||||||
|
border-bottom: 1px solid rgba(22, 49, 38, 0.08);
|
||||||
|
backdrop-filter: blur(14px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__eyebrow {
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.24em;
|
||||||
|
color: #6a8778;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar h1 {
|
||||||
|
margin: 6px 0 8px;
|
||||||
|
font-size: 30px;
|
||||||
|
line-height: 1.1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__links {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__links a {
|
||||||
|
color: #24523e;
|
||||||
|
text-decoration: none;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__status {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 16px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-bar {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
justify-content: flex-end;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-pill {
|
||||||
|
min-width: 112px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: rgba(235, 242, 236, 0.95);
|
||||||
|
box-shadow: inset 0 0 0 1px rgba(22, 49, 38, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-pill__label {
|
||||||
|
display: block;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
font-size: 11px;
|
||||||
|
font-weight: 700;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #6f8a7d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-pill__value {
|
||||||
|
display: block;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #2b4338;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-pill__value.is-ok {
|
||||||
|
color: #0a7a3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-connection-pill__value.is-warn {
|
||||||
|
color: #9a5b10;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-layout {
|
||||||
|
min-height: 0;
|
||||||
|
display: grid;
|
||||||
|
grid-template-columns: 380px 1fr;
|
||||||
|
gap: 18px;
|
||||||
|
padding: 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-sidebar {
|
||||||
|
min-height: 0;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding-right: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-stage {
|
||||||
|
position: relative;
|
||||||
|
min-height: 0;
|
||||||
|
overflow: hidden;
|
||||||
|
border-radius: 28px;
|
||||||
|
box-shadow: 0 28px 60px rgba(20, 41, 31, 0.18);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-bottom-strip {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-card--bottom .log {
|
||||||
|
max-height: 180px;
|
||||||
|
}
|
||||||
|
|
||||||
|
#map {
|
||||||
|
width: 100%;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-section,
|
||||||
|
.wb-card {
|
||||||
|
margin-bottom: 14px;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.92);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.7);
|
||||||
|
box-shadow: 0 16px 40px rgba(25, 44, 34, 0.08);
|
||||||
|
overflow: hidden;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-section summary,
|
||||||
|
.wb-card__title {
|
||||||
|
list-style: none;
|
||||||
|
cursor: pointer;
|
||||||
|
padding: 18px 18px 16px;
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #537062;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-section summary::-webkit-details-marker {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-section[open] summary {
|
||||||
|
border-bottom: 1px solid rgba(22, 49, 38, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-section__body,
|
||||||
|
.wb-card {
|
||||||
|
padding: 0 18px 18px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-card__title {
|
||||||
|
padding: 18px 18px 12px;
|
||||||
|
margin: 0 -18px 6px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge {
|
||||||
|
display: inline-flex;
|
||||||
|
align-items: center;
|
||||||
|
min-height: 34px;
|
||||||
|
padding: 0 14px;
|
||||||
|
border-radius: 999px;
|
||||||
|
font-size: 13px;
|
||||||
|
font-weight: 700;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--muted {
|
||||||
|
background: #e5ece5;
|
||||||
|
color: #4f6458;
|
||||||
|
}
|
||||||
|
|
||||||
|
.badge--ok {
|
||||||
|
background: #d8f7e3;
|
||||||
|
color: #0a7a3d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.group__status,
|
||||||
|
.hint {
|
||||||
|
min-height: 18px;
|
||||||
|
margin: 0 0 12px;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.5;
|
||||||
|
color: #5e786d;
|
||||||
|
}
|
||||||
|
|
||||||
|
.row {
|
||||||
|
display: flex;
|
||||||
|
gap: 10px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn {
|
||||||
|
flex: 1;
|
||||||
|
min-height: 42px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 14px;
|
||||||
|
background: #ebf0ea;
|
||||||
|
color: #193226;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
transition: transform 120ms ease, background 120ms ease, color 120ms ease, opacity 120ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:hover {
|
||||||
|
transform: translateY(-1px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--primary {
|
||||||
|
background: #103f2f;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn--accent {
|
||||||
|
background: #0ea5a4;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn.is-active {
|
||||||
|
outline: 2px solid #ffb300;
|
||||||
|
}
|
||||||
|
|
||||||
|
.btn:disabled {
|
||||||
|
opacity: 0.56;
|
||||||
|
cursor: not-allowed;
|
||||||
|
transform: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 6px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
font-size: 13px;
|
||||||
|
color: #557266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.field input,
|
||||||
|
.field select {
|
||||||
|
min-height: 40px;
|
||||||
|
border: 1px solid rgba(22, 49, 38, 0.12);
|
||||||
|
border-radius: 12px;
|
||||||
|
padding: 0 12px;
|
||||||
|
font: inherit;
|
||||||
|
background: rgba(249, 252, 249, 0.96);
|
||||||
|
}
|
||||||
|
|
||||||
|
.field--check {
|
||||||
|
flex-direction: row;
|
||||||
|
align-items: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.file-input-hidden {
|
||||||
|
position: absolute;
|
||||||
|
width: 1px;
|
||||||
|
height: 1px;
|
||||||
|
opacity: 0;
|
||||||
|
pointer-events: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat {
|
||||||
|
display: flex;
|
||||||
|
justify-content: space-between;
|
||||||
|
padding: 10px 0;
|
||||||
|
border-bottom: 1px solid rgba(22, 49, 38, 0.06);
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat:last-child {
|
||||||
|
border-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat span {
|
||||||
|
color: #668073;
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.stat strong {
|
||||||
|
font-size: 14px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log {
|
||||||
|
min-height: 180px;
|
||||||
|
max-height: 300px;
|
||||||
|
overflow-y: auto;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 16px;
|
||||||
|
background: #f3f7f1;
|
||||||
|
font-size: 12px;
|
||||||
|
line-height: 1.6;
|
||||||
|
color: #486257;
|
||||||
|
white-space: pre-wrap;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log--debug {
|
||||||
|
max-height: 320px;
|
||||||
|
background: #111917;
|
||||||
|
color: #d6f3df;
|
||||||
|
font-family: Consolas, "SFMono-Regular", monospace;
|
||||||
|
}
|
||||||
|
|
||||||
|
.log--floating {
|
||||||
|
min-height: 340px;
|
||||||
|
max-height: min(54vh, 560px);
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-list {
|
||||||
|
display: flex;
|
||||||
|
flex-wrap: wrap;
|
||||||
|
gap: 8px;
|
||||||
|
margin-top: 12px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-chip {
|
||||||
|
min-height: 32px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: #eef6ea;
|
||||||
|
color: #244132;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-chip--start {
|
||||||
|
background: #fff0c9;
|
||||||
|
}
|
||||||
|
|
||||||
|
.jump-chip--finish {
|
||||||
|
background: #ffe2b8;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log {
|
||||||
|
position: absolute;
|
||||||
|
right: 18px;
|
||||||
|
bottom: 18px;
|
||||||
|
z-index: 600;
|
||||||
|
width: min(620px, calc(100vw - 500px));
|
||||||
|
min-width: 440px;
|
||||||
|
max-width: 720px;
|
||||||
|
padding: 14px;
|
||||||
|
border-radius: 22px;
|
||||||
|
background: rgba(255, 255, 255, 0.94);
|
||||||
|
border: 1px solid rgba(255, 255, 255, 0.52);
|
||||||
|
box-shadow: 0 22px 60px rgba(17, 33, 26, 0.22);
|
||||||
|
backdrop-filter: blur(16px);
|
||||||
|
transition: width 160ms ease, min-width 160ms ease, max-width 160ms ease, transform 160ms ease, box-shadow 160ms ease;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log.is-minimized {
|
||||||
|
width: 236px;
|
||||||
|
min-width: 236px;
|
||||||
|
max-width: 236px;
|
||||||
|
padding: 12px 14px;
|
||||||
|
border-radius: 18px;
|
||||||
|
box-shadow: 0 16px 36px rgba(17, 33, 26, 0.18);
|
||||||
|
transform: translateY(4px);
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log.is-minimized .log--floating,
|
||||||
|
.floating-debug-log.is-minimized .floating-debug-log__filter,
|
||||||
|
.floating-debug-log.is-minimized .floating-debug-log__clear {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log.is-minimized .floating-debug-log__header {
|
||||||
|
margin-bottom: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log.is-minimized .floating-debug-log__actions {
|
||||||
|
gap: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__header {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: space-between;
|
||||||
|
gap: 12px;
|
||||||
|
margin-bottom: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__title-wrap {
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
gap: 4px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__title {
|
||||||
|
font-size: 14px;
|
||||||
|
font-weight: 800;
|
||||||
|
letter-spacing: 0.08em;
|
||||||
|
color: #4a6a5e;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__meta {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #6a8278;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__clear {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(17, 33, 26, 0.1);
|
||||||
|
color: #244132;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__toggle {
|
||||||
|
min-height: 30px;
|
||||||
|
padding: 0 12px;
|
||||||
|
border: 0;
|
||||||
|
border-radius: 999px;
|
||||||
|
background: rgba(17, 33, 26, 0.1);
|
||||||
|
color: #244132;
|
||||||
|
font-size: 12px;
|
||||||
|
font-weight: 700;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__actions {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__filter {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
font-size: 12px;
|
||||||
|
color: #557266;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log__filter select {
|
||||||
|
min-height: 30px;
|
||||||
|
border: 1px solid rgba(22, 49, 38, 0.12);
|
||||||
|
border-radius: 10px;
|
||||||
|
padding: 0 10px;
|
||||||
|
background: rgba(249, 252, 249, 0.96);
|
||||||
|
font: inherit;
|
||||||
|
}
|
||||||
|
|
||||||
|
.leaflet-container {
|
||||||
|
background: #dfeadb;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-marker {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-marker__control {
|
||||||
|
width: 34px;
|
||||||
|
height: 34px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 3px solid #cc0077;
|
||||||
|
color: #cc0077;
|
||||||
|
background: rgba(255, 255, 255, 0.9);
|
||||||
|
font-size: 16px;
|
||||||
|
font-weight: 800;
|
||||||
|
line-height: 1;
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-marker__start {
|
||||||
|
width: 0;
|
||||||
|
height: 0;
|
||||||
|
border-left: 16px solid transparent;
|
||||||
|
border-right: 16px solid transparent;
|
||||||
|
border-bottom: 28px solid #cc0077;
|
||||||
|
filter: drop-shadow(0 3px 10px rgba(22, 49, 38, 0.22));
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-marker__finish {
|
||||||
|
position: relative;
|
||||||
|
width: 36px;
|
||||||
|
height: 36px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 4px solid #cc0077;
|
||||||
|
background: rgba(255, 255, 255, 0.76);
|
||||||
|
}
|
||||||
|
|
||||||
|
.course-marker__finish::after {
|
||||||
|
content: "";
|
||||||
|
position: absolute;
|
||||||
|
inset: 6px;
|
||||||
|
border-radius: 999px;
|
||||||
|
border: 3px solid #cc0077;
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1380px) {
|
||||||
|
.wb-layout {
|
||||||
|
grid-template-columns: 340px 1fr;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log {
|
||||||
|
width: min(520px, calc(100vw - 440px));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
@media (max-width: 1120px) {
|
||||||
|
.wb-layout {
|
||||||
|
grid-template-columns: 1fr;
|
||||||
|
grid-template-rows: auto minmax(420px, 1fr);
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-sidebar {
|
||||||
|
max-height: 32vh;
|
||||||
|
}
|
||||||
|
|
||||||
|
.floating-debug-log {
|
||||||
|
width: min(420px, calc(100vw - 48px));
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar {
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-topbar__status {
|
||||||
|
width: 100%;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: flex-start;
|
||||||
|
}
|
||||||
|
|
||||||
|
.wb-bottom-strip {
|
||||||
|
padding-top: 18px;
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -57,7 +57,12 @@ function respondJson(response, statusCode, payload) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
function serveStatic(requestPath, response) {
|
function serveStatic(requestPath, response) {
|
||||||
const safePath = requestPath === '/' ? '/index.html' : requestPath
|
let safePath = requestPath === '/' ? '/index.html' : requestPath
|
||||||
|
if (safePath.endsWith('/')) {
|
||||||
|
safePath = `${safePath}index.html`
|
||||||
|
} else if (!path.extname(safePath)) {
|
||||||
|
safePath = `${safePath}/index.html`
|
||||||
|
}
|
||||||
const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
|
const resolvedPath = path.normalize(path.join(PUBLIC_DIR, safePath))
|
||||||
if (!resolvedPath.startsWith(PUBLIC_DIR)) {
|
if (!resolvedPath.startsWith(PUBLIC_DIR)) {
|
||||||
response.writeHead(403)
|
response.writeHead(403)
|
||||||
|
|||||||
Reference in New Issue
Block a user