const { app, BrowserWindow, ipcMain, net, protocol, screen, shell } = require('electron'); const fs = require('node:fs'); const path = require('node:path'); const { pathToFileURL } = require('node:url'); const APP_SCHEME = 'ydevent'; const API_ENVIRONMENTS = { test: { label: '测试服', origin: process.env.ELECTRON_TEST_API_URL || 'http://8.153.108.90:9996', }, production: { label: '正式服', origin: process.env.ELECTRON_PRODUCTION_API_URL || process.env.ELECTRON_API_URL || 'https://event.yuedongsports.com', }, }; let selectedApiEnvironment = process.env.ELECTRON_API_ENV === 'test' ? 'test' : 'production'; let mainWindow = null; let presentationWindow = null; const environmentConfigPath = () => path.join(app.getPath('userData'), 'environment.json'); const loadApiEnvironment = () => { try { const saved = JSON.parse(fs.readFileSync(environmentConfigPath(), 'utf8')); if (API_ENVIRONMENTS[saved.environment]) selectedApiEnvironment = saved.environment; } catch { // 首次运行时使用默认环境。 } }; const saveApiEnvironment = (environment) => { fs.mkdirSync(path.dirname(environmentConfigPath()), { recursive: true }); fs.writeFileSync(environmentConfigPath(), JSON.stringify({ environment }, null, 2), 'utf8'); }; const apiEnvironmentState = () => ({ current: selectedApiEnvironment, environments: Object.entries(API_ENVIRONMENTS).map(([value, item]) => ({ value, label: item.label, origin: item.origin })), }); protocol.registerSchemesAsPrivileged([ { scheme: APP_SCHEME, privileges: { standard: true, secure: true, supportFetchAPI: true, corsEnabled: true, stream: true, }, }, ]); const registerAppProtocol = () => { const rendererDirectory = path.resolve(__dirname, '..', 'dist'); protocol.handle(APP_SCHEME, async (request) => { const requestUrl = new URL(request.url); if (requestUrl.pathname === '/api' || requestUrl.pathname.startsWith('/api/')) { const apiOrigin = API_ENVIRONMENTS[selectedApiEnvironment].origin; const targetUrl = new URL(`${requestUrl.pathname}${requestUrl.search}`, apiOrigin); const options = { method: request.method, headers: request.headers, }; if (request.method !== 'GET' && request.method !== 'HEAD') { options.body = request.body; options.duplex = 'half'; } return net.fetch(targetUrl.toString(), options); } const pathname = decodeURIComponent(requestUrl.pathname); const requestedPath = pathname === '/' ? 'index.html' : pathname.replace(/^\/+/, ''); let filePath = path.resolve(rendererDirectory, requestedPath); if (!filePath.startsWith(`${rendererDirectory}${path.sep}`) && filePath !== rendererDirectory) { return new Response('Not found', { status: 404 }); } if (!path.extname(filePath)) { filePath = path.join(rendererDirectory, 'index.html'); } return net.fetch(pathToFileURL(filePath).toString()); }); }; const targetPresentationDisplay = () => { const displays = screen.getAllDisplays(); if (!mainWindow || displays.length < 2) return displays[0] || screen.getPrimaryDisplay(); const operatorDisplay = screen.getDisplayMatching(mainWindow.getBounds()); return displays.find((display) => display.id !== operatorDisplay.id) || operatorDisplay; }; const positionPresentationWindow = () => { if (!presentationWindow || presentationWindow.isDestroyed()) return; const targetDisplay = targetPresentationDisplay(); const currentDisplay = screen.getDisplayMatching(presentationWindow.getBounds()); if (currentDisplay.id !== targetDisplay.id) { presentationWindow.setFullScreen(false); presentationWindow.setBounds(targetDisplay.bounds); } if (!presentationWindow.isFullScreen()) presentationWindow.setFullScreen(true); }; const openPresentationWindow = async (rawUrl) => { const baseUrl = mainWindow?.webContents.getURL() || `${APP_SCHEME}://app/index.html`; const targetUrl = new URL(rawUrl, baseUrl); const isAppUrl = targetUrl.protocol === `${APP_SCHEME}:`; const isDevUrl = !app.isPackaged && ['127.0.0.1', 'localhost'].includes(targetUrl.hostname); if (!isAppUrl && !isDevUrl) throw new Error('不允许在投屏窗口中打开外部地址'); if (!presentationWindow || presentationWindow.isDestroyed()) { const display = targetPresentationDisplay(); presentationWindow = new BrowserWindow({ x: display.bounds.x, y: display.bounds.y, width: display.bounds.width, height: display.bounds.height, frame: false, fullscreen: true, autoHideMenuBar: true, show: false, backgroundColor: '#020936', webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); presentationWindow.on('closed', () => { presentationWindow = null; }); presentationWindow.once('ready-to-show', () => presentationWindow?.showInactive()); } else { positionPresentationWindow(); } await presentationWindow.loadURL(targetUrl.toString()); presentationWindow.setFullScreen(true); presentationWindow.showInactive(); return { opened: true, displayId: targetPresentationDisplay().id }; }; const registerIpcHandlers = () => { ipcMain.handle('app:get-api-environment', () => apiEnvironmentState()); ipcMain.handle('app:set-api-environment', (_event, environment) => { if (!API_ENVIRONMENTS[environment]) throw new Error('未知接口环境'); selectedApiEnvironment = environment; saveApiEnvironment(environment); if (presentationWindow && !presentationWindow.isDestroyed()) presentationWindow.reload(); return apiEnvironmentState(); }); ipcMain.handle('presentation:open', (_event, url) => openPresentationWindow(url)); }; const createWindow = () => { mainWindow = new BrowserWindow({ width: 1440, height: 900, minWidth: 1100, minHeight: 700, show: false, autoHideMenuBar: true, icon: path.join(__dirname, '..', 'public', 'app.ico'), webPreferences: { preload: path.join(__dirname, 'preload.cjs'), contextIsolation: true, nodeIntegration: false, sandbox: true, }, }); mainWindow.once('ready-to-show', () => mainWindow?.show()); mainWindow.webContents.setWindowOpenHandler(({ url }) => { if (url.startsWith('http://') || url.startsWith('https://')) { void shell.openExternal(url); } return { action: 'deny' }; }); if (app.isPackaged) { void mainWindow.loadURL(`${APP_SCHEME}://app/index.html`); } else { void mainWindow.loadURL(process.env.VITE_DEV_SERVER_URL || 'http://127.0.0.1:9991'); mainWindow.webContents.openDevTools({ mode: 'detach' }); } mainWindow.on('closed', () => { if (presentationWindow && !presentationWindow.isDestroyed()) presentationWindow.close(); mainWindow = null; }); }; app.whenReady().then(() => { loadApiEnvironment(); registerAppProtocol(); registerIpcHandlers(); createWindow(); app.on('activate', () => { if (BrowserWindow.getAllWindows().length === 0) createWindow(); }); }); app.on('window-all-closed', () => { if (process.platform !== 'darwin') app.quit(); });