feat: electron support

This commit is contained in:
Netfan
2025-03-24 23:51:31 +08:00
parent 329a176a5c
commit 7536be5e49
22 changed files with 1945 additions and 11 deletions

1
.gitignore vendored
View File

@@ -5,6 +5,7 @@ dist-ssr
dist.zip
dist.tar
dist.war
dist-electron
.nitro
.output
*-dist.zip

1
.npmrc
View File

@@ -1,4 +1,5 @@
registry = "https://registry.npmmirror.com"
ELECTRON_MIRROR="https://npmmirror.com/mirrors/electron/"
public-hoist-pattern[]=husky
public-hoist-pattern[]=eslint
public-hoist-pattern[]=prettier

View File

@@ -47,12 +47,15 @@
"@vitejs/plugin-vue-jsx": "catalog:",
"dayjs": "catalog:",
"dotenv": "catalog:",
"electron": "catalog:",
"rollup": "catalog:",
"rollup-plugin-visualizer": "catalog:",
"sass": "catalog:",
"vite": "catalog:",
"vite-plugin-compression": "catalog:",
"vite-plugin-dts": "catalog:",
"vite-plugin-electron": "catalog:",
"vite-plugin-electron-renderer": "catalog:",
"vite-plugin-html": "catalog:",
"vite-plugin-lazy-import": "catalog:"
}

View File

@@ -0,0 +1,40 @@
import type { PluginOption } from 'vite';
import type { CommonPluginOptions } from '../typing';
import fs from 'node:fs';
import electron from 'vite-plugin-electron/simple';
export const viteElectronPlugin = (
options: CommonPluginOptions,
): PluginOption => {
fs.rmSync('dist-electron', { force: true, recursive: true });
const isServe = !options.isBuild;
const isBuild = options.isBuild;
const sourcemap = isServe || !!process.env.VSCODE_DEBUG;
return electron({
main: {
entry: 'electron/main.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/main',
sourcemap,
},
},
},
preload: {
input: 'electron/preload.ts',
vite: {
build: {
minify: isBuild,
outDir: 'dist-electron/preload',
sourcemap: sourcemap ? 'inline' : undefined, // #332
},
},
},
renderer: {},
});
};

View File

@@ -18,6 +18,7 @@ import { VitePWA } from 'vite-plugin-pwa';
import viteVueDevTools from 'vite-plugin-vue-devtools';
import { viteArchiverPlugin } from './archiver';
import { viteElectronPlugin } from './electron';
import { viteExtraAppConfigPlugin } from './extra-app-config';
import { viteImportMapPlugin } from './importmap';
import { viteInjectAppLoadingPlugin } from './inject-app-loading';
@@ -97,6 +98,7 @@ async function loadApplicationPlugins(
archiverPluginOptions,
compress,
compressTypes,
electron,
extraAppConfig,
html,
i18n,
@@ -213,6 +215,10 @@ async function loadApplicationPlugins(
return [await viteArchiverPlugin(archiverPluginOptions)];
},
},
{
condition: electron,
plugins: () => [viteElectronPlugin(commonOptions)],
},
]);
}

View File

@@ -95,6 +95,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
compress?: boolean;
/** 压缩类型 */
compressTypes?: ('brotli' | 'gzip')[];
/** 启用electron */
electron?: boolean;
/** 在构建的时候抽离配置文件 */
extraAppConfig?: boolean;
/** 是否开启html插件 */

View File

@@ -98,6 +98,8 @@
"@types/lodash.get": "catalog:",
"@types/lodash.isequal": "catalog:",
"@types/lodash.set": "catalog:",
"@types/nprogress": "catalog:"
"@types/nprogress": "catalog:",
"@vben-core/typings": "workspace:*",
"electron": "catalog:"
}
}

View File

