import { app, BrowserWindow, Event, Input, ipcMain, Menu, WebContentsView } from 'electron' import { fileURLToPath } from 'node:url' import path from 'node:path' import fs from 'node:fs' const __dirname = path.dirname(fileURLToPath(import.meta.url)) // The built directory structure // // ├─┬─┬ dist // │ │ └── index.html // │ │ // │ ├─┬ dist-electron // │ │ ├── main.js // │ │ └── preload.mjs // │ process.env.APP_ROOT = path.join(__dirname, '..') // 🚧 Use ['ENV_NAME'] avoid vite:define plugin - Vite@2.x export const VITE_DEV_SERVER_URL = process.env['VITE_DEV_SERVER_URL'] export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron') export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist') process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL ? path.join(process.env.APP_ROOT, 'public') : RENDERER_DIST let mainWindow: BrowserWindow | null let childView: WebContentsView | null let isSideOpen = true let childViewAspectRatio = 0; const SIDE_WIDTH = 250 const EXPAND_VIEW_SIZE = 40 const LOADING_VIEW_WIDTH = 150 const LOADING_VIEW_HEIGHT = 100 function loadWindowPage(window: BrowserWindow, subPath: string) { if (VITE_DEV_SERVER_URL) { window.loadURL(VITE_DEV_SERVER_URL + "#" + subPath) } else { window.loadFile(path.join(RENDERER_DIST, 'index.html') + "#" + subPath) } } function loadViewUrl(view: WebContentsView, subPath: string) { if (!mainWindow) { return } if (VITE_DEV_SERVER_URL) { view.webContents.loadURL(VITE_DEV_SERVER_URL + "#" + subPath) } else { view.webContents.loadFile(path.join(RENDERER_DIST, 'index.html') + "#" + subPath) } } Menu.setApplicationMenu(null) function createWindow() { mainWindow = new BrowserWindow({ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, allowRunningInsecureContent: true, partition: 'persist:minnan-demo-app', }, width: 1200, height: 800, }) childView = new WebContentsView({ webPreferences: { partition: 'persist:minnan-demo-app', allowRunningInsecureContent: true, enableBlinkFeatures: 'PasswordManager', }, }) const expandButtonView = new WebContentsView({ webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, }, }) const loadingView = new BrowserWindow({ skipTaskbar: true, width: LOADING_VIEW_WIDTH, height: LOADING_VIEW_HEIGHT, parent: mainWindow, thickFrame: true, titleBarStyle: 'hidden', webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, }, }) mainWindow.contentView.addChildView(childView) mainWindow.contentView.addChildView(expandButtonView) expandButtonView.setVisible(false) childView.webContents.on('did-start-loading', () => { loadingView.show() }); childView.webContents.on('did-stop-loading', () => { loadingView.hide() }); childView.webContents.on('did-fail-load', (_, errorCode, errorDescription) => { loadingView.hide() loadWindowPage(loadingView, '/error?code=' + errorCode + '&message=' + errorDescription); }); function updateChildWindowBounds() { const bounds = mainWindow!.getBounds(); expandButtonView.setBounds({ x: 0, y: bounds.height - 100, width: EXPAND_VIEW_SIZE, height: EXPAND_VIEW_SIZE }) loadingView.setBounds({ x: bounds.x + (bounds.width - LOADING_VIEW_WIDTH) / 2, y: bounds.y + (bounds.height - LOADING_VIEW_HEIGHT) / 2, width: LOADING_VIEW_WIDTH, height: LOADING_VIEW_HEIGHT }) if (childViewAspectRatio) { // 保持子页纵横比 const rect = { x: isSideOpen ? SIDE_WIDTH : 0, y: 0, width: bounds.width - (isSideOpen ? SIDE_WIDTH : 0), height: bounds.height }; // 计算子窗口的最佳尺寸(contain模式) const availableRatio = rect.width / rect.height; let childWidth, childHeight; if (availableRatio > childViewAspectRatio) { // 高度受限制 childHeight = rect.height; childWidth = childHeight * childViewAspectRatio; } else { // 宽度受限制 childWidth = rect.width; childHeight = childWidth / childViewAspectRatio; } // 计算居中位置 const childX = rect.x + (rect.width - childWidth) / 2; const childY = rect.y + (rect.height - childHeight) / 2; // 应用到子窗口 childView!.setBounds({ x: Math.round(childX), y: Math.round(childY), width: Math.round(childWidth), height: Math.round(childHeight) }); } else { childView!.setBounds({ x: isSideOpen ? SIDE_WIDTH : 0, y: 0, width: bounds.width - (isSideOpen ? SIDE_WIDTH : 0), height: bounds.height }); } } loadWindowPage(mainWindow, '/'); loadWindowPage(loadingView, '/loading'); loadViewUrl(childView, '/hello'); loadViewUrl(expandButtonView, '/expand'); updateChildWindowBounds(); mainWindow.on('resize', () => { updateChildWindowBounds() }) mainWindow.webContents.on('did-finish-load', () => { mainWindow?.webContents.send('main-process-message', (new Date).toLocaleString()) }) function handleWindowFullScreenKeys(event: Event, input: Input) { if (input.key === 'F11' && input.type === 'keyDown') { event.preventDefault(); mainWindow?.setFullScreen(!mainWindow?.isFullScreen()) } else if (input.key === 'F12' && input.type === 'keyDown') { event.preventDefault(); mainWindow?.webContents.toggleDevTools(); } } // 添加F11全屏切换功能 mainWindow.webContents.on('before-input-event', (event, input) => { handleWindowFullScreenKeys(event, input) }) childView.webContents.on('before-input-event', (event, input) => { handleWindowFullScreenKeys(event, input) }) // 处理退出应用事件 ipcMain.on('exit-app', () => { app.quit() }) // 处理全屏切换事件 ipcMain.on('toggle-fullscreen', (_event, isFullScreen: boolean) => { if (mainWindow) { if (isFullScreen) { mainWindow.setFullScreen(true) } else { mainWindow.setFullScreen(false) } } }) // 加载子页URL ipcMain.on('load-child-url', (_event, url: string, aspectRatio: number) => { if (childView) childView.webContents.loadURL(url) childViewAspectRatio = aspectRatio updateChildWindowBounds() }) // 子页侧边栏开关 ipcMain.on('toggle-child-side', (_event, value: boolean) => { isSideOpen = value expandButtonView.setVisible(!isSideOpen) mainWindow?.webContents.send('main-side-state-changed', isSideOpen) updateChildWindowBounds() }) // 处理获取应用路径事件 ipcMain.handle('get-app-path', () => { return app.getAppPath() }) // 处理打开窗口事件 ipcMain.on('open-window', (_event, url: string) => { const newWin = new BrowserWindow({ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, allowRunningInsecureContent: true, }, fullscreenable: true, maximizable: true, width: 1200, height: 800, }) newWin.loadURL(url) newWin.maximize(); // 添加F11全屏切换功能 newWin.webContents.on('before-input-event', (event, input) => { if (input.key === 'F11' && input.type === 'keyDown') { event.preventDefault(); newWin?.setFullScreen(!newWin?.fullScreen) } else if (input.key === 'F12' && input.type === 'keyDown') { event.preventDefault(); newWin?.webContents.toggleDevTools(); } }) }) // 处理加载apps.json事件 ipcMain.handle('load-apps-json', async () => { const appPath = process.cwd() const appsJsonPath = path.join(appPath, 'apps.json') try { if (fs.existsSync(appsJsonPath)) { const data = fs.readFileSync(appsJsonPath, 'utf8') return JSON.parse(data) } else { // 开发环境下回退到public目录 const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json') if (fs.existsSync(devAppsJsonPath)) { const data = fs.readFileSync(devAppsJsonPath, 'utf8') return JSON.parse(data) } throw new Error('apps.json not found') } } catch (error) { console.error('Error loading apps.json:', error) throw error } }) // 处理加载默认apps.json事件 ipcMain.handle('load-default-apps-json', async () => { const devAppsJsonPath = path.join(process.env.VITE_PUBLIC || '', 'apps.json') if (fs.existsSync(devAppsJsonPath)) { const data = fs.readFileSync(devAppsJsonPath, 'utf8') return JSON.parse(data) } throw new Error('apps.json not found') }) // 处理显示配置窗口事件 ipcMain.on('show-config', () => { const configWindow = new BrowserWindow({ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, allowRunningInsecureContent: true, }, title: '列表配置', parent: mainWindow || undefined, skipTaskbar: true, minimizable: false, maximizable: false, modal: true, width: 800, height: 600, }) loadWindowPage(configWindow, '/config') }); // 处理保存apps.json事件 ipcMain.on('save-apps-json', (_event, appsJson: string) => { const appPath = process.cwd() const appsJsonPath = path.join(appPath, 'apps.json') try { fs.writeFileSync(appsJsonPath, appsJson) mainWindow?.webContents.send('main-config-changed') } catch (error) { console.error('Error saving apps.json:', error) throw error } }) // 处理显示关于窗口事件 ipcMain.on('show-about', () => { const aboutWindow = new BrowserWindow({ icon: path.join(process.env.VITE_PUBLIC, 'icon.ico'), webPreferences: { preload: path.join(__dirname, 'preload.mjs'), contextIsolation: true, allowRunningInsecureContent: true, }, parent: mainWindow || undefined, title: '关于程序', skipTaskbar: true, maximizable: false, minimizable: false, modal: true, width: 450, height: 470, }) loadWindowPage(aboutWindow, '/about') }); } // Quit when all mainWindowdows are closed, except on macOS. There, it's common // for applications and their menu bar to stay active until the user quits // explicitly with Cmd + Q. app.on('window-all-closed', () => { if (process.platform !== 'darwin') { app.quit() mainWindow = null } }) app.on('activate', () => { // On OS X it's common to re-create a mainWindowdow in the app when the // dock icon is clicked and there are no other mainWindowdows open. if (BrowserWindow.getAllWindows().length === 0) { createWindow() } }) app.whenReady().then(createWindow)