├── .cz-config.js ├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .eslintrc ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .vscode ├── extensions.json ├── settings.json └── tsx.code-snippets ├── .yarnrc ├── LICENSE ├── README.md ├── build ├── config │ ├── define.ts │ ├── index.ts │ ├── path.ts │ └── resolve.ts ├── index.ts └── plugins │ ├── auto-import.ts │ ├── index.ts │ ├── macros.ts │ ├── visualizer.ts │ └── windicss.ts ├── commitlint.config.js ├── icons ├── 256x256.png ├── icon.icns └── icon.ico ├── package.json ├── public ├── hide.png ├── icon.ico ├── icon.png └── tray.png ├── scripts ├── builder.config.ts ├── default-build.ts ├── dev.ts ├── options-build.ts ├── patternLog.ts ├── rollup.config.ts ├── vite.config.ts └── watch.ts ├── src ├── common │ ├── Time.ts │ └── typeof.ts ├── enums │ ├── system.ts │ └── window.ts ├── main │ ├── config │ │ ├── index.ts │ │ └── modules │ │ │ ├── app.ts │ │ │ ├── log.ts │ │ │ ├── menu.ts │ │ │ ├── paths.ts │ │ │ └── tray.ts │ ├── index.ts │ ├── ipc │ │ ├── bucket │ │ │ ├── download.ts │ │ │ ├── files.ts │ │ │ ├── print.ts │ │ │ └── sql.ts │ │ ├── handle │ │ │ └── index.ts │ │ ├── index.ts │ │ └── on │ │ │ └── index.ts │ ├── tools │ │ ├── electron │ │ │ └── download.ts │ │ ├── index.ts │ │ └── utils │ │ │ ├── ffi.ts │ │ │ ├── fileUtils.ts │ │ │ └── mainEnv.ts │ ├── typeorm │ │ ├── data-source.ts │ │ ├── entity │ │ │ ├── User.ts │ │ │ └── index.ts │ │ ├── index.ts │ │ └── manager │ │ │ ├── index.ts │ │ │ └── user.ts │ └── window │ │ ├── common.ts │ │ ├── create │ │ ├── CreateWebView.ts │ │ ├── CreateWindow.ts │ │ └── win.map.ts │ │ └── index.ts ├── preload │ └── main │ │ └── index.ts ├── renderer │ ├── App.tsx │ ├── api │ │ ├── demo │ │ │ └── test.ts │ │ └── index.ts │ ├── assets │ │ ├── favicon.ico │ │ ├── logo.png │ │ └── svg │ │ │ └── logo.svg │ ├── components │ │ ├── CardGroup │ │ │ ├── index.tsx │ │ │ └── index.vue │ │ └── IpcOnMounted │ │ │ └── index.tsx │ ├── directive │ │ ├── index.ts │ │ └── modules │ │ │ ├── demo.ts │ │ │ ├── ellipsis.ts │ │ │ └── intersecting.ts │ ├── hooks │ │ ├── index.ts │ │ └── modules │ │ │ ├── useCustomEvent.ts │ │ │ ├── useDownload.ts │ │ │ └── useMainResize.ts │ ├── i18n │ │ ├── index.ts │ │ └── modules │ │ │ ├── demo.ts │ │ │ └── demo1.ts │ ├── index.html │ ├── layout │ │ ├── Content │ │ │ ├── Content.vue │ │ │ └── index.ts │ │ ├── Header │ │ │ ├── Control │ │ │ │ └── index.vue │ │ │ ├── Header.vue │ │ │ └── index.ts │ │ ├── Sidebar │ │ │ ├── Menu │ │ │ │ └── Menu.vue │ │ │ ├── Sidebar.vue │ │ │ └── index.ts │ │ └── index.tsx │ ├── pages │ │ ├── Electron │ │ │ └── index.tsx │ │ ├── JavaScript │ │ │ └── index.tsx │ │ ├── NotFound.vue │ │ ├── Test │ │ │ ├── i18n.vue │ │ │ ├── index.tsx │ │ │ ├── testTsx.tsx │ │ │ └── windicss.vue │ │ ├── Vite │ │ │ └── index.tsx │ │ └── Vue │ │ │ └── index.tsx │ ├── renderer.ts │ ├── router │ │ ├── index.ts │ │ └── modules │ │ │ ├── home.ts │ │ │ └── wins.ts │ ├── store │ │ ├── index.ts │ │ ├── modules │ │ │ ├── demo.ts │ │ │ └── index.ts │ │ └── theme │ │ │ ├── helpers.ts │ │ │ ├── index.ts │ │ │ ├── settings │ │ │ ├── color.json │ │ │ ├── color.ts │ │ │ ├── index.ts │ │ │ ├── options.ts │ │ │ ├── theme.json │ │ │ └── theme.ts │ │ │ └── subTheme.ts │ ├── styles │ │ ├── common │ │ │ ├── header.scss │ │ │ ├── transition.css │ │ │ └── vars.css │ │ └── index.css │ ├── utils │ │ ├── construction │ │ │ ├── CreateEvent.ts │ │ │ ├── EventEmitter.ts │ │ │ ├── ImageTools.ts │ │ │ └── Time.ts │ │ ├── index.ts │ │ ├── methods │ │ │ ├── auth.ts │ │ │ ├── renderEnv.ts │ │ │ └── textToImg.ts │ │ └── request │ │ │ ├── createRequest.ts │ │ │ ├── handleAxiosError.ts │ │ │ ├── index.ts │ │ │ ├── statusCodeMap.ts │ │ │ └── toFormData.ts │ └── win │ │ ├── DownloadDemo │ │ └── index.tsx │ │ ├── DropDemo │ │ └── index.vue │ │ ├── FeelBrid │ │ └── index.vue │ │ ├── FontDemo │ │ └── index.vue │ │ ├── Loading │ │ └── index.vue │ │ ├── PrintDemo │ │ └── index.vue │ │ └── SqlDemo │ │ └── index.vue └── types │ ├── component.d.ts │ ├── download.d.ts │ ├── env.d.ts │ ├── global.d.ts │ ├── sql.d.ts │ ├── test.d.ts │ └── wicket.d.ts ├── tsconfig.json └── yarn.lock /.cz-config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | types: [ 3 | { value: 'feat', name: 'feat: 新增功能' }, 4 | { value: 'fix', name: 'fix: 修复bug' }, 5 | { value: 'docs', name: 'docs: 文档变更' }, 6 | { value: 'style', name: 'style: 代码格式(不影响功能,例如空格、分号等格式修正)' }, 7 | { value: 'refactor', name: 'refactor: 代码重构(不包括 bug 修复、功能新增)' }, 8 | { value: 'perf', name: 'perf: 性能优化' }, 9 | { value: 'test', name: 'test: 添加、修改测试用例' }, 10 | { value: 'build', name: 'build: 构建流程、外部依赖变更(如升级 npm 包、修改 脚手架 配置等)' }, 11 | { value: 'ci', name: 'ci: 修改 CI 配置、脚本' }, 12 | { value: 'chore', name: 'chore: 对构建过程或辅助工具和库的更改(不影响源文件、测试用例)' }, 13 | { value: 'revert', name: 'revert: 回滚 commit' } 14 | ], 15 | scopes: [ 16 | ['projects', '项目搭建'], 17 | ['components', '组件相关'], 18 | ['hooks', 'hook 相关'], 19 | ['utils', 'utils 相关'], 20 | ['types', 'ts类型相关'], 21 | ['styles', '样式相关'], 22 | ['deps', '项目依赖'], 23 | ['auth', '对 auth 修改'], 24 | ['other', '其他修改'], 25 | ['custom', '以上都不是?我要自定义'] 26 | ].map(([value, description]) => { 27 | return { 28 | value, 29 | name: `${value.padEnd(30)} (${description})` 30 | } 31 | }), 32 | messages: { 33 | type: '确保本次提交遵循 Angular 规范!\n选择你要提交的类型:', 34 | scope: '\n选择一个 scope(可选):', 35 | customScope: '请输入自定义的 scope:', 36 | subject: '填写简短精炼的变更描述:\n', 37 | body: '填写更加详细的变更描述(可选)。使用 "|" 换行:\n', 38 | breaking: '列举非兼容性重大的变更(可选):\n', 39 | footer: '列举出所有变更的 ISSUES CLOSED(可选)。 例如: #31, #34:\n', 40 | confirmCommit: '确认提交?' 41 | }, 42 | allowBreakingChanges: ['feat', 'fix'], 43 | subjectLimit: 100, 44 | breaklineChar: '|' 45 | } 46 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | end_of_line = lf 7 | insert_final_newline = true 8 | 9 | charset = utf-8 10 | indent_style = tab 11 | indent_size = 2 12 | trim_trailing_whitespace = true 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_PROT = 8090 2 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 该环境变量在渲染进程可通过 2 | NODE_ENV = 'development' 3 | 4 | # 以 VITE_ 开头的环境变量会通过 import.meta.env 暴露在你的客户端源码中 5 | VITE_BASE_URL = 'http://127.0.0.1:30000' 6 | VITE_UPLOAD_URL = 'http://127.0.0.1:30000' -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | NODE_ENV = 'production' 2 | 3 | # 渲染进程环境变量 4 | VITE_BASE_URL = 'http://127.0.0.1:30000' 5 | VITE_UPLOAD_URL = 'http://127.0.0.1:30000' 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@quiteer" 3 | } 4 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | logs 2 | yarn-debug.log* 3 | yarn-error.log* 4 | 5 | node_modules 6 | coverage 7 | dist 8 | out 9 | *.local 10 | stats.html 11 | components.d.ts 12 | auto-imports.d.ts 13 | database.sqlite 14 | .DS_Store 15 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npx --no-install commitlint --edit 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | npm run lint && npm run typecheck 5 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | // 汉化插件 4 | "ms-ceintl.vscode-language-pack-zh-hans", 5 | // 标签输入相关 6 | "formulahendry.auto-close-tag", 7 | "formulahendry.auto-complete-tag", 8 | "formulahendry.auto-rename-tag", 9 | // electron片段 10 | "antoniormrzz.electron-snippets", 11 | // es6片段 12 | "xabikos.javascriptsnippets", 13 | // naive-ui片段 14 | "jimdong.naive-ui-snippets", 15 | // 路径提示 16 | "christian-kohler.path-intellisense", 17 | // 注释美化 18 | "aaron-bond.better-comments", 19 | // 便捷console 20 | "whtouche.vscode-js-console-utils", 21 | // 块注释 22 | "lllllllqw.jsdoc", 23 | // 错误行内显示 24 | "usernamehw.errorlens", 25 | // 颜色高亮 26 | "naumovs.color-highlight", 27 | // env高亮 28 | "mikestead.dotenv", 29 | // icon预览 30 | "antfu.iconify", 31 | // 图片预览 32 | "kisstkondoros.vscode-gutter-preview", 33 | // 编辑器配置 34 | "editorconfig.editorconfig", 35 | // 计算引入包大小 36 | "wix.vscode-import-cost", 37 | // 即时搜索icon 38 | "afzalsayed96.icones", 39 | // ts@next版本 40 | "ms-vscode.vscode-typescript-next", 41 | // json转ts类型 42 | "mariusalchimavicius.json-to-ts", 43 | // 代码规范 44 | "dbaeumer.vscode-eslint", 45 | // prettier格式化 46 | "esbenp.prettier-vscode", 47 | // prettier-eslint格式化 48 | "rvest.vs-code-prettier-eslint", 49 | // ts-vue组件类型 50 | "vue.vscode-typescript-vue-plugin", 51 | // vue3语法支持 52 | "vue.volar", 53 | // windicss语法提示 54 | "voorjaar.windicss-intellisense" 55 | ] 56 | } 57 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "prettier.enable": false, 3 | "editor.formatOnSave": false, 4 | "editor.codeActionsOnSave": { 5 | "source.fixAll.eslint": true 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.vscode/tsx.code-snippets: -------------------------------------------------------------------------------- 1 | { 2 | // Place your 全局 snippets here. Each snippet is defined under a snippet name and has a scope, prefix, body and 3 | // description. Add comma separated ids of the languages where the snippet is applicable in the scope field. If scope 4 | // is left empty or omitted, the snippet gets applied to all languages. The prefix is what is 5 | // used to trigger the snippet and the body will be expanded and inserted. Possible variables are: 6 | // $1, $2 for tab stops, $0 for the final cursor position, and ${1:label}, ${2:another} for placeholders. 7 | // Placeholders with the same ids are connected. 8 | // Example: 9 | "Print to console": { 10 | "prefix": "!tsx", 11 | "body": [ 12 | "export default defineComponent({", 13 | " name: '$1',", 14 | " setup(props, { attrs, emit, expose, slots }) {", 15 | " ", 16 | " return () => (<$2>)", 17 | " }", 18 | "})" 19 | ], 20 | "description": "vue3 tsx setup template" 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /.yarnrc: -------------------------------------------------------------------------------- 1 | registry=https://registry.npmmirror.com 2 | electron_mirror=https://cdn.npmmirror.com/binaries/electron/ 3 | electron_builder_binaries_mirror=https://npmmirror.com/mirrors/electron-builder-binaries/ 4 | msvs_version=2017 5 | sqlite3_mirror=https://cdn.npmmirror.com/binaries/sqlite3/ 6 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 安静 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 | # electron-vue3-quiet 2 | 3 | > 这是一个基于 electron 的 vue3 的模板,使用当下最新的技术栈. 4 | 5 | ## github 地址 6 | 7 | > https://github.com/TaiAiAc/electron-vue3-quiet.git 8 | 9 | ## gitee 镜像仓库地址 10 | 11 | > https://gitee.com/TaiAi/electron-vue3-quiet.git 12 | 13 | ## 文档地址 14 | 15 | > https://taiaiac.github.io/electron-vue3-quiet-doc/ 16 | 17 | ## 技术栈关键字 18 | 19 | - typesctript 20 | - vite + rollup 21 | - electron + electron-builder 22 | - vue3 + setup + tsx 23 | - windicss 24 | - prettier 25 | - eslint 26 | - sqlite3 27 | - ... 28 | 29 | > ### **请确保您的 node 环境是大于或等于 16** 30 | 31 | ## 安装使用 32 | 33 | - 获取项目代码 34 | 35 | ```bash 36 | git clone https://gitee.com/TaiAi/electron-vue3-quiet.git 37 | ``` 38 | 39 | - 安装依赖 40 | 41 | ```bash 42 | cd electron-vue3-quiet 43 | 44 | npm i -g @antfu/ni 45 | 46 | ni 47 | ``` 48 | 49 | - 运行(开发) 50 | 51 | ```bash 52 | nr dev 53 | ``` 54 | 55 | - 打包(生产) 56 | 57 | ```bash 58 | nr build 59 | ``` 60 | -------------------------------------------------------------------------------- /build/config/define.ts: -------------------------------------------------------------------------------- 1 | import dayjs from 'dayjs' 2 | 3 | /** 项目构建时间 */ 4 | const PROJECT_BUILD_TIME = JSON.stringify(dayjs().format('YYYY-MM-DD HH:mm:ss')) 5 | 6 | export const viteDefine = { 7 | PROJECT_BUILD_TIME 8 | } 9 | -------------------------------------------------------------------------------- /build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './path' 2 | export * from './define' 3 | export * from './resolve' 4 | -------------------------------------------------------------------------------- /build/config/path.ts: -------------------------------------------------------------------------------- 1 | import { fileURLToPath } from 'url' 2 | import { normalizePath } from 'vite' 3 | 4 | /** 5 | * 解析路径 6 | * @param basePath - 基础路径 7 | */ 8 | export function resolvePath(rootPath: string, basePath: string) { 9 | const root = fileURLToPath(new URL(rootPath, basePath)) 10 | const src = `${root}src/renderer` 11 | 12 | return { 13 | root: normalizePath(root), 14 | renderer: normalizePath(src) 15 | } 16 | } 17 | -------------------------------------------------------------------------------- /build/config/resolve.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import type { UserConfig } from 'vite' 3 | 4 | export function resolveConfig(root: string): UserConfig['resolve'] { 5 | return { 6 | alias: { 7 | '@common': resolve(root, '../common'), 8 | '@enums': resolve(root, '../enums'), 9 | '@': root 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /build/index.ts: -------------------------------------------------------------------------------- 1 | export * from './plugins' 2 | export * from './config' 3 | -------------------------------------------------------------------------------- /build/plugins/auto-import.ts: -------------------------------------------------------------------------------- 1 | import AutoImport from 'unplugin-auto-import/vite' 2 | import { FileSystemIconLoader } from 'unplugin-icons/loaders' 3 | import IconsResolver from 'unplugin-icons/resolver' 4 | import Icons from 'unplugin-icons/vite' 5 | import { NaiveUiResolver } from 'unplugin-vue-components/resolvers' 6 | import Components from 'unplugin-vue-components/vite' 7 | 8 | export default (srcPath: string) => { 9 | return [ 10 | Icons({ 11 | compiler: 'vue3', 12 | customCollections: { 13 | custom: FileSystemIconLoader(`${srcPath}/assets/svg`) 14 | }, 15 | autoInstall: true, 16 | scale: 1, 17 | defaultClass: 'inline-block', 18 | jsx: 'preact' 19 | }), 20 | AutoImport({ 21 | dts: '../types/auto-imports.d.ts', 22 | imports: [ 23 | 'vue', 24 | 'vue-router', 25 | 'vue-i18n', 26 | '@vueuse/core', 27 | { 28 | 'vue-router': ['RouterView', 'RouterLink'] 29 | } 30 | ] 31 | }), 32 | Components({ 33 | dts: '../types/components.d.ts', 34 | resolvers: [ 35 | IconsResolver({ 36 | customCollections: ['custom'], 37 | componentPrefix: 'icon' 38 | }), 39 | NaiveUiResolver() 40 | ] 41 | }) 42 | ] 43 | } 44 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { ConfigEnv, PluginOption } from 'vite' 2 | import autoImport from './auto-import' 3 | import windicss from './windicss' 4 | import visualizer from './visualizer' 5 | import macros from './macros' 6 | 7 | /** 8 | * vite插件 9 | * @param configEnv - 环境 10 | * @param srcPath - src路径 11 | * @param viteEnv - 环境变量配置 12 | */ 13 | export function setupVitePlugins(configEnv: ConfigEnv, srcPath: string): (PluginOption | PluginOption[])[] { 14 | const plugins = [macros, ...autoImport(srcPath), windicss] 15 | 16 | if (configEnv.command === 'build') 17 | plugins.push(visualizer) 18 | 19 | return plugins 20 | } 21 | -------------------------------------------------------------------------------- /build/plugins/macros.ts: -------------------------------------------------------------------------------- 1 | // vite.config.ts 2 | import VueMacros from 'unplugin-vue-macros/vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | import VueJsx from '@vitejs/plugin-vue-jsx' 5 | 6 | export default VueMacros({ 7 | plugins: { 8 | vue: Vue(), 9 | vueJsx: VueJsx() // if needed 10 | } 11 | }) 12 | -------------------------------------------------------------------------------- /build/plugins/visualizer.ts: -------------------------------------------------------------------------------- 1 | import { visualizer } from 'rollup-plugin-visualizer' 2 | 3 | export default visualizer({ 4 | gzipSize: true, 5 | brotliSize: true 6 | }) 7 | -------------------------------------------------------------------------------- /build/plugins/windicss.ts: -------------------------------------------------------------------------------- 1 | import windiCSS from 'vite-plugin-windicss' 2 | 3 | export default windiCSS({ 4 | scan: { 5 | dirs: '.', // 当前目录下所有文件 6 | fileExtensions: ['vue', 'js', 'ts', 'tsx'] // 同时启用扫描vue/js/ts 7 | }, 8 | config: { 9 | darkMode: 'class', 10 | transformCSS: 'post', 11 | attributify: { 12 | prefix: 'a:' 13 | }, 14 | alias: { 15 | test: 'bg-[#0ECAFF]' 16 | }, 17 | shortcuts: { 18 | 'wh-full': 'w-full h-full', 19 | 'flex-center': 'flex justify-center items-center', 20 | 'flex-col-center': 'flex-center flex-col' 21 | }, 22 | theme: { 23 | extend: { 24 | colors: { 25 | 'primary': 'var(--primary-color)', 26 | 'primary-hover': 'var(--primary-color-hover)', 27 | 'primary-pressed': 'var(--primary-color-pressed)', 28 | 'primary-active': 'var(--primary-color-active)', 29 | 'info': 'var(--info-color)', 30 | 'info-hover': 'var(--info-color-hover)', 31 | 'info-pressed': 'var(--info-color-pressed)', 32 | 'info-active': 'var(--info-color-active)', 33 | 'success': 'var(--success-color)', 34 | 'success-hover': 'var(--success-color-hover)', 35 | 'success-pressed': 'var(--success-color-pressed)', 36 | 'success-active': 'var(--success-color-active)', 37 | 'warning': 'var(--warning-color)', 38 | 'warning-hover': 'var(--warning-color-hover)', 39 | 'warning-pressed': 'var(--warning-color-pressed)', 40 | 'warning-active': 'var(--warning-color-active)', 41 | 'error': 'var(--error-color)', 42 | 'error-hover': 'var(--error-color-hover)', 43 | 'error-pressed': 'var(--error-color-pressed)', 44 | 'error-active': 'var(--error-color-active)' 45 | }, 46 | backgroundColor: { 47 | dark: '#101014', 48 | light: '#FCFCFC', 49 | base: '#1F222A' 50 | }, 51 | textColor: { 52 | base: '#FCFCFC' 53 | }, 54 | transitionProperty: [ 55 | 'width', 56 | 'height', 57 | 'background', 58 | 'background-color', 59 | 'padding-left', 60 | 'border-color', 61 | 'right', 62 | 'fill' 63 | ] 64 | } 65 | } 66 | } 67 | }) 68 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { extends: ['@commitlint/config-conventional'] } 2 | -------------------------------------------------------------------------------- /icons/256x256.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/icons/256x256.png -------------------------------------------------------------------------------- /icons/icon.icns: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/icons/icon.icns -------------------------------------------------------------------------------- /icons/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/icons/icon.ico -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "electron-vue3-quiet", 3 | "version": "0.0.1", 4 | "description": "桌面端脚手架", 5 | "author": "安静 <528627554@qq.com>", 6 | "license": "MIT", 7 | "homepage": "https://github.com/TaiAiAc/electron-vue3-quiet#readme", 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/TaiAiAc/electron-vue3-quiet.git" 11 | }, 12 | "bugs": { 13 | "url": "https://github.com/TaiAiAc/electron-vue3-quiet/issues" 14 | }, 15 | "keywords": [ 16 | "electron", 17 | "vue3", 18 | "typescript", 19 | "vite", 20 | "rollup", 21 | "electron-builder", 22 | "typeorm" 23 | ], 24 | "main": "./dist/main.js", 25 | "engines": { 26 | "node": ">=16" 27 | }, 28 | "scripts": { 29 | "dev": "esno scripts/dev.ts", 30 | "build": "esno scripts/default-build.ts", 31 | "build:options": "esno scripts/options-build.ts", 32 | "clear:build": "rimraf dist && rimraf out", 33 | "typecheck": "vue-tsc --noEmit --skipLibCheck", 34 | "lint": "eslint . --fix", 35 | "postinstall": "electron-builder install-app-deps", 36 | "prepare": "husky install", 37 | "analysis": "windicss-analysis", 38 | "dep:upgrade": "yarn upgrade-interactive --latest", 39 | "commit-push": "git pull && git add . && git-cz && git push" 40 | }, 41 | "config": { 42 | "commitizen": { 43 | "path": "node_modules/cz-customizable" 44 | } 45 | }, 46 | "dependencies": { 47 | "@quiteer/electron-ipc": "^0.1.0", 48 | "@quiteer/electron-preload": "^0.0.8", 49 | "axios": "^0.27.2", 50 | "ffi-napi": "^4.0.3", 51 | "font-list": "^1.4.5", 52 | "reflect-metadata": "^0.1.13", 53 | "rxjs": "^7.5.6", 54 | "sqlite3": "^5.0.11", 55 | "typeorm": "^0.3.7", 56 | "uuid": "^8.3.2" 57 | }, 58 | "devDependencies": { 59 | "@commitlint/cli": "^17.2.0", 60 | "@commitlint/config-conventional": "^17.2.0", 61 | "@iconify/json": "^2.1.141", 62 | "@iconify/vue": "^3.2.1", 63 | "@quiteer/eslint-config": "^0.0.3", 64 | "@rollup/plugin-alias": "^3.1.9", 65 | "@rollup/plugin-commonjs": "^22.0.0", 66 | "@rollup/plugin-json": "^4.1.0", 67 | "@rollup/plugin-node-resolve": "^13.3.0", 68 | "@rollup/plugin-replace": "^4.0.0", 69 | "@types/ffi-napi": "^4.0.5", 70 | "@types/inquirer": "^8.2.1", 71 | "@types/sqlite3": "^3.1.8", 72 | "@types/uuid": "^8.3.4", 73 | "@vitejs/plugin-vue": "^3.2.0", 74 | "@vitejs/plugin-vue-jsx": "^2.1.1", 75 | "@vueuse/core": "^9.5.0", 76 | "colord": "^2.9.2", 77 | "commitizen": "^4.2.5", 78 | "cz-conventional-changelog": "^3.3.0", 79 | "cz-customizable": "^6.9.1", 80 | "dayjs": "^1.11.4", 81 | "dotenv": "^16.0.1", 82 | "electron": "^20.0.0", 83 | "electron-builder": "^23.3.3", 84 | "electron-devtools-vendor": "^1.1.0", 85 | "electron-log": "^4.4.7", 86 | "esbuild": "^0.14.53", 87 | "eslint": "^8.28.0", 88 | "esno": "^0.16.3", 89 | "follow-redirects": "^1.15.1", 90 | "fs-extra": "^10.1.0", 91 | "husky": "^8.0.2", 92 | "inquirer": "^9.0.2", 93 | "javascript-obfuscator": "^4.0.0", 94 | "lint-staged": "^13.0.1", 95 | "naive-ui": "^2.34.0", 96 | "pinia": "^2.0.24", 97 | "portfinder": "^1.0.28", 98 | "rimraf": "^3.0.2", 99 | "rollup": "^2.77.2", 100 | "rollup-plugin-esbuild": "^4.8.2", 101 | "rollup-plugin-obfuscator": "^0.2.2", 102 | "rollup-plugin-visualizer": "^5.7.1", 103 | "sass": "^1.56.1", 104 | "tslib": "^2.4.1", 105 | "typescript": "^4.9.3", 106 | "unplugin-auto-import": "^0.10.3", 107 | "unplugin-icons": "^0.14.13", 108 | "unplugin-vue-components": "0.21.2", 109 | "unplugin-vue-macros": "^0.15.2", 110 | "vite": "3.2.4", 111 | "vite-plugin-windicss": "^1.8.7", 112 | "vue": "^3.2.45", 113 | "vue-i18n": "^9.2.0", 114 | "vue-router": "^4.1.6", 115 | "vue-tsc": "^1.0.9", 116 | "windicss": "^3.5.4", 117 | "windicss-analysis": "^0.3.5", 118 | "zx": "^7.0.8" 119 | } 120 | } 121 | -------------------------------------------------------------------------------- /public/hide.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/public/hide.png -------------------------------------------------------------------------------- /public/icon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/public/icon.ico -------------------------------------------------------------------------------- /public/icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/public/icon.png -------------------------------------------------------------------------------- /public/tray.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/public/tray.png -------------------------------------------------------------------------------- /scripts/builder.config.ts: -------------------------------------------------------------------------------- 1 | import type { CliOptions, Configuration } from 'electron-builder' 2 | import { name, version } from '../package.json' 3 | 4 | interface BuilderOptions { 5 | isCreateExe: boolean 6 | isAsar: boolean 7 | archs: string[] 8 | } 9 | 10 | const config: Configuration = { 11 | asar: false, 12 | appId: 'org.TaiAi.electron-vue3-quiet', 13 | productName: name, 14 | protocols: { 15 | name, 16 | schemes: ['deeplink'] 17 | }, 18 | nsis: { 19 | oneClick: false, 20 | language: '2052', 21 | perMachine: true, 22 | allowElevation: true, 23 | allowToChangeInstallationDirectory: true, 24 | runAfterFinish: true, 25 | createDesktopShortcut: true, 26 | createStartMenuShortcut: true, 27 | artifactName: `${name} \${arch} Setup ${version}.\${ext}` 28 | }, 29 | files: ['dist/**/*'], 30 | extraFiles: ['lib'], 31 | directories: { 32 | output: 'out' 33 | }, 34 | publish: [ 35 | { 36 | provider: 'generic', 37 | url: 'http://127.0.0.1' 38 | } 39 | ], 40 | dmg: { 41 | contents: [ 42 | { 43 | x: 410, 44 | y: 150, 45 | type: 'link', 46 | path: '/Applications' 47 | }, 48 | { 49 | x: 130, 50 | y: 150, 51 | type: 'file' 52 | } 53 | ] 54 | }, 55 | mac: { icon: 'icons/icon.icns', target: 'dmg' }, 56 | win: { icon: 'icons/icon.ico', target: 'nsis' }, 57 | linux: { 58 | target: ['AppImage', 'rpm', 'deb'], 59 | icon: 'icons', 60 | desktop: { 61 | StartupNotify: 'false', 62 | Encoding: 'UTF-8', 63 | MimeType: 'x-scheme-handler/deeplink' 64 | } 65 | } 66 | } 67 | 68 | export default (isDefault: boolean, options?: BuilderOptions): CliOptions => { 69 | const defaultPlatformKey = process.arch 70 | 71 | if (isDefault) 72 | return { config, [defaultPlatformKey]: true } 73 | 74 | if (!options) 75 | throw new Error('electron-builder配置项缺失') 76 | 77 | const { isCreateExe, isAsar, archs } = options 78 | 79 | const rawOptions: CliOptions = { config: { ...config, asar: isAsar } } 80 | const setArch = (options: string[]) => options.forEach(key => (rawOptions[key] = true)) 81 | 82 | if (archs.length) 83 | setArch(archs) 84 | else setArch([defaultPlatformKey]) 85 | 86 | return { 87 | ...rawOptions, 88 | dir: !isCreateExe 89 | } 90 | } 91 | -------------------------------------------------------------------------------- /scripts/default-build.ts: -------------------------------------------------------------------------------- 1 | import { $, path } from 'zx' 2 | import { config as getEnv } from 'dotenv' 3 | import type { OutputOptions } from 'rollup' 4 | import { rollup } from 'rollup' 5 | import { build } from 'electron-builder' 6 | import getRollupConfig from './rollup.config' 7 | import getBuilderConfig from './builder.config' 8 | import { colorLog, runnerLog } from './patternLog' 9 | 10 | const defaultBuildCli = async () => { 11 | const allLog = colorLog('build') 12 | console.time(allLog.timeKey('命令行执行')) 13 | 14 | await runnerLog(() => $`rimraf dist && rimraf out`, { 15 | name: 'rimraf', 16 | info: '清除 dist & out 目录 成功', 17 | timeKey: '清除' 18 | }) 19 | 20 | await runnerLog(() => $`vue-tsc --noEmit --skipLibCheck`, { 21 | name: 'vue-tsc', 22 | info: '完成ts类型检查', 23 | timeKey: 'ts类型检查' 24 | }) 25 | 26 | await runnerLog(() => $`vite build --config scripts/vite.config.ts`, { 27 | name: 'vite', 28 | info: '完成渲染进程代码构建', 29 | timeKey: '构建' 30 | }) 31 | 32 | // 启动环境变量 33 | const { parsed: globalEnv } = getEnv({ path: path.resolve(process.cwd(), '.env') }) 34 | const { parsed: env } = getEnv({ path: path.resolve(process.cwd(), '.env.production') }) 35 | const rollupConfig = getRollupConfig({ ...globalEnv, ...env } as NodeJS.ProcessEnv) 36 | const rollupBuild = await rollup(rollupConfig) 37 | 38 | await runnerLog(() => rollupBuild.write(rollupConfig.output as OutputOptions), { 39 | name: 'rollup', 40 | info: '完成主进程代码构建', 41 | timeKey: '构建' 42 | }) 43 | 44 | const builderConfig = getBuilderConfig(true) 45 | await runnerLog(() => build(builderConfig), { 46 | name: 'electron-build', 47 | info: '输出可执行程序成功', 48 | timeKey: '安装包' 49 | }) 50 | 51 | console.timeEnd(allLog.timeKey('命令行执行')) 52 | } 53 | 54 | defaultBuildCli() 55 | -------------------------------------------------------------------------------- /scripts/dev.ts: -------------------------------------------------------------------------------- 1 | import { $, path } from 'zx' 2 | import { config as getEnv } from 'dotenv' 3 | import { getPortPromise } from 'portfinder' 4 | 5 | async function devCli() { 6 | const { parsed: devEnv } = getEnv({ path: path.resolve(process.cwd(), '.env') }) 7 | const port = await getPortPromise({ 8 | port: Number(devEnv?.VITE_BASE_PROT) 9 | }) 10 | 11 | $`vite --config scripts/vite.config.ts --port ${port}` 12 | 13 | await $`esno scripts/watch.ts ${port}` 14 | 15 | process.exit() 16 | } 17 | 18 | devCli() 19 | -------------------------------------------------------------------------------- /scripts/options-build.ts: -------------------------------------------------------------------------------- 1 | import { $, path } from 'zx' 2 | import inquirer from 'inquirer' 3 | import { config as getEnv } from 'dotenv' 4 | import type { OutputOptions } from 'rollup' 5 | import { rollup } from 'rollup' 6 | import { build } from 'electron-builder' 7 | import getRollupConfig from './rollup.config' 8 | import { colorLog, runnerLog } from './patternLog' 9 | import getBuilderConfig from './builder.config' 10 | 11 | const inquirerCli = async () => { 12 | const { isTypecheck } = await inquirer 13 | .prompt([{ type: 'confirm', name: 'isTypecheck', message: '是否执行ts类型检查?' }]) 14 | .catch((err) => { 15 | console.log('err: ', err) 16 | process.exit(1) 17 | }) 18 | 19 | const { isClearness } = await inquirer 20 | .prompt([{ type: 'confirm', name: 'isClearness', message: '是否执行代码混淆压缩?' }]) 21 | .catch((err) => { 22 | console.log('err: ', err) 23 | process.exit(1) 24 | }) 25 | 26 | const { isCreateExe } = await inquirer 27 | .prompt([{ type: 'confirm', name: 'isCreateExe', message: '是否生成安装包?' }]) 28 | .catch((err) => { 29 | console.log('err: ', err) 30 | process.exit(1) 31 | }) 32 | 33 | const { isAsar } = await inquirer 34 | .prompt([{ type: 'confirm', name: 'isAsar', message: '是否开启asar?' }]) 35 | .catch((err) => { 36 | console.log('err: ', err) 37 | process.exit(1) 38 | }) 39 | 40 | const isMac = process.platform === 'darwin' 41 | const isLiunx = process.platform === 'linux' 42 | const isWin32 = process.arch === 'ia32' 43 | const isWin64 = process.arch === 'x64' 44 | 45 | const { pattern } = await inquirer 46 | .prompt([ 47 | { 48 | type: 'checkbox', 49 | name: 'pattern', 50 | message: '请选择构建模式 , 默认为当前操作系统平台 ~', 51 | pageSize: 10, 52 | choices: [ 53 | { name: 'win64', value: 'x64', disabled: !(isWin64 || isMac) }, 54 | { name: 'win32', value: 'ia32', disabled: !(isWin64 || isWin32 || isMac) }, 55 | { name: 'mac', value: 'arm64', disabled: !isMac }, 56 | { 57 | name: 'linux', 58 | value: 'armv7l', 59 | disabled: !(isLiunx || isMac) 60 | }, 61 | { 62 | name: 'all', 63 | value: 'universal', 64 | disabled: !isMac 65 | } 66 | ] 67 | } 68 | ]) 69 | .catch((err) => { 70 | console.log('err: ', err) 71 | process.exit(1) 72 | }) 73 | 74 | return { 75 | isTypecheck, 76 | isClearness: !isClearness, 77 | isCreateExe, 78 | isAsar, 79 | pattern 80 | } 81 | } 82 | 83 | const optionsBuildCli = async () => { 84 | await runnerLog(() => $`rimraf dist && rimraf out`, { 85 | name: 'rimraf', 86 | info: '清除 dist & out 目录 成功', 87 | timeKey: '清除' 88 | }) 89 | 90 | const { isTypecheck, isClearness, isCreateExe, isAsar, pattern } = await inquirerCli() 91 | const allLog = colorLog('build') 92 | console.time(allLog.timeKey('命令行执行')) 93 | 94 | if (isTypecheck) { 95 | await runnerLog(() => $`vue-tsc --noEmit --skipLibCheck`, { 96 | name: 'vue-tsc', 97 | info: '完成ts类型检查', 98 | timeKey: 'ts类型检查' 99 | }) 100 | } 101 | 102 | await runnerLog(() => $`vite build --config scripts/vite.config.ts`, { 103 | name: 'vite', 104 | info: '完成渲染进程代码构建', 105 | timeKey: '构建' 106 | }) 107 | 108 | // 启动环境变量 109 | const { parsed: globalEnv } = getEnv({ path: path.resolve(process.cwd(), '.env') }) 110 | const { parsed: env } = getEnv({ path: path.resolve(process.cwd(), '.env.production') }) 111 | const rollupConfig = getRollupConfig({ ...globalEnv, ...env } as NodeJS.ProcessEnv, isClearness) 112 | const rollupBuild = await rollup(rollupConfig) 113 | 114 | await runnerLog(() => rollupBuild.write(rollupConfig.output as OutputOptions), { 115 | name: 'rollup', 116 | info: '完成主进程代码构建', 117 | timeKey: '构建' 118 | }) 119 | 120 | const builderConfig = getBuilderConfig(false, { isCreateExe, isAsar, archs: pattern }) 121 | await runnerLog(() => build(builderConfig), { 122 | name: 'electron-build', 123 | info: '输出可执行程序成功', 124 | timeKey: '安装包' 125 | }) 126 | 127 | console.timeEnd(allLog.timeKey('命令行执行')) 128 | } 129 | 130 | optionsBuildCli() 131 | -------------------------------------------------------------------------------- /scripts/patternLog.ts: -------------------------------------------------------------------------------- 1 | import { chalk } from 'zx' 2 | 3 | interface LogInfo { 4 | name: string 5 | info: string 6 | timeKey: string 7 | } 8 | 9 | export function colorLog(name: string) { 10 | const logo = ` ${name} ` 11 | // eslint-disable-next-line no-console 12 | const doneLog = (info: string) => console.log(`\n${chalk.bgGreen.white(logo)} ${info}`) 13 | 14 | // eslint-disable-next-line no-console 15 | const errorLog = (info: string) => console.log(`\n${chalk.bgRed.white(logo)} ${info}`) 16 | 17 | // eslint-disable-next-line no-console 18 | const okayLog = (info: string) => console.log(`\n${chalk.bgBlue.white(logo)} ${info}`) 19 | 20 | const timeKey = (info: string) => `\n${chalk.bgBlue.white(logo)} 本次 ${info} 用时为` 21 | 22 | return { 23 | logo, 24 | doneLog, 25 | errorLog, 26 | okayLog, 27 | timeKey 28 | } 29 | } 30 | 31 | export async function runnerLog(promiseCli: () => Promise, logInfo: LogInfo) { 32 | const loger = colorLog(logInfo.name) 33 | console.time(loger.timeKey(logInfo.timeKey)) 34 | await promiseCli().catch((error) => { 35 | loger.errorLog(JSON.stringify(error)) 36 | process.exit(1) 37 | }) 38 | loger.doneLog(logInfo.info) 39 | console.timeEnd(loger.timeKey(logInfo.timeKey)) 40 | } 41 | -------------------------------------------------------------------------------- /scripts/rollup.config.ts: -------------------------------------------------------------------------------- 1 | // Node.js 提供的所有模块的名称列表。 2 | import { builtinModules } from 'module' 3 | import path from 'path' 4 | // 提供别名 5 | import { readdirSync } from 'fs' 6 | import alias from '@rollup/plugin-alias' 7 | // 将common依赖包转为es模块 8 | import commonjs from '@rollup/plugin-commonjs' 9 | // 使用Node解析算法来定位模块,以便在node_modules中使用第三方模块 10 | import nodeResolve from '@rollup/plugin-node-resolve' 11 | // .json文件转换为ES6模块 12 | import json from '@rollup/plugin-json' 13 | // 构建及压缩 14 | import esbuild from 'rollup-plugin-esbuild' 15 | // 混淆 16 | import obfuscator from 'rollup-plugin-obfuscator' 17 | // 注入环境变量 18 | import replace from '@rollup/plugin-replace' 19 | import { defineConfig } from 'rollup' 20 | 21 | import { dependencies } from '../package.json' 22 | 23 | const resolve = (filePath: string) => path.resolve(__dirname, `../${filePath}`) 24 | 25 | const transformEnv = (env: NodeJS.ProcessEnv) => { 26 | const prefix = 'process.env.' 27 | const envObj = {} 28 | Object.entries(env).forEach(([key, value]) => { 29 | envObj[`${prefix}${key}`] = JSON.stringify(value) 30 | }) 31 | 32 | return envObj 33 | } 34 | 35 | const inputOptions = () => { 36 | const files: string[] = readdirSync(resolve('src/preload')) 37 | return files.reduce( 38 | (pre, now) => { 39 | if (now === 'main') { 40 | return { 41 | ...pre, 42 | preload: resolve(`src/preload/${now}/index.ts`) 43 | } 44 | } 45 | return { 46 | ...pre, 47 | [now]: resolve(`src/preload/${now}/index.ts`) 48 | } 49 | }, 50 | { main: resolve('src/main/index.ts') } 51 | ) 52 | } 53 | 54 | export default (env: NodeJS.ProcessEnv, isClearness?: boolean) => { 55 | const isPord = env.NODE_ENV === 'production' 56 | 57 | return defineConfig({ 58 | input: inputOptions(), 59 | output: { 60 | dir: 'dist', 61 | format: 'cjs', 62 | sourcemap: false, 63 | sanitizeFileName: (fileName: string) => { 64 | if (fileName === 'preload') 65 | return 'preload/main' 66 | if (fileName !== 'main') 67 | return `preload/${fileName}` 68 | return fileName 69 | } 70 | }, 71 | plugins: [ 72 | replace({ 73 | preventAssignment: true, 74 | ...transformEnv(env) 75 | }), 76 | alias({ 77 | entries: [ 78 | { find: '~', replacement: resolve('src/main') }, 79 | { find: '@common', replacement: resolve('src/common') }, 80 | { find: '@enums', replacement: resolve('src/enums') } 81 | ] 82 | }), 83 | commonjs({ 84 | // 如果为false,则跳过CommonJS模块的源映射生成。这将提高性能。 85 | sourceMap: false 86 | }), 87 | nodeResolve({ 88 | // 指定插件将要操作的文件的扩展名。 89 | extensions: ['.mjs', '.ts', '.js', '.json', '.node'] 90 | }), 91 | json(), 92 | esbuild({ 93 | include: /\.[jt]s?$/, 94 | exclude: /node_modules/, 95 | // watch: process.argv.includes('--watch'), // rollup 中有配置 96 | sourceMap: false, // default 97 | minify: isClearness ? false : isPord, 98 | target: 'esnext', // default, or 'es20XX', 'esnext' 99 | // Like @rollup/plugin-replace 100 | define: { 101 | __VERSION__: '"x.y.z"' 102 | }, 103 | loaders: { 104 | '.json': 'json', 105 | '.ts': 'ts' 106 | } 107 | }), 108 | isPord && !isClearness ? obfuscator({}) : null 109 | ], 110 | external: [...builtinModules, ...Object.keys(dependencies), 'electron'] 111 | }) 112 | } 113 | -------------------------------------------------------------------------------- /scripts/vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'path' 2 | import { defineConfig } from 'vite' 3 | import { resolveConfig, resolvePath, setupVitePlugins, viteDefine } from '../build' 4 | 5 | export default defineConfig((configEnv) => { 6 | const { root, renderer } = resolvePath('../', import.meta.url) 7 | 8 | return { 9 | base: './', 10 | root: renderer, 11 | resolve: resolveConfig(renderer), 12 | define: viteDefine, 13 | build: { 14 | outDir: resolve(root, 'dist'), 15 | target: 'esnext', 16 | minify: 'esbuild', 17 | reportCompressedSize: false, 18 | emptyOutDir: false, 19 | chunkSizeWarningLimit: 2000 20 | }, 21 | server: { 22 | host: '0.0.0.0' 23 | }, 24 | plugins: [...setupVitePlugins(configEnv, renderer)], 25 | publicDir: resolve(root, 'public') 26 | } 27 | }) 28 | -------------------------------------------------------------------------------- /scripts/watch.ts: -------------------------------------------------------------------------------- 1 | import type { ChildProcessWithoutNullStreams } from 'child_process' 2 | import { $, path } from 'zx' 3 | import { watch } from 'rollup' 4 | import { config as getEnv } from 'dotenv' 5 | import electron from 'electron' 6 | import getOption from './rollup.config' 7 | 8 | const [, , port] = process.argv 9 | const { parsed: globalEnv } = getEnv({ path: path.resolve(process.cwd(), '.env') }) 10 | const { parsed: devEnv } = getEnv({ path: path.resolve(process.cwd(), '.env.development') }) 11 | const mainOptions = getOption({ ...devEnv, ...globalEnv, PORT: port } as unknown as NodeJS.ProcessEnv) 12 | 13 | const watcher = watch(mainOptions) 14 | 15 | watcher.on('change', (filename) => { 16 | // eslint-disable-next-line no-console 17 | console.log('主进程文件变更', filename) 18 | }) 19 | 20 | watcher.on('event', async (event) => { 21 | if (event.code === 'END') 22 | startElectron() 23 | else if (event.code === 'ERROR') 24 | process.exit(1) 25 | }) 26 | 27 | let electronProcess: ChildProcessWithoutNullStreams | null 28 | let manualRestart = false 29 | 30 | function startElectron() { 31 | if (electronProcess) { 32 | manualRestart = true 33 | electronProcess.pid && process.kill(electronProcess.pid) 34 | electronProcess = null 35 | 36 | setTimeout(() => { 37 | manualRestart = false 38 | }, 5000) 39 | } 40 | 41 | const mainPath = path.resolve(__dirname, '../dist/main.js') 42 | 43 | electronProcess = $.spawn(electron as any, [mainPath, '--inspect=9528']) 44 | 45 | electronProcess.stdout.on('data', removeJunk) 46 | 47 | electronProcess.stderr.on('data', removeJunk) 48 | 49 | electronProcess.on('close', () => { 50 | manualRestart || process.exit() 51 | }) 52 | } 53 | 54 | function removeJunk(chunk: string) { 55 | if (/\d+-\d+-\d+ \d+:\d+:\d+\.\d+ Electron(?: Helper)?\[\d+:\d+] /.test(chunk)) 56 | return false 57 | if (/\[\d+:\d+\/|\d+\.\d+:ERROR:CONSOLE\(\d+\)\]/.test(chunk)) 58 | return false 59 | if (/ALSA lib [a-z]+\.c:\d+:\([a-z_]+\)/.test(chunk)) 60 | return false 61 | 62 | const data = chunk.toString().split(/\r?\n/) 63 | let log = '' 64 | data.forEach((line) => { 65 | log += ` ${line}\n` 66 | }) 67 | // eslint-disable-next-line no-console 68 | console.log(log) 69 | } 70 | -------------------------------------------------------------------------------- /src/common/Time.ts: -------------------------------------------------------------------------------- 1 | export class Time { 2 | time: Date 3 | now: Date 4 | constructor(time: string | Date) { 5 | this.time = new Date(time) 6 | this.now = new Date() 7 | } 8 | 9 | get timeStr() { 10 | return this.ifYear() || this.ifMonth() || this.ifWeek() || this.ifYesterday() || this.ifDay() || '' 11 | } 12 | 13 | // 判断是否今年 14 | ifYear() { 15 | const date = this.time.getFullYear() 16 | const now = this.now.getFullYear() 17 | 18 | if (date !== now) 19 | return `${this.time.getFullYear()}/${this.time.getMonth() + 1}/${this.time.getDate()}` 20 | return false 21 | } 22 | 23 | // 是否本月 24 | ifMonth() { 25 | const date = this.time.getMonth() + 1 26 | const now = this.now.getMonth() + 1 27 | 28 | if (date !== now) 29 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 30 | return false 31 | } 32 | 33 | // 判断是否本周 34 | ifWeek() { 35 | const week = ['日', '一', '二', '三', '四', '五', '六'] 36 | const date = this.time.getDate() 37 | const now = this.now.getDate() 38 | const weekSum = this.now.getDay() 39 | 40 | if (now - date < 2) 41 | return false 42 | 43 | if (now - date > 6) 44 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 45 | 46 | if (!weekSum) 47 | return `星期${week[this.time.getDay()]}` 48 | 49 | if (now - date < weekSum) 50 | return `星期${week[this.time.getDay()]}` 51 | 52 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 53 | } 54 | 55 | // 判断是否是昨天 56 | ifYesterday() { 57 | const date = this.time.getDate() 58 | const now = this.now.getDate() 59 | if (now - date > 0) 60 | return '昨天' 61 | return false 62 | } 63 | 64 | // 判断是否当天 65 | ifDay() { 66 | const date = this.time.getDate() 67 | const now = this.now.getDate() 68 | if (date === now) 69 | return `${this.time.getHours()}:${this.time.getMinutes().toString().padStart(2, '0')}` 70 | 71 | return false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/common/typeof.ts: -------------------------------------------------------------------------------- 1 | enum ObjectString { 2 | number = '[object Number]', 3 | string = '[object String]', 4 | boolean = '[object Boolean]', 5 | null = '[object Null]', 6 | undefined = '[object Undefined]', 7 | object = '[object Object]', 8 | array = '[object Array]', 9 | date = '[object Date]', 10 | regexp = '[object RegExp]', 11 | set = '[object Set]', 12 | map = '[object Map]', 13 | file = '[object File]' 14 | } 15 | 16 | export function isNumber(data: unknown) { 17 | return Object.prototype.toString.call(data) === ObjectString.number 18 | } 19 | 20 | export function isString(data: unknown) { 21 | return Object.prototype.toString.call(data) === ObjectString.string 22 | } 23 | 24 | export function isBoolean(data: unknown) { 25 | return Object.prototype.toString.call(data) === ObjectString.boolean 26 | } 27 | 28 | export function isNull(data: unknown) { 29 | return Object.prototype.toString.call(data) === ObjectString.null 30 | } 31 | 32 | export function isUndefined(data: unknown) { 33 | return Object.prototype.toString.call(data) === ObjectString.undefined 34 | } 35 | 36 | export function isObject(data: unknown) { 37 | return Object.prototype.toString.call(data) === ObjectString.object 38 | } 39 | 40 | export function isArray(data: unknown) { 41 | return Object.prototype.toString.call(data) === ObjectString.array 42 | } 43 | 44 | export function isDate(data: unknown) { 45 | return Object.prototype.toString.call(data) === ObjectString.date 46 | } 47 | 48 | export function isRegExp(data: unknown) { 49 | return Object.prototype.toString.call(data) === ObjectString.regexp 50 | } 51 | 52 | export function isSet(data: unknown) { 53 | return Object.prototype.toString.call(data) === ObjectString.set 54 | } 55 | 56 | export function isMap(data: unknown) { 57 | return Object.prototype.toString.call(data) === ObjectString.map 58 | } 59 | 60 | export function isFile(data: unknown) { 61 | return Object.prototype.toString.call(data) === ObjectString.file 62 | } 63 | -------------------------------------------------------------------------------- /src/enums/system.ts: -------------------------------------------------------------------------------- 1 | /** 布局组件的名称 */ 2 | export enum EnumLayoutComponentName { 3 | basic = 'basic-layout', 4 | blank = 'blank-layout' 5 | } 6 | 7 | /** 布局模式 */ 8 | export enum EnumThemeLayoutMode { 9 | 'vertical' = '左侧菜单模式', 10 | 'horizontal' = '顶部菜单模式', 11 | 'vertical-mix' = '左侧菜单混合模式', 12 | 'horizontal-mix' = '顶部菜单混合模式' 13 | } 14 | 15 | /** 多页签风格 */ 16 | export enum EnumThemeTabMode { 17 | 'chrome' = '谷歌风格', 18 | 'button' = '按钮风格' 19 | } 20 | 21 | /** 水平模式的菜单位置 */ 22 | export enum EnumThemeHorizontalMenuPosition { 23 | 'flex-start' = '居左', 24 | 'center' = '居中', 25 | 'flex-end' = '居右' 26 | } 27 | 28 | /** 过渡动画类型 */ 29 | export enum EnumThemeAnimateMode { 30 | 'zoom-fade' = '渐变', 31 | 'zoom-out' = '闪现', 32 | 'fade-slide' = '滑动', 33 | 'fade' = '消退', 34 | 'fade-bottom' = '底部消退', 35 | 'fade-scale' = '缩放消退' 36 | } 37 | -------------------------------------------------------------------------------- /src/enums/window.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 集中存放所有窗口的key 3 | */ 4 | 5 | export const enum WinKey { 6 | MAIN = 'main', 7 | WEBVIEW = 'webview', 8 | LOADING = 'loading', 9 | FEELBRID = 'feel-brid', 10 | PRINT = 'print-demo', 11 | DROP = 'drop-demo', 12 | SQL = 'sql-demo', 13 | DOWNLOAD = 'download-demo', 14 | FONT = 'font-demo' 15 | } 16 | -------------------------------------------------------------------------------- /src/main/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules/app' 2 | export * from './modules/paths' 3 | export * from './modules/log' 4 | export * from './modules/tray' 5 | export * from './modules/menu' 6 | -------------------------------------------------------------------------------- /src/main/config/modules/app.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, app, dialog, powerSaveBlocker, session } from 'electron' 2 | import { WinKey } from '@enums/window' 3 | import { name } from '../../../../package.json' 4 | // import { printInfo } from './log' 5 | import { getWin, hasWin } from '~/window/create/win.map' 6 | import { getMainEnv, mainDevExecFn, mainProExecFn } from '~/tools/index' 7 | 8 | class CreateApp { 9 | private static _instance: CreateApp | null = null 10 | private readyList: any[] = [] 11 | constructor() { 12 | this.init() 13 | this.anomalyHandle() 14 | } 15 | 16 | static getInstance() { 17 | if (!this._instance) 18 | return (this._instance = new CreateApp()) 19 | else 20 | return this._instance 21 | } 22 | 23 | use(callback: () => void) { 24 | app.whenReady().then(callback) 25 | this.readyList.push(callback) 26 | return this 27 | } 28 | 29 | installDevtools() { 30 | app.whenReady().then(() => { 31 | mainDevExecFn(async () => { 32 | // eslint-disable-next-line @typescript-eslint/no-var-requires 33 | const { VUEJS3_DEVTOOLS } = require('electron-devtools-vendor') 34 | console.log('VUEJS3_DEVTOOLS: ', VUEJS3_DEVTOOLS) 35 | session.defaultSession.loadExtension(VUEJS3_DEVTOOLS, { 36 | allowFileAccess: true 37 | }) 38 | }) 39 | }) 40 | } 41 | 42 | private init() { 43 | getMainEnv((env) => { 44 | app.setName(env.NODE_ENV ? `dev-${name}` : name) 45 | }) 46 | 47 | // console.log('app.requestSingleInstanceLock(): ', app.requestSingleInstanceLock()) 48 | 49 | if (app.requestSingleInstanceLock()) { 50 | app.on('second-instance', () => { 51 | hasWin(WinKey.MAIN) && getWin(WinKey.MAIN)?.show() 52 | }) 53 | } 54 | else { 55 | mainProExecFn(app.quit) 56 | } 57 | 58 | app.on('activate', () => { 59 | const allWin = BrowserWindow.getAllWindows() 60 | if (allWin.length) 61 | allWin.map(win => win.show()) 62 | else 63 | this.readyList.forEach(fn => fn()) 64 | }) 65 | 66 | app.whenReady().then(() => { 67 | // 保活 68 | const id = powerSaveBlocker.start('prevent-display-sleep') 69 | setTimeout(() => { 70 | powerSaveBlocker.stop(id) 71 | }, 60000) 72 | }) 73 | 74 | // 由于9.x版本问题,需要加入该配置关闭跨域问题 75 | app.commandLine.appendSwitch('disable-features', 'OutOfBlinkCors') 76 | 77 | app.on('window-all-closed', () => { 78 | // 所有平台均为所有窗口关闭就退出软件 79 | app.quit() 80 | }) 81 | 82 | app.on('browser-window-created', () => { 83 | // 在创建新的 browserWindow 时发出。 84 | // console.log('window-created') 85 | // printInfo('info', 'window-created') 86 | }) 87 | } 88 | 89 | private anomalyHandle() { 90 | /** 91 | * 子进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。 子进程不包括渲染器进程。 92 | * @returns {void} 93 | * @author zmr (umbrella22) 94 | * @date 2020-11-27 95 | */ 96 | app.on('child-process-gone', (event, details) => { 97 | const message = { 98 | title: '', 99 | buttons: [], 100 | message: '' 101 | } 102 | switch (details.type) { 103 | case 'GPU': 104 | switch (details.reason) { 105 | case 'crashed': 106 | message.title = '警告' 107 | message.message = '硬件加速进程已崩溃,是否关闭硬件加速并重启?' 108 | break 109 | case 'killed': 110 | message.title = '警告' 111 | message.message = '硬件加速进程被意外终止,是否关闭硬件加速并重启?' 112 | break 113 | default: 114 | break 115 | } 116 | break 117 | 118 | default: 119 | break 120 | } 121 | dialog 122 | .showMessageBox(null as any, { 123 | type: 'warning', 124 | title: message.title, 125 | buttons: message.buttons, 126 | message: message.message, 127 | noLink: true 128 | }) 129 | .then((res) => { 130 | // 当显卡出现崩溃现象时使用该设置禁用显卡加速模式。 131 | if (res.response === 0) { 132 | if (details.type === 'GPU') 133 | app.disableHardwareAcceleration() 134 | app.relaunch({ 135 | args: process.argv.slice(1).concat(['--relaunch']) 136 | }) 137 | app.exit(0) 138 | } 139 | else { 140 | app.exit(0) 141 | } 142 | }) 143 | }) 144 | 145 | /** 146 | * 描述 渲染器进程意外消失时触发。 这种情况通常因为进程崩溃或被杀死。 147 | * @date 2022-03-10 148 | * @param {any} 'render-process-gone' 149 | * @param {any} (event 150 | * @param {any} webContents 151 | * @param {any} details 152 | * @returns {any} 153 | */ 154 | app.on('render-process-gone', (event, webContents, details) => { 155 | switch (details.reason) { 156 | case 'killed': 157 | // 进程发送一个SIGTERM,否则是被外部杀死的。 158 | break 159 | case 'crashed': 160 | // 进程崩溃 161 | break 162 | case 'oom': 163 | // 进程内存不足 164 | break 165 | case 'launch-failed': 166 | // 进程从未成功启动 167 | break 168 | case 'integrity-failure': 169 | // 窗口代码完整性检查失败 170 | break 171 | default: 172 | break 173 | } 174 | }) 175 | } 176 | } 177 | 178 | export const App = CreateApp.getInstance() 179 | -------------------------------------------------------------------------------- /src/main/config/modules/log.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import log from 'electron-log' 3 | import { logsPath } from './paths' 4 | function getTime() { 5 | const time = new Date() 6 | const year = time.getFullYear() 7 | const mounth = time.getMonth() + 1 8 | const day = time.getDate() 9 | const hours = time.getHours() 10 | 11 | return `${year}年${mounth}月${day}日${hours}时.log` 12 | } 13 | 14 | log.transports.file.resolvePath = () => join(logsPath, `${getTime()}`) 15 | 16 | export function printInfo(type: 'info' | 'error', info: string, value: any = '') { 17 | if (typeof value === 'object') 18 | value = JSON.stringify(value) 19 | 20 | switch (type) { 21 | case 'info': 22 | log.info(` \u2618 ${info} \u27A4 \u27A4 \u27A4 ${value}`) 23 | break 24 | case 'error': 25 | log.error(` \u2622 ${info} \u27A4 \u27A4 \u27A4 ${value}`) 26 | break 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/main/config/modules/menu.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, Menu, MenuItem } from 'electron' 2 | 3 | class GlobalMenuShortcuts { 4 | private static instance: GlobalMenuShortcuts 5 | menu: Menu 6 | 7 | static getInstance() { 8 | if (this.instance) 9 | return this.instance 10 | 11 | return (this.instance = new GlobalMenuShortcuts()) 12 | } 13 | 14 | constructor() { 15 | const template = [ 16 | { 17 | accelerator: 'Escape', 18 | label: '退出', 19 | click() { 20 | BrowserWindow.getAllWindows().map(win => win.isFocused() && win.hide()) 21 | } 22 | } 23 | ] 24 | 25 | this.menu = Menu.buildFromTemplate(template) 26 | 27 | Menu.setApplicationMenu(this.menu) 28 | 29 | this.addItem({ 30 | label: '开发', 31 | submenu: [ 32 | { 33 | label: '刷新', 34 | role: 'reload', 35 | accelerator: 'Ctrl + R' 36 | }, 37 | { 38 | label: '控制台', 39 | role: 'toggleDevTools', 40 | accelerator: 'Ctrl + Shift + I' 41 | } 42 | ] 43 | }) 44 | } 45 | 46 | addItem(template: Electron.MenuItemConstructorOptions) { 47 | this.menu.append(new MenuItem(template)) 48 | Menu.setApplicationMenu(this.menu) 49 | } 50 | } 51 | 52 | export const globalMenuInit = () => GlobalMenuShortcuts.getInstance() 53 | -------------------------------------------------------------------------------- /src/main/config/modules/paths.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 描述 处理不同环境下的文件路径 3 | * 项目中所有使用到的路劲在此注册后导出 4 | * 减少挂载全局的操作 5 | * process.env.__lib = libPath 6 | * @date 2022-03-04 7 | * @publicPath 静态资源对应pubilc目录 8 | * @libPath lib目录打包后在目录下第一级 9 | * @winURL html路径 10 | */ 11 | import { resolve } from 'path' 12 | 13 | const join = (path: string) => resolve(__dirname, path) 14 | const isDev = process.env.NODE_ENV === 'development' 15 | 16 | const publicPath = isDev ? join('../public') : __dirname 17 | 18 | export const libPath = isDev ? join('../lib') : join('../../../lib') 19 | 20 | export const winURL = isDev ? `http://localhost:${process.env.PORT}` : `file://${join('index.html')}` 21 | 22 | export const logsPath = isDev ? resolve(publicPath, '../logs') : resolve(publicPath, 'logs') 23 | 24 | // icon 25 | export const appIcon = resolve(publicPath, 'icon.png') 26 | export const hideIcon = resolve(publicPath, 'hide.png') 27 | export const trayIcon = resolve(publicPath, 'tray.png') 28 | 29 | // preload 30 | export const mainPreload = resolve(__dirname, 'preload/main.js') 31 | -------------------------------------------------------------------------------- /src/main/config/modules/tray.ts: -------------------------------------------------------------------------------- 1 | import { Menu, Tray, app } from 'electron' 2 | import { WinKey } from '@enums/window' 3 | import { hideIcon, trayIcon } from './paths' 4 | import { getWin, hasWin } from '~/window/create/win.map' 5 | 6 | class TrayInit { 7 | private static instance: TrayInit | null = null 8 | tray: Tray 9 | blink: NodeJS.Timeout | null = null 10 | 11 | static getInstance() { 12 | if (this.instance) 13 | return this.instance 14 | 15 | return (this.instance = new TrayInit()) 16 | } 17 | 18 | constructor() { 19 | this.tray = new Tray(trayIcon) 20 | 21 | this.init() 22 | } 23 | 24 | private init() { 25 | this.tray.on('click', () => { 26 | if (!hasWin(WinKey.MAIN)) 27 | return 28 | 29 | if (getWin(WinKey.MAIN)?.isVisible()) 30 | getWin(WinKey.MAIN)?.hide() 31 | else 32 | getWin(WinKey.MAIN)?.show() 33 | }) 34 | 35 | let empty = false 36 | this.tray.on('right-click', () => { 37 | const menuConfig = Menu.buildFromTemplate([ 38 | { 39 | label: empty ? '停止闪烁' : '开始闪烁', 40 | click: () => { 41 | empty = !empty 42 | if (this.blink) { 43 | clearInterval(this.blink) 44 | this.tray.setImage(trayIcon) 45 | } 46 | 47 | if (!empty) 48 | return 49 | 50 | let flag = false 51 | this.blink = setInterval(() => { 52 | this.tray.setImage(flag ? hideIcon : trayIcon) 53 | flag = !flag 54 | }, 600) 55 | } 56 | }, 57 | { 58 | label: '重启', 59 | click: () => { 60 | app.relaunch({ 61 | args: process.argv.slice(1).concat(['--relaunch']) 62 | }) 63 | app.exit(0) 64 | } 65 | }, 66 | { 67 | label: '退出', 68 | click: () => { 69 | app.exit(0) 70 | } 71 | } 72 | ]) 73 | this.tray.popUpContextMenu(menuConfig) 74 | }) 75 | } 76 | } 77 | 78 | export const trayInit = () => TrayInit.getInstance() 79 | -------------------------------------------------------------------------------- /src/main/index.ts: -------------------------------------------------------------------------------- 1 | import { createLoadingWindow } from './window' 2 | import { initIpc } from './ipc' 3 | import { App, globalMenuInit, trayInit } from './config' 4 | import initTypeorm from '~/typeorm' 5 | 6 | App.installDevtools() 7 | 8 | App.use(trayInit).use(globalMenuInit).use(initIpc).use(createLoadingWindow) 9 | 10 | initTypeorm() 11 | -------------------------------------------------------------------------------- /src/main/ipc/bucket/download.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import { Downloader } from '~/tools' 3 | 4 | export const ipcBus = new Map any>() 5 | 6 | ipcBus.set('file', async (event, options: Download.DownloadOptions) => { 7 | const win = BrowserWindow.fromWebContents(event.sender) 8 | 9 | if (!win) 10 | return 11 | 12 | const download = new Downloader(win, options) 13 | const details: Download.DownloadDetails = await download.start() 14 | 15 | return { ...details } 16 | }) 17 | -------------------------------------------------------------------------------- /src/main/ipc/bucket/files.ts: -------------------------------------------------------------------------------- 1 | export {} 2 | -------------------------------------------------------------------------------- /src/main/ipc/bucket/print.ts: -------------------------------------------------------------------------------- 1 | import type { WebContentsPrintOptions } from 'electron' 2 | 3 | export const ipcBus = new Map any>() 4 | 5 | ipcBus.set('get-printers', event => event.sender.getPrintersAsync()) 6 | 7 | ipcBus.set('print', async (event, options: WebContentsPrintOptions) => { 8 | return new Promise((resolve) => { 9 | event.sender.print(options, (success: boolean, failureReason: string) => { 10 | resolve({ success, failureReason }) 11 | }) 12 | }) 13 | }) 14 | -------------------------------------------------------------------------------- /src/main/ipc/bucket/sql.ts: -------------------------------------------------------------------------------- 1 | import { addSingle, searchAll } from '~/typeorm/manager' 2 | 3 | export const ipcBus = new Map any>() 4 | 5 | ipcBus.set('user-search-all', () => searchAll()) 6 | 7 | ipcBus.set('user-add-single', async (event, data: Sql.User) => addSingle(data)) 8 | -------------------------------------------------------------------------------- /src/main/ipc/handle/index.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { WinKey } from '@enums/window' 3 | import { BrowserWindow, ipcMain } from 'electron' 4 | 5 | import { getFonts } from 'font-list' 6 | import { Library } from 'ffi-napi' 7 | import { ipcBus as printBus } from '../bucket/print' 8 | import { ipcBus as downloadBus } from '../bucket/download' 9 | import { ipcBus as sqlBus } from '../bucket/sql' 10 | import { libPath } from '~/config' 11 | import { getWin, winMap } from '~/window/create/win.map' 12 | 13 | const busCallback = ( 14 | ipcBus: Map any>, 15 | event: Electron.IpcMainInvokeEvent, 16 | type: string, 17 | args: any[] 18 | ) => { 19 | const performFunc = ipcBus.get(type) 20 | if (performFunc instanceof Function) 21 | return performFunc(event, args) 22 | } 23 | 24 | export function initHandleIpc() { 25 | // 获取当前窗口状态 26 | ipcMain.handle('get-win-status', (event) => { 27 | const win = BrowserWindow.fromWebContents(event.sender) 28 | for (const v of winMap.values()) { 29 | if (v.id === win?.id) 30 | return v 31 | } 32 | }) 33 | 34 | // 打印 35 | ipcMain.handle('print-option', (event, type, args) => busCallback(printBus, event, type, args)) 36 | 37 | // 下载 38 | ipcMain.handle('download-option', (event, type, args) => busCallback(downloadBus, event, type, args)) 39 | 40 | // sql 41 | ipcMain.handle('sql-option', (event, type, args) => busCallback(sqlBus, event, type, args)) 42 | 43 | ipcMain.handle('back-img', async () => { 44 | const nativeImage = await getWin(WinKey.MAIN)?.capturePage() 45 | return nativeImage?.toDataURL() 46 | }) 47 | 48 | ipcMain.handle('ffi-add', async (event, a = 0, b = 0) => { 49 | let dllName = '' 50 | if (process.platform === 'win32' && ['x64', 'ia32'].includes(process.arch)) 51 | dllName = `dll_test_${process.platform}_${process.arch}.dll` 52 | else if (process.platform === 'darwin' && process.arch === 'arm64') 53 | dllName = 'libdll_test_darwin_arm64.dylib' 54 | 55 | if (dllName) { 56 | const lib = Library(join(libPath, dllName), { 57 | add: ['int', ['int', 'int']] 58 | }) 59 | // add (a: number, b: number) => (a + b) 60 | return lib.add(a, b) 61 | } 62 | }) 63 | 64 | ipcMain.handle('getFonts', async () => { 65 | return await getFonts().catch(() => []) 66 | }) 67 | } 68 | -------------------------------------------------------------------------------- /src/main/ipc/index.ts: -------------------------------------------------------------------------------- 1 | import { Ipc } from '@quiteer/electron-ipc' 2 | import { initOnIpc } from './on' 3 | import { initHandleIpc } from './handle' 4 | 5 | export const initIpc = () => { 6 | Ipc.init() 7 | initOnIpc() 8 | initHandleIpc() 9 | } 10 | -------------------------------------------------------------------------------- /src/main/ipc/on/index.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, ipcMain } from 'electron' 2 | import { getWin, hasWin, winRead } from '~/window/create/win.map' 3 | import { CreateWindow } from '~/window/create/CreateWindow' 4 | 5 | export function initOnIpc() { 6 | // 页面准备完毕 7 | ipcMain.on('dom-mounted', (event) => { 8 | const id = BrowserWindow.fromWebContents(event.sender)?.id 9 | id && winRead(id) 10 | }) 11 | 12 | ipcMain.on('main-open', (event, state: Component.CardState) => { 13 | if (hasWin(state.key)) 14 | return getWin(state.key)?.show() 15 | 16 | const win = new CreateWindow(state.key, { frame: true }) 17 | win.loadURL(state.path).setTitle(state.title).setSize(660, 480, true).show().unClose() 18 | }) 19 | } 20 | -------------------------------------------------------------------------------- /src/main/tools/electron/download.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import type { BrowserWindow } from 'electron' 3 | import { app } from 'electron' 4 | 5 | export const downloadItemMap = new Map() 6 | 7 | /** 8 | * 描述 9 | * @date 2022-03-18 10 | * @param {any} win:BrowserWindow 11 | * @param {any} options:DownloadOptions 12 | * @returns {any} 13 | */ 14 | export class Downloader { 15 | private downloadItem: Electron.DownloadItem 16 | private options: Download.DownloadOptions 17 | private win: BrowserWindow 18 | status: Download.DownloadStatus 19 | constructor(win: BrowserWindow, options: Download.DownloadOptions) { 20 | if (!options.savePath && !options.isShowSaveDialog) 21 | options.savePath = app.getPath('downloads') 22 | 23 | this.options = options 24 | this.win = win 25 | } 26 | 27 | /** 28 | * 描述 返回控制器 29 | * @date 2022-03-18 30 | * @returns {any} 31 | */ 32 | get item(): Electron.DownloadItem { 33 | return this.downloadItem 34 | } 35 | 36 | /** 37 | * 描述 下载状态变化 38 | * @date 2022-03-18 39 | * @param {any} state 40 | * @param {any} message 41 | * @param {any} progress=0 42 | * @returns {any} 43 | */ 44 | private statusChange(state: any, message: string, progress = 0) { 45 | this.status = { 46 | state, 47 | isSuccess: state === 'completed', 48 | message, 49 | progress 50 | } 51 | } 52 | 53 | /** 54 | * 描述 本次下载信息 55 | * @date 2022-03-18 56 | * @param {any} status 57 | * @returns {any} 58 | */ 59 | private getDtails(status: any) { 60 | return { 61 | ...status, 62 | time: this.getTime(), 63 | url: this.item.getURL(), 64 | savePath: this.item.savePath, 65 | filename: this.item.getFilename() 66 | } 67 | } 68 | 69 | /** 70 | * 描述 下载时间 71 | * @date 2022-03-18 72 | * @returns {any} 73 | */ 74 | private getTime() { 75 | const int = (n: number): number => parseInt(`${n}`) 76 | const pad = (n: number): string => n.toString().padStart(2, '0') 77 | 78 | const start = this.item.getStartTime() * 1000 79 | const now = new Date().getTime() 80 | const time = +((now - start) / 1000).toFixed(2) 81 | 82 | if (time < 60) 83 | return `${time}秒` 84 | if (time / 60 < 60) 85 | return `${int(time / 60)}分${pad(int(time % 60))}秒` 86 | return `${int(time / 60 / 60)}时${pad(int(time / 60))}分${pad(int(time % 60))}秒` 87 | } 88 | 89 | /** 90 | * 描述 启动下载 91 | * @date 2022-03-18 92 | * @returns DownloadDetails 93 | */ 94 | start(): Promise { 95 | return new Promise((resolve) => { 96 | this.win.webContents.downloadURL(this.options.url) 97 | 98 | this.win.webContents.session.on('will-download', async (event, item) => { 99 | // console.log('this.options.isShowSaveDialog: ', this.options.isShowSaveDialog) 100 | if (this.options.isShowSaveDialog) { 101 | item.setSaveDialogOptions({ title: '请选择文件下载路径' }) 102 | } 103 | else { 104 | const filePath = join(this.options.savePath as string, this.options.filename) 105 | item.setSavePath(filePath) 106 | } 107 | 108 | this.downloadItem = item 109 | downloadItemMap.set(this.options.eventKey as string, item) 110 | 111 | await this.hanleStatus() 112 | 113 | resolve(this.getDtails(this.status)) 114 | }) 115 | }) 116 | } 117 | 118 | /** 119 | * 描述 监控下载状态 120 | * @date 2022-03-18 121 | * @returns {any} 122 | */ 123 | private hanleStatus() { 124 | let num = 0 125 | return new Promise((resolve) => { 126 | this.item 127 | .on('updated', (event, state) => { 128 | switch (state) { 129 | case 'interrupted': 130 | this.statusChange(state, '下载中断,可以恢复') 131 | if (this.item.canResume()) { 132 | if (num === 3) 133 | return resolve(this.status) 134 | num++ 135 | this.item.resume() 136 | } 137 | break 138 | case 'progressing': 139 | if (this.downloadItem.isPaused()) { 140 | this.statusChange(state, '暂停下载') 141 | } 142 | else { 143 | const current = this.downloadItem.getReceivedBytes() 144 | const total = this.downloadItem.getTotalBytes() 145 | const progress = +((current / total) * 100).toFixed(0) 146 | 147 | this.statusChange(state, '当前进度', progress) 148 | if (this.options.isSendProgress) 149 | this.win.webContents.send(`download-progressing-${this.options.eventKey}`, progress) 150 | } 151 | break 152 | } 153 | }) 154 | .once('done', (event, state) => { 155 | switch (state) { 156 | case 'completed': 157 | this.statusChange(state, '下载成功') 158 | downloadItemMap.delete(this.options.eventKey as string) 159 | break 160 | case 'cancelled': 161 | this.statusChange(state, '下载取消') 162 | break 163 | case 'interrupted': 164 | this.statusChange(state, '下载中断,无法恢复') 165 | downloadItemMap.delete(this.options.eventKey as string) 166 | break 167 | } 168 | resolve(this.status) 169 | }) 170 | }) 171 | } 172 | } 173 | -------------------------------------------------------------------------------- /src/main/tools/index.ts: -------------------------------------------------------------------------------- 1 | export * from './electron/download' 2 | 3 | export * from './utils/mainEnv' 4 | export * from './utils/fileUtils' 5 | -------------------------------------------------------------------------------- /src/main/tools/utils/ffi.ts: -------------------------------------------------------------------------------- 1 | import { join } from 'path' 2 | import { Library } from 'ffi-napi' 3 | import { libPath } from '~/config' 4 | 5 | const lib = Library(join(libPath, `dll_test_${process.platform}_${process.arch}.dll`), { 6 | add: ['int', ['int', 'int']] 7 | }) 8 | 9 | // add (a: number, b: number) => (a + b) 10 | 11 | console.log(lib.add(1, 2)) 12 | // https://github.com/node-ffi/node-ffi/wiki/Node-FFI-Tutorial 13 | -------------------------------------------------------------------------------- /src/main/tools/utils/fileUtils.ts: -------------------------------------------------------------------------------- 1 | import type { 2 | Stats 3 | } from 'fs' 4 | import { 5 | access, 6 | createReadStream, 7 | createWriteStream, 8 | existsSync, 9 | mkdir, 10 | readFile, 11 | readdir, 12 | readdirSync, 13 | rename, 14 | rmdirSync, 15 | stat, 16 | statSync, 17 | unlink, 18 | unlinkSync, 19 | writeFile 20 | } from 'fs' 21 | import ElectronLog from 'electron-log' 22 | import axios from 'axios' 23 | 24 | export class FileUtils { 25 | // isBuffer: boolean 26 | // isPath: boolean 27 | // buffer: Buffer 28 | // path: string 29 | 30 | // static fromBase64(base64: string) { 31 | // const fileUtils = new FileUtils() 32 | // fileUtils.buffer = Buffer.from(base64, 'base64') 33 | // return fileUtils 34 | // } 35 | 36 | // static async fromPath(path: string) { 37 | // if (await this.access(path)) { 38 | // const fileUtils = new FileUtils() 39 | // fileUtils.path = path 40 | // return fileUtils 41 | // } 42 | // } 43 | 44 | static async downloadFromHttp(sourcePath: string, targetPath: string): Promise { 45 | return await new Promise((resolve) => { 46 | axios({ 47 | method: 'get', 48 | url: sourcePath, 49 | responseType: 'stream' 50 | }) 51 | .then((response) => { 52 | try { 53 | response.data.pipe(createWriteStream(targetPath)).on('end', async () => { 54 | resolve(await this.access(targetPath)) 55 | }) 56 | } 57 | catch (err) { 58 | this.error('downloadFromHttp stream err', err) 59 | resolve(false) 60 | } 61 | }) 62 | .catch((err) => { 63 | this.error('downloadFromHttp err', err) 64 | resolve(false) 65 | }) 66 | }) 67 | } 68 | 69 | static readFile(filePath: string, options = {}): Promise { 70 | return new Promise((resolve) => { 71 | readFile(filePath, options, (err, data) => { 72 | if (err) { 73 | this.error('FileUtils readFile', err) 74 | resolve(null) 75 | } 76 | else { 77 | resolve(data) 78 | } 79 | }) 80 | }) 81 | } 82 | 83 | static writeFile(filePath: string, buffer: any, options = {}): Promise { 84 | return new Promise((resolve) => { 85 | writeFile(filePath, buffer, options, (err) => { 86 | if (err) 87 | this.error('FileUtils writeFile', err) 88 | resolve(!err) 89 | }) 90 | }) 91 | } 92 | 93 | static mkdir(filePath: string, options = {}): Promise { 94 | return new Promise((resolve) => { 95 | mkdir(filePath, options, (err) => { 96 | if (err) 97 | this.error('FileUtils mkdir', err) 98 | resolve(!err) 99 | }) 100 | }) 101 | } 102 | 103 | static unlink(filePath: string): Promise { 104 | return new Promise((resolve) => { 105 | unlink(filePath, (err) => { 106 | if (err) 107 | this.error('FileUtils unlink', err) 108 | resolve(!err) 109 | }) 110 | }) 111 | } 112 | 113 | static copyFile(sourcePath: string, targetPath: string): Promise { 114 | return new Promise((resolve) => { 115 | let error = false 116 | const source = createReadStream(sourcePath) 117 | const target = createWriteStream(targetPath) 118 | source.on('error', () => { 119 | error = true 120 | }) 121 | target 122 | .on('error', () => { 123 | error = true 124 | }) 125 | .on('close', () => { 126 | const exists = existsSync(targetPath) 127 | if (error) { 128 | if (exists) 129 | unlinkSync(targetPath) 130 | } 131 | resolve(!error && exists) 132 | }) 133 | source.pipe(target) 134 | }) 135 | } 136 | 137 | static rename(oldPath: string, newPath: string) { 138 | return new Promise((resolve) => { 139 | rename(oldPath, newPath, (err) => { 140 | if (err) 141 | this.error('FileUtils rename', err) 142 | resolve(!err) 143 | }) 144 | }) 145 | } 146 | 147 | static access(filePath: string): Promise { 148 | return new Promise((resolve) => { 149 | access(filePath, (err) => { 150 | // if (err) this.error('FileUtils access', err) 151 | resolve(!err) 152 | }) 153 | }) 154 | } 155 | 156 | static existsSync(filePath: string) { 157 | return existsSync(filePath) 158 | } 159 | 160 | static readdir(filePath: string): Promise> { 161 | return new Promise((resolve) => { 162 | readdir(filePath, (err, files) => { 163 | if (err) { 164 | this.error('FileUtils readdir', err) 165 | resolve([]) 166 | } 167 | else { 168 | resolve(files) 169 | } 170 | }) 171 | }) 172 | } 173 | 174 | static stat(filePath: string): Promise { 175 | return new Promise((resolve) => { 176 | stat(filePath, (err, stats) => { 177 | if (err) { 178 | this.error('FileUtils stat', err) 179 | resolve(false) 180 | } 181 | else { 182 | resolve(stats) 183 | } 184 | }) 185 | }) 186 | } 187 | 188 | static rmRf(filePath: string) { 189 | if (existsSync(filePath)) { 190 | readdirSync(filePath).forEach((file) => { 191 | const curPath = `${filePath}/${file}` 192 | if (statSync(curPath).isDirectory()) 193 | this.rmRf(curPath) 194 | else 195 | unlinkSync(curPath) 196 | }) 197 | rmdirSync(filePath) 198 | } 199 | } 200 | 201 | private static error(...data: any[]) { 202 | ElectronLog.error(...data) 203 | } 204 | } 205 | -------------------------------------------------------------------------------- /src/main/tools/utils/mainEnv.ts: -------------------------------------------------------------------------------- 1 | export const mainDevExecFn = (callback: () => void) => process.env.NODE_ENV === 'development' && callback() 2 | 3 | export const mainProExecFn = (callback: () => void) => process.env.NODE_ENV === 'production' && callback() 4 | 5 | export const getMainEnv = (callback: (env: NodeJS.ProcessEnv) => void) => callback(process.env) 6 | -------------------------------------------------------------------------------- /src/main/typeorm/data-source.ts: -------------------------------------------------------------------------------- 1 | import 'reflect-metadata' 2 | import { DataSource } from 'typeorm' 3 | import { User } from './entity' 4 | 5 | export const AppDataSource = new DataSource({ 6 | type: 'sqlite', 7 | database: 'database.sqlite', 8 | synchronize: true, 9 | logging: false, 10 | entities: [User], 11 | migrations: [], 12 | subscribers: [] 13 | }) 14 | -------------------------------------------------------------------------------- /src/main/typeorm/entity/User.ts: -------------------------------------------------------------------------------- 1 | import { BaseEntity, Column, Entity, PrimaryGeneratedColumn } from 'typeorm' 2 | 3 | @Entity() 4 | export class User extends BaseEntity { 5 | @PrimaryGeneratedColumn() 6 | id: number 7 | 8 | @Column('text', { nullable: true }) 9 | name: string 10 | 11 | @Column('boolean') 12 | sex: boolean 13 | 14 | @Column('int') 15 | age: number 16 | } 17 | -------------------------------------------------------------------------------- /src/main/typeorm/entity/index.ts: -------------------------------------------------------------------------------- 1 | export * from './User' 2 | -------------------------------------------------------------------------------- /src/main/typeorm/index.ts: -------------------------------------------------------------------------------- 1 | import { AppDataSource } from './data-source' 2 | 3 | export default function initTypeorm() { 4 | AppDataSource.initialize().catch(error => global.console.log(error)) 5 | } 6 | -------------------------------------------------------------------------------- /src/main/typeorm/manager/index.ts: -------------------------------------------------------------------------------- 1 | export * from './user' 2 | -------------------------------------------------------------------------------- /src/main/typeorm/manager/user.ts: -------------------------------------------------------------------------------- 1 | import { User } from '../entity' 2 | 3 | export const searchAll = () => User.find() 4 | 5 | export const addSingle = (state: Sql.User) => { 6 | const { name, sex, age } = state 7 | const user = new User() 8 | user.name = name 9 | user.sex = sex 10 | user.age = age 11 | 12 | return user.save() 13 | } 14 | -------------------------------------------------------------------------------- /src/main/window/common.ts: -------------------------------------------------------------------------------- 1 | import { WinKey } from '@enums/window' 2 | import { getWin, hasWin, onMounted } from './create/win.map' 3 | import { CreateWindow } from './create/CreateWindow' 4 | 5 | export function createMainWindow() { 6 | if (hasWin(WinKey.MAIN)) 7 | return 8 | 9 | const main = new CreateWindow(WinKey.MAIN) 10 | .setTitle('主窗口') 11 | .setSize(768, 560, true) 12 | .loadURL() 13 | .hideMenu() 14 | .openDevTools() 15 | .show() 16 | .unClose() 17 | 18 | onMounted(main.winKey, async () => { 19 | getWin(WinKey.LOADING)?.destroy() 20 | // const webview = new CreateWebView(WinKey.WEBVIEW, { frame: true }) 21 | // .setSize(1080, 720, true) 22 | // .loadURL('https://www.electronjs.org/zh/docs/latest/api/browser-view') 23 | // .loadURL('http://nodejs.cn/learn') 24 | // .loadURL('https://staging-cn.vuejs.org/') 25 | }) 26 | } 27 | 28 | export function createLoadingWindow() { 29 | const loading = new CreateWindow(WinKey.LOADING, { transparent: true }) 30 | .setTitle('加载页') 31 | .setSize(480, 500) 32 | .loadURL('loading') 33 | .show() 34 | 35 | onMounted(loading.winKey, () => { 36 | setTimeout(() => { 37 | createMainWindow() 38 | }, 2000) 39 | }) 40 | } 41 | -------------------------------------------------------------------------------- /src/main/window/create/CreateWebView.ts: -------------------------------------------------------------------------------- 1 | import { BrowserView, BrowserWindow, dialog, nativeImage, shell } from 'electron' 2 | import type { WinKey } from '@enums/window' 3 | import axios from 'axios' 4 | import { addWin, delWin, focusChange, showChange } from './win.map' 5 | import { appIcon, mainPreload, printInfo, trayIcon } from '~/config/index' 6 | import { getMainEnv } from '~/tools/index' 7 | 8 | /** 9 | * 描述 创建窗口(不可重复) 10 | * 提供基础预置配置及回收和错误监听 11 | * 销毁后可重新创建 始终保持同一窗口只有一个 12 | * @date 2022-03-08 13 | * @param {any} key:WinKey 14 | * @param {any} options:Electron.BrowserWindowConstructorOptions={} 15 | * @returns {any} 16 | */ 17 | export class CreateWebView { 18 | win: BrowserWindow 19 | view: BrowserView 20 | winKey: WinKey 21 | isViewReady: boolean 22 | 23 | constructor(key: WinKey, options: Electron.BrowserWindowConstructorOptions = {}) { 24 | this.winKey = key 25 | 26 | const { webPreferences: preferences, ...option } = options 27 | 28 | this.win = new BrowserWindow({ 29 | frame: false, 30 | useContentSize: true, 31 | paintWhenInitiallyHidden: false, 32 | ...option, 33 | webPreferences: { 34 | preload: mainPreload, 35 | // 预加载选项 36 | ...preferences, 37 | // 允许跨域 38 | webSecurity: false, 39 | // 在macos中启用橡皮动画 40 | scrollBounce: process.platform === 'darwin' 41 | } 42 | }) 43 | 44 | this.win.setMenuBarVisibility(false) 45 | 46 | getMainEnv(env => this.win.setIcon(env.NODE_ENV ? appIcon : trayIcon)) 47 | 48 | addWin(this.winKey, this.win.id) 49 | 50 | this.view = new BrowserView({ 51 | webPreferences: { 52 | preload: mainPreload, 53 | // 允许跨域 54 | webSecurity: false, 55 | // 在macos中启用橡皮动画 56 | scrollBounce: process.platform === 'darwin' 57 | } 58 | }) 59 | 60 | this.win.setBrowserView(this.view) 61 | 62 | this.view.setAutoResize({ 63 | width: true, 64 | height: true, 65 | horizontal: true, 66 | vertical: true 67 | }) 68 | this.view.setBounds({ x: 0, y: 0, width: 960, height: 600 }) 69 | 70 | this.win.setTitle(this.view.webContents.getTitle()) 71 | 72 | this.webContents.on('did-finish-load', () => { 73 | this.win.setTitle(this.view.webContents.getTitle()) 74 | }) 75 | 76 | this.webContents.on('did-navigate-in-page', () => { 77 | setTimeout(() => { 78 | this.win.setTitle(this.view.webContents.getTitle()) 79 | }, 100) 80 | }) 81 | 82 | this.webContents.on('page-favicon-updated', async (event, [faviconURL]) => { 83 | const res = await axios.get(faviconURL, { responseType: 'arraybuffer' }) 84 | 85 | const icon = nativeImage.createFromBuffer(Buffer.from(res.data), { 86 | width: 128, 87 | height: 128 88 | }) 89 | 90 | this.win.setIcon(icon) 91 | }) 92 | 93 | this.onClosed() 94 | this.unresponsive() 95 | this.preloadError() 96 | this.handleStatus() 97 | } 98 | 99 | get webContents() { 100 | return this.view.webContents 101 | } 102 | 103 | loadURL(url = '') { 104 | this.view.webContents.loadURL(url) 105 | return this 106 | } 107 | 108 | setSize(width: number, height: number, isResizable = false) { 109 | this.win.setSize(width, height) 110 | this.win.setMinimumSize(width, height) 111 | this.win.setResizable(isResizable) 112 | this.view.setBounds({ x: 0, y: 0, width, height }) 113 | return this 114 | } 115 | 116 | hideTaskbar() { 117 | this.win.setSkipTaskbar(true) 118 | return this 119 | } 120 | 121 | alwaysOnTop() { 122 | this.win.setAlwaysOnTop(true) 123 | return this 124 | } 125 | 126 | // 阻止窗口销毁使窗口隐藏 127 | unClose() { 128 | this.win.on('close', (event) => { 129 | event.preventDefault() 130 | this.win.hide() 131 | }) 132 | return this 133 | } 134 | 135 | listen(eventName: string, callback: () => void) { 136 | this.win.on(eventName as any, callback) 137 | return this 138 | } 139 | 140 | // 外链打开url 141 | openExternal() { 142 | this.webContents.setWindowOpenHandler((event) => { 143 | shell.openExternal(event.url) 144 | return { action: 'deny' } 145 | }) 146 | return this 147 | } 148 | 149 | // 窗口异常 150 | private unresponsive() { 151 | // 网页变得未响应时触发 152 | this.win.on('unresponsive', () => { 153 | printInfo('error', '网页未响应') 154 | 155 | dialog 156 | .showMessageBox(this.win, { 157 | type: 'warning', 158 | title: '警告', 159 | buttons: ['重载', '退出'], 160 | message: '图形化进程失去响应,是否等待其恢复?', 161 | noLink: true 162 | }) 163 | .then((res) => { 164 | if (res.response === 0) 165 | this.win.reload() 166 | else this.win.close() 167 | }) 168 | }) 169 | } 170 | 171 | // 当预加载脚本mainPreload抛出一个未处理的异常错误时触发。 172 | private preloadError() { 173 | this.win.webContents.on('preload-error', (event, mainPreload, err) => { 174 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 event ', event) 175 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 ', mainPreload) 176 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 err ', err) 177 | }) 178 | } 179 | 180 | private handleStatus() { 181 | this.win.on('show', () => showChange(this.winKey, true)) 182 | 183 | this.win.on('hide', () => showChange(this.winKey, false)) 184 | 185 | this.win.on('focus', () => focusChange(this.winKey, true)) 186 | 187 | this.win.on('blur', () => focusChange(this.winKey, false)) 188 | 189 | this.win.on('minimize', () => { 190 | focusChange(this.winKey, false) 191 | showChange(this.winKey, false) 192 | }) 193 | 194 | this.win.on('restore', () => { 195 | focusChange(this.winKey, true) 196 | showChange(this.winKey, true) 197 | }) 198 | } 199 | 200 | private onClosed() { 201 | this.win.on('closed', () => { 202 | delWin(this.winKey) 203 | printInfo('info', `${this.winKey} 窗口已关闭`) 204 | }) 205 | } 206 | } 207 | -------------------------------------------------------------------------------- /src/main/window/create/CreateWindow.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow, dialog, shell } from 'electron' 2 | import type { WinKey } from '@enums/window' 3 | import preload from '@quiteer/electron-preload' 4 | import { addWin, delWin, focusChange, onMounted, showChange } from './win.map' 5 | import { appIcon, printInfo, trayIcon, winURL } from '~/config/index' 6 | import { getMainEnv, mainDevExecFn, mainProExecFn } from '~/tools/index' 7 | 8 | const existWins = new Map() 9 | 10 | /** 11 | * 描述 创建窗口(不可重复) 12 | * 提供基础预置配置及回收和错误监听 13 | * 销毁后可重新创建 始终保持同一窗口只有一个 14 | * @date 2022-03-08 15 | * @param {any} key:WinKey 16 | * @param {any} options:Electron.BrowserWindowConstructorOptions={} 17 | * @returns {any} 18 | */ 19 | export class CreateWindow { 20 | win: BrowserWindow 21 | winKey: WinKey 22 | 23 | constructor(key: WinKey, options: Electron.BrowserWindowConstructorOptions = {}) { 24 | if (existWins.has(key)) { 25 | existWins.get(key) 26 | } 27 | else { 28 | existWins.set(key, this) 29 | 30 | this.winKey = key 31 | 32 | const { webPreferences: preferences, ...option } = options 33 | 34 | this.win = new BrowserWindow({ 35 | show: false, 36 | frame: false, 37 | useContentSize: true, 38 | paintWhenInitiallyHidden: false, 39 | ...option, 40 | webPreferences: { 41 | preload: preload as string, 42 | sandbox: false, 43 | // 预加载选项 44 | ...preferences, 45 | // 允许跨域 46 | webSecurity: false, 47 | // 在macos中启用橡皮动画 48 | scrollBounce: process.platform === 'darwin' 49 | } 50 | }) 51 | 52 | getMainEnv(env => this.win.setIcon(env.NODE_ENV ? appIcon : trayIcon)) 53 | 54 | addWin(this.winKey, this.win.id) 55 | 56 | this.#onClosed() 57 | this.#unresponsive() 58 | this.#preloadError() 59 | this.#handleStatus() 60 | } 61 | } 62 | 63 | get webContents() { 64 | return this.win.webContents 65 | } 66 | 67 | setTitle(title = '') { 68 | this.win.setTitle(title) 69 | return this 70 | } 71 | 72 | setSize(width: number, height: number, isResizable = false) { 73 | this.win.setSize(width, height) 74 | this.win.setMinimumSize(width, height) 75 | this.win.setResizable(isResizable) 76 | 77 | return this 78 | } 79 | 80 | hideMenu() { 81 | this.win.setMenuBarVisibility(false) 82 | return this 83 | } 84 | 85 | hideTaskbar() { 86 | this.win.setSkipTaskbar(true) 87 | return this 88 | } 89 | 90 | alwaysOnTop() { 91 | this.win.setAlwaysOnTop(true) 92 | return this 93 | } 94 | 95 | loadURL(path = '') { 96 | this.win.loadURL(`${winURL}#/${path}`) 97 | 98 | return this 99 | } 100 | 101 | setUserAgent() { 102 | mainProExecFn(() => { 103 | let userAgent = this.win.webContents.getUserAgent() 104 | userAgent = userAgent.replace(/([^u4e00-\u9FA5])/g, $ => encodeURIComponent($)) 105 | this.win.webContents.setUserAgent(userAgent) 106 | }) 107 | 108 | return this 109 | } 110 | 111 | openDevTools() { 112 | mainDevExecFn(() => { 113 | this.win.webContents.openDevTools({ 114 | mode: 'undocked', 115 | activate: true 116 | }) 117 | }) 118 | 119 | return this 120 | } 121 | 122 | // 居中显示 123 | show() { 124 | onMounted(this.winKey, () => { 125 | this.win.center() 126 | this.win.show() 127 | }) 128 | 129 | return this 130 | } 131 | 132 | // 阻止窗口销毁使窗口隐藏 133 | unClose() { 134 | this.win.on('close', (event) => { 135 | event.preventDefault() 136 | this.win.hide() 137 | }) 138 | return this 139 | } 140 | 141 | listen(event: any, listen: () => void) { 142 | this.win.on(event, listen) 143 | return this 144 | } 145 | 146 | // 外链打开url 147 | openExternal() { 148 | this.webContents.setWindowOpenHandler((event) => { 149 | shell.openExternal(event.url) 150 | return { action: 'deny' } 151 | }) 152 | return this 153 | } 154 | 155 | // 窗口异常 156 | #unresponsive() { 157 | // 网页变得未响应时触发 158 | this.win.on('unresponsive', () => { 159 | printInfo('error', '网页未响应') 160 | 161 | dialog 162 | .showMessageBox(this.win, { 163 | type: 'warning', 164 | title: '警告', 165 | buttons: ['重载', '退出'], 166 | message: '图形化进程失去响应,是否等待其恢复?', 167 | noLink: true 168 | }) 169 | .then((res) => { 170 | if (res.response === 0) 171 | this.win.reload() 172 | else this.win.close() 173 | }) 174 | }) 175 | } 176 | 177 | // 当预加载脚本mainPreload抛出一个未处理的异常错误时触发。 178 | #preloadError() { 179 | this.win.webContents.on('preload-error', (event, mainPreload, err) => { 180 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 event ', event) 181 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 mainPreload ', mainPreload) 182 | printInfo('error', '预加载脚本抛出一个未处理的异常错误 err ', err) 183 | }) 184 | } 185 | 186 | #handleStatus() { 187 | this.win.on('show', () => showChange(this.winKey, true)) 188 | 189 | this.win.on('hide', () => showChange(this.winKey, false)) 190 | 191 | this.win.on('focus', () => focusChange(this.winKey, true)) 192 | 193 | this.win.on('blur', () => focusChange(this.winKey, false)) 194 | 195 | this.win.on('minimize', () => { 196 | focusChange(this.winKey, false) 197 | showChange(this.winKey, false) 198 | }) 199 | 200 | this.win.on('restore', () => { 201 | focusChange(this.winKey, true) 202 | showChange(this.winKey, true) 203 | }) 204 | } 205 | 206 | #onClosed() { 207 | this.win.on('closed', () => { 208 | existWins.delete(this.winKey) 209 | delWin(this.winKey) 210 | printInfo('info', `${this.winKey} 窗口已关闭`) 211 | }) 212 | } 213 | } 214 | -------------------------------------------------------------------------------- /src/main/window/create/win.map.ts: -------------------------------------------------------------------------------- 1 | import { BrowserWindow } from 'electron' 2 | import type { WinKey } from '@enums/window' 3 | 4 | export const winMap = new Map() 5 | 6 | export const addWin = (key: WinKey, winId: number) => { 7 | winMap.set(key, { 8 | id: winId, 9 | name: key, 10 | isCreate: true, 11 | isRead: false, 12 | isShow: false, 13 | isFocus: false 14 | }) 15 | } 16 | 17 | export const getWin = (key: WinKey): BrowserWindow | null => { 18 | const status = winMap.get(key) 19 | if (!status) 20 | return null 21 | return BrowserWindow.fromId(status.id) 22 | } 23 | 24 | export const delWin = (key: WinKey) => winMap.delete(key) 25 | 26 | export const hasWin = (key: WinKey): boolean => winMap.has(key) 27 | 28 | export const winRead = (winId: number): Wicket.WinStatus | undefined => { 29 | let state: Wicket.WinStatus | undefined 30 | 31 | for (const [key, { id }] of winMap.entries()) { 32 | if (id === winId) 33 | state = winMap.get(key) 34 | } 35 | 36 | if (!state) 37 | return 38 | 39 | state.isRead = true 40 | winMap.set(state.name, state) 41 | 42 | return state 43 | } 44 | 45 | export const showChange = (key: WinKey, flag: boolean) => { 46 | const state = winMap.get(key) 47 | if (state) { 48 | state.isShow = flag 49 | winMap.set(key, state) 50 | } 51 | } 52 | 53 | export const focusChange = (key: WinKey, flag: boolean) => { 54 | const state = winMap.get(key) 55 | if (state) { 56 | state.isFocus = flag 57 | winMap.set(key, state) 58 | } 59 | } 60 | 61 | export const onMounted = (key: WinKey, callback: () => void) => { 62 | const status = winMap.get(key) 63 | 64 | if (!status?.isCreate) 65 | return 66 | const id: NodeJS.Timer = setInterval(() => { 67 | if (status.isRead) { 68 | callback() 69 | clearInterval(id) 70 | } 71 | }, 200) 72 | } 73 | -------------------------------------------------------------------------------- /src/main/window/index.ts: -------------------------------------------------------------------------------- 1 | export * from './common' 2 | 3 | export * from './create/CreateWebView' 4 | export * from './create/CreateWindow' 5 | export * from './create/win.map' 6 | -------------------------------------------------------------------------------- /src/preload/main/index.ts: -------------------------------------------------------------------------------- 1 | import { contextBridge, ipcRenderer } from 'electron' 2 | 3 | type IpcParameters = Parameters 4 | type IpcReturnType = ReturnType 5 | 6 | contextBridge.exposeInMainWorld('$ipc', { 7 | send: (...args: IpcParameters<'send'>): IpcReturnType<'send'> => ipcRenderer.send(...args), 8 | sendSync: (...args: IpcParameters<'sendSync'>): IpcReturnType<'sendSync'> => ipcRenderer.sendSync(...args), 9 | invoke: (...args: IpcParameters<'invoke'>): IpcReturnType<'invoke'> => ipcRenderer.invoke(...args), 10 | on: (...args: IpcParameters<'on'>): IpcReturnType<'on'> => ipcRenderer.on(...args), 11 | once: (...args: IpcParameters<'once'>): IpcReturnType<'once'> => ipcRenderer.once(...args), 12 | removeAllListeners: (...args: IpcParameters<'removeAllListeners'>): IpcReturnType<'removeAllListeners'> => 13 | ipcRenderer.removeAllListeners(...args) 14 | }) 15 | -------------------------------------------------------------------------------- /src/renderer/App.tsx: -------------------------------------------------------------------------------- 1 | import { dateZhCN, zhCN } from 'naive-ui' 2 | import { RouterView } from 'vue-router' 3 | import { useThemeStore } from '@/store' 4 | import { subTheme } from '@/store/theme/subTheme' 5 | 6 | export default defineComponent({ 7 | setup() { 8 | const theme = useThemeStore() 9 | 10 | subTheme() 11 | 12 | return () => ( 13 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | ) 30 | } 31 | }) 32 | -------------------------------------------------------------------------------- /src/renderer/api/demo/test.ts: -------------------------------------------------------------------------------- 1 | import { request } from '@/utils/index' 2 | 3 | export const testApi = () => request.get('/test') 4 | -------------------------------------------------------------------------------- /src/renderer/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from './demo/test' 2 | -------------------------------------------------------------------------------- /src/renderer/assets/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/src/renderer/assets/favicon.ico -------------------------------------------------------------------------------- /src/renderer/assets/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/QuiteerJs/electron-vue3-quiet/c939a56e97115803d45e91ab0662eb91c047e535/src/renderer/assets/logo.png -------------------------------------------------------------------------------- /src/renderer/assets/svg/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/renderer/components/CardGroup/index.tsx: -------------------------------------------------------------------------------- 1 | import type { PropType } from 'vue' 2 | 3 | const props = { 4 | cardList: { 5 | type: Array as PropType, 6 | default: () => [] 7 | } 8 | } 9 | 10 | export default defineComponent({ 11 | name: 'CardGroup', 12 | props, 13 | setup(props) { 14 | const { cardList } = props 15 | 16 | const openNewWindow = (item: Component.CardState) => { 17 | window.$ipc.send('main-open', toRaw(item)) 18 | } 19 | 20 | return () => ( 21 |
22 | {cardList.map((item) => { 23 | return ( 24 | openNewWindow(item)} 29 | > 30 | {item.content} 31 | 32 | ) 33 | })} 34 |
35 | ) 36 | } 37 | }) 38 | -------------------------------------------------------------------------------- /src/renderer/components/CardGroup/index.vue: -------------------------------------------------------------------------------- 1 | 32 | -------------------------------------------------------------------------------- /src/renderer/components/IpcOnMounted/index.tsx: -------------------------------------------------------------------------------- 1 | import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui' 2 | 3 | export default defineComponent({ 4 | name: 'IpcOnMounted', 5 | setup(props, { expose }) { 6 | window.$message = useMessage() 7 | window.$dialog = useDialog() 8 | window.$notification = useNotification() 9 | window.$loadingBar = useLoadingBar() 10 | 11 | onMounted(() => { 12 | window.$ipc.send('dom-mounted') 13 | }) 14 | 15 | const winStatus = (): Promise => window.$ipc.invoke('get-win-status') 16 | 17 | expose({ winStatus }) 18 | 19 | return { winStatus } 20 | } 21 | }) 22 | -------------------------------------------------------------------------------- /src/renderer/directive/index.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from 'vue' 2 | 3 | type Model = ImportMetaGlob<{ directive: Directive; name: string }> 4 | 5 | const modules = import.meta.glob('./modules/*.ts', { eager: true }) 6 | 7 | export default { 8 | install(app: App) { 9 | Object.values(modules).forEach((item) => { 10 | const model = item as Model 11 | app.directive(model.default.name, model.default.directive) 12 | }) 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/directive/modules/demo.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | 3 | const directive: Directive = { 4 | // 在绑定元素的 attribute 前调用 5 | // 或事件监听器应用前调用 6 | created(el, binding, vnode, prevVnode) { 7 | el.style.color = 'red' 8 | const { arg, modifiers, value } = binding 9 | console.log('created ', { el, binding, vnode, prevVnode }) 10 | console.log('{ arg,modifiers,value }: ', { arg, modifiers, value }) 11 | console.log(typeof value) 12 | }, 13 | // 在元素被插入到 DOM 前调用 14 | beforeMount() { 15 | // console.log('beforeMount ', { el, binding, vnode, prevVnode }) 16 | }, 17 | // 在绑定元素的父组件 18 | // 及他自己的所有子节点都 挂载 完成后调用 19 | mounted() { 20 | // console.log('mounted ', { el, binding, vnode, prevVnode }) 21 | }, 22 | // 绑定元素的父组件更新前调用 23 | beforeUpdate() { 24 | // console.log('beforeUpdate ', { el, binding, vnode, prevVnode }) 25 | }, 26 | // 在绑定元素的父组件 27 | // 及他自己的所有子节点都 更新 完成后调用 28 | updated() { 29 | // console.log('updated ', { el, binding, vnode, prevVnode }) 30 | }, 31 | // 绑定元素的父组件卸载之前调用 32 | beforeUnmount() { 33 | // console.log('beforeUnmount ', { el, binding, vnode, prevVnode }) 34 | }, 35 | // 绑定元素的父组件卸载之后调用 36 | unmounted() { 37 | // console.log('unmounted ', { el, binding, vnode, prevVnode }) 38 | } 39 | } 40 | 41 | export default { 42 | name: 'demo', 43 | directive 44 | } 45 | -------------------------------------------------------------------------------- /src/renderer/directive/modules/ellipsis.ts: -------------------------------------------------------------------------------- 1 | import type { Directive } from 'vue' 2 | 3 | const directive: Directive = (el, binding) => { 4 | el.style.overflow = 'hidden' 5 | el.style.textOverflow = 'ellipsis' 6 | el.style.display = '-webkit-box' 7 | el.style['-webkit-line-clamp'] = binding.value || 1 8 | el.style['-webkit-box-orient'] = 'vertical' 9 | } 10 | 11 | export default { 12 | name: 'ellipsis', 13 | directive 14 | } 15 | -------------------------------------------------------------------------------- /src/renderer/directive/modules/intersecting.ts: -------------------------------------------------------------------------------- 1 | // v-intersecting:show 2 | // v-intersecting:hide 3 | import type { Directive } from 'vue' 4 | 5 | type Fn = (isIntersecting: boolean) => void 6 | 7 | interface Params { 8 | show?: Fn 9 | hide?: Fn 10 | } 11 | 12 | const directive: Directive = { 13 | mounted(el, { arg, value }) { 14 | const observer = new IntersectionObserver(([{ isIntersecting }]) => { 15 | if (!arg && typeof value === 'object') { 16 | isIntersecting && value.show && value.show(isIntersecting) 17 | isIntersecting || !value.hide || value.hide(isIntersecting) 18 | } 19 | 20 | if (arg && typeof value === 'function') { 21 | if (arg === 'show' && isIntersecting) 22 | value(isIntersecting) 23 | 24 | if (arg === 'hide' && !isIntersecting) 25 | value(isIntersecting) 26 | } 27 | }) 28 | observer.observe(el) 29 | } 30 | } 31 | 32 | export default { 33 | name: 'intersecting', 34 | directive 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules/useDownload' 2 | export * from './modules/useMainResize' 3 | export * from './modules/useCustomEvent' 4 | -------------------------------------------------------------------------------- /src/renderer/hooks/modules/useCustomEvent.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted } from 'vue' 2 | 3 | export function useCustomEvent() { 4 | const initiate = (eventName: string, options: T) => { 5 | dispatchEvent(new CustomEvent(eventName, { detail: options })) 6 | } 7 | 8 | const listener = (eventName: string, callback: (options?: T) => void) => { 9 | const listener = (event: CustomEvent) => { 10 | callback(event?.detail ?? event) 11 | } 12 | 13 | addEventListener(eventName as any, listener) 14 | 15 | onUnmounted(() => { 16 | removeEventListener(eventName as any, listener) 17 | }) 18 | } 19 | 20 | return { initiateCustomEvent: initiate, listenerCustomEvent: listener } 21 | } 22 | -------------------------------------------------------------------------------- /src/renderer/hooks/modules/useDownload.ts: -------------------------------------------------------------------------------- 1 | import { v4 as uuidv4 } from 'uuid' 2 | 3 | export function useDownload() { 4 | const eventKey = uuidv4() 5 | const eventName = `download-progressing-${eventKey}` 6 | 7 | const progress = ref(0) 8 | const info = ref() 9 | 10 | window.$ipc.on(eventName, (event, num: number | undefined) => { 11 | progress.value = num ?? 0 12 | }) 13 | 14 | const start = (optsions: Download.DownloadOptions): Promise => { 15 | return new Promise((resolve, reject) => { 16 | window.$ipc 17 | .invoke<['file', Download.DownloadOptions], Download.DownloadDetails>('download-option', 'file', { 18 | ...optsions, 19 | eventKey 20 | }) 21 | .then((data) => { 22 | if (data.isSuccess) 23 | window.$message.success(data.message) 24 | 25 | info.value = data 26 | window.$ipc.removeAllListeners(eventName) 27 | resolve(data) 28 | }) 29 | .catch(reject) 30 | }) 31 | } 32 | 33 | return { progress, downloadInfo: info, downloadStart: start } 34 | } 35 | -------------------------------------------------------------------------------- /src/renderer/hooks/modules/useMainResize.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onUnmounted, ref } from 'vue' 2 | 3 | // 窗口尺寸调整钩子 4 | export function useMainResize() { 5 | const height = ref(document.body.clientHeight) 6 | const width = ref(document.body.clientWidth) 7 | 8 | const updateSize = () => { 9 | height.value = document.body.clientHeight 10 | width.value = document.body.clientWidth 11 | } 12 | 13 | onMounted(() => { 14 | window.addEventListener('resize', updateSize, { passive: true }) 15 | }) 16 | 17 | onUnmounted(() => { 18 | window.removeEventListener('resize', updateSize) 19 | }) 20 | 21 | return { height, width } 22 | } 23 | -------------------------------------------------------------------------------- /src/renderer/i18n/index.ts: -------------------------------------------------------------------------------- 1 | import { createI18n } from 'vue-i18n' 2 | 3 | type LangStr = Record 4 | 5 | interface I18nLang { 6 | en: LangStr 7 | zh: LangStr 8 | } 9 | 10 | type Model = ImportMetaGlob 11 | 12 | const modules = import.meta.glob('./modules/*.ts', { eager: true }) 13 | 14 | const messages = Object.values(modules).reduce( 15 | (pre: I18nLang, now) => { 16 | const lang = (now as Model).default 17 | return { 18 | en: { ...pre.en, ...lang.en }, 19 | zh: { ...pre.zh, ...lang.zh } 20 | } 21 | }, 22 | { en: {}, zh: {} } 23 | ) 24 | 25 | export default createI18n({ 26 | // 使用 Composition API 模式,则需要将其设置为false 27 | legacy: false, 28 | locale: 'zh', 29 | messages 30 | }) 31 | -------------------------------------------------------------------------------- /src/renderer/i18n/modules/demo.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | en: { 3 | demo: { 4 | test: 'hello demo' 5 | } 6 | }, 7 | zh: { 8 | demo: { 9 | test: '你好 德莫' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/i18n/modules/demo1.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | en: { 3 | demo1: { 4 | test: 'demo1' 5 | } 6 | }, 7 | zh: { 8 | demo1: { 9 | test: '德莫一' 10 | } 11 | } 12 | } 13 | -------------------------------------------------------------------------------- /src/renderer/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
10 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/layout/Content/Content.vue: -------------------------------------------------------------------------------- 1 | 2 | 3 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /src/renderer/layout/Content/index.ts: -------------------------------------------------------------------------------- 1 | import Content from './Content.vue' 2 | 3 | export { Content } 4 | -------------------------------------------------------------------------------- /src/renderer/layout/Header/Control/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 22 | 23 | 28 | -------------------------------------------------------------------------------- /src/renderer/layout/Header/Header.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 23 | 24 | 29 | -------------------------------------------------------------------------------- /src/renderer/layout/Header/index.ts: -------------------------------------------------------------------------------- 1 | import Header from './Header.vue' 2 | 3 | export { Header } 4 | -------------------------------------------------------------------------------- /src/renderer/layout/Sidebar/Menu/Menu.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 30 | -------------------------------------------------------------------------------- /src/renderer/layout/Sidebar/Sidebar.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | 14 | 27 | -------------------------------------------------------------------------------- /src/renderer/layout/Sidebar/index.ts: -------------------------------------------------------------------------------- 1 | import Sidebar from './Sidebar.vue' 2 | 3 | export { Sidebar } 4 | -------------------------------------------------------------------------------- /src/renderer/layout/index.tsx: -------------------------------------------------------------------------------- 1 | import { Content } from './Content' 2 | import { Sidebar } from './Sidebar' 3 | import { Header } from './Header' 4 | import IpcOnMounted from '@/components/IpcOnMounted' 5 | // import { testApi } from '@/api' 6 | 7 | export default defineComponent({ 8 | name: 'Layout', 9 | setup() { 10 | const win = ref | null>() 11 | 12 | onMounted(async () => { 13 | // const status = await win.value?.winStatus() 14 | // console.log('status :>> ', status) 15 | }) 16 | 17 | return () => ( 18 | <> 19 | 20 |
21 |
22 |
23 | 24 |
25 | 26 |
27 |
28 |
29 | 30 | ) 31 | } 32 | }) 33 | -------------------------------------------------------------------------------- /src/renderer/pages/Electron/index.tsx: -------------------------------------------------------------------------------- 1 | // import { Icon } from '@iconify/vue' 2 | import { WinKey } from '@enums/window' 3 | // import CardGroup from '@/components/CardGroup' 4 | import CardGroup from '@/components/CardGroup/index.vue' 5 | 6 | export default defineComponent({ 7 | name: 'Electron', 8 | setup() { 9 | const cardList = reactive([ 10 | { key: WinKey.PRINT, title: '打印', content: '使用electron的api打印', path: 'print-demo' }, 11 | { key: WinKey.DROP, title: '文件拖拽', content: 'window的api实现文件拖拽', path: 'drop-demo' }, 12 | { key: WinKey.DOWNLOAD, title: '下载', content: '使用electron的api下载', path: 'download-demo' }, 13 | { key: WinKey.SQL, title: '数据库操作', content: 'sqlite3在electron的应用', path: 'sql-demo' }, 14 | { key: WinKey.FONT, title: '系统字体', content: '获取系统字体', path: 'font-demo' } 15 | ]) 16 | return () => 17 | } 18 | }) 19 | -------------------------------------------------------------------------------- /src/renderer/pages/JavaScript/index.tsx: -------------------------------------------------------------------------------- 1 | // import { Icon } from '@iconify/vue' 2 | import { WinKey } from '@enums/window' 3 | import CardGroup from '@/components/CardGroup' 4 | 5 | export default defineComponent({ 6 | name: 'Electron', 7 | setup() { 8 | const cardList = reactive([ 9 | { key: WinKey.PRINT, title: '打印', content: '使用electron的api打印', path: 'print-demo' }, 10 | { key: WinKey.DROP, title: '文件拖拽', content: 'window的api实现文件拖拽', path: 'drop-demo' }, 11 | { key: WinKey.DOWNLOAD, title: '下载', content: '使用electron的api下载', path: 'download-demo' }, 12 | { key: WinKey.SQL, title: '数据库操作', content: 'sqlite3在electron的应用', path: 'sql-demo' } 13 | ]) 14 | return () => 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/renderer/pages/NotFound.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | 12 | 13 | -------------------------------------------------------------------------------- /src/renderer/pages/Test/i18n.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 21 | -------------------------------------------------------------------------------- /src/renderer/pages/Test/index.tsx: -------------------------------------------------------------------------------- 1 | import { RouterLink, RouterView } from 'vue-router' 2 | import { useDialog, useLoadingBar, useMessage, useNotification } from 'naive-ui' 3 | import TestTsx from './testTsx' 4 | import IpcOnMounted from '@/components/IpcOnMounted' 5 | // import { getRenderEnv } from '@/utils' 6 | import { useDemoStore } from '@/store' 7 | 8 | export default defineComponent({ 9 | setup() { 10 | window.$message = useMessage() 11 | window.$dialog = useDialog() 12 | window.$notification = useNotification() 13 | window.$loadingBar = useLoadingBar() 14 | // const router = useRouter() 15 | // const route = useRoute() 16 | const demoStore = useDemoStore() 17 | demoStore.actionDemo('德莫') 18 | 19 | const testTsx = ref>() 20 | const win = ref>() 21 | 22 | onMounted(async () => { 23 | console.log('win: ', win) 24 | const status = await win.value?.winStatus() 25 | console.log('status :>> ', status) 26 | console.log('testTsx: ', testTsx.value?.msg) 27 | }) 28 | 29 | const slots = { default: () => '默认插槽', footer: () => '具名插槽' } 30 | 31 | const showMsg = (str: string, type: string) => { 32 | window.$message.destroyAll() 33 | window.$message[type](str) 34 | } 35 | 36 | return () => ( 37 | <> 38 | 39 | 40 | 跳转 41 | 42 |

通过vite注入的全局变量PROJECT_BUILD_TIME: {PROJECT_BUILD_TIME}

43 | 44 | {{ default: () => '默认插槽', footer: () => '具名插槽' }} 45 |
46 |
showMsg('show', 'info')} 48 | // v-intersecting:hide={isIntersecting => showMsg('hide', 'warning')} 49 | v-intersecting={{ 50 | show: () => showMsg('show', 'info'), 51 | hide: () => showMsg('hide', 'warning') 52 | }} 53 | // v-intersecting={{ hide: isIntersecting => showMsg('hide', 'warning') }} 54 | v-text={'测试的哈斯大苏打'} 55 | /> 56 | 57 | ) 58 | } 59 | }) 60 | -------------------------------------------------------------------------------- /src/renderer/pages/Test/testTsx.tsx: -------------------------------------------------------------------------------- 1 | import { resolveDirective, withDirectives } from 'vue' 2 | 3 | export default defineComponent({ 4 | setup() { 5 | const msg = ref('msg') 6 | return { 7 | msg 8 | } 9 | }, 10 | expose: ['msg'], 11 | render() { 12 | const el =

{'
'}

13 | const slot = this.$slots.default && this.$slots.default() 14 | const footerSlot = this.$slots.footer && this.$slots.footer() 15 | return ( 16 |
17 | {this.msg} 18 | {withDirectives(el, [ 19 | [resolveDirective('demo')!, () => 1, 'value', { suffix: true }], 20 | [resolveDirective('ellipsis')!, 1] 21 | ])} 22 |
23 | 24 |
25 |
26 | 27 |
28 |
{slot}
29 |
{footerSlot}
30 |
31 | ) 32 | } 33 | }) 34 | -------------------------------------------------------------------------------- /src/renderer/pages/Test/windicss.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/renderer/pages/Vite/index.tsx: -------------------------------------------------------------------------------- 1 | // import { Icon } from '@iconify/vue' 2 | 3 | export default defineComponent({ 4 | name: 'Vite', 5 | setup() { 6 | return () => ( 7 | <> 8 |
这是vite
9 | 10 | ) 11 | } 12 | }) 13 | -------------------------------------------------------------------------------- /src/renderer/pages/Vue/index.tsx: -------------------------------------------------------------------------------- 1 | // import { Icon } from '@iconify/vue' 2 | import { WinKey } from '@enums/window' 3 | import CardGroup from '@/components/CardGroup' 4 | 5 | export default defineComponent({ 6 | name: 'Vue', 7 | setup() { 8 | const cardList = reactive([ 9 | { key: WinKey.PRINT, title: '打印', content: '这是测试1的内容', path: 'print-demo' }, 10 | { key: WinKey.DROP, title: '文件拖拽', content: '这是测试2的内容', path: 'drop-demo' }, 11 | { key: WinKey.DOWNLOAD, title: '下载', content: '使用electron的api下载', path: 'download-demo' }, 12 | { key: WinKey.SQL, title: '数据库操作', content: 'sqlite3在electron的应用', path: 'sql-demo' } 13 | ]) 14 | return () => 15 | } 16 | }) 17 | -------------------------------------------------------------------------------- /src/renderer/renderer.ts: -------------------------------------------------------------------------------- 1 | import 'virtual:windi.css' 2 | import './styles/index.css' 3 | 4 | import { createApp } from 'vue' 5 | import { createPinia } from 'pinia' 6 | import naive from 'naive-ui' 7 | import App from './App' 8 | import router from './router' 9 | import i18n from './i18n' 10 | import customDirective from './directive' 11 | 12 | // 解决tailwindcss覆盖naive样式的问题 13 | // const meta = document.createElement('meta') 14 | // meta.name = 'naive-ui-style' 15 | // document.head.appendChild(meta) 16 | 17 | const app = createApp(App) 18 | 19 | app.use(createPinia()).use(router) 20 | app.use(i18n).use(naive) 21 | app.use(customDirective) 22 | 23 | app.mount('#app') 24 | -------------------------------------------------------------------------------- /src/renderer/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory } from 'vue-router' 2 | import type { RouteRecordRaw } from 'vue-router' 3 | 4 | const stairRouters: Array = [ 5 | { 6 | path: '/:pathMatch(.*)*', 7 | name: 'NotFound', 8 | component: () => import('@/pages/NotFound.vue') 9 | }, 10 | { path: '/', redirect: '/home' } 11 | ] 12 | 13 | const childRoute: Record = import.meta.globEager('./modules/*.ts') 14 | 15 | const mainRouters: Array = Object.values(childRoute).reduce( 16 | (pre, now) => [...pre, ...now.default], 17 | [] 18 | ) 19 | 20 | const routes: Array = [...stairRouters, ...mainRouters] 21 | 22 | const router = createRouter({ 23 | history: createWebHashHistory(), 24 | routes 25 | }) 26 | 27 | export default router 28 | -------------------------------------------------------------------------------- /src/renderer/router/modules/home.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | import Layout from '@/layout' 3 | 4 | export default [ 5 | { 6 | name: 'home', 7 | path: '/home', 8 | component: Layout, 9 | redirect: '/home/electron', 10 | children: [ 11 | { 12 | name: 'electron', 13 | path: 'electron', 14 | component: () => import('@/pages/Electron'), 15 | meta: { 16 | title: 'electron', 17 | icon: 'logos:electron' 18 | } 19 | }, 20 | { 21 | name: 'vite', 22 | path: 'vite', 23 | component: () => import('@/pages/Vite'), 24 | meta: { 25 | title: 'vite', 26 | icon: 'vscode-icons:file-type-vite' 27 | } 28 | }, 29 | { 30 | name: 'vue', 31 | path: 'vue', 32 | component: () => import('@/pages/Vue'), 33 | meta: { 34 | title: 'vue', 35 | icon: 'vscode-icons:file-type-vue' 36 | } 37 | }, 38 | { 39 | name: 'javascript', 40 | path: 'javascript', 41 | component: () => import('@/pages/Vue'), 42 | meta: { 43 | title: 'javascript', 44 | icon: 'logos:javascript' 45 | } 46 | } 47 | // { 48 | // name: 'windicss', 49 | // path: 'windicss', 50 | // component: () => import('@/pages/Test/windicss.vue') 51 | // }, 52 | // { 53 | // name: 'i18n', 54 | // path: 'i18n', 55 | // component: () => import('@/pages/Test/i18n.vue') 56 | // }, 57 | // { path: '/print', name: 'Print', component: () => import('@/pages/Print.vue') } 58 | ] 59 | } 60 | ] as RouteRecordRaw[] 61 | -------------------------------------------------------------------------------- /src/renderer/router/modules/wins.ts: -------------------------------------------------------------------------------- 1 | import type { RouteRecordRaw } from 'vue-router' 2 | 3 | export default [ 4 | { 5 | name: 'loading', 6 | path: '/loading', 7 | component: () => import('@/win/Loading/index.vue') 8 | }, 9 | { 10 | name: 'feel-brid', 11 | path: '/feel-brid', 12 | component: () => import('@/win/FeelBrid/index.vue') 13 | }, 14 | { 15 | name: 'drop-demo', 16 | path: '/drop-demo', 17 | component: () => import('@/win/DropDemo/index.vue') 18 | }, 19 | { 20 | name: 'print-demo', 21 | path: '/print-demo', 22 | component: () => import('@/win/PrintDemo/index.vue') 23 | }, 24 | { 25 | name: 'sql-demo', 26 | path: '/sql-demo', 27 | component: () => import('@/win/SqlDemo/index.vue') 28 | }, 29 | { 30 | name: 'download-demo', 31 | path: '/download-demo', 32 | component: () => import('@/win/DownloadDemo') 33 | }, 34 | { 35 | name: 'Font-demo', 36 | path: '/Font-demo', 37 | component: () => import('@/win/FontDemo/index.vue') 38 | } 39 | ] as RouteRecordRaw[] 40 | -------------------------------------------------------------------------------- /src/renderer/store/index.ts: -------------------------------------------------------------------------------- 1 | export * from './modules' 2 | export * from './theme' 3 | -------------------------------------------------------------------------------- /src/renderer/store/modules/demo.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | 3 | interface DemoState { 4 | demo: { 5 | name: string 6 | } 7 | } 8 | 9 | export const useDemoStore = defineStore('demo', { 10 | state: (): DemoState => ({ 11 | demo: { 12 | name: 'demo' 13 | } 14 | }), 15 | getters: { 16 | name: (state): string => state.demo.name 17 | }, 18 | actions: { 19 | actionDemo(data: string) { 20 | this.demo.name = data 21 | } 22 | } 23 | }) 24 | -------------------------------------------------------------------------------- /src/renderer/store/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from './demo' 2 | -------------------------------------------------------------------------------- /src/renderer/store/theme/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { GlobalThemeOverrides } from 'naive-ui' 2 | import { cloneDeep } from 'lodash-es' 3 | import { addColorAlpha, getColorPalette, themeSetting } from './settings' 4 | 5 | /** 获取主题配置 */ 6 | export function getThemeSettings() { 7 | const themeColor = themeSetting.themeColor 8 | const info = themeSetting.isCustomizeInfoColor 9 | ? themeSetting.otherColor.info 10 | : getColorPalette(themeColor, 7) 11 | const otherColor = { ...themeSetting.otherColor, info } 12 | const setting = cloneDeep({ ...themeSetting, themeColor, otherColor }) 13 | return setting 14 | } 15 | 16 | type ColorType = 'primary' | 'info' | 'success' | 'warning' | 'error' 17 | type ColorScene = '' | 'Suppl' | 'Hover' | 'Pressed' | 'Active' 18 | type ColorKey = `${ColorType}Color${ColorScene}` 19 | type ThemeColor = { 20 | [key in ColorKey]?: string 21 | } 22 | interface ColorAction { 23 | scene: ColorScene 24 | handler: (color: string) => string 25 | } 26 | 27 | /** 获取主题颜色的各种场景对应的颜色 */ 28 | function getThemeColors(colors: [ColorType, string][]) { 29 | const colorActions: ColorAction[] = [ 30 | { scene: '', handler: color => color }, 31 | { scene: 'Suppl', handler: color => color }, 32 | { scene: 'Hover', handler: color => getColorPalette(color, 5) }, 33 | { scene: 'Pressed', handler: color => getColorPalette(color, 7) }, 34 | { scene: 'Active', handler: color => addColorAlpha(color, 0.1) } 35 | ] 36 | 37 | const themeColor: ThemeColor = {} 38 | 39 | colors.forEach((color) => { 40 | colorActions.forEach((action) => { 41 | const [colorType, colorValue] = color 42 | const colorKey: ColorKey = `${colorType}Color${action.scene}` 43 | themeColor[colorKey] = action.handler(colorValue) 44 | }) 45 | }) 46 | 47 | return themeColor 48 | } 49 | 50 | /** 获取naive的主题颜色 */ 51 | export function getNaiveThemeOverrides(colors: Record): GlobalThemeOverrides { 52 | const { primary, success, warning, error } = colors 53 | 54 | const info = themeSetting.isCustomizeInfoColor ? colors.info : getColorPalette(primary, 7) 55 | 56 | const themeColors = getThemeColors([ 57 | ['primary', primary], 58 | ['info', info], 59 | ['success', success], 60 | ['warning', warning], 61 | ['error', error] 62 | ]) 63 | 64 | const colorLoading = primary 65 | 66 | return { 67 | common: { 68 | ...themeColors 69 | }, 70 | LoadingBar: { 71 | colorLoading 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/renderer/store/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from 'pinia' 2 | import { darkTheme } from 'naive-ui' 3 | import { getNaiveThemeOverrides, getThemeSettings } from './helpers' 4 | 5 | export const useThemeStore = defineStore('theme-store', { 6 | state: () => getThemeSettings(), 7 | getters: { 8 | /** naiveUI的主题配置 */ 9 | naiveThemeOverrides(state) { 10 | const overrides = getNaiveThemeOverrides({ primary: state.themeColor, ...state.otherColor }) 11 | return overrides 12 | }, 13 | /** naive-ui暗黑主题 */ 14 | naiveTheme(state) { 15 | return state.darkMode ? darkTheme : undefined 16 | }, 17 | pageAnimateMode(state) { 18 | return state.page.animate ? state.page.animateMode : undefined 19 | } 20 | }, 21 | actions: { 22 | /** 重置theme状态 */ 23 | resetThemeStore() { 24 | this.$reset() 25 | }, 26 | /** 设置暗黑模式 */ 27 | setDarkMode(darkMode: boolean) { 28 | this.darkMode = darkMode 29 | }, 30 | /** 设置自动跟随系统主题 */ 31 | setFollowSystemTheme(visible: boolean) { 32 | this.followSystemTheme = visible 33 | }, 34 | /** 自动跟随系统主题 */ 35 | autoFollowSystemMode(darkMode: boolean) { 36 | if (this.followSystemTheme) 37 | this.darkMode = darkMode 38 | }, 39 | /** 切换/关闭 暗黑模式 */ 40 | toggleDarkMode() { 41 | this.darkMode = !this.darkMode 42 | }, 43 | /** 设置布局最小宽度 */ 44 | setLayoutMinWidth(minWidth: number) { 45 | this.layout.minWidth = minWidth 46 | }, 47 | /** 设置侧边栏反转色 */ 48 | setSiderInverted(isInverted: boolean) { 49 | this.sider.inverted = isInverted 50 | }, 51 | /** 设置头部反转色 */ 52 | setHeaderInverted(isInverted: boolean) { 53 | this.header.inverted = isInverted 54 | }, 55 | /** 设置系统主题颜色 */ 56 | setThemeColor(themeColor: string) { 57 | this.themeColor = themeColor 58 | }, 59 | /** 设置固定头部和多页签 */ 60 | setIsFixedHeaderAndTab(isFixed: boolean) { 61 | this.fixedHeaderAndTab = isFixed 62 | }, 63 | /** 设置重载按钮可见状态 */ 64 | setReloadVisible(visible: boolean) { 65 | this.showReload = visible 66 | }, 67 | /** 设置头部高度 */ 68 | setHeaderHeight(height: number | null) { 69 | if (height) 70 | this.header.height = height 71 | }, 72 | /** 设置头部面包屑可见 */ 73 | setHeaderCrumbVisible(visible: boolean) { 74 | this.header.crumb.visible = visible 75 | }, 76 | /** 设置头部面包屑图标可见 */ 77 | setHeaderCrumbIconVisible(visible: boolean) { 78 | this.header.crumb.showIcon = visible 79 | }, 80 | /** 设置多页签可见 */ 81 | setTabVisible(visible: boolean) { 82 | this.tab.visible = visible 83 | }, 84 | /** 设置多页签高度 */ 85 | setTabHeight(height: number | null) { 86 | if (height) 87 | this.tab.height = height 88 | }, 89 | /** 设置多页签缓存 */ 90 | setTabIsCache(isCache: boolean) { 91 | this.tab.isCache = isCache 92 | }, 93 | /** 侧边栏宽度 */ 94 | setSiderWidth(width: number | null) { 95 | if (width) 96 | this.sider.width = width 97 | }, 98 | /** 侧边栏折叠时的宽度 */ 99 | setSiderCollapsedWidth(width: number) { 100 | this.sider.collapsedWidth = width 101 | }, 102 | /** vertical-mix模式下侧边栏宽度 */ 103 | setMixSiderWidth(width: number | null) { 104 | if (width) 105 | this.sider.mixWidth = width 106 | }, 107 | /** vertical-mix模式下侧边栏折叠时的宽度 */ 108 | setMixSiderCollapsedWidth(width: number) { 109 | this.sider.mixCollapsedWidth = width 110 | }, 111 | /** vertical-mix模式下侧边栏展示子菜单的宽度 */ 112 | setMixSiderChildMenuWidth(width: number) { 113 | this.sider.mixChildMenuWidth = width 114 | }, 115 | /** 设置底部是否固定 */ 116 | setFooterIsFixed(isFixed: boolean) { 117 | this.footer.fixed = isFixed 118 | }, 119 | /** 设置底部高度 */ 120 | setFooterHeight(height: number) { 121 | this.footer.height = height 122 | }, 123 | /** 设置切换页面时是否过渡动画 */ 124 | setPageIsAnimate(animate: boolean) { 125 | this.page.animate = animate 126 | } 127 | } 128 | }) 129 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/color.json: -------------------------------------------------------------------------------- 1 | [ 2 | { 3 | "label": "红色系", 4 | "data": [ 5 | { 6 | "label": "绾", 7 | "color": "#A98175" 8 | }, 9 | { 10 | "label": "檀", 11 | "color": "#B36D61" 12 | }, 13 | { 14 | "label": "栗色", 15 | "color": "#60281E" 16 | }, 17 | { 18 | "label": "玄", 19 | "color": "#622A1D" 20 | }, 21 | { 22 | "label": "胭脂", 23 | "color": "#9D2933" 24 | }, 25 | { 26 | "label": "殷红", 27 | "color": "#BE002F" 28 | }, 29 | { 30 | "label": "枣红", 31 | "color": "#C32136" 32 | }, 33 | { 34 | "label": "赤", 35 | "color": "#C3272B" 36 | }, 37 | { 38 | "label": "绯红", 39 | "color": "#C83C23" 40 | }, 41 | { 42 | "label": "赫赤", 43 | "color": "#C91F37" 44 | }, 45 | { 46 | "label": "樱桃红", 47 | "color": "#C93756" 48 | }, 49 | { 50 | "label": "茜色", 51 | "color": "#CB3A56" 52 | }, 53 | { 54 | "label": "海棠红", 55 | "color": "#DB5A6B" 56 | }, 57 | { 58 | "label": "酡红", 59 | "color": "#DC3023" 60 | }, 61 | { 62 | "label": "妃色", 63 | "color": "#ED5736" 64 | }, 65 | { 66 | "label": "嫣红", 67 | "color": "#EF7A82" 68 | }, 69 | { 70 | "label": "品红", 71 | "color": "#F00056" 72 | }, 73 | { 74 | "label": "石榴红", 75 | "color": "#F20C00" 76 | }, 77 | { 78 | "label": "银红", 79 | "color": "#F05654" 80 | }, 81 | { 82 | "label": "彤", 83 | "color": "#F35336" 84 | }, 85 | { 86 | "label": "桃红", 87 | "color": "#F47983" 88 | }, 89 | { 90 | "label": "酡颜", 91 | "color": "#F9906F" 92 | }, 93 | { 94 | "label": "洋红", 95 | "color": "#FF0097" 96 | }, 97 | { 98 | "label": "大红", 99 | "color": "#FF2121" 100 | }, 101 | { 102 | "label": "火红", 103 | "color": "#FF2D51" 104 | }, 105 | { 106 | "label": "炎", 107 | "color": "#FF3300" 108 | }, 109 | { 110 | "label": "朱红", 111 | "color": "#FF4C00" 112 | }, 113 | { 114 | "label": "丹", 115 | "color": "#FF4E20" 116 | }, 117 | { 118 | "label": "粉红", 119 | "color": "#FFB3A7" 120 | }, 121 | { 122 | "label": "藕荷", 123 | "color": "#E4C6D0" 124 | }, 125 | { 126 | "label": "藕", 127 | "color": "#EDD1D8" 128 | }, 129 | { 130 | "label": "水红", 131 | "color": "#F3D3E7" 132 | }, 133 | { 134 | "label": "鱼肚白", 135 | "color": "#FCEFE8" 136 | } 137 | ] 138 | }, 139 | { 140 | "label": "橙色系", 141 | "data": [ 142 | { 143 | "label": "褐色", 144 | "color": "#6E511E" 145 | }, 146 | { 147 | "label": "棕黑", 148 | "color": "#7C4B00" 149 | }, 150 | { 151 | "label": "赭色", 152 | "color": "#955539" 153 | }, 154 | { 155 | "label": "棕红", 156 | "color": "#9B4400" 157 | }, 158 | { 159 | "label": "赭", 160 | "color": "#9C5333" 161 | }, 162 | { 163 | "label": "驼色", 164 | "color": "#A88462" 165 | }, 166 | { 167 | "label": "棕色", 168 | "color": "#B25D25" 169 | }, 170 | { 171 | "label": "茶色", 172 | "color": "#B35C44" 173 | }, 174 | { 175 | "label": "琥珀", 176 | "color": "#CA6924" 177 | }, 178 | { 179 | "label": "黄栌", 180 | "color": "#E29C45" 181 | }, 182 | { 183 | "label": "橙色", 184 | "color": "#FA8C35" 185 | }, 186 | { 187 | "label": "橘红", 188 | "color": "#FF7500" 189 | }, 190 | { 191 | "label": "橘黄", 192 | "color": "#FF8936" 193 | }, 194 | { 195 | "label": "杏红", 196 | "color": "#FF8C31" 197 | }, 198 | { 199 | "label": "橙黄", 200 | "color": "#FFA400" 201 | }, 202 | { 203 | "label": "杏黄", 204 | "color": "#FFA631" 205 | }, 206 | { 207 | "label": "姜黄", 208 | "color": "#FFC773" 209 | } 210 | ] 211 | }, 212 | { 213 | "label": "黄色系", 214 | "data": [ 215 | { 216 | "label": "黧", 217 | "color": "#5D513C" 218 | }, 219 | { 220 | "label": "黎", 221 | "color": "#75664D" 222 | }, 223 | { 224 | "label": "棕绿", 225 | "color": "#827100" 226 | }, 227 | { 228 | "label": "秋色", 229 | "color": "#896C39" 230 | }, 231 | { 232 | "label": "苍黄", 233 | "color": "#A29B7C" 234 | }, 235 | { 236 | "label": "乌金", 237 | "color": "#A78E44" 238 | }, 239 | { 240 | "label": "棕黄", 241 | "color": "#AE7000" 242 | }, 243 | { 244 | "label": "昏黄", 245 | "color": "#C89B40" 246 | }, 247 | { 248 | "label": "枯黄", 249 | "color": "#D3B17D" 250 | }, 251 | { 252 | "label": "秋香色", 253 | "color": "#D9B611" 254 | }, 255 | { 256 | "label": "金", 257 | "color": "#EACD76" 258 | }, 259 | { 260 | "label": "牙", 261 | "color": "#EEDEB0" 262 | }, 263 | { 264 | "label": "缃色", 265 | "color": "#F0C239" 266 | }, 267 | { 268 | "label": "赤金", 269 | "color": "#F2BE45" 270 | }, 271 | { 272 | "label": "鸭黄", 273 | "color": "#FAFF72" 274 | }, 275 | { 276 | "label": "鹅黄", 277 | "color": "#FFF143" 278 | }, 279 | { 280 | "label": "缟", 281 | "color": "#F2ECDE" 282 | }, 283 | { 284 | "label": "象牙白", 285 | "color": "#FFFBF0" 286 | } 287 | ] 288 | }, 289 | { 290 | "label": "绿色系", 291 | "data": [ 292 | { 293 | "label": "竹青", 294 | "color": "#789262" 295 | }, 296 | { 297 | "label": "黯", 298 | "color": "#41555D" 299 | }, 300 | { 301 | "label": "黛绿", 302 | "color": "#426666" 303 | }, 304 | { 305 | "label": "松花绿", 306 | "color": "#057748" 307 | }, 308 | { 309 | "label": "绿沈", 310 | "color": "#0C8918" 311 | }, 312 | { 313 | "label": "深绿", 314 | "color": "#009900" 315 | }, 316 | { 317 | "label": "青葱", 318 | "color": "#0AA344" 319 | }, 320 | { 321 | "label": "铜绿", 322 | "color": "#549688" 323 | }, 324 | { 325 | "label": "苍翠", 326 | "color": "#519A73" 327 | }, 328 | { 329 | "label": "松柏绿", 330 | "color": "#21A675" 331 | }, 332 | { 333 | "label": "葱青", 334 | "color": "#0EB83A" 335 | }, 336 | { 337 | "label": "油绿", 338 | "color": "#00BC12" 339 | }, 340 | { 341 | "label": "绿", 342 | "color": "#00E500" 343 | }, 344 | { 345 | "label": "草绿", 346 | "color": "#40DE5A" 347 | }, 348 | { 349 | "label": "豆青", 350 | "color": "#96CE54" 351 | }, 352 | { 353 | "label": "豆绿", 354 | "color": "#9ED048" 355 | }, 356 | { 357 | "label": "葱绿", 358 | "color": "#9ED900" 359 | }, 360 | { 361 | "label": "葱黄", 362 | "color": "#A3D900" 363 | }, 364 | { 365 | "label": "柳绿", 366 | "color": "#AFDD22" 367 | }, 368 | { 369 | "label": "嫩绿", 370 | "color": "#BDDD22" 371 | }, 372 | { 373 | "label": "柳黄", 374 | "color": "#C9DD22" 375 | }, 376 | { 377 | "label": "松花", 378 | "color": "#BCE672" 379 | }, 380 | { 381 | "label": "樱草色", 382 | "color": "#EAFF56" 383 | } 384 | ] 385 | }, 386 | { 387 | "label": "青色系", 388 | "data": [ 389 | { 390 | "label": "水", 391 | "color": "#88ADA6" 392 | }, 393 | { 394 | "label": "青碧", 395 | "color": "#48C0A3" 396 | }, 397 | { 398 | "label": "碧", 399 | "color": "#1BD1A5" 400 | }, 401 | { 402 | "label": "石青", 403 | "color": "#7BCFA6" 404 | }, 405 | { 406 | "label": "青翠", 407 | "color": "#00E079" 408 | }, 409 | { 410 | "label": "青", 411 | "color": "#00E09E" 412 | }, 413 | { 414 | "label": "碧绿", 415 | "color": "#2ADD9C" 416 | }, 417 | { 418 | "label": "玉", 419 | "color": "#2EDFA3" 420 | }, 421 | { 422 | "label": "翡翠", 423 | "color": "#3DE1AD" 424 | }, 425 | { 426 | "label": "缥", 427 | "color": "#7FECAD" 428 | }, 429 | { 430 | "label": "碧蓝", 431 | "color": "#3EEDE7" 432 | }, 433 | { 434 | "label": "湖绿", 435 | "color": "#25F8CD" 436 | }, 437 | { 438 | "label": "艾绿", 439 | "color": "#A4E2C6" 440 | }, 441 | { 442 | "label": "青白", 443 | "color": "#C0EBD7" 444 | }, 445 | { 446 | "label": "水绿", 447 | "color": "#D4F2E7" 448 | }, 449 | { 450 | "label": "鸭卵青", 451 | "color": "#E0EEE8" 452 | }, 453 | { 454 | "label": "素", 455 | "color": "#E0F0E9" 456 | }, 457 | { 458 | "label": "荼白", 459 | "color": "#F3F9F1" 460 | } 461 | ] 462 | }, 463 | { 464 | "label": "蓝色系", 465 | "data": [ 466 | { 467 | "label": "藏蓝", 468 | "color": "#3B2E7E" 469 | }, 470 | { 471 | "label": "宝蓝", 472 | "color": "#4B5CC4" 473 | }, 474 | { 475 | "label": "绀青", 476 | "color": "#003371" 477 | }, 478 | { 479 | "label": "藏青", 480 | "color": "#2E4E7E" 481 | }, 482 | { 483 | "label": "靛蓝", 484 | "color": "#065279" 485 | }, 486 | { 487 | "label": "靛青", 488 | "color": "#177CB0" 489 | }, 490 | { 491 | "label": "群青", 492 | "color": "#4C8DAE" 493 | }, 494 | { 495 | "label": "蓝", 496 | "color": "#44CEF6" 497 | }, 498 | { 499 | "label": "湖蓝", 500 | "color": "#30DFF3" 501 | }, 502 | { 503 | "label": "蔚蓝", 504 | "color": "#70F3FF" 505 | }, 506 | { 507 | "label": "月白", 508 | "color": "#D6ECF0" 509 | }, 510 | { 511 | "label": "水蓝", 512 | "color": "#D2F0F4" 513 | }, 514 | { 515 | "label": "莹白", 516 | "color": "#E3F9FD" 517 | }, 518 | { 519 | "label": "雪白", 520 | "color": "#F0FCFF" 521 | } 522 | ] 523 | }, 524 | { 525 | "label": "紫色系", 526 | "data": [ 527 | { 528 | "label": "黛", 529 | "color": "#4A4266" 530 | }, 531 | { 532 | "label": "紫檀", 533 | "color": "#4C211B" 534 | }, 535 | { 536 | "label": "紫棠", 537 | "color": "#56004F" 538 | }, 539 | { 540 | "label": "黛紫", 541 | "color": "#574266" 542 | }, 543 | { 544 | "label": "绛紫", 545 | "color": "#8C4356" 546 | }, 547 | { 548 | "label": "紫酱", 549 | "color": "#815463" 550 | }, 551 | { 552 | "label": "酱紫", 553 | "color": "#815476" 554 | }, 555 | { 556 | "label": "黝", 557 | "color": "#6B6882" 558 | }, 559 | { 560 | "label": "青莲", 561 | "color": "#801DAE" 562 | }, 563 | { 564 | "label": "紫", 565 | "color": "#8D4BBB" 566 | }, 567 | { 568 | "label": "雪青", 569 | "color": "#B0A4E3" 570 | }, 571 | { 572 | "label": "丁香", 573 | "color": "#CCA4E3" 574 | } 575 | ] 576 | }, 577 | { 578 | "label": "灰色系", 579 | "data": [ 580 | { 581 | "label": "黑", 582 | "color": "#000000" 583 | }, 584 | { 585 | "label": "漆黑", 586 | "color": "#161823" 587 | }, 588 | { 589 | "label": "象牙黑", 590 | "color": "#312520" 591 | }, 592 | { 593 | "label": "乌黑", 594 | "color": "#392F41" 595 | }, 596 | { 597 | "label": "玄青", 598 | "color": "#3D3B4F" 599 | }, 600 | { 601 | "label": "缁", 602 | "color": "#493131" 603 | }, 604 | { 605 | "label": "黝黑", 606 | "color": "#665757" 607 | }, 608 | { 609 | "label": "鸦青", 610 | "color": "#424C50" 611 | }, 612 | { 613 | "label": "黛蓝", 614 | "color": "#425066" 615 | }, 616 | { 617 | "label": "苍黑", 618 | "color": "#395260" 619 | }, 620 | { 621 | "label": "墨", 622 | "color": "#50616D" 623 | }, 624 | { 625 | "label": "灰", 626 | "color": "#808080" 627 | }, 628 | { 629 | "label": "苍", 630 | "color": "#75878A" 631 | }, 632 | { 633 | "label": "墨灰", 634 | "color": "#758A99" 635 | }, 636 | { 637 | "label": "苍青", 638 | "color": "#7397AB" 639 | }, 640 | { 641 | "label": "蓝灰", 642 | "color": "#A1AFC9" 643 | }, 644 | { 645 | "label": "老银", 646 | "color": "#BACAC6" 647 | }, 648 | { 649 | "label": "蟹壳青", 650 | "color": "#BBCDC5" 651 | }, 652 | { 653 | "label": "苍白", 654 | "color": "#D1D9E0" 655 | }, 656 | { 657 | "label": "淡青", 658 | "color": "#D3E0F3" 659 | }, 660 | { 661 | "label": "银白", 662 | "color": "#E9E7EF" 663 | }, 664 | { 665 | "label": "霜", 666 | "color": "#E9F1F6" 667 | }, 668 | { 669 | "label": "铅白", 670 | "color": "#F0F0F4" 671 | }, 672 | { 673 | "label": "精白", 674 | "color": "#FFFFFF" 675 | } 676 | ] 677 | } 678 | ] 679 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/color.ts: -------------------------------------------------------------------------------- 1 | import colorJson from './color.json' 2 | 3 | interface TraditionColorDetail { 4 | label: string 5 | color: string 6 | } 7 | interface TraditionColor { 8 | label: string 9 | data: TraditionColorDetail[] 10 | } 11 | 12 | /** 中国传统颜色 */ 13 | export const traditionColors = colorJson as TraditionColor[] 14 | 15 | export function isInTraditionColors(color: string) { 16 | return traditionColors.some((item) => { 17 | const flag = item.data.some(v => v.color === color) 18 | return flag 19 | }) 20 | } 21 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/index.ts: -------------------------------------------------------------------------------- 1 | export * from './theme' 2 | export * from './color' 3 | export * from './options' 4 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/options.ts: -------------------------------------------------------------------------------- 1 | import { colord, extend } from 'colord' 2 | import mixPlugin from 'colord/plugins/mix' 3 | import type { HsvColor } from 'colord' 4 | 5 | extend([mixPlugin]) 6 | 7 | type ColorIndex = 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 8 | 9 | const hueStep = 2 10 | const saturationStep = 16 11 | const saturationStep2 = 5 12 | const brightnessStep1 = 5 13 | const brightnessStep2 = 15 14 | const lightColorCount = 5 15 | const darkColorCount = 4 16 | 17 | /** 18 | * 根据颜色获取调色板颜色(从左至右颜色从浅到深,6为主色号) 19 | * @param color - 颜色 20 | * @param index - 调色板的对应的色号(6为主色号) 21 | * @description 算法实现从ant-design调色板算法中借鉴 https://github.com/ant-design/ant-design/blob/master/components/style/color/colorPalette.less 22 | */ 23 | export function getColorPalette(color: string, index: ColorIndex) { 24 | if (index === 6) 25 | return color 26 | 27 | const isLight = index < 6 28 | const hsv = colord(color).toHsv() 29 | const i = isLight ? lightColorCount + 1 - index : index - lightColorCount - 1 30 | 31 | const newHsv: HsvColor = { 32 | h: getHue(hsv, i, isLight), 33 | s: getSaturation(hsv, i, isLight), 34 | v: getValue(hsv, i, isLight) 35 | } 36 | 37 | return colord(newHsv).toHex() 38 | } 39 | 40 | /** 41 | * 根据颜色获取调色板颜色所有颜色 42 | * @param color - 颜色 43 | */ 44 | export function getAllColorPalette(color: string) { 45 | const indexs: ColorIndex[] = [1, 2, 3, 4, 5, 6, 7, 8, 9, 10] 46 | return indexs.map(index => getColorPalette(color, index)) 47 | } 48 | 49 | /** 50 | * 获取色相渐变 51 | * @param hsv - hsv格式颜色值 52 | * @param i - 与6的相对距离 53 | * @param isLight - 是否是亮颜色 54 | */ 55 | function getHue(hsv: HsvColor, i: number, isLight: boolean) { 56 | let hue: number 57 | if (hsv.h >= 60 && hsv.h <= 240) { 58 | // 冷色调 59 | // 减淡变亮 色相顺时针旋转 更暖 60 | // 加深变暗 色相逆时针旋转 更冷 61 | hue = isLight ? hsv.h - hueStep * i : hsv.h + hueStep * i 62 | } 63 | else { 64 | // 暖色调 65 | // 减淡变亮 色相逆时针旋转 更暖 66 | // 加深变暗 色相顺时针旋转 更冷 67 | hue = isLight ? hsv.h + hueStep * i : hsv.h - hueStep * i 68 | } 69 | if (hue < 0) 70 | hue += 360 71 | else if (hue >= 360) 72 | hue -= 360 73 | 74 | return hue 75 | } 76 | 77 | /** 78 | * 获取饱和度渐变 79 | * @param hsv - hsv格式颜色值 80 | * @param i - 与6的相对距离 81 | * @param isLight - 是否是亮颜色 82 | */ 83 | function getSaturation(hsv: HsvColor, i: number, isLight: boolean) { 84 | let saturation: number 85 | if (isLight) 86 | saturation = hsv.s - saturationStep * i 87 | else if (i === darkColorCount) 88 | saturation = hsv.s + saturationStep 89 | else 90 | saturation = hsv.s + saturationStep2 * i 91 | 92 | if (saturation > 100) 93 | saturation = 100 94 | 95 | if (isLight && i === lightColorCount && saturation > 10) 96 | saturation = 10 97 | 98 | if (saturation < 6) 99 | saturation = 6 100 | 101 | return saturation 102 | } 103 | 104 | /** 105 | * 获取明度渐变 106 | * @param hsv - hsv格式颜色值 107 | * @param i - 与6的相对距离 108 | * @param isLight - 是否是亮颜色 109 | */ 110 | function getValue(hsv: HsvColor, i: number, isLight: boolean) { 111 | let value: number 112 | if (isLight) 113 | value = hsv.v + brightnessStep1 * i 114 | else 115 | value = hsv.v - brightnessStep2 * i 116 | 117 | if (value > 100) 118 | value = 100 119 | 120 | return value 121 | } 122 | 123 | /** 124 | * 给颜色加透明度 125 | * @param color - 颜色 126 | * @param alpha - 透明度(0 - 1) 127 | */ 128 | export function addColorAlpha(color: string, alpha: number) { 129 | return colord(color).alpha(alpha).toHex() 130 | } 131 | 132 | /** 133 | * 颜色混合 134 | * @param firstColor - 第一个颜色 135 | * @param secondColor - 第二个颜色 136 | * @param ratio - 第二个颜色占比 137 | */ 138 | export function mixColor(firstColor: string, secondColor: string, ratio: number) { 139 | return colord(firstColor).mix(secondColor, ratio).toHex() 140 | } 141 | 142 | /** 143 | * 是否是白颜色 144 | * @param color - 颜色 145 | */ 146 | export function isWhiteColor(color: string) { 147 | return colord(color).isEqual('#ffffff') 148 | } 149 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/theme.json: -------------------------------------------------------------------------------- 1 | { 2 | "darkMode": false, 3 | "followSystemTheme": true, 4 | "layout": { 5 | "minWidth": 900, 6 | "mode": "vertical", 7 | "modeList": [ 8 | { 9 | "value": "vertical", 10 | "label": "左侧菜单模式" 11 | }, 12 | { 13 | "value": "vertical-mix", 14 | "label": "左侧菜单混合模式" 15 | }, 16 | { 17 | "value": "horizontal", 18 | "label": "顶部菜单模式" 19 | }, 20 | { 21 | "value": "horizontal-mix", 22 | "label": "顶部菜单混合模式" 23 | } 24 | ] 25 | }, 26 | "themeColor": "#1890ff", 27 | "themeColorList": [ 28 | "#1890ff", 29 | "#409EFF", 30 | "#2d8cf0", 31 | "#007AFF", 32 | "#5ac8fa", 33 | "#5856D6", 34 | "#536dfe", 35 | "#9c27b0", 36 | "#AF52DE", 37 | "#0096c7", 38 | "#00C1D4", 39 | "#34C759", 40 | "#43a047", 41 | "#7cb342", 42 | "#c0ca33", 43 | "#78DEC7", 44 | "#e53935", 45 | "#d81b60", 46 | "#f4511e", 47 | "#fb8c00", 48 | "#ffb300", 49 | "#fdd835", 50 | "#6d4c41", 51 | "#546e7a" 52 | ], 53 | "otherColor": { 54 | "info": "#0099ad", 55 | "success": "#52c41a", 56 | "warning": "#faad14", 57 | "error": "#f5222d" 58 | }, 59 | "isCustomizeInfoColor": false, 60 | "fixedHeaderAndTab": true, 61 | "showReload": true, 62 | "header": { 63 | "inverted": false, 64 | "height": 56, 65 | "crumb": { 66 | "visible": true, 67 | "showIcon": true 68 | } 69 | }, 70 | "tab": { 71 | "visible": true, 72 | "height": 44, 73 | "mode": "chrome", 74 | "modeList": [ 75 | { 76 | "value": "chrome", 77 | "label": "谷歌风格" 78 | }, 79 | { 80 | "value": "button", 81 | "label": "按钮风格" 82 | } 83 | ], 84 | "isCache": true 85 | }, 86 | "sider": { 87 | "inverted": false, 88 | "width": 220, 89 | "collapsedWidth": 64, 90 | "mixWidth": 80, 91 | "mixCollapsedWidth": 48, 92 | "mixChildMenuWidth": 200 93 | }, 94 | "menu": { 95 | "horizontalPosition": "flex-start", 96 | "horizontalPositionList": [ 97 | { 98 | "value": "flex-start", 99 | "label": "居左" 100 | }, 101 | { 102 | "value": "center", 103 | "label": "居中" 104 | }, 105 | { 106 | "value": "flex-end", 107 | "label": "居右" 108 | } 109 | ] 110 | }, 111 | "footer": { 112 | "fixed": false, 113 | "height": 48 114 | }, 115 | "page": { 116 | "animate": true, 117 | "animateMode": "fade-slide", 118 | "animateModeList": [ 119 | { 120 | "value": "fade-slide", 121 | "label": "滑动" 122 | }, 123 | { 124 | "value": "fade", 125 | "label": "消退" 126 | }, 127 | { 128 | "value": "fade-bottom", 129 | "label": "底部消退" 130 | }, 131 | { 132 | "value": "fade-scale", 133 | "label": "缩放消退" 134 | }, 135 | { 136 | "value": "zoom-fade", 137 | "label": "渐变" 138 | }, 139 | { 140 | "value": "zoom-out", 141 | "label": "闪现" 142 | } 143 | ] 144 | } 145 | } 146 | -------------------------------------------------------------------------------- /src/renderer/store/theme/settings/theme.ts: -------------------------------------------------------------------------------- 1 | import { 2 | EnumThemeAnimateMode, 3 | EnumThemeHorizontalMenuPosition, 4 | EnumThemeLayoutMode, 5 | EnumThemeTabMode 6 | } from '@enums/system' 7 | import jsonSetting from './theme.json' 8 | 9 | const themeColorList = [ 10 | '#1890ff', 11 | '#409EFF', 12 | '#2d8cf0', 13 | '#007AFF', 14 | '#5ac8fa', 15 | '#5856D6', 16 | '#536dfe', 17 | '#9c27b0', 18 | '#AF52DE', 19 | '#0096c7', 20 | '#00C1D4', 21 | '#34C759', 22 | '#43a047', 23 | '#7cb342', 24 | '#c0ca33', 25 | '#78DEC7', 26 | '#e53935', 27 | '#d81b60', 28 | '#f4511e', 29 | '#fb8c00', 30 | '#ffb300', 31 | '#fdd835', 32 | '#6d4c41', 33 | '#546e7a' 34 | ] 35 | 36 | const defaultThemeSetting = { 37 | darkMode: false, 38 | followSystemTheme: true, 39 | layout: { 40 | minWidth: 900, 41 | mode: 'vertical', 42 | modeList: [ 43 | { value: 'vertical', label: EnumThemeLayoutMode.vertical }, 44 | { value: 'vertical-mix', label: EnumThemeLayoutMode['vertical-mix'] }, 45 | { value: 'horizontal', label: EnumThemeLayoutMode.horizontal }, 46 | { value: 'horizontal-mix', label: EnumThemeLayoutMode['horizontal-mix'] } 47 | ] 48 | }, 49 | themeColor: themeColorList[0], 50 | themeColorList, 51 | otherColor: { 52 | info: '#2080f0', 53 | success: '#52c41a', 54 | warning: '#faad14', 55 | error: '#f5222d' 56 | }, 57 | isCustomizeInfoColor: false, 58 | fixedHeaderAndTab: true, 59 | showReload: true, 60 | header: { 61 | inverted: false, 62 | height: 56, 63 | crumb: { 64 | visible: true, 65 | showIcon: true 66 | } 67 | }, 68 | tab: { 69 | visible: true, 70 | height: 44, 71 | mode: 'chrome', 72 | modeList: [ 73 | { value: 'chrome', label: EnumThemeTabMode.chrome }, 74 | { value: 'button', label: EnumThemeTabMode.button } 75 | ], 76 | isCache: true 77 | }, 78 | sider: { 79 | inverted: false, 80 | width: 220, 81 | collapsedWidth: 64, 82 | mixWidth: 80, 83 | mixCollapsedWidth: 48, 84 | mixChildMenuWidth: 200 85 | }, 86 | menu: { 87 | horizontalPosition: 'flex-start', 88 | horizontalPositionList: [ 89 | { value: 'flex-start', label: EnumThemeHorizontalMenuPosition['flex-start'] }, 90 | { value: 'center', label: EnumThemeHorizontalMenuPosition.center }, 91 | { value: 'flex-end', label: EnumThemeHorizontalMenuPosition['flex-end'] } 92 | ] 93 | }, 94 | footer: { 95 | fixed: false, 96 | height: 48 97 | }, 98 | page: { 99 | animate: true, 100 | animateMode: 'fade-slide', 101 | animateModeList: [ 102 | { value: 'fade-slide', label: EnumThemeAnimateMode['fade-slide'] }, 103 | { value: 'fade', label: EnumThemeAnimateMode.fade }, 104 | { value: 'fade-bottom', label: EnumThemeAnimateMode['fade-bottom'] }, 105 | { value: 'fade-scale', label: EnumThemeAnimateMode['fade-scale'] }, 106 | { value: 'zoom-fade', label: EnumThemeAnimateMode['zoom-fade'] }, 107 | { value: 'zoom-out', label: EnumThemeAnimateMode['zoom-out'] } 108 | ] 109 | } 110 | } 111 | 112 | export const themeSetting = jsonSetting || defaultThemeSetting 113 | -------------------------------------------------------------------------------- /src/renderer/store/theme/subTheme.ts: -------------------------------------------------------------------------------- 1 | import { onUnmounted, watch } from 'vue' 2 | import { useOsTheme } from 'naive-ui' 3 | import type { GlobalThemeOverrides } from 'naive-ui' 4 | import { kebabCase } from 'lodash-es' 5 | import { useThemeStore } from './index' 6 | 7 | /** 订阅theme store */ 8 | export function subTheme() { 9 | const theme = useThemeStore() 10 | const osTheme = useOsTheme() 11 | const { width } = useElementSize(document.documentElement) 12 | const { addDarkClass, removeDarkClass } = handleCssDarkMode() 13 | 14 | // 监听主题颜色 15 | const stopThemeColor = watch( 16 | () => theme.themeColor, 17 | (newValue) => { 18 | console.log('newValue: ', newValue) 19 | }, 20 | { immediate: true } 21 | ) 22 | 23 | // 监听naiveUI themeOverrides 24 | const stopThemeOverrides = watch( 25 | () => theme.naiveThemeOverrides, 26 | (newValue) => { 27 | if (newValue.common) 28 | addThemeCssVarsToHtml(newValue.common) 29 | }, 30 | { immediate: true } 31 | ) 32 | 33 | // 监听暗黑模式 34 | const stopDarkMode = watch( 35 | () => theme.darkMode, 36 | (newValue) => { 37 | if (newValue) 38 | addDarkClass() 39 | else 40 | removeDarkClass() 41 | }, 42 | { 43 | immediate: true 44 | } 45 | ) 46 | 47 | // 监听操作系统主题模式 48 | const stopOsTheme = watch( 49 | osTheme, 50 | (newValue) => { 51 | const isDark = newValue === 'dark' 52 | theme.autoFollowSystemMode(isDark) 53 | }, 54 | { immediate: true } 55 | ) 56 | 57 | // 禁用横向滚动(页面切换时,过渡动画会产生水平方向的滚动条, 小于最小宽度时,不禁止) 58 | const stopWidth = watch(width, (newValue) => { 59 | if (newValue < theme.layout.minWidth) 60 | document.documentElement.style.overflowX = 'auto' 61 | else 62 | document.documentElement.style.overflowX = 'hidden' 63 | }) 64 | 65 | onUnmounted(() => { 66 | stopThemeColor() 67 | stopThemeOverrides() 68 | stopDarkMode() 69 | stopOsTheme() 70 | stopWidth() 71 | }) 72 | } 73 | 74 | /** css 暗黑模式 */ 75 | function handleCssDarkMode() { 76 | const DARK_CLASS = 'dark' 77 | function addDarkClass() { 78 | document.documentElement.classList.add(DARK_CLASS) 79 | } 80 | function removeDarkClass() { 81 | document.documentElement.classList.remove(DARK_CLASS) 82 | } 83 | return { 84 | addDarkClass, 85 | removeDarkClass 86 | } 87 | } 88 | 89 | type ThemeVars = Exclude 90 | type ThemeVarsKeys = keyof ThemeVars 91 | 92 | /** 添加css vars至html */ 93 | function addThemeCssVarsToHtml(themeVars: ThemeVars) { 94 | const keys = Object.keys(themeVars) as ThemeVarsKeys[] 95 | const style: string[] = [] 96 | keys.forEach((key) => { 97 | style.push(`--${kebabCase(key)}: ${themeVars[key]}`) 98 | }) 99 | const styleStr = style.join(';') 100 | document.documentElement.style.cssText += styleStr 101 | } 102 | -------------------------------------------------------------------------------- /src/renderer/styles/common/header.scss: -------------------------------------------------------------------------------- 1 | // sidebar 2 | $menuText: #bfcbd9; 3 | $menuActiveText: #409eff; 4 | $subMenuActiveText: #f4f4f5; //https://github.com/ElemeFE/element/issues/12951 5 | 6 | $menuBg: #304156; 7 | $menuHover: #263445; 8 | 9 | $subMenuBg: #1f2d3d; 10 | $subMenuHover: #001528; 11 | 12 | $sideBarWidth: 210px; 13 | 14 | // 导出变量,可以在js中使用 15 | :export { 16 | menuText: $menuText; 17 | menuActiveText: $menuActiveText; 18 | subMenuActiveText: $subMenuActiveText; 19 | menuBg: $menuBg; 20 | menuHover: $menuHover; 21 | subMenuBg: $subMenuBg; 22 | subMenuHover: $subMenuHover; 23 | sideBarWidth: $sideBarWidth; 24 | } 25 | -------------------------------------------------------------------------------- /src/renderer/styles/common/transition.css: -------------------------------------------------------------------------------- 1 | /* 消退 fade */ 2 | .fade-enter-active, 3 | .fade-leave-active { 4 | transition: opacity 0.3s ease-in-out; 5 | } 6 | .fade-enter-from, 7 | .fade-leave-to { 8 | opacity: 0; 9 | } 10 | 11 | /* 滑动 fade-slide */ 12 | .fade-slide-leave-active, 13 | .fade-slide-enter-active { 14 | transition: all 0.3s; 15 | } 16 | .fade-slide-enter-from { 17 | opacity: 0; 18 | transform: translateX(-30px); 19 | } 20 | .fade-slide-leave-to { 21 | opacity: 0; 22 | transform: translateX(30px); 23 | } 24 | 25 | /* 底部消退 fade-bottom */ 26 | .fade-bottom-enter-active, 27 | .fade-bottom-leave-active { 28 | transition: opacity 0.25s, transform 0.3s; 29 | } 30 | .fade-bottom-enter-from { 31 | opacity: 0; 32 | transform: translateY(-10%); 33 | } 34 | .fade-bottom-leave-to { 35 | opacity: 0; 36 | transform: translateY(10%); 37 | } 38 | 39 | /* 缩放消退 fade-scale */ 40 | .fade-scale-leave-active, 41 | .fade-scale-enter-active { 42 | transition: all 0.28s; 43 | } 44 | .fade-scale-enter-from { 45 | opacity: 0; 46 | transform: scale(1.2); 47 | } 48 | .fade-scale-leave-to { 49 | opacity: 0; 50 | transform: scale(0.8); 51 | } 52 | 53 | /* 渐变 zoom-fade */ 54 | .zoom-fade-enter-active, 55 | .zoom-fade-leave-active { 56 | transition: transform 0.2s, opacity 0.3s ease-out; 57 | } 58 | .zoom-fade-enter-from { 59 | opacity: 0; 60 | transform: scale(0.92); 61 | } 62 | .zoom-fade-leave-to { 63 | opacity: 0; 64 | transform: scale(1.06); 65 | } 66 | 67 | /* 闪现 zoom-out */ 68 | .zoom-out-enter-active, 69 | .zoom-out-leave-active { 70 | transition: opacity 0.1s ease-in-out, transform 0.15s ease-out; 71 | } 72 | .zoom-out-enter-from, 73 | .zoom-out-leave-to { 74 | opacity: 0; 75 | transform: scale(0); 76 | } 77 | -------------------------------------------------------------------------------- /src/renderer/styles/common/vars.css: -------------------------------------------------------------------------------- 1 | :root { 2 | --main-color: #4d4e53; 3 | --main-bg: rgb(255, 255, 255); 4 | --logo-border-color: rebeccapurple; 5 | 6 | --header-height: 68px; 7 | --content-padding: 10px 20px; 8 | 9 | --base-line-height: 1.428571429; 10 | --transition-duration: 0.35s; 11 | --external-link: 'external link'; 12 | --margin-top: calc(2vh + 20px); 13 | } 14 | -------------------------------------------------------------------------------- /src/renderer/styles/index.css: -------------------------------------------------------------------------------- 1 | @import './common/transition.css'; 2 | 3 | /*滚动条整体样式*/ 4 | ::-webkit-scrollbar { 5 | width: 10px; /*高宽分别对应横竖滚动条的尺寸*/ 6 | } 7 | 8 | /*滚动条里面轨道*/ 9 | ::-webkit-scrollbar-track { 10 | border-radius: 2px; 11 | background: #1f222a; 12 | } 13 | 14 | /*滚动条里面小方块*/ 15 | ::-webkit-scrollbar-thumb { 16 | border-radius: 10px; 17 | border-style: dashed; 18 | border-color: transparent; 19 | border-width: 4px; 20 | background-color: rgba(157, 165, 183, 0.4); 21 | background-clip: padding-box; 22 | transition: 0.6s; 23 | } 24 | 25 | ::-webkit-scrollbar-thumb:hover { 26 | background: rgba(157, 165, 183, 0.7); 27 | } 28 | 29 | .drag { 30 | -webkit-app-region: drag; 31 | } 32 | 33 | .no-drag { 34 | -webkit-app-region: no-drag; 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/utils/construction/CreateEvent.ts: -------------------------------------------------------------------------------- 1 | interface MyEvent extends Event { 2 | detail: Data 3 | } 4 | 5 | export class CreateEvent { 6 | private eventName: string 7 | private listener: (event: MyEvent) => void 8 | private callback: (...args: any[]) => void 9 | 10 | constructor(eventName: string, callback?: () => void) { 11 | this.eventName = eventName 12 | callback && (this.callback = callback) 13 | } 14 | 15 | dispatch(args?: Args) { 16 | if (!this.eventName) 17 | return 18 | 19 | const event = args ? new CustomEvent(this.eventName, { detail: args }) : new Event(this.eventName) 20 | 21 | window.dispatchEvent(event) 22 | } 23 | 24 | listen() { 25 | this.listener = (event: MyEvent) => { 26 | this.callback(event?.detail ?? event) 27 | } 28 | 29 | window.addEventListener(this.eventName as any, this.listener) 30 | } 31 | 32 | destroy() { 33 | window.removeEventListener(this.eventName as any, this.listener) 34 | } 35 | } 36 | -------------------------------------------------------------------------------- /src/renderer/utils/construction/EventEmitter.ts: -------------------------------------------------------------------------------- 1 | type AnyFn = (...args: any[]) => void 2 | 3 | export class EventEmitter { 4 | private cache: Map> 5 | private static _instance: EventEmitter 6 | 7 | private constructor() { 8 | this.cache = new Map() 9 | } 10 | 11 | static getInstance() { 12 | if (!this._instance) 13 | return (this._instance = new EventEmitter()) 14 | 15 | return this._instance 16 | } 17 | 18 | on(name: string, callback: () => void) { 19 | let fncs = this.cache.get(name) 20 | 21 | if (!fncs) 22 | fncs = new Set() 23 | 24 | fncs.add(callback) 25 | 26 | this.cache.set(name, fncs) 27 | } 28 | 29 | once(name: string, callback: () => void) { 30 | this.on(`once-${name}`, callback) 31 | } 32 | 33 | emit(name: string, ...args: any[]) { 34 | this.cache.get(name)?.forEach((callback) => { 35 | callback(...args) 36 | }) 37 | 38 | const onceEvent = `once-${name}` 39 | 40 | this.cache.get(onceEvent)?.forEach((callback) => { 41 | callback(...args) 42 | }) 43 | 44 | this.cache.delete(onceEvent) 45 | } 46 | 47 | off(name: string, callback: () => void) { 48 | const funcs = this.cache.get(name) 49 | 50 | funcs?.delete(callback) 51 | 52 | funcs && this.cache.set(name, funcs) 53 | } 54 | } 55 | 56 | export const eventBus = EventEmitter.getInstance() 57 | -------------------------------------------------------------------------------- /src/renderer/utils/construction/ImageTools.ts: -------------------------------------------------------------------------------- 1 | export class ImageTools { 2 | static toBase64(localPath: string): Promise { 3 | return new Promise((resolve, reject) => { 4 | let img: HTMLImageElement | null = new Image() 5 | if (img) { 6 | img.src = localPath 7 | img.onload = () => { 8 | let canvas: HTMLCanvasElement | null = document.createElement('canvas') 9 | const ctx: CanvasRenderingContext2D | null = canvas.getContext('2d') 10 | 11 | if (!ctx || !img) 12 | return 13 | 14 | canvas.width = img.width 15 | canvas.height = img.height 16 | ctx.drawImage(img, 0, 0, canvas.width, canvas.height) 17 | const dataURL = canvas.toDataURL() 18 | img = null 19 | canvas = null 20 | resolve(dataURL) 21 | } 22 | 23 | img.onerror = () => { 24 | reject(new Error(`图片加载失败或该地址无法解析为base64 ==> ${localPath}`)) 25 | } 26 | } 27 | }) 28 | } 29 | 30 | static Base64toFile(base64: string, filename: string): File { 31 | if (!base64.startsWith('data:')) 32 | return new File([], filename) 33 | 34 | const [, str] = base64.split(',') 35 | const buff = Buffer.from(str, 'base64') 36 | 37 | return new File([buff], filename) 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/renderer/utils/construction/Time.ts: -------------------------------------------------------------------------------- 1 | export class Time { 2 | time: Date 3 | now: Date 4 | constructor(time: string | Date) { 5 | this.time = new Date(time) 6 | this.now = new Date() 7 | } 8 | 9 | get timeStr() { 10 | return this.ifYear() || this.ifMonth() || this.ifWeek() || this.ifYesterday() || this.ifDay() || '' 11 | } 12 | 13 | // 判断是否今年 14 | ifYear() { 15 | const date = this.time.getFullYear() 16 | const now = this.now.getFullYear() 17 | 18 | if (date !== now) 19 | return `${this.time.getFullYear()}/${this.time.getMonth() + 1}/${this.time.getDate()}` 20 | return false 21 | } 22 | 23 | // 是否本月 24 | ifMonth() { 25 | const date = this.time.getMonth() + 1 26 | const now = this.now.getMonth() + 1 27 | 28 | if (date !== now) 29 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 30 | return false 31 | } 32 | 33 | // 判断是否本周 34 | ifWeek() { 35 | const week = ['日', '一', '二', '三', '四', '五', '六'] 36 | const date = this.time.getDate() 37 | const now = this.now.getDate() 38 | const weekSum = this.now.getDay() 39 | 40 | if (now - date < 2) 41 | return false 42 | 43 | if (now - date > 6) 44 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 45 | 46 | if (!weekSum) 47 | return `星期${week[this.time.getDay()]}` 48 | 49 | if (now - date < weekSum) 50 | return `星期${week[this.time.getDay()]}` 51 | 52 | return `${this.time.getMonth() + 1}月${this.time.getDate()}日` 53 | } 54 | 55 | // 判断是否是昨天 56 | ifYesterday() { 57 | const date = this.time.getDate() 58 | const now = this.now.getDate() 59 | if (now - date > 0) 60 | return '昨天' 61 | return false 62 | } 63 | 64 | // 判断是否当天 65 | ifDay() { 66 | const date = this.time.getDate() 67 | const now = this.now.getDate() 68 | if (date === now) 69 | return `${this.time.getHours()}:${this.time.getMinutes().toString().padStart(2, '0')}` 70 | 71 | return false 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/renderer/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from './construction/CreateEvent' 2 | export * from './construction/EventEmitter' 3 | export * from './construction/Time' 4 | export * from './construction/ImageTools' 5 | 6 | export * from './methods/renderEnv' 7 | export * from './methods/auth' 8 | export * from './methods/textToImg' 9 | 10 | export * from './request' 11 | -------------------------------------------------------------------------------- /src/renderer/utils/methods/auth.ts: -------------------------------------------------------------------------------- 1 | export const enum StoreKey { 2 | TokenKey = 'access_token', 3 | UserKey = 'user_info' 4 | } 5 | 6 | export const getToken = () => localStorage.getItem(StoreKey.TokenKey) 7 | 8 | export const setToken = (token: string) => localStorage.setItem(StoreKey.TokenKey, token) 9 | 10 | export const getUserInfo = () => localStorage.getItem(StoreKey.UserKey) 11 | 12 | export const setUserInfo = (userInfo: any) => localStorage.setItem(StoreKey.UserKey, userInfo) 13 | 14 | export const remove = () => { 15 | localStorage.removeItem(StoreKey.TokenKey) 16 | localStorage.removeItem(StoreKey.UserKey) 17 | } 18 | -------------------------------------------------------------------------------- /src/renderer/utils/methods/renderEnv.ts: -------------------------------------------------------------------------------- 1 | export const renderDevExecFn = (callback: () => void) => import.meta.env.DEV && callback() 2 | 3 | export const renderProExecFn = (callback: () => void) => import.meta.env.PROD && callback() 4 | 5 | export const getRenderEnv = (callback: (env: ImportMetaEnv) => void) => callback(import.meta.env) 6 | -------------------------------------------------------------------------------- /src/renderer/utils/methods/textToImg.ts: -------------------------------------------------------------------------------- 1 | // text,需要生成的文字 2 | // font,字体样式 3 | 4 | export function textToImg(text: string) { 5 | // 创建画布 6 | const canvas = document.createElement('canvas') 7 | // 绘制文字环境 8 | const context = canvas.getContext('2d') 9 | 10 | if (!context) 11 | return 12 | // 画布宽度 13 | canvas.width = 160 14 | // 画布高度 15 | canvas.height = 130 16 | // 填充白色 17 | context.fillStyle = '#f3f4f5' 18 | // 绘制文字之前填充白色 19 | context.fillRect(0, 0, canvas.width, canvas.height) 20 | context.font = 'italic small-caps 100 12px arial' 21 | context.fillStyle = '#e2e1e4' 22 | context.textAlign = 'center' 23 | context.textBaseline = 'middle' 24 | 25 | context.rotate((20 * Math.PI) / -180) 26 | // 绘制文字(参数:要写的字,x坐标,y坐标) 27 | 28 | context.fillText(text, 50, canvas.height / 2) 29 | // context.fillText(text, canvas.width / 2, canvas.height / 2) 30 | // 生成图片信息 31 | const dataUrl = canvas.toDataURL('image/png') 32 | return dataUrl 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/utils/request/createRequest.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosInstance, AxiosRequestConfig } from 'axios' 2 | import axios from 'axios' 3 | import { handleAxiosError } from './handleAxiosError' 4 | 5 | interface RequestOptions { 6 | url: string 7 | method?: 'get' | 'post' | 'put' | 'delete' 8 | data?: any 9 | params?: any 10 | config?: AxiosRequestConfig 11 | } 12 | 13 | /** 14 | * 封装axios请求类 15 | */ 16 | class CustomAxiosInstance { 17 | instance: AxiosInstance 18 | 19 | constructor(axiosConfig: AxiosRequestConfig) { 20 | this.instance = axios.create(axiosConfig) 21 | this.setInterceptor() 22 | } 23 | 24 | setInterceptor() { 25 | this.instance.interceptors.request.use(async (config) => { 26 | return config 27 | }, handleAxiosError) 28 | 29 | this.instance.interceptors.response.use( 30 | async response => 31 | new Promise((resolve, reject): any => { 32 | const { status, data } = response 33 | 34 | if (status !== 200) 35 | reject(new Error('状态码错误!')) 36 | 37 | resolve(data) 38 | }), 39 | handleAxiosError 40 | ) 41 | } 42 | } 43 | 44 | async function getRequestResponse(instance: AxiosInstance, options: RequestOptions) { 45 | options.method = options.method || 'get' 46 | const { method, url, config } = options 47 | const res: any = await instance[method](url, config) 48 | return res 49 | } 50 | 51 | export function createRequest(axiosConfig: AxiosRequestConfig) { 52 | const customInstance = new CustomAxiosInstance(axiosConfig) 53 | 54 | /** 55 | * 异步promise请求 56 | * @param param - 请求参数 57 | * - url: 请求地址 58 | * - method: 请求方法(默认get) 59 | * - data: 请求的body的data 60 | * - axiosConfig: axios配置 61 | */ 62 | async function request(options: RequestOptions): Promise { 63 | const { instance } = customInstance 64 | const res: T = await getRequestResponse(instance, options) 65 | 66 | return res 67 | } 68 | 69 | /** 70 | * get请求 71 | * @param url - 请求地址 72 | * @param config - axios配置 73 | */ 74 | function get(url: string, params?: any, config?: AxiosRequestConfig) { 75 | return request({ url, method: 'get', params, config }) 76 | } 77 | 78 | /** 79 | * post请求 80 | * @param url - 请求地址 81 | * @param data - 请求的body的data 82 | * @param config - axios配置 83 | */ 84 | function post(url: string, data?: any, config?: AxiosRequestConfig) { 85 | return request({ url, method: 'post', data, config }) 86 | } 87 | /** 88 | * put请求 89 | * @param url - 请求地址 90 | * @param data - 请求的body的data 91 | * @param config - axios配置 92 | */ 93 | function put(url: string, data?: any, config?: AxiosRequestConfig) { 94 | return request({ url, method: 'put', data, config }) 95 | } 96 | 97 | /** 98 | * delete请求 99 | * @param url - 请求地址 100 | * @param config - axios配置 101 | */ 102 | function handleDelete(url: string, config: AxiosRequestConfig) { 103 | return request({ url, method: 'delete', config }) 104 | } 105 | 106 | return { 107 | get, 108 | post, 109 | put, 110 | delete: handleDelete 111 | } 112 | } 113 | -------------------------------------------------------------------------------- /src/renderer/utils/request/handleAxiosError.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosError } from 'axios' 2 | import { statusCodeMap } from './statusCodeMap' 3 | 4 | async function showMessage(key: string | number, axiosError: AxiosError) { 5 | if (!statusCodeMap.has(key)) 6 | return 7 | 8 | console.log('已捕获的错误处理') 9 | window.$message.destroyAll() 10 | const handle = statusCodeMap.get(key) 11 | if (!handle) 12 | return 13 | const message = await handle(axiosError) 14 | window.$message.warning(message) 15 | } 16 | 17 | /** 18 | * 处理axios请求失败的错误 19 | * @param error - 错误 20 | */ 21 | export async function handleAxiosError(axiosError: AxiosError) { 22 | const err = { 23 | status: axiosError.response?.status, 24 | code: axiosError.code, 25 | message: axiosError.message 26 | } 27 | 28 | for (const key in err) showMessage(err[key], axiosError) 29 | 30 | console.log('err :>> ', err) 31 | 32 | return Promise.reject(axiosError) 33 | } 34 | -------------------------------------------------------------------------------- /src/renderer/utils/request/index.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from './createRequest' 2 | 3 | export const request = createRequest({ 4 | baseURL: import.meta.env.VITE_BASE_URL, 5 | timeout: 1000 * 6 6 | }) 7 | 8 | export const uploadFile = createRequest({ 9 | baseURL: import.meta.env.VITE_UPLOAD_URL, 10 | timeout: 1000 * 60, 11 | onUploadProgress(progressEvent) { 12 | const progress = parseInt(`${(progressEvent.loaded / progressEvent.total) * 100}`) 13 | console.log('onUploadProgress :>> ', progress) 14 | }, 15 | onDownloadProgress(progressEvent) { 16 | const progress = parseInt(`${(progressEvent.loaded / progressEvent.total) * 100}`) 17 | console.log('onDownloadProgress :>> ', progress) 18 | } 19 | }) 20 | -------------------------------------------------------------------------------- /src/renderer/utils/request/statusCodeMap.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosError } from 'axios' 2 | 3 | type ErrorMessage = (err: AxiosError) => Promise 4 | 5 | export const statusCodeMap = new Map() 6 | 7 | statusCodeMap.set('Network Error', () => { 8 | return Promise.resolve('网络连接失败,请检查网络连接后重试!') 9 | }) 10 | 11 | statusCodeMap.set('ECONNABORTED', () => { 12 | return Promise.resolve('网络连接超时,请检查网络后重试!') 13 | }) 14 | 15 | statusCodeMap.set(400, () => { 16 | return Promise.resolve('语义有误,当前请求无法被服务器理解!') 17 | }) 18 | 19 | statusCodeMap.set(401, () => { 20 | return Promise.resolve('登录状态失效!请重新登录!') 21 | }) 22 | 23 | statusCodeMap.set(403, () => { 24 | return Promise.resolve('用户得到授权,但是访问是被禁止的!') 25 | }) 26 | 27 | statusCodeMap.set(404, () => { 28 | return Promise.resolve('网络请求错误,未找到该资源!') 29 | }) 30 | 31 | // 请求方法错误 32 | statusCodeMap.set(405, () => { 33 | return Promise.resolve('网络请求错误,请求方法未允许!') 34 | }) 35 | 36 | statusCodeMap.set(408, () => { 37 | return Promise.resolve('网络请求超时!') 38 | }) 39 | 40 | statusCodeMap.set(500, () => { 41 | return Promise.resolve('服务器错误,请联系管理员!') 42 | }) 43 | 44 | statusCodeMap.set(501, () => { 45 | return Promise.resolve('网络未实现!') 46 | }) 47 | 48 | statusCodeMap.set(502, () => { 49 | return Promise.resolve('网络错误!') 50 | }) 51 | 52 | statusCodeMap.set(503, () => { 53 | return Promise.resolve('服务不可用,服务器暂时过载或维护!!') 54 | }) 55 | 56 | statusCodeMap.set(504, () => { 57 | return Promise.resolve('网络超时!') 58 | }) 59 | 60 | statusCodeMap.set(505, () => { 61 | return Promise.resolve('http版本不支持该请求!') 62 | }) 63 | -------------------------------------------------------------------------------- /src/renderer/utils/request/toFormData.ts: -------------------------------------------------------------------------------- 1 | const toFormData = (obj: object) => { 2 | const formData = new FormData() 3 | Object.keys(obj).forEach((key) => { 4 | if (Array.isArray(obj[key])) { 5 | obj[key].forEach((item: any) => { 6 | formData.append(key, item) 7 | }) 8 | return 9 | } 10 | formData.append(key, obj[key]) 11 | }) 12 | return formData 13 | } 14 | 15 | export default toFormData 16 | -------------------------------------------------------------------------------- /src/renderer/win/DownloadDemo/index.tsx: -------------------------------------------------------------------------------- 1 | import IpcOnMounted from '@/components/IpcOnMounted' 2 | import { useDownload } from '@/hooks' 3 | 4 | export default defineComponent({ 5 | name: 'DownloadDemo', 6 | setup() { 7 | const { progress, downloadInfo, downloadStart } = useDownload() 8 | 9 | watchEffect(() => { 10 | console.log('downloadInfo: ', downloadInfo.value) 11 | }) 12 | 13 | const click = () => 14 | downloadStart({ 15 | // url: 'http://file.ghaomc.com/api/v1/media/attach/knowledge/e79b4487e0f7466e9a202bb1d8d40c8a银杰优优 Setup 0.5.5.exeexe', 16 | url: 'http://erp.ghaomc.com/api/v1/media/temp/temp/694da8bd-849a-493a-929a-a478ce65b8b7.png', 17 | filename: '银杰优优.exe', 18 | // 是否返回进度 19 | isSendProgress: true 20 | }) 21 | 22 | return () => ( 23 | <> 24 |
25 | 26 | 27 | 28 | 29 |
{downloadInfo.value}
30 |
31 |
{downloadInfo.value}
32 |
33 | 34 | ) 35 | } 36 | }) 37 | -------------------------------------------------------------------------------- /src/renderer/win/DropDemo/index.vue: -------------------------------------------------------------------------------- 1 | 86 | 87 | 95 | 96 | 131 | -------------------------------------------------------------------------------- /src/renderer/win/FeelBrid/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 34 | 35 | 408 | -------------------------------------------------------------------------------- /src/renderer/win/FontDemo/index.vue: -------------------------------------------------------------------------------- 1 | 14 | 15 | 36 | 37 | 66 | -------------------------------------------------------------------------------- /src/renderer/win/Loading/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 74 | 75 | 334 | -------------------------------------------------------------------------------- /src/renderer/win/PrintDemo/index.vue: -------------------------------------------------------------------------------- 1 | 47 | 48 | 123 | 124 | 201 | -------------------------------------------------------------------------------- /src/renderer/win/SqlDemo/index.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 54 | -------------------------------------------------------------------------------- /src/types/component.d.ts: -------------------------------------------------------------------------------- 1 | // 全局组件的类型定义 2 | declare namespace Component { 3 | interface CardState { 4 | key: import('@enums/window').WinKey 5 | title: string 6 | content: string 7 | path: Wicket.WindowRoute 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/types/download.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Download { 2 | interface DownloadOptions { 3 | // 目标资源 4 | url: string 5 | // 要保存的文件名(带后缀) 6 | filename: string 7 | // 确保事件唯一性的key 8 | eventKey?: string 9 | // 要保存到的地址 10 | savePath?: string 11 | // 是否展示路径选择 12 | isShowSaveDialog?: boolean 13 | // 是否返回进度 14 | isSendProgress?: boolean 15 | } 16 | 17 | interface DownloadStatus { 18 | // 当前状态 19 | state: string 20 | // 是否成功 21 | isSuccess: boolean 22 | // 提示语句 23 | message: string 24 | // 进度 25 | progress?: number 26 | } 27 | 28 | interface DownloadDetails extends DownloadStatus { 29 | // 耗时 30 | time: string 31 | // 目标资源 32 | url: string 33 | // 保存到本地的路径 34 | savePath: string 35 | // 文件名称 36 | filename: string 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/types/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | declare module '*.vue' { 4 | import type { DefineComponent } from 'vue' 5 | // eslint-disable-next-line @typescript-eslint/no-explicit-any, @typescript-eslint/ban-types 6 | const component: DefineComponent<{}, {}, any> 7 | export default component 8 | } 9 | 10 | declare interface ImportMetaEnv { 11 | readonly VITE_BASE_URL: string 12 | readonly VITE_UPLOAD_URL: string 13 | readonly VITE_BASE_PROT: string 14 | } 15 | 16 | declare interface ImportMeta { 17 | readonly env: ImportMetaEnv 18 | } 19 | 20 | /** import.meta.glob */ 21 | declare interface ImportMetaGlob { 22 | readonly default: T 23 | } 24 | 25 | /** 通过vite注入的全局变量 */ 26 | declare const PROJECT_BUILD_TIME: string 27 | 28 | // 主进程环境变量 29 | declare namespace NodeJS { 30 | export interface ProcessEnv { 31 | readonly NODE_ENV: 'development' | 'production' 32 | readonly PORT?: string 33 | readonly VITE_BASE_URL: string 34 | readonly VITE_UPLOAD_URL: string 35 | readonly VITE_BASE_PROT: string 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/types/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $ipc: import('@quiteer/electron-preload').PreloadIpc 3 | $clipboard: import('@quiteer/electron-preload').PreLoadPath 4 | $webFrame: import('@quiteer/electron-preload').PreloadWebFrame 5 | $path: import('@quiteer/electron-preload').PreLoadPath 6 | $loadingBar: import('naive-ui').LoadingBarProviderInst 7 | $dialog: import('naive-ui').DialogProviderInst 8 | $message: import('naive-ui').MessageProviderInst 9 | $notification: import('naive-ui').NotificationProviderInst 10 | } 11 | 12 | declare module '*.json' 13 | -------------------------------------------------------------------------------- /src/types/sql.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Sql { 2 | interface User { 3 | name: string 4 | sex: boolean 5 | age: number 6 | } 7 | 8 | interface UserEntity extends User { 9 | id: number 10 | } 11 | } 12 | -------------------------------------------------------------------------------- /src/types/test.d.ts: -------------------------------------------------------------------------------- 1 | declare namespace Test { 2 | interface api { 3 | message: string 4 | code: string 5 | data: string[] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /src/types/wicket.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 窗口实例相关的类型 3 | */ 4 | 5 | declare namespace Wicket { 6 | // 窗口实例的状态 7 | interface WinStatus { 8 | id: number 9 | name: import('@enums/window').WinKey 10 | isCreate: boolean 11 | isRead: boolean 12 | isShow: boolean 13 | isFocus: boolean 14 | } 15 | 16 | // 定义win文件夹下的所有窗口 17 | type WindowRoute = 18 | | 'loading' 19 | | 'feel-brid' 20 | | 'drop-demo' 21 | | 'print-demo' 22 | | 'sql-demo' 23 | | 'download-demo' 24 | | 'font-demo' 25 | } 26 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compileOnSave": false, 3 | "compilerOptions": { 4 | "strict": true, 5 | // 元素隐式具有 "any" 类型,因为类型为 "string" 的表达式不能用于索引类型 "xx" 6 | "suppressImplicitAnyIndexErrors": true, 7 | "strictPropertyInitialization": false, 8 | // "noImplicitThis": true, 9 | "isolatedModules": true, 10 | "resolveJsonModule": true, 11 | "baseUrl": ".", 12 | "sourceMap": true, 13 | "esModuleInterop": true, 14 | "declaration": false, 15 | "module": "esnext", 16 | "target": "esnext", 17 | "lib": ["esnext", "DOM"], 18 | "jsx": "preserve", 19 | "moduleResolution": "node", 20 | "allowSyntheticDefaultImports": true, 21 | "emitDecoratorMetadata": true, 22 | "experimentalDecorators": true, 23 | "importHelpers": true, 24 | "types": [ 25 | "node", 26 | "vite/client", 27 | "naive-ui/volar", 28 | "unplugin-icons/types/vue", 29 | "unplugin-vue-macros/macros-global" 30 | ], 31 | "paths": { 32 | "@common/*": ["src/common/*"], 33 | "@enums/*": ["src/enums/*"], 34 | "~/*": ["src/main/*"], 35 | "@/*": ["src/renderer/*"] 36 | }, 37 | "typeRoots": ["node_modules/@types"] 38 | }, 39 | "include": ["src", "build", "scripts", "test", "vite.config.ts"], 40 | "exclude": ["node_modules"] 41 | } 42 | --------------------------------------------------------------------------------