diff --git a/console/main/index.js b/console/main/index.js index ec7eb22..c7ba27b 100644 --- a/console/main/index.js +++ b/console/main/index.js @@ -30,6 +30,7 @@ function main() { let service = null let displays = null let adminWindow = null + let serverAdminWindow = null let tray = null let powerBlockerId = null let displayFallbackTimer = null @@ -90,6 +91,9 @@ function main() { title: 'Scoreboard Console · 启动管理', backgroundColor: '#0f1420', show: false, + // 标题栏由页面自绘;titleBarStyle 只是兜底,防止某些平台忽略 frame:false + frame: false, + titleBarStyle: 'hidden', webPreferences: { preload: path.join(__dirname, '..', 'renderer', 'preload.js'), contextIsolation: true, @@ -98,6 +102,7 @@ function main() { } }) + bindWindowStateEvents(adminWindow) adminWindow.loadFile(path.join(__dirname, '..', 'renderer', 'index.html')) adminWindow.once('ready-to-show', () => adminWindow.show()) @@ -114,6 +119,79 @@ function main() { }) } + function adminPanelUrl() { + // 复用服务端的地址推导:host 配成 0.0.0.0 时本机也要走回环 + if (service) return service.status.adminUrl + const host = (cfg.values.server.host || '127.0.0.1') === '0.0.0.0' ? '127.0.0.1' : cfg.values.server.host + return `http://${host || '127.0.0.1'}:${cfg.values.server.port}/admin` + } + + function showServerAdmin() { + if (!serverAdminWindow || serverAdminWindow.isDestroyed()) { + createServerAdminWindow() + return + } + if (serverAdminWindow.isMinimized()) serverAdminWindow.restore() + serverAdminWindow.show() + serverAdminWindow.focus() + } + + // 服务端管理面板是 Web 页面,用无边框 Electron 窗口承载,省得现场再开浏览器 + function createServerAdminWindow() { + const { width, height } = cfg.values.admin + serverAdminWindow = new BrowserWindow({ + width: Math.max(width, 1100), + height, + minWidth: 900, + minHeight: 620, + title: 'Scoreboard · 服务端管理面板', + backgroundColor: '#161616', + show: false, + frame: false, + titleBarStyle: 'hidden', + webPreferences: { + preload: path.join(__dirname, '..', 'renderer', 'shell-preload.js'), + contextIsolation: true, + nodeIntegration: false, + sandbox: true + } + }) + + bindWindowStateEvents(serverAdminWindow) + + const url = adminPanelUrl() + log.push('console', `打开服务端管理面板:${url}`) + serverAdminWindow.loadURL(url).catch((err) => { + log.push('console', `服务端管理面板加载失败:${err.message}`) + }) + + // 加载失败时 ready-to-show 不会触发,兜底把窗口显示出来,否则用户看不到任何反馈 + const reveal = () => { + if (serverAdminWindow && !serverAdminWindow.isDestroyed()) { + serverAdminWindow.show() + serverAdminWindow.focus() + } + } + serverAdminWindow.once('ready-to-show', reveal) + serverAdminWindow.webContents.on('did-fail-load', (_event, _code, _desc, _url, isMainFrame) => { + if (isMainFrame) reveal() + }) + + serverAdminWindow.on('closed', () => { + serverAdminWindow = null + }) + } + + // 渲染层要随时知道最大化状态,才能在「最大化/还原」之间换图标 + function bindWindowStateEvents(win) { + const notify = () => { + if (win.isDestroyed()) return + win.webContents.send('window:maximize-changed', win.isMaximized()) + } + win.on('maximize', notify) + win.on('unmaximize', notify) + } + function ensureDisplays() { if (displaysEnabled || !displays) return displaysEnabled = true @@ -138,10 +216,7 @@ function main() { { label: '重启服务', enabled: busy, click: () => service.restart() }, { type: 'separator' }, { label: '打开管理端', click: () => showAdmin() }, - { - label: '打开服务端管理面板', - click: () => shell.openExternal(status.adminUrl || `http://127.0.0.1:${cfg.values.server.port}/admin`) - }, + { label: '打开服务端管理面板', click: () => showServerAdmin() }, { type: 'separator' }, { label: '退出', @@ -202,6 +277,35 @@ function main() { showAdmin() return true }) + ipcMain.handle('app:open-server-admin', () => { + showServerAdmin() + return true + }) + + // 窗口控制统一从 event.sender 反查窗口,多个窗口共用同一套通道 + ipcMain.handle('window:minimize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win && !win.isDestroyed()) win.minimize() + return true + }) + ipcMain.handle('window:maximize', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (!win || win.isDestroyed()) return false + // 以「切换」语义实现,避免按钮在已最大化时点了没反应 + const next = !win.isMaximized() + if (next) win.maximize() + else win.unmaximize() + return next + }) + ipcMain.handle('window:close', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + if (win && !win.isDestroyed()) win.close() + return true + }) + ipcMain.handle('window:is-maximized', (event) => { + const win = BrowserWindow.fromWebContents(event.sender) + return Boolean(win && !win.isDestroyed() && win.isMaximized()) + }) } app.on('second-instance', () => showAdmin()) diff --git a/console/renderer/app.js b/console/renderer/app.js index a83622b..aa953f1 100644 --- a/console/renderer/app.js +++ b/console/renderer/app.js @@ -20,7 +20,11 @@ const el = { btnRestart: document.getElementById('btn-restart'), btnRelayout: document.getElementById('btn-relayout'), btnOpenAdmin: document.getElementById('btn-open-admin'), - btnClearLog: document.getElementById('btn-clear-log') + btnClearLog: document.getElementById('btn-clear-log'), + titlebar: document.getElementById('titlebar'), + winMin: document.getElementById('win-min'), + winMax: document.getElementById('win-max'), + winClose: document.getElementById('win-close') } const DOT_CLASS = { @@ -31,10 +35,7 @@ const DOT_CLASS = { stopped: '' } -let lastStatus = null - function renderStatus(status) { - lastStatus = status el.version.textContent = status.appVersion ? `v${status.appVersion}` : '' el.stateDot.className = `dot ${status.healthy ? 'ok' : DOT_CLASS[status.state] || ''}` @@ -104,6 +105,28 @@ 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())) @@ -113,14 +136,12 @@ function bindActions() { await window.consoleApi.clearLog() setLog([]) }) - el.btnOpenAdmin.addEventListener('click', () => { - const url = (lastStatus && lastStatus.adminUrl) || 'http://127.0.0.1:8080/admin' - window.consoleApi.openExternal(url) - }) + el.btnOpenAdmin.addEventListener('click', () => window.consoleApi.openServerAdmin()) } async function boot() { bindActions() + bindWindowControls() window.consoleApi.onStatus(renderStatus) window.consoleApi.onLog(appendLog) setLog(await window.consoleApi.getLog()) diff --git a/console/renderer/index.html b/console/renderer/index.html index 7ed94d4..23add5d 100644 --- a/console/renderer/index.html +++ b/console/renderer/index.html @@ -10,7 +10,8 @@
-