mirror of
https://github.com/vbenjs/vue-vben-admin.git
synced 2025-08-25 08:06:30 +08:00
feat: electron support
This commit is contained in:
1
.gitignore
vendored
1
.gitignore
vendored
@@ -5,6 +5,7 @@ dist-ssr
|
||||
dist.zip
|
||||
dist.tar
|
||||
dist.war
|
||||
dist-electron
|
||||
.nitro
|
||||
.output
|
||||
*-dist.zip
|
||||
|
1
.npmrc
1
.npmrc
@@ -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
|
||||
|
@@ -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:"
|
||||
}
|
||||
|
40
internal/vite-config/src/plugins/electron.ts
Normal file
40
internal/vite-config/src/plugins/electron.ts
Normal 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: {},
|
||||
});
|
||||
};
|
@@ -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)],
|
||||
},
|
||||
]);
|
||||
}
|
||||
|
||||
|
@@ -95,6 +95,8 @@ interface ApplicationPluginOptions extends CommonPluginOptions {
|
||||
compress?: boolean;
|
||||
/** 压缩类型 */
|
||||
compressTypes?: ('brotli' | 'gzip')[];
|
||||
/** 启用electron */
|
||||
electron?: boolean;
|
||||
/** 在构建的时候抽离配置文件 */
|
||||
extraAppConfig?: boolean;
|
||||
/** 是否开启html插件 */
|
||||
|
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
@@ -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 };
|
||||
|
@@ -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"]
|
||||
}
|
||||
|
22
packages/@core/base/typings/electron.d.ts
vendored
Normal file
22
packages/@core/base/typings/electron.d.ts
vendored
Normal 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 {};
|
@@ -27,6 +27,9 @@
|
||||
},
|
||||
"./vue-router": {
|
||||
"types": "./vue-router.d.ts"
|
||||
},
|
||||
"./electron": {
|
||||
"types": "./electron.d.ts"
|
||||
}
|
||||
},
|
||||
"publishConfig": {
|
||||
|
@@ -3,3 +3,6 @@ VITE_APP_TITLE=Vben Admin
|
||||
|
||||
# 应用命名空间,用于缓存、store等功能的前缀,确保隔离
|
||||
VITE_APP_NAMESPACE=vben-web-play
|
||||
|
||||
# vue-router 的模式
|
||||
VITE_ROUTER_HISTORY=hash
|
||||
|
@@ -1,4 +1,4 @@
|
||||
VITE_BASE=/
|
||||
VITE_BASE=./
|
||||
|
||||
# 接口地址
|
||||
VITE_GLOB_API_URL=https://mock-napi.vben.pro/api
|
||||
|
BIN
playground/electron/logo/logo_128.ico
Normal file
BIN
playground/electron/logo/logo_128.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 66 KiB |
BIN
playground/electron/logo/logo_256.ico
Normal file
BIN
playground/electron/logo/logo_256.ico
Normal file
Binary file not shown.
After Width: | Height: | Size: 47 KiB |
149
playground/electron/main.ts
Normal file
149
playground/electron/main.ts
Normal 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 });
|
||||
}
|
||||
});
|
114
playground/electron/preload.ts
Normal file
114
playground/electron/preload.ts
Normal 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);
|
@@ -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:"
|
||||
}
|
||||
}
|
||||
|
@@ -8,6 +8,7 @@ import { defineOverridesPreferences } from '@vben/preferences';
|
||||
export const overridesPreferences = defineOverridesPreferences({
|
||||
// overrides
|
||||
app: {
|
||||
enableCheckUpdates: false,
|
||||
name: import.meta.env.VITE_APP_TITLE,
|
||||
},
|
||||
});
|
||||
|
@@ -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
1535
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
@@ -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
|
||||
|
Reference in New Issue
Block a user