完善样式系统与调试链路底座

This commit is contained in:
2026-03-30 18:19:05 +08:00
parent 2c0fd4c549
commit 3b9117427e
40 changed files with 7526 additions and 389 deletions

View File

@@ -11,7 +11,9 @@ npm run mock-gps-sim
启动后:
- 控制台页面: `http://127.0.0.1:17865/`
- 小程序接收地址: `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/debug-log`
- 资源代理: `http://127.0.0.1:17865/proxy?url=<remote-url>`
## 当前能力
@@ -28,6 +30,43 @@ npm run mock-gps-sim
- 路径回放
- 速度、频率、精度调节
- 可选桥接到新实时网关
- 接收小程序侧 `debug-log` 调试日志
## 调试日志
调试日志 websocket 独立地址:
```text
ws://127.0.0.1:17865/debug-log
```
发送消息格式:
```json
{
"type": "debug-log",
"timestamp": 1712345678901,
"scope": "gps-logo",
"level": "info",
"message": "wx.getImageInfo success",
"payload": {
"src": "https://example.com/logo.png"
}
}
```
当前 UI 会通过独立日志通道把这类消息显示到“调试日志”区域。
第一阶段主要用于承接:
- `gps-logo`
后面可以继续扩到:
- `compass`
- `h5`
- `content-card`
- `heart-rate`
## 桥接到新网关
@@ -35,7 +74,9 @@ npm run mock-gps-sim
默认行为:
- 小程序仍可继续连接 `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/debug-log`
- 页面里可以直接配置并启用新网关桥接
- 环境变量只作为服务启动时的默认值
@@ -184,8 +225,20 @@ http://127.0.0.1:17865/bridge-config
ws://192.168.1.23:17865/mock-gps
```
心率模拟地址应配置为:
```text
ws://192.168.1.23:17865/mock-hr
```
同理,浏览器里的模拟器页面也建议用电脑局域网地址打开,例如:
```text
http://192.168.1.23:17865/
```
如果你要在小程序里看调试日志Logger 地址应配置为:
```text
ws://192.168.1.23:17865/debug-log
```

View File

@@ -210,10 +210,18 @@
<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>

View File

@@ -3,7 +3,9 @@
const DEFAULT_CONFIG_URL = 'https://oss-mbh5.colormaprun.com/wxmini/test/game.json'
const DEFAULT_TILE_URL = 'https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png'
const PROXY_BASE_URL = `${location.origin}/proxy?url=`
const WS_URL = `ws://${location.hostname}:17865/mock-gps`
const GPS_WS_URL = `ws://${location.hostname}:17865/mock-gps`
const HEART_RATE_WS_URL = `ws://${location.hostname}:17865/mock-hr`
const DEBUG_LOG_WS_URL = `ws://${location.hostname}:17865/debug-log`
const DEFAULT_GATEWAY_BRIDGE_URL = 'ws://127.0.0.1:18080/ws'
const LEGACY_GATEWAY_BRIDGE_URLS = new Set([
'ws://127.0.0.1:8080/ws',
@@ -11,6 +13,7 @@
])
const BRIDGE_CONFIG_STORAGE_KEY = 'mock-gps-sim.bridge-config'
const BRIDGE_PRESETS_STORAGE_KEY = 'mock-gps-sim.bridge-presets'
const MAX_DEBUG_LOG_LINES = 400
const map = L.map('map').setView(DEFAULT_CENTER, 16)
let tileLayer = createTileLayer(DEFAULT_TILE_URL, {
@@ -37,8 +40,13 @@
const pathPoints = []
const state = {
socket: null,
heartRateSocket: null,
debugSocket: null,
connected: false,
heartRateConnected: false,
socketConnecting: false,
heartRateSocketConnecting: false,
debugSocketConnecting: false,
streaming: false,
heartRateStreaming: false,
heartRateSampleMode: false,
@@ -134,6 +142,8 @@
headingText: document.getElementById('headingText'),
pathCountText: document.getElementById('pathCountText'),
log: document.getElementById('log'),
debugLog: document.getElementById('debugLog'),
clearDebugLogBtn: document.getElementById('clearDebugLogBtn'),
}
elements.configUrlInput.value = DEFAULT_CONFIG_URL
@@ -150,6 +160,29 @@
elements.log.textContent = `[${time}] ${message}\n` + elements.log.textContent
}
function logDebug(entry) {
if (!elements.debugLog) {
return
}
const time = new Date(entry.timestamp || Date.now()).toLocaleTimeString()
const scope = String(entry.scope || 'app')
const level = String(entry.level || 'info').toUpperCase()
const message = String(entry.message || '')
const payloadText = entry.payload ? ` ${JSON.stringify(entry.payload)}` : ''
const nextText = `[${time}] [${scope}] [${level}] ${message}${payloadText}\n${elements.debugLog.textContent || ''}`
elements.debugLog.textContent = nextText
.split('\n')
.slice(0, MAX_DEBUG_LOG_LINES)
.join('\n')
}
function clearDebugLog() {
if (elements.debugLog) {
elements.debugLog.textContent = ''
}
}
function setResourceStatus(message, tone) {
elements.resourceStatus.textContent = message
elements.resourceStatus.className = 'hint'
@@ -191,10 +224,10 @@
elements.streamBtn.classList.toggle('is-active', state.streaming)
elements.streamBtn.disabled = !state.connected || state.streaming
elements.stopStreamBtn.disabled = !state.streaming
elements.sendHeartRateOnceBtn.disabled = !state.connected
elements.sendHeartRateOnceBtn.disabled = !state.heartRateConnected
elements.startHeartRateStreamBtn.textContent = state.heartRateStreaming ? '发送中' : '开始连续发送'
elements.startHeartRateStreamBtn.classList.toggle('is-active', state.heartRateStreaming)
elements.startHeartRateStreamBtn.disabled = !state.connected || state.heartRateStreaming
elements.startHeartRateStreamBtn.disabled = !state.heartRateConnected || state.heartRateStreaming
elements.stopHeartRateStreamBtn.disabled = !state.heartRateStreaming
elements.toggleHeartRateSampleBtn.textContent = state.heartRateSampleMode ? '关闭真实样本' : '模拟真实样本'
elements.toggleHeartRateSampleBtn.classList.toggle('is-active', state.heartRateSampleMode)
@@ -250,13 +283,13 @@
elements.realtimeStatus.textContent = '桥接未连接'
}
if (state.connected && state.heartRateStreaming) {
if (state.heartRateConnected && state.heartRateStreaming) {
elements.heartRateStatus.textContent = state.heartRateSampleMode
? `桥接已连接,正在以 ${elements.heartRateHzSelect.value} Hz 发送真实心率样本`
: `桥接已连接,正在以 ${elements.heartRateHzSelect.value} Hz 连续发送心率`
} else if (state.connected) {
} else if (state.heartRateConnected) {
elements.heartRateStatus.textContent = state.heartRateSampleMode ? '真实心率样本待命' : '心率模拟待命'
} else if (state.socketConnecting) {
} else if (state.heartRateSocketConnecting) {
elements.heartRateStatus.textContent = '桥接连接中'
} else {
elements.heartRateStatus.textContent = '桥接未连接'
@@ -476,12 +509,12 @@
return
}
const socket = new WebSocket(WS_URL)
const socket = new WebSocket(GPS_WS_URL)
state.socket = socket
state.socketConnecting = true
setSocketBadge(false)
updateUiState()
log(`连接 ${WS_URL}`)
log(`连接 ${GPS_WS_URL}`)
socket.addEventListener('open', () => {
state.connected = true
@@ -495,7 +528,6 @@
state.connected = false
state.socketConnecting = false
stopStream()
stopHeartRateStream()
setSocketBadge(false)
updateUiState()
log('桥接已断开')
@@ -505,13 +537,91 @@
state.connected = false
state.socketConnecting = false
stopStream()
stopHeartRateStream()
setSocketBadge(false)
updateUiState()
log('桥接连接失败')
})
}
function connectHeartRateSocket() {
if (state.heartRateSocket && (state.heartRateSocket.readyState === WebSocket.OPEN || state.heartRateSocket.readyState === WebSocket.CONNECTING)) {
return
}
const socket = new WebSocket(HEART_RATE_WS_URL)
state.heartRateSocket = socket
state.heartRateSocketConnecting = true
updateUiState()
log(`连接心率模拟 ${HEART_RATE_WS_URL}`)
socket.addEventListener('open', () => {
state.heartRateConnected = true
state.heartRateSocketConnecting = false
updateUiState()
log('心率模拟已连接')
})
socket.addEventListener('close', () => {
state.heartRateConnected = false
state.heartRateSocketConnecting = false
state.heartRateSocket = null
stopHeartRateStream()
updateUiState()
log('心率模拟已断开')
})
socket.addEventListener('error', () => {
state.heartRateConnected = false
state.heartRateSocketConnecting = false
state.heartRateSocket = null
stopHeartRateStream()
updateUiState()
log('心率模拟连接失败')
})
}
function connectDebugSocket() {
if (state.debugSocket && (state.debugSocket.readyState === WebSocket.OPEN || state.debugSocket.readyState === WebSocket.CONNECTING)) {
return
}
const socket = new WebSocket(DEBUG_LOG_WS_URL)
state.debugSocket = socket
state.debugSocketConnecting = true
log(`连接日志通道 ${DEBUG_LOG_WS_URL}`)
socket.addEventListener('message', (event) => {
let parsed = null
try {
parsed = JSON.parse(String(event.data || ''))
} catch (_error) {
return
}
if (parsed && parsed.type === 'debug-log') {
logDebug(parsed)
}
})
socket.addEventListener('open', () => {
state.debugSocketConnecting = false
log('日志通道已连接')
})
socket.addEventListener('close', () => {
state.debugSocketConnecting = false
state.debugSocket = null
log('日志通道已断开')
window.setTimeout(connectDebugSocket, 1500)
})
socket.addEventListener('error', () => {
state.debugSocketConnecting = false
state.debugSocket = null
log('日志通道连接失败')
})
}
async function refreshGatewayBridgeStatus() {
try {
const response = await fetch('/bridge-status', {
@@ -1159,8 +1269,8 @@
}
function sendCurrentHeartRate() {
if (!state.socket || state.socket.readyState !== WebSocket.OPEN) {
log('未连接桥接,无法发送心率')
if (!state.heartRateSocket || state.heartRateSocket.readyState !== WebSocket.OPEN) {
log('未连接心率模拟,无法发送心率')
return
}
@@ -1169,7 +1279,7 @@
timestamp: Date.now(),
bpm: state.heartRateSampleMode ? getSampleHeartRateBpm() : getHeartRateBpm(),
}
state.socket.send(JSON.stringify(payload))
state.heartRateSocket.send(JSON.stringify(payload))
state.lastHeartRateSentText = `${formatClockTime(payload.timestamp)} @ ${payload.bpm} bpm`
updateUiState()
}
@@ -1690,6 +1800,9 @@
stopPlayback()
log('已暂停回放')
})
if (elements.clearDebugLogBtn) {
elements.clearDebugLogBtn.addEventListener('click', clearDebugLog)
}
updateReadout()
setSocketBadge(false)
@@ -1715,4 +1828,6 @@
refreshGatewayBridgeStatus()
window.setInterval(refreshGatewayBridgeStatus, 3000)
connectSocket()
connectHeartRateSocket()
connectDebugSocket()
})()

