├── .electron-vue ├── build.ts ├── dev-runner.ts ├── hot-updater.ts ├── log │ └── index.ts ├── rspack.config.ts ├── tools.ts └── utils.ts ├── .github └── workflows │ └── build-test.yml ├── .gitignore ├── .lintstagedrc.cjs ├── .postcssrc.js ├── .vscode ├── extensions.json └── settings.json ├── CHANGELOG.md ├── LICENSE ├── README.md ├── README_ZH.md ├── build.json ├── build └── icons │ ├── 256x256.png │ ├── icon.icns │ └── icon.ico ├── config └── index.ts ├── customTypes ├── Item.d.ts ├── global.d.ts ├── image.d.ts └── shims-vue.d.ts ├── dist └── web │ └── .gitkeep ├── env ├── .env └── sit.env ├── package-lock.json ├── package.json ├── prettier.config.mjs ├── src ├── index.html ├── ipc-manager │ ├── channel.ts │ └── index.ts ├── main │ ├── config │ │ ├── hot-publish.ts │ │ └── static-path.ts │ ├── hooks │ │ ├── disable-button-hook.ts │ │ ├── exception-hook.ts │ │ └── menu-hook.ts │ ├── index.ts │ └── services │ │ ├── check-update.ts │ │ ├── download-file.ts │ │ ├── hot-updater.ts │ │ ├── ipc-main-handle.ts │ │ ├── ipc-main.ts │ │ ├── web-content-send.ts │ │ └── window-manager.ts ├── preload │ └── index.ts └── renderer │ ├── App.vue │ ├── api │ └── login.ts │ ├── assets │ ├── 404_images │ │ ├── 404.png │ │ └── 404_cloud.png │ ├── icons │ │ └── svg │ │ │ └── electron-logo.svg │ └── logo.png │ ├── components │ └── title-bar │ │ └── title-bar.vue │ ├── error.ts │ ├── i18n │ ├── index.ts │ └── languages │ │ ├── en.ts │ │ └── zh-cn.ts │ ├── main.ts │ ├── permission.ts │ ├── public │ └── loader.html │ ├── router │ ├── constant-router-map.ts │ └── index.ts │ ├── store │ └── modules │ │ └── template.ts │ ├── styles │ ├── index.scss │ └── transition.css │ ├── utils │ ├── hackIpcRenderer.ts │ ├── notification.ts │ ├── performance.ts │ ├── request.ts │ └── timer.ts │ └── views │ ├── 404.vue │ └── landing-page │ ├── components │ └── system-info-mation.vue │ └── landing-page.vue ├── tsconfig.json └── 关于一些插件的记录.md /.electron-vue/build.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'production' 2 | 3 | import { say } from 'cfonts' 4 | import { deleteAsync } from 'del' 5 | import chalk from 'chalk' 6 | import { Listr } from 'listr2' 7 | import { Configuration, rspack } from '@rspack/core' 8 | import { errorLog, doneLog, okayLog } from './log' 9 | import { DetailedError, getArgv } from './utils' 10 | import { 11 | createMainConfig, 12 | createPreloadConfig, 13 | createRendererConfig, 14 | } from './rspack.config' 15 | 16 | const { clean = false, target = 'client' } = getArgv() 17 | const isCI = process.env.CI || false 18 | if (target === 'web') web() 19 | else unionBuild() 20 | 21 | async function cleanBuid() { 22 | await deleteAsync([ 23 | 'dist/electron/main/*', 24 | 'dist/electron/renderer/*', 25 | 'dist/web/*', 26 | 'build/*', 27 | '!build/icons', 28 | ]) 29 | doneLog(`清理构建目录成功`) 30 | if (clean) process.exit() 31 | } 32 | 33 | async function unionBuild() { 34 | greeting() 35 | console.time('构建耗时') 36 | await cleanBuid() 37 | 38 | const tasksLister = new Listr( 39 | [ 40 | { 41 | title: '构建资源文件', 42 | task: async (_, tasks) => { 43 | try { 44 | await pack([ 45 | createMainConfig({ env: 'production' }), 46 | createPreloadConfig({ env: 'production', filename: 'index.ts' }), 47 | createRendererConfig({ env: 'production', target }), 48 | ]) 49 | okayLog( 50 | `资源文件构建完成,构建交付 ${chalk.yellow( 51 | 'electron-builder', 52 | )} 请稍等...\n`, 53 | ) 54 | console.timeEnd('构建耗时') 55 | } catch (error) { 56 | errorLog(`\n 资源文件构建失败 \n`) 57 | return Promise.reject(error) 58 | } 59 | }, 60 | }, 61 | ], 62 | { 63 | concurrent: true, 64 | exitOnError: true, 65 | }, 66 | ) 67 | await tasksLister.run() 68 | } 69 | 70 | async function web() { 71 | await deleteAsync(['dist/web/*', '!.gitkeep']) 72 | await pack(createRendererConfig({ target })) 73 | doneLog(`web build success`) 74 | process.exit() 75 | } 76 | function pack( 77 | config: Configuration | Configuration[], 78 | ): Promise { 79 | return new Promise((resolve, reject) => { 80 | rspack(config, (err: DetailedError | null, stats) => { 81 | if (err) reject(err.stack || err) 82 | else if (stats?.hasErrors()) { 83 | let err = '' 84 | 85 | stats 86 | .toString({ 87 | chunks: false, 88 | colors: true, 89 | }) 90 | .split(/\r?\n/) 91 | .forEach((line) => { 92 | err += ` ${line}\n` 93 | }) 94 | 95 | reject(err) 96 | } else { 97 | resolve( 98 | stats?.toString({ 99 | chunks: false, 100 | colors: true, 101 | }), 102 | ) 103 | } 104 | }) 105 | }) 106 | } 107 | 108 | function greeting() { 109 | const cols = process.stdout.columns 110 | let text: boolean | string = '' 111 | 112 | if (cols > 85) text = `let's-build` 113 | else if (cols > 60) text = `let's-|build` 114 | else text = false 115 | 116 | if (text && !isCI) { 117 | say(text, { 118 | colors: ['yellow'], 119 | font: 'simple3d', 120 | space: false, 121 | }) 122 | } else console.log(chalk.yellow.bold(`\n let's-build`)) 123 | console.log() 124 | } 125 | -------------------------------------------------------------------------------- /.electron-vue/dev-runner.ts: -------------------------------------------------------------------------------- 1 | process.env.NODE_ENV = 'development' 2 | 3 | import readline from 'node:readline' 4 | import electron from 'electron' 5 | import chalk from 'chalk' 6 | import { join } from 'path' 7 | import { rspack } from '@rspack/core' 8 | import { RspackDevServer } from '@rspack/dev-server' 9 | import { detect } from 'detect-port' 10 | import config from '../config' 11 | import { say } from 'cfonts' 12 | import { spawn } from 'child_process' 13 | import type { ChildProcess } from 'child_process' 14 | import { 15 | DetailedError, 16 | electronLog, 17 | getArgv, 18 | logStats, 19 | removeJunk, 20 | workPath, 21 | } from './utils' 22 | import { 23 | createMainConfig, 24 | createPreloadConfig, 25 | createRendererConfig, 26 | } from './rspack.config' 27 | import { errorLog } from './log' 28 | const { target = 'client', controlledRestart = false } = getArgv() 29 | 30 | let electronProcess: ChildProcess | null = null 31 | let manualRestart = false 32 | let readlineInterface: readline.Interface | null = null 33 | 34 | interface Shortcut { 35 | key: string 36 | description: string 37 | action: () => void 38 | } 39 | 40 | const shortcutList: Shortcut[] = [ 41 | { 42 | key: 'r', 43 | description: '重启主进程', 44 | action() { 45 | restartElectron() 46 | }, 47 | }, 48 | { 49 | key: 'q', 50 | description: '退出', 51 | action() { 52 | electronProcess?.kill() 53 | readlineInterface?.close() 54 | process.exit() 55 | }, 56 | }, 57 | { 58 | key: 'h', 59 | description: '显示帮助', 60 | action() { 61 | showHelp() 62 | }, 63 | }, 64 | ] 65 | 66 | async function startRenderer(): Promise { 67 | const port = await detect(config.dev.port || 9080) 68 | const compiler = rspack(createRendererConfig({ target })) 69 | 70 | compiler.hooks.done.tap('done', (stats) => { 71 | logStats('渲染进程', stats) 72 | }) 73 | process.env.PORT = String(port) 74 | const server = new RspackDevServer( 75 | { 76 | port, 77 | static: { 78 | directory: join(workPath, 'src', 'renderer', 'public'), 79 | publicPath: '/public/', 80 | }, 81 | }, 82 | compiler, 83 | ) 84 | await server.start() 85 | console.log('\n\n' + chalk.blue(` 正在准备主进程,请等待...`) + '\n\n') 86 | } 87 | 88 | function startMain(): Promise { 89 | return new Promise((resolve, reject) => { 90 | const rsWatcher = rspack([ 91 | createMainConfig({}), 92 | createPreloadConfig({ filename: 'index.ts' }), 93 | ]) 94 | rsWatcher.hooks.watchRun.tapAsync('watch-run', (compilation, done) => { 95 | logStats(`主进程`, chalk.white.bold(`正在处理资源文件...`)) 96 | done() 97 | }) 98 | rsWatcher.watch( 99 | { 100 | ignored: /node_modules/, 101 | aggregateTimeout: 300, 102 | poll: false, 103 | }, 104 | (err: DetailedError | null, stats) => { 105 | logStats(`主进程`, stats) 106 | if (err || stats?.hasErrors()) { 107 | errorLog(err?.stack ?? err) 108 | if (err?.details) { 109 | console.error(err.details) 110 | } else { 111 | console.error(stats?.toString({})) 112 | } 113 | throw new Error('Error occured in main process') 114 | } 115 | if (electronProcess && !controlledRestart) { 116 | restartElectron() 117 | } 118 | resolve() 119 | }, 120 | ) 121 | }) 122 | } 123 | 124 | function startElectron() { 125 | let args = [ 126 | '--inspect=5858', 127 | join(__dirname, '../dist/electron/main/main.js'), 128 | ] 129 | 130 | // detect yarn or npm and process commandline args accordingly 131 | if (process.env.npm_execpath?.endsWith('yarn.js')) { 132 | args = args.concat(process.argv.slice(3)) 133 | } else if (process.env.npm_execpath?.endsWith('npm-cli.js')) { 134 | args = args.concat(process.argv.slice(2)) 135 | } 136 | 137 | electronProcess = spawn(electron as any, args) 138 | 139 | electronProcess.stdout?.on('data', (data: string) => { 140 | electronLog(removeJunk(data), 'blue') 141 | }) 142 | electronProcess.stderr?.on('data', (data: string) => { 143 | electronLog(removeJunk(data), 'red') 144 | }) 145 | 146 | electronProcess.on('close', () => { 147 | if (!manualRestart) { 148 | readlineInterface?.close() 149 | process.exit() 150 | } 151 | }) 152 | } 153 | 154 | function restartElectron() { 155 | manualRestart = true 156 | electronProcess?.pid && process.kill(electronProcess.pid) 157 | electronProcess = null 158 | electronProcess = null 159 | startElectron() 160 | setTimeout(() => { 161 | manualRestart = false 162 | }, 5000) 163 | } 164 | 165 | function onInputAction(input: string) { 166 | if (!controlledRestart && input === 'r') { 167 | console.log( 168 | chalk.yellow.bold( 169 | '受控重启被禁用,请在启动时使用 --controlledRestart 选项启用', 170 | ), 171 | ) 172 | return 173 | } 174 | const shortcut = shortcutList.find((shortcut) => shortcut.key === input) 175 | if (shortcut) { 176 | shortcut.action() 177 | } 178 | } 179 | 180 | function initReadline() { 181 | readlineInterface = readline.createInterface({ 182 | input: process.stdin, 183 | output: process.stdout, 184 | }) 185 | readlineInterface.on('line', onInputAction) 186 | } 187 | 188 | function showHelp() { 189 | console.log(chalk.green.bold('可用快捷键:\n')) 190 | shortcutList.forEach((shortcut) => { 191 | console.log( 192 | `输入 ${chalk.green.bold(shortcut.key)} + 回车 ${shortcut.description}`, 193 | ) 194 | }) 195 | console.log('\n') 196 | } 197 | 198 | function greeting() { 199 | const cols = process.stdout.columns 200 | let text: string | boolean = '' 201 | 202 | if (cols > 104) text = 'rspack-electron' 203 | else if (cols > 76) text = 'rspack-|electron' 204 | else text = false 205 | 206 | if (text) { 207 | say(text, { 208 | colors: ['yellow'], 209 | font: 'simple3d', 210 | space: false, 211 | }) 212 | } else console.log(chalk.yellow.bold('\n rspack-electron')) 213 | console.log(chalk.blue(`准备启动...`) + '\n') 214 | showHelp() 215 | } 216 | 217 | async function init() { 218 | if (target === 'web') { 219 | await startRenderer() 220 | return 221 | } 222 | greeting() 223 | try { 224 | await startRenderer() 225 | await startMain() 226 | startElectron() 227 | initReadline() 228 | } catch (error) { 229 | console.error(error) 230 | process.exit(1) 231 | } 232 | } 233 | 234 | init() 235 | -------------------------------------------------------------------------------- /.electron-vue/hot-updater.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * power by biuuu 3 | */ 4 | 5 | import chalk from 'chalk' 6 | import { join } from 'path' 7 | import { 8 | ensureDir, 9 | emptyDir, 10 | copy, 11 | outputJSON, 12 | remove, 13 | stat, 14 | readFile, 15 | } from 'fs-extra' 16 | import { createHmac } from 'crypto' 17 | import AdmZip from 'adm-zip' 18 | import packageFile from '../package.json' 19 | import buildConfig from '../build.json' 20 | import config from '../config' 21 | import { okayLog, errorLog, doneLog } from './log' 22 | 23 | const buildPath = join('.', 'dist', 'electron') 24 | 25 | const hash = (data, type = 'sha256') => { 26 | const hmac = createHmac(type, 'Sky') 27 | hmac.update(data) 28 | return hmac.digest('hex') 29 | } 30 | 31 | const createZip = (filePath: string, dest: string) => { 32 | const zip = new AdmZip() 33 | zip.addLocalFolder(filePath, '') 34 | zip.toBuffer() 35 | zip.writeZip(dest) 36 | } 37 | 38 | const start = async () => { 39 | console.log(chalk.green.bold(`Start packing \n`)) 40 | 41 | if (buildConfig.asar) { 42 | errorLog( 43 | `${chalk.red( 44 | 'Please make sure the build.asar option in the Package.json file is set to false', 45 | )}\n`, 46 | ) 47 | return 48 | } 49 | 50 | if (config.build.hotPublishConfigName === '') { 51 | errorLog( 52 | `${ 53 | chalk.red( 54 | 'HotPublishConfigName is not set, which will cause the update to fail, please set it in the config/index.js \n', 55 | ) + chalk.red.bold(`\n Packing failed \n`) 56 | }`, 57 | ) 58 | process.exit(1) 59 | } 60 | 61 | stat(join(buildPath, 'main'), async (err, stats) => { 62 | if (err) { 63 | errorLog( 64 | `${chalk.red( 65 | 'No resource files were found, please execute this command after the build command', 66 | )}\n`, 67 | ) 68 | return 69 | } 70 | 71 | try { 72 | console.log(chalk.green.bold(`Check the resource files \n`)) 73 | const packResourcesPath = join('.', 'build', 'resources', 'dist') 74 | const packPackagePath = join('.', 'build', 'resources') 75 | const resourcesPath = join('.', 'dist') 76 | const appPath = join('.', 'build', 'resources') 77 | const name = 'app.zip' 78 | const outputPath = join('.', 'build', 'update') 79 | const zipPath = join(outputPath, name) 80 | 81 | await ensureDir(packResourcesPath) 82 | await emptyDir(packResourcesPath) 83 | await copy(resourcesPath, packResourcesPath) 84 | okayLog(chalk.cyan.bold(`File copy complete \n`)) 85 | await outputJSON(join(packPackagePath, 'package.json'), { 86 | name: packageFile.name, 87 | productName: buildConfig.productName, 88 | version: packageFile.version, 89 | description: packageFile.description, 90 | main: packageFile.main, 91 | author: packageFile.author, 92 | dependencies: packageFile.dependencies, 93 | }) 94 | okayLog(chalk.cyan.bold(`Rewrite package file complete \n`)) 95 | await ensureDir(outputPath) 96 | await emptyDir(outputPath) 97 | createZip(appPath, zipPath) 98 | const buffer = await readFile(zipPath) 99 | const sha256 = hash(buffer) 100 | const hashName = sha256.slice(7, 12) 101 | await copy(zipPath, join(outputPath, `${hashName}.zip`)) 102 | await outputJSON( 103 | join(outputPath, `${config.build.hotPublishConfigName}.json`), 104 | { 105 | version: packageFile.version, 106 | name: `${hashName}.zip`, 107 | hash: sha256, 108 | }, 109 | ) 110 | okayLog( 111 | chalk.cyan.bold( 112 | `Zip file complete, Start cleaning up redundant files \n`, 113 | ), 114 | ) 115 | await remove(zipPath) 116 | await remove(appPath) 117 | okayLog(chalk.cyan.bold(`Cleaning up redundant files completed \n`)) 118 | doneLog('The resource file is packaged!\n') 119 | console.log('File location: ' + chalk.green(outputPath) + '\n') 120 | } catch (error) { 121 | errorLog(`${chalk.red(error.message || error)}\n`) 122 | process.exit(1) 123 | } 124 | }) 125 | } 126 | 127 | start() 128 | -------------------------------------------------------------------------------- /.electron-vue/log/index.ts: -------------------------------------------------------------------------------- 1 | import chalk from 'chalk' 2 | 3 | export const doneLog = (text: string) => { 4 | console.log('\n' + chalk.bgGreen.white(' DONE ') + ' ', text) 5 | } 6 | export const errorLog = (text: string | Error | null) => { 7 | console.log('\n ' + chalk.bgRed.white(' ERROR ') + ' ', text) 8 | } 9 | export const okayLog = (text: string) => { 10 | console.log('\n ' + chalk.bgBlue.white(' OKAY ') + ' ', text) 11 | } 12 | export const warningLog = (text: string) => { 13 | console.log('\n ' + chalk.bgYellow.white(' WARNING ') + ' ', text) 14 | } 15 | export const infoLog = (text: string) => { 16 | console.log('\n ' + chalk.bgCyan.white(' INFO ') + ' ', text) 17 | } 18 | -------------------------------------------------------------------------------- /.electron-vue/rspack.config.ts: -------------------------------------------------------------------------------- 1 | import { Configuration, rspack } from '@rspack/core' 2 | import { CreateLoader, CreatePlugins } from './tools' 3 | import { join } from 'path' 4 | import { VueLoaderPlugin } from 'vue-loader' 5 | import { extensions, tsConfig, workPath } from './utils' 6 | 7 | const getCommonConfig = ( 8 | env: 'development' | 'none' | 'production', 9 | ): Configuration => { 10 | const loaderHelper = new CreateLoader() 11 | const pluginHelper = new CreatePlugins() 12 | 13 | const rules = loaderHelper.useDefaultScriptLoader().end() 14 | const plugins = pluginHelper.useDefaultEnvPlugin().end() 15 | 16 | return { 17 | mode: env, 18 | resolve: { 19 | extensions, 20 | tsConfig, 21 | }, 22 | plugins, 23 | module: { 24 | rules, 25 | }, 26 | } 27 | } 28 | 29 | export const createMainConfig = ({ 30 | env = 'development', 31 | }: { 32 | env?: 'development' | 'none' | 'production' 33 | }): Configuration => { 34 | const commonConfig = getCommonConfig(env) 35 | return { 36 | ...commonConfig, 37 | entry: join(workPath, 'src', 'main', 'index.ts'), 38 | output: { 39 | path: join(workPath, 'dist', 'electron', 'main'), 40 | filename: 'main.js', 41 | }, 42 | target: 'electron-main', 43 | } 44 | } 45 | 46 | export const createPreloadConfig = ({ 47 | env = 'development', 48 | filename, 49 | }: { 50 | env?: 'development' | 'none' | 'production' 51 | filename: string 52 | }): Configuration => { 53 | const commonConfig = getCommonConfig(env) 54 | return { 55 | ...commonConfig, 56 | entry: join(workPath, 'src', 'preload', filename), 57 | output: { 58 | path: join(workPath, 'dist', 'electron', 'main'), 59 | filename: 'main-preload.js', 60 | }, 61 | target: 'electron-preload', 62 | } 63 | } 64 | 65 | export const createRendererConfig = ({ 66 | env = 'development', 67 | target = 'client', 68 | }: { 69 | env?: 'development' | 'none' | 'production' 70 | target: 'web' | 'client' 71 | }): Configuration => { 72 | const copyPath = 73 | target === 'client' 74 | ? join(workPath, 'dist', 'electron', 'renderer', 'public') 75 | : join(workPath, 'dist', 'web', 'public') 76 | const loaderHelper = new CreateLoader() 77 | const pluginHelper = new CreatePlugins() 78 | 79 | const rules = loaderHelper 80 | .useDefaultResourceLoader() 81 | .useDefaultScriptLoader() 82 | .useDefaultCssLoader() 83 | .add({ 84 | test: /\.vue$/, 85 | loader: 'vue-loader', 86 | options: { 87 | experimentalInlineMatchResource: true, 88 | }, 89 | }) 90 | .end() 91 | 92 | const plugins = pluginHelper 93 | .useDefaultEnvPlugin({ 94 | // 如果不是ui组件库使用,强烈建议关闭 95 | __VUE_OPTIONS_API__: true, 96 | __VUE_PROD_DEVTOOLS__: false, 97 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: false, 98 | }) 99 | .add(new VueLoaderPlugin()) 100 | .add( 101 | new rspack.HtmlRspackPlugin({ 102 | template: join(workPath, 'src', 'index.html'), 103 | }), 104 | ) 105 | .add( 106 | env === 'production' 107 | ? new rspack.CopyRspackPlugin({ 108 | patterns: [ 109 | { 110 | from: join(workPath, 'src', 'renderer', 'public'), 111 | to: copyPath, 112 | globOptions: { 113 | ignore: ['.*'], 114 | }, 115 | }, 116 | ], 117 | }) 118 | : undefined, 119 | ) 120 | .end() 121 | const commonConfig = getCommonConfig(env) 122 | return { 123 | ...commonConfig, 124 | entry: join(workPath, 'src', 'renderer', 'main.ts'), 125 | output: { 126 | path: join(workPath, 'dist', 'electron', 'renderer'), 127 | filename: '[name].js', 128 | }, 129 | plugins, 130 | module: { 131 | rules, 132 | }, 133 | } 134 | } 135 | -------------------------------------------------------------------------------- /.electron-vue/tools.ts: -------------------------------------------------------------------------------- 1 | import { 2 | type RspackPluginFunction, 3 | type RspackPluginInstance, 4 | type RuleSetRule, 5 | type DefinePluginOptions, 6 | type WebpackPluginInstance, 7 | type WebpackPluginFunction, 8 | type LightningcssLoaderOptions, 9 | rspack, 10 | } from '@rspack/core' 11 | import { getConfig } from './utils' 12 | 13 | type ListItemType = 14 | | RspackPluginInstance 15 | | RspackPluginFunction 16 | | RuleSetRule 17 | | WebpackPluginInstance 18 | | WebpackPluginFunction 19 | 20 | export class BaseCreate { 21 | protected list: T[] = [] 22 | 23 | add(item: T | undefined): this { 24 | item && this.list.push(item) 25 | return this 26 | } 27 | 28 | end(): T[] { 29 | return this.list 30 | } 31 | } 32 | 33 | export class CreateLoader extends BaseCreate { 34 | private typeScriptLoader: RuleSetRule = { 35 | test: /\.m?[t]s$/, 36 | exclude: [/node_modules/], 37 | loader: 'builtin:swc-loader', 38 | options: { 39 | jsc: { 40 | parser: { 41 | syntax: 'typescript', 42 | }, 43 | }, 44 | }, 45 | type: 'javascript/auto', 46 | } 47 | private javascriptLoader: RuleSetRule = { 48 | test: /\.m?[j]s$/, 49 | exclude: [/node_modules/], 50 | loader: 'builtin:swc-loader', 51 | options: { 52 | isModule: 'unknown', 53 | }, 54 | type: 'javascript/auto', 55 | } 56 | private defaultResourceLoader: RuleSetRule[] = [ 57 | { 58 | test: /\.(png|jpe?g|gif|svg)(\?.*)?$/, 59 | type: 'asset/resource', 60 | }, 61 | { 62 | test: /\.(mp4|webm|ogg|mp3|wav|flac|aac)(\?.*)?$/, 63 | type: 'asset/resource', 64 | }, 65 | { 66 | test: /\.(woff2?|eot|ttf|otf)(\?.*)?$/, 67 | type: 'asset/resource', 68 | }, 69 | ] 70 | 71 | useDefaultCssLoader(options?: CssLoaderOptions): this { 72 | const defaultCssLoader = buildCssLoaders(options) 73 | defaultCssLoader.forEach((item) => this.add(item)) 74 | return this 75 | } 76 | 77 | useDefaultScriptLoader(): this { 78 | this.add(this.typeScriptLoader).add(this.javascriptLoader) 79 | return this 80 | } 81 | useDefaultResourceLoader(): this { 82 | this.defaultResourceLoader.forEach((item) => this.add(item)) 83 | return this 84 | } 85 | } 86 | 87 | export class CreatePlugins extends BaseCreate< 88 | | RspackPluginInstance 89 | | RspackPluginFunction 90 | | WebpackPluginInstance 91 | | WebpackPluginFunction 92 | > { 93 | useDefaultEnvPlugin(otherEnv?: DefinePluginOptions): this { 94 | this.add(createEnvPlugin(otherEnv)) 95 | return this 96 | } 97 | } 98 | 99 | export interface CssLoaderOptions { 100 | lightningcssOptions: LightningcssLoaderOptions 101 | sourceMap: boolean 102 | } 103 | 104 | const cssLoaders = (options?: CssLoaderOptions) => { 105 | const { lightningcssOptions, sourceMap } = options ?? {} 106 | const cssLoader = { 107 | loader: 'css-loader', 108 | options: { 109 | sourceMap, 110 | esModule: false, 111 | }, 112 | } 113 | 114 | const lightningcssLoader = { 115 | loader: 'builtin:lightningcss-loader', 116 | options: { 117 | ...lightningcssOptions, 118 | }, 119 | } 120 | // 这里就是生成loader和其对应的配置 121 | const generateLoaders = (loader: string, loaderOptions?: any) => { 122 | const loaders = ['vue-style-loader', cssLoader, lightningcssLoader] 123 | 124 | if (loader) { 125 | loaders.push({ 126 | loader: loader + '-loader', 127 | options: Object.assign({}, loaderOptions, { 128 | sourceMap, 129 | }), 130 | }) 131 | } 132 | 133 | return loaders 134 | } 135 | return { 136 | less: generateLoaders('less'), 137 | sass: generateLoaders('sass', { 138 | indentedSyntax: true, 139 | api: 'modern-compiler', 140 | }), 141 | scss: generateLoaders('sass', { api: 'modern-compiler' }), 142 | stylus: generateLoaders('stylus'), 143 | styl: generateLoaders('stylus'), 144 | } 145 | } 146 | 147 | export const buildCssLoaders = (options?: CssLoaderOptions) => { 148 | const output: RuleSetRule[] = [] 149 | const loaders = cssLoaders(options) 150 | 151 | for (const extension in loaders) { 152 | const loader = loaders[extension] 153 | output.push({ 154 | test: new RegExp('\\.' + extension + '$'), 155 | use: loader, 156 | type: 'javascript/auto', 157 | }) 158 | } 159 | 160 | return output 161 | } 162 | 163 | export const createEnvPlugin = ( 164 | otherEnv: DefinePluginOptions = {}, 165 | ): RspackPluginInstance => { 166 | const baseEnv = Object.assign({}, getConfig()) 167 | const clientEnvs = Object.fromEntries( 168 | Object.entries(baseEnv).map(([key, val]) => { 169 | return [`import.meta.env.${key}`, JSON.stringify(val)] 170 | }), 171 | ) 172 | const envs = Object.fromEntries( 173 | Object.entries({ ...clientEnvs, ...otherEnv }).map(([key, val]) => { 174 | return [key, val] 175 | }), 176 | ) 177 | return new rspack.DefinePlugin(envs) 178 | } 179 | -------------------------------------------------------------------------------- /.electron-vue/utils.ts: -------------------------------------------------------------------------------- 1 | import { config } from 'dotenv' 2 | import { join } from 'path' 3 | import minimist from 'minimist' 4 | import chalk from 'chalk' 5 | 6 | const argv = minimist(process.argv.slice(2)) 7 | const rootResolve = (...pathSegments) => join(__dirname, '..', ...pathSegments) 8 | 9 | export const getEnv = () => argv['m'] 10 | export const getArgv = () => argv 11 | 12 | const getEnvPath = () => { 13 | if ( 14 | String(typeof getEnv()) === 'boolean' || 15 | String(typeof getEnv()) === 'undefined' 16 | ) { 17 | return rootResolve('env/.env') 18 | } 19 | return rootResolve(`env/.${getEnv()}.env`) 20 | } 21 | 22 | export const getConfig = () => config({ path: getEnvPath() }).parsed 23 | 24 | export const logStats = (proc: string, data: any) => { 25 | let log = '' 26 | 27 | log += chalk.yellow.bold( 28 | `┏ ${proc} "编译日志" ${new Array(19 - proc.length + 1).join('-')}`, 29 | ) 30 | log += '\n\n' 31 | 32 | if (typeof data === 'object') { 33 | data 34 | .toString({ 35 | colors: true, 36 | chunks: false, 37 | }) 38 | .split(/\r?\n/) 39 | .forEach((line) => { 40 | log += ' ' + line + '\n' 41 | }) 42 | } else { 43 | log += ` ${data}\n` 44 | } 45 | 46 | log += '\n' + chalk.yellow.bold(`┗ ${new Array(28 + 1).join('-')}`) + '\n' 47 | console.log(log) 48 | } 49 | 50 | export const removeJunk = (chunk: string) => { 51 | // Example: 2018-08-10 22:48:42.866 Electron[90311:4883863] *** WARNING: Textured window 52 | if ( 53 | /\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test(chunk) 54 | ) { 55 | return false 56 | } 57 | 58 | // Example: [90789:0810/225804.894349:ERROR:CONSOLE(105)] "Uncaught (in promise) Error: Could not instantiate: ProductRegistryImpl.Registry", source: chrome-devtools://devtools/bundled/inspector.js (105) 59 | if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(chunk)) { 60 | return false 61 | } 62 | 63 | // Example: ALSA lib confmisc.c:767:(parse_card) cannot find card '0' 64 | if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(chunk)) { 65 | return false 66 | } 67 | 68 | return chunk 69 | } 70 | 71 | export const electronLog = (data: any, color: string) => { 72 | if (data) { 73 | let log = '' 74 | data = data.toString().split(/\r?\n/) 75 | data.forEach((line: string) => { 76 | log += ` ${line}\n` 77 | }) 78 | console.log( 79 | chalk[color].bold(`┏ ------- 主进程日志 -----------`) + 80 | '\n\n' + 81 | log + 82 | chalk[color].bold('┗ -------------------------------') + 83 | '\n', 84 | ) 85 | } 86 | } 87 | 88 | export const workPath = join(__dirname, '..') 89 | export const extensions = ['.mjs', '.ts', '.js', '.json', '.node'] 90 | export const tsConfig = join(workPath, 'tsconfig.json') 91 | 92 | export interface DetailedError extends Error { 93 | details?: string 94 | } 95 | -------------------------------------------------------------------------------- /.github/workflows/build-test.yml: -------------------------------------------------------------------------------- 1 | # This workflow will do a clean install of node dependencies, build the source code and run tests across different versions of node 2 | # For more information see: https://help.github.com/actions/language-and-framework-guides/using-nodejs-with-github-actions 3 | 4 | name: Build TEST 5 | 6 | on: 7 | push: 8 | branches: [ master ] 9 | pull_request: 10 | branches: [ master ] 11 | 12 | jobs: 13 | build: 14 | 15 | runs-on: windows-latest 16 | 17 | strategy: 18 | matrix: 19 | node-version: [ 20.x ] 20 | # See supported Node.js release schedule at https://nodejs.org/en/about/releases/ 21 | 22 | steps: 23 | - uses: actions/checkout@v2 24 | - name: Use Node.js ${{ matrix.node-version }} 25 | uses: actions/setup-node@v1 26 | with: 27 | node-version: ${{ matrix.node-version }} 28 | - run: yarn --frozen-lockfile 29 | - run: yarn build 30 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules/ 3 | /dist/ 4 | !build/icons 5 | build/* 6 | npm-debug.log* 7 | yarn-debug.log* 8 | yarn-error.log* 9 | 10 | # Editor directories and files 11 | .idea 12 | *.suo 13 | *.ntvs* 14 | *.njsproj 15 | *.sln 16 | server/client/electron-vue-admin Setup 0.0.1.exe 17 | /build/builder-effective-config.yaml 18 | -------------------------------------------------------------------------------- /.lintstagedrc.cjs: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | '*.{json,html,md}': 'prettier --write --ignore-unknown', 3 | '*.{css,scss,less}': 'prettier --write --ignore-unknown', 4 | } 5 | -------------------------------------------------------------------------------- /.postcssrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: { 3 | autoprefixer: {}, 4 | } 5 | } -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.unocss", 4 | "Vue.volar", 5 | "esbenp.prettier-vscode", 6 | "editorconfig.editorconfig", 7 | "dbaeumer.vscode-eslint", 8 | "stylelint.vscode-stylelint" 9 | ] 10 | } 11 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "editor.defaultFormatter": "esbenp.prettier-vscode", 3 | "npm.packageManager": "yarn", 4 | "editor.formatOnSave": true, 5 | "prettier.enable": true, 6 | "eslint.validate": [ 7 | "javascript", 8 | "javascriptreact", 9 | "typescript", 10 | "typescriptreact", 11 | "vue" 12 | ], 13 | "files.exclude": { 14 | "**/node_modules": true 15 | }, 16 | "editor.codeActionsOnSave": { 17 | "source.fixAll.eslint": "explicit" 18 | }, 19 | "[javascript]": { 20 | "editor.formatOnSave": true, 21 | "editor.defaultFormatter": "esbenp.prettier-vscode" 22 | }, 23 | "[typescript]": { 24 | "editor.formatOnSave": true, 25 | "editor.defaultFormatter": "esbenp.prettier-vscode" 26 | }, 27 | "[vue]": { 28 | "editor.codeActionsOnSave": { 29 | "source.fixAll.eslint": "explicit", 30 | "source.fixAll.stylelint": "explicit" 31 | }, 32 | "editor.defaultFormatter": "esbenp.prettier-vscode" 33 | }, 34 | "files.associations": { 35 | "*.env": "env", 36 | "*.env.*": "env" 37 | }, 38 | "typescript.tsdk": "node_modules\\typescript\\lib" 39 | } 40 | -------------------------------------------------------------------------------- /CHANGELOG.md: -------------------------------------------------------------------------------- 1 | # 2024 年 09 月 22 日 2 | 3 | **重大变更** 4 | 5 | 1. 更换[webpack](https://webpack.js.org/)为[rspack](https://rspack.dev/index) 6 | 2. 删除过期的依赖,并切换 vue 版本为 3.x 7 | 3. 移除 ui 库组件,现在可以随意选择自己的 ui 库,而不必担心因为模板本身和 ui 库有耦合 8 | 4. 预加载脚本和 ipc 通讯现在有了更好的类型提示 @mashirooooo 9 | 5. 现在构建脚本可以识别到例如 `-m` , `--clean` 等标识,不在使用 `cross-env` 作为变量注入 10 | 6. 构建后产物文件夹更加清晰,现在构建后产物文件夹会分类好渲染进程和主进程的代码 11 | 7. 环境变量现在由 dotenv 提供,并且严格遵守字符串替换,不在由对象提供也不建议环境变量是一个对象 12 | 13 | 文档将也会在随后的日子里进行一次更新 14 | 15 | # 历史更新日志 16 | 17 | - 2021 年 04 月 13 日:例行更新依赖,并跟进 webpack5,使用 esbuild-loader 对渲染进程的 css 和 js 进行处理,加快编译速度。 18 | - 2020 年 10 月 12 日:例行更新基础依赖,准备跟进 webpack5,去除已经废弃的插件 19 | - 2020 年 09 月 12 日:更新依赖,去除.electron-vue 中的冗余代码,将已经转入维护模式 happypack 修改为 thread-loader。 20 | - 2020 年 09 月 10 日:例行更新依赖,感谢 @BelinChung 贡献代码,修正 bug。 21 | - 2020 年 04 月 30 日:添加内置服务端关闭方法,进一步简化登录流程;多窗口文档已就绪,服务端说明已补充。 22 | - 2020 年 04 月 29 日:添加了路由多窗口示例,修复 web 打包,提升依赖;文档还未就绪 23 | - 2020 年 02 月 09 日:添加[中文在线文档](https://umbrella22.github.io/electron-vue-template-doc/),[国内访问地址](https://zh-sky.gitee.io/electron-vue-template-doc/) 24 | - 剔除 win 打包依赖,因为太大了,将它放到码云的额外仓库中,[地址](https://gitee.com/Zh-Sky/HardToDownloadLib) 25 | - 2020 年 02 月 06 日更新:激进分支更新至 8.0.0. 26 | - 2020 年 01 月 09 日更新:例行更新依赖,在 dev 中加入了端口监听检测,如果 9080 端口被占用就会自动向后开启一个端口号并使用,同时在 config/index.js 的 dev 中加入了端口设置,可以快捷设置端口号而不用去更改 webpack 的配置了。 27 | - 2019 年 12 月 18 日更新:我在 build 文件夹内添加了 windows 的打包依赖,在打包爆错的话,可以尝试使用/build/lib 内的压缩包,记得看使用说明哦~ 28 | - 2019 年 11 月 22 日更新:得益于群里老哥的提醒,通过修改系统环境变量得到了通过 yarn 下载 electron 失败的问题,具体操作如下:用户环境变量中新增两个个变量,一个是变量名为`ELECTRON_MIRROR`,变量值为`https://npm.taobao.org/mirrors/electron/`,另一个是变量名为`registry`,变量值为`https://registry.npm.taobao.org/`,然后系统变量中同样也加上这两个值,完成之后,删除 node_module 文件夹。然后执行 yarn install,如果还是提示未安装,那就去 electron 文件夹内执行一次 yarn install,就好了。这样的话,不仅仅只是 yarn 更快了,electron 的 rebuild 也会加速很多。所以推荐使用 yarn。 29 | (优先尝试)使用 npm config edit 打开 npm 配置文件,添加上 electron_mirror=https://cdn.npm.taobao.org/dist/electron/ ,然后重启窗口删除 node_module 文件夹,重新安装依赖即可。 30 | - 2019 年 11 月 19 日更新:更新了不使用 updater 进行全量更新的方法,但是该方法不会校验安装包 md5 值,也就是说,包如果被拦截了。。可能就会出问题,这一点我正在想办法处理。 31 | - 2019 年 10 月 31 日更新:升级 electron 版本至 7,但是需要做一些修改,由于淘宝的问题,导致 electron 新的下载器出现故障,故我们需要对 electron 的下载器做一些更改,这非常容易,不用担心 32 | 首先我们在淘宝代理设置下,安装完成依赖,此时是报错的,现在进入项目的 node_modules 文件夹内找到 electron,点击进入,然后修改其中的 package.json 文件,修改 dependencies 对象中的依赖为: 33 | 34 | ```json 35 | "dependencies": { 36 | "@types/node": "^12.0.12", 37 | "extract-zip": "^1.0.3", 38 | "electron-download": "^4.1.0" 39 | }, 40 | ``` 41 | 42 | 然后我们需要再修改 install.js 中的代码(实际就是 6 中的 install 代码) 43 | 44 | ```js 45 | #!/usr/bin/env node 46 | 47 | var version = require('./package').version 48 | 49 | var fs = require('fs') 50 | var os = require('os') 51 | var path = require('path') 52 | var extract = require('extract-zip') 53 | var download = require('electron-download') 54 | 55 | var installedVersion = null 56 | try { 57 | installedVersion = fs 58 | .readFileSync(path.join(__dirname, 'dist', 'version'), 'utf-8') 59 | .replace(/^v/, '') 60 | } catch (ignored) { 61 | // do nothing 62 | } 63 | 64 | if (process.env.ELECTRON_SKIP_BINARY_DOWNLOAD) { 65 | process.exit(0) 66 | } 67 | 68 | var platformPath = getPlatformPath() 69 | 70 | var electronPath = 71 | process.env.ELECTRON_OVERRIDE_DIST_PATH || 72 | path.join(__dirname, 'dist', platformPath) 73 | 74 | if (installedVersion === version && fs.existsSync(electronPath)) { 75 | process.exit(0) 76 | } 77 | 78 | // downloads if not cached 79 | download( 80 | { 81 | cache: process.env.electron_config_cache, 82 | version: version, 83 | platform: process.env.npm_config_platform, 84 | arch: process.env.npm_config_arch, 85 | strictSSL: process.env.npm_config_strict_ssl === 'true', 86 | force: process.env.force_no_cache === 'true', 87 | quiet: process.env.npm_config_loglevel === 'silent' || process.env.CI, 88 | }, 89 | extractFile, 90 | ) 91 | 92 | // unzips and makes path.txt point at the correct executable 93 | function extractFile(err, zipPath) { 94 | if (err) return onerror(err) 95 | extract(zipPath, { dir: path.join(__dirname, 'dist') }, function (err) { 96 | if (err) return onerror(err) 97 | fs.writeFile( 98 | path.join(__dirname, 'path.txt'), 99 | platformPath, 100 | function (err) { 101 | if (err) return onerror(err) 102 | }, 103 | ) 104 | }) 105 | } 106 | 107 | function onerror(err) { 108 | throw err 109 | } 110 | 111 | function getPlatformPath() { 112 | var platform = process.env.npm_config_platform || os.platform() 113 | 114 | switch (platform) { 115 | case 'darwin': 116 | return 'Electron.app/Contents/MacOS/Electron' 117 | case 'freebsd': 118 | case 'linux': 119 | return 'electron' 120 | case 'win32': 121 | return 'electron.exe' 122 | default: 123 | throw new Error( 124 | 'Electron builds are not available on platform: ' + platform, 125 | ) 126 | } 127 | } 128 | ``` 129 | 130 | 然后执行 npm i 即可完成安装,至于打包的话,您可能需要去淘宝镜像手动下载并且放好位置,才能完成打包操作,不然依旧还是报下载错误的信息。 131 | 132 | - 2019 年 10 月 18 日更新:不知不觉中倒也过去了一个月,啊哈哈这次更新给大家带来的是 updater 的示例,这依旧是个实验特性,所以在新分支中才可以使用,使用方式则是,安装依赖, 133 | 运行 `npm run update:serve` 来启动这个 node 服务器,然后您如果想在 dev 的时候就看到效果需要先运行 build 拿到 `latest.yml`文件,然后将其更名为 `dev-app-update.yml` 放入`dist/electron`中,和`main.js`同级,然后你需要关闭或者排除 webpack 的自动清除插件(我已经屏蔽了,所以无需大家自己动手),然后点击软件中的检查更新即可,记住当软件正在运行的时候,是无法应用安装的,所以您需要关闭之后方可安装。这并不是一个错误! 134 | 135 | - 2019 年 9 月 18 日更新:修正生产环境时,没有正确去除控制台输出的问题,双分支例行更新依赖,修正 ui 部分颜色问题,日后准备使用 element 主题功能 136 | - 2019 年 9 月 16 日更新:去除 easymock,直接粗暴更改登陆验证,如有需要请自行修改,例行更新新分支依赖,修正当自定义头部和系统头部互换时,布局不会做出相应变化的问题。 137 | - 2019 年 9 月 3 日更新:修正了当 nodejs >= 12 时,出现 process 未定义的问题,新分支加入自定义头部,现在我们可以做出更 cooool~~的效果了。 138 | - 2019 年 8 月 20 日更新:添加登录拦击,实现登录功能,在 dev 中加入关闭 ELECTRON 无用控制台输出,新分支例行更新依赖,加入生产环境屏蔽 f12 按键。 139 | - 2019 年 8 月 13 日更新:将新分支的所有依赖均更新至最新(但是我觉得,babel 似乎有些东西不需要,还是保留着吧,日后测试后移除)依赖更新之后通过打包和 dev 测试 140 | - 2019 年 8 月 12 日更新:添加一个新分支,该新分支后续将会持续保持 ELECTRON(包括其对应的辅助组件)的版本处于最新状态,去除了单元测试和一些无用的文件。master 分支中则是为路由添加新参数具体 141 | 用途,详看路由中的注释 142 | - 2019 年 8 月 10 日更新:添加各个平台的 build 脚本,当您直接使用 build 时,则会打包您当前操作系统对应的安装包,mac 需要在 macos 上才能进行打包,而 linux 打包 win 的话,需要 wine 的支持,否则会失败 143 | - 2019 年 8 月 4 日更新:修正原 webpack 配置中没有将 config 注入的小问题,添加了拦截实例,修改了侧栏,侧栏需要底色的请勿更新,此更新可能会导致侧栏底色无法完全覆盖(待修正),添加 axios 接口示例,待测。 144 | - 2019 年 8 月 1 日更新:将 node-sass 版本更新至最新版本,尝试修正由于 nodejs 环境是 12 版导致失败(注意!此次更新可能会导致 32 位系统或者 nodejs 版本低于 10 的用户安装依赖报错)去除路由表中重复路由,解决控制台无端报错问题。 145 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Original work Copyright (c) 2016 Greg Holguin 4 | Modified work Copyright (c) 2019-present umbrella22 5 | 6 | Permission is hereby granted, free of charge, to any person obtaining a copy 7 | of this software and associated documentation files (the "Software"), to deal 8 | in the Software without restriction, including without limitation the rights 9 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 10 | copies of the Software, and to permit persons to whom the Software is 11 | furnished to do so, subject to the following conditions: 12 | 13 | The above copyright notice and this permission notice shall be included in all 14 | copies or substantial portions of the Software. 15 | 16 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 17 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 18 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 19 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 20 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 21 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 22 | SOFTWARE. -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # electron-vue-template 2 | 3 | ![GitHub Repo stars](https://img.shields.io/github/stars/umbrella22/electron-vue-template) 4 | [![vue](https://img.shields.io/badge/vue-3.5.8-brightgreen.svg)](https://github.com/vuejs/vue-next) 5 | [![rspack](https://img.shields.io/badge/rspack-1.0.5-brightgreen.svg)](https://rspack.dev/index) 6 | [![electron](https://img.shields.io/badge/electron-32.1.2-brightgreen.svg)](https://github.com/electron/electron) 7 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/umbrella22/electron-vue-template/blob/master/LICENSE) 8 | 9 | # Installation 10 | 11 | You can choose to clone the project or fork repository, or download the zip file directly. It is recommended to clone the repository so that you can receive the latest patches. 12 | 13 | To run a project, you need to have **node version 20** or higher and **use npm as your dependency management tool** 14 | 15 | [Document (Chinese only)](https://umbrella22.github.io/electron-vue-template-doc/) 16 | 17 | [For Chinese Developers](/README_ZH.md) 18 | 19 | # Build Setup 20 | 21 | ```bash 22 | # Clone this repository 23 | $ git clone https://github.com/umbrella22/electron-vite-template.git 24 | # Go into the repository 25 | $ cd electron-vue-template 26 | # install dependencies 27 | $ npm ci 28 | 29 | # serve with hot reload at localhost:9080 30 | $ npm run dev 31 | 32 | # build electron application for production 33 | $ npm run build 34 | 35 | 36 | ``` 37 | 38 | --- 39 | 40 | # Function list 41 | 42 | [x] Auto update 43 | [x] Incremental update 44 | [x] Loading animation before startup 45 | [x] i18n 46 | 47 | # Built-in 48 | 49 | - [vue-router](https://next.router.vuejs.org/index.html) 50 | - [pinia](https://pinia.esm.dev/) 51 | - [electron](http://www.electronjs.org/docs) 52 | - [typescript](https://www.typescriptlang.org/) 53 | - [rspack](https://rspack.dev/index) 54 | 55 | # Note 56 | 57 | - [gitee](https://gitee.com/Zh-Sky/electron-vue-template) is only for domestic users to pull code,from github to synchronize,please visit github for PR 58 | - **Welcome to Issues and PR** 59 | -------------------------------------------------------------------------------- /README_ZH.md: -------------------------------------------------------------------------------- 1 | # Electron-vue-template 2 | 3 | ![GitHub Repo stars](https://img.shields.io/github/stars/umbrella22/electron-vue-template) 4 | [![vue](https://img.shields.io/badge/vue-3.5.8-brightgreen.svg)](https://github.com/vuejs/vue-next) 5 | [![rspack](https://img.shields.io/badge/rspack-1.0.5-brightgreen.svg)](https://rspack.dev/index) 6 | [![electron](https://img.shields.io/badge/electron-32.1.2-brightgreen.svg)](https://github.com/electron/electron) 7 | [![license](https://img.shields.io/github/license/mashape/apistatus.svg)](https://github.com/umbrella22/electron-vue-template/blob/master/LICENSE) 8 | 9 | [国内访问地址](https://gitee.com/Zh-Sky/electron-vue-template) 10 | 11 | ### 请确保您的 node 版本大于等于 20. 12 | 13 | #### 如何安装 14 | 15 | ```bash 16 | npm config edit 17 | # 该命令会打开npm的配置文件,请在空白处添加 18 | # registry=https://registry.npmmirror.com 19 | # ELECTRON_MIRROR=https://npmmirror.com/mirrors/electron/ 20 | # ELECTRON_BUILDER_BINARIES_MIRROR=https://npmmirror.com/mirrors/electron-builder-binaries/ 21 | # 然后关闭该窗口,重启命令行. 22 | npm ci 23 | 24 | # 启动之后,会在9080端口监听 25 | npm run dev 26 | 27 | # build命令在不同系统环境中,需要的的不一样,需要自己根据自身环境进行配置 28 | npm run build 29 | 30 | ``` 31 | 32 | --- 33 | 34 | # [更新日志](/CHANGELOG.md) 35 | -------------------------------------------------------------------------------- /build.json: -------------------------------------------------------------------------------- 1 | { 2 | "asar": false, 3 | "extraFiles": [], 4 | "publish": [ 5 | { 6 | "provider": "generic", 7 | "url": "http://127.0.0.1" 8 | } 9 | ], 10 | "productName": "electron-vue-template", 11 | "appId": "org.sky.electron-vue-template", 12 | "directories": { 13 | "output": "build" 14 | }, 15 | "files": [ 16 | "dist/electron/**/*" 17 | ], 18 | "dmg": { 19 | "contents": [ 20 | { 21 | "x": 410, 22 | "y": 150, 23 | "type": "link", 24 | "path": "/Applications" 25 | }, 26 | { 27 | "x": 130, 28 | "y": 150, 29 | "type": "file" 30 | } 31 | ] 32 | }, 33 | "mac": { 34 | "icon": "build/icons/icon.icns" 35 | }, 36 | "win": { 37 | "icon": "build/icons/icon.ico" 38 | }, 39 | "linux": { 40 | "icon": "build/icons" 41 | } 42 | } -------------------------------------------------------------------------------- /build/icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/build/icons/256x256.png -------------------------------------------------------------------------------- /build/icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/build/icons/icon.icns -------------------------------------------------------------------------------- /build/icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/build/icons/icon.ico -------------------------------------------------------------------------------- /config/index.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | build: { 3 | hotPublishUrl: '', 4 | hotPublishConfigName: 'update-config', 5 | }, 6 | dev: { 7 | removeElectronJunk: true, 8 | port: 9080, 9 | }, 10 | DllFolder: '', 11 | HotUpdateFolder: 'update', 12 | UseStartupChart: true, 13 | IsUseSysTitle: false, 14 | } 15 | -------------------------------------------------------------------------------- /customTypes/Item.d.ts: -------------------------------------------------------------------------------- 1 | interface ColorInfo { 2 | color?: string, 3 | percentage?: number 4 | } -------------------------------------------------------------------------------- /customTypes/global.d.ts: -------------------------------------------------------------------------------- 1 | import { shell } from 'electron' 2 | import type { IIpcRendererInvoke, IIpcRendererOn } from '@ipcManager/index' 3 | type IpcRendererInvoke = { 4 | [key in keyof IIpcRendererInvoke]: { 5 | invoke: IIpcRendererInvoke[key] 6 | } 7 | } 8 | type IpcRendererOn = { 9 | [key in keyof IIpcRendererOn]: { 10 | on: (listener: IIpcRendererOn[key]) => void 11 | once: (listener: IIpcRendererOn[key]) => void 12 | removeAllListeners: () => void 13 | } 14 | } 15 | 16 | interface AnyObject { 17 | [key: string]: any 18 | } 19 | 20 | interface memoryInfo { 21 | jsHeapSizeLimit: number 22 | totalJSHeapSize: number 23 | usedJSHeapSize: number 24 | } 25 | 26 | declare global { 27 | interface Window { 28 | performance: { 29 | memory: memoryInfo 30 | } 31 | ipcRendererChannel: IpcRendererInvoke & IpcRendererOn 32 | systemInfo: { 33 | platform: string 34 | release: string 35 | arch: string 36 | nodeVersion: string 37 | electronVersion: string 38 | } 39 | shell: typeof shell 40 | crash: { 41 | start: () => void 42 | } 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /customTypes/image.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' 2 | declare module '*.png' 3 | declare module '*.jpg' 4 | declare module '*.jpeg' 5 | declare module '*.gif' 6 | declare module '*.bmp' 7 | declare module '*.tiff' -------------------------------------------------------------------------------- /customTypes/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.vue' { 2 | import { DefineComponent } from 'vue' 3 | const component: DefineComponent<{}, {}, any> 4 | export default component 5 | } 6 | interface ImportMeta { 7 | readonly env: Readonly 8 | } 9 | interface ImportMetaEnv { 10 | API_HOST: string 11 | NODE_ENV: string 12 | } 13 | -------------------------------------------------------------------------------- /dist/web/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/dist/web/.gitkeep -------------------------------------------------------------------------------- /env/.env: -------------------------------------------------------------------------------- 1 | API_HOST = '' 2 | API_PREFIX = '' 3 | NODE_ENV = 'development' -------------------------------------------------------------------------------- /env/sit.env: -------------------------------------------------------------------------------- 1 | API_HOST = '' 2 | API_PREFIX = '' 3 | NODE_ENV = 'sit' -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vue-template", 3 | "version": "0.0.0", 4 | "author": "sky ", 5 | "description": "An electron-vue project", 6 | "license": "MIT", 7 | "main": "./dist/electron/main/main.js", 8 | "scripts": { 9 | "dev": "tsx .electron-vue/dev-runner.ts", 10 | "dev:sit": "tsx .electron-vue/dev-runner.ts -m sit", 11 | "build": "tsx .electron-vue/build.ts && electron-builder -c build.json", 12 | "build:win32": "tsx .electron-vue/build.ts && electron-builder -c build.json --win --ia32", 13 | "build:win64": "tsx .electron-vue/build.ts && electron-builder -c build.json --win --x64", 14 | "build:mac": "tsx .electron-vue/build.ts && electron-builder -c build.json --mac", 15 | "build:dir": "tsx .electron-vue/build.ts && electron-builder -c build.json --dir", 16 | "build:clean": "tsx .electron-vue/build.ts --clean", 17 | "build:web": "tsx .electron-vue/build.ts --target web", 18 | "pack:resources": "tsx .electron-vue/hot-updater.ts", 19 | "dep:upgrade": "yarn upgrade-interactive --latest", 20 | "postinstall": "electron-builder install-app-deps" 21 | }, 22 | "engines": { 23 | "node": ">=20.17.0", 24 | "npm": ">=10.9.0" 25 | }, 26 | "dependencies": { 27 | "electron-updater": "^6.6.5" 28 | }, 29 | "devDependencies": { 30 | "@ikaros-cli/prettier-config": "^0.1.0", 31 | "@ikaros-cli/stylelint-config": "^0.2.0", 32 | "@rspack/core": "^1.3.15", 33 | "@rspack/dev-server": "^1.1.3", 34 | "@types/adm-zip": "^0.5.7", 35 | "@types/fs-extra": "^11.0.4", 36 | "@types/node": "^22.13.1", 37 | "@types/semver": "^7.7.0", 38 | "adm-zip": "^0.5.16", 39 | "axios": "^1.9.0", 40 | "cfonts": "^3.3.0", 41 | "chalk": "^5.4.1", 42 | "css-loader": "^7.1.2", 43 | "del": "^7.1.0", 44 | "dotenv": "^16.5.0", 45 | "electron": "^34.5.8", 46 | "electron-builder": "^26.0.12", 47 | "electron-devtools-vendor": "^3.0.0", 48 | "fs-extra": "^11.3.0", 49 | "listr2": "^8.3.3", 50 | "minimist": "^1.2.8", 51 | "pinia": "3.0.1", 52 | "detect-port": "^2.1.0", 53 | "sass-embedded": "^1.89.0", 54 | "sass-loader": "^16.0.5", 55 | "tsx": "^4.19.4", 56 | "typescript": "^5.8.3", 57 | "vue": "^3.5.16", 58 | "vue-loader": "^17.4.2", 59 | "vue-router": "^4.5.1", 60 | "vue-style-loader": "^4.1.3" 61 | } 62 | } -------------------------------------------------------------------------------- /prettier.config.mjs: -------------------------------------------------------------------------------- 1 | import { 2 | endOfLine, 3 | htmlWhitespaceSensitivity, 4 | printWidth, 5 | semi, 6 | singleQuote, 7 | trailingComma, 8 | } from '@ikaros-cli/prettier-config' 9 | export default { 10 | endOfLine, 11 | htmlWhitespaceSensitivity, 12 | printWidth, 13 | semi, 14 | singleQuote, 15 | trailingComma, 16 | } 17 | -------------------------------------------------------------------------------- /src/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | electron-vue-template 8 | 9 | 10 | 11 |
12 | 13 | 14 | -------------------------------------------------------------------------------- /src/ipc-manager/channel.ts: -------------------------------------------------------------------------------- 1 | import type { ProgressInfo } from 'electron-updater' 2 | 3 | export interface IpcMainEventListener { 4 | ipcMainHandle: Send extends void 5 | ? (event: Electron.IpcMainInvokeEvent) => Receive | Promise 6 | : ( 7 | event: Electron.IpcMainInvokeEvent, 8 | args: Send, 9 | ) => Receive | Promise 10 | ipcRendererInvoke: Send extends void 11 | ? () => Promise 12 | : (args: Send) => Promise 13 | } 14 | 15 | export interface IpcRendererEventListener { 16 | ipcRendererOn: Send extends void 17 | ? (event: Electron.IpcRendererEvent) => void 18 | : (event: Electron.IpcRendererEvent, args: Send) => void 19 | webContentSend: Send extends void 20 | ? (webContents: Electron.WebContents) => void 21 | : (webContents: Electron.WebContents, args: Send) => void 22 | } 23 | 24 | export class IpcChannelMainClass { 25 | IsUseSysTitle!: IpcMainEventListener 26 | /** 27 | * 退出应用 28 | */ 29 | AppClose!: IpcMainEventListener 30 | CheckUpdate!: IpcMainEventListener 31 | ConfirmUpdate!: IpcMainEventListener 32 | OpenMessagebox!: IpcMainEventListener< 33 | Electron.MessageBoxOptions, 34 | Electron.MessageBoxReturnValue 35 | > 36 | StartDownload!: IpcMainEventListener 37 | OpenErrorbox!: IpcMainEventListener<{ title: string; message: string }> 38 | StartServer!: IpcMainEventListener 39 | StopServer!: IpcMainEventListener 40 | HotUpdate!: IpcMainEventListener 41 | 42 | /** 43 | * 44 | * 打开窗口 45 | */ 46 | OpenWin!: IpcMainEventListener<{ 47 | /** 48 | * 新的窗口地址 49 | * 50 | * @type {string} 51 | */ 52 | url: string 53 | 54 | /** 55 | * 是否是支付页 56 | * 57 | * @type {boolean} 58 | */ 59 | IsPay?: boolean 60 | 61 | /** 62 | * 支付参数 63 | * 64 | * @type {string} 65 | */ 66 | PayUrl?: string 67 | 68 | /** 69 | * 发送的新页面数据 70 | * 71 | * @type {unknown} 72 | */ 73 | sendData?: unknown 74 | }> 75 | } 76 | export class IpcChannelRendererClass { 77 | // ipcRenderer 78 | DownloadProgress!: IpcRendererEventListener 79 | DownloadError!: IpcRendererEventListener 80 | DownloadPaused!: IpcRendererEventListener 81 | DownloadDone!: IpcRendererEventListener<{ 82 | /** 83 | * 下载的文件路径 84 | * 85 | * @type {string} 86 | */ 87 | filePath: string 88 | }> 89 | updateMsg!: IpcRendererEventListener<{ 90 | state: number 91 | msg: string | ProgressInfo 92 | }> 93 | UpdateProcessStatus!: IpcRendererEventListener<{ 94 | status: 95 | | 'init' 96 | | 'downloading' 97 | | 'moving' 98 | | 'finished' 99 | | 'failed' 100 | | 'download' 101 | message: string 102 | }> 103 | 104 | SendDataTest!: IpcRendererEventListener 105 | BrowserViewTabDataUpdate!: IpcRendererEventListener<{ 106 | bvWebContentsId: number 107 | title: string 108 | url: string 109 | status: 1 | -1 // 1 添加/更新 -1 删除 110 | }> 111 | BrowserViewTabPositionXUpdate!: IpcRendererEventListener<{ 112 | dragTabOffsetX: number 113 | positionX: number 114 | bvWebContentsId: number 115 | }> 116 | BrowserTabMouseup!: IpcRendererEventListener 117 | HotUpdateStatus!: IpcRendererEventListener<{ 118 | status: string 119 | message: string 120 | }> 121 | } 122 | -------------------------------------------------------------------------------- /src/ipc-manager/index.ts: -------------------------------------------------------------------------------- 1 | import { 2 | IpcChannelMainClass, 3 | IpcChannelRendererClass, 4 | IpcMainEventListener, 5 | IpcRendererEventListener, 6 | } from './channel' 7 | 8 | type GetChannelType< 9 | T extends IpcChannelMainClass | IpcChannelRendererClass, 10 | K extends keyof IpcMainEventListener | keyof IpcRendererEventListener, 11 | > = { 12 | [Key in keyof T]: K extends keyof T[Key] ? T[Key][K] : never 13 | } 14 | 15 | export interface IIpcMainHandle 16 | extends GetChannelType {} 17 | export interface IIpcRendererInvoke 18 | extends GetChannelType {} 19 | export interface IIpcRendererOn 20 | extends GetChannelType {} 21 | export interface IWebContentSend 22 | extends GetChannelType {} 23 | 24 | export * from './channel' 25 | -------------------------------------------------------------------------------- /src/main/config/hot-publish.ts: -------------------------------------------------------------------------------- 1 | import config from '@config/index' 2 | interface hotPublish { 3 | url: string; 4 | configName: string; 5 | } 6 | 7 | export const hotPublishConfig: hotPublish = { 8 | url: config.build.hotPublishUrl, 9 | configName: config.build.hotPublishConfigName 10 | } -------------------------------------------------------------------------------- /src/main/config/static-path.ts: -------------------------------------------------------------------------------- 1 | // 这里定义了静态文件路径的位置 2 | import { join } from 'path' 3 | import config from '@config/index' 4 | import { app } from 'electron' 5 | 6 | const env = app.isPackaged ? 'production' : 'development' 7 | 8 | const filePath = { 9 | winURL: { 10 | development: `http://localhost:${process.env.PORT}`, 11 | production: `file://${join( 12 | app.getAppPath(), 13 | 'dist', 14 | 'electron', 15 | 'renderer', 16 | 'index.html', 17 | )}`, 18 | }, 19 | loadingURL: { 20 | development: `http://localhost:${process.env.PORT}/public/loader.html`, 21 | production: `file://${join( 22 | app.getAppPath(), 23 | 'dist', 24 | 'electron', 25 | 'renderer', 26 | 'public', 27 | 'loader.html', 28 | )}`, 29 | }, 30 | __static: { 31 | development: join( 32 | __dirname, 33 | '..', 34 | '..', 35 | '..', 36 | 'src', 37 | 'renderer', 38 | 'public', 39 | ).replace(/\\/g, '\\\\'), 40 | production: join( 41 | app.getAppPath(), 42 | 'dist', 43 | 'electron', 44 | 'renderer', 45 | 'pubilc', 46 | ).replace(/\\/g, '\\\\'), 47 | }, 48 | getPreloadFile(fileName: string) { 49 | if (env !== 'development') { 50 | return join( 51 | app.getAppPath(), 52 | 'dist', 53 | 'electron', 54 | 'main', 55 | `${fileName}.js`, 56 | ) 57 | } 58 | return join(app.getAppPath(), `${fileName}.js`) 59 | }, 60 | } 61 | 62 | process.env.__static = filePath.__static[env] 63 | 64 | process.env.__lib = getAppRootPath(config.DllFolder) 65 | process.env.__updateFolder = getAppRootPath(config.HotUpdateFolder) 66 | 67 | function getAppRootPath(path: string) { 68 | return env !== 'development' 69 | ? join(__dirname, '..', '..', '..', '..', path).replace(/\\/g, '\\\\') 70 | : join(__dirname, '..', '..', '..', path).replace(/\\/g, '\\\\') 71 | } 72 | 73 | export const winURL = filePath.winURL[env] 74 | export const loadingURL = filePath.loadingURL[env] 75 | export const lib = process.env.__lib 76 | export const updateFolder = process.env.__updateFolder 77 | export const getPreloadFile = filePath.getPreloadFile 78 | -------------------------------------------------------------------------------- /src/main/hooks/disable-button-hook.ts: -------------------------------------------------------------------------------- 1 | import { globalShortcut } from 'electron' 2 | 3 | export const useDisableButton = () =>{ 4 | const disableF12 = ()=>{ 5 | globalShortcut.register('f12', () => { 6 | console.log('用户试图启动控制台') 7 | }) 8 | } 9 | return{ 10 | disableF12 11 | } 12 | } 13 | 14 | -------------------------------------------------------------------------------- /src/main/hooks/exception-hook.ts: -------------------------------------------------------------------------------- 1 | import { WebContents, app, dialog } from 'electron' 2 | import type { 3 | Details, 4 | RenderProcessGoneDetails, 5 | Event, 6 | BrowserWindow, 7 | } from 'electron' 8 | 9 | export interface UseProcessExceptionRetrun { 10 | /** 11 | * Emitted when the renderer process unexpectedly disappears. This is normally because it was crashed or killed. 12 | * If a listener is not passed in, it will default to following the crash prompt 13 | * 14 | * @see https://www.electronjs.org/docs/latest/api/app#event-render-process-gone 15 | */ 16 | renderProcessGone: ( 17 | listener?: ( 18 | event: Event, 19 | webContents: WebContents, 20 | details: RenderProcessGoneDetails, 21 | ) => void, 22 | ) => void 23 | /** 24 | * Emitted when the child process unexpectedly disappears. This is normally because it was crashed or killed. It does not include renderer processes. 25 | * If a listener is not passed in, it will default to following the crash prompt 26 | * 27 | * @see https://www.electronjs.org/docs/latest/api/app#event-child-process-gone 28 | */ 29 | childProcessGone: ( 30 | window: BrowserWindow, 31 | listener?: (event: Event, details: Details) => void, 32 | ) => void 33 | } 34 | interface Message { 35 | title: string 36 | buttons: string[] 37 | message: string 38 | } 39 | 40 | export const useProcessException = (): UseProcessExceptionRetrun => { 41 | const renderProcessGone = ( 42 | listener?: ( 43 | event: Event, 44 | webContents: WebContents, 45 | details: RenderProcessGoneDetails, 46 | ) => void, 47 | ) => { 48 | app.on('render-process-gone', (event, webContents, details) => { 49 | if (listener) { 50 | listener(event, webContents, details) 51 | return 52 | } 53 | const message: Message = { 54 | title: '', 55 | buttons: [], 56 | message: '', 57 | } 58 | switch (details.reason) { 59 | case 'crashed': 60 | message.title = '警告' 61 | message.buttons = ['确定', '退出'] 62 | message.message = '图形化进程崩溃,是否进行软重启操作?' 63 | break 64 | case 'killed': 65 | message.title = '警告' 66 | message.buttons = ['确定', '退出'] 67 | message.message = 68 | '由于未知原因导致图形化进程被终止,是否进行软重启操作?' 69 | break 70 | case 'oom': 71 | message.title = '警告' 72 | message.buttons = ['确定', '退出'] 73 | message.message = '内存不足,是否软重启释放内存?' 74 | break 75 | 76 | default: 77 | break 78 | } 79 | dialog 80 | .showMessageBox({ 81 | type: 'warning', 82 | title: message.title, 83 | buttons: message.buttons, 84 | message: message.message, 85 | noLink: true, 86 | }) 87 | .then((res) => { 88 | if (res.response === 0) webContents.reload() 89 | else webContents.close() 90 | }) 91 | }) 92 | } 93 | const childProcessGone = ( 94 | window: BrowserWindow, 95 | listener?: (event: Event, details: Details) => void, 96 | ) => { 97 | app.on('child-process-gone', (event, details) => { 98 | if (listener) { 99 | listener(event, details) 100 | return 101 | } 102 | const message: Message = { 103 | title: '', 104 | buttons: [], 105 | message: '', 106 | } 107 | switch (details.type) { 108 | case 'GPU': 109 | switch (details.reason) { 110 | case 'crashed': 111 | message.title = '警告' 112 | message.buttons = ['确定', '退出'] 113 | message.message = '硬件加速进程已崩溃,是否关闭硬件加速并重启?' 114 | break 115 | case 'killed': 116 | message.title = '警告' 117 | message.buttons = ['确定', '退出'] 118 | message.message = 119 | '硬件加速进程被意外终止,是否关闭硬件加速并重启?' 120 | break 121 | default: 122 | break 123 | } 124 | break 125 | 126 | default: 127 | break 128 | } 129 | dialog 130 | .showMessageBox(window, { 131 | type: 'warning', 132 | title: message.title, 133 | buttons: message.buttons, 134 | message: message.message, 135 | noLink: true, 136 | }) 137 | .then((res) => { 138 | // 当显卡出现崩溃现象时使用该设置禁用显卡加速模式。 139 | if (res.response === 0) { 140 | if (details.type === 'GPU') app.disableHardwareAcceleration() 141 | window.reload() 142 | } else { 143 | window.close() 144 | } 145 | }) 146 | }) 147 | } 148 | return { 149 | renderProcessGone, 150 | childProcessGone, 151 | } 152 | } 153 | -------------------------------------------------------------------------------- /src/main/hooks/menu-hook.ts: -------------------------------------------------------------------------------- 1 | // 这里是定义菜单的地方,详情请查看 https://electronjs.org/docs/api/menu 2 | import { dialog, Menu } from "electron"; 3 | import type { MenuItemConstructorOptions, MenuItem } from "electron" 4 | import { type, arch, release } from "os"; 5 | import { version } from "../../../package.json"; 6 | 7 | const menu: Array<(MenuItemConstructorOptions) | (MenuItem)> = [ 8 | { 9 | label: "设置", 10 | submenu: [ 11 | { 12 | label: "快速重启", 13 | accelerator: "F5", 14 | role: "reload", 15 | }, 16 | { 17 | label: "退出", 18 | accelerator: "CmdOrCtrl+F4", 19 | role: "close", 20 | }, 21 | ], 22 | }, 23 | { 24 | label: "帮助", 25 | submenu: [ 26 | { 27 | label: "关于", 28 | click: function () { 29 | dialog.showMessageBox({ 30 | title: "关于", 31 | type: "info", 32 | message: "electron-Vue框架", 33 | detail: `版本信息:${version}\n引擎版本:${process.versions.v8 34 | }\n当前系统:${type()} ${arch()} ${release()}`, 35 | noLink: true, 36 | buttons: ["查看github", "确定"], 37 | }); 38 | }, 39 | }, 40 | ], 41 | }, 42 | ]; 43 | 44 | export const useMenu = () => { 45 | const creactMenu = () => { 46 | if (process.env.NODE_ENV === "development") { 47 | menu.push({ 48 | label: "开发者设置", 49 | submenu: [ 50 | { 51 | label: "切换到开发者模式", 52 | accelerator: "CmdOrCtrl+I", 53 | role: "toggleDevTools", 54 | }, 55 | ], 56 | }); 57 | } 58 | // 赋予模板 59 | const menuTemplate = Menu.buildFromTemplate(menu); 60 | // 加载模板 61 | Menu.setApplicationMenu(menuTemplate); 62 | } 63 | return { 64 | creactMenu 65 | } 66 | } -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | 'use strict' 2 | 3 | import { useMainDefaultIpc } from './services/ipc-main' 4 | import { app, session } from 'electron' 5 | import InitWindow from './services/window-manager' 6 | import { useDisableButton } from './hooks/disable-button-hook' 7 | import { useProcessException } from '@main/hooks/exception-hook' 8 | import { useMenu } from '@main/hooks/menu-hook' 9 | 10 | async function onAppReady() { 11 | const { disableF12 } = useDisableButton() 12 | const { renderProcessGone } = useProcessException() 13 | const { defaultIpc } = useMainDefaultIpc() 14 | const { creactMenu } = useMenu() 15 | disableF12() 16 | renderProcessGone() 17 | defaultIpc() 18 | creactMenu() 19 | new InitWindow().initWindow() 20 | if (import.meta.env.NODE_ENV === 'development') { 21 | const { VUEJS3_DEVTOOLS } = require('electron-devtools-vendor') 22 | session.defaultSession.loadExtension(VUEJS3_DEVTOOLS, { 23 | allowFileAccess: true, 24 | }) 25 | console.log('已安装: vue-devtools') 26 | } 27 | } 28 | 29 | app.whenReady().then(onAppReady) 30 | // 由于9.x版本问题,需要加入该配置关闭跨域问题 31 | app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') 32 | 33 | app.on('window-all-closed', () => { 34 | // 所有平台均为所有窗口关闭就退出软件 35 | app.quit() 36 | }) 37 | app.on('browser-window-created', () => { 38 | console.log('window-created') 39 | }) 40 | 41 | if (process.defaultApp) { 42 | if (process.argv.length >= 2) { 43 | app.removeAsDefaultProtocolClient('electron-vue-template') 44 | console.log('由于框架特殊性开发环境下无法使用') 45 | } 46 | } else { 47 | app.setAsDefaultProtocolClient('electron-vue-template') 48 | } 49 | -------------------------------------------------------------------------------- /src/main/services/check-update.ts: -------------------------------------------------------------------------------- 1 | import { autoUpdater } from 'electron-updater' 2 | import { BrowserWindow } from 'electron' 3 | import { webContentSend } from './web-content-send' 4 | /** 5 | * -1 检查更新失败 0 正在检查更新 1 检测到新版本,准备下载 2 未检测到新版本 3 下载中 4 下载完成 6 | **/ 7 | class Update { 8 | public mainWindow: BrowserWindow 9 | constructor() { 10 | // 设置url 11 | autoUpdater.setFeedURL('http://127.0.0.1:25565/') 12 | 13 | // 当更新发生错误的时候触发。 14 | autoUpdater.on('error', (err) => { 15 | console.log('更新出现错误', err.message) 16 | if (err.message.includes('sha512 checksum mismatch')) { 17 | this.Message(this.mainWindow, -1, 'sha512校验失败') 18 | } else { 19 | this.Message(this.mainWindow, -1, '错误信息请看主进程控制台') 20 | } 21 | }) 22 | 23 | // 当开始检查更新的时候触发 24 | autoUpdater.on('checking-for-update', () => { 25 | console.log('开始检查更新') 26 | this.Message(this.mainWindow, 0) 27 | }) 28 | 29 | // 发现可更新数据时 30 | autoUpdater.on('update-available', () => { 31 | console.log('有更新') 32 | this.Message(this.mainWindow, 1) 33 | }) 34 | 35 | // 没有可更新数据时 36 | autoUpdater.on('update-not-available', () => { 37 | console.log('没有更新') 38 | this.Message(this.mainWindow, 2) 39 | }) 40 | 41 | // 下载监听 42 | autoUpdater.on('download-progress', (progressObj) => { 43 | this.Message(this.mainWindow, 3, `${progressObj}`) 44 | }) 45 | 46 | // 下载完成 47 | autoUpdater.on('update-downloaded', () => { 48 | console.log('下载完成') 49 | this.Message(this.mainWindow, 4) 50 | }) 51 | } 52 | // 负责向渲染进程发送信息 53 | Message(mainWindow: BrowserWindow, type: number, data?: string) { 54 | const senddata = { 55 | state: type, 56 | msg: data || '', 57 | } 58 | webContentSend.updateMsg(mainWindow.webContents, senddata) 59 | } 60 | 61 | // 执行自动更新检查 62 | checkUpdate(mainWindow: BrowserWindow) { 63 | this.mainWindow = mainWindow 64 | autoUpdater.checkForUpdates().catch((err) => { 65 | console.log('网络连接问题', err) 66 | }) 67 | } 68 | // 退出并安装 69 | quitAndInstall() { 70 | autoUpdater.quitAndInstall() 71 | } 72 | } 73 | 74 | export default Update 75 | -------------------------------------------------------------------------------- /src/main/services/download-file.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, dialog } from "electron"; 2 | import { join } from "path"; 3 | import { arch, platform } from "os"; 4 | import { stat, remove } from "fs-extra"; 5 | import packageInfo from "../../../package.json"; 6 | import { webContentSend } from "./web-content-send"; 7 | 8 | /** 9 | * 10 | * @description 11 | * @returns {void} 下载类 12 | * @param {mainWindow} 主窗口 13 | * @param {downloadUrl} 下载地址,当未传入时则会使用预先设置好的baseUrl拼接名称 14 | * @author Sky 15 | * @date 2020-08-12 16 | */ 17 | 18 | class Main { 19 | public mainWindow: BrowserWindow = null; 20 | public downloadUrl: string = ""; 21 | public version: string = packageInfo.version; 22 | public baseUrl: string = ""; 23 | public Sysarch: string = arch().includes("64") ? "win64" : "win32"; 24 | public HistoryFilePath = join( 25 | app.getPath("downloads"), 26 | platform().includes("win32") 27 | ? `electron_${this.version}_${this.Sysarch}.exe` 28 | : `electron_${this.version}_mac.dmg` 29 | ); 30 | 31 | constructor(mainWindow: BrowserWindow, downloadUrl?: string) { 32 | this.mainWindow = mainWindow; 33 | this.downloadUrl = 34 | downloadUrl || platform().includes("win32") 35 | ? this.baseUrl + 36 | `electron_${this.version}_${this.Sysarch}.exe?${new Date().getTime()}` 37 | : this.baseUrl + 38 | `electron_${this.version}_mac.dmg?${new Date().getTime()}`; 39 | } 40 | 41 | start() { 42 | // 更新时检查有无同名文件,若有就删除,若无就开始下载 43 | stat(this.HistoryFilePath, async (err, stats) => { 44 | try { 45 | if (stats) { 46 | await remove(this.HistoryFilePath); 47 | } 48 | this.mainWindow.webContents.downloadURL(this.downloadUrl); 49 | } catch (error) { 50 | console.log(error); 51 | } 52 | }); 53 | this.mainWindow.webContents.session.on( 54 | "will-download", 55 | (event: any, item: any, webContents: any) => { 56 | const filePath = join(app.getPath("downloads"), item.getFilename()); 57 | item.setSavePath(filePath); 58 | item.on("updated", (event: any, state: String) => { 59 | switch (state) { 60 | case "progressing": 61 | webContentSend.DownloadProgress( 62 | this.mainWindow.webContents, 63 | Number( 64 | ( 65 | (item.getReceivedBytes() / item.getTotalBytes()) * 66 | 100 67 | ).toFixed(0) 68 | ) 69 | ); 70 | break; 71 | default: 72 | webContentSend.DownloadError(this.mainWindow.webContents, true); 73 | dialog.showErrorBox( 74 | "下载出错", 75 | "由于网络或其他未知原因导致下载出错" 76 | ); 77 | break; 78 | } 79 | }); 80 | item.once("done", (event: any, state: String) => { 81 | switch (state) { 82 | case "completed": 83 | const data = { 84 | filePath, 85 | }; 86 | webContentSend.DownloadDone(this.mainWindow.webContents, data); 87 | break; 88 | case "interrupted": 89 | webContentSend.DownloadError(this.mainWindow.webContents, true); 90 | dialog.showErrorBox( 91 | "下载出错", 92 | "由于网络或其他未知原因导致下载出错." 93 | ); 94 | break; 95 | default: 96 | break; 97 | } 98 | }); 99 | } 100 | ); 101 | } 102 | } 103 | 104 | export default Main; 105 | -------------------------------------------------------------------------------- /src/main/services/hot-updater.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * power by biuuu 3 | */ 4 | 5 | import { emptyDir, createWriteStream, readFile, copy, remove } from 'fs-extra' 6 | import { join, resolve } from 'path' 7 | import { promisify } from 'util' 8 | import { pipeline } from 'stream' 9 | import { app, BrowserWindow } from 'electron' 10 | import { gt } from 'semver' 11 | import { createHmac } from 'crypto' 12 | import AdmZip from 'adm-zip' 13 | import { version } from '../../../package.json' 14 | import { hotPublishConfig } from '../config/hot-publish' 15 | import axios, { AxiosResponse } from 'axios' 16 | import { webContentSend } from './web-content-send' 17 | 18 | const streamPipeline = promisify(pipeline) 19 | const appPath = app.getAppPath() 20 | const updatePath = resolve(appPath, '..', '..', 'update') 21 | const request = axios.create() 22 | 23 | /** 24 | * @param data 文件流 25 | * @param type 类型,默认sha256 26 | * @param key 密钥,用于匹配计算结果 27 | * @returns {string} 计算结果 28 | * @author umbrella22 29 | * @date 2021-03-05 30 | */ 31 | function hash(data: Buffer, type = 'sha256', key = 'Sky'): string { 32 | const hmac = createHmac(type, key) 33 | hmac.update(data) 34 | return hmac.digest('hex') 35 | } 36 | 37 | /** 38 | * @param url 下载地址 39 | * @param filePath 文件存放地址 40 | * @returns {void} 41 | * @author umbrella22 42 | * @date 2021-03-05 43 | */ 44 | async function download(url: string, filePath: string): Promise { 45 | const res = await request({ url, responseType: 'stream' }) 46 | await streamPipeline(res.data, createWriteStream(filePath)) 47 | } 48 | 49 | const updateInfo = { 50 | status: 'init', 51 | message: '', 52 | } 53 | 54 | interface Res extends AxiosResponse { 55 | data: { 56 | version: string 57 | name: string 58 | hash: string 59 | } 60 | } 61 | 62 | /** 63 | * @param windows 指主窗口 64 | * @returns {void} 65 | * @author umbrella22 66 | * @date 2021-03-05 67 | */ 68 | export const updater = async (windows?: BrowserWindow): Promise => { 69 | try { 70 | const res: Res = await request({ 71 | url: `${hotPublishConfig.url}/${ 72 | hotPublishConfig.configName 73 | }.json?time=${new Date().getTime()}`, 74 | }) 75 | if (gt(res.data.version, version)) { 76 | await emptyDir(updatePath) 77 | const filePath = join(updatePath, res.data.name) 78 | updateInfo.status = 'downloading' 79 | if (windows) 80 | webContentSend.HotUpdateStatus(windows.webContents, updateInfo) 81 | await download(`${hotPublishConfig.url}/${res.data.name}`, filePath) 82 | const buffer = await readFile(filePath) 83 | const sha256 = hash(buffer) 84 | if (sha256 !== res.data.hash) throw new Error('sha256 error') 85 | const appPathTemp = join(updatePath, 'temp') 86 | const zip = new AdmZip(filePath) 87 | zip.extractAllTo(appPathTemp, true, true) 88 | updateInfo.status = 'moving' 89 | if (windows) 90 | webContentSend.HotUpdateStatus(windows.webContents, updateInfo) 91 | await remove(join(`${appPath}`, 'dist')) 92 | await remove(join(`${appPath}`, 'package.json')) 93 | await copy(appPathTemp, appPath) 94 | updateInfo.status = 'finished' 95 | if (windows) 96 | webContentSend.HotUpdateStatus(windows.webContents, updateInfo) 97 | } 98 | } catch (error) { 99 | updateInfo.status = 'failed' 100 | updateInfo.message = error 101 | 102 | if (windows) webContentSend.HotUpdateStatus(windows.webContents, updateInfo) 103 | } 104 | } 105 | 106 | export const getUpdateInfo = () => updateInfo 107 | -------------------------------------------------------------------------------- /src/main/services/ipc-main-handle.ts: -------------------------------------------------------------------------------- 1 | import { dialog, BrowserWindow, app } from 'electron' 2 | import { getPreloadFile, winURL } from '../config/static-path' 3 | import { updater } from '../services/hot-updater' 4 | import DownloadFile from '../services/download-file' 5 | import Update from '../services/check-update' 6 | import config from '@config/index' 7 | import { IIpcMainHandle } from '@ipcManager/index' 8 | import { webContentSend } from './web-content-send' 9 | 10 | export class IpcMainHandleClass implements IIpcMainHandle { 11 | private allUpdater: Update 12 | constructor() { 13 | this.allUpdater = new Update() 14 | } 15 | StartDownload: ( 16 | event: Electron.IpcMainInvokeEvent, 17 | args: string, 18 | ) => void | Promise = (event, downloadUrl) => { 19 | const windwos = BrowserWindow.fromWebContents(event.sender) 20 | if (!windwos) return 21 | new DownloadFile(windwos, downloadUrl).start() 22 | } 23 | StartServer: ( 24 | event: Electron.IpcMainInvokeEvent, 25 | ) => string | Promise = async () => { 26 | dialog.showErrorBox('error', 'API is obsolete') 27 | return 'API is obsolete' 28 | } 29 | StopServer: (event: Electron.IpcMainInvokeEvent) => string | Promise = 30 | async () => { 31 | dialog.showErrorBox('error', 'API is obsolete') 32 | return 'API is obsolete' 33 | } 34 | HotUpdate: (event: Electron.IpcMainInvokeEvent) => void | Promise = ( 35 | event, 36 | ) => { 37 | const windows = BrowserWindow.fromWebContents(event.sender) 38 | if (!windows) return 39 | updater(windows) 40 | } 41 | OpenWin: ( 42 | event: Electron.IpcMainInvokeEvent, 43 | args: { url: string; IsPay?: boolean; PayUrl?: string; sendData?: unknown }, 44 | ) => void | Promise = (event, arg) => { 45 | const childWin = new BrowserWindow({ 46 | titleBarStyle: config.IsUseSysTitle ? 'default' : 'hidden', 47 | height: 595, 48 | useContentSize: true, 49 | width: 1140, 50 | autoHideMenuBar: true, 51 | minWidth: 842, 52 | frame: config.IsUseSysTitle, 53 | show: false, 54 | webPreferences: { 55 | sandbox: false, 56 | webSecurity: false, 57 | // 如果是开发模式可以使用devTools 58 | devTools: process.env.NODE_ENV === 'development', 59 | // 在macos中启用橡皮动画 60 | scrollBounce: process.platform === 'darwin', 61 | preload: getPreloadFile('main-preload'), 62 | }, 63 | }) 64 | // 开发模式下自动开启devtools 65 | if (process.env.NODE_ENV === 'development') { 66 | childWin.webContents.openDevTools({ mode: 'undocked', activate: true }) 67 | } 68 | childWin.loadURL(winURL + `#${arg.url}`) 69 | childWin.once('ready-to-show', () => { 70 | childWin.show() 71 | if (arg.IsPay) { 72 | // 检查支付时候自动关闭小窗口 73 | const testUrl = setInterval(() => { 74 | const Url = childWin.webContents.getURL() 75 | if (arg.PayUrl && Url.includes(arg.PayUrl)) { 76 | childWin.close() 77 | } 78 | }, 1200) 79 | childWin.on('close', () => { 80 | clearInterval(testUrl) 81 | }) 82 | } 83 | }) 84 | // 渲染进程显示时触发 85 | childWin.once('show', () => { 86 | webContentSend.SendDataTest(childWin.webContents, arg.sendData) 87 | }) 88 | } 89 | 90 | IsUseSysTitle: ( 91 | event: Electron.IpcMainInvokeEvent, 92 | ) => boolean | Promise = async () => { 93 | return config.IsUseSysTitle 94 | } 95 | AppClose: (event: Electron.IpcMainInvokeEvent) => void | Promise = ( 96 | event, 97 | ) => { 98 | app.quit() 99 | } 100 | CheckUpdate: (event: Electron.IpcMainInvokeEvent) => void | Promise = ( 101 | event, 102 | ) => { 103 | const windows = BrowserWindow.fromWebContents(event.sender) 104 | if (!windows) return 105 | this.allUpdater.checkUpdate(windows) 106 | } 107 | ConfirmUpdate: (event: Electron.IpcMainInvokeEvent) => void | Promise = 108 | () => { 109 | this.allUpdater.quitAndInstall() 110 | } 111 | OpenMessagebox: ( 112 | event: Electron.IpcMainInvokeEvent, 113 | args: Electron.MessageBoxOptions, 114 | ) => 115 | | Electron.MessageBoxReturnValue 116 | | Promise = async (event, arg) => { 117 | const res = await dialog.showMessageBox( 118 | BrowserWindow.fromWebContents(event.sender), 119 | { 120 | type: arg.type || 'info', 121 | title: arg.title || '', 122 | buttons: arg.buttons || [], 123 | message: arg.message || '', 124 | noLink: arg.noLink || true, 125 | }, 126 | ) 127 | return res 128 | } 129 | OpenErrorbox: ( 130 | event: Electron.IpcMainInvokeEvent, 131 | arg: { title: string; message: string }, 132 | ) => void | Promise = (event, arg) => { 133 | dialog.showErrorBox(arg.title, arg.message) 134 | } 135 | } 136 | -------------------------------------------------------------------------------- /src/main/services/ipc-main.ts: -------------------------------------------------------------------------------- 1 | // todo 是否将ipc-main.ts文件中的代码拆分到多个文件中?通过abstract继承?或者注册回调函数? 2 | import { ipcMain } from 'electron' 3 | import { IpcMainHandleClass } from './ipc-main-handle' 4 | 5 | export const useMainDefaultIpc = () => { 6 | return { 7 | defaultIpc: () => { 8 | const ipcMainHandle = new IpcMainHandleClass() 9 | Object.entries(ipcMainHandle).forEach( 10 | ([ipcChannelName, ipcListener]: [string, () => void]) => { 11 | console.log('已挂载ipcChannelName:', ipcChannelName) 12 | if (typeof ipcListener === 'function') { 13 | ipcMain.handle(ipcChannelName, ipcListener) 14 | } 15 | }, 16 | ) 17 | }, 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/services/web-content-send.ts: -------------------------------------------------------------------------------- 1 | import { IWebContentSend } from '@ipcManager/index' 2 | 3 | export const webContentSend: IWebContentSend = new Proxy( 4 | {}, 5 | { 6 | get(target, channel: keyof IWebContentSend) { 7 | return (webContents: Electron.WebContents, args: unknown) => { 8 | webContents.send(channel, args) 9 | } 10 | }, 11 | }, 12 | ) as IWebContentSend 13 | -------------------------------------------------------------------------------- /src/main/services/window-manager.ts: -------------------------------------------------------------------------------- 1 | import config from '@config/index' 2 | import { BrowserWindow, Details, dialog } from 'electron' 3 | import { winURL, loadingURL, getPreloadFile } from '../config/static-path' 4 | import { useProcessException } from '@main/hooks/exception-hook' 5 | 6 | class MainInit { 7 | public winURL: string = '' 8 | public shartURL: string = '' 9 | public loadWindow: BrowserWindow | null = null 10 | public mainWindow: BrowserWindow | null = null 11 | private childProcessGone: ( 12 | window: BrowserWindow, 13 | listener?: ( 14 | event: { preventDefault: () => void; readonly defaultPrevented: boolean }, 15 | details: Details, 16 | ) => void, 17 | ) => void 18 | 19 | constructor() { 20 | const { childProcessGone } = useProcessException() 21 | this.winURL = winURL 22 | this.shartURL = loadingURL 23 | this.childProcessGone = childProcessGone 24 | } 25 | // 主窗口函数 26 | createMainWindow() { 27 | this.mainWindow = new BrowserWindow({ 28 | titleBarOverlay: { 29 | color: '#fff', 30 | }, 31 | titleBarStyle: config.IsUseSysTitle ? 'default' : 'hidden', 32 | height: 800, 33 | useContentSize: true, 34 | width: 1700, 35 | minWidth: 1366, 36 | show: false, 37 | frame: config.IsUseSysTitle, 38 | webPreferences: { 39 | sandbox: false, 40 | webSecurity: false, 41 | // 如果是开发模式可以使用devTools 42 | devTools: process.env.NODE_ENV === 'development', 43 | // 在macos中启用橡皮动画 44 | scrollBounce: process.platform === 'darwin', 45 | preload: getPreloadFile('main-preload'), 46 | }, 47 | }) 48 | 49 | // 加载主窗口 50 | this.mainWindow.loadURL(this.winURL) 51 | // dom-ready之后显示界面 52 | this.mainWindow.once('ready-to-show', () => { 53 | this.mainWindow!.show() 54 | if (config.UseStartupChart) this.loadWindow!.destroy() 55 | }) 56 | // 开发模式下自动开启devtools 57 | if (process.env.NODE_ENV === 'development') { 58 | this.mainWindow.webContents.openDevTools({ 59 | mode: 'undocked', 60 | activate: true, 61 | }) 62 | } 63 | // 不知道什么原因,反正就是这个窗口里的页面触发了假死时执行 64 | this.mainWindow.on('unresponsive', () => { 65 | dialog 66 | .showMessageBox(this.mainWindow, { 67 | type: 'warning', 68 | title: '警告', 69 | buttons: ['重载', '退出'], 70 | message: '图形化进程失去响应,是否等待其恢复?', 71 | noLink: true, 72 | }) 73 | .then((res) => { 74 | if (res.response === 0) this.mainWindow!.reload() 75 | else this.mainWindow!.close() 76 | }) 77 | }) 78 | /** 79 | * 新的gpu崩溃检测,详细参数详见:http://www.electronjs.org/docs/api/app 80 | * @returns {void} 81 | * @author zmr (umbrella22) 82 | * @date 2020-11-27 83 | */ 84 | this.childProcessGone(this.mainWindow) 85 | this.mainWindow.on('closed', () => { 86 | this.mainWindow = null 87 | }) 88 | } 89 | // 加载窗口函数 90 | loadingWindow(loadingURL: string) { 91 | this.loadWindow = new BrowserWindow({ 92 | width: 400, 93 | height: 600, 94 | frame: false, 95 | skipTaskbar: true, 96 | transparent: true, 97 | resizable: false, 98 | webPreferences: { 99 | experimentalFeatures: true, 100 | preload: getPreloadFile('main-preload'), 101 | }, 102 | }) 103 | 104 | this.loadWindow.loadURL(loadingURL) 105 | this.loadWindow.show() 106 | this.loadWindow.setAlwaysOnTop(true) 107 | // 延迟两秒可以根据情况后续调快,= =,就相当于个,sleep吧,就那种。 = =。。。 108 | setTimeout(() => { 109 | this.createMainWindow() 110 | }, 1500) 111 | } 112 | // 初始化窗口函数 113 | initWindow() { 114 | if (config.UseStartupChart) { 115 | return this.loadingWindow(this.shartURL) 116 | } else { 117 | return this.createMainWindow() 118 | } 119 | } 120 | } 121 | export default MainInit 122 | -------------------------------------------------------------------------------- /src/preload/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, shell } from 'electron' 2 | import { platform, release, arch } from 'os' 3 | import { IpcChannelMainClass, IpcChannelRendererClass } from '@ipcManager/index' 4 | 5 | function getIpcRenderer() { 6 | const IpcRenderer: Record = {} 7 | Object.keys(new IpcChannelMainClass()).forEach((channel) => { 8 | IpcRenderer[channel] = { 9 | invoke: async (args: any) => ipcRenderer.invoke(channel, args), 10 | } 11 | }) 12 | Object.keys(new IpcChannelRendererClass()).forEach((channel) => { 13 | IpcRenderer[channel] = { 14 | on: (listener: (...args: any[]) => void) => { 15 | ipcRenderer.removeListener(channel, listener) 16 | ipcRenderer.on(channel, listener) 17 | }, 18 | once: (listener: (...args: any[]) => void) => { 19 | ipcRenderer.removeListener(channel, listener) 20 | ipcRenderer.once(channel, listener) 21 | }, 22 | removeAllListeners: () => ipcRenderer.removeAllListeners(channel), 23 | } 24 | }) 25 | return IpcRenderer 26 | } 27 | 28 | contextBridge.exposeInMainWorld('ipcRendererChannel', getIpcRenderer()) 29 | 30 | contextBridge.exposeInMainWorld('systemInfo', { 31 | platform: platform(), 32 | release: release(), 33 | arch: arch(), 34 | }) 35 | 36 | contextBridge.exposeInMainWorld('shell', shell) 37 | 38 | contextBridge.exposeInMainWorld('crash', { 39 | start: () => { 40 | process.crash() 41 | }, 42 | }) 43 | -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/api/login.ts: -------------------------------------------------------------------------------- 1 | // 仅示例 2 | import request from '@renderer/utils/request' 3 | 4 | // export function login (data) { 5 | // return request({ 6 | // url: '/user/login', 7 | // method: 'post', 8 | // data 9 | // }) 10 | // } 11 | 12 | // export function getInfo (token) { 13 | // return request({ 14 | // url: '/user/info', 15 | // method: 'get', 16 | // params: { token } 17 | // }) 18 | // } 19 | 20 | export function message() { 21 | return request({ 22 | url: '/message', 23 | method: 'get' 24 | }) 25 | } 26 | -------------------------------------------------------------------------------- /src/renderer/assets/404_images/404.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/src/renderer/assets/404_images/404.png -------------------------------------------------------------------------------- /src/renderer/assets/404_images/404_cloud.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/src/renderer/assets/404_images/404_cloud.png -------------------------------------------------------------------------------- /src/renderer/assets/icons/svg/electron-logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 529 | 530 | -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/umbrella22/electron-vue-template/c6af08342df313bed311eba153761c9d104f6e89/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/components/title-bar/title-bar.vue: -------------------------------------------------------------------------------- 1 | 17 | 18 | 32 | 33 | 102 | -------------------------------------------------------------------------------- /src/renderer/error.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import { nextTick } from "vue" 3 | export const errorHandler = (App: App) => { 4 | App.config.errorHandler = (err, vm, info) => { 5 | nextTick(() => { 6 | if (process.env.NODE_ENV === 'development') { 7 | console.group('%c >>>>>> 错误信息 >>>>>>', 'color:red') 8 | console.log(`%c ${info}`, 'color:blue') 9 | console.groupEnd() 10 | console.group('%c >>>>>> 发生错误的Vue 实例对象 >>>>>>', 'color:green') 11 | console.log(vm) 12 | console.groupEnd() 13 | console.group('%c >>>>>> 发生错误的原因及位置 >>>>>>', 'color:red') 14 | console.error(err) 15 | console.groupEnd() 16 | } 17 | }) 18 | } 19 | 20 | } -------------------------------------------------------------------------------- /src/renderer/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref } from 'vue' 2 | 3 | export const globalLang = ref('zh-cn') 4 | export function loadLanguages() { 5 | const context = import.meta.webpackContext('./languages', { 6 | recursive: false, 7 | regExp: /\.ts$/, 8 | }) 9 | const languages: any = {} 10 | context.keys().forEach((key: string) => { 11 | if (key === './index.ts') return 12 | const lang = context(key).lang 13 | const name = key.replace(/^\.\//, '').replace(/\.ts$/, '') 14 | languages[name] = lang 15 | }) 16 | 17 | return languages 18 | } 19 | 20 | export const i18nt = computed(() => { 21 | const lang = loadLanguages() 22 | return lang[globalLang.value] 23 | }) 24 | 25 | export function setLanguage(locale: string) { 26 | globalLang.value = locale 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/i18n/languages/en.ts: -------------------------------------------------------------------------------- 1 | export const lang = { 2 | welcome: "Welcome use the framework", 3 | buttonTips: "You can click buttons to experience", 4 | waitDataLoading: "Wait data loading", 5 | about: { 6 | system: "About system", 7 | language: "language:", 8 | languageValue: "English", 9 | currentPagePath: "current page path:", 10 | currentPageName: "current page name:", 11 | vueVersion: "Vue version:", 12 | electronVersion: "Electron version:", 13 | nodeVersion: "Node version:", 14 | systemPlatform: "system platform:", 15 | systemVersion: "system version:", 16 | systemArch: "system arch:", 17 | }, 18 | buttons: { 19 | console: "Console", 20 | checkUpdate: "Check update", 21 | checkUpdate2: "Check update(plan 2)", 22 | checkUpdateInc: "Check update(increment)", 23 | startServer: "Start server(obsolete)", 24 | stopServer: "Stop server(obsolete)", 25 | viewMessage: "view message", 26 | openNewWindow: "Open new window", 27 | simulatedCrash: "Simulated crash", 28 | changeLanguage: "切换语言", 29 | ForcedUpdate: "Forced Update Mode", 30 | }, 31 | }; 32 | -------------------------------------------------------------------------------- /src/renderer/i18n/languages/zh-cn.ts: -------------------------------------------------------------------------------- 1 | 2 | export const lang = { 3 | welcome: "欢迎进入本框架", 4 | buttonTips: "您可以点击的按钮测试功能", 5 | waitDataLoading: "等待数据读取", 6 | about: { 7 | system: "关于系统", 8 | language: "语言:", 9 | languageValue: "中文简体", 10 | currentPagePath: "当前页面路径:", 11 | currentPageName: "当前页面名称:", 12 | vueVersion: "Vue版本:", 13 | electronVersion: "Electron版本:", 14 | nodeVersion: "Node版本:", 15 | systemPlatform: "系统平台:", 16 | systemVersion: "系统版本:", 17 | systemArch: "系统位数:", 18 | }, 19 | buttons: { 20 | console: "控制台打印", 21 | checkUpdate: "检查更新", 22 | checkUpdate2: "检查更新(第二种方法)", 23 | checkUpdateInc: "检查更新(增量更新)", 24 | startServer: "启动内置服务端(已废弃)", 25 | stopServer: "关闭内置服务端(已废弃)", 26 | viewMessage: "查看消息", 27 | openNewWindow: "打开新窗口", 28 | simulatedCrash: "模拟崩溃", 29 | changeLanguage: "Change language", 30 | ForcedUpdate: "强制更新模式", 31 | }, 32 | }; 33 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import { createPinia } from "pinia"; 3 | 4 | import "./styles/index.scss"; 5 | import "./permission"; 6 | import App from "./App.vue"; 7 | import router from "./router"; 8 | import { errorHandler } from "./error"; 9 | import "./utils/hackIpcRenderer"; 10 | 11 | const app = createApp(App); 12 | const store = createPinia(); 13 | app.use(router); 14 | app.use(store); 15 | errorHandler(app); 16 | 17 | app.mount("#app"); 18 | -------------------------------------------------------------------------------- /src/renderer/permission.ts: -------------------------------------------------------------------------------- 1 | import router from './router' 2 | import Performance from '@renderer/utils/performance' 3 | 4 | var end = null 5 | router.beforeEach((to, from, next) => { 6 | end = Performance.startExecute(`${from.path} => ${to.path} 路由耗时`) /// 路由性能监控 7 | next() 8 | setTimeout(() => { 9 | end() 10 | }, 0) 11 | }) 12 | 13 | router.afterEach(() => { }) 14 | -------------------------------------------------------------------------------- /src/renderer/public/loader.html: -------------------------------------------------------------------------------- 1 |  2 | 3 | 4 | 5 | 6 | 7 | 8 | 156 | 157 | 158 | 159 |
160 | 164 |
165 | 166 | 167 | 168 | 169 |
170 |
正在准备资源中...
171 |
172 | 173 | 174 | 175 | -------------------------------------------------------------------------------- /src/renderer/router/constant-router-map.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from 'vue-router' 2 | import notFound from '@renderer/views/404.vue' 3 | import landingPage from '@renderer/views/landing-page/landing-page.vue' 4 | 5 | const routes: Array = [ 6 | { path: '/:pathMatch(.*)*', component: notFound }, 7 | { path: '/', name: '总览', component: landingPage }, 8 | ] 9 | 10 | export default routes 11 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import routerMap from './constant-router-map' 3 | 4 | export default createRouter({ 5 | history: createWebHashHistory(), 6 | routes: routerMap 7 | }) -------------------------------------------------------------------------------- /src/renderer/store/modules/template.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | interface StateType { 4 | testData: string 5 | } 6 | 7 | export const useStoreTemplate = defineStore('template',{ 8 | state: (): StateType => ({ 9 | testData: localStorage.getItem('testData') || '' 10 | }), 11 | getters: { 12 | getTest: (state): string => state.testData, 13 | getTest1(): string { 14 | return this.testData 15 | } 16 | }, 17 | actions: { 18 | TEST_ACTION(data: string) { 19 | this.testData = data 20 | localStorage.setItem('testData', data) 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/renderer/styles/index.scss: -------------------------------------------------------------------------------- 1 | @import './transition.css'; 2 | 3 | html, 4 | body, 5 | #app { 6 | width: 100%; 7 | height: 100%; 8 | } 9 | 10 | body { 11 | -moz-osx-font-smoothing: grayscale; 12 | -webkit-font-smoothing: antialiased; 13 | text-rendering: optimizeLegibility; 14 | font-family: Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif; 15 | margin: 0; 16 | padding: 0; 17 | } 18 | 19 | html { 20 | box-sizing: border-box; 21 | 22 | ::-webkit-scrollbar { 23 | width: 6px; 24 | height: 6px; 25 | background-color: none; 26 | } 27 | 28 | ::-webkit-scrollbar-track { 29 | background-color: none; 30 | } 31 | 32 | ::-webkit-scrollbar-thumb { 33 | border-radius: 10px; 34 | background-color: #555; 35 | } 36 | } 37 | 38 | *, 39 | *:before, 40 | *:after { 41 | box-sizing: inherit; 42 | } 43 | 44 | div:focus { 45 | outline: none; 46 | } 47 | 48 | a:focus, 49 | a:active { 50 | outline: none; 51 | } 52 | 53 | a, 54 | a:focus, 55 | a:hover { 56 | cursor: pointer; 57 | color: inherit; 58 | text-decoration: none; 59 | } 60 | 61 | .clearfix { 62 | &:after { 63 | visibility: hidden; 64 | display: block; 65 | font-size: 0; 66 | content: " "; 67 | clear: both; 68 | height: 0; 69 | } 70 | } 71 | 72 | //main-container全局样式 73 | .app-main { 74 | min-height: 100% 75 | } 76 | 77 | .app-container { 78 | padding: 20px; 79 | } -------------------------------------------------------------------------------- /src/renderer/styles/transition.css: -------------------------------------------------------------------------------- 1 | //globl transition css 2 | 3 | /*fade*/ 4 | .fade-enter-active, 5 | .fade-leave-active { 6 | transition: opacity 0.28s; 7 | } 8 | 9 | .fade-enter, 10 | .fade-leave-active { 11 | opacity: 0; 12 | } 13 | 14 | /*fade*/ 15 | .breadcrumb-enter-active, 16 | .breadcrumb-leave-active { 17 | transition: all .5s; 18 | } 19 | 20 | .breadcrumb-enter, 21 | .breadcrumb-leave-active { 22 | opacity: 0; 23 | transform: translateX(20px); 24 | } 25 | 26 | .breadcrumb-move { 27 | transition: all .5s; 28 | } 29 | 30 | .breadcrumb-leave-active { 31 | position: absolute; 32 | } 33 | -------------------------------------------------------------------------------- /src/renderer/utils/hackIpcRenderer.ts: -------------------------------------------------------------------------------- 1 | function throwIpcError() { 2 | throw new Error('ipcRenderer is not available') 3 | } 4 | const IpcRendererProxyHandler = { 5 | get() { 6 | return { 7 | on: throwIpcError, 8 | once: throwIpcError, 9 | removeAllListeners: throwIpcError, 10 | invoke: throwIpcError, 11 | } 12 | }, 13 | } 14 | 15 | if (!window.ipcRendererChannel) { 16 | window.ipcRendererChannel = new Proxy({}, IpcRendererProxyHandler) as any 17 | window.systemInfo = { 18 | platform: 'web', 19 | release: 'web', 20 | arch: 'web', 21 | nodeVersion: 'web', 22 | electronVersion: 'web', 23 | } 24 | window.crash = { 25 | start: throwIpcError, 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/renderer/utils/notification.ts: -------------------------------------------------------------------------------- 1 | interface DesktopMsgProps { 2 | /** 标题 */ 3 | title: string, 4 | /** 正文 */ 5 | body: string, 6 | /** ICON */ 7 | icon?: string 8 | } 9 | 10 | /** 11 | * @export 12 | * @Author: Sky 13 | * @Date: 2019-09-29 20:23:16 14 | * @Last Modified by: Sky 15 | * @Last Modified time: 2019-09-29 21:01:24 16 | * @param {DesktopMsgProps} option 17 | * @returns 18 | * @feature 对于普通的通知只需要加入传入title,body;而对于需要图标的还需要传入icon,当然它也接受一个图片链接,当用户点击通知之后,会返回一个true 19 | * 由于是一个promise,请使用then接受 20 | **/ 21 | 22 | export function DesktopMsg (option: DesktopMsgProps): Promise { 23 | const msgfunc = new window.Notification(option.title, option) 24 | return new Promise((resolve) => { 25 | msgfunc.onclick = () => { 26 | resolve(true) 27 | } 28 | }) 29 | } 30 | -------------------------------------------------------------------------------- /src/renderer/utils/performance.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 性能工具 3 | * 1. 计算方法执行时间 4 | * @returns {void} 5 | * @date 2019-11-29 6 | */ 7 | 8 | import { memoryInfo } from "customTypes/global"; 9 | import Timer from "./timer"; 10 | 11 | class Performance { 12 | /** 13 | * 计算情况 14 | * @returns {Function} 执行返回值获取时间信息 15 | * @date 2019-11-29 16 | */ 17 | startExecute(name = ""): Function { 18 | const timer = Timer.start(); 19 | const usedJSHeapSize = this.getMemoryInfo().usedJSHeapSize; 20 | return (name2 = "") => { 21 | const executeTime = timer.stop(); 22 | const endMemoryInfo = this.getMemoryInfo(); 23 | console.log( 24 | "%cPerformance%c \n1. 路由路径:%c%s%c\n2. 执行耗时: %c%sms%c \n3. 内存波动:%sB \n4. 已分配内存: %sMB \n5. 已使用内存:%sMB \n6. 剩余内存: %sMB", 25 | "padding: 2px 4px 2px 4px; background-color: #4caf50; color: #fff; border-radius: 4px;", 26 | "", 27 | "color: #ff6f00", 28 | `${name} ${name2}`, 29 | "", 30 | "color: #ff6f00", 31 | executeTime, 32 | "", 33 | endMemoryInfo.usedJSHeapSize - usedJSHeapSize, 34 | this.toMBSize(endMemoryInfo.jsHeapSizeLimit), 35 | this.toMBSize(endMemoryInfo.usedJSHeapSize), 36 | this.toMBSize(endMemoryInfo.totalJSHeapSize) 37 | ); 38 | }; 39 | } 40 | 41 | /** 42 | * 获取内存信息 43 | * @returns {memoryInfo} 44 | * @date 2019-11-29 45 | */ 46 | 47 | getMemoryInfo(): memoryInfo { 48 | let memoryinfo = {}; 49 | if (window.performance && window.performance.memory) { 50 | memoryinfo = window.performance.memory; 51 | } 52 | return memoryinfo; 53 | } 54 | 55 | /** 56 | * 转化为MB 57 | * @returns {string} 58 | * @date 2019-11-29 59 | */ 60 | toMBSize(byteSize: number): string { 61 | return (byteSize / (1024 * 1024)).toFixed(1); 62 | } 63 | } 64 | 65 | export default new Performance(); 66 | -------------------------------------------------------------------------------- /src/renderer/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios from 'axios' 2 | const serves = axios.create({ 3 | baseURL: import.meta.env.API_HOST, 4 | timeout: 5000, 5 | }) 6 | 7 | // 设置请求发送之前的拦截器 8 | serves.interceptors.request.use( 9 | (config) => { 10 | // 设置发送之前数据需要做什么处理 11 | return config 12 | }, 13 | (err) => Promise.reject(err), 14 | ) 15 | 16 | // 设置请求接受拦截器 17 | serves.interceptors.response.use( 18 | (res) => { 19 | // 设置接受数据之后,做什么处理 20 | if (res.data.code === 50000) { 21 | // ElMessage.error(res.data.data); 22 | } 23 | return res 24 | }, 25 | (err) => { 26 | // 判断请求异常信息中是否含有超时timeout字符串 27 | if (err.message.includes('timeout')) { 28 | console.log('错误回调', err) 29 | } 30 | if (err.message.includes('Network Error')) { 31 | console.log('错误回调', err) 32 | } 33 | return Promise.reject(err) 34 | }, 35 | ) 36 | 37 | // 将serves抛出去 38 | export default serves 39 | -------------------------------------------------------------------------------- /src/renderer/utils/timer.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 计时器 3 | * 支持链式调用 4 | * timeout() 5 | * .then(()=>{ 6 | * return inTheEnd(); 7 | * }) 8 | * .then(()=>{ 9 | * return inTheEnd(); 10 | * }); 11 | * 12 | * @date 2019-11-25 13 | */ 14 | class Timer { 15 | /** 16 | * 延时操作 17 | * @returns {void} 18 | * @date 2019-11-25 19 | */ 20 | timeout(interval: number, args?: any): Promise { 21 | return new Promise((resolve) => { 22 | setTimeout(() => { 23 | resolve(args); 24 | }, interval); 25 | }); 26 | } 27 | 28 | /** 29 | * 等待代码片段执行完毕后再执行 30 | * @returns {void} 31 | * @date 2019-11-25 32 | */ 33 | inTheEnd(): Promise { 34 | return this.timeout(0); 35 | } 36 | 37 | /** 38 | * 循环定时, 执行回调后再继续下一轮循环 39 | * @param {Number} interval 执行间隔 40 | * @param {Function} [callback] 回调 41 | * @returns {Object} 42 | * @date 2019-11-25 43 | */ 44 | interval(interval: number, callback: Function) { 45 | this.timeout(interval).then(() => { 46 | typeof callback === "function" && 47 | callback() !== false && 48 | this.interval(interval, callback); 49 | }); 50 | return { then: (c) => (callback = c) }; 51 | } 52 | 53 | /** 54 | * 计时,单位毫秒 55 | * @returns {void} 56 | * @date 2019-11-29 57 | */ 58 | start() { 59 | const startDate = new Date(); 60 | return { 61 | stop() { 62 | const stopDate = new Date(); 63 | return stopDate.getTime() - startDate.getTime(); 64 | }, 65 | }; 66 | } 67 | } 68 | 69 | export default new Timer(); 70 | -------------------------------------------------------------------------------- /src/renderer/views/404.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 249 | -------------------------------------------------------------------------------- /src/renderer/views/landing-page/components/system-info-mation.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 63 | -------------------------------------------------------------------------------- /src/renderer/views/landing-page/landing-page.vue: -------------------------------------------------------------------------------- 1 | 60 | 61 | 234 | 235 | 336 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "resolveJsonModule": true, 5 | "baseUrl": "./", 6 | "outDir": "./dist/electron", 7 | "sourceMap": true, 8 | "declaration": false, 9 | "module": "esnext", 10 | "moduleResolution": "node", 11 | "allowSyntheticDefaultImports": true, 12 | "emitDecoratorMetadata": true, 13 | "experimentalDecorators": true, 14 | "importHelpers": true, 15 | "target": "ESNext", 16 | "paths": { 17 | "@config/*": ["config/*"], 18 | "@ipcManager/*": ["src/ipc-manager/*"], 19 | "@renderer/*": ["src/renderer/*"], 20 | "@main/*": ["src/main/*"] 21 | }, 22 | "typeRoots": ["node_modules/@types"], 23 | "lib": ["es2018", "dom"], 24 | "forceConsistentCasingInFileNames": true, 25 | "strict": true 26 | }, 27 | "include": ["src/**/*", "customTypes/*"], 28 | "exclude": ["node_modules"] 29 | } 30 | -------------------------------------------------------------------------------- /关于一些插件的记录.md: -------------------------------------------------------------------------------- 1 | - iohook
地址是:https://github.com/wilix-team/iohook 文档地址:https://wilix-team.github.io/iohook/
该库功能为全局监听键盘和鼠标事件,因为是由c++去实现的,所以可能需要rebuild。具体使用请看文档 2 | - --------------------------------------------------------------------------------