重构模拟器工作台与日志浮层
This commit is contained in:
@@ -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('')
|
||||
|
||||
Reference in New Issue
Block a user