完善样式系统与调试链路底座
This commit is contained in:
@@ -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>
|
||||
|
||||
|
||||
@@ -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()
|
||||
})()
|
||||
|
||||
@@ -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;
|
||||
|
||||
Reference in New Issue
Block a user