├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .vscode └── settings.json ├── LICENSE ├── README.md ├── forge.config.js ├── package.json ├── screenshot ├── after-nsis-zip-uncompressed.png ├── main.png ├── nsis-setup-1.png ├── why-not-prettier-1.jpg └── why-not-prettier-2.jpg ├── scripts ├── build.mjs ├── create-ipc.mjs ├── create-window.mjs ├── dev-server.mjs ├── private │ ├── tsc.mjs │ └── utils.mjs └── template-ts │ └── main-window │ ├── index.ts │ └── preload.ts ├── setup ├── NSIS │ ├── install.ico │ ├── license.rtf │ ├── nsis-3.08.zip │ ├── nsis-3.08 │ │ └── .gitkeep │ ├── uninstall.ico │ ├── win-setup-x64.nsi │ └── win-setup-x86.nsi └── exe.ico ├── src ├── lib │ ├── axios-inst │ │ ├── main │ │ │ └── index.ts │ │ ├── renderer │ │ │ └── index.ts │ │ └── shared │ │ │ └── index.ts │ ├── file-download │ │ ├── main │ │ │ ├── file-download-preload.ts │ │ │ └── index.ts │ │ ├── renderer │ │ │ └── index.ts │ │ └── shared │ │ │ └── index.ts │ └── utils │ │ ├── main │ │ ├── file-util.ts │ │ ├── index.ts │ │ └── utils-preload.ts │ │ ├── renderer │ │ └── index.ts │ │ └── shared │ │ ├── error-utils.ts │ │ ├── index.ts │ │ └── singleton.ts ├── main │ ├── app-state.ts │ ├── main.ts │ ├── static │ │ ├── .gitkeep │ │ ├── tray.ico │ │ └── tray.png │ ├── tray.ts │ └── windows │ │ ├── frameless │ │ ├── index.ts │ │ └── preload.ts │ │ ├── primary │ │ ├── index.ts │ │ └── preload.ts │ │ └── window-base.ts ├── renderer │ ├── .env.development │ ├── .env.production │ ├── .env.test │ ├── App.vue │ ├── components.d.ts │ ├── components │ │ └── hello-world.vue │ ├── index.html │ ├── main.ts │ ├── public │ │ ├── electron.svg │ │ ├── vite.svg │ │ └── vue.svg │ ├── router │ │ ├── index.ts │ │ └── router-map.ts │ ├── style.css │ ├── tsconfig.json │ ├── typings │ │ ├── electron.d.ts │ │ └── shims-vue.d.ts │ ├── views │ │ ├── frameless-sample.vue │ │ └── primary.vue │ └── vite.config.mjs └── tsconfig.json └── yarn.lock /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | node_modules 3 | .vscode 4 | out 5 | build 6 | dist 7 | babel.config.js 8 | **/jquery-*.min.js 9 | .eslintrc.js 10 | **/typings 11 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | root: true, 3 | env: { 4 | node: true, 5 | browser: true, 6 | }, 7 | parser: "vue-eslint-parser", 8 | parserOptions: { 9 | ecmaVersion: "latest", 10 | parser: "@typescript-eslint/parser", 11 | sourceType: "module", 12 | }, 13 | plugins: [ 14 | "vue", 15 | "@typescript-eslint" 16 | ], 17 | extends: [ 18 | "eslint:recommended", 19 | "plugin:@typescript-eslint/eslint-recommended", 20 | "plugin:@typescript-eslint/recommended", 21 | "plugin:vue/vue3-recommended", 22 | ], 23 | rules: { 24 | // override/add rules settings here, such as: 25 | "max-len": ["error", {code: 300}], 26 | "semi": ["warn", "always"], 27 | // 不能使用undefined 28 | "no-undefined": "error", 29 | // 变量初始化时不能直接给它赋值为undefined 30 | "no-undef-init": "error", 31 | "vue/no-unused-vars": "warn", 32 | // 不允许连续空格 33 | "no-multi-spaces": "error", 34 | // 空2格 switch case 35 | "indent": [ "error", 2, { "SwitchCase": 1 } ], 36 | // 对象大括号空格 { a:b } => { a:b } 37 | "object-curly-spacing": [ "error", "always" ], 38 | // 括号去除空格 foo( 'bar' ) => foo('bar'); 39 | "space-in-parens": [ "error", "never" ], 40 | // 对象分号前后只有一个空格{ "foo" : 42 } => { "foo": 42 }; 41 | "key-spacing": [ "error", { mode: "strict" } ], 42 | // 逗号前后的空格 [1, 2, 3 ,4] => [1, 2, 4, 4] 43 | "comma-spacing": [ "error", { "before": false, "after": true } ], 44 | // 括号内使用空格 [ 1,2 ] => [ 1,2 ] 45 | "array-bracket-spacing": [ "error", "always" ], 46 | // if else 风格 47 | "brace-style": [ "error", "1tbs" ], 48 | // 函数调用空格 fn () => fn() 49 | "func-call-spacing": [ "error", "never" ], 50 | // 函数左括号空格 function name () {} => function name(){} 51 | "space-before-function-paren": [ "error", "never" ], 52 | // 语句块的空格 function name() {} => function name(){} 53 | "space-before-blocks": [ "error", "never" ], 54 | // 关键字前后空格 if () => if() 55 | "keyword-spacing": [ "error", { 56 | "overrides": { 57 | "if": { "after": false, before: false }, 58 | "else": { "after": false, before: false }, 59 | } 60 | } ], 61 | // 对象取值不能有空格 obj . foo => obj.foo 62 | "no-whitespace-before-property": "error", 63 | // 最大连续空行数 64 | "no-multiple-empty-lines": [ "error", { "max": 2, "maxEOF": 1, "maxBOF": 0 } ], 65 | // 代码块中去除前后空行 66 | "padded-blocks": [ "error", "never" ], 67 | // ;前后空格 var foo ; var bar; => var foo;var bar; 68 | "semi-spacing": [ "error", { "before": false, "after": false } ], 69 | // 操作符是否空格 a=0 => a = 0 70 | "space-infix-ops": "error", 71 | // 操作符空格 + - 72 | "space-unary-ops": "error", 73 | // 箭头函数空格 ()=>{} => () => {} 74 | "arrow-spacing": [ "error", { "before": true, "after": true } ], 75 | // 扩展运算符 {... f} => {...f} 76 | "rest-spread-spacing": "error", 77 | // 字符串拼接使用模版字符串 'hello' + world => `hello${world}` 78 | "prefer-template": "off", 79 | // 模版字符串中去除空格 `${ fo }` =>${fo} 80 | "template-curly-spacing": [ "error", "never" ], 81 | // 链式调用换行 82 | "newline-per-chained-call": [ "error", { "ignoreChainWithDepth": 1 } ], 83 | // 禁止重复模块导入 84 | "no-duplicate-imports": "error", 85 | // 注释开头添加空格 86 | "spaced-comment": [ "error", "always" ], 87 | // 使用双引号,字符串中包含了一个其它引号 允许"a string containing 'single' quotes" 88 | quotes: [ "warn", "double", { "avoidEscape": true } ], 89 | "no-else-return": "error", 90 | "@typescript-eslint/no-var-requires": "off", 91 | "@typescript-eslint/no-unused-vars": "warn", 92 | "@typescript-eslint/no-explicit-any": "error", 93 | // 禁用组件名称必须多个单词的要求 94 | "vue/multi-word-component-names": "off", 95 | // vue单行语句中每行最多允许100个属性,多行属性中每行一个属性 96 | "vue/max-attributes-per-line": ["error", { 97 | "singleline": { 98 | "max": 100 99 | }, 100 | "multiline": { 101 | "max": 1 102 | } 103 | }], 104 | // 一行中最多允许4个链式调用 105 | "newline-per-chained-call": ["error", { "ignoreChainWithDepth": 4 }], 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | dist 3 | build 4 | out 5 | 6 | .idea 7 | *.exe 8 | nsis-3.08/ -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "eslint.validate": [ 3 | "javascript", 4 | "javascriptreact", 5 | "html", 6 | "vue" 7 | ], 8 | "editor.codeActionsOnSave": { 9 | "source.fixAll.eslint": "explicit" 10 | } 11 | } -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2021 Deluze 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # 1. 写在前面 2 | 已经有了那么多的 Electron 项目模板,为什么还要再造一个?是重复造轮子吗? 3 | 4 | 我相信大多数人在选择使用 Electron 开发客户端时,或多或少都看上了 Web 开发的高效率,但 Web 开发人员在客户端和系统编程方面的经验相对缺乏,又加上 Electron 和前端框架(如 Vue )结合起来也不是那么容易,因此为了节省时间,开发人员大多会选择基于模板来快速搭建Electron 项目。 5 | 6 | 目前,Electron 的模板项目已经有很多,比较流行的有 [electron-vite](https://github.com/alex8088/electron-vite)、[electron-vite-vue](https://github.com/electron-vite/electron-vite-vue) 等。在这些已有的模板中,有的功能过于完善,代码太复杂,远远超过了很多 Electron 客户端项目本身的代码量,需要花很多时间来熟悉模板,不适合新手快速上手和修改,一旦出现问题也难以维护;而有的模板又年久失修,使用的技术早已被淘汰,也不适合用来开发线上产品,而且这些模板都有一个通病,都是在用 Web 开发的思维来开发客户端,很难开发出纯正地道的客户端产品。 7 | 8 | 基于上述原因,我开发了这个 Electron 项目模板,在开发过程中,我也一直遵循稳定、易于上手和维护的初衷。 9 | 10 | # 2. electron-vue3-boilerplate 11 | 12 | 基于 **Vue3** + **Electron** + **TypeScript** 的客户端程序模板,使用 **Vite** 和 **Electron Forge** 构建和打包。 13 | 14 | 真正做到开箱即用,面向跨平台客户端设计,产品级的项目模板。 15 | 16 | ![Main UI](./screenshot/main.png) 17 | 18 | ## 2.1 特性 19 | 20 | - 使用 [ViteJS](https://vitejs.dev) 构建和驱动前端页面,支持热加载(HMR),使开发和调试变得更加高效 ⚡ 21 | - 支持 Electron 窗口快捷创建指令,并且可隔离不同窗口的 IPC 事件 💖 22 | - 简化 IPC 调用方式,并提供 IPC 函数快速创建指令,主进程与渲染进程的相互调用从未如此简单(见`utils`库) 👍 23 | - 主进程和渲染进程支持热加载 ⚡ 24 | - 精选依赖包,提升项目稳定性 25 | - 代码简洁并附有完善地中文注释,易掌控,可定制性强,小白也能快速上手 26 | - 日志文件,主进程和渲染进程可以直接写文件日志 27 | - 支持本地配置文件 28 | - 文件下载(支持后台同时下载多文件、哈希校验、进度反馈等),渲染进程亦可异步调用 👍 29 | - 功能完善的无边框窗口、程序单实例、托盘和右键菜单、axios HTTP请求...... 30 | - 基于 ESLint 的代码规范和自动格式化 31 | - 使用 Electron 官方推荐的[Electron Forge](https://www.electronforge.io/)进行客户端构建和打包 32 | - 仅打包必须的依赖组件,减少安装包体积,并精简 package.json 文件,防止信息泄露 33 | - 支持 NSIS 安装包 😎 34 | - ...... 35 | 36 | ## 2.2 快速开始 🌈 37 | 38 | 点击右上角绿色的 **Use this template** 按钮,使用该模板创建一个新的仓库并克隆到本地。 39 | 40 | **或者..** 41 | 42 | 直接克隆该项目: `git clone https://github.com/winsoft666/electron-vue3-boilerplate.git` 43 | 44 | ### 2.2.1 Visual Studio Code 45 | 46 | 推荐使用 `Visual Studio Code` 进行项目开发,并安装如下插件: 47 | 48 | - ESLint 49 | - Vue - Official 50 | 51 | ### 2.2.2 安装依赖 ⏬ 52 | 53 | ```bash 54 | yarn install 55 | ``` 56 | 57 | > 国内用户可以将 yarn 源设置为国内源,避免因网络问题导致安装依赖失败,设置方法可以参考:[《NPM和Yarn设置国内源》](https://jiangxueqiao.com/post/908211703.html) 58 | > 59 | > 在执行 build 命令打包时,electron-forge 会下载一些依赖组件,国内用户可能因网络问题下载失败。可以使用全局代理,并在终端中通过如下命令设置环境变量(命令中的IP和端口根据实际情况进行修改): 60 | > 61 | > set HTTP_PROXY=http://127.0.0.1:7890 62 | > 63 | > set HTTPS_PROXY=http://127.0.0.1:7890 64 | 65 | ### 2.2.3 开发 ⚒️ 66 | 67 | ```bash 68 | yarn run dev # 以开发环境启动应用并支持热加载 69 | yarn run test # 以测试环境启动应用并支持热加载 70 | yarn run production # 以生产环境启动应用并支持热加载 71 | ``` 72 | 73 | ### 2.2.4 其他命令 74 | 75 | ```bash 76 | yarn run build # 构建应用,可发布的包位于"out\make"目录 77 | 78 | # 或者 79 | yarn run build:win32 # 构建Windows平台 32位应用 80 | yarn run build:win64 # 构建Windows平台 64位应用 81 | yarn run build:mac # 构建macOS平台应用 82 | yarn run build:linux # 构建Linux平台应用 83 | 84 | yarn run new:page # 创建新的Vue页面 85 | yarn run new:window # 创建新的Electron窗口 86 | ``` 87 | 88 | 更多的可选配置项可以参考 [Electron Forge CLI docs](https://www.electronforge.io/cli)。 89 | 90 | ### 2.2.5 NSIS安装包 🪟 91 | 92 | > 这一步是可选的。 93 | > 94 | > NSIS 只支持生成 Windows 平台安装包,如果您不需要使用生成 NSIS 安装包,可以跳过该节。 95 | > 96 | > 更多NSIS介绍,可以查看我的 NSIS 教程:[《打包狂魔之NSIS教程》](https://blog.csdn.net/china_jeffery/category_9271543.html) 97 | 98 | **首先需要将`setup\NSIS\nsis-3.08.zip`文件解压到当前目录,即将文件释放到 nsis-3.08 目录,解压后的 nsis-3.08 目录结构如下:** 99 | 100 | ![scrennshot-after-nsis-zip-uncomoressed](./screenshot/after-nsis-zip-uncompressed.png) 101 | 102 | 运行如下命令构建Windows平台 32位应用并使用NSIS生成安装包: 103 | 104 | ```bash 105 | yarn run build:nsis-win32 106 | ``` 107 | 108 | 运行如下命令构建Windows平台 64位应用并使用NSIS生成安装包: 109 | 110 | ```bash 111 | yarn run build:nsis-win64 112 | ``` 113 | 114 | 生成的安装包位于`setup\NSIS\`目录。 115 | 116 | NSIS安装界面截图: 117 | 118 | ![NSIS Setup UI](./screenshot/nsis-setup-1.png) 119 | 120 | NSIS安装包支持完全定制化,如需定制,可以修改`setup\NSIS\win-setup-*.nsi`文件,但请注意NSIS脚本文件需要以ANSI编码格式保存。 121 | 122 | # 3. 项目介绍 123 | ## 3.1 工程结构 🌳 124 | 125 | ```yaml 126 | - scripts/ # 该目录中的脚本用构建应用程序和驱动前端页面 127 | - screenshots # 本文档中用到的截图 128 | - setup/ # 存储编译和构建相关文件 129 | - NSIS/ # NSIS安装包脚本 130 | - install.ico # NSIS安装包图标 131 | - uninstall.ico # NSIS卸载程序图标 132 | - exe.ico # 构建后的可执行文件图标(非安装包图标) 133 | - src/ 134 | - lib/ # 公共库,为了方便修改,未做成独立的包 135 | - file-download/ # 文件下载库 136 | - main # 仅供主进程使用 137 | - renderer # 仅供渲染进程使用 138 | - shared # 主进程和渲染进程都可以使用 139 | - utils/ # 公共代码库 140 | - main/ # 主进程的代码 (Electron) 141 | - static/ # 静态资源 142 | - windows/ # 多窗口文件夹 (每个子目录表示一个窗口) 143 | - primary/ # 主窗口(客户端通常都会有一个主窗口) 144 | - frameless/ # 无边框示例窗口 145 | - ... 146 | - renderer/ # 渲染进程的代码 (VueJS) 147 | - public # 静态资源 148 | - router # 定义路由 149 | - typings/ # ts声明文件 150 | - views/ # 视图 151 | - primary.vue # 主窗口 152 | - frameless-sample.vue # 无边框示例窗口 153 | - ... 154 | ``` 155 | 156 | ## 3.2 使用静态文件 157 | 158 | - `src/main/static`目录存放主进程使用的静态文件。 159 | - `src/renderer/public`目录存放渲染进程使用的静态文件。 160 | 161 | #### 在主进程中引用静态文件 162 | 163 | ```ts 164 | // 假设 src/main/static/tray.ico 文件存在 165 | // 使用 appState.mainStaticPath 属性获取主进程的静态文件存储目录 166 | import path from "path"; 167 | import appState from "./app-state"; 168 | 169 | const iconPath = path.join(appState.mainStaticPath, "tray.ico"); 170 | ``` 171 | 172 | ## 3.3 AppState对象 173 | 为了方便在主进程中跨模块访问某些对象(如`primaryWindow`、`tray`、`cfgStore`等)和应用配置(如`onlyAllowSingleInstance`等),我们定义了单实例对象AppState 来存储这些数据。 174 | 175 | 使用方法如下: 176 | 177 | ```javascript 178 | import appState from "./app-state"; 179 | 180 | appState.primaryWindow?.show(); 181 | ``` 182 | 183 | 更多与应用有关的对象和配置,请查看 [app-state.ts](./src/main/app-state.ts) 184 | 185 | ### 3.3.1 应用环境 186 | 187 | 本模板预置了三种应用环境:开发环境、测试环境、生产环境,在Electron和Vue均可获取当前运行环境: 188 | 189 | ```javascript 190 | // Electron 191 | appState.appEnv 192 | 193 | // Vue 194 | import.meta.env.MODE 195 | ``` 196 | 197 | ### 3.3.2 环境变量 198 | 199 | 如果需要针对不同应用环境为 Vue 添加环境变量,可以在`scr\renderer\.env.*`文件中添加,如: 200 | 201 | ```javascript 202 | // .env.development 203 | VITE_BASE_URL=http://127.0.0.1/api/dev/base/url/ 204 | ``` 205 | 206 | ## 3.4 快速创建Electron窗口 207 | 虽然直接创建 Electron 窗口并非难事,直接创建一个 BrowerWindow 对象就可以创建一个新的 Electron 窗口,但为了方便代码管理和 ipcMain 消息的隔离,本模板中的每个窗口都继承自`WindowBase`对象,每个窗口的相关代码都位于 `src\main\windows\` 的不同子目录中,目录名为我们指定的窗口名称(小写)。 208 | 209 | ```bash 210 | yarn run new:window 211 | ``` 212 | 213 | 需要手动修改窗口对应的路由路径: 214 | 215 | ```javascript 216 | this.openRouter("/ROUTER-PATH"); 217 | ~~~~~~~~~~~ 218 | ``` 219 | 220 | 创建窗口后,需要在`registerIpcMainHandler`方法中注册该窗口的ipcMain事件及处理函数。 221 | 222 | 每个窗口暴露到渲染进程的 apiKey 都不一样,如 primaryWindow: 223 | 224 | ```javascript 225 | contextBridge.exposeInMainWorld("primaryWindowAPI", { 226 | ... 227 | } 228 | ``` 229 | 230 | 这样就不用担心在多个窗口注册了同名的事件时,渲染进程发送该名称的事件到主进程,导致所有窗口对象都收到该事件通知。 231 | 232 | ## 3.5 快速创建IPC函数 233 | 在 `src\renderer\pages\primary\App.vue` 中获取文件 MD5 的示例代码如下: 234 | 235 | ```javascript 236 | async function onGetFileMd5(){ 237 | const result = await utils.showOpenDialog({ 238 | properties: [ "openFile" ], 239 | filters: [ 240 | { name: "All Files", extensions: [ "*" ] } 241 | ] 242 | }); 243 | 244 | if(result.filePaths.length > 0){ 245 | utils.getFileMd5(result.filePaths[0]) 246 | .then((md5) => { 247 | message.success(md5); 248 | }).catch((e) => { 249 | message.error(GetErrorMessage(e)); 250 | }); 251 | } 252 | } 253 | ``` 254 | 255 | 上述代码通过调用 Utils 库的`showOpenDialog`、`getFileMd5`函数轻松实现了通知主进程选择文件、计算文件 MD5 并获取相应结果的操作,代码非常简洁。 256 | 257 | 但是 Utils 只预置了部分常用的功能,预置功能肯定无法满足我们产品开发的所有需求。在此情况下,我们可以向 Utils 库中添加自定义的功能函数,该如何添加了? 258 | 259 | 不用担心,本模板已经提供了 IPC 函数快速创建指令: 260 | 261 | ```bash 262 | yarn run new:ipc 263 | ``` 264 | 265 | 执行上面指令后,会出现如下提示: 266 | ```txt 267 | 创建语法: 调用方向,函数名称,函数类型 268 | 调用方向: 269 | rm = 渲染进程调用主进程的函数 270 | mr = 主进程调用渲染进程的函数(忽略函数类型) 271 | 函数名称: 272 | xxx-xxx-xxx 273 | 函数类型: 274 | a = 异步调用, 不带返回值 275 | ap = 异步调用, 带 Promise 类型的返回值 276 | s = 同步调用, 带返回值 277 | 输入指令: 278 | ``` 279 | 280 | 参数1(调用方向)表示函数调用方向: 281 | - rm 表示渲染进程调用主进程的函数,可以支持同步调用、异步调用,并且可以返回 Promise 结果。 282 | - mr 表示主进程调用渲染进程的函数,该方向只能是异步调用,而且不支持返回结果,会忽略第三个参数(函数类型)。 283 | 284 | 参数2(函数名称),函数名称的单词间使用`-`分隔,如`GetFileSha256`需要指定为`get-file-sha256`。 285 | 286 | 参数3(函数类型): 287 | - a 表示不返回结果的异步函数 288 | - ap 表示返回 Promise 结果的异步函数 289 | - s 表示同步函数 290 | 291 | 292 | ### 示例 293 | 依次输入如下命令: 294 | ```bash 295 | yarn run new:ipc 296 | 297 | 输入指令: 298 | rm,get-file-sha256,ap 299 | ``` 300 | 301 | 命令执行成功后,会自动在`src\lib\utils\renderer\index.ts`生成`Utils.getFileSha256`函数: 302 | 303 | ```javascript 304 | public async getFileSha256(){ 305 | return await (window as any).__ElectronUtils__.getFileSha256(); 306 | } 307 | ``` 308 | 309 | 自动生成的函数都没有指定参数和返回值,需要我们手动添加,如修改后的函数如下: 310 | 311 | ```javascript 312 | public async getFileSha256(filePath: string) : string { 313 | return await (window as any).__ElectronUtils__.getFileSha256(filePath) as string; 314 | } 315 | ``` 316 | 317 | 在渲染进程中(如App.vue)中可以直接调用该函数: 318 | 319 | ```javascript 320 | import utils from "@utils/renderer"; 321 | 322 | const sha256 = await utils.getFileSha256("file-path.txt"); 323 | ``` 324 | 325 | IPC函数创建指令只会创建函数骨架,不会为我们实现具体的功能,我们还需要在主进程ipcMain处理函数中实现计算文件 SHA256 的具体功能。 326 | 327 | 自动生成的主进程 ipcMain 处理函数如下: 328 | 329 | ```javascript 330 | ipcMain.handle("electron-utils-get-file-sha256", async(event) => { 331 | }); 332 | ``` 333 | 334 | 手动添加参数、返回值,及具体的功能代码(此处省略): 335 | 336 | ```javascript 337 | ipcMain.handle("electron-utils-get-file-sha256", async(event, filePath: string) : Promise => { 338 | // ..... 339 | }); 340 | ``` 341 | 342 | # 4. 代码规范 343 | 344 | 本项目使用 ESLint 进行代码检查和格式化,没有使用 Prettier 进行代码格式化。 345 | 346 | 原因大体如下: 347 | 1. 需要额外的插件和配置来避免 ESLint 和 Prettier 的规则冲突。 348 | 349 | 2. Prettier的`printWidth`配置项会损害代码和 Git Diff 的可读性。 350 | ![Why not use prettier](./screenshot/why-not-prettier-1.jpg) 351 | 352 | ![Why not use prettier](./screenshot/why-not-prettier-2.jpg) 353 | 354 | [在线演示](https://prettier.io/playground/#N4Igxg9gdgLgprEAuc0DOMAEBXNcBOamAvJgNoA6UmmwOe+AkgCZKYCMANPQVAIYBbOGwogAggBsAZgEs4mAMJ98QiTJh9RmAL6cqNOrgIs2AJm5H8-ISJABxGf0wAlCGgAWfKFt37aPJlZMAGYLBmthTFEAZXdsAHNMADk+ACNsHz1qf0sTTAAWMN5BSNFnPncBL0wAMXw+Bky-QwY8gFYiqxLbABU3d3kAGQBPbFSEJuyW4yCANk6I22iCeJkIZJkJCCllSYBdAG4qEE4QCAAHGDWoNGRQZXwIAHcABWUEW5Q+CSe+YdvTql6mAANZwGDREqDRxwZA7CR4QHAsEQ858MCOeLIGD4bBwU5wATjZjMODMQZeeLYPjxOA1CAqPgwK5QLFfbAwCAnEDuGACCQAdXc6jgaDRYDgyxu6hkADd1MNkOA0ACQI4GDAXvV4lU4d9ESAAFZoAAe0UxEjgAEVsBB4HqEfiQGjCAQlak0nAJNzzvhHDABTJmDB3Mh8uZnY88AL6uclb7RQRZbDTgBHW3wLUXT4gBoAWigcDJZO5+Dg6ZkZa1NN1SHhBrwAhk2NxTrQFutGdhdf1To0qUDwdDSAjOL4m0xCggAlrIFFbW5Rh6aU+9adsrxjCgpNg0TAfsuYm30Rgw0tDrw2m0QA) 355 | 356 | # 5. 依赖包 🎈 357 | 358 | ## 5.1 基本原则 359 | 360 | > 一个构建在众多不稳定性因素下的项目,是没有稳定性可言的。 361 | 362 | 为了保证项目的稳定性,本模板项目只使用具有知名度、稳定性强的依赖包(库),如`electron-log`等。 363 | 364 | 对于作者自己写的库(如`file-download`等),统一以源码形式提供在`src\lib\`目录,方便模板使用者进行bug修复和功能扩充,在使用时直接采用相对路径进行导入即可。 365 | 366 | ## 5.2 dependencies和devDependencies的区别 367 | 368 | 由于 Electron Forge 会将 `dependencies` 中的所有依赖项都进行打包,因此为了减少安装包的体积,我们只将主进程需要使用的依赖安装到 `dependencies` 项下,而其他的依赖均安装到`devDependencies`。 369 | 370 | 如将vue作为开发依赖进行安装: 371 | 372 | ```bash 373 | yarn add -D vue 374 | ``` 375 | 376 | ## 5.3 依赖包说明 377 | 378 | > 作为开发者,应知晓每个依赖包的用途,避免 node_modules 黑洞的产生。 379 | 380 | - unplugin-vue-components 381 | 实现自动按需引入 AntDesign-Vue 组件。 382 | 383 | - electron-log 384 | 提供本地日志文件的打印和输出。 385 | 386 | - @fortawesome-* 387 | 提供对 FontAwesome 图标字体的支持。 388 | 389 | - uuid 390 | 生成 uuid 字符串,在 file-download 库中使用。 391 | 392 | - chalk 393 | 用于在命令行终端输出带颜色样式的字符串,仅在`scripts\*.js`中使用。 394 | 395 | - chokidar 396 | 轻量级的文件监控组件,用于实现热加载,仅在`scripts\*.js`中使用。 397 | 398 | - @electron-forge/* 399 | 与 Electron Forge 构建和打包相关的依赖包,除了`@electron-forge/cli`是必须的,其他的可以根据`forge.config.js -> makers`的配置按需引用。 400 | 401 | - axios 402 | 异步 HTTP 网络请求组件,在主进程和渲染进程中使用。 403 | 404 | # 6. 客户端版本号 405 | 406 | 使用`package.json`文件的 `version` 字段标识客户端的版本号,在主进程内可以通过 `appState.appVersion` 属性获取。 407 | 408 | 💡 不需要设置`forge.config.js`文件的`appVersion`字段。 409 | 410 | 在渲染进程可以直接使用`utils.getAppVersion()`获取版本号。 411 | 412 | ```javascript 413 | import utils from "@utils/renderer"; 414 | 415 | console.log(utils.getAppVersion()); 416 | ``` 417 | 418 | # 7. 期待你的反馈 🥳 419 | 420 | 个人能力有限,代码不免有错误和不足之处,欢迎提交 issue 和 PR 。 421 | 422 | 如果这个项目对你有帮助,请点击右上角 Star ⭐或 Fork 该项目,为项目增加一丝热度,让更多的人发现该项目。 423 | 424 | # 8. 赞助 425 | 426 | 感谢您能使用本项目,如果这个项目能对您产生帮助,对我而言也是一件非常开心的事情。 427 | 428 | **可以前往我的 Github [主页](https://github.com/winsoft666) 进行赞助。** -------------------------------------------------------------------------------- /forge.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | const fsPromises = require("fs/promises"); 3 | const fs = require("fs"); 4 | const path = require("path"); 5 | // +++ 新增依赖 +++ 6 | const JavaScriptObfuscator = require("javascript-obfuscator"); 7 | 8 | async function prunePackageJson(buildPath) { 9 | const packageDotJsonPath = path.join(buildPath, "package.json"); 10 | const content = await fsPromises.readFile(packageDotJsonPath); 11 | const json = JSON.parse(content.toString()); 12 | Object.keys(json).forEach((key) => { 13 | switch (key) { 14 | case 'name': { 15 | break; 16 | } 17 | case 'version': { 18 | break; 19 | } 20 | case 'main': { 21 | break; 22 | } 23 | case 'author': { 24 | break; 25 | } 26 | case 'description': { 27 | break; 28 | } 29 | default: { 30 | delete json[key]; 31 | break; 32 | } 33 | } 34 | }); 35 | await fsPromises.writeFile(packageDotJsonPath, JSON.stringify(json, null, "\t")); 36 | } 37 | 38 | // +++ 新增混淆函数 +++ 39 | async function obfuscateMainProcess(buildPath) { 40 | console.log('[混淆调试] 开始处理目录:', buildPath); // +++ 新增日志 +++ 41 | try { 42 | // 匹配主进程 JS 文件(根据你的入口文件调整模式) 43 | const dirs = await fsPromises.readdir(path.join(buildPath, 'build'), { recursive: true }) 44 | const files = dirs.filter(item => item.endsWith('.js')) 45 | console.log(files, 'filesfilesfiles'); 46 | // 混淆配置(根据需求调整) 47 | const obfuscationOptions = { 48 | compact: true, 49 | controlFlowFlattening: true, 50 | controlFlowFlatteningThreshold: 0.75, 51 | numbersToExpressions: true, 52 | simplify: true, 53 | stringArrayShuffle: true, 54 | splitStrings: true, 55 | stringArrayThreshold: 0.75, 56 | reservedNames: [ 57 | 'electron', 'require', 'module', 'exports', 58 | 'BrowserWindow', 'app' // 保留 Electron 关键 API 59 | ], 60 | renameGlobals: false 61 | }; 62 | 63 | // 批量混淆文件 64 | for (const file of files) { 65 | const filePath = path.join(buildPath, 'build', file); 66 | 67 | const code = await fsPromises.readFile(filePath, "utf8"); 68 | const obfuscatedCode = JavaScriptObfuscator.obfuscate(code, obfuscationOptions).getObfuscatedCode(); 69 | await fsPromises.writeFile(filePath, obfuscatedCode); 70 | } 71 | } catch (error) { 72 | throw new Error(`混淆失败: ${error.message}`); 73 | } 74 | } 75 | 76 | module.exports = { 77 | packagerConfig: { 78 | name: "Electron-Vue3-Boilerplate", 79 | appCopyright: "Copyright (C) 2024", 80 | icon: "./setup/exe.ico", 81 | // ElectronForge默认会将项目根目录下的所有文件及目录打包到resources 82 | // 因此需要在这里忽略不需要打入到安装包的文件和目录 83 | // 对于node_modules目录,只会打包dependencies依赖项 84 | // 不支持正则表达式 85 | ignore: [ 86 | ".vscode", 87 | "node_modules/.vite", 88 | "node_modules/.ignored", 89 | "node_modules/.cache", 90 | "README.md", 91 | "yarn.lock", 92 | "vite.config.js", 93 | "forge.config.js", 94 | ".gitignore", 95 | ".eslintrc.js", 96 | ".eslintignore", 97 | ".prettierignore", 98 | ".prettierrc.js", 99 | "LICENSE", 100 | "^(\/src$)", 101 | "^(\/scripts$)", 102 | "^(\/out$)", 103 | "^(\/setup$)", 104 | "^(\/screenshot$)", 105 | ], 106 | win32metadata: { 107 | ProductName: "electron-vue-boilerplate", 108 | CompanyName: "", 109 | FileDescription: "A Electron + Vue3 + Vite Boilerplate", 110 | // 如果安装包需要以管理员权限运行,请打开下面的注释 111 | // "requested-execution-level": "requireAdministrator", 112 | }, 113 | // 是否使用asar压缩资源 114 | asar: true, 115 | }, 116 | // 定义钩子,更多钩子请参考:https://www.electronforge.io/config/hooks 117 | hooks: { 118 | // 在文件拷贝完成后触发 119 | packageAfterCopy: async(config, buildPath, electronVersion, platform, arch) => { 120 | // 比如在拷贝完成后需要删除src目录 121 | //await fsPromises.rmdir(path.join(buildPath, "src"), { recursive: true }); 122 | 123 | // 加密生产代码,不影响 build 目录下代码 124 | await obfuscateMainProcess(buildPath) 125 | // 精简package.json,删除无需暴露的属性 126 | await prunePackageJson(buildPath); 127 | }, 128 | }, 129 | rebuildConfig: {}, 130 | // Maker用于最终生成对应平台的安装包 131 | // 更多Maker请参考:https://www.electronforge.io/config/makers 132 | makers: [ 133 | { 134 | // 仅支持Windows平台 135 | name: "@electron-forge/maker-squirrel", 136 | config: { 137 | // 用于数字签名的证书路径和密码 138 | // certificateFile: './cert.pfx', 139 | // certificatePassword: process.env.CERTIFICATE_PASSWORD 140 | }, 141 | }, 142 | { 143 | // 仅支持macOS平台 144 | name: "@electron-forge/maker-dmg", 145 | config: { 146 | // background: './assets/dmg-background.png', 147 | format: "ULFO", 148 | }, 149 | }, 150 | { 151 | // 创建一个zip压缩包,支持所有平台 152 | name: "@electron-forge/maker-zip", 153 | }, 154 | ], 155 | } 156 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vue3-boilerplate", 3 | "version": "1.0.1", 4 | "description": "A Electron + Vue3 boilerplate.", 5 | "author": { 6 | "name": "winsoft666", 7 | "url": "https://github.com/winsoft666" 8 | }, 9 | "repository": "https://github.com/winsoft666/electron-vue3-boilerplate", 10 | "main": "build/main/main.js", 11 | "scripts": { 12 | "dev": "node scripts/dev-server.mjs development", 13 | "test": "node scripts/dev-server.mjs test", 14 | "production": "node scripts/dev-server.mjs production", 15 | "lint": "eslint .", 16 | "lint:fix": "eslint . --fix", 17 | "new:window": "node scripts/create-window.mjs", 18 | "new:ipc": "node scripts/create-ipc.mjs", 19 | "build": "node scripts/build.mjs && electron-forge make", 20 | "build:win32": "node scripts/build.mjs && electron-forge make --platform=win32 --arch=ia32", 21 | "build:win64": "node scripts/build.mjs && electron-forge make --platform=win32 --arch=x64", 22 | "build:mac": "node scripts/build.mjs && electron-forge make --platform=darwin", 23 | "build:linux": "node scripts/build.mjs && electron-forge make --platform=linux", 24 | "build:nsis-win32": "node scripts/build.mjs && electron-forge package --platform=win32 --arch=ia32 && ./setup/NSIS/nsis-3.08/makensis.exe ./setup/NSIS/win-setup-x86.nsi", 25 | "build:nsis-win64": "node scripts/build.mjs && electron-forge package --platform=win32 --arch=x64 && ./setup/NSIS/nsis-3.08/makensis.exe ./setup/NSIS/win-setup-x64.nsi" 26 | }, 27 | "dependencies": { 28 | "axios": "^1.6.7", 29 | "electron-log": "^5.1.0", 30 | "uuid": "^9.0.1" 31 | }, 32 | "devDependencies": { 33 | "@electron-forge/cli": "^7.4.0", 34 | "@electron-forge/maker-dmg": "^7.4.0", 35 | "@electron-forge/maker-squirrel": "^7.4.0", 36 | "@electron-forge/maker-zip": "^7.4.0", 37 | "@fortawesome/fontawesome-svg-core": "^6.5.1", 38 | "@fortawesome/free-brands-svg-icons": "^6.5.1", 39 | "@fortawesome/free-regular-svg-icons": "^6.5.1", 40 | "@fortawesome/free-solid-svg-icons": "^6.5.1", 41 | "@fortawesome/vue-fontawesome": "^3.0.5", 42 | "@typescript-eslint/eslint-plugin": "^7.11.0", 43 | "@typescript-eslint/parser": "^7.11.0", 44 | "@vitejs/plugin-vue": "^5.0.5", 45 | "@vue/eslint-config-typescript": "^13.0.0", 46 | "javascript-obfuscator": "^4.1.1", 47 | "ant-design-vue": "^4.1.1", 48 | "chalk": "^5.3.0", 49 | "chokidar": "^3.5.3", 50 | "electron": "^30.0.9", 51 | "eslint": "^9.4.0", 52 | "eslint-plugin-vue": "^9.20.1", 53 | "typescript": "^5.3.3", 54 | "unplugin-vue-components": "^0.27.0", 55 | "vite": "^5.2.12", 56 | "vue": "^3.4.15", 57 | "vue-eslint-parser": "^9.4.2", 58 | "vue-router": "^4.3.0" 59 | } 60 | } -------------------------------------------------------------------------------- /screenshot/after-nsis-zip-uncompressed.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/screenshot/after-nsis-zip-uncompressed.png -------------------------------------------------------------------------------- /screenshot/main.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/screenshot/main.png -------------------------------------------------------------------------------- /screenshot/nsis-setup-1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/screenshot/nsis-setup-1.png -------------------------------------------------------------------------------- /screenshot/why-not-prettier-1.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/screenshot/why-not-prettier-1.jpg -------------------------------------------------------------------------------- /screenshot/why-not-prettier-2.jpg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/screenshot/why-not-prettier-2.jpg -------------------------------------------------------------------------------- /scripts/build.mjs: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import fs from "fs"; 3 | import { fileURLToPath } from "url"; 4 | import fsPromises from "fs/promises"; 5 | import chalk from "chalk"; 6 | import * as vite from "vite"; 7 | import { CompileTS } from "./private/tsc.mjs"; 8 | 9 | const __filename = fileURLToPath(import.meta.url); 10 | const __dirname = path.dirname(__filename); 11 | 12 | function buildRenderer(){ 13 | return vite.build({ 14 | configFile: path.join(__dirname, "../src/renderer/vite.config.mjs"), 15 | base: "./", 16 | mode: "production", 17 | }); 18 | } 19 | 20 | function buildMain(){ 21 | const mainPath = path.join(__dirname, "../src/main"); 22 | return CompileTS(mainPath); 23 | } 24 | 25 | function copyStaticFiles(){ 26 | return copyMainSubFiles("static"); 27 | } 28 | 29 | /* 30 | 注意:Electron的工作目录是 build/main 而不是 src/main 31 | tsc不能复制编译后的JS静态文件,所以需要手动复制编译后的文件到build/main 32 | */ 33 | function copyMainSubFiles(subPath){ 34 | return fsPromises.cp(path.join(__dirname, "../src/main", subPath), path.join(__dirname, "../build/main", subPath), { recursive: true }); 35 | } 36 | 37 | fs.rmSync(path.join(__dirname, "../build"), { 38 | recursive: true, 39 | force: true, 40 | }); 41 | 42 | console.log(chalk.blueBright("Transpiling renderer & main...")); 43 | 44 | Promise.allSettled([ buildRenderer(), buildMain(), copyStaticFiles() ]) 45 | .then(() => { 46 | console.log(chalk.greenBright("Renderer & main successfully transpiled! (ready to be built with electron-forge)")); 47 | }); 48 | -------------------------------------------------------------------------------- /scripts/create-ipc.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 实现创建IPC函数的快捷指令 3 | */ 4 | 5 | /* eslint-disable */ 6 | import chalk from "chalk"; 7 | import path from "path"; 8 | import { fileURLToPath } from "url"; 9 | import fs from "fs"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | const outputTips = (message) => console.log(chalk.blue(`${message}`)); 15 | const outputSuccess = (message) => console.log(chalk.green(`${message}`)); 16 | const outputError = (error) => console.log(chalk.red(`${error}`)); 17 | 18 | let callWay = ""; 19 | let funcName = ""; 20 | let funcType = ""; 21 | let humpFuncName = ""; 22 | 23 | function outputUsage(){ 24 | outputTips("创建语法: 调用方向,函数名称,函数类型"); 25 | outputTips("调用方向:\n\trm = 渲染进程调用主进程的函数\n\tmr = 主进程调用渲染进程的函数(忽略函数类型)"); 26 | outputTips("函数名称:\n\txxx-xxx-xxx"); 27 | outputTips("函数类型:\n\ta = 异步调用, 不带返回值\n\tap = 异步调用, 带Promise类型的返回值\n\ts = 同步调用, 带返回值"); 28 | } 29 | 30 | outputUsage(); 31 | outputTips("输入指令:"); 32 | 33 | process.stdin.on("data", async(chunk) => { 34 | const inputStr = String(chunk).trim().toString().toLowerCase(); 35 | const values = inputStr.split(","); 36 | if(values.length < 2){ 37 | outputError("语法错误!"); 38 | outputTips("\n输入指令:"); 39 | return; 40 | } 41 | 42 | callWay = values.at(0); 43 | funcName = values.at(1); 44 | if(values.length > 2) 45 | funcType = values.at(2); 46 | 47 | if(callWay != "rm" && callWay != "mr"){ 48 | outputError("调用方向类型错误!"); 49 | outputTips("\n输入指令:"); 50 | return; 51 | } 52 | 53 | if(!funcName){ 54 | outputError("函数名称不能为空!"); 55 | outputTips("\n输入指令:"); 56 | return; 57 | } 58 | 59 | if(callWay == "rm"){ 60 | if(funcType != "a" && funcType != "ap" && funcType != "s"){ 61 | outputError("函数类型不能为空!"); 62 | outputTips("\n输入指令:"); 63 | return; 64 | } 65 | } 66 | 67 | humpFuncName = toHumpFunctionName(funcName); 68 | if(callWay == "rm"){ 69 | handleMainIndex_rm(); 70 | handleProload_rm(); 71 | handleRendererIndex_rm(); 72 | }else if(callWay == "mr"){ 73 | handleMainIndex_mr(); 74 | handleProload_mr(); 75 | handleRendererIndex_mr(); 76 | } 77 | 78 | process.stdin.emit("end"); 79 | }); 80 | 81 | function toHumpFunctionName(funcName){ 82 | let funcNameCopy = funcName; 83 | let offset = 0; 84 | for (;;){ 85 | const pos = funcNameCopy.indexOf("-", offset); 86 | if(pos == -1){ 87 | humpFuncName += funcNameCopy.charAt(0).toUpperCase(); 88 | humpFuncName += funcNameCopy.substring(1); 89 | break; 90 | } 91 | 92 | const slice = funcNameCopy.substring(0, pos); 93 | humpFuncName += slice.charAt(0).toUpperCase(); 94 | humpFuncName += slice.substring(1); 95 | 96 | funcNameCopy = funcNameCopy.substring(pos + 1); 97 | } 98 | humpFuncName = humpFuncName.charAt(0).toLowerCase() + humpFuncName.substring(1); 99 | return humpFuncName; 100 | } 101 | 102 | function handleMainIndex_rm(){ 103 | const filePath = path.join(__dirname, "../src/lib/utils/main/index.ts"); 104 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 105 | 106 | let funcFlag = ""; 107 | let ipcMainFunctionName = ""; 108 | if(funcType == "ap"){ 109 | funcFlag = "async"; 110 | ipcMainFunctionName = "handle"; 111 | }else{ 112 | ipcMainFunctionName = "on"; 113 | } 114 | 115 | const generateCode = `ipcMain.${ipcMainFunctionName}("electron-utils-${funcName}", ${funcFlag}(event) => { 116 | }); 117 | // === FALG LINE (DO NOT MODIFY/REMOVE) ===`; 118 | 119 | code = code.replace("// === FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 120 | 121 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 122 | } 123 | 124 | function handleProload_rm(){ 125 | const filePath = path.join(__dirname, "../src/lib/utils/main/utils-preload.ts"); 126 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 127 | 128 | let ipcRendererFuncName = ""; 129 | if(funcType == "ap"){ 130 | ipcRendererFuncName = "invoke"; 131 | }else if(funcType == "a"){ 132 | ipcRendererFuncName = "send"; 133 | }else if(funcType == "s"){ 134 | ipcRendererFuncName = "sendSync"; 135 | } 136 | 137 | const generateCode = ` ${humpFuncName}: () => ipcRenderer.${ipcRendererFuncName}("electron-utils-${funcName}"), 138 | // === FALG LINE (DO NOT MODIFY/REMOVE) ===`; 139 | 140 | code = code.replace(" // === FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 141 | 142 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 143 | } 144 | 145 | function handleRendererIndex_rm(){ 146 | const filePath = path.join(__dirname, "../src/lib/utils/renderer/index.ts"); 147 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 148 | 149 | let funcFlag = ""; 150 | if(funcType == "ap"){ 151 | funcFlag = " async"; 152 | } 153 | 154 | let callFlag = ""; 155 | if(funcType == "ap"){ 156 | callFlag = " await"; 157 | } 158 | 159 | const generateCode = ` public${funcFlag} ${humpFuncName}(){ 160 | return${callFlag} (window as any).__ElectronUtils__.${humpFuncName}(); 161 | } 162 | // === FALG LINE (DO NOT MODIFY/REMOVE) ===`; 163 | 164 | code = code.replace(" // === FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 165 | 166 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 167 | } 168 | 169 | function handleMainIndex_mr(){ 170 | const filePath = path.join(__dirname, "../src/lib/utils/main/index.ts"); 171 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 172 | 173 | const generateCode = ` public ${humpFuncName}(browserWindow: BrowserWindow | null){ 174 | if(browserWindow){ 175 | browserWindow.webContents.send("electron-utils-${funcName}"); 176 | } 177 | } 178 | // === PUBLIC METHOD FALG LINE (DO NOT MODIFY/REMOVE) ===`; 179 | 180 | code = code.replace(" // === PUBLIC METHOD FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 181 | 182 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 183 | } 184 | 185 | function handleProload_mr(){ 186 | const filePath = path.join(__dirname, "../src/lib/utils/main/utils-preload.ts"); 187 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 188 | 189 | const humpFuncNameUpper = humpFuncName.charAt(0).toUpperCase() + humpFuncName.substring(1); 190 | 191 | const generateCode = ` on${humpFuncNameUpper}: (callback) => ipcRenderer.on("electron-utils-${funcName}", (event) => { 192 | callback(); 193 | }), 194 | // === FALG LINE (DO NOT MODIFY/REMOVE) ===`; 195 | 196 | code = code.replace(" // === FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 197 | 198 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 199 | } 200 | 201 | function handleRendererIndex_mr(){ 202 | const filePath = path.join(__dirname, "../src/lib/utils/renderer/index.ts"); 203 | let code = fs.readFileSync(filePath, { encoding: "utf-8" }); 204 | 205 | const humpFuncNameUpper = humpFuncName.charAt(0).toUpperCase() + humpFuncName.substring(1); 206 | 207 | const generateCode = ` public on${humpFuncNameUpper}(callback){ 208 | (window as any).__ElectronUtils__.on${humpFuncNameUpper}(callback); 209 | } 210 | // === FALG LINE (DO NOT MODIFY/REMOVE) ===`; 211 | 212 | code = code.replace(" // === FALG LINE (DO NOT MODIFY/REMOVE) ===", generateCode); 213 | 214 | fs.writeFileSync(filePath, code, { encoding: "utf-8" }); 215 | } 216 | 217 | process.stdin.on("end", () => { 218 | if(callWay == "mr"){ 219 | const humpFuncNameUpper = humpFuncName.charAt(0).toUpperCase() + humpFuncName.substring(1); 220 | outputSuccess(`Create IPC (Main -> Renderer) function successful!\n\n[Usage]\n Main process:\n\tutils.${humpFuncName}(...)\n Renderer process:\n\tutils.on${humpFuncNameUpper}(...)`); 221 | }else if(callWay == "rm"){ 222 | outputSuccess(`Create IPC (Renderer -> Main) function successful!\n\n[Usage]\n Renderer process:\n\tutils.${humpFuncName}(...)`); 223 | } 224 | process.exit(); 225 | }); 226 | -------------------------------------------------------------------------------- /scripts/create-window.mjs: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 实现创建Electron窗口的快捷指令 3 | */ 4 | 5 | import chalk from "chalk"; 6 | import path from "path"; 7 | import { fileURLToPath } from "url"; 8 | import fs from "fs"; 9 | import { ToCamelName } from "./private/utils.mjs"; 10 | 11 | const __filename = fileURLToPath(import.meta.url); 12 | const __dirname = path.dirname(__filename); 13 | 14 | const outputTips = (message) => console.log(chalk.blue(`${message}`)); 15 | const outputSuccess = (message) => console.log(chalk.green(`${message}`)); 16 | const outputError = (error) => console.log(chalk.red(`${error}`)); 17 | 18 | let windowName = ""; 19 | let className = ""; 20 | 21 | outputTips("输入窗口名称:"); 22 | 23 | process.stdin.on("data", async(chunk) => { 24 | // Input page name 25 | windowName = String(chunk).trim().toString().toLowerCase(); 26 | if(!windowName){ 27 | outputError("窗口名称不能为空!"); 28 | outputTips("\n输入窗口名称:"); 29 | return; 30 | } 31 | 32 | className = ToCamelName(windowName); 33 | 34 | const targetPath = path.join(__dirname, "../src/main/windows", windowName); 35 | // Check whether page is exist or not 36 | const pageExists = fs.existsSync(targetPath); 37 | if(pageExists){ 38 | outputError(`窗口 ${windowName} 已经存在!`); 39 | outputTips("\n输入窗口名称:"); 40 | return; 41 | } 42 | 43 | fs.mkdirSync(targetPath); 44 | const sourcePath = path.join(__dirname, "template-ts/main-window"); 45 | copyFile(sourcePath, targetPath); 46 | 47 | handleIndexTsFile(targetPath); 48 | handlePreloadTsFile(targetPath); 49 | 50 | process.stdin.emit("end"); 51 | }); 52 | 53 | function handleIndexTsFile(targetPath){ 54 | const indexTsPath = path.join(targetPath, "index.ts"); 55 | let code = fs.readFileSync(indexTsPath, { encoding: "utf-8" }); 56 | 57 | code = code.replaceAll("XXXWindow", className + "Window"); 58 | code = code.replaceAll("%renderer_page_name%", windowName); 59 | 60 | fs.writeFileSync(indexTsPath, code, { encoding: "utf-8" }); 61 | } 62 | 63 | function handlePreloadTsFile(targetPath){ 64 | const preloadTsPath = path.join(targetPath, "preload.ts"); 65 | let code = fs.readFileSync(preloadTsPath, { encoding: "utf-8" }); 66 | 67 | code = code.replaceAll("XXXWindow", className + "Window"); 68 | 69 | fs.writeFileSync(preloadTsPath, code, { encoding: "utf-8" }); 70 | } 71 | 72 | process.stdin.on("end", () => { 73 | outputSuccess(`Create ${windowName} window successful!`); 74 | process.exit(); 75 | }); 76 | 77 | const createDirectories = (path) => { 78 | if(!fs.existsSync(path)){ 79 | fs.mkdirSync(path); 80 | } 81 | }; 82 | 83 | const copyFile = (sourcePath, targetPath) => { 84 | const sourceFile = fs.readdirSync(sourcePath, { withFileTypes: true }); 85 | 86 | sourceFile.forEach((file) => { 87 | const newSourcePath = path.resolve(sourcePath, file.name); 88 | const newTargetPath = path.resolve(targetPath, file.name); 89 | 90 | if(file.isDirectory()){ 91 | createDirectories(newTargetPath); 92 | copyFile(newSourcePath, newTargetPath); 93 | }else{ 94 | fs.copyFileSync(newSourcePath, newTargetPath); 95 | } 96 | }); 97 | }; 98 | -------------------------------------------------------------------------------- /scripts/dev-server.mjs: -------------------------------------------------------------------------------- 1 | import childProcess from "child_process"; 2 | import path from "path"; 3 | import { fileURLToPath } from "url"; 4 | import fs from "fs"; 5 | import { EOL } from "os"; 6 | import * as vite from "vite"; 7 | import chalk from "chalk"; 8 | import chokidar from "chokidar"; 9 | import electron from "electron"; 10 | import { CompileTS } from "./private/tsc.mjs"; 11 | 12 | let viteServer = null; 13 | let electronProcess = null; 14 | let electronProcessLocker = false; 15 | let rendererPort = 0; 16 | let envStr = process.argv[2]; 17 | 18 | const __filename = fileURLToPath(import.meta.url); 19 | const __dirname = path.dirname(__filename); 20 | 21 | async function startRenderer(){ 22 | viteServer = await vite.createServer({ 23 | configFile: path.join(__dirname, "../src/renderer/vite.config.mjs"), 24 | mode: envStr, 25 | }); 26 | 27 | return viteServer.listen(); 28 | } 29 | 30 | async function startElectron(){ 31 | if(electronProcess){ 32 | // single instance lock 33 | return; 34 | } 35 | 36 | try { 37 | const tsDir = path.join(__dirname, "..", "src"); 38 | await CompileTS(tsDir); 39 | } catch { 40 | console.log(chalk.redBright("Could not start Electron because of the above typescript error(s).")); 41 | electronProcessLocker = false; 42 | return; 43 | } 44 | 45 | const args = [ path.join(__dirname, "..", "build", "main", "main.js"), rendererPort, envStr ]; 46 | const electronPath = electron; 47 | electronProcess = childProcess.spawn(electronPath, args); 48 | electronProcessLocker = false; 49 | 50 | electronProcess.stdout.on("data", (data) => { 51 | if(data == EOL) 52 | return; 53 | 54 | process.stdout.write(chalk.blueBright("[Electron] ") + chalk.white(data.toString())); 55 | }); 56 | 57 | electronProcess.stderr.on("data", (data) => { 58 | process.stderr.write(chalk.blueBright("[Electron] ") + chalk.white(data.toString())); 59 | }); 60 | 61 | electronProcess.on("exit", () => stop()); 62 | } 63 | 64 | function restartElectron(){ 65 | if(electronProcess){ 66 | electronProcess.removeAllListeners("exit"); 67 | electronProcess.kill(); 68 | electronProcess = null; 69 | } 70 | 71 | if(!electronProcessLocker){ 72 | electronProcessLocker = true; 73 | startElectron(); 74 | } 75 | } 76 | 77 | function copyStaticFiles(){ 78 | copyMainSubFiles("static"); 79 | } 80 | 81 | /* 82 | 注意:Electron的工作目录是 build/main 而不是 src/main 83 | tsc不能复制编译后的JS静态文件,所以需要手动复制编译后的文件到build/main 84 | */ 85 | function copyMainSubFiles(subPath){ 86 | fs.cpSync(path.join(__dirname, "../src/main", subPath), path.join(__dirname, "../build/main", subPath), { recursive: true }); 87 | } 88 | 89 | function stop(){ 90 | viteServer.close(); 91 | process.exit(); 92 | } 93 | 94 | async function start(){ 95 | console.log(`${chalk.greenBright("=========================================")}`); 96 | console.log(`${chalk.greenBright("Starting Electron + Vue3 Dev Server...")}`); 97 | console.log(`${chalk.greenBright("=========================================")}`); 98 | 99 | const devServer = await startRenderer(); 100 | rendererPort = devServer.config.server.port; 101 | 102 | copyStaticFiles(); 103 | await startElectron(); 104 | 105 | const mainFolder = path.join(__dirname, "../src/main"); 106 | console.log(mainFolder); 107 | chokidar.watch(mainFolder, { 108 | cwd: mainFolder, 109 | }) 110 | .on("change", (mainFolder) => { 111 | console.log(`${chalk.blueBright("[electron] ")}Change in ${mainFolder}. reloading... 🚀`); 112 | 113 | if(mainFolder.startsWith(path.join("static", "/"))) 114 | copyMainSubFiles(mainFolder); 115 | 116 | restartElectron(); 117 | }); 118 | } 119 | 120 | start(); 121 | -------------------------------------------------------------------------------- /scripts/private/tsc.mjs: -------------------------------------------------------------------------------- 1 | import ChildProcess from "child_process"; 2 | import Chalk from "chalk"; 3 | 4 | function CompileTS(directory){ 5 | console.log("Compiling", directory); 6 | return new Promise((resolve, reject) => { 7 | const tscProcess = ChildProcess.exec("tsc", { 8 | cwd: directory, 9 | }); 10 | 11 | tscProcess.stdout.on("data", (data) => { 12 | process.stdout.write(Chalk.yellowBright("[tsc] ") + Chalk.white(data.toString())); 13 | }); 14 | 15 | tscProcess.on("exit", (exitCode) => { 16 | if(exitCode > 0) 17 | reject(exitCode); 18 | else 19 | resolve(); 20 | }); 21 | }); 22 | } 23 | 24 | export { CompileTS }; 25 | -------------------------------------------------------------------------------- /scripts/private/utils.mjs: -------------------------------------------------------------------------------- 1 | function ToCamelName(name){ 2 | const lowerName = name.toLowerCase(); 3 | const arr = lowerName.split("-"); 4 | let result = arr[0]; 5 | for (let i = 1;i < arr.length;i++){ 6 | const tmp = arr[i]; 7 | result += tmp.charAt(0).toUpperCase() + tmp.substring(1); 8 | } 9 | 10 | return result; 11 | } 12 | 13 | export { 14 | ToCamelName 15 | }; 16 | -------------------------------------------------------------------------------- /scripts/template-ts/main-window/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { ipcMain } from "electron"; 3 | import WindowBase from "../window-base"; 4 | 5 | class XXXWindow extends WindowBase{ 6 | constructor(){ 7 | // 调用WindowBase构造函数创建窗口 8 | super({ 9 | width: 800, 10 | height: 600, 11 | webPreferences: { 12 | preload: path.join(__dirname, "preload.js"), 13 | }, 14 | }); 15 | 16 | this.openRouter("/ROUTER-PATH"); 17 | } 18 | 19 | protected registerIpcMainHandler(): void{ 20 | // 一个简单的 IPC 示例 21 | ipcMain.on("send-message", (event, message) => { 22 | console.log(message); 23 | }); 24 | 25 | // 添加更多的 ipcMain 处理函数 26 | } 27 | } 28 | 29 | export default XXXWindow; -------------------------------------------------------------------------------- /scripts/template-ts/main-window/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | 3 | /* 4 | 暴露XXXWindow窗口主进程的方法到XXXWindow窗口的渲染进程 5 | */ 6 | contextBridge.exposeInMainWorld("XXXWindowAPI", { 7 | // 一个简单的示例 8 | sendMessage: (message) => ipcRenderer.send("send-message", message), 9 | 10 | // 暴露更多的API到渲染进程... 11 | }); 12 | -------------------------------------------------------------------------------- /setup/NSIS/install.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/setup/NSIS/install.ico -------------------------------------------------------------------------------- /setup/NSIS/license.rtf: -------------------------------------------------------------------------------- 1 | {\rtf1\adeflang1025\ansi\ansicpg936\uc2\adeff0\deff0\stshfdbch31505\stshfloch31506\stshfhich31506\stshfbi0\deflang1033\deflangfe2052\themelang1033\themelangfe2052\themelangcs0{\fonttbl{\f0\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\f10\fbidi \fnil\fcharset2\fprq2{\*\panose 05000000000000000000}Wingdings;} 2 | {\f13\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'cb\'ce\'cc\'e5{\*\falt SimSun};}{\f34\fbidi \froman\fcharset0\fprq2{\*\panose 02040503050406030204}Cambria Math;} 3 | {\f36\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'b5\'c8\'cf\'df{\*\falt DengXian};}{\f44\fbidi \fswiss\fcharset0\fprq2{\*\panose 00000000000000000000}Segoe UI;} 4 | {\f46\fbidi \fnil\fcharset134\fprq2{\*\panose 00000000000000000000}@\'cb\'ce\'cc\'e5;}{\f50\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}@\'b5\'c8\'cf\'df;} 5 | {\flomajor\f31500\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbmajor\f31501\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'b5\'c8\'cf\'df Light;} 6 | {\fhimajor\f31502\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'b5\'c8\'cf\'df Light;}{\fbimajor\f31503\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} 7 | {\flominor\f31504\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;}{\fdbminor\f31505\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'b5\'c8\'cf\'df{\*\falt DengXian};} 8 | {\fhiminor\f31506\fbidi \fnil\fcharset134\fprq2{\*\panose 02010600030101010101}\'b5\'c8\'cf\'df{\*\falt DengXian};}{\fbiminor\f31507\fbidi \froman\fcharset0\fprq2{\*\panose 02020603050405020304}Times New Roman;} 9 | {\f52\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\f53\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\f55\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\f56\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} 10 | {\f57\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\f58\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\f59\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} 11 | {\f60\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\f184\fbidi \fnil\fcharset0\fprq2 SimSun Western{\*\falt SimSun};}{\f392\fbidi \froman\fcharset238\fprq2 Cambria Math CE;}{\f393\fbidi \froman\fcharset204\fprq2 Cambria Math Cyr;} 12 | {\f395\fbidi \froman\fcharset161\fprq2 Cambria Math Greek;}{\f396\fbidi \froman\fcharset162\fprq2 Cambria Math Tur;}{\f399\fbidi \froman\fcharset186\fprq2 Cambria Math Baltic;}{\f400\fbidi \froman\fcharset163\fprq2 Cambria Math (Vietnamese);} 13 | {\f414\fbidi \fnil\fcharset0\fprq2 DengXian Western{\*\falt DengXian};}{\f412\fbidi \fnil\fcharset238\fprq2 DengXian CE{\*\falt DengXian};}{\f413\fbidi \fnil\fcharset204\fprq2 DengXian Cyr{\*\falt DengXian};} 14 | {\f415\fbidi \fnil\fcharset161\fprq2 DengXian Greek{\*\falt DengXian};}{\f492\fbidi \fswiss\fcharset238\fprq2 Segoe UI CE;}{\f493\fbidi \fswiss\fcharset204\fprq2 Segoe UI Cyr;}{\f495\fbidi \fswiss\fcharset161\fprq2 Segoe UI Greek;} 15 | {\f496\fbidi \fswiss\fcharset162\fprq2 Segoe UI Tur;}{\f497\fbidi \fswiss\fcharset177\fprq2 Segoe UI (Hebrew);}{\f498\fbidi \fswiss\fcharset178\fprq2 Segoe UI (Arabic);}{\f499\fbidi \fswiss\fcharset186\fprq2 Segoe UI Baltic;} 16 | {\f500\fbidi \fswiss\fcharset163\fprq2 Segoe UI (Vietnamese);}{\f514\fbidi \fnil\fcharset0\fprq2 @SimSun Western;}{\f554\fbidi \fnil\fcharset0\fprq2 @DengXian Western;}{\f552\fbidi \fnil\fcharset238\fprq2 @DengXian CE;} 17 | {\f553\fbidi \fnil\fcharset204\fprq2 @DengXian Cyr;}{\f555\fbidi \fnil\fcharset161\fprq2 @DengXian Greek;}{\flomajor\f31508\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flomajor\f31509\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 18 | {\flomajor\f31511\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flomajor\f31512\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flomajor\f31513\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} 19 | {\flomajor\f31514\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flomajor\f31515\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flomajor\f31516\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} 20 | {\fdbmajor\f31520\fbidi \fnil\fcharset0\fprq2 DengXian Light Western;}{\fdbmajor\f31518\fbidi \fnil\fcharset238\fprq2 DengXian Light CE;}{\fdbmajor\f31519\fbidi \fnil\fcharset204\fprq2 DengXian Light Cyr;} 21 | {\fdbmajor\f31521\fbidi \fnil\fcharset161\fprq2 DengXian Light Greek;}{\fhimajor\f31530\fbidi \fnil\fcharset0\fprq2 DengXian Light Western;}{\fhimajor\f31528\fbidi \fnil\fcharset238\fprq2 DengXian Light CE;} 22 | {\fhimajor\f31529\fbidi \fnil\fcharset204\fprq2 DengXian Light Cyr;}{\fhimajor\f31531\fbidi \fnil\fcharset161\fprq2 DengXian Light Greek;}{\fbimajor\f31538\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} 23 | {\fbimajor\f31539\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbimajor\f31541\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbimajor\f31542\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} 24 | {\fbimajor\f31543\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbimajor\f31544\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbimajor\f31545\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} 25 | {\fbimajor\f31546\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}{\flominor\f31548\fbidi \froman\fcharset238\fprq2 Times New Roman CE;}{\flominor\f31549\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;} 26 | {\flominor\f31551\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\flominor\f31552\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;}{\flominor\f31553\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);} 27 | {\flominor\f31554\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\flominor\f31555\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;}{\flominor\f31556\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);} 28 | {\fdbminor\f31560\fbidi \fnil\fcharset0\fprq2 DengXian Western{\*\falt DengXian};}{\fdbminor\f31558\fbidi \fnil\fcharset238\fprq2 DengXian CE{\*\falt DengXian};}{\fdbminor\f31559\fbidi \fnil\fcharset204\fprq2 DengXian Cyr{\*\falt DengXian};} 29 | {\fdbminor\f31561\fbidi \fnil\fcharset161\fprq2 DengXian Greek{\*\falt DengXian};}{\fhiminor\f31570\fbidi \fnil\fcharset0\fprq2 DengXian Western{\*\falt DengXian};}{\fhiminor\f31568\fbidi \fnil\fcharset238\fprq2 DengXian CE{\*\falt DengXian};} 30 | {\fhiminor\f31569\fbidi \fnil\fcharset204\fprq2 DengXian Cyr{\*\falt DengXian};}{\fhiminor\f31571\fbidi \fnil\fcharset161\fprq2 DengXian Greek{\*\falt DengXian};}{\fbiminor\f31578\fbidi \froman\fcharset238\fprq2 Times New Roman CE;} 31 | {\fbiminor\f31579\fbidi \froman\fcharset204\fprq2 Times New Roman Cyr;}{\fbiminor\f31581\fbidi \froman\fcharset161\fprq2 Times New Roman Greek;}{\fbiminor\f31582\fbidi \froman\fcharset162\fprq2 Times New Roman Tur;} 32 | {\fbiminor\f31583\fbidi \froman\fcharset177\fprq2 Times New Roman (Hebrew);}{\fbiminor\f31584\fbidi \froman\fcharset178\fprq2 Times New Roman (Arabic);}{\fbiminor\f31585\fbidi \froman\fcharset186\fprq2 Times New Roman Baltic;} 33 | {\fbiminor\f31586\fbidi \froman\fcharset163\fprq2 Times New Roman (Vietnamese);}}{\colortbl;\red0\green0\blue0;\red0\green0\blue255;\red0\green255\blue255;\red0\green255\blue0;\red255\green0\blue255;\red255\green0\blue0;\red255\green255\blue0; 34 | \red255\green255\blue255;\red0\green0\blue128;\red0\green128\blue128;\red0\green128\blue0;\red128\green0\blue128;\red128\green0\blue0;\red128\green128\blue0;\red128\green128\blue128;\red192\green192\blue192;\red0\green0\blue0;\red0\green0\blue0; 35 | \red36\green41\blue46;}{\*\defchp \fs21\kerning2\loch\af31506\hich\af31506\dbch\af31505 }{\*\defpap \ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 }\noqfpromote {\stylesheet{ 36 | \qj \li0\ri0\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 \fs21\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 37 | \snext0 \sqformat \spriority0 Normal;}{\s1\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel0\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af13\afs48\alang1025 \ltrch\fcs0 38 | \b\fs48\lang1033\langfe2052\kerning36\loch\f13\hich\af13\dbch\af13\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext1 \slink15 \sqformat \spriority9 \styrsid4065096 heading 1;}{\s2\qj \li0\ri0\sb260\sa260\sl416\slmult1 39 | \keep\keepn\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel1\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0 \b\fs32\lang1033\langfe2052\kerning2\loch\f31502\hich\af31502\dbch\af31501\cgrid\langnp1033\langfenp2052 40 | \sbasedon0 \snext0 \slink16 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid16393825 heading 2;}{\s3\qj \li0\ri0\sb260\sa260\sl416\slmult1\keep\keepn\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\outlinelevel2\adjustright\rin0\lin0\itap0 41 | \rtlch\fcs1 \ab\af0\afs32\alang1025 \ltrch\fcs0 \b\fs32\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext0 \slink17 \ssemihidden \sunhideused \sqformat \spriority9 \styrsid16393825 42 | heading 3;}{\*\cs10 \additive \ssemihidden \sunhideused \spriority1 Default Paragraph Font;}{\* 43 | \ts11\tsrowd\trftsWidthB3\trpaddl108\trpaddr108\trpaddfl3\trpaddft3\trpaddfb3\trpaddfr3\trcbpat1\trcfpat1\tblind0\tblindtype3\tsvertalt\tsbrdrt\tsbrdrl\tsbrdrb\tsbrdrr\tsbrdrdgl\tsbrdrdgr\tsbrdrh\tsbrdrv 44 | \ql \li0\ri0\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs21\alang1025 \ltrch\fcs0 \fs21\lang1033\langfe2052\kerning2\loch\f31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 45 | \snext11 \ssemihidden \sunhideused Normal Table;}{\*\cs15 \additive \rtlch\fcs1 \ab\af13\afs48 \ltrch\fcs0 \b\fs48\kerning36\loch\f13\hich\af13\dbch\af13 \sbasedon10 \slink1 \slocked \spriority9 \styrsid4065096 \'b1\'ea\'cc\'e2 1 \'d7\'d6\'b7\'fb;}{\* 46 | \cs16 \additive \rtlch\fcs1 \ab\af0\afs32 \ltrch\fcs0 \b\fs32\loch\f31502\hich\af31502\dbch\af31501 \sbasedon10 \slink2 \slocked \ssemihidden \spriority9 \styrsid16393825 \'b1\'ea\'cc\'e2 2 \'d7\'d6\'b7\'fb;}{\*\cs17 \additive \rtlch\fcs1 \ab\af0\afs32 47 | \ltrch\fcs0 \b\fs32 \sbasedon10 \slink3 \slocked \ssemihidden \spriority9 \styrsid16393825 \'b1\'ea\'cc\'e2 3 \'d7\'d6\'b7\'fb;}{\s18\ql \li0\ri0\sb100\sa100\sbauto1\saauto1\widctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 48 | \rtlch\fcs1 \af13\afs24\alang1025 \ltrch\fcs0 \fs24\lang1033\langfe2052\loch\f13\hich\af13\dbch\af13\cgrid\langnp1033\langfenp2052 \sbasedon0 \snext18 \ssemihidden \sunhideused \styrsid4065096 Normal (Web);}{\*\cs19 \additive \rtlch\fcs1 \ab\af0 49 | \ltrch\fcs0 \b \sbasedon10 \sqformat \spriority22 \styrsid4065096 Strong;}}{\*\listtable{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext 50 | \leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li420\lin420 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat0\levelspace0\levelindent0{\leveltext\leveltemplateid1360718182 51 | \'01\'a1\'a4;}{\levelnumbers;}\fs21\loch\af13\hich\af13\dbch\af13\fbias1 \fi-947\li1367\lin1367 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698693 52 | \'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li1260\lin1260 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;} 53 | \f10\fbias0 \fi-420\li1680\lin1680 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01{\uc1\u-3986 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2100\lin2100 } 54 | {\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2520\lin2520 }{\listlevel\levelnfc23\levelnfcn23 55 | \leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2940\lin2940 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 56 | \levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01{\uc1\u-3986 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li3360\lin3360 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0 57 | {\leveltext\leveltemplateid67698693\'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li3780\lin3780 }{\listname ;}\listid505632830}{\list\listtemplateid-1\listhybrid{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1 58 | \levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li420\lin420 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext 59 | \leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li840\lin840 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698693 60 | \'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li1260\lin1260 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;} 61 | \f10\fbias0 \fi-420\li1680\lin1680 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01{\uc1\u-3986 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2100\lin2100 } 62 | {\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698693\'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2520\lin2520 }{\listlevel\levelnfc23\levelnfcn23 63 | \leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698689\'01{\uc1\u-3988 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li2940\lin2940 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0 64 | \levelstartat1\levelspace0\levelindent0{\leveltext\leveltemplateid67698691\'01{\uc1\u-3986 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li3360\lin3360 }{\listlevel\levelnfc23\levelnfcn23\leveljc0\leveljcn0\levelfollow0\levelstartat1\levelspace0\levelindent0 65 | {\leveltext\leveltemplateid67698693\'01{\uc1\u-3979 ?};}{\levelnumbers;}\f10\fbias0 \fi-420\li3780\lin3780 }{\listname ;}\listid563372932}}{\*\listoverridetable{\listoverride\listid505632830\listoverridecount9{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel} 66 | {\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}\ls1}{\listoverride\listid563372932\listoverridecount9{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}{\lfolevel}\ls2}}{\*\pgptbl {\pgp\ipgp0\itap0\li0\ri0\sb0 67 | \sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}{\pgp\ipgp0\itap0\li0\ri0\sb0\sa0}}{\*\rsidtbl \rsid1053617\rsid1319046\rsid1403075\rsid1865963\rsid2309166\rsid3611510\rsid4065096\rsid4480084\rsid4526074\rsid4872300\rsid5442686\rsid5788866\rsid6635619\rsid7104180 68 | \rsid7762485\rsid8064802\rsid8541373\rsid9395139\rsid9533150\rsid10122502\rsid10573233\rsid11434721\rsid11623888\rsid12151422\rsid12988849\rsid13056685\rsid13315385\rsid13388623\rsid16387383\rsid16393825\rsid16460058}{\mmathPr\mmathFont34\mbrkBin0 69 | \mbrkBinSub0\msmallFrac0\mdispDef1\mlMargin0\mrMargin0\mdefJc1\mwrapIndent1440\mintLim0\mnaryLim1}{\info{\author 666 Winsoft}{\operator Niu Niu}{\creatim\yr2022\mo4\dy21\hr18\min14}{\revtim\yr2024\mo1\dy30\hr11\min25}{\version29}{\edmins36}{\nofpages1} 70 | {\nofwords2}{\nofchars12}{\nofcharsws13}{\vern87}}{\*\xmlnstbl {\xmlns1 http://schemas.microsoft.com/office/word/2003/wordml}}\paperw11906\paperh16838\margl1800\margr1800\margt1440\margb1440\gutter0\ltrsect 71 | \deftab420\ftnbj\aenddoc\trackmoves0\trackformatting1\donotembedsysfont1\relyonvml0\donotembedlingdata0\grfdocevents0\validatexml1\showplaceholdtext0\ignoremixedcontent0\saveinvalidxml0\showxmlerrors1\formshade\horzdoc\dgmargin\dghspace180\dgvspace156 72 | \dghorigin1800\dgvorigin1440\dghshow0\dgvshow2\jcompress\lnongrid 73 | \viewkind1\viewscale120\splytwnine\ftnlytwnine\htmautsp\useltbaln\alntblind\lytcalctblwd\lyttblrtgr\lnbrkrule\nobrkwrptbl\snaptogridincell\allowfieldendsel\wrppunct\asianbrkrule\rsidroot1053617\newtblstyruls 74 | \nogrowautofit\usenormstyforlist\noindnmbrts\felnbrelev\nocxsptable\indrlsweleven\noafcnsttbl\afelev\utinl\hwelev\spltpgpar\notcvasp\notbrkcnstfrctbl\notvatxbx\krnprsnet\cachedcolbal \nouicompat {\upr{\*\fchars 75 | !%),.:\'3b>?]\'7d\'a1\'e9\'a1\'a7\'a1\'e3\'a1\'a4\'a1\'a6\'a1\'a5\'a8\'44\'a1\'ac\'a1\'af\'a1\'b1\'a1\'ad\'a1\'eb\'a1\'e4\'a1\'e5?\'a1\'e6\'a1\'c3\'a1\'a2\'a1\'a3\'a1\'a8\'a1\'b5\'a1\'b7\'a1\'b9\'a1\'bb\'a1\'bf\'a1\'b3\'a1\'bd\'a8\'95\'a6\'e1\'a6\'e3\'a6\'e7\'a6\'e5\'a6\'eb\'a9\'77\'a9\'79\'a9\'7b\'a3\'a1\'a3\'a2\'a3\'a5\'a3\'a7\'a3\'a9\'a3\'ac\'a3\'ae\'a3\'ba\'a3\'bb\'a3\'bf\'a3\'dd\'a3\'e0\'a3\'fc\'a3\'fd\'a1\'ab\'a1\'e9 76 | }{\*\ud\uc0{\*\fchars 77 | !%),.:\'3b>?]\'7d{\uc2\u162 \'a1\'e9\'a1\'a7\'a1\'e3\'a1\'a4\'a1\'a6\'a1\'a5\'a8D\'a1\'ac\'a1\'af\'a1\'b1\'a1\'ad\'a1\'eb\'a1\'e4\'a1\'e5}{\uc1\u8250 ?\'a1\'e6\'a1\'c3\'a1\'a2\'a1\'a3\'a1\'a8\'a1\'b5\'a1\'b7\'a1\'b9\'a1\'bb\'a1\'bf\'a1\'b3\'a1\'bd\'a8\'95\'a6\'e1\'a6\'e3\'a6\'e7\'a6\'e5\'a6\'eb\'a9w\'a9y\'a9\'7b\'a3\'a1\'a3\'a2\'a3\'a5\'a3\'a7\'a3\'a9\'a3\'ac\'a3\'ae\'a3\'ba\'a3\'bb\'a3\'bf\'a3\'dd\'a3\'e0\'a3\'fc\'a3\'fd\'a1\'ab\'a1\'e9} 78 | }}}{\upr{\*\lchars $([\'7b\'a1\'ea\'a3\'a4\'a1\'a4\'a1\'ae\'a1\'b0\'a1\'b4\'a1\'b6\'a1\'b8\'a1\'ba\'a1\'be\'a1\'b2\'a1\'bc\'a8\'94\'a9\'76\'a9\'78\'a9\'7a\'a1\'e7\'a3\'a8\'a3\'ae\'a3\'db\'a3\'fb\'a1\'ea\'a3\'a4}{\*\ud\uc0{\*\lchars 79 | $([\'7b{\uc2\u163 \'a1\'ea\u165 \'a3\'a4\'a1\'a4\'a1\'ae\'a1\'b0\'a1\'b4\'a1\'b6\'a1\'b8\'a1\'ba\'a1\'be\'a1\'b2\'a1\'bc\'a8\'94\'a9v\'a9x\'a9z\'a1\'e7\'a3\'a8\'a3\'ae\'a3\'db\'a3\'fb\'a1\'ea\'a3\'a4}}}}\fet0{\*\wgrffmtfilter 2450}\nofeaturethrottle1 80 | \ilfomacatclnup0\ltrpar \sectd \ltrsect\linex0\headery851\footery992\colsx425\endnhere\sectlinegrid312\sectspecifyl\sftnbj {\*\pnseclvl1\pnucrm\pnstart1\pnindent720\pnhang {\pntxta \dbch .}}{\*\pnseclvl2\pnucltr\pnstart1\pnindent720\pnhang 81 | {\pntxta \dbch .}}{\*\pnseclvl3\pndec\pnstart1\pnindent720\pnhang {\pntxta \dbch .}}{\*\pnseclvl4\pnlcltr\pnstart1\pnindent720\pnhang {\pntxta \dbch )}}{\*\pnseclvl5\pndec\pnstart1\pnindent720\pnhang {\pntxtb \dbch (}{\pntxta \dbch )}}{\*\pnseclvl6 82 | \pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb \dbch (}{\pntxta \dbch )}}{\*\pnseclvl7\pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb \dbch (}{\pntxta \dbch )}}{\*\pnseclvl8\pnlcltr\pnstart1\pnindent720\pnhang {\pntxtb \dbch (}{\pntxta \dbch )}}{\*\pnseclvl9 83 | \pnlcrm\pnstart1\pnindent720\pnhang {\pntxtb \dbch (}{\pntxta \dbch )}}\pard\plain \ltrpar\qj \li0\ri0\nowidctlpar\wrapdefault\aspalpha\aspnum\faauto\adjustright\rin0\lin0\itap0 \rtlch\fcs1 \af0\afs22\alang1025 \ltrch\fcs0 84 | \fs21\lang1033\langfe2052\kerning2\loch\af31506\hich\af31506\dbch\af31505\cgrid\langnp1033\langfenp2052 {\rtlch\fcs1 \ab\af44\afs21 \ltrch\fcs0 \b\cf19\kerning36\loch\af13\hich\af13\dbch\af13\insrsid11623888 \hich\af13\dbch\af13\loch\f13 Em}{\rtlch\fcs1 85 | \ab\af44\afs21 \ltrch\fcs0 \b\cf19\kerning36\loch\af13\hich\af13\dbch\af13\insrsid11623888 \hich\af13\dbch\af13\loch\f13 pty License}{\rtlch\fcs1 \af0\afs21 \ltrch\fcs0 \loch\af13\hich\af13\dbch\af13\insrsid11434721\charrsid2309166 86 | \par }{\*\themedata 504b030414000600080000002100e9de0fbfff0000001c020000130000005b436f6e74656e745f54797065735d2e786d6cac91cb4ec3301045f748fc83e52d4a 87 | 9cb2400825e982c78ec7a27cc0c8992416c9d8b2a755fbf74cd25442a820166c2cd933f79e3be372bd1f07b5c3989ca74aaff2422b24eb1b475da5df374fd9ad 88 | 5689811a183c61a50f98f4babebc2837878049899a52a57be670674cb23d8e90721f90a4d2fa3802cb35762680fd800ecd7551dc18eb899138e3c943d7e503b6 89 | b01d583deee5f99824e290b4ba3f364eac4a430883b3c092d4eca8f946c916422ecab927f52ea42b89a1cd59c254f919b0e85e6535d135a8de20f20b8c12c3b0 90 | 0c895fcf6720192de6bf3b9e89ecdbd6596cbcdd8eb28e7c365ecc4ec1ff1460f53fe813d3cc7f5b7f020000ffff0300504b030414000600080000002100a5d6 91 | a7e7c0000000360100000b0000005f72656c732f2e72656c73848fcf6ac3300c87ef85bd83d17d51d2c31825762fa590432fa37d00e1287f68221bdb1bebdb4f 92 | c7060abb0884a4eff7a93dfeae8bf9e194e720169aaa06c3e2433fcb68e1763dbf7f82c985a4a725085b787086a37bdbb55fbc50d1a33ccd311ba548b6309512 93 | 0f88d94fbc52ae4264d1c910d24a45db3462247fa791715fd71f989e19e0364cd3f51652d73760ae8fa8c9ffb3c330cc9e4fc17faf2ce545046e37944c69e462 94 | a1a82fe353bd90a865aad41ed0b5b8f9d6fd010000ffff0300504b0304140006000800000021006b799616830000008a0000001c0000007468656d652f746865 95 | 6d652f7468656d654d616e616765722e786d6c0ccc4d0ac3201040e17da17790d93763bb284562b2cbaebbf600439c1a41c7a0d29fdbd7e5e38337cedf14d59b 96 | 4b0d592c9c070d8a65cd2e88b7f07c2ca71ba8da481cc52c6ce1c715e6e97818c9b48d13df49c873517d23d59085adb5dd20d6b52bd521ef2cdd5eb9246a3d8b 97 | 4757e8d3f729e245eb2b260a0238fd010000ffff0300504b0304140006000800000021003a05cc19a2070000ce200000160000007468656d652f7468656d652f 98 | 7468656d65312e786d6cec59dd8b1b47127f3fc8ff30ccbbacaf197d2c96833ebdb1776d63c93ef2d82bb534eded9916ddad5d8b6008ce5308040249c84302e1 99 | 5eee21840b5ce04ceee1fe97f36193cbfd1157dd339ae9965af1eee20373eceeb2687a7e55fdebaaeaaa52f7cdf79fc6d43bc35c109674fcea8d8aefe164ca66 100 | 245974fc479351a9e57b42a26486284b70c75f63e1bf7febbd3fdc440732c231f6403e1107a8e347522e0fca65318561246eb0254ee0dd9cf1184978e48bf28c 101 | a373d01bd372ad526994634412df4b500c6aefcfe7648abd7fbdf8fbaf7ffae69f1f7f067ffeadcd1c430a132552a88129e5633503b6043576765a5508b1167d 102 | cabd33443b3e4c3763e713fc54fa1e4542c28b8e5fd13f7ef9d6cd323ac884a8dc236bc88df44f269709cc4e6b7a4ebe38c9270d8230687473fd1a40e52e6ed8 103 | 1c36868d5c9f06a0e914569a72b175366bfd20c31aa0f4a343f7a039a8572dbca1bfbec3b91baa5f0baf41a9fe60073f1af5c18a165e83527cb8830f7beddec0 104 | d6af4129beb1836f56ba83a069e9d7a08892e474075d091bf5fe66b53964cee8a113de0e8351b396292f50100d7974a929e62c91fb622d464f181f0140012992 105 | 24f1e47a89e7680ac1fcfac74f5ffff20fef882c2288bb254a9880d14aad32aad4e1bffa0df427ed5074809121ac680111b133a4e87862cac95276fc3ba0d537 106 | 20af5ebc78f9fce797cffff6f2934f5e3eff4b36b75665c91da26461cafdf6e72ffef3ddc7debffffafd6f5f7e954ebd8d1726de5a9a533dacb8b0c4abaf7f7a 107 | fdf34fafbef9fcd71fbe7468ef727462c22724c6c2bb87cfbd872c86053a26c027fc721293081153a29b2c044a909ac5a17f28230b7d6f8d2872e07ad8b6e363 108 | 0e99c605bcbd7a62111e477c258943e3dd28b680c78cd11ee34e2bdc557319669eac92857b72be32710f113a73cddd4789e5e5e16a092996b854f6236cd17c40 109 | 5122d10227587aea1d3bc5d8b1ba0f09b1ec7a4ca69c093697de87c4eb21e234c9849c58d154081d9218fcb27611047f5bb6397eecf51875ad7a80cf6c24ec0d 110 | 441de427985a66bc8d5612c52e95131453d3e04748462e92e3359f9ab8a190e0e905a6cc1bceb0102e99fb1cd66b38fd2e82e4e674fb315dc736924b72ead279 111 | 841833910376da8f50bc7461c724894cec07e2144214790f9874c18f99bd43d433f801257bddfd9860cbdd6fce068f20c39a948a00516f56dce1cbdb9859f13b 112 | 5ed339c2ae54d3e5b19562bb9c38a3a3b75a58a17d843145e76886b1f7e80307831e5b5a362f48df8920ab1c625760dd4176acaae7040bece9de66374f1e1161 113 | 85ec182fd81e3ec7ebadc4b346498cf83ecdf7c0eba6cd87271c36a3639df7e9f4d404de23d00942bc388d725f800e23b8f76a7d1021ab80a967e18ed735b7fc 114 | 77913d06fbf28945e302fb1264f0a56520b19b32bf6b9b09a2d60445c04c10f18e5ce916442cf71722aab86ab195536e6e6fdac20dd01c593d4f4c923735405b 115 | ad4ff8bf6b7da0c178f5ed778e187c3bed8e5bb195ab2ed9e8eccb25875bedcd3edc7653d3677c46defd9e668056c9030c656437615db734d72d8dff7fdfd2ec 116 | dbcfd78dccbe76e3ba91f1a1c1b86e64b2a395b7d3c814bd0bb435eabc233de6d1873ef1de339f39a1742cd7141f097dec23e0ebcc6c04834a4e1f7be2fc0c70 117 | 19c14755e660020bb7e048cb789cc93f12198d23b484c3a1aaaf942c44a67a21bc25137066a4879dba159eaee263364b8f3aab5575ac9956568164315e09f371 118 | 38a69229bad12c8eef72f59aed421fb36e0828d9cb903026b349d41d249a9b4165247da80b467390d02b7b2b2cda0e162da57ee3aa1d16402df70a7cdff6e05b 119 | 7ac70f03100121388e83de7ca6fc94ba7ae35dedccb7e9e97dc6b422001aec4d04149e6e2bae7b97a7569786da053c6d9130c2cd26a12da31b3c11c1b7e02c3a 120 | d5e845685cd6d7edc2a5163d650a3d1f845641a3d9fa3d1657f535c86de7069a98998226de79c76fd4430899295a76fc391c19c3c77809b123d4572e441770fd 121 | 32953cddf057c92c4b2ee400892835b84e3a69368889c4dca324eef86af9b91b68a27388e656ad41427867c9b521adbc6be4c0e9b693f17c8ea7d274bb31a22c 122 | 9d3e42864f7385f3ad16bf3a5849b215b87b1ccdcebd13bae20f118458d8ac2a03ce8880ab836a6acd1981abb03c9115f1b75598b2b46bde45e9184ac7115d46 123 | 28ab2866324fe13a95e774f4536e03e3295b3318d4304956084f16aac09a46b5aa695e35520e7babee9b8594e58ca459d44c2baba8aae9ce62d60c9b32b065cb 124 | ab157983d5c6c490d3cc0a9fa6eeed94dbdee4baad3e21af1260f0dc7e8eaa7b818260502b26b3a829c6bb6958e5ec6cd4ae1d9b05be81da458a8491f51b1bb5 125 | 5b76cb6b84733a18bc52e507b9eda885a1f9a6afd496d657e7e6b5363b7902c963005dee8a4aa15d0997d61c414334d63d499a36608b3c95d9d6804fde8a938e 126 | ff5125ec06fd5ad82f555ae1b014d4834aa91576eba56e18d6abc3b05a19f46acfa0b0c828ae86e9b5fd08ee2fe83abbbcd7e33b17f8f1e68ae6c694c565a62f 127 | e8cb9ab8bec0afd65c17f8137535ef7b0492ce478ddaa85d6ff71aa576bd3b2a05835eabd4ee377aa541a3df1c8c06fdb0d51e3df3bd330d0ebaf57ed018b64a 128 | 8d6abf5f0a1a1545bfd52e35835aad1b34bbad61d07d96b531b0f2347d64b600f36a5eb7fe0b0000ffff0300504b0304140006000800000021000dd1909fb600 129 | 00001b010000270000007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73848f4d0ac2301484f78277086f6fd3 130 | ba109126dd88d0add40384e4350d363f2451eced0dae2c082e8761be9969bb979dc9136332de3168aa1a083ae995719ac16db8ec8e4052164e89d93b64b06082 131 | 8e6f37ed1567914b284d262452282e3198720e274a939cd08a54f980ae38a38f56e422a3a641c8bbd048f7757da0f19b017cc524bd62107bd5001996509affb3 132 | fd381a89672f1f165dfe514173d9850528a2c6cce0239baa4c04ca5bbabac4df000000ffff0300504b01022d0014000600080000002100e9de0fbfff0000001c 133 | 0200001300000000000000000000000000000000005b436f6e74656e745f54797065735d2e786d6c504b01022d0014000600080000002100a5d6a7e7c0000000 134 | 360100000b00000000000000000000000000300100005f72656c732f2e72656c73504b01022d00140006000800000021006b799616830000008a0000001c0000 135 | 0000000000000000000000190200007468656d652f7468656d652f7468656d654d616e616765722e786d6c504b01022d00140006000800000021003a05cc19a2 136 | 070000ce2000001600000000000000000000000000d60200007468656d652f7468656d652f7468656d65312e786d6c504b01022d00140006000800000021000d 137 | d1909fb60000001b0100002700000000000000000000000000ac0a00007468656d652f7468656d652f5f72656c732f7468656d654d616e616765722e786d6c2e72656c73504b050600000000050005005d010000a70b00000000} 138 | {\*\colorschememapping 3c3f786d6c2076657273696f6e3d22312e302220656e636f64696e673d225554462d3822207374616e64616c6f6e653d22796573223f3e0d0a3c613a636c724d 139 | 617020786d6c6e733a613d22687474703a2f2f736368656d61732e6f70656e786d6c666f726d6174732e6f72672f64726177696e676d6c2f323030362f6d6169 140 | 6e22206267313d226c743122207478313d22646b3122206267323d226c743222207478323d22646b322220616363656e74313d22616363656e74312220616363 141 | 656e74323d22616363656e74322220616363656e74333d22616363656e74332220616363656e74343d22616363656e74342220616363656e74353d22616363656e74352220616363656e74363d22616363656e74362220686c696e6b3d22686c696e6b2220666f6c486c696e6b3d22666f6c486c696e6b222f3e} 142 | {\*\latentstyles\lsdstimax376\lsdlockeddef0\lsdsemihiddendef0\lsdunhideuseddef0\lsdqformatdef0\lsdprioritydef99{\lsdlockedexcept \lsdqformat1 \lsdpriority0 \lsdlocked0 Normal;\lsdqformat1 \lsdpriority9 \lsdlocked0 heading 1; 143 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 2;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 3;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 4; 144 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 5;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 6;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 7; 145 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 8;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority9 \lsdlocked0 heading 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 1; 146 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 5; 147 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 7;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 8;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index 9; 148 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 1;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 2;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 3; 149 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 4;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 5;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 6; 150 | \lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 7;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 8;\lsdsemihidden1 \lsdunhideused1 \lsdpriority39 \lsdlocked0 toc 9;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal Indent; 151 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 header;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footer; 152 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 index heading;\lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority35 \lsdlocked0 caption;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of figures; 153 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 envelope return;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 footnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation reference; 154 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 line number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 page number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote reference;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 endnote text; 155 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 table of authorities;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 macro;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 toa heading;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List; 156 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 3; 157 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 3; 158 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Bullet 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 3; 159 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 4;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Number 5;\lsdqformat1 \lsdpriority10 \lsdlocked0 Title;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Closing; 160 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Signature;\lsdsemihidden1 \lsdunhideused1 \lsdpriority1 \lsdlocked0 Default Paragraph Font;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent; 161 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 4; 162 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 List Continue 5;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Message Header;\lsdqformat1 \lsdpriority11 \lsdlocked0 Subtitle;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Salutation; 163 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Date;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text First Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Note Heading; 164 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Body Text Indent 3; 165 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Block Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 FollowedHyperlink;\lsdqformat1 \lsdpriority22 \lsdlocked0 Strong; 166 | \lsdqformat1 \lsdpriority20 \lsdlocked0 Emphasis;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Document Map;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Plain Text;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 E-mail Signature; 167 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Top of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Bottom of Form;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Normal (Web);\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Acronym; 168 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Address;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Cite;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Code;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Definition; 169 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Keyboard;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Preformatted;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Sample;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Typewriter; 170 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 HTML Variable;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 annotation subject;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 No List;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 1; 171 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 2;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Outline List 3;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Balloon Text;\lsdpriority39 \lsdlocked0 Table Grid; 172 | \lsdsemihidden1 \lsdlocked0 Placeholder Text;\lsdqformat1 \lsdpriority1 \lsdlocked0 No Spacing;\lsdpriority60 \lsdlocked0 Light Shading;\lsdpriority61 \lsdlocked0 Light List;\lsdpriority62 \lsdlocked0 Light Grid; 173 | \lsdpriority63 \lsdlocked0 Medium Shading 1;\lsdpriority64 \lsdlocked0 Medium Shading 2;\lsdpriority65 \lsdlocked0 Medium List 1;\lsdpriority66 \lsdlocked0 Medium List 2;\lsdpriority67 \lsdlocked0 Medium Grid 1;\lsdpriority68 \lsdlocked0 Medium Grid 2; 174 | \lsdpriority69 \lsdlocked0 Medium Grid 3;\lsdpriority70 \lsdlocked0 Dark List;\lsdpriority71 \lsdlocked0 Colorful Shading;\lsdpriority72 \lsdlocked0 Colorful List;\lsdpriority73 \lsdlocked0 Colorful Grid;\lsdpriority60 \lsdlocked0 Light Shading Accent 1; 175 | \lsdpriority61 \lsdlocked0 Light List Accent 1;\lsdpriority62 \lsdlocked0 Light Grid Accent 1;\lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 1;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 1;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 1; 176 | \lsdsemihidden1 \lsdlocked0 Revision;\lsdqformat1 \lsdpriority34 \lsdlocked0 List Paragraph;\lsdqformat1 \lsdpriority29 \lsdlocked0 Quote;\lsdqformat1 \lsdpriority30 \lsdlocked0 Intense Quote;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 1; 177 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 1;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 1;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 1;\lsdpriority70 \lsdlocked0 Dark List Accent 1;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 1; 178 | \lsdpriority72 \lsdlocked0 Colorful List Accent 1;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 1;\lsdpriority60 \lsdlocked0 Light Shading Accent 2;\lsdpriority61 \lsdlocked0 Light List Accent 2;\lsdpriority62 \lsdlocked0 Light Grid Accent 2; 179 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 2;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 2;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 2;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 2; 180 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 2;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 2;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 2;\lsdpriority70 \lsdlocked0 Dark List Accent 2;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 2; 181 | \lsdpriority72 \lsdlocked0 Colorful List Accent 2;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 2;\lsdpriority60 \lsdlocked0 Light Shading Accent 3;\lsdpriority61 \lsdlocked0 Light List Accent 3;\lsdpriority62 \lsdlocked0 Light Grid Accent 3; 182 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 3;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 3;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 3;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 3; 183 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 3;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 3;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 3;\lsdpriority70 \lsdlocked0 Dark List Accent 3;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 3; 184 | \lsdpriority72 \lsdlocked0 Colorful List Accent 3;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 3;\lsdpriority60 \lsdlocked0 Light Shading Accent 4;\lsdpriority61 \lsdlocked0 Light List Accent 4;\lsdpriority62 \lsdlocked0 Light Grid Accent 4; 185 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 4;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 4;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 4;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 4; 186 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 4;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 4;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 4;\lsdpriority70 \lsdlocked0 Dark List Accent 4;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 4; 187 | \lsdpriority72 \lsdlocked0 Colorful List Accent 4;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 4;\lsdpriority60 \lsdlocked0 Light Shading Accent 5;\lsdpriority61 \lsdlocked0 Light List Accent 5;\lsdpriority62 \lsdlocked0 Light Grid Accent 5; 188 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 5;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 5;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 5;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 5; 189 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 5;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 5;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 5;\lsdpriority70 \lsdlocked0 Dark List Accent 5;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 5; 190 | \lsdpriority72 \lsdlocked0 Colorful List Accent 5;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 5;\lsdpriority60 \lsdlocked0 Light Shading Accent 6;\lsdpriority61 \lsdlocked0 Light List Accent 6;\lsdpriority62 \lsdlocked0 Light Grid Accent 6; 191 | \lsdpriority63 \lsdlocked0 Medium Shading 1 Accent 6;\lsdpriority64 \lsdlocked0 Medium Shading 2 Accent 6;\lsdpriority65 \lsdlocked0 Medium List 1 Accent 6;\lsdpriority66 \lsdlocked0 Medium List 2 Accent 6; 192 | \lsdpriority67 \lsdlocked0 Medium Grid 1 Accent 6;\lsdpriority68 \lsdlocked0 Medium Grid 2 Accent 6;\lsdpriority69 \lsdlocked0 Medium Grid 3 Accent 6;\lsdpriority70 \lsdlocked0 Dark List Accent 6;\lsdpriority71 \lsdlocked0 Colorful Shading Accent 6; 193 | \lsdpriority72 \lsdlocked0 Colorful List Accent 6;\lsdpriority73 \lsdlocked0 Colorful Grid Accent 6;\lsdqformat1 \lsdpriority19 \lsdlocked0 Subtle Emphasis;\lsdqformat1 \lsdpriority21 \lsdlocked0 Intense Emphasis; 194 | \lsdqformat1 \lsdpriority31 \lsdlocked0 Subtle Reference;\lsdqformat1 \lsdpriority32 \lsdlocked0 Intense Reference;\lsdqformat1 \lsdpriority33 \lsdlocked0 Book Title;\lsdsemihidden1 \lsdunhideused1 \lsdpriority37 \lsdlocked0 Bibliography; 195 | \lsdsemihidden1 \lsdunhideused1 \lsdqformat1 \lsdpriority39 \lsdlocked0 TOC Heading;\lsdpriority41 \lsdlocked0 Plain Table 1;\lsdpriority42 \lsdlocked0 Plain Table 2;\lsdpriority43 \lsdlocked0 Plain Table 3;\lsdpriority44 \lsdlocked0 Plain Table 4; 196 | \lsdpriority45 \lsdlocked0 Plain Table 5;\lsdpriority40 \lsdlocked0 Grid Table Light;\lsdpriority46 \lsdlocked0 Grid Table 1 Light;\lsdpriority47 \lsdlocked0 Grid Table 2;\lsdpriority48 \lsdlocked0 Grid Table 3;\lsdpriority49 \lsdlocked0 Grid Table 4; 197 | \lsdpriority50 \lsdlocked0 Grid Table 5 Dark;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 1; 198 | \lsdpriority48 \lsdlocked0 Grid Table 3 Accent 1;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 1;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 1; 199 | \lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 1;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 2;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 2; 200 | \lsdpriority49 \lsdlocked0 Grid Table 4 Accent 2;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 2; 201 | \lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 3;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 3;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 3;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 3; 202 | \lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 3;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 4; 203 | \lsdpriority47 \lsdlocked0 Grid Table 2 Accent 4;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 4;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 4;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 4; 204 | \lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 4;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 5; 205 | \lsdpriority48 \lsdlocked0 Grid Table 3 Accent 5;\lsdpriority49 \lsdlocked0 Grid Table 4 Accent 5;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 5; 206 | \lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 5;\lsdpriority46 \lsdlocked0 Grid Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 Grid Table 2 Accent 6;\lsdpriority48 \lsdlocked0 Grid Table 3 Accent 6; 207 | \lsdpriority49 \lsdlocked0 Grid Table 4 Accent 6;\lsdpriority50 \lsdlocked0 Grid Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 Grid Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 Grid Table 7 Colorful Accent 6; 208 | \lsdpriority46 \lsdlocked0 List Table 1 Light;\lsdpriority47 \lsdlocked0 List Table 2;\lsdpriority48 \lsdlocked0 List Table 3;\lsdpriority49 \lsdlocked0 List Table 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark; 209 | \lsdpriority51 \lsdlocked0 List Table 6 Colorful;\lsdpriority52 \lsdlocked0 List Table 7 Colorful;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 1;\lsdpriority47 \lsdlocked0 List Table 2 Accent 1;\lsdpriority48 \lsdlocked0 List Table 3 Accent 1; 210 | \lsdpriority49 \lsdlocked0 List Table 4 Accent 1;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 1;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 1;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 1; 211 | \lsdpriority46 \lsdlocked0 List Table 1 Light Accent 2;\lsdpriority47 \lsdlocked0 List Table 2 Accent 2;\lsdpriority48 \lsdlocked0 List Table 3 Accent 2;\lsdpriority49 \lsdlocked0 List Table 4 Accent 2; 212 | \lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 2;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 2;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 2;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 3; 213 | \lsdpriority47 \lsdlocked0 List Table 2 Accent 3;\lsdpriority48 \lsdlocked0 List Table 3 Accent 3;\lsdpriority49 \lsdlocked0 List Table 4 Accent 3;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 3; 214 | \lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 3;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 3;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 4;\lsdpriority47 \lsdlocked0 List Table 2 Accent 4; 215 | \lsdpriority48 \lsdlocked0 List Table 3 Accent 4;\lsdpriority49 \lsdlocked0 List Table 4 Accent 4;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 4;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 4; 216 | \lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 4;\lsdpriority46 \lsdlocked0 List Table 1 Light Accent 5;\lsdpriority47 \lsdlocked0 List Table 2 Accent 5;\lsdpriority48 \lsdlocked0 List Table 3 Accent 5; 217 | \lsdpriority49 \lsdlocked0 List Table 4 Accent 5;\lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 5;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 5;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 5; 218 | \lsdpriority46 \lsdlocked0 List Table 1 Light Accent 6;\lsdpriority47 \lsdlocked0 List Table 2 Accent 6;\lsdpriority48 \lsdlocked0 List Table 3 Accent 6;\lsdpriority49 \lsdlocked0 List Table 4 Accent 6; 219 | \lsdpriority50 \lsdlocked0 List Table 5 Dark Accent 6;\lsdpriority51 \lsdlocked0 List Table 6 Colorful Accent 6;\lsdpriority52 \lsdlocked0 List Table 7 Colorful Accent 6;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Mention; 220 | \lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Hyperlink;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Hashtag;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Unresolved Mention;\lsdsemihidden1 \lsdunhideused1 \lsdlocked0 Smart Link;}}{\*\datastore 01050000 221 | 02000000180000004d73786d6c322e534158584d4c5265616465722e362e3000000000000000000000060000 222 | d0cf11e0a1b11ae1000000000000000000000000000000003e000300feff090006000000000000000000000001000000010000000000000000100000feffffff00000000feffffff0000000000000000ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 223 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 224 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 225 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 226 | fffffffffffffffffdfffffffeffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 227 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 228 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 229 | ffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffffff 230 | ffffffffffffffffffffffffffffffff52006f006f007400200045006e00740072007900000000000000000000000000000000000000000000000000000000000000000000000000000000000000000016000500ffffffffffffffffffffffff0c6ad98892f1d411a65f0040963251e50000000000000000000000007008 231 | 23fa2b53da01feffffff00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff00000000000000000000000000000000000000000000000000000000 232 | 00000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff0000000000000000000000000000000000000000000000000000 233 | 000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000000ffffffffffffffffffffffff000000000000000000000000000000000000000000000000 234 | 0000000000000000000000000000000000000000000000000105000000000000}} -------------------------------------------------------------------------------- /setup/NSIS/nsis-3.08.zip: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/setup/NSIS/nsis-3.08.zip -------------------------------------------------------------------------------- /setup/NSIS/nsis-3.08/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/setup/NSIS/nsis-3.08/.gitkeep -------------------------------------------------------------------------------- /setup/NSIS/uninstall.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/setup/NSIS/uninstall.ico -------------------------------------------------------------------------------- /setup/NSIS/win-setup-x64.nsi: -------------------------------------------------------------------------------- 1 | !define PRODUCT_NAME "Electron-Vue3-Boilerplate" 2 | !define PRODUCT_DESC "A Electron + Vue3 boilerplate." 3 | !define EXE_NAME "Electron-Vue3-Boilerplate.exe" 4 | !define PRODUCT_VERSION "1.0.0.0" 5 | !define PRODUCT_PUBLISHER "https://github.com/winsoft666/" 6 | !define PRODUCT_LEGAL "Copyright (C) 2024. All Rights Reserved" 7 | !define DESKTOP_LNK_NAME "Electron-Vue3-Boilerplate" 8 | !define REG_ORGANIZATION_NAME "Electron-Vue3-Boilerplate" 9 | !define REG_APPLICATION_NAME "Electron-Vue3-Boilerplate" 10 | 11 | !include "MUI2.nsh" 12 | !include "FileFunc.nsh" 13 | !insertmacro GetParameters 14 | !insertmacro GetOptions 15 | 16 | SetCompressor LZMA 17 | Unicode True 18 | 19 | Name "${PRODUCT_NAME}" 20 | OutFile "${PRODUCT_NAME}-Setup-x64.exe" 21 | InstallDir "$LocalAppdata\Programs\${PRODUCT_NAME}" 22 | ShowInstDetails hide 23 | ShowUnInstDetails hide 24 | ManifestDPIAware true 25 | 26 | BrandingText "${REG_ORGANIZATION_NAME}" 27 | # RequestExecutionLevel none|user|highest|admin 28 | RequestExecutionLevel user 29 | 30 | !define MUI_ICON ".\install.ico" 31 | !define MUI_UNICON ".\install.ico" 32 | !define MUI_CUSTOMFUNCTION_GUIINIT onGUIInit 33 | 34 | !insertmacro MUI_PAGE_WELCOME 35 | !insertmacro MUI_PAGE_LICENSE ".\license.rtf" 36 | !insertmacro MUI_PAGE_DIRECTORY 37 | !insertmacro MUI_PAGE_INSTFILES 38 | !insertmacro MUI_PAGE_FINISH 39 | 40 | !insertmacro MUI_UNPAGE_WELCOME 41 | !insertmacro MUI_UNPAGE_INSTFILES 42 | !insertmacro MUI_UNPAGE_FINISH 43 | 44 | !insertmacro MUI_LANGUAGE "English" 45 | 46 | VIProductVersion "${PRODUCT_VERSION}" 47 | VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}" 48 | VIAddVersionKey "ProductName" "${PRODUCT_NAME}" 49 | VIAddVersionKey "CompanyName" "${PRODUCT_PUBLISHER}" 50 | VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}" 51 | VIAddVersionKey "InternalName" "${EXE_NAME}" 52 | VIAddVersionKey "FileDescription" "${PRODUCT_DESC}" 53 | VIAddVersionKey "LegalCopyright" "${PRODUCT_LEGAL}" 54 | 55 | Var RunByAs 56 | 57 | !macro ParseParameters 58 | ${GetParameters} $R0 59 | ${GetOptions} $R0 '-RunByAs' $R1 60 | StrCpy $RunByAs $R1 61 | !macroend 62 | 63 | 64 | Section "!Files" 65 | SetPluginUnload alwaysoff 66 | SetOutPath $INSTDIR 67 | 68 | # TODO 69 | File /r "..\..\out\Electron-Vue3-Boilerplate-win32-x64\*" 70 | SectionEnd 71 | 72 | 73 | Section "Shortcut" 74 | SetPluginUnload alwaysoff 75 | SetShellVarContext current 76 | 77 | CreateShortCut "$DESKTOP\${DESKTOP_LNK_NAME}.lnk" "$INSTDIR\${EXE_NAME}" 78 | CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" 79 | CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${DESKTOP_LNK_NAME}.lnk" "$INSTDIR\${EXE_NAME}" 80 | CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe" 81 | SectionEnd 82 | 83 | Section "-Necessary" 84 | SetPluginUnload alwaysoff 85 | WriteUninstaller "$INSTDIR\uninst.exe" 86 | 87 | WriteRegStr HKCU "Software\${REG_ORGANIZATION_NAME}\${REG_APPLICATION_NAME}" "Path" "$INSTDIR" 88 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}" 89 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "$INSTDIR\uninst.exe" 90 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${EXE_NAME}" 91 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "${PRODUCT_PUBLISHER}" 92 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "${PRODUCT_PUBLISHER}" 93 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "${PRODUCT_PUBLISHER}" 94 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" 95 | SectionEnd 96 | 97 | Section "-Uninstall" 98 | SetPluginUnload alwaysoff 99 | 100 | SetShellVarContext current 101 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\${DESKTOP_LNK_NAME}.lnk" 102 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" 103 | RMDir "$SMPROGRAMS\${PRODUCT_NAME}\" 104 | Delete "$DESKTOP\${DESKTOP_LNK_NAME}.lnk" 105 | 106 | RMDir /r "$INSTDIR" 107 | 108 | DeleteRegKey HKCU "Software\${REG_ORGANIZATION_NAME}" 109 | DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" 110 | SetAutoClose true 111 | SectionEnd 112 | 113 | Function .onInit 114 | ReadRegStr $R0 HKCU "Software\${REG_ORGANIZATION_NAME}\${REG_APPLICATION_NAME}" "Path" 115 | ${If} $R0 != "" 116 | StrCpy $INSTDIR $R0 117 | ${EndIf} 118 | FunctionEnd 119 | 120 | Function .onVerifyInstDir 121 | # TODO 122 | FunctionEnd 123 | 124 | Function onGUIInit 125 | # TODO 126 | FunctionEnd 127 | 128 | 129 | Function .onInstSuccess 130 | Exec '"$INSTDIR\${EXE_NAME}" -BySetup' 131 | FunctionEnd 132 | 133 | 134 | Function un.onInit 135 | StrCpy $RunByAs "" 136 | 137 | !insertmacro ParseParameters 138 | 139 | ${If} "$RunByAs" != "1" 140 | ExecShell "runas" "$INSTDIR\uninst.exe" "-RunByAs 1" 141 | Abort 142 | ${EndIf} 143 | FunctionEnd 144 | 145 | Function un.onUninstSuccess 146 | # TODO 147 | FunctionEnd -------------------------------------------------------------------------------- /setup/NSIS/win-setup-x86.nsi: -------------------------------------------------------------------------------- 1 | !define PRODUCT_NAME "Electron-Vue3-Boilerplate" 2 | !define PRODUCT_DESC "A Electron + Vue3 boilerplate." 3 | !define EXE_NAME "Electron-Vue3-Boilerplate.exe" 4 | !define PRODUCT_VERSION "1.0.0.0" 5 | !define PRODUCT_PUBLISHER "https://github.com/winsoft666/" 6 | !define PRODUCT_LEGAL "Copyright (C) 2024. All Rights Reserved" 7 | !define DESKTOP_LNK_NAME "Electron-Vue3-Boilerplate" 8 | !define REG_ORGANIZATION_NAME "Electron-Vue3-Boilerplate" 9 | !define REG_APPLICATION_NAME "Electron-Vue3-Boilerplate" 10 | 11 | !include "MUI2.nsh" 12 | !include "FileFunc.nsh" 13 | !insertmacro GetParameters 14 | !insertmacro GetOptions 15 | 16 | SetCompressor LZMA 17 | Unicode True 18 | 19 | Name "${PRODUCT_NAME}" 20 | OutFile "${PRODUCT_NAME}-Setup-x86.exe" 21 | InstallDir "$LocalAppdata\Programs\${PRODUCT_NAME}" 22 | ShowInstDetails hide 23 | ShowUnInstDetails hide 24 | ManifestDPIAware true 25 | 26 | BrandingText "${REG_ORGANIZATION_NAME}" 27 | # RequestExecutionLevel none|user|highest|admin 28 | RequestExecutionLevel user 29 | 30 | !define MUI_ICON ".\install.ico" 31 | !define MUI_UNICON ".\install.ico" 32 | !define MUI_CUSTOMFUNCTION_GUIINIT onGUIInit 33 | 34 | !insertmacro MUI_PAGE_WELCOME 35 | !insertmacro MUI_PAGE_LICENSE ".\license.rtf" 36 | !insertmacro MUI_PAGE_DIRECTORY 37 | !insertmacro MUI_PAGE_INSTFILES 38 | !insertmacro MUI_PAGE_FINISH 39 | 40 | !insertmacro MUI_UNPAGE_WELCOME 41 | !insertmacro MUI_UNPAGE_INSTFILES 42 | !insertmacro MUI_UNPAGE_FINISH 43 | 44 | !insertmacro MUI_LANGUAGE "English" 45 | 46 | VIProductVersion "${PRODUCT_VERSION}" 47 | VIAddVersionKey "ProductVersion" "${PRODUCT_VERSION}" 48 | VIAddVersionKey "ProductName" "${PRODUCT_NAME}" 49 | VIAddVersionKey "CompanyName" "${PRODUCT_PUBLISHER}" 50 | VIAddVersionKey "FileVersion" "${PRODUCT_VERSION}" 51 | VIAddVersionKey "InternalName" "${EXE_NAME}" 52 | VIAddVersionKey "FileDescription" "${PRODUCT_DESC}" 53 | VIAddVersionKey "LegalCopyright" "${PRODUCT_LEGAL}" 54 | 55 | Var RunByAs 56 | 57 | !macro ParseParameters 58 | ${GetParameters} $R0 59 | ${GetOptions} $R0 '-RunByAs' $R1 60 | StrCpy $RunByAs $R1 61 | !macroend 62 | 63 | 64 | Section "!Files" 65 | SetPluginUnload alwaysoff 66 | SetOutPath $INSTDIR 67 | 68 | # TODO 69 | File /r "..\..\out\Electron-Vue3-Boilerplate-win32-ia32\*" 70 | SectionEnd 71 | 72 | 73 | Section "Shortcut" 74 | SetPluginUnload alwaysoff 75 | SetShellVarContext current 76 | 77 | CreateShortCut "$DESKTOP\${DESKTOP_LNK_NAME}.lnk" "$INSTDIR\${EXE_NAME}" 78 | CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}" 79 | CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\${DESKTOP_LNK_NAME}.lnk" "$INSTDIR\${EXE_NAME}" 80 | CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninst.exe" 81 | SectionEnd 82 | 83 | Section "-Necessary" 84 | SetPluginUnload alwaysoff 85 | WriteUninstaller "$INSTDIR\uninst.exe" 86 | 87 | WriteRegStr HKCU "Software\${REG_ORGANIZATION_NAME}\${REG_APPLICATION_NAME}" "Path" "$INSTDIR" 88 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayName" "${PRODUCT_NAME}" 89 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "UninstallString" "$INSTDIR\uninst.exe" 90 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayIcon" "$INSTDIR\${EXE_NAME}" 91 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "Publisher" "${PRODUCT_PUBLISHER}" 92 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "URLInfoAbout" "${PRODUCT_PUBLISHER}" 93 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "HelpLink" "${PRODUCT_PUBLISHER}" 94 | WriteRegStr HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" "DisplayVersion" "${PRODUCT_VERSION}" 95 | SectionEnd 96 | 97 | Section "-Uninstall" 98 | SetPluginUnload alwaysoff 99 | 100 | SetShellVarContext current 101 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\${DESKTOP_LNK_NAME}.lnk" 102 | Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" 103 | RMDir "$SMPROGRAMS\${PRODUCT_NAME}\" 104 | Delete "$DESKTOP\${DESKTOP_LNK_NAME}.lnk" 105 | 106 | RMDir /r "$INSTDIR" 107 | 108 | DeleteRegKey HKCU "Software\${REG_ORGANIZATION_NAME}" 109 | DeleteRegKey HKCU "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}" 110 | SetAutoClose true 111 | SectionEnd 112 | 113 | Function .onInit 114 | ReadRegStr $R0 HKCU "Software\${REG_ORGANIZATION_NAME}\${REG_APPLICATION_NAME}" "Path" 115 | ${If} $R0 != "" 116 | StrCpy $INSTDIR $R0 117 | ${EndIf} 118 | FunctionEnd 119 | 120 | Function .onVerifyInstDir 121 | # TODO 122 | FunctionEnd 123 | 124 | Function onGUIInit 125 | # TODO 126 | FunctionEnd 127 | 128 | 129 | Function .onInstSuccess 130 | Exec '"$INSTDIR\${EXE_NAME}" -BySetup' 131 | FunctionEnd 132 | 133 | 134 | Function un.onInit 135 | StrCpy $RunByAs "" 136 | 137 | !insertmacro ParseParameters 138 | 139 | ${If} "$RunByAs" != "1" 140 | ExecShell "runas" "$INSTDIR\uninst.exe" "-RunByAs 1" 141 | Abort 142 | ${EndIf} 143 | FunctionEnd 144 | 145 | Function un.onUninstSuccess 146 | # TODO 147 | FunctionEnd -------------------------------------------------------------------------------- /setup/exe.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/setup/exe.ico -------------------------------------------------------------------------------- /src/lib/axios-inst/main/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { PrepareAxios } from "../shared"; 3 | 4 | const axiosInstance = axios.create({ 5 | // Always use Node.js adapter 6 | adapter: "http" 7 | }); 8 | 9 | PrepareAxios(axiosInstance); 10 | 11 | export default axiosInstance; -------------------------------------------------------------------------------- /src/lib/axios-inst/renderer/index.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import { PrepareAxios } from "../shared"; 3 | 4 | 5 | const axiosInstance = axios.create(); 6 | PrepareAxios(axiosInstance); 7 | 8 | export default axiosInstance; -------------------------------------------------------------------------------- /src/lib/axios-inst/shared/index.ts: -------------------------------------------------------------------------------- 1 | import { AxiosInstance } from "axios"; 2 | 3 | function PrepareAxios(axiosInst: AxiosInstance){ 4 | axiosInst.defaults.baseURL = ""; 5 | axiosInst.defaults.timeout = 3000; 6 | 7 | axiosInst.interceptors.request.use( 8 | config => { 9 | config.headers["repo"] = "https://github.com/winsoft666/electron-vue3-template"; 10 | return config; 11 | }, 12 | error => { 13 | // TODO: error handler 14 | return Promise.reject(error); 15 | } 16 | ); 17 | 18 | axiosInst.interceptors.response.use( 19 | response => { 20 | return response; 21 | }, 22 | error => { 23 | // TODO: error handler 24 | return Promise.reject(error); 25 | } 26 | ); 27 | } 28 | 29 | export { 30 | PrepareAxios 31 | }; -------------------------------------------------------------------------------- /src/lib/file-download/main/file-download-preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | import { Options } from "../shared"; 3 | 4 | function initialize(){ 5 | if(!ipcRenderer){ 6 | return; 7 | } 8 | 9 | if(contextBridge && process.contextIsolated){ 10 | try { 11 | contextBridge.exposeInMainWorld("__ElectronFileDownload__", { 12 | asyncDownloadFile: (options: Options) => ipcRenderer.invoke("electron-file-download-async-download-file", options), 13 | cancelDownloadFile: (uuid: string) => ipcRenderer.send("electron-file-download-cancel-download-file", uuid), 14 | onDownloadFilePrgressFeedback: (callback) => ipcRenderer.on("electron-file-download-download-file-progress-feedback", (_event, uuid: string, bytesDone: number, bytesTotal: number) => { 15 | callback(uuid, bytesDone, bytesTotal); 16 | }), 17 | }); 18 | } catch { 19 | // Sometimes this files can be included twice 20 | } 21 | } 22 | } 23 | 24 | initialize(); -------------------------------------------------------------------------------- /src/lib/file-download/main/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This code can only be used in the main process. 3 | */ 4 | 5 | import { app, session, BrowserWindow, ipcMain } from "electron"; 6 | import fs from "fs"; 7 | import path from "path"; 8 | import { FileUtils } from "../../utils/main"; 9 | import { GetErrorMessage } from "../../utils/shared"; 10 | import * as fdTypes from "../shared"; 11 | 12 | class FileDownload{ 13 | public initialize(){ 14 | this._preloadFilePath = path.join(__dirname, "file-download-preload.js"); 15 | // console.log("File download preload path: " + this._preloadFilePath); 16 | this.setPreload(session.defaultSession); 17 | 18 | app.on("session-created", (session) => { 19 | this.setPreload(session); 20 | }); 21 | } 22 | 23 | // Public: Download a file and store it on a file system using streaming with appropriate progress callback. 24 | // Returns a {Promise} that will accept when complete. 25 | public async download(options: fdTypes.Options, browserWindow : BrowserWindow | null): Promise{ 26 | const result : fdTypes.Result = { 27 | success: false, 28 | canceled: false, 29 | error: "", 30 | uuid: options.uuid, 31 | fileSize: 0, 32 | }; 33 | 34 | if(!this.checkParam(options)){ 35 | throw Error("Param error"); 36 | } 37 | 38 | if(await this.checkWhetherHasDownloaded(options)){ 39 | result.success = true; 40 | result.fileSize = FileUtils.GetFileSize(options.savePath); 41 | if(options.feedbackProgressToRenderer && browserWindow){ 42 | browserWindow.webContents.send("file-download-progress-feedback", options.uuid, result.fileSize, result.fileSize); 43 | } 44 | return result; 45 | } 46 | 47 | const dir = path.dirname(options.savePath); 48 | if(dir && !FileUtils.IsPathExist(dir)){ 49 | if(!FileUtils.CreateDirectories(dir)){ 50 | throw Error(`Unable to create directory ${dir}`); 51 | } 52 | } 53 | 54 | const request = new Request(options.url, { 55 | headers: new Headers({ "Content-Type": "application/octet-stream" }) 56 | }); 57 | 58 | const response = await fetch(request); 59 | if(!response.ok){ 60 | throw Error(`Unable to download, server returned ${response.status} ${response.statusText}`); 61 | } 62 | 63 | const body = response.body; 64 | if(body == null){ 65 | throw Error("No response body"); 66 | } 67 | 68 | const finalLength = options.fileSize || parseInt(response.headers.get("Content-Length") || "0", 10); 69 | const reader = body.getReader(); 70 | const writer = fs.createWriteStream(options.savePath); 71 | 72 | this._readerMap.set(options.uuid, reader); 73 | this._cancelFlagMap.set(options.uuid, false); 74 | 75 | await this.streamWithProgress(finalLength, reader, writer, options, browserWindow); 76 | 77 | writer.end(); 78 | 79 | if(options.verifyMd5 && options.md5){ 80 | try { 81 | const actualMd5 = await FileUtils.GetFileMd5(options.savePath); 82 | if(actualMd5.toLowerCase() != options.md5.toLowerCase()){ 83 | throw Error(`${actualMd5} is not equal to ${options.md5}`); 84 | } 85 | } catch (e){ 86 | throw Error(`Hash verification not pass: ${GetErrorMessage(e)}`); 87 | } 88 | } 89 | 90 | result.success = true; 91 | result.fileSize = FileUtils.GetFileSize(options.savePath); 92 | return result; 93 | } 94 | 95 | public cancel(uuid: string){ 96 | if(uuid){ 97 | if(this._readerMap.has(uuid) && this._cancelFlagMap.has(uuid)){ 98 | const reader = this._readerMap.get(uuid); 99 | if(reader){ 100 | this._cancelFlagMap.set(uuid, true); 101 | reader.cancel(); 102 | } 103 | } 104 | } 105 | } 106 | 107 | protected checkParam(options: fdTypes.Options) : boolean{ 108 | if(!options.uuid) 109 | return false; 110 | 111 | if(!options.url) 112 | return false; 113 | 114 | if(!options.savePath) 115 | return false; 116 | 117 | if(options.verifyMd5 && !options.md5) 118 | return false; 119 | 120 | return true; 121 | } 122 | 123 | protected async checkWhetherHasDownloaded(options: fdTypes.Options) : Promise{ 124 | if(options.skipWhenFileExist){ 125 | if(FileUtils.IsPathExist(options.savePath)){ 126 | return true; 127 | } 128 | } 129 | 130 | if(options.skipWhenMd5Same && options.md5){ 131 | if(FileUtils.IsPathExist(options.savePath)){ 132 | try { 133 | const curMd5 = await FileUtils.GetFileMd5(options.savePath); 134 | if(curMd5.toLowerCase() == options.md5.toLowerCase()){ 135 | return true; 136 | } 137 | } catch (e){ 138 | (e); 139 | } 140 | } 141 | } 142 | 143 | return false; 144 | } 145 | 146 | // Stream from a {ReadableStreamReader} to a {WriteStream} with progress callback. 147 | // Returns a {Promise} that will accept when complete. 148 | protected async streamWithProgress( 149 | finalLength: number, 150 | reader: ReadableStreamReader, 151 | writer: fs.WriteStream, 152 | options : fdTypes.Options, 153 | browserWindow: BrowserWindow | null 154 | ): Promise{ 155 | let bytesDone = 0; 156 | 157 | for (;;){ 158 | const result = await reader.read(new Uint8Array()); 159 | if(result.done){ 160 | if(options.feedbackProgressToRenderer && browserWindow){ 161 | browserWindow.webContents.send("electron-file-download-download-file-progress-feedback", options.uuid, bytesDone, finalLength); 162 | } 163 | 164 | if(this._cancelFlagMap.get(options.uuid)){ 165 | this._cancelFlagMap.delete(options.uuid); 166 | this._readerMap.delete(options.uuid); 167 | throw new fdTypes.CancelError(); 168 | } 169 | 170 | this._cancelFlagMap.delete(options.uuid); 171 | this._readerMap.delete(options.uuid); 172 | return; 173 | } 174 | 175 | const chunk = result.value; 176 | if(chunk == null){ 177 | throw Error("Empty chunk received during download"); 178 | }else{ 179 | writer.write(Buffer.from(chunk)); 180 | if(options.feedbackProgressToRenderer && browserWindow){ 181 | bytesDone += chunk.byteLength; 182 | browserWindow.webContents.send("electron-file-download-download-file-progress-feedback", options.uuid, bytesDone, finalLength); 183 | } 184 | } 185 | } 186 | } 187 | 188 | protected setPreload(session){ 189 | session.setPreloads([ ...session.getPreloads(), this._preloadFilePath ]); 190 | } 191 | 192 | protected _preloadFilePath : string = ""; 193 | protected _readerMap = new Map(); 194 | protected _cancelFlagMap = new Map(); 195 | } 196 | 197 | const fd = new FileDownload(); 198 | 199 | // According to bug https://github.com/electron/electron/issues/25196 200 | // Electron can not pass rejected promise to renderer correctly. 201 | // So we do not throw exception in handle function. 202 | ipcMain.handle("electron-file-download-async-download-file", async(event, options: fdTypes.Options) => { 203 | const win = BrowserWindow.getFocusedWindow(); 204 | try { 205 | const result = await fd.download(options, win); 206 | return result; 207 | } catch (err){ 208 | const result : fdTypes.Result = { 209 | uuid: options.uuid, 210 | success: false, 211 | canceled: false, 212 | error: GetErrorMessage(err), 213 | fileSize: 0, 214 | }; 215 | return result; 216 | } 217 | }); 218 | 219 | ipcMain.on("electron-file-download-cancel-download-file", (event, uuid) => { 220 | if(uuid){ 221 | fd.cancel(uuid); 222 | } 223 | }); 224 | 225 | export default fd; -------------------------------------------------------------------------------- /src/lib/file-download/renderer/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /* 4 | * This code can only be used in the renderer process. 5 | */ 6 | import { Options, ProgressCallback } from "../shared"; 7 | 8 | class FileDownload{ 9 | public async download(options: Options, progressCb : ProgressCallback){ 10 | if(progressCb && options.uuid){ 11 | this._callbackMap.set(options.uuid, progressCb); 12 | } 13 | return (window as any).__ElectronFileDownload__.asyncDownloadFile(options); 14 | } 15 | 16 | public cancel(uuid: string){ 17 | if(uuid){ 18 | (window as any).__ElectronFileDownload__.cancelDownloadFile(uuid); 19 | } 20 | } 21 | 22 | public emitCallback(uuid: string, bytesDone: number, bytesTotal: number){ 23 | if(this._callbackMap.has(uuid)){ 24 | const cb = this._callbackMap.get(uuid) as ProgressCallback; 25 | if(cb){ 26 | cb(uuid, bytesDone, bytesTotal); 27 | } 28 | } 29 | } 30 | 31 | protected _callbackMap = new Map(); 32 | } 33 | 34 | (window as any).__ElectronFileDownload__.onDownloadFilePrgressFeedback((uuid: string, bytesDone: number, bytesTotal: number) => { 35 | console.log("onDownloadFilePrgressFeedback", uuid, bytesDone); 36 | fd.emitCallback(uuid, bytesDone, bytesTotal); 37 | }); 38 | 39 | const fd = new FileDownload(); 40 | 41 | export default fd; -------------------------------------------------------------------------------- /src/lib/file-download/shared/index.ts: -------------------------------------------------------------------------------- 1 | /* 2 | * This code can be used in both renderer and main process. 3 | */ 4 | 5 | import { v4 as uuidv4 } from "uuid"; 6 | 7 | class Options{ 8 | /** 9 | The unique id for each download, can use this uuid to cancel download. 10 | */ 11 | uuid: string = uuidv4(); 12 | 13 | /** 14 | The file url. 15 | */ 16 | url: string = ""; 17 | 18 | /** 19 | The path to save the file in. 20 | The downloader will automatically create directories. 21 | */ 22 | savePath: string = ""; 23 | 24 | /** 25 | Specify file size to avoid missing a "Content-Length" in the response header. 26 | This value only used in progress callback. 27 | */ 28 | fileSize: number = 0; 29 | 30 | /** 31 | Whether send download pogress to renderer process or not. 32 | If set true, will send 'file-download-progress-feedback' to renderer. 33 | */ 34 | feedbackProgressToRenderer: boolean = false; 35 | 36 | /** 37 | The MD5 value of target file. 38 | */ 39 | md5: string = ""; 40 | 41 | /** 42 | Whether skip download when target file exist. 43 | */ 44 | skipWhenFileExist: boolean = false; 45 | 46 | /** 47 | Whether skip download when target file exist and the md5 is same. 48 | */ 49 | skipWhenMd5Same: boolean = false; 50 | 51 | /** 52 | Whether verify target file md5 after download finished. 53 | */ 54 | verifyMd5: boolean = false; 55 | } 56 | 57 | class CancelError extends Error{} 58 | 59 | interface Result{ 60 | uuid: string; 61 | success: boolean; 62 | canceled: boolean; 63 | error: string; 64 | fileSize: number; 65 | } 66 | 67 | interface ProgressCallback { 68 | (uuid: string, bytesDone: number, bytesTotal: number): void; 69 | } 70 | 71 | export { 72 | CancelError, 73 | Options, 74 | type Result, 75 | type ProgressCallback, 76 | }; -------------------------------------------------------------------------------- /src/lib/utils/main/file-util.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 与文件处理相关的通用方法 3 | */ 4 | import crypto from "crypto"; 5 | import fs from "fs"; 6 | 7 | // Synchronous check file or directory exist. 8 | function IsPathExist(path: string) : boolean{ 9 | let exist = false; 10 | try { 11 | if(path){ 12 | fs.accessSync(path, fs.constants.F_OK); 13 | exist = true; 14 | } 15 | } catch (err){ 16 | exist = false; 17 | } 18 | return exist; 19 | } 20 | 21 | // Synchronous get file size 22 | function GetFileSize(filePath: string) : number{ 23 | let fileSize = -1; 24 | try { 25 | if(filePath){ 26 | const stat = fs.statSync(filePath); 27 | fileSize = stat.size; 28 | } 29 | } catch (err){ 30 | fileSize = -1; 31 | } 32 | 33 | return fileSize; 34 | } 35 | 36 | // Syncronous recursive create directories. 37 | function CreateDirectories(path: string) : boolean{ 38 | let result = false; 39 | try { 40 | if(path){ 41 | fs.mkdirSync(path, { recursive: true }); 42 | result = true; 43 | } 44 | } catch (err){ 45 | result = false; 46 | } 47 | return result; 48 | } 49 | 50 | // Asynchronous computation of file md5 51 | function GetFileMd5(filePath: string) : Promise{ 52 | return new Promise((resolve, reject) => { 53 | if(!filePath){ 54 | reject("File path is empty"); 55 | return; 56 | } 57 | 58 | if(IsPathExist(filePath)){ 59 | const stream = fs.createReadStream(filePath); 60 | const hash = crypto.createHash("md5"); 61 | stream.on("data", (chunk) => { 62 | hash.update(chunk); 63 | }); 64 | stream.on("end", () => { 65 | const md5 = hash.digest("hex").toLowerCase(); 66 | resolve(md5); 67 | }); 68 | stream.on("error", function(err){ 69 | reject(err); 70 | }); 71 | }else{ 72 | reject("File not exist"); 73 | } 74 | }); 75 | } 76 | 77 | export { 78 | GetFileMd5, 79 | GetFileSize, 80 | IsPathExist, 81 | CreateDirectories, 82 | }; -------------------------------------------------------------------------------- /src/lib/utils/main/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 当前目录的代码只能被主进程所使用 3 | */ 4 | import { app, session, BrowserWindow, ipcMain, shell, dialog, OpenDialogOptions } from "electron"; 5 | import path from "path"; 6 | import * as FileUtils from "./file-util"; 7 | import appState from "../../../main/app-state"; 8 | 9 | class Utils{ 10 | public initialize(){ 11 | this._preloadFilePath = path.join(__dirname, "utils-preload.js"); 12 | // console.log("Utils preload path: " + this._preloadFilePath); 13 | this.setPreload(session.defaultSession); 14 | 15 | app.on("session-created", (session) => { 16 | this.setPreload(session); 17 | }); 18 | } 19 | 20 | protected setPreload(session){ 21 | session.setPreloads([ ...session.getPreloads(), this._preloadFilePath ]); 22 | } 23 | 24 | protected _preloadFilePath : string = ""; 25 | 26 | // === PUBLIC METHOD FALG LINE (DO NOT MODIFY/REMOVE) === 27 | } 28 | 29 | const utils = new Utils(); 30 | 31 | ipcMain.on("electron-utils-open-dev-tools", () => { 32 | const win = BrowserWindow.getFocusedWindow(); 33 | if(win){ 34 | win.webContents.openDevTools(); 35 | } 36 | }); 37 | 38 | ipcMain.on("electron-utils-open-external-url", (event, url) => { 39 | if(url){ 40 | shell.openExternal(url); 41 | } 42 | }); 43 | 44 | ipcMain.handle("electron-utils-show-open-dialog", async(event, options: OpenDialogOptions) => { 45 | return await dialog.showOpenDialog(options); 46 | }); 47 | 48 | ipcMain.on("electron-utils-check-path-exist", (event, path) => { 49 | let exist = false; 50 | if(path){ 51 | exist = FileUtils.IsPathExist(path); 52 | } 53 | event.returnValue = exist; 54 | }); 55 | 56 | ipcMain.handle("electron-utils-get-file-md5", async(event, filePath) => { 57 | return await FileUtils.GetFileMd5(filePath); 58 | }); 59 | 60 | ipcMain.on("electron-utils-get-app-version", (event) => { 61 | event.returnValue = appState.appVersion; 62 | }); 63 | 64 | // === FALG LINE (DO NOT MODIFY/REMOVE) === 65 | 66 | export default utils; 67 | export { 68 | FileUtils 69 | }; -------------------------------------------------------------------------------- /src/lib/utils/main/utils-preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer, OpenDialogOptions } from "electron"; 2 | 3 | function initialize(){ 4 | if(!ipcRenderer){ 5 | return; 6 | } 7 | 8 | if(contextBridge && process.contextIsolated){ 9 | try { 10 | contextBridge.exposeInMainWorld("__ElectronUtils__", { 11 | openDevTools: () => ipcRenderer.send("electron-utils-open-dev-tools"), 12 | openExternalLink: (url: string) => ipcRenderer.send("electron-utils-open-external-url", url), 13 | showOpenDialog: (options: OpenDialogOptions) => ipcRenderer.invoke("electron-utils-show-open-dialog", options), 14 | checkPathExist: (path: string) => ipcRenderer.sendSync("electron-utils-check-path-exist", path), 15 | getFileMd5: (filePath: string) => ipcRenderer.invoke("electron-utils-get-file-md5", filePath), 16 | getAppVersion: () => ipcRenderer.sendSync("electron-utils-get-app-version"), 17 | // === FALG LINE (DO NOT MODIFY/REMOVE) === 18 | }); 19 | } catch { 20 | // Sometimes this files can be included twice 21 | } 22 | } 23 | } 24 | 25 | initialize(); -------------------------------------------------------------------------------- /src/lib/utils/renderer/index.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable @typescript-eslint/no-explicit-any */ 2 | 3 | /** 4 | * @file 当前目录的代码只能被渲染进程所使用 5 | */ 6 | 7 | import { OpenDialogOptions, OpenDialogReturnValue } from "electron"; 8 | 9 | class Utils{ 10 | public openDevTools(){ 11 | (window as any).__ElectronUtils__.openDevTools(); 12 | } 13 | 14 | public openExternalLink(url : string){ 15 | (window as any).__ElectronUtils__.openExternalLink(url); 16 | } 17 | 18 | public async showOpenDialog(options: OpenDialogOptions) : Promise{ 19 | return await (window as any).__ElectronUtils__.showOpenDialog(options) as OpenDialogReturnValue; 20 | } 21 | 22 | public checkPathExist(path : string) : boolean{ 23 | return (window as any).__ElectronUtils__.checkPathExist(path) as boolean; 24 | } 25 | 26 | public async getFileMd5(filePath : string) : Promise{ 27 | return await (window as any).__ElectronUtils__.getFileMd5(filePath) as string; 28 | } 29 | 30 | public getAppVersion() : string{ 31 | return (window as any).__ElectronUtils__.getAppVersion() as string; 32 | } 33 | 34 | // === FALG LINE (DO NOT MODIFY/REMOVE) === 35 | } 36 | 37 | const utils = new Utils(); 38 | 39 | export default utils; -------------------------------------------------------------------------------- /src/lib/utils/shared/error-utils.ts: -------------------------------------------------------------------------------- 1 | type ErrorWithMessage = { 2 | message: string 3 | } 4 | 5 | function isErrorWithMessage(error: unknown): error is ErrorWithMessage{ 6 | return ( 7 | typeof error === "object" && 8 | error !== null && 9 | "message" in error && 10 | typeof (error as Record).message === "string" 11 | ); 12 | } 13 | 14 | function toErrorWithMessage(maybeError: unknown): ErrorWithMessage{ 15 | if(isErrorWithMessage(maybeError)) return maybeError; 16 | 17 | try { 18 | return new Error(JSON.stringify(maybeError)); 19 | } catch { 20 | // fallback in case there's an error stringifying the maybeError 21 | // like with circular references for example. 22 | return new Error(String(maybeError)); 23 | } 24 | } 25 | 26 | function GetErrorMessage(error: unknown){ 27 | return toErrorWithMessage(error).message; 28 | } 29 | 30 | export { 31 | GetErrorMessage 32 | }; -------------------------------------------------------------------------------- /src/lib/utils/shared/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 当前目录的代码可以被主进程和渲染进程所使用 3 | */ 4 | 5 | import { GetErrorMessage } from "./error-utils"; 6 | import { Singleton } from "./singleton"; 7 | 8 | export { 9 | GetErrorMessage, 10 | Singleton 11 | }; -------------------------------------------------------------------------------- /src/lib/utils/shared/singleton.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | export class Singleton{ 3 | static instance(this: new() => T): T{ 4 | if(!( this)._instance) 5 | ( this)._instance = new this() 6 | 7 | return ( this)._instance 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/app-state.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 存储应用的状态数据 3 | */ 4 | 5 | import path from "path"; 6 | import { Tray, app, dialog } from "electron"; 7 | import PrimaryWindow from "./windows/primary"; 8 | import FramelessWindow from "./windows/frameless"; 9 | import log from "electron-log/main"; 10 | import { Singleton } from "../lib/utils/shared"; 11 | import fd from "../lib/file-download/main"; 12 | import utils from "../lib/utils/main"; 13 | 14 | // 应用环境(开发环境、测试环境、生产环境) 15 | enum AppEnv { 16 | Development, 17 | Test, 18 | Production 19 | } 20 | 21 | /** 22 | * 单实例类 23 | * 全局存储应用程序的状态数据,包含窗口对象、托盘对象等 24 | * @class 25 | */ 26 | class AppState extends Singleton{ 27 | constructor(){ 28 | super(); 29 | let envStr = ""; 30 | if(process.argv.length > 2){ 31 | envStr = process.argv[2].toLowerCase(); 32 | } 33 | 34 | if(envStr == "development"){ 35 | this.appEnv = AppEnv.Development; 36 | }else if(envStr == "test"){ 37 | this.appEnv = AppEnv.Test; 38 | }else if(envStr == "production"){ 39 | this.appEnv = AppEnv.Production; 40 | } 41 | } 42 | 43 | // 初始化应用程序,应用程序启动时会调用该方法 44 | public initialize(): boolean{ 45 | if(app.isPackaged){ 46 | const packageJSON = require(path.join(app.getAppPath(), "package.json")); 47 | this._appVersion = packageJSON.version; 48 | 49 | this._mainStaticPath = path.join(app.getAppPath(), "build/main/static"); 50 | }else{ 51 | const packageJSON = require(path.join(app.getAppPath(), "../../package.json")); 52 | this._appVersion = packageJSON.version; 53 | 54 | // 在非打包环境, appPath为 ./build/main 55 | this._mainStaticPath = path.join(app.getAppPath(), "static"); 56 | } 57 | 58 | if(!this.initLogger()){ 59 | return false; 60 | } 61 | 62 | log.info(`Env: ${AppEnv[this.appEnv]}, Version: ${this._appVersion}`); 63 | 64 | // 初始化文件下载组件 65 | fd.initialize(); 66 | 67 | // 初始化Utils组件 68 | utils.initialize(); 69 | 70 | this._isInit = true; 71 | return true; 72 | } 73 | 74 | // 反初始化应用程序,应用程序程序退出前会调用该方法 75 | public uninitialize(){ 76 | log.eventLogger.stopLogging(); 77 | this._isInit = false; 78 | } 79 | 80 | public get isInit(){ 81 | return this._isInit; 82 | } 83 | 84 | public get appVersion(){ 85 | return this._appVersion; 86 | } 87 | 88 | public get mainStaticPath(){ 89 | return this._mainStaticPath; 90 | } 91 | 92 | // 应用程序的环境(默认为生产环境) 93 | public readonly appEnv: AppEnv = AppEnv.Production; 94 | 95 | // 主窗口对象 96 | public primaryWindow: null | PrimaryWindow = null; 97 | 98 | // 无边框示例窗口的对象 99 | public framelessWindow : null | FramelessWindow = null; 100 | 101 | // 系统托盘 102 | public tray: null | Tray = null; 103 | 104 | // 是否即将退出应用程序 105 | // 该变量用来拦截非用户主动触发的关闭消息,防止主窗口收到close事件时退出应用 106 | public allowExitApp: boolean = false; 107 | 108 | // 当前应用程序仅允许运行一个实例 109 | public onlyAllowSingleInstance : boolean = true; 110 | 111 | // ======== Protected 成员 ======== 112 | // 113 | // 应用程序是否已初始化 114 | protected _isInit: boolean = false; 115 | 116 | // 当前应用的版本号 117 | protected _appVersion: string = ""; 118 | 119 | // 主进程静态资源目录的路径 120 | protected _mainStaticPath: string = ""; 121 | 122 | // 初始化文件日志系统 123 | protected initLogger(): boolean{ 124 | log.initialize(); 125 | 126 | // save electron events to file. 127 | log.eventLogger.startLogging({ 128 | events: { 129 | app: { 130 | "certificate-error": true, 131 | "child-process-gone": true, 132 | "render-process-gone": true, 133 | }, 134 | webContents: { 135 | "did-fail-load": true, 136 | "did-fail-provisional-load": true, 137 | "plugin-crashed": true, 138 | "preload-error": true, 139 | "unresponsive": true, 140 | }, 141 | }, 142 | scope: "ElectronEvent", 143 | }); 144 | 145 | // collect all unhandled errors/rejections 146 | log.errorHandler.startCatching({ 147 | showDialog: false, 148 | onError({ createIssue, error, processType, versions }){ 149 | if(processType === "renderer") 150 | return; 151 | 152 | dialog.showMessageBox({ 153 | title: "An error occurred", 154 | message: error.message, 155 | detail: error.stack, 156 | type: "error", 157 | buttons: [ "Ignore", "Report", "Exit" ], 158 | }) 159 | .then((result) => { 160 | if(result.response === 1){ 161 | createIssue("https://github.com/winsoft666/electron-vue3-template/issues/new", { 162 | title: `Error report for ${versions.app}`, 163 | body: `Error:\n\`\`\`${error.stack}\n\`\`\`\n` + `OS: ${versions.os}`, 164 | }); 165 | return; 166 | } 167 | 168 | if(result.response === 2) 169 | app.quit(); 170 | }); 171 | }, 172 | }); 173 | return true; 174 | } 175 | } 176 | 177 | function getAppState(): AppState{ 178 | return AppState.instance(); 179 | } 180 | 181 | const appState = getAppState(); 182 | 183 | export default appState; 184 | export { AppEnv }; -------------------------------------------------------------------------------- /src/main/main.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, app, dialog, session, Menu } from "electron"; 2 | import log from "electron-log/main"; 3 | import PrimaryWindow from "./windows/primary"; 4 | import { CreateAppTray } from "./tray"; 5 | import appState from "./app-state"; 6 | 7 | // 禁用沙盒 8 | // 在某些系统环境上,不禁用沙盒会导致界面花屏 9 | // app.commandLine.appendSwitch("no-sandbox"); 10 | 11 | // 移除默认菜单栏 12 | Menu.setApplicationMenu(null); 13 | 14 | const gotLock = app.requestSingleInstanceLock(); 15 | 16 | // 如果程序只允许启动一个实例时,第二个实例启动后会直接退出 17 | if(!gotLock && appState.onlyAllowSingleInstance){ 18 | app.quit(); 19 | }else{ 20 | app.whenReady().then(() => { 21 | if(!appState.initialize()){ 22 | dialog.showErrorBox("App initialization failed", "The program will exit after click the OK button.",); 23 | app.exit(); 24 | return; 25 | } 26 | 27 | log.info("App initialize ok"); 28 | 29 | appState.primaryWindow = new PrimaryWindow(); 30 | appState.tray = CreateAppTray(); 31 | 32 | session.defaultSession.webRequest.onHeadersReceived((details, callback) => { 33 | callback({ 34 | responseHeaders: { 35 | ...details.responseHeaders, 36 | "Content-Security-Policy": [ "script-src 'self'" ], 37 | }, 38 | }); 39 | }); 40 | }); 41 | 42 | // 当程序的第二个实例启动时,显示第一个实例的主窗口 43 | app.on("second-instance", () => { 44 | appState.primaryWindow?.browserWindow?.show(); 45 | }); 46 | 47 | app.on("activate", () => { 48 | // On macOS it's common to re-create a window in the app when the 49 | // dock icon is clicked and there are no other windows open. 50 | if(BrowserWindow.getAllWindows().length === 0) 51 | appState.primaryWindow = new PrimaryWindow(); 52 | }); 53 | 54 | app.on("window-all-closed", () => { 55 | if(process.platform !== "darwin") 56 | app.quit(); 57 | }); 58 | 59 | app.on("will-quit", () => { 60 | appState.uninitialize(); 61 | }); 62 | } -------------------------------------------------------------------------------- /src/main/static/.gitkeep: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/src/main/static/.gitkeep -------------------------------------------------------------------------------- /src/main/static/tray.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/src/main/static/tray.ico -------------------------------------------------------------------------------- /src/main/static/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/winsoft666/electron-vue3-boilerplate/97a3b0bc49f0bf84ff33eb297173b9844456952f/src/main/static/tray.png -------------------------------------------------------------------------------- /src/main/tray.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @file 与系统托盘的相关的功能 3 | */ 4 | 5 | import path from "path"; 6 | import { Menu, MenuItem, Tray } from "electron"; 7 | import appState, { AppEnv } from "./app-state"; 8 | 9 | // 创建系统托盘 10 | function CreateAppTray() : Tray{ 11 | const iconPath = process.platform === "win32" ? 12 | path.join(appState.mainStaticPath, "tray.ico") : 13 | path.join(appState.mainStaticPath, "tray.png"); 14 | 15 | const tray = new Tray(iconPath); 16 | 17 | tray.on("click", () => { 18 | appState.primaryWindow?.browserWindow?.show(); 19 | }); 20 | 21 | // 创建托盘右键菜单 22 | const contextMenu = Menu.buildFromTemplate([ 23 | { 24 | label: "打开", 25 | type: "normal", 26 | accelerator: "Alt+O", 27 | registerAccelerator: true, 28 | click: () => { 29 | const win = appState.primaryWindow?.browserWindow; 30 | if(win) { 31 | if(win.isVisible()) { 32 | if(win.isMinimized()) { 33 | win.restore(); 34 | } 35 | } 36 | else { 37 | win.show(); 38 | } 39 | } 40 | }, 41 | }, 42 | { 43 | label: "退出", 44 | type: "normal", 45 | click: () => { 46 | const win = appState.primaryWindow?.browserWindow; 47 | if(win) { 48 | if(win.isVisible()) { 49 | if(win.isMinimized()) { 50 | win.restore(); 51 | } 52 | } 53 | else { 54 | win.show(); 55 | } 56 | win.webContents.send("show-exit-app-msgbox"); 57 | } 58 | }, 59 | }, 60 | ]); 61 | 62 | // 在非生产环境添加一个打开调试工具菜单,方便调试 63 | if(appState.appEnv != AppEnv.Production){ 64 | contextMenu.insert( 65 | 0, 66 | new MenuItem({ 67 | label: "打开DevTools", 68 | type: "normal", 69 | accelerator: "Alt+D", 70 | registerAccelerator: true, 71 | click: () => { 72 | appState.primaryWindow?.browserWindow?.webContents.openDevTools(); 73 | }, 74 | }), 75 | ); 76 | } 77 | 78 | tray.setContextMenu(contextMenu); 79 | tray.setToolTip("A Electron + Vue3 boilerplate"); 80 | tray.setTitle("electron-vue3-boilerplate"); 81 | 82 | return tray; 83 | } 84 | 85 | export { CreateAppTray }; 86 | -------------------------------------------------------------------------------- /src/main/windows/frameless/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { BrowserWindow, ipcMain } from "electron"; 3 | import WindowBase from "../window-base"; 4 | import appState from "../../app-state"; 5 | 6 | class FramelessWindow extends WindowBase{ 7 | constructor(){ 8 | // 调用WindowBase构造函数创建窗口 9 | super({ 10 | width: 600, 11 | height: 360, 12 | frame: false, 13 | webPreferences: { 14 | preload: path.join(__dirname, "preload.js"), 15 | }, 16 | // 设置父窗口 17 | parent: appState.primaryWindow?.browserWindow as BrowserWindow, 18 | }); 19 | 20 | this.openRouter("/frameless-sample"); 21 | } 22 | 23 | protected registerIpcMainHandler(): void{ 24 | ipcMain.on("minimize-window", (event) => { 25 | this._browserWindow?.minimize(); 26 | }); 27 | 28 | ipcMain.on("restore-window", (event) => { 29 | if(this.browserWindow){ 30 | if(this.browserWindow.isMaximized()) 31 | this.browserWindow.restore(); 32 | else 33 | this.browserWindow.maximize(); 34 | } 35 | }); 36 | 37 | ipcMain.on("close-window", (event) => { 38 | this.browserWindow?.close(); 39 | }); 40 | } 41 | } 42 | 43 | export default FramelessWindow; -------------------------------------------------------------------------------- /src/main/windows/frameless/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | 3 | /* 4 | 暴露frameless窗口主进程的方法到frameless窗口的渲染进程 5 | */ 6 | contextBridge.exposeInMainWorld("framelessWindowAPI", { 7 | minimizeWindow: () => ipcRenderer.send("minimize-window"), 8 | restoreWindow: () => ipcRenderer.send("restore-window"), 9 | closeWindow: () => ipcRenderer.send("close-window"), 10 | }); 11 | -------------------------------------------------------------------------------- /src/main/windows/primary/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import { app, dialog, ipcMain } from "electron"; 3 | import appState from "../../app-state"; 4 | import WindowBase from "../window-base"; 5 | import FramelessWindow from "../frameless"; 6 | import axiosInst from "../../../lib/axios-inst/main"; 7 | 8 | class PrimaryWindow extends WindowBase{ 9 | constructor(){ 10 | // 调用WindowBase构造函数创建窗口 11 | super({ 12 | width: 800, 13 | height: 600, 14 | webPreferences: { 15 | preload: path.join(__dirname, "preload.js"), 16 | }, 17 | }); 18 | 19 | // 拦截close事件 20 | this._browserWindow?.on("close", (e) => { 21 | if(!appState.allowExitApp){ 22 | const win = this._browserWindow; 23 | if(win) { 24 | if(win.isVisible()) { 25 | if(win.isMinimized()) { 26 | win.restore(); 27 | } 28 | } 29 | else { 30 | win.show(); 31 | } 32 | win.webContents.send("show-close-primary-win-msgbox"); 33 | } 34 | e.preventDefault(); 35 | } 36 | }); 37 | 38 | this.openRouter("/primary"); 39 | } 40 | 41 | protected registerIpcMainHandler(): void{ 42 | ipcMain.on("message", (event, message) => { 43 | if(!this.isIpcMainEventBelongMe(event)) 44 | return; 45 | 46 | console.log(message); 47 | }); 48 | 49 | ipcMain.on("show-frameless-sample-window", (event) => { 50 | if(!appState.framelessWindow?.valid){ 51 | appState.framelessWindow = new FramelessWindow(); 52 | } 53 | 54 | const win = appState.framelessWindow?.browserWindow; 55 | if(win){ 56 | // 居中到父窗体中 57 | const parent = win.getParentWindow(); 58 | if(parent){ 59 | const parentBounds = parent.getBounds(); 60 | const x = Math.round(parentBounds.x + (parentBounds.width - win.getSize()[0]) / 2); 61 | const y = Math.round(parentBounds.y + (parentBounds.height - win.getSize()[1]) / 2); 62 | 63 | win.setPosition(x, y, false); 64 | } 65 | win.show(); 66 | } 67 | }); 68 | 69 | function delay(time){ 70 | return new Promise(resolve => setTimeout(resolve, time)); 71 | } 72 | 73 | ipcMain.on("min-to-tray", (event) => { 74 | if(!this.isIpcMainEventBelongMe(event)) 75 | return; 76 | 77 | if(process.platform == 'win32') { 78 | this.browserWindow?.hide(); 79 | } 80 | else { // macos or other 81 | this.browserWindow?.minimize(); 82 | } 83 | 84 | if(appState.tray){ 85 | appState.tray.displayBalloon({ 86 | title: "electron-vue-boilerplate", 87 | content: "客户端已经最小化到系统托盘!" 88 | }); 89 | } 90 | }); 91 | 92 | ipcMain.handle("async-exit-app", async(event) => { 93 | // 暂停1500毫秒,模拟退出程序时的清理操作 94 | await delay(1500); 95 | appState.allowExitApp = true; 96 | app.quit(); 97 | }); 98 | 99 | ipcMain.on("http-get-request", (event, url) => { 100 | axiosInst.get(url) 101 | .then((rsp) => { 102 | dialog.showMessageBox(this._browserWindow!, { 103 | message: `在主进程中请求 ${url} 成功!状态码:${rsp.status}`, 104 | type: "info" 105 | }); 106 | }) 107 | .catch((err) => { 108 | dialog.showMessageBox(this._browserWindow!, { 109 | message: `在主进程中请求 ${url} 失败!错误消息:${err.message}`, 110 | type: "error" 111 | }); 112 | }); 113 | }); 114 | } 115 | } 116 | 117 | export default PrimaryWindow; 118 | -------------------------------------------------------------------------------- /src/main/windows/primary/preload.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from "electron"; 2 | 3 | /* 4 | 暴露primary窗口主进程的方法到primary窗口的渲染进程 5 | */ 6 | contextBridge.exposeInMainWorld("primaryWindowAPI", { 7 | sendMessage: (message: string) => ipcRenderer.send("message", message), 8 | showFramelessSampleWindow: () => ipcRenderer.send("show-frameless-sample-window"), 9 | openExternalLink: (url: string) => ipcRenderer.send("open-external-link", url), 10 | clearAppConfiguration: () => ipcRenderer.send("clear-app-configuration"), 11 | onShowExitAppMsgbox: (callback) => ipcRenderer.on("show-exit-app-msgbox", () => { 12 | callback(); 13 | }), 14 | onShowClosePrimaryWinMsgbox: (callback) => ipcRenderer.on("show-close-primary-win-msgbox", () => { 15 | callback(); 16 | }), 17 | asyncExitApp: () => ipcRenderer.invoke("async-exit-app"), 18 | minToTray: () => ipcRenderer.send("min-to-tray"), 19 | httpGetRequest: (url:string) => ipcRenderer.send("http-get-request", url), 20 | }); 21 | -------------------------------------------------------------------------------- /src/main/windows/window-base.ts: -------------------------------------------------------------------------------- 1 | import { app, BrowserWindow, IpcMainEvent, IpcMainInvokeEvent, BrowserWindowConstructorOptions } from "electron"; 2 | 3 | /** 4 | * 窗口基类,所有的窗口都继承自该类,如 PrimaryWindow、FramelessWindow 5 | * @class 6 | */ 7 | abstract class WindowBase{ 8 | constructor(options?: BrowserWindowConstructorOptions){ 9 | this._browserWindow = new BrowserWindow(options); 10 | 11 | if(this._browserWindow){ 12 | // After received closed event, remove the reference to the window and avoid using it any more. 13 | this._browserWindow.on("closed", () => { 14 | this._browserWindow = null; 15 | }); 16 | } 17 | 18 | this.registerIpcMainHandler(); 19 | } 20 | 21 | public openRouter(routerPath : string){ 22 | let url = ""; 23 | if(app.isPackaged){ 24 | url = `file://${app.getAppPath()}/build/renderer/index.html#${routerPath}`; 25 | }else{ 26 | const rendererPort = process.argv[2]; 27 | url = `http://localhost:${rendererPort}/#${routerPath}`; 28 | } 29 | 30 | console.log(`Load URL: ${url}`); 31 | 32 | if(this._browserWindow){ 33 | this._browserWindow.loadURL(url); 34 | } 35 | } 36 | 37 | public get valid(){ 38 | return this.browserWindow != null; 39 | } 40 | 41 | public get browserWindow(){ 42 | return this._browserWindow; 43 | } 44 | 45 | protected abstract registerIpcMainHandler() : void; 46 | 47 | protected _browserWindow : BrowserWindow | null = null; 48 | 49 | public isIpcMainEventBelongMe(event : IpcMainEvent | IpcMainInvokeEvent) : boolean{ 50 | if(!this._browserWindow) 51 | return false; 52 | return (event.sender.id == this.browserWindow?.webContents.id); 53 | } 54 | } 55 | 56 | export default WindowBase; -------------------------------------------------------------------------------- /src/renderer/.env.development: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=http://127.0.0.1/api/dev/base/url/sample/ 2 | -------------------------------------------------------------------------------- /src/renderer/.env.production: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=http://127.0.0.1/api/production/base/url/sample/ -------------------------------------------------------------------------------- /src/renderer/.env.test: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=http://127.0.0.1/api/test/base/url/sample/ -------------------------------------------------------------------------------- /src/renderer/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 9 | 10 | 11 | -------------------------------------------------------------------------------- /src/renderer/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | // @ts-nocheck 3 | // Generated by unplugin-vue-components 4 | // Read more: https://github.com/vuejs/core/pull/3399 5 | export {} 6 | 7 | /* prettier-ignore */ 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | AButton: typeof import('ant-design-vue/es')['Button'] 11 | ACollapse: typeof import('ant-design-vue/es')['Collapse'] 12 | ACollapsePanel: typeof import('ant-design-vue/es')['CollapsePanel'] 13 | AForm: typeof import('ant-design-vue/es')['Form'] 14 | AFormItem: typeof import('ant-design-vue/es')['FormItem'] 15 | AInput: typeof import('ant-design-vue/es')['Input'] 16 | AModal: typeof import('ant-design-vue/es')['Modal'] 17 | AProgress: typeof import('ant-design-vue/es')['Progress'] 18 | ASpace: typeof import('ant-design-vue/es')['Space'] 19 | RouterLink: typeof import('vue-router')['RouterLink'] 20 | RouterView: typeof import('vue-router')['RouterView'] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/components/hello-world.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | Electron + Vue3 + Vite boilerplate 6 | 7 | 8 |
9 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | import "./style.css"; 3 | 4 | // 导入 FontAwesome 图标 5 | import { library as fontAwesomeLibrary } from "@fortawesome/fontawesome-svg-core"; 6 | import { FontAwesomeIcon } from "@fortawesome/vue-fontawesome"; 7 | import { fas } from "@fortawesome/free-solid-svg-icons"; // solid样式图标 8 | fontAwesomeLibrary.add(fas); 9 | 10 | import App from "./App.vue"; 11 | import router from "./router"; 12 | 13 | const app = createApp(App); 14 | 15 | app.use(router); 16 | app.component("FontAwesomeIcon", FontAwesomeIcon); 17 | app.mount("#app"); 18 | -------------------------------------------------------------------------------- /src/renderer/public/electron.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/renderer/public/vite.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/renderer/public/vue.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from "vue-router"; 2 | import routeMap from "./router-map"; 3 | 4 | const router = createRouter({ 5 | // 指定使用Hash路由 6 | history: createWebHashHistory(), 7 | // 路由规则数组,每一个路由规则都是一个对象 8 | routes: routeMap 9 | }); 10 | 11 | // 在路由跳转之前执行,可以用于进行全局的访问控制或重定向跳转等操作 12 | router.beforeEach((to, from, next) => { 13 | // ... 14 | // 继续执行下一个路由守卫 15 | next(); 16 | }); 17 | 18 | // 在路由跳转完成后执行,可以用于对页面进行一些操作,如监测页面埋点等 19 | router.afterEach((to, from) => { 20 | }); 21 | 22 | export default router; 23 | -------------------------------------------------------------------------------- /src/renderer/router/router-map.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | 3 | // 定义路由规则 4 | const routeMap: Array = [ 5 | { 6 | path: "/primary", 7 | name: "primary", 8 | component: () => import("@views/primary.vue"), 9 | }, 10 | { 11 | path: "/frameless-sample", 12 | name: "frameless-sample", 13 | component: () => import("@views/frameless-sample.vue"), 14 | } 15 | ]; 16 | 17 | export default routeMap; -------------------------------------------------------------------------------- /src/renderer/style.css: -------------------------------------------------------------------------------- 1 | html, 2 | body, 3 | #app { 4 | width: 100%; 5 | height: 100%; 6 | padding: 0; 7 | margin: 0; 8 | color: #141547; 9 | font-size: 14px; 10 | } 11 | 12 | * { 13 | padding: 0; 14 | margin: 0; 15 | } -------------------------------------------------------------------------------- /src/renderer/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", 4 | "jsx": "preserve", 5 | "lib": ["esnext", "dom"], 6 | "useDefineForClassFields": true, 7 | "module": "esnext", 8 | "moduleResolution": "node", 9 | "resolveJsonModule": true, 10 | "types": ["vite/client"], 11 | "strict": true, 12 | "sourceMap": true, 13 | "esModuleInterop": true 14 | }, 15 | "include": ["./**/*.ts", "./**/*.d.ts", "./**/*.tsx", "./**/*.vue"] 16 | } -------------------------------------------------------------------------------- /src/renderer/typings/electron.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * Should match main/preload.ts for typescript support in renderer 3 | */ 4 | export default interface ElectronApi { 5 | } 6 | 7 | declare global { 8 | interface Window { 9 | electronAPI: ElectronApi 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/renderer/typings/shims-vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module "*.vue" { 2 | import type { DefineComponent } from "vue" 3 | 4 | const component: DefineComponent<{}, {}, any> 5 | export default component 6 | } 7 | -------------------------------------------------------------------------------- /src/renderer/views/frameless-sample.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 47 | 48 | 90 | -------------------------------------------------------------------------------- /src/renderer/views/primary.vue: -------------------------------------------------------------------------------- 1 | 117 | 118 | 291 | 292 | 327 | -------------------------------------------------------------------------------- /src/renderer/vite.config.mjs: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | import vuePlugin from "@vitejs/plugin-vue"; 3 | import { defineConfig } from "vite"; 4 | import Components from "unplugin-vue-components/vite"; 5 | import { AntDesignVueResolver } from "unplugin-vue-components/resolvers"; 6 | 7 | /** 8 | * https://vitejs.dev/config 9 | */ 10 | export default defineConfig({ 11 | root: path.join(__dirname), 12 | publicDir: "public", 13 | server: { 14 | port: 8080, 15 | }, 16 | open: false, 17 | base: "/", 18 | build: { 19 | // outDir 的位置与 src/tsconfig.json 中的 outDir 息息相关 20 | outDir: path.join(__dirname, "../../build/renderer"), 21 | emptyOutDir: true, 22 | }, 23 | resolve: { 24 | alias: { 25 | "@views": path.join(__dirname, "views"), 26 | "@lib": path.join(__dirname, "../lib"), 27 | "@file-download": path.join(__dirname, "../lib/file-download"), 28 | "@utils": path.join(__dirname, "../lib/utils"), 29 | }, 30 | }, 31 | plugins: [ 32 | vuePlugin(), 33 | Components({ 34 | resolvers: [ 35 | AntDesignVueResolver({ 36 | importStyle: false, // css in js 37 | }), 38 | ], 39 | }), 40 | ], 41 | }); 42 | -------------------------------------------------------------------------------- /src/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ES2015", 4 | "module": "commonjs", 5 | "allowJs": true, 6 | "strict": true, 7 | "noImplicitAny": false, 8 | "outDir": "../build", 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "skipLibCheck": true, 12 | "moduleResolution": "node", 13 | "baseUrl": "./", 14 | "paths": { 15 | "@utils/*": ["src/lib/utils/*"], 16 | "@lib/*": ["src/lib/*"], 17 | "@file-download/*": ["src/file-download/*"] 18 | } 19 | }, 20 | "exclude": ["renderer"] 21 | } --------------------------------------------------------------------------------