217 lines
6.8 KiB
JavaScript
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();
|
||
|
|
});
|