2026-09-18 15:31:01 +08:00

217 lines
6.8 KiB
JavaScript

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();
});