@@ -28,10 +28,11 @@ function openWindow(url: string, options: OpenWindowOptions = {}): void {
* @param path
*/
function openRouteInNewWindow(path: string) {
const { hash, origin } = location;
// const { hash, origin } = location;
const fullPath = path.startsWith('/') ? path : `/${path}`;
const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
openWindow(url, { target: '_blank' });
// const url = `${origin}${hash ? '/#' : ''}${fullPath}`;
// openWindow(url, { target: '_blank' });
window.ipcRenderer.invoke('open-win', fullPath);
}
export { openRouteInNewWindow, openWindow };

View File

@@ -1,6 +1,9 @@
{
"$schema": "https://json.schemastore.org/tsconfig",
"extends": "@vben/tsconfig/library.json",
"compilerOptions": {
"types": ["@vben-core/typings/electron"]
},
"include": ["src"],
"exclude": ["node_modules"]
}

View File

@@ -0,0 +1,22 @@
import type { IpcRendererEvent } from 'electron';
export type IpcRendererInvoke = 'open-win';
declare global {
interface Window {
ipcRenderer: {
invoke: (channel: IpcRendererInvoke, ...args: any[]) => Promise<any>;
off: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
on: (
channel: string,
listener: (event: IpcRendererEvent, ...args: any[]) => void,
) => void;
send: (channel: string, data: any) => Promise<any>;
};
}
}
export {};

View File

@@ -27,6 +27,9 @@
},
"./vue-router": {
"types": "./vue-router.d.ts"
},
"./electron": {
"types": "./electron.d.ts"
}
},
"publishConfig": {

View File

@@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin
# 应用命名空间用于缓存、store等功能的前缀确保隔离
VITE_APP_NAMESPACE=vben-web-play
# vue-router 的模式
VITE_ROUTER_HISTORY=hash

View File

@@ -1,4 +1,4 @@
VITE_BASE=/
VITE_BASE=./
# 接口地址
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

149
playground/electron/main.ts Normal file
View File

@@ -0,0 +1,149 @@
import os from 'node:os';
import path from 'node:path';
import process from 'node:process';
import { fileURLToPath } from 'node:url';
import {
app,
BrowserWindow,
globalShortcut,
ipcMain,
Menu,
shell,
} from 'electron';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
process.env.APP_ROOT = path.join(__dirname, '../..');
export const MAIN_DIST = path.join(process.env.APP_ROOT, 'dist-electron');
export const RENDERER_DIST = path.join(process.env.APP_ROOT, 'dist');
export const VITE_DEV_SERVER_URL = process.env.VITE_DEV_SERVER_URL;
process.env.VITE_PUBLIC = VITE_DEV_SERVER_URL
? path.join(process.env.APP_ROOT, 'public')
: RENDERER_DIST;
// Disable GPU Acceleration for Windows 7
if (os.release().startsWith('6.1')) app.disableHardwareAcceleration();
// Set application name for Windows 10+ notifications
if (process.platform === 'win32') app.setAppUserModelId(app.getName());
if (!app.requestSingleInstanceLock()) {
app.quit();
}
let win: BrowserWindow | null = null;
const preload = path.join(__dirname, '../preload/preload.mjs');
const indexHtml = path.join(RENDERER_DIST, 'index.html');
async function createWindow() {
win = new BrowserWindow({
autoHideMenuBar: true,
height: 900,
icon: path.join(process.env.VITE_PUBLIC as string, 'favicon.ico'),
show: false,
title: 'Main window',
webPreferences: {
contextIsolation: true,
nodeIntegration: false,
preload,
webSecurity: true,
},
width: 1440,
});
// 监听窗口准备好显示的事件
win.once('ready-to-show', () => {
win?.maximize(); // 最大化窗口
win?.show(); // 显示窗口
});
if (VITE_DEV_SERVER_URL) {
win.loadURL(VITE_DEV_SERVER_URL);
} else {
win.loadFile(indexHtml);
}
// Test actively push message to the Electron-Renderer
win.webContents.on('did-finish-load', () => {
win?.webContents.send('main-process-message', new Date().toLocaleString());
});
// Make all links open with the browser, not with the application
win.webContents.setWindowOpenHandler(({ url }) => {
if (url.startsWith('https:')) shell.openExternal(url);
return { action: 'deny' };
});
// win.webContents.on('will-navigate', (event, url) => { }) #344
}
Menu.setApplicationMenu(null);
app
.whenReady()
.then(createWindow)
.then(() => {
// 禁用了菜单之后,默认的快捷键也会被禁用,这里重新注册部分常用快捷键
if (VITE_DEV_SERVER_URL) {
// 开发模式下监听快捷键来打开开发者工具
globalShortcut.register('CmdOrCtrl+Shift+I', () => {
BrowserWindow.getFocusedWindow()?.webContents.toggleDevTools();
});
}
// 监听快捷键来刷新页面
globalShortcut.registerAll(['CmdOrCtrl+R', 'CmdOrCtrl+F5'], () => {
BrowserWindow.getFocusedWindow()?.webContents.reload();
});
// 监听快捷键来强制刷新页面
globalShortcut.registerAll(
['CmdOrCtrl+Shift+R', 'CmdOrCtrl+Shift+F5'],
() => {
BrowserWindow.getFocusedWindow()?.webContents.reloadIgnoringCache();
},
);
});
app.on('window-all-closed', () => {
win = null;
if (process.platform !== 'darwin') app.quit();
});
app.on('will-quit', () => {
globalShortcut.unregisterAll();
});
app.on('second-instance', () => {
if (win) {
// Focus on the main window if the user tried to open another
if (win.isMinimized()) win.restore();
win.focus();
}
});
app.on('activate', () => {
const allWindows = BrowserWindow.getAllWindows();
if (allWindows.length > 0) {
allWindows[0].focus();
} else {
createWindow();
}
});
// New window example arg: new windows url
ipcMain.handle('open-win', (_, arg) => {
const childWindow = new BrowserWindow({
webPreferences: {
contextIsolation: true,
nodeIntegration: true,
preload,
webviewTag: true,
},
});
if (VITE_DEV_SERVER_URL) {
childWindow.loadURL(`${VITE_DEV_SERVER_URL}#${arg}`);
childWindow.webContents.openDevTools();
} else {
childWindow.loadFile(indexHtml, { hash: arg });
}
});

View File

@@ -0,0 +1,114 @@
import { contextBridge, ipcRenderer } from 'electron';
// --------- Expose some API to the Renderer process ---------
contextBridge.exposeInMainWorld('ipcRenderer', {
invoke(...args: Parameters<typeof ipcRenderer.invoke>) {
const [channel, ...omit] = args;
return ipcRenderer.invoke(channel, ...omit);
},
off(...args: Parameters<typeof ipcRenderer.off>) {
const [channel, ...omit] = args;
return ipcRenderer.off(channel, ...omit);
},
on(...args: Parameters<typeof ipcRenderer.on>) {
const [channel, listener] = args;
return ipcRenderer.on(channel, (event, ...args) =>
listener(event, ...args),
);
},
send(...args: Parameters<typeof ipcRenderer.send>) {
const [channel, ...omit] = args;
return ipcRenderer.send(channel, ...omit);
},
// You can expose other APTs you need here.
// ...
});
// --------- Preload scripts loading ---------
// function domReady(
// condition: DocumentReadyState[] = ['complete', 'interactive'],
// ) {
// return new Promise((resolve) => {
// if (condition.includes(document.readyState)) {
// resolve(true);
// } else {
// document.addEventListener('readystatechange', () => {
// if (condition.includes(document.readyState)) {
// resolve(true);
// }
// });
// }
// });
// }
// const safeDOM = {
// append(parent: HTMLElement, child: HTMLElement) {
// if (![...parent.children].includes(child)) {
// return parent.append(child);
// }
// },
// remove(parent: HTMLElement, child: HTMLElement) {
// if ([...parent.children].includes(child)) {
// return child.remove();
// }
// },
// };
// function useLoading() {
// const className = `loaders-css__square-spin`;
// const styleContent = `
// @keyframes square-spin {
// 25% { transform: perspective(100px) rotateX(180deg) rotateY(0); }
// 50% { transform: perspective(100px) rotateX(180deg) rotateY(180deg); }
// 75% { transform: perspective(100px) rotateX(0) rotateY(180deg); }
// 100% { transform: perspective(100px) rotateX(0) rotateY(0); }
// }
// .${className} > div {
// animation-fill-mode: both;
// width: 50px;
// height: 50px;
// background: #fff;
// animation: square-spin 3s 0s cubic-bezier(0.09, 0.57, 0.49, 0.9) infinite;
// }
// .app-loading-wrap {
// position: fixed;
// top: 0;
// left: 0;
// width: 100vw;
// height: 100vh;
// display: flex;
// align-items: center;
// justify-content: center;
// background: #282c34;
// z-index: 9;
// }
// `;
// const oStyle = document.createElement('style');
// const oDiv = document.createElement('div');
// oStyle.id = 'app-loading-style';
// oStyle.innerHTML = styleContent;
// oDiv.className = 'app-loading-wrap';
// oDiv.innerHTML = `<div class="${className}"><div></div></div>`;
// return {
// appendLoading() {
// safeDOM.append(document.head, oStyle);
// safeDOM.append(document.body, oDiv);
// },
// removeLoading() {
// safeDOM.remove(document.head, oStyle);
// safeDOM.remove(document.body, oDiv);
// },
// };
// }
// const { appendLoading, removeLoading } = useLoading();
// domReady().then(appendLoading);
// window.onmessage = (ev) => {
// ev.data.payload === 'removeLoading' && removeLoading();
// };
// setTimeout(removeLoading, 4999);

View File

@@ -16,18 +16,61 @@
},
"type": "module",
"scripts": {
"build": "pnpm vite build --mode production",
"build": "pnpm vite build --mode production && electron-builder",
"build:analyze": "pnpm vite build --mode analyze",
"dev": "pnpm vite --mode development",
"dev": "cross-env ELECTRON_DISABLE_SECURITY_WARNINGS=true pnpm vite --mode development",
"preview": "vite preview",
"typecheck": "vue-tsc --noEmit --skipLibCheck",
"test:e2e": "playwright test",
"test:e2e-ui": "playwright test --ui",
"test:e2e-codegen": "playwright codegen"
},
"main": "dist-electron/main/main.js",
"debug": {
"env": {
"VITE_DEV_SERVER_URL": "http://127.0.0.1:5555/"
}
},
"imports": {
"#/*": "./src/*"
},
"build": {
"productName": "VbenAdminPlayground",
"appId": "pro.vben.playground",
"copyright": "vben.pro © 2025",
"compression": "maximum",
"artifactName": "${productName}-v${version}-${platform}-${arch}.${ext}",
"asar": true,
"directories": {
"output": "dist-electron/release"
},
"files": [
"dist/**/*",
"dist-electron/**/*",
"package.json",
"!node_modules/**",
"!dist-electron/release/**"
],
"nsis": {
"oneClick": false,
"allowToChangeInstallationDirectory": true,
"perMachine": true,
"deleteAppDataOnUninstall": true,
"createDesktopShortcut": true,
"createStartMenuShortcut": true,
"shortcutName": "VbenAdmin"
},
"win": {
"icon": "./electron/logo/logo_256.ico",
"target": "nsis"
},
"mac": {
"icon": "./electron/logo/logo_256.ico"
},
"linux": {
"icon": "./electron/logo/logo_256.ico"
}
},
"dependencies": {
"@tanstack/vue-query": "catalog:",
"@vben-core/menu-ui": "workspace:*",
@@ -51,5 +94,10 @@
"pinia": "catalog:",
"vue": "catalog:",
"vue-router": "catalog:"
},
"devDependencies": {
"cross-env": "catalog:",
"electron": "catalog:",
"electron-builder": "catalog:"
}
}

View File

@@ -8,6 +8,7 @@ import { defineOverridesPreferences } from '@vben/preferences';
export const overridesPreferences = defineOverridesPreferences({
// overrides
app: {
enableCheckUpdates: false,
name: import.meta.env.VITE_APP_TITLE,
},
});

View File

@@ -2,7 +2,9 @@ import { defineConfig } from '@vben/vite-config';
export default defineConfig(async () => {
return {
application: {},
application: {
electron: true,
},
vite: {
server: {
proxy: {

1535
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff

View File

@@ -87,6 +87,8 @@ catalog:
depcheck: ^1.4.7
dotenv: ^16.4.7
echarts: ^5.6.0
electron: ^35.0.3
electron-builder: ^25.1.8
element-plus: ^2.9.7
eslint: ^9.24.0
eslint-config-turbo: ^2.5.0
@@ -169,6 +171,8 @@ catalog:
vee-validate: ^4.15.0
vite: ^6.2.5
vite-plugin-compression: ^0.5.1
vite-plugin-electron: ^0.29.0
vite-plugin-electron-renderer: ^0.14.6
vite-plugin-dts: ^4.5.3
vite-plugin-html: ^3.2.2
vite-plugin-lazy-import: ^1.0.7