'use strict' const el = { version: document.getElementById('app-version'), stateDot: document.getElementById('state-dot'), stateLabel: document.getElementById('state-label'), stateDetail: document.getElementById('state-detail'), url: document.getElementById('k-url'), pid: document.getElementById('k-pid'), restarts: document.getElementById('k-restarts'), power: document.getElementById('k-power'), exe: document.getElementById('k-exe'), config: document.getElementById('k-config'), configWarning: document.getElementById('config-warning'), displayList: document.getElementById('display-list'), instanceList: document.getElementById('instance-list'), instancesSummary: document.getElementById('instances-summary'), log: document.getElementById('log'), autoscroll: document.getElementById('autoscroll'), btnStart: document.getElementById('btn-start'), btnStop: document.getElementById('btn-stop'), btnRestart: document.getElementById('btn-restart'), btnRelayout: document.getElementById('btn-relayout'), btnOpenAdmin: document.getElementById('btn-open-admin'), btnClearLog: document.getElementById('btn-clear-log'), btnResetAccount: document.getElementById('btn-reset-account'), acctUser: document.getElementById('acct-user'), acctNewUser: document.getElementById('acct-new-user'), acctPassword: document.getElementById('acct-password'), acctStatus: document.getElementById('acct-status'), acctOutput: document.getElementById('acct-output'), titlebar: document.getElementById('titlebar'), winMin: document.getElementById('win-min'), winMax: document.getElementById('win-max'), winClose: document.getElementById('win-close') } const DOT_CLASS = { running: 'ok', starting: 'warn', stopping: 'warn', crashed: 'err', stopped: '' } function renderStatus(status) { el.version.textContent = status.appVersion ? `v${status.appVersion}` : '' el.stateDot.className = `dot ${status.healthy ? 'ok' : DOT_CLASS[status.state] || ''}` el.stateLabel.textContent = status.stateLabel || status.state el.stateDetail.textContent = status.healthy ? '健康检查通过' : status.lastError || (status.state === 'stopped' ? '服务未运行' : '等待健康检查') el.url.textContent = status.url || '—' el.pid.textContent = status.pid || '—' el.restarts.textContent = status.restarts ? `${status.restarts} 次` : '0 次' el.power.textContent = status.powerSaveBlocked ? '已启用(阻止显示器休眠)' : '未启用' el.exe.textContent = status.exePath || '—' el.config.textContent = status.configPath || '—' if (status.configWarning) { el.configWarning.hidden = false el.configWarning.textContent = status.configWarning } else { el.configWarning.hidden = true } const busy = ['starting', 'running', 'stopping'].includes(status.state) el.btnStart.disabled = busy el.btnStop.disabled = !busy el.btnRestart.disabled = !busy renderDisplays(status.displays || []) } function renderDisplays(displays) { el.displayList.textContent = '' if (!displays.length) { const li = document.createElement('li') li.className = 'muted' li.textContent = '尚未打开大屏窗口' el.displayList.append(li) return } for (const item of displays) { const li = document.createElement('li') const page = document.createElement('span') page.className = 'page' page.textContent = `屏 ${item.displayId} · ${item.page}` const url = document.createElement('span') url.className = 'url' url.textContent = item.url || '(加载中)' li.append(page, url) el.displayList.append(li) } } function renderInstances(payload) { el.instanceList.textContent = '' if (!payload || !payload.ok) { el.instancesSummary.textContent = '' const li = document.createElement('li') li.className = 'muted' li.textContent = (payload && payload.message) || '暂时无法读取大屏实例' el.instanceList.append(li) return } const list = payload.instances || [] el.instancesSummary.textContent = `共 ${list.length} 个 · ${list.filter((item) => item.online).length} 在线` if (!list.length) { const li = document.createElement('li') li.className = 'muted' li.textContent = '当前没有大屏实例连接' el.instanceList.append(li) return } for (const item of list) { const li = document.createElement('li') const page = document.createElement('span') page.className = 'page' page.textContent = item.page || '未知页面' const id = document.createElement('span') id.className = 'url' id.textContent = item.id || '—' const state = document.createElement('span') state.className = `state ${item.online ? 'online' : 'offline'}` state.textContent = item.online ? '在线' : '离线' li.append(page, id, state) el.instanceList.append(li) } } function appendLog(line) { const atBottom = el.log.scrollTop + el.log.clientHeight >= el.log.scrollHeight - 8 el.log.textContent += `${line}\n` if (el.autoscroll.checked && atBottom) { el.log.scrollTop = el.log.scrollHeight } } function setLog(lines) { el.log.textContent = lines.length ? `${lines.join('\n')}\n` : '' el.log.scrollTop = el.log.scrollHeight } async function refresh() { renderStatus(await window.consoleApi.getStatus()) } function renderMaximized(maximized) { el.winMax.classList.toggle('is-maximized', maximized) const label = maximized ? '还原' : '最大化' el.winMax.title = label el.winMax.setAttribute('aria-label', label) } function bindWindowControls() { el.winMin.addEventListener('click', () => window.consoleApi.minimize()) el.winMax.addEventListener('click', () => window.consoleApi.maximize()) el.winClose.addEventListener('click', () => window.consoleApi.close()) // 双击标题栏切换最大化;点在按钮上的双击不算,否则连点会误触 el.titlebar.addEventListener('dblclick', (event) => { if (event.target instanceof Element && event.target.closest('.win-btn')) return window.consoleApi.maximize() }) window.consoleApi.onMaximizeChange(renderMaximized) window.consoleApi.isMaximized().then(renderMaximized) } function bindActions() { el.btnStart.addEventListener('click', async () => renderStatus(await window.consoleApi.startService())) el.btnStop.addEventListener('click', async () => renderStatus(await window.consoleApi.stopService())) el.btnRestart.addEventListener('click', async () => renderStatus(await window.consoleApi.restartService())) el.btnRelayout.addEventListener('click', async () => renderStatus(await window.consoleApi.relayoutDisplays())) el.btnClearLog.addEventListener('click', async () => { await window.consoleApi.clearLog() setLog([]) }) el.btnOpenAdmin.addEventListener('click', () => window.consoleApi.openServerAdmin()) el.btnResetAccount.addEventListener('click', resetAccount) } let accountStreaming = false function setAccountOutput(text) { el.acctOutput.hidden = !text el.acctOutput.textContent = text || '' if (text) el.acctOutput.scrollTop = el.acctOutput.scrollHeight } function appendAccountOutput(text) { el.acctOutput.hidden = false el.acctOutput.textContent += text el.acctOutput.scrollTop = el.acctOutput.scrollHeight } function handleAccountOutput(text) { // 只在提交中接收流式输出:重置结束后的最终结果由返回值统一渲染,避免重复 if (accountStreaming) appendAccountOutput(text) } function setAccountStatus(text, kind) { el.acctStatus.hidden = !text el.acctStatus.textContent = text || '' el.acctStatus.className = kind ? `hint ${kind}` : 'hint' } function setAccountBusy(busy) { el.btnResetAccount.disabled = busy el.btnResetAccount.textContent = busy ? '正在重置…' : '重置账号' for (const input of [el.acctUser, el.acctNewUser, el.acctPassword]) input.disabled = busy } async function resetAccount() { const currentUsername = el.acctUser.value.trim() const newUsername = el.acctNewUser.value.trim() const password = el.acctPassword.value if (!currentUsername) { setAccountStatus('请先填写当前用户名', 'err') return } const target = newUsername ? `用户名改为「${newUsername}」` : '只重置密码' // 重置会踢掉该用户的所有登录态,属于不可撤销操作,必须先二次确认 const confirmed = window.confirm( '确认重置管理员账号?\n\n' + `当前用户名:${currentUsername}\n` + `本次操作:${target}\n` + `新密码:${password ? '使用输入框中的密码' : '由程序随机生成'}\n\n` + '重置会作废该用户的全部登录会话,正在使用管理面板的人会被强制退出。' ) if (!confirmed) return accountStreaming = true setAccountBusy(true) setAccountOutput('') setAccountStatus('正在调用 scoreboard.exe passwd …') try { const result = await window.consoleApi.resetAdminAccount({ currentUsername, newUsername, password }) accountStreaming = false setAccountOutput(result.output || result.error || '') if (result.ok) { setAccountStatus( password ? '重置成功,原会话已全部作废。' : '重置成功,密码为随机生成,请从下方输出抄录后立即修改。', 'ok' ) el.acctUser.value = newUsername || currentUsername el.acctNewUser.value = '' el.acctPassword.value = '' } else { setAccountStatus(`重置失败:${result.error || `passwd 退出码 ${result.exitCode}`}`, 'err') } } catch (err) { setAccountStatus(`重置失败:${err.message}`, 'err') } finally { accountStreaming = false setAccountBusy(false) } } async function boot() { bindActions() bindWindowControls() window.consoleApi.onStatus(renderStatus) window.consoleApi.onLog(appendLog) window.consoleApi.onAccountOutput(handleAccountOutput) window.consoleApi.onDisplayInstances(renderInstances) setLog(await window.consoleApi.getLog()) await refresh() // 主进程只在窗口可见时轮询;这里补一次,保证窗口刚打开就有数据 renderInstances(await window.consoleApi.getDisplayInstances()) } boot()