252 lines
5.8 KiB
JavaScript
252 lines
5.8 KiB
JavaScript
const { app, BrowserWindow, dialog, ipcMain, Menu } = require('electron')
|
|
const { spawn } = require('child_process')
|
|
const fs = require('fs')
|
|
const http = require('http')
|
|
const path = require('path')
|
|
|
|
const BACKEND_HOST = 'localhost'
|
|
const BACKEND_PORT = 9996
|
|
const BACKEND_URL = `http://${BACKEND_HOST}:${BACKEND_PORT}`
|
|
const STARTUP_TIMEOUT_MS = 30000
|
|
|
|
let mainWindow = null
|
|
let backendProcess = null
|
|
let startedBackend = false
|
|
const projectionWindows = new Set()
|
|
|
|
function getSharedWebPreferences() {
|
|
return {
|
|
contextIsolation: true,
|
|
nodeIntegration: false,
|
|
sandbox: true,
|
|
preload: path.join(__dirname, 'preload.cjs'),
|
|
}
|
|
}
|
|
|
|
function isProjectionUrl(url) {
|
|
try {
|
|
return new URL(url).searchParams.get('screen') === 'counter'
|
|
} catch {
|
|
return false
|
|
}
|
|
}
|
|
|
|
function createProjectionWindow(url) {
|
|
const projectionWindow = new BrowserWindow({
|
|
width: 1280,
|
|
height: 720,
|
|
minWidth: 960,
|
|
minHeight: 540,
|
|
frame: false,
|
|
fullscreen: false,
|
|
movable: true,
|
|
resizable: true,
|
|
autoHideMenuBar: true,
|
|
title: '投屏计数',
|
|
backgroundColor: '#000000',
|
|
thickFrame: false,
|
|
webPreferences: getSharedWebPreferences(),
|
|
})
|
|
|
|
projectionWindows.add(projectionWindow)
|
|
hideMenuBar(projectionWindow)
|
|
projectionWindow.loadURL(url)
|
|
|
|
projectionWindow.on('closed', () => {
|
|
projectionWindows.delete(projectionWindow)
|
|
})
|
|
|
|
return projectionWindow
|
|
}
|
|
|
|
function hideMenuBar(win) {
|
|
win.setMenuBarVisibility(false)
|
|
win.removeMenu()
|
|
}
|
|
|
|
function attachWindowOpenHandler(win) {
|
|
win.webContents.setWindowOpenHandler(({ url }) => {
|
|
if (isProjectionUrl(url)) {
|
|
createProjectionWindow(url)
|
|
return { action: 'deny' }
|
|
}
|
|
|
|
return { action: 'allow' }
|
|
})
|
|
|
|
win.webContents.on('did-create-window', (childWindow) => {
|
|
hideMenuBar(childWindow)
|
|
})
|
|
}
|
|
|
|
function getBackendExecutablePath() {
|
|
if (app.isPackaged) {
|
|
return path.join(
|
|
process.resourcesPath,
|
|
'backend',
|
|
process.platform === 'win32' ? 'YD_TrainingCompetition_Api.exe' : 'YD_TrainingCompetition_Api',
|
|
)
|
|
}
|
|
|
|
return path.join(
|
|
__dirname,
|
|
'..',
|
|
'build',
|
|
'backend',
|
|
process.platform === 'win32' ? 'YD_TrainingCompetition_Api.exe' : 'YD_TrainingCompetition_Api',
|
|
)
|
|
}
|
|
|
|
function getFrontendIndexPath() {
|
|
return path.join(__dirname, '..', 'frontend', 'index.html')
|
|
}
|
|
|
|
function probeBackend() {
|
|
return new Promise((resolve) => {
|
|
const request = http.get(BACKEND_URL, (response) => {
|
|
response.resume()
|
|
resolve(true)
|
|
})
|
|
|
|
request.on('error', () => resolve(false))
|
|
request.setTimeout(1000, () => {
|
|
request.destroy()
|
|
resolve(false)
|
|
})
|
|
})
|
|
}
|
|
|
|
async function waitForBackend(timeoutMs = STARTUP_TIMEOUT_MS) {
|
|
const startedAt = Date.now()
|
|
|
|
while (Date.now() - startedAt < timeoutMs) {
|
|
if (await probeBackend()) {
|
|
return
|
|
}
|
|
|
|
await new Promise((resolve) => setTimeout(resolve, 300))
|
|
}
|
|
|
|
throw new Error(`后端服务启动超时:${BACKEND_URL}`)
|
|
}
|
|
|
|
async function startBackend() {
|
|
if (await probeBackend()) {
|
|
return
|
|
}
|
|
|
|
const backendPath = getBackendExecutablePath()
|
|
if (!fs.existsSync(backendPath)) {
|
|
throw new Error(`未找到后端程序:${backendPath}\n请先执行 npm run build:assets。`)
|
|
}
|
|
|
|
backendProcess = spawn(backendPath, [], {
|
|
cwd: path.dirname(backendPath),
|
|
env: {
|
|
...process.env,
|
|
ASPNETCORE_ENVIRONMENT: 'Production',
|
|
ASPNETCORE_URLS: BACKEND_URL,
|
|
},
|
|
stdio: ['ignore', 'pipe', 'pipe'],
|
|
windowsHide: true,
|
|
})
|
|
startedBackend = true
|
|
|
|
backendProcess.stdout.on('data', (data) => {
|
|
console.log(`[backend] ${data.toString().trimEnd()}`)
|
|
})
|
|
backendProcess.stderr.on('data', (data) => {
|
|
console.error(`[backend] ${data.toString().trimEnd()}`)
|
|
})
|
|
backendProcess.on('exit', (code, signal) => {
|
|
console.log(`[backend] exited code=${code} signal=${signal}`)
|
|
backendProcess = null
|
|
})
|
|
|
|
await waitForBackend()
|
|
}
|
|
|
|
function stopBackend() {
|
|
if (!startedBackend || !backendProcess || backendProcess.killed) {
|
|
return
|
|
}
|
|
|
|
if (process.platform === 'win32') {
|
|
spawn('taskkill', ['/pid', String(backendProcess.pid), '/f', '/t'], {
|
|
windowsHide: true,
|
|
stdio: 'ignore',
|
|
})
|
|
} else {
|
|
backendProcess.kill('SIGTERM')
|
|
}
|
|
|
|
backendProcess = null
|
|
}
|
|
|
|
async function createWindow() {
|
|
mainWindow = new BrowserWindow({
|
|
width: 1366,
|
|
height: 900,
|
|
minWidth: 1024,
|
|
minHeight: 700,
|
|
title: '赛事训练比赛系统',
|
|
autoHideMenuBar: true,
|
|
webPreferences: getSharedWebPreferences(),
|
|
})
|
|
|
|
hideMenuBar(mainWindow)
|
|
attachWindowOpenHandler(mainWindow)
|
|
|
|
const devServerUrl = process.env.ELECTRON_RENDERER_URL
|
|
if (!app.isPackaged && devServerUrl) {
|
|
await mainWindow.loadURL(devServerUrl)
|
|
mainWindow.webContents.openDevTools({ mode: 'detach' })
|
|
return
|
|
}
|
|
|
|
// 由后端通过 http 托管前端:绝对路径 /static、fetch、SignalR 与 API 均同源可用。
|
|
await mainWindow.loadURL(BACKEND_URL)
|
|
}
|
|
|
|
ipcMain.handle('dialog:pickFolder', async () => {
|
|
const targetWindow = mainWindow ?? BrowserWindow.getFocusedWindow()
|
|
const result = await dialog.showOpenDialog(targetWindow, {
|
|
title: '选择赛事文件夹',
|
|
properties: ['openDirectory'],
|
|
})
|
|
|
|
if (result.canceled || result.filePaths.length === 0) {
|
|
return null
|
|
}
|
|
|
|
return result.filePaths[0]
|
|
})
|
|
|
|
app.whenReady().then(async () => {
|
|
if (app.isPackaged) {
|
|
Menu.setApplicationMenu(null)
|
|
}
|
|
|
|
try {
|
|
await startBackend()
|
|
await createWindow()
|
|
} catch (error) {
|
|
dialog.showErrorBox('应用启动失败', error instanceof Error ? error.message : String(error))
|
|
app.quit()
|
|
}
|
|
|
|
app.on('activate', () => {
|
|
if (BrowserWindow.getAllWindows().length === 0) {
|
|
createWindow()
|
|
}
|
|
})
|
|
})
|
|
|
|
app.on('window-all-closed', () => {
|
|
if (process.platform !== 'darwin') {
|
|
app.quit()
|
|
}
|
|
})
|
|
|
|
app.on('before-quit', stopBackend)
|