View File

@@ -199,6 +199,20 @@ body {
white-space: pre-wrap;
}
.log--debug {
max-height: 280px;
background: #111917;
color: #d6f3df;
font-family: Consolas, "SFMono-Regular", monospace;
}
.log--floating {
min-height: 260px;
max-height: min(44vh, 420px);
font-size: 13px;
line-height: 1.6;
}
.jump-list {
display: flex;
flex-wrap: wrap;
@@ -232,6 +246,49 @@ body {
overflow: hidden;
}
.floating-debug-log {
position: absolute;
right: 20px;
bottom: 20px;
z-index: 600;
width: min(460px, calc(100vw - 480px));
min-width: 360px;
max-width: 520px;
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);
}
.floating-debug-log__header {
display: flex;
align-items: center;
justify-content: space-between;
gap: 12px;
margin-bottom: 10px;
}
.floating-debug-log__title {
font-size: 14px;
font-weight: 800;
letter-spacing: 0.08em;
color: #4a6a5e;
}
.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;
}
#map {
width: 100%;
height: 100vh;

View File

@@ -6,7 +6,9 @@ const { WebSocketServer } = WebSocket
const HOST = '0.0.0.0'
const PORT = 17865
const WS_PATH = '/mock-gps'
const GPS_WS_PATH = '/mock-gps'
const HEART_RATE_WS_PATH = '/mock-hr'
const DEBUG_LOG_WS_PATH = '/debug-log'
const PROXY_PATH = '/proxy'
const BRIDGE_STATUS_PATH = '/bridge-status'
const BRIDGE_CONFIG_PATH = '/bridge-config'
@@ -91,6 +93,14 @@ function isMockHeartRatePayload(payload) {
&& Number.isFinite(payload.bpm)
}
function isDebugLogPayload(payload) {
return payload
&& payload.type === 'debug-log'
&& typeof payload.scope === 'string'
&& typeof payload.level === 'string'
&& typeof payload.message === 'string'
}
async function handleProxyRequest(request, response) {
const requestUrl = new URL(request.url || '/', `http://127.0.0.1:${PORT}`)
const targetUrl = requestUrl.searchParams.get('url')
@@ -497,9 +507,11 @@ const server = http.createServer((request, response) => {
serveStatic(request.url || '/', response)
})
const wss = new WebSocketServer({ noServer: true })
const gpsWss = new WebSocketServer({ noServer: true })
const heartRateWss = new WebSocketServer({ noServer: true })
const debugLogWss = new WebSocketServer({ noServer: true })
wss.on('connection', (socket) => {
gpsWss.on('connection', (socket) => {
socket.on('message', (rawMessage) => {
const text = String(rawMessage)
let parsed
@@ -509,51 +521,126 @@ wss.on('connection', (socket) => {
return
}
if (!isMockGpsPayload(parsed) && !isMockHeartRatePayload(parsed)) {
if (!isMockGpsPayload(parsed)) {
return
}
const serialized = isMockGpsPayload(parsed)
? JSON.stringify({
type: 'mock_gps',
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
lat: Number(parsed.lat),
lon: Number(parsed.lon),
accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
})
: JSON.stringify({
type: 'mock_heart_rate',
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
bpm: Math.max(1, Math.round(Number(parsed.bpm))),
})
const outgoing = JSON.stringify({
type: 'mock_gps',
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
lat: Number(parsed.lat),
lon: Number(parsed.lon),
accuracyMeters: Number.isFinite(parsed.accuracyMeters) ? Number(parsed.accuracyMeters) : 6,
speedMps: Number.isFinite(parsed.speedMps) ? Number(parsed.speedMps) : 0,
headingDeg: Number.isFinite(parsed.headingDeg) ? Number(parsed.headingDeg) : 0,
})
gatewayBridge.publish(JSON.parse(outgoing))
gatewayBridge.publish(JSON.parse(serialized))
wss.clients.forEach((client) => {
gpsWss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(serialized)
client.send(outgoing)
}
})
})
})
heartRateWss.on('connection', (socket) => {
socket.on('message', (rawMessage) => {
const text = String(rawMessage)
let parsed
try {
parsed = JSON.parse(text)
} catch (_error) {
return
}
if (!isMockHeartRatePayload(parsed)) {
return
}
const outgoing = JSON.stringify({
type: 'mock_heart_rate',
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
bpm: Math.max(1, Math.round(Number(parsed.bpm))),
})
gatewayBridge.publish(JSON.parse(outgoing))
heartRateWss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(outgoing)
}
})
})
})
debugLogWss.on('connection', (socket) => {
socket.on('message', (rawMessage) => {
const text = String(rawMessage)
let parsed
try {
parsed = JSON.parse(text)
} catch (_error) {
return
}
if (!isDebugLogPayload(parsed)) {
return
}
const outgoing = JSON.stringify({
type: 'debug-log',
timestamp: Number.isFinite(parsed.timestamp) ? parsed.timestamp : Date.now(),
scope: String(parsed.scope || 'app').slice(0, 64),
level: parsed.level === 'warn' || parsed.level === 'error' ? parsed.level : 'info',
message: String(parsed.message || '').slice(0, 400),
...(parsed.payload && typeof parsed.payload === 'object'
? { payload: parsed.payload }
: {}),
})
debugLogWss.clients.forEach((client) => {
if (client.readyState === client.OPEN) {
client.send(outgoing)
}
})
})
})
server.on('upgrade', (request, socket, head) => {
if (!request.url || !request.url.startsWith(WS_PATH)) {
socket.destroy()
const requestUrl = request.url || ''
if (requestUrl.startsWith(GPS_WS_PATH)) {
gpsWss.handleUpgrade(request, socket, head, (ws) => {
gpsWss.emit('connection', ws, request)
})
return
}
wss.handleUpgrade(request, socket, head, (ws) => {
wss.emit('connection', ws, request)
})
if (requestUrl.startsWith(HEART_RATE_WS_PATH)) {
heartRateWss.handleUpgrade(request, socket, head, (ws) => {
heartRateWss.emit('connection', ws, request)
})
return
}
if (requestUrl.startsWith(DEBUG_LOG_WS_PATH)) {
debugLogWss.handleUpgrade(request, socket, head, (ws) => {
debugLogWss.emit('connection', ws, request)
})
return
}
if (!requestUrl) {
socket.destroy()
}
socket.destroy()
})
server.listen(PORT, HOST, () => {
console.log(`Mock GPS simulator running:`)
console.log(` UI: http://127.0.0.1:${PORT}/`)
console.log(` WS: ws://127.0.0.1:${PORT}${WS_PATH}`)
console.log(` GPS WS: ws://127.0.0.1:${PORT}${GPS_WS_PATH}`)
console.log(` HR WS: ws://127.0.0.1:${PORT}${HEART_RATE_WS_PATH}`)
console.log(` Logger WS: ws://127.0.0.1:${PORT}${DEBUG_LOG_WS_PATH}`)
console.log(` Proxy: http://127.0.0.1:${PORT}${PROXY_PATH}?url=<remote-url>`)
console.log(` Bridge status: http://127.0.0.1:${PORT}${BRIDGE_STATUS_PATH}`)
console.log(` Bridge config: http://127.0.0.1:${PORT}${BRIDGE_CONFIG_PATH}`)