重构模拟器工作台与日志浮层

This commit is contained in:
2026-03-30 19:19:31 +08:00
parent 24bc60bc7f
commit 1635a11780
10 changed files with 1349 additions and 225 deletions

View File

@@ -44,6 +44,7 @@
debugSocket: null,
connected: false,
heartRateConnected: false,
debugConnected: false,
socketConnecting: false,
heartRateSocketConnecting: false,
debugSocketConnecting: false,
@@ -74,6 +75,9 @@
bridgeLastStatusText: '--',
bridgeConfigSaving: false,
bridgePresets: [],
debugLogEntries: [],
debugLogScopeFilter: 'all',
debugLogPanelMinimized: false,
}
const elements = {
@@ -143,7 +147,20 @@
pathCountText: document.getElementById('pathCountText'),
log: document.getElementById('log'),
debugLog: document.getElementById('debugLog'),
debugLogMeta: document.getElementById('debugLogMeta'),
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
@@ -165,22 +182,79 @@
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')
const normalized = {
timestamp: entry.timestamp || Date.now(),
scope: String(entry.scope || 'app'),
level: String(entry.level || 'info'),
message: String(entry.message || ''),
payload: entry.payload && typeof entry.payload === 'object' ? entry.payload : null,
}
state.debugLogEntries.unshift(normalized)
if (state.debugLogEntries.length > MAX_DEBUG_LOG_LINES) {
state.debugLogEntries = state.debugLogEntries.slice(0, MAX_DEBUG_LOG_LINES)
}
renderDebugScopeOptions()
renderDebugLog()
}
function clearDebugLog() {
if (elements.debugLog) {
elements.debugLog.textContent = ''
state.debugLogEntries = []
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) {
@@ -206,6 +280,19 @@
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) {
if (!timestamp) {
return '--'
@@ -304,6 +391,43 @@
} else {
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) {
@@ -587,6 +711,7 @@
const socket = new WebSocket(DEBUG_LOG_WS_URL)
state.debugSocket = socket
state.debugConnected = false
state.debugSocketConnecting = true
log(`连接日志通道 ${DEBUG_LOG_WS_URL}`)
@@ -605,20 +730,27 @@
socket.addEventListener('open', () => {
state.debugSocketConnecting = false
state.debugSocket = socket
state.debugConnected = true
log('日志通道已连接')
updateUiState()
})
socket.addEventListener('close', () => {
state.debugSocketConnecting = false
state.debugSocket = null
state.debugConnected = false
log('日志通道已断开')
updateUiState()
window.setTimeout(connectDebugSocket, 1500)
})
socket.addEventListener('error', () => {
state.debugSocketConnecting = false
state.debugSocket = null
state.debugConnected = false
log('日志通道连接失败')
updateUiState()
})
}
@@ -1803,9 +1935,24 @@
if (elements.clearDebugLogBtn) {
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()
setSocketBadge(false)
renderDebugScopeOptions()
renderDebugLog()
updateDebugLogPanelState()
setResourceStatus('支持直接载入 game.json也支持单独填瓦片模板和 KML 地址。', null)
state.bridgePresets = loadBridgePresets()
renderBridgePresetOptions('')