diff --git a/.browserslistrc b/.browserslistrc new file mode 100644 index 00000000..40bd99ce --- /dev/null +++ b/.browserslistrc @@ -0,0 +1,4 @@ +> 1% +last 2 versions +not dead +not ie 11 \ No newline at end of file diff --git a/.electron-builder.config.js b/.electron-builder.config.js deleted file mode 100644 index f231f8e5..00000000 --- a/.electron-builder.config.js +++ /dev/null @@ -1,51 +0,0 @@ -/** - * @type {() => import('electron-builder').Configuration} - * @see https://www.electron.build/configuration/configuration - */ -module.exports = async function () { - return { - appId: "com.pure.electron", - productName: "electron-pure-admin", - copyright: "Copyright © 2020-present, pure-admin", - publish: { - provider: "github", - releaseType: "release" - }, - directories: { - buildResources: "dist", - output: "release/${version}" - }, - extraMetadata: { - version: process.env.npm_package_version - }, - files: ["dist-electron/**", "dist/**"], - nsis: { - allowToChangeInstallationDirectory: true, - createDesktopShortcut: true, - createStartMenuShortcut: true, - shortcutName: "pure-admin", - perMachine: true, - oneClick: false - }, - mac: { - icon: "dist/icons/mac/icon.icns", - artifactName: "${productName}_${version}.${ext}", - target: ["dmg"] - }, - win: { - icon: "dist/icons/win/icon.ico", - artifactName: "${productName}_${version}.${ext}", - target: [ - { - target: "nsis", - arch: ["x64"] - } - ] - }, - linux: { - icon: "dist/icons/png", - artifactName: "${productName}_${version}.${ext}", - target: ["deb", "AppImage"] - } - }; -}; diff --git a/.env.staging b/.env.staging index c591ca1c..00db0cc6 100644 --- a/.env.staging +++ b/.env.staging @@ -1,6 +1,6 @@ # 预发布也需要生产环境的行为 # https://cn.vitejs.dev/guide/env-and-mode.html#modes -NODE_ENV=production +# NODE_ENV = development VITE_PUBLIC_PATH = ./ diff --git a/.eslintignore b/.eslintignore deleted file mode 100644 index 34063653..00000000 --- a/.eslintignore +++ /dev/null @@ -1,11 +0,0 @@ -public -dist -*.d.ts -/src/assets -package.json -.eslintrc.js -.prettierrc.js -commitlint.config.js -postcss.config.js -tailwind.config.js -stylelint.config.js \ No newline at end of file diff --git a/.eslintrc.js b/.eslintrc.js deleted file mode 100644 index 009f5bc0..00000000 --- a/.eslintrc.js +++ /dev/null @@ -1,120 +0,0 @@ -module.exports = { - root: true, - env: { - node: true - }, - globals: { - // Ref sugar (take 2) - $: "readonly", - $$: "readonly", - $ref: "readonly", - $shallowRef: "readonly", - $computed: "readonly", - - // index.d.ts - // global.d.ts - Fn: "readonly", - PromiseFn: "readonly", - RefType: "readonly", - LabelValueOptions: "readonly", - EmitType: "readonly", - TargetContext: "readonly", - ComponentElRef: "readonly", - ComponentRef: "readonly", - ElRef: "readonly", - global: "readonly", - ForDataType: "readonly", - ComponentRoutes: "readonly", - - // script setup - defineProps: "readonly", - defineEmits: "readonly", - defineExpose: "readonly", - withDefaults: "readonly" - }, - extends: [ - "plugin:vue/vue3-essential", - "eslint:recommended", - "@vue/typescript/recommended", - "@vue/prettier", - "@vue/eslint-config-typescript" - ], - parser: "vue-eslint-parser", - parserOptions: { - parser: "@typescript-eslint/parser", - ecmaVersion: 2020, - sourceType: "module", - jsxPragma: "React", - ecmaFeatures: { - jsx: true - } - }, - overrides: [ - { - files: ["*.ts", "*.vue"], - rules: { - "no-undef": "off" - } - }, - { - files: ["*.vue"], - parser: "vue-eslint-parser", - parserOptions: { - parser: "@typescript-eslint/parser", - extraFileExtensions: [".vue"], - ecmaVersion: "latest", - ecmaFeatures: { - jsx: true - } - }, - rules: { - "no-undef": "off" - } - } - ], - rules: { - "vue/no-v-html": "off", - "vue/require-default-prop": "off", - "vue/require-explicit-emits": "off", - "vue/multi-word-component-names": "off", - "@typescript-eslint/no-explicit-any": "off", // any - "no-debugger": "off", - "@typescript-eslint/explicit-module-boundary-types": "off", // setup() - "@typescript-eslint/ban-types": "off", - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/no-empty-function": "off", - "@typescript-eslint/no-non-null-assertion": "off", - "vue/html-self-closing": [ - "error", - { - html: { - void: "always", - normal: "always", - component: "always" - }, - svg: "always", - math: "always" - } - ], - "@typescript-eslint/no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_" - } - ], - "no-unused-vars": [ - "error", - { - argsIgnorePattern: "^_", - varsIgnorePattern: "^_" - } - ], - "prettier/prettier": [ - "error", - { - endOfLine: "auto" - } - ] - } -}; diff --git a/.gitignore b/.gitignore index fed3ca50..289560a3 100644 --- a/.gitignore +++ b/.gitignore @@ -5,6 +5,7 @@ dist-ssr *.local .eslintcache report.html +vite.config.*.timestamp* yarn-error.log npm-debug.log* diff --git a/.husky/commit-msg b/.husky/commit-msg index 567ff71f..5ee2d163 100755 --- a/.husky/commit-msg +++ b/.husky/commit-msg @@ -3,4 +3,6 @@ # shellcheck source=./_/husky.sh . "$(dirname "$0")/_/husky.sh" -npx --no-install commitlint --edit "$1" +PATH="/usr/local/bin:$PATH" + +npx --no-install commitlint --edit "$1" \ No newline at end of file diff --git a/.husky/common.sh b/.husky/common.sh index 5f0540b7..9d5129bd 100644 --- a/.husky/common.sh +++ b/.husky/common.sh @@ -3,7 +3,7 @@ command_exists () { command -v "$1" >/dev/null 2>&1 } -# Workaround for Windows 10, Git Bash and Pnpm +# Workaround for Windows 10, Git Bash and Yarn if command_exists winpty && test -t 1; then exec < /dev/tty fi diff --git a/.husky/lintstagedrc.js b/.husky/lintstagedrc.js deleted file mode 100644 index a9b439cc..00000000 --- a/.husky/lintstagedrc.js +++ /dev/null @@ -1,8 +0,0 @@ -module.exports = { - "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], - "{!(package)*.json}": ["prettier --write--parser json"], - "package.json": ["prettier --write"], - "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], - "*.{vue,css,scss,postcss,less}": ["stylelint --fix", "prettier --write"], - "*.md": ["prettier --write"] -}; diff --git a/.husky/pre-commit b/.husky/pre-commit index c7d15f24..86734bfc 100755 --- a/.husky/pre-commit +++ b/.husky/pre-commit @@ -4,7 +4,7 @@ [ -n "$CI" ] && exit 0 -# Format and submit code according to lintstagedrc.js configuration -npm run lint:lint-staged +PATH="/usr/local/bin:$PATH" -npm run lint:pretty +# Perform lint check on files in the staging area through .lintstagedrc configuration +yarn exec lint-staged \ No newline at end of file diff --git a/.lintstagedrc b/.lintstagedrc new file mode 100644 index 00000000..f844cbff --- /dev/null +++ b/.lintstagedrc @@ -0,0 +1,20 @@ +{ + "*.{js,jsx,ts,tsx}": [ + "prettier --cache --ignore-unknown --write", + "eslint --cache --fix" + ], + "{!(package)*.json,*.code-snippets,.!({browserslist,nvm})*rc}": [ + "prettier --cache --write--parser json" + ], + "package.json": ["prettier --cache --write"], + "*.vue": [ + "prettier --write", + "eslint --cache --fix", + "stylelint --fix --allow-empty-input" + ], + "*.{css,scss,html}": [ + "prettier --cache --ignore-unknown --write", + "stylelint --fix --allow-empty-input" + ], + "*.md": ["prettier --cache --ignore-unknown --write"] +} diff --git a/.nvmrc b/.nvmrc new file mode 100644 index 00000000..238155bf --- /dev/null +++ b/.nvmrc @@ -0,0 +1 @@ +v20.12.2 \ No newline at end of file diff --git a/.prettierrc.js b/.prettierrc.js index 16bb32c3..775d970a 100644 --- a/.prettierrc.js +++ b/.prettierrc.js @@ -1,4 +1,7 @@ -module.exports = { +// @ts-check + +/** @type {import("prettier").Config} */ +export default { bracketSpacing: true, singleQuote: false, arrowParens: "avoid", diff --git a/.vscode/extensions.json b/.vscode/extensions.json index 041d9c93..72ac9925 100644 --- a/.vscode/extensions.json +++ b/.vscode/extensions.json @@ -7,6 +7,7 @@ "bradlc.vscode-tailwindcss", "dbaeumer.vscode-eslint", "esbenp.prettier-vscode", + "mrmlnc.vscode-json5", "redhat.vscode-yaml", "csstools.postcss", "mikestead.dotenv", @@ -14,4 +15,4 @@ "antfu.iconify", "Vue.volar" ] -} +} \ No newline at end of file diff --git a/.vscode/settings.json b/.vscode/settings.json index 1aa0ecb0..c8be7573 100644 --- a/.vscode/settings.json +++ b/.vscode/settings.json @@ -25,7 +25,20 @@ "editor.defaultFormatter": "esbenp.prettier-vscode" }, "editor.codeActionsOnSave": { - "source.fixAll.eslint": true + "source.fixAll.eslint": "explicit" }, - "iconify.excludes": ["el"] -} + "iconify.excludes": [ + "el" + ], + "typescript.tsdk": "node_modules/typescript/lib", + "typescript.tsc.autoDetect": "off", + "json.schemas": [ + { + "fileMatch": [ + "/*electron-builder.json5", + "/*electron-builder.json" + ], + "url": "https://json.schemastore.org/electron-builder" + } + ] +} \ No newline at end of file diff --git a/.vscode/vue3.0.code-snippets b/.vscode/vue3.0.code-snippets index fdab05b7..bb43589a 100644 --- a/.vscode/vue3.0.code-snippets +++ b/.vscode/vue3.0.code-snippets @@ -1,19 +1,19 @@ { "Vue3.0快速生成模板": { + "scope": "vue", "prefix": "Vue3.0", "body": [ "\n", "\n", - "", "$2" ], diff --git a/.vscode/vue3.2.code-snippets b/.vscode/vue3.2.code-snippets index fa637f57..2cebb463 100644 --- a/.vscode/vue3.2.code-snippets +++ b/.vscode/vue3.2.code-snippets @@ -1,14 +1,14 @@ { "Vue3.2+快速生成模板": { + "scope": "vue", "prefix": "Vue3.2+", "body": [ "\n", "\n", - "", "$2" ], diff --git a/.vscode/vue3.3.code-snippets b/.vscode/vue3.3.code-snippets new file mode 100644 index 00000000..dc7a1062 --- /dev/null +++ b/.vscode/vue3.3.code-snippets @@ -0,0 +1,20 @@ +{ + "Vue3.3+defineOptions快速生成模板": { + "scope": "vue", + "prefix": "Vue3.3+", + "body": [ + "\n", + "\n", + "", + "$2" + ], + "description": "Vue3.3+defineOptions快速生成模板" + } +} diff --git a/build/cdn.ts b/build/cdn.ts index d57587bb..c56e4ad7 100644 --- a/build/cdn.ts +++ b/build/cdn.ts @@ -3,7 +3,6 @@ import { Plugin as importToCDN } from "vite-plugin-cdn-import"; /** * @description 打包时采用`cdn`模式,仅限外网使用(默认不采用,如果需要采用cdn模式,请在 .env.production 文件,将 VITE_CDN 设置成true) * 平台采用国内cdn:https://www.bootcdn.cn,当然你也可以选择 https://unpkg.com 或者 https://www.jsdelivr.com - * 提醒:mockjs不能用cdn模式引入,会报错。正确的方式是,生产环境删除mockjs,使用真实的后端请求 * 注意:上面提到的仅限外网使用也不是完全肯定的,如果你们公司内网部署的有相关js、css文件,也可以将下面配置对应改一下,整一套内网版cdn */ export const cdn = importToCDN({ diff --git a/build/icon.ts b/build/icon.ts index 20dea63c..882fca98 100644 --- a/build/icon.ts +++ b/build/icon.ts @@ -1,8 +1,8 @@ -import fs from "fs"; import Jimp from "jimp"; import args from "args"; -import path from "path"; import icongen from "icon-gen"; +import { resolve, join } from "node:path"; +import { renameSync, existsSync, mkdirSync } from "node:fs"; const pngSizes = [16, 24, 32, 48, 64, 128, 256, 512, 1024]; @@ -14,14 +14,14 @@ args const flags = args.parse(process.argv); // correct paths -const input = path.resolve(process.cwd(), flags.input); -const output = path.resolve(process.cwd(), flags.output); +const input = resolve(process.cwd(), flags.input); +const output = resolve(process.cwd(), flags.output); const flatten = flags.flatten; const o = output; -const oSub = path.join(o, "icons/"); -const PNGoutputDir = flatten ? oSub : path.join(oSub, "png"); -const macOutputDir = flatten ? oSub : path.join(oSub, "mac"); -const winOutputDir = flatten ? oSub : path.join(oSub, "win"); +const oSub = join(o, "icons/"); +const PNGoutputDir = flatten ? oSub : join(oSub, "png"); +const macOutputDir = flatten ? oSub : join(oSub, "mac"); +const winOutputDir = flatten ? oSub : join(oSub, "win"); createPNGs(0).catch(err => { console.log(err); @@ -58,10 +58,7 @@ async function createPNGs(position) { async function renamePNGs(position) { const startName = pngSizes[position] + ".png"; const endName = pngSizes[position] + "x" + pngSizes[position] + ".png"; - fs.renameSync( - path.join(PNGoutputDir, startName), - path.join(PNGoutputDir, endName) - ); + renameSync(join(PNGoutputDir, startName), join(PNGoutputDir, endName)); console.log("Renamed " + startName + " to " + endName); if (position < pngSizes.length - 1) { @@ -84,13 +81,13 @@ async function createPNG(size) { const image = await Jimp.read(input); image.resize(size, size); - await image.writeAsync(path.join(PNGoutputDir, fileName)); + await image.writeAsync(join(PNGoutputDir, fileName)); - return "Created " + path.join(PNGoutputDir, fileName); + return "Created " + join(PNGoutputDir, fileName); } function ensureDirExists(dir) { - if (!fs.existsSync(dir)) { - fs.mkdirSync(dir); + if (!existsSync(dir)) { + mkdirSync(dir); } } diff --git a/build/index.ts b/build/index.ts deleted file mode 100644 index f125097d..00000000 --- a/build/index.ts +++ /dev/null @@ -1,31 +0,0 @@ -/** 处理环境变量 */ -const warpperEnv = (envConf: Recordable): ViteEnv => { - /** 此处为默认值 */ - const ret: ViteEnv = { - VITE_PORT: 8848, - VITE_PUBLIC_PATH: "", - VITE_ROUTER_HISTORY: "", - VITE_CDN: false, - VITE_HIDE_HOME: "false", - VITE_COMPRESSION: "none" - }; - - for (const envName of Object.keys(envConf)) { - let realName = envConf[envName].replace(/\\n/g, "\n"); - realName = - realName === "true" ? true : realName === "false" ? false : realName; - - if (envName === "VITE_PORT") { - realName = Number(realName); - } - ret[envName] = realName; - if (typeof realName === "string") { - process.env[envName] = realName; - } else if (typeof realName === "object") { - process.env[envName] = JSON.stringify(realName); - } - } - return ret; -}; - -export { warpperEnv }; diff --git a/build/info.ts b/build/info.ts index 7ef3f11f..6d7c8be2 100644 --- a/build/info.ts +++ b/build/info.ts @@ -1,10 +1,21 @@ import type { Plugin } from "vite"; -import dayjs, { Dayjs } from "dayjs"; -import utils from "@pureadmin/utils"; +import { getPackageSize } from "./utils"; +import dayjs, { type Dayjs } from "dayjs"; import duration from "dayjs/plugin/duration"; -import { green, blue, bold } from "picocolors"; +import gradientString from "gradient-string"; +import boxen, { type Options as BoxenOptions } from "boxen"; dayjs.extend(duration); +const welcomeMessage = gradientString("cyan", "magenta").multiline( + `您好! 欢迎使用 pure-admin 开源项目\n我们为您精心准备了下面两个贴心的保姆级文档\nhttps://pure-admin.github.io/pure-admin-doc\nhttps://pure-admin-utils.netlify.app` +); + +const boxenOptions: BoxenOptions = { + padding: 0.5, + borderColor: "cyan", + borderStyle: "round" +}; + export function viteBuildInfo(): Plugin { let config: { command: string }; let startTime: Dayjs; @@ -17,15 +28,7 @@ export function viteBuildInfo(): Plugin { outDir = resolvedConfig.build?.outDir ?? "dist"; }, buildStart() { - console.log( - bold( - green( - `👏欢迎使用${blue( - "[vue-pure-admin]" - )},如果您感觉不错,记得点击后面链接给个star哦💖 https://github.com/pure-admin/vue-pure-admin` - ) - ) - ); + console.log(boxen(welcomeMessage, boxenOptions)); if (config.command === "build") { startTime = dayjs(new Date()); } @@ -33,16 +36,17 @@ export function viteBuildInfo(): Plugin { closeBundle() { if (config.command === "build") { endTime = dayjs(new Date()); - utils.getPackageSize({ + getPackageSize({ folder: outDir, callback: (size: string) => { console.log( - bold( - green( - `🎉恭喜打包完成(总用时${dayjs + boxen( + gradientString("cyan", "magenta").multiline( + `🎉 恭喜打包完成(总用时${dayjs .duration(endTime.diff(startTime)) .format("mm分ss秒")},打包后的大小为${size})` - ) + ), + boxenOptions ) ); } diff --git a/build/optimize.ts b/build/optimize.ts index 203f0072..6b9648aa 100644 --- a/build/optimize.ts +++ b/build/optimize.ts @@ -10,12 +10,14 @@ const include = [ "dayjs", "axios", "pinia", + "vue-types", "js-cookie", + "vue-tippy", + "pinyin-pro", "sortablejs", "@vueuse/core", "@pureadmin/utils", - "responsive-storage", - "element-resize-detector" + "responsive-storage" ]; /** diff --git a/build/plugins.ts b/build/plugins.ts index 682d0b20..ed27237d 100644 --- a/build/plugins.ts +++ b/build/plugins.ts @@ -2,23 +2,24 @@ import { cdn } from "./cdn"; import vue from "@vitejs/plugin-vue"; import { viteBuildInfo } from "./info"; import svgLoader from "vite-svg-loader"; +import type { PluginOption } from "vite"; import vueJsx from "@vitejs/plugin-vue-jsx"; import electron from "vite-plugin-electron"; -import { viteMockServe } from "vite-plugin-mock"; import { configCompressPlugin } from "./compress"; +import removeNoMatch from "vite-plugin-router-warn"; import renderer from "vite-plugin-electron-renderer"; -// import ElementPlus from "unplugin-element-plus/vite"; import { visualizer } from "rollup-plugin-visualizer"; import removeConsole from "vite-plugin-remove-console"; -import themePreprocessorPlugin from "@pureadmin/theme"; +import { themePreprocessorPlugin } from "@pureadmin/theme"; import { genScssMultipleScopeVars } from "../src/layout/theme"; +import { vitePluginFakeServer } from "vite-plugin-fake-server"; import pkg from "../package.json"; export function getPluginsList( command: string, VITE_CDN: boolean, VITE_COMPRESSION: ViteCompression -) { +): PluginOption[] { const prodMock = true; const isServe = command === "serve"; const isBuild = command === "build"; @@ -28,11 +29,20 @@ export function getPluginsList( vue(), // jsx、tsx语法支持 vueJsx(), - VITE_CDN ? cdn : null, - configCompressPlugin(VITE_COMPRESSION), - // 线上环境删除console - removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }), viteBuildInfo(), + /** + * 开发环境下移除非必要的vue-router动态路由警告No match found for location with path + * 非必要具体看 https://github.com/vuejs/router/issues/521 和 https://github.com/vuejs/router/issues/359 + * vite-plugin-router-warn只在开发环境下启用,只处理vue-router文件并且只在服务启动或重启时运行一次,性能消耗可忽略不计 + */ + removeNoMatch(), + // mock支持 + vitePluginFakeServer({ + logger: false, + include: "mock", + infixName: false, + enableProd: command !== "serve" && prodMock + }), // 自定义主题 themePreprocessorPlugin({ scss: { @@ -42,22 +52,14 @@ export function getPluginsList( }), // svg组件化支持 svgLoader(), - // ElementPlus({}), - // mock支持 - viteMockServe({ - mockPath: "mock", - localEnabled: isServe, - prodEnabled: command !== "serve" && prodMock, - injectCode: ` - import { setupProdMockServer } from './mockProdServer'; - setupProdMockServer(); - `, - logger: false - }), + VITE_CDN ? cdn : null, + configCompressPlugin(VITE_COMPRESSION), + // 线上环境删除console + removeConsole({ external: ["src/assets/iconfont/iconfont.js"] }), // 打包分析 lifecycle === "report" ? visualizer({ open: true, brotliSize: true, filename: "report.html" }) - : null, + : (null as any), !lifecycle.includes("browser") ? [ // 支持electron diff --git a/build/utils.ts b/build/utils.ts new file mode 100644 index 00000000..4c92d4fd --- /dev/null +++ b/build/utils.ts @@ -0,0 +1,110 @@ +import dayjs from "dayjs"; +import { readdir, stat } from "node:fs"; +import { fileURLToPath } from "node:url"; +import { dirname, resolve } from "node:path"; +import { sum, formatBytes } from "@pureadmin/utils"; +import { + name, + version, + engines, + dependencies, + devDependencies +} from "../package.json"; + +/** 启动`node`进程时所在工作目录的绝对路径 */ +const root: string = process.cwd(); + +/** + * @description 根据可选的路径片段生成一个新的绝对路径 + * @param dir 路径片段,默认`build` + * @param metaUrl 模块的完整`url`,如果在`build`目录外调用必传`import.meta.url` + */ +const pathResolve = (dir = ".", metaUrl = import.meta.url) => { + // 当前文件目录的绝对路径 + const currentFileDir = dirname(fileURLToPath(metaUrl)); + // build 目录的绝对路径 + const buildDir = resolve(currentFileDir, "build"); + // 解析的绝对路径 + const resolvedPath = resolve(currentFileDir, dir); + // 检查解析的绝对路径是否在 build 目录内 + if (resolvedPath.startsWith(buildDir)) { + // 在 build 目录内,返回当前文件路径 + return fileURLToPath(metaUrl); + } + // 不在 build 目录内,返回解析后的绝对路径 + return resolvedPath; +}; + +/** 设置别名 */ +const alias: Record = { + "@": pathResolve("../src"), + "@build": pathResolve() +}; + +/** 平台的名称、版本、运行所需的`node`版本、依赖、最后构建时间的类型提示 */ +const __APP_INFO__ = { + pkg: { name, version, engines, dependencies, devDependencies }, + lastBuildTime: dayjs(new Date()).format("YYYY-MM-DD HH:mm:ss") +}; + +/** 处理环境变量 */ +const wrapperEnv = (envConf: Recordable): ViteEnv => { + // 默认值 + const ret: ViteEnv = { + VITE_PORT: 8848, + VITE_PUBLIC_PATH: "", + VITE_ROUTER_HISTORY: "", + VITE_CDN: false, + VITE_HIDE_HOME: "false", + VITE_COMPRESSION: "none" + }; + + for (const envName of Object.keys(envConf)) { + let realName = envConf[envName].replace(/\\n/g, "\n"); + realName = + realName === "true" ? true : realName === "false" ? false : realName; + + if (envName === "VITE_PORT") { + realName = Number(realName); + } + ret[envName] = realName; + if (typeof realName === "string") { + process.env[envName] = realName; + } else if (typeof realName === "object") { + process.env[envName] = JSON.stringify(realName); + } + } + return ret; +}; + +const fileListTotal: number[] = []; + +/** 获取指定文件夹中所有文件的总大小 */ +const getPackageSize = options => { + const { folder = "dist", callback, format = true } = options; + readdir(folder, (err, files: string[]) => { + if (err) throw err; + let count = 0; + const checkEnd = () => { + ++count == files.length && + callback(format ? formatBytes(sum(fileListTotal)) : sum(fileListTotal)); + }; + files.forEach((item: string) => { + stat(`${folder}/${item}`, async (err, stats) => { + if (err) throw err; + if (stats.isFile()) { + fileListTotal.push(stats.size); + checkEnd(); + } else if (stats.isDirectory()) { + getPackageSize({ + folder: `${folder}/${item}/`, + callback: checkEnd + }); + } + }); + }); + files.length === 0 && callback(0); + }); +}; + +export { root, pathResolve, alias, __APP_INFO__, wrapperEnv, getPackageSize }; diff --git a/commitlint.config.js b/commitlint.config.js index 71bc3731..eea755d0 100644 --- a/commitlint.config.js +++ b/commitlint.config.js @@ -1,4 +1,7 @@ -module.exports = { +// @ts-check + +/** @type {import("@commitlint/types").UserConfig} */ +export default { ignores: [commit => commit.includes("init")], extends: ["@commitlint/config-conventional"], rules: { diff --git a/electron-builder.json5 b/electron-builder.json5 new file mode 100644 index 00000000..5f4764df --- /dev/null +++ b/electron-builder.json5 @@ -0,0 +1,44 @@ +// @see https://www.electron.build/configuration/configuration +{ + $schema: "https://raw.githubusercontent.com/electron-userland/electron-builder/master/packages/app-builder-lib/scheme.json", + appId: "com.pure.electron", + productName: "electron-pure-admin", + copyright: "Copyright © 2020-present, pure-admin", + publish: { + provider: "github", + releaseType: "release" + }, + directories: { + buildResources: "dist", + output: "release/${version}" + }, + files: ["dist-electron/**", "dist/**"], + nsis: { + allowToChangeInstallationDirectory: true, + createDesktopShortcut: true, + createStartMenuShortcut: true, + shortcutName: "pure-admin", + perMachine: true, + oneClick: false + }, + mac: { + icon: "dist/icons/mac/icon.icns", + artifactName: "${productName}_${version}.${ext}", + target: ["dmg"] + }, + win: { + icon: "dist/icons/win/icon.ico", + artifactName: "${productName}_${version}.${ext}", + target: [ + { + target: "nsis", + arch: ["x64"] + } + ] + }, + linux: { + icon: "dist/icons/png", + artifactName: "${productName}_${version}.${ext}", + target: ["deb", "AppImage"] + } +} diff --git a/electron/main/index.ts b/electron/main/index.ts index 71635138..cb9e1f72 100644 --- a/electron/main/index.ts +++ b/electron/main/index.ts @@ -1,5 +1,6 @@ -import { join } from "node:path"; import { release } from "node:os"; +import { fileURLToPath } from "node:url"; +import { join, dirname } from "node:path"; import { type MenuItem, type MenuItemConstructorOptions, @@ -20,6 +21,8 @@ import { // ├─┬ dist // │ └── index.html > Electron-Renderer // +const __filename = fileURLToPath(import.meta.url); +const __dirname = dirname(__filename); process.env.DIST_ELECTRON = join(__dirname, ".."); process.env.DIST = join(process.env.DIST_ELECTRON, "../dist"); process.env.PUBLIC = process.env.VITE_DEV_SERVER_URL diff --git a/electron/preload/index.ts b/electron/preload/index.ts index 8ea0afe1..077c5342 100644 --- a/electron/preload/index.ts +++ b/electron/preload/index.ts @@ -1,3 +1,31 @@ +import { ipcRenderer, contextBridge } from "electron"; + +// --------- Expose some API to the Renderer process --------- +contextBridge.exposeInMainWorld("ipcRenderer", { + on(...args: Parameters) { + const [channel, listener] = args; + return ipcRenderer.on(channel, (event, ...args) => + listener(event, ...args) + ); + }, + off(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.off(channel, ...omit); + }, + send(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.send(channel, ...omit); + }, + invoke(...args: Parameters) { + const [channel, ...omit] = args; + return ipcRenderer.invoke(channel, ...omit); + } + + // You can expose other APTs you need here. + // ... +}); + +// --------- Preload scripts loading --------- function domReady( condition: DocumentReadyState[] = ["complete", "interactive"] ) { diff --git a/eslint.config.js b/eslint.config.js new file mode 100644 index 00000000..a48bd520 --- /dev/null +++ b/eslint.config.js @@ -0,0 +1,183 @@ +import js from "@eslint/js"; +import pluginVue from "eslint-plugin-vue"; +import * as parserVue from "vue-eslint-parser"; +import configPrettier from "eslint-config-prettier"; +import pluginPrettier from "eslint-plugin-prettier"; +import { defineFlatConfig } from "eslint-define-config"; +import * as parserTypeScript from "@typescript-eslint/parser"; +import pluginTypeScript from "@typescript-eslint/eslint-plugin"; + +export default defineFlatConfig([ + { + ...js.configs.recommended, + ignores: [ + "**/.*", + "dist/*", + "release/*", + "*.d.ts", + "public/*", + "src/assets/**", + "dist-electron/*", + "src/**/iconfont/**" + ], + languageOptions: { + globals: { + // index.d.ts + RefType: "readonly", + EmitType: "readonly", + TargetContext: "readonly", + ComponentRef: "readonly", + ElRef: "readonly", + ForDataType: "readonly", + AnyFunction: "readonly", + PropType: "readonly", + Writable: "readonly", + Nullable: "readonly", + NonNullable: "readonly", + Recordable: "readonly", + ReadonlyRecordable: "readonly", + Indexable: "readonly", + DeepPartial: "readonly", + Without: "readonly", + Exclusive: "readonly", + TimeoutHandle: "readonly", + IntervalHandle: "readonly", + Effect: "readonly", + ChangeEvent: "readonly", + WheelEvent: "readonly", + ImportMetaEnv: "readonly", + Fn: "readonly", + PromiseFn: "readonly", + ComponentElRef: "readonly", + parseInt: "readonly", + parseFloat: "readonly" + } + }, + plugins: { + prettier: pluginPrettier + }, + rules: { + ...configPrettier.rules, + ...pluginPrettier.configs.recommended.rules, + "no-debugger": "off", + "no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_" + } + ], + "prettier/prettier": [ + "error", + { + endOfLine: "auto" + } + ] + } + }, + { + files: ["**/*.?([cm])ts", "**/*.?([cm])tsx"], + languageOptions: { + parser: parserTypeScript, + parserOptions: { + sourceType: "module" + } + }, + plugins: { + "@typescript-eslint": pluginTypeScript + }, + rules: { + ...pluginTypeScript.configs.strict.rules, + "@typescript-eslint/ban-types": "off", + "@typescript-eslint/no-redeclare": "error", + "@typescript-eslint/ban-ts-comment": "off", + "@typescript-eslint/no-explicit-any": "off", + "@typescript-eslint/prefer-as-const": "warn", + "@typescript-eslint/no-empty-function": "off", + "@typescript-eslint/no-non-null-assertion": "off", + "@typescript-eslint/no-import-type-side-effects": "error", + "@typescript-eslint/explicit-module-boundary-types": "off", + "@typescript-eslint/consistent-type-imports": [ + "error", + { disallowTypeAnnotations: false, fixStyle: "inline-type-imports" } + ], + "@typescript-eslint/prefer-literal-enum-member": [ + "error", + { allowBitwiseExpressions: true } + ], + "@typescript-eslint/no-unused-vars": [ + "error", + { + argsIgnorePattern: "^_", + varsIgnorePattern: "^_" + } + ] + } + }, + { + files: ["**/*.d.ts"], + rules: { + "eslint-comments/no-unlimited-disable": "off", + "import/no-duplicates": "off", + "unused-imports/no-unused-vars": "off" + } + }, + { + files: ["**/*.?([cm])js"], + rules: { + "@typescript-eslint/no-require-imports": "off", + "@typescript-eslint/no-var-requires": "off" + } + }, + { + files: ["**/*.vue"], + languageOptions: { + globals: { + $: "readonly", + $$: "readonly", + $computed: "readonly", + $customRef: "readonly", + $ref: "readonly", + $shallowRef: "readonly", + $toRef: "readonly" + }, + parser: parserVue, + parserOptions: { + ecmaFeatures: { + jsx: true + }, + extraFileExtensions: [".vue"], + parser: "@typescript-eslint/parser", + sourceType: "module" + } + }, + plugins: { + vue: pluginVue + }, + processor: pluginVue.processors[".vue"], + rules: { + ...pluginVue.configs.base.rules, + ...pluginVue.configs["vue3-essential"].rules, + ...pluginVue.configs["vue3-recommended"].rules, + "no-undef": "off", + "no-unused-vars": "off", + "vue/no-v-html": "off", + "vue/require-default-prop": "off", + "vue/require-explicit-emits": "off", + "vue/multi-word-component-names": "off", + "vue/no-setup-props-reactivity-loss": "off", + "vue/html-self-closing": [ + "error", + { + html: { + void: "always", + normal: "always", + component: "always" + }, + svg: "always", + math: "always" + } + ] + } + } +]); diff --git a/mock/asyncRoutes.ts b/mock/asyncRoutes.ts index da6a559f..202bf362 100644 --- a/mock/asyncRoutes.ts +++ b/mock/asyncRoutes.ts @@ -1,17 +1,16 @@ // 模拟后端动态生成路由 -import { MockMethod } from "vite-plugin-mock"; +import { defineFakeRoute } from "vite-plugin-fake-server/client"; /** * roles:页面级别权限,这里模拟二种 "admin"、"common" * admin:管理员角色 * common:普通角色 */ - const permissionRouter = { path: "/permission", meta: { title: "权限管理", - icon: "lollipop", + icon: "ep:lollipop", rank: 10 }, children: [ @@ -29,15 +28,19 @@ const permissionRouter = { meta: { title: "按钮权限", roles: ["admin", "common"], - auths: ["btn_add", "btn_edit", "btn_delete"] + auths: [ + "permission:btn:add", + "permission:btn:edit", + "permission:btn:delete" + ] } } ] }; -export default [ +export default defineFakeRoute([ { - url: "/getAsyncRoutes", + url: "/get-async-routes", method: "get", response: () => { return { @@ -46,4 +49,4 @@ export default [ }; } } -] as MockMethod[]; +]); diff --git a/mock/login.ts b/mock/login.ts index cddd4e42..a9c71b15 100644 --- a/mock/login.ts +++ b/mock/login.ts @@ -1,7 +1,7 @@ // 根据角色动态生成路由 -import { MockMethod } from "vite-plugin-mock"; +import { defineFakeRoute } from "vite-plugin-fake-server/client"; -export default [ +export default defineFakeRoute([ { url: "/login", method: "post", @@ -10,27 +10,30 @@ export default [ return { success: true, data: { + avatar: "https://avatars.githubusercontent.com/u/44761321", username: "admin", + nickname: "小铭", // 一个用户可能有多个角色 roles: ["admin"], accessToken: "eyJhbGciOiJIUzUxMiJ9.admin", refreshToken: "eyJhbGciOiJIUzUxMiJ9.adminRefresh", - expires: "2023/10/30 00:00:00" + expires: "2030/10/30 00:00:00" } }; } else { return { success: true, data: { + avatar: "https://avatars.githubusercontent.com/u/52823142", username: "common", - // 一个用户可能有多个角色 + nickname: "小林", roles: ["common"], accessToken: "eyJhbGciOiJIUzUxMiJ9.common", refreshToken: "eyJhbGciOiJIUzUxMiJ9.commonRefresh", - expires: "2023/10/30 00:00:00" + expires: "2030/10/30 00:00:00" } }; } } } -] as MockMethod[]; +]); diff --git a/mock/refreshToken.ts b/mock/refreshToken.ts index 87b89953..34d0e876 100644 --- a/mock/refreshToken.ts +++ b/mock/refreshToken.ts @@ -1,9 +1,9 @@ -import { MockMethod } from "vite-plugin-mock"; +import { defineFakeRoute } from "vite-plugin-fake-server/client"; // 模拟刷新token接口 -export default [ +export default defineFakeRoute([ { - url: "/refreshToken", + url: "/refresh-token", method: "post", response: ({ body }) => { if (body.refreshToken) { @@ -13,7 +13,7 @@ export default [ accessToken: "eyJhbGciOiJIUzUxMiJ9.newAdmin", refreshToken: "eyJhbGciOiJIUzUxMiJ9.newAdminRefresh", // `expires`选择这种日期格式是为了方便调试,后端直接设置时间戳或许更方便(每次都应该递增)。如果后端返回的是时间戳格式,前端开发请来到这个目录`src/utils/auth.ts`,把第`38`行的代码换成expires = data.expires即可。 - expires: "2023/10/30 23:59:59" + expires: "2030/10/30 23:59:59" } }; } else { @@ -24,4 +24,4 @@ export default [ } } } -] as MockMethod[]; +]); diff --git a/package.json b/package.json index b95c6ac2..fe719d06 100644 --- a/package.json +++ b/package.json @@ -1,139 +1,151 @@ { "name": "electron-pure-admin", - "version": "4.1.0", + "version": "5.5.0", "description": "electron-pure-admin", "private": true, + "type": "module", "main": "dist-electron/main/index.js", "scripts": { "dev": "vite", "serve": "vite", "icon": "esno build/icon.ts --input=public/icon.png --output=dist", - "build": "rimraf dist && rimraf release && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build && yarn icon && electron-builder build --config .electron-builder.config.js", - "build:staging": "rimraf dist && rimraf release && vite build --mode staging && yarn icon && electron-builder build --config .electron-builder.config.js", + "build": "rimraf dist && rimraf release && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build && yarn icon && electron-builder", + "build:staging": "rimraf dist && rimraf release && vite build --mode staging && yarn icon && electron-builder", "browser:dev": "cross-env NODE_OPTIONS=--max-old-space-size=4096 vite", "browser:build": "rimraf dist && cross-env NODE_OPTIONS=--max-old-space-size=8192 vite build", "preview": "vite preview", "preview:build": "yarn browser:build && vite preview", "report": "rimraf dist && vite build", "typecheck": "tsc --noEmit && vue-tsc --noEmit --skipLibCheck", - "svgo": "svgo -f src/assets/svg -o src/assets/svg", - "cloc": "cross-env NODE_OPTIONS=--max-old-space-size=4096 cloc . --exclude-dir=node_modules --exclude-lang=YAML", - "clean:cache": "rm -rf node_modules && rm -rf .eslintcache && npm cache clean --force && yarn install", + "svgo": "svgo -f . -r", + "clean:cache": "rimraf .eslintcache && rimraf node_modules && yarn cache clean && yarn install", "lint:eslint": "eslint --cache --max-warnings 0 \"{src,mock,build}/**/*.{vue,js,ts,tsx}\" --fix", "lint:prettier": "prettier --write \"src/**/*.{js,ts,json,tsx,css,scss,vue,html,md}\"", - "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", - "lint:lint-staged": "lint-staged -c ./.husky/lintstagedrc.js", - "lint:pretty": "pretty-quick --staged", + "lint:stylelint": "stylelint --cache --fix \"**/*.{html,vue,css,scss}\" --cache-location node_modules/.cache/stylelint/", "lint": "yarn lint:eslint && yarn lint:prettier && yarn lint:stylelint", - "prepare": "husky install" + "prepare": "husky" }, - "browserslist": [ - "> 1%", - "not ie 11", - "not op_mini all" + "keywords": [ + "electron-pure-admin", + "vue-pure-admin", + "element-plus", + "tailwindcss", + "pure-admin", + "typescript", + "electron", + "pinia", + "vue3", + "vite", + "esm" ], + "homepage": "https://github.com/pure-admin/electron-pure-admin", + "repository": { + "type": "git", + "url": "git+https://github.com/pure-admin/electron-pure-admin.git" + }, + "bugs": { + "url": "https://github.com/pure-admin/electron-pure-admin/issues" + }, + "license": "MIT", + "author": { + "name": "xiaoxian521", + "email": "pureadmin@163.com", + "url": "https://github.com/xiaoxian521" + }, "dependencies": { - "@pureadmin/descriptions": "^1.1.1", - "@pureadmin/table": "^2.1.0", - "@pureadmin/utils": "^1.8.9", - "@vueuse/core": "^10.1.2", - "@vueuse/motion": "2.0.0-beta.12", + "@pureadmin/descriptions": "^1.2.1", + "@pureadmin/table": "^3.1.2", + "@pureadmin/utils": "^2.4.7", + "@vueuse/core": "^10.9.0", + "@vueuse/motion": "^2.1.0", "animate.css": "^4.1.1", - "axios": "^1.4.0", - "dayjs": "^1.11.7", - "echarts": "^5.4.2", - "element-plus": "^2.3.4", - "element-resize-detector": "^1.2.4", + "axios": "^1.6.8", + "dayjs": "^1.11.11", + "echarts": "^5.5.0", + "element-plus": "2.7.1", "js-cookie": "^3.0.5", - "mitt": "^3.0.0", - "mockjs": "^1.1.0", + "localforage": "^1.10.0", + "mitt": "^3.0.1", "nprogress": "^0.2.0", "path": "^0.12.7", - "pinia": "^2.0.36", - "qs": "^6.11.1", + "pinia": "^2.1.7", + "pinyin-pro": "^3.20.4", + "qs": "^6.12.1", "responsive-storage": "^2.2.0", - "sortablejs": "^1.15.0", - "vue": "^3.3.1", - "vue-router": "^4.1.6", - "vue-types": "^5.0.2" + "sortablejs": "^1.15.2", + "vue": "^3.4.27", + "vue-router": "^4.3.2", + "vue-tippy": "^6.4.1", + "vue-types": "^5.1.2" }, "devDependencies": { - "@commitlint/cli": "^17.6.3", - "@commitlint/config-conventional": "^17.6.3", - "@iconify-icons/ep": "^1.2.11", - "@iconify-icons/ri": "^1.2.7", - "@iconify/vue": "^4.1.1", - "@pureadmin/theme": "^3.0.0", - "@types/element-resize-detector": "1.1.3", - "@types/js-cookie": "^3.0.3", - "@types/mockjs": "^1.0.7", - "@types/node": "^18.15.12", - "@types/nprogress": "0.2.0", - "@types/qs": "^6.9.7", - "@types/sortablejs": "^1.15.1", - "@typescript-eslint/eslint-plugin": "^5.59.5", - "@typescript-eslint/parser": "^5.59.5", - "@vitejs/plugin-vue": "^4.2.2", - "@vitejs/plugin-vue-jsx": "^3.0.1", - "@vue/eslint-config-prettier": "^7.1.0", - "@vue/eslint-config-typescript": "^11.0.3", + "@commitlint/cli": "^19.3.0", + "@commitlint/config-conventional": "^19.2.2", + "@commitlint/types": "^19.0.3", + "@eslint/js": "^9.2.0", + "@faker-js/faker": "^8.4.1", + "@iconify-icons/ep": "^1.2.12", + "@iconify-icons/ri": "^1.2.10", + "@iconify/vue": "^4.1.2", + "@pureadmin/theme": "^3.2.0", + "@types/args": "^5.0.3", + "@types/gradient-string": "^1.1.6", + "@types/js-cookie": "^3.0.6", + "@types/node": "^20.12.11", + "@types/nprogress": "^0.2.3", + "@types/qs": "^6.9.15", + "@types/sortablejs": "^1.15.8", + "@typescript-eslint/eslint-plugin": "^7.8.0", + "@typescript-eslint/parser": "^7.8.0", + "@vitejs/plugin-vue": "^5.0.4", + "@vitejs/plugin-vue-jsx": "^3.1.0", "args": "^5.0.3", - "autoprefixer": "^10.4.14", - "cloc": "^2.11.0", + "autoprefixer": "^10.4.19", + "boxen": "^7.1.1", "cross-env": "^7.0.3", - "cssnano": "^6.0.1", - "electron": "^25.0.0", - "electron-builder": "^23.6.0", - "eslint": "^8.40.0", - "eslint-plugin-prettier": "^4.2.1", - "eslint-plugin-vue": "^9.12.0", - "esno": "^0.17.0", - "husky": "^8.0.3", - "icon-gen": "^3.0.1", - "jimp": "^0.22.10", - "lint-staged": "^13.2.2", - "picocolors": "^1.0.0", - "postcss": "^8.4.23", - "postcss-html": "^1.5.0", - "postcss-import": "^15.1.0", - "postcss-scss": "^4.0.6", - "prettier": "^2.8.7", - "pretty-quick": "3.1.1", - "rimraf": "^5.0.0", - "rollup-plugin-visualizer": "^5.9.0", - "sass": "^1.62.1", - "sass-loader": "^13.2.2", - "stylelint": "^15.6.1", - "stylelint-config-html": "^1.1.0", - "stylelint-config-recess-order": "^4.0.0", - "stylelint-config-recommended": "^12.0.0", - "stylelint-config-recommended-scss": "^11.0.0", - "stylelint-config-recommended-vue": "^1.4.0", - "stylelint-config-standard": "^33.0.0", - "stylelint-config-standard-scss": "^9.0.0", - "stylelint-order": "^6.0.3", - "stylelint-prettier": "^3.0.0", - "stylelint-scss": "^5.0.0", - "svgo": "^3.0.2", - "tailwindcss": "^3.3.2", - "terser": "^5.17.1", - "typescript": "^5.0.4", - "vite": "^4.3.9", + "cssnano": "^7.0.1", + "electron": "^30.0.3", + "electron-builder": "^24.13.3", + "eslint": "^9.2.0", + "eslint-config-prettier": "^9.1.0", + "eslint-define-config": "^2.1.0", + "eslint-plugin-prettier": "^5.1.3", + "eslint-plugin-vue": "^9.25.0", + "esno": "^4.7.0", + "gradient-string": "^2.0.2", + "husky": "^9.0.11", + "icon-gen": "^4.0.0", + "jimp": "^0.22.12", + "lint-staged": "^15.2.2", + "postcss": "^8.4.38", + "postcss-html": "^1.7.0", + "postcss-import": "^16.1.0", + "postcss-scss": "^4.0.9", + "prettier": "^3.2.5", + "rimraf": "^5.0.5", + "rollup-plugin-visualizer": "^5.12.0", + "sass": "^1.77.0", + "stylelint": "^16.5.0", + "stylelint-config-recess-order": "^5.0.1", + "stylelint-config-recommended-vue": "^1.5.0", + "stylelint-config-standard-scss": "^13.1.0", + "stylelint-prettier": "^5.0.0", + "svgo": "^3.3.0", + "tailwindcss": "^3.4.3", + "typescript": "^5.4.5", + "vite": "^5.2.11", "vite-plugin-cdn-import": "^0.3.5", "vite-plugin-compression": "^0.5.1", - "vite-plugin-electron": "^0.11.2", + "vite-plugin-electron": "^0.28.7", "vite-plugin-electron-renderer": "^0.14.5", - "vite-plugin-mock": "^2.9.6", - "vite-plugin-remove-console": "^2.1.1", - "vite-svg-loader": "^4.0.0", - "vue-eslint-parser": "^9.2.1", - "vue-tsc": "^1.6.4" - }, - "repository": "https://github.com/pure-admin/electron-pure-admin.git", - "author": { - "email": "822388890@qq.com", - "name": "xiaoxian521", - "url": "https://github.com/xiaoxian521" + "vite-plugin-fake-server": "^2.1.1", + "vite-plugin-remove-console": "^2.2.0", + "vite-plugin-router-warn": "^1.0.0", + "vite-svg-loader": "^5.1.0", + "vue-eslint-parser": "^9.4.2", + "vue-tsc": "^1.8.27" }, - "license": "MIT" + "engines": { + "node": "^18.18.0 || ^20.9.0 || >=21.1.0" + } } diff --git a/postcss.config.js b/postcss.config.js index f479069f..86239486 100644 --- a/postcss.config.js +++ b/postcss.config.js @@ -1,4 +1,7 @@ -module.exports = { +// @ts-check + +/** @type {import('postcss-load-config').Config} */ +export default { plugins: { "postcss-import": {}, "tailwindcss/nesting": {}, diff --git a/public/logo.svg b/public/logo.svg index bc26056b..a63d2b1a 100644 --- a/public/logo.svg +++ b/public/logo.svg @@ -1 +1 @@ - \ No newline at end of file + \ No newline at end of file diff --git a/public/serverConfig.json b/public/platform-config.json similarity index 63% rename from public/serverConfig.json rename to public/platform-config.json index a15c2575..a81a3937 100644 --- a/public/serverConfig.json +++ b/public/platform-config.json @@ -1,22 +1,26 @@ { - "Version": "4.1.0", + "Version": "5.5.0", "Title": "PureAdmin", "FixedHeader": true, "HiddenSideBar": false, "MultiTagsCache": false, "KeepAlive": true, "Layout": "vertical", - "Theme": "default", + "Theme": "light", "DarkMode": false, + "OverallStyle": "light", "Grey": false, "Weak": false, "HideTabs": false, + "HideFooter": false, + "Stretch": false, "SidebarStatus": true, "EpThemeColor": "#409EFF", "ShowLogo": true, "ShowModel": "smart", - "MenuArrowIconNoTransition": true, + "MenuArrowIconNoTransition": false, "CachingAsyncRoutes": false, "TooltipEffect": "light", - "ResponsiveStorageNameSpace": "responsive-" + "ResponsiveStorageNameSpace": "responsive-", + "MenuSearchHistory": 6 } diff --git a/src/App.vue b/src/App.vue index 77c3cf4d..7294c443 100644 --- a/src/App.vue +++ b/src/App.vue @@ -8,8 +8,9 @@ + + diff --git a/src/config/index.ts b/src/config/index.ts index 4cd30166..c81d1c4d 100644 --- a/src/config/index.ts +++ b/src/config/index.ts @@ -1,5 +1,5 @@ -import { App } from "vue"; import axios from "axios"; +import type { App } from "vue"; let config: object = {}; const { VITE_PUBLIC_PATH } = import.meta.env; @@ -8,7 +8,7 @@ const setConfig = (cfg?: unknown) => { config = Object.assign(config, cfg); }; -const getConfig = (key?: string): ServerConfigs => { +const getConfig = (key?: string): PlatformConfigs => { if (typeof key === "string") { const arr = key.split("."); if (arr && arr.length) { @@ -27,15 +27,15 @@ const getConfig = (key?: string): ServerConfigs => { }; /** 获取项目动态全局配置 */ -export const getServerConfig = async (app: App): Promise => { +export const getPlatformConfig = async (app: App): Promise => { app.config.globalProperties.$config = getConfig(); return axios({ method: "get", - url: `${VITE_PUBLIC_PATH}serverConfig.json` + url: `${VITE_PUBLIC_PATH}platform-config.json` }) .then(({ data: config }) => { let $config = app.config.globalProperties.$config; - // 自动注入项目配置 + // 自动注入系统配置 if (app && $config && typeof config === "object") { $config = Object.assign($config, config); app.config.globalProperties.$config = $config; @@ -45,7 +45,7 @@ export const getServerConfig = async (app: App): Promise => { return $config; }) .catch(() => { - throw "请在public文件夹下添加serverConfig.json配置文件"; + throw "请在public文件夹下添加platform-config.json配置文件"; }); }; diff --git a/src/directives/auth/index.ts b/src/directives/auth/index.ts index 627ea896..a7a4f221 100644 --- a/src/directives/auth/index.ts +++ b/src/directives/auth/index.ts @@ -1,5 +1,5 @@ import { hasAuth } from "@/router/utils"; -import { Directive, type DirectiveBinding } from "vue"; +import type { Directive, DirectiveBinding } from "vue"; export const auth: Directive = { mounted(el: HTMLElement, binding: DirectiveBinding) { @@ -7,7 +7,9 @@ export const auth: Directive = { if (value) { !hasAuth(value) && el.parentNode?.removeChild(el); } else { - throw new Error("need auths! Like v-auth=\"['btn.add','btn.edit']\""); + throw new Error( + "[Directive: auth]: need auths! Like v-auth=\"['btn.add','btn.edit']\"" + ); } } }; diff --git a/src/directives/copy/index.ts b/src/directives/copy/index.ts new file mode 100644 index 00000000..8e978338 --- /dev/null +++ b/src/directives/copy/index.ts @@ -0,0 +1,33 @@ +import { message } from "@/utils/message"; +import { useEventListener } from "@vueuse/core"; +import { copyTextToClipboard } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +interface CopyEl extends HTMLElement { + copyValue: string; +} + +/** 文本复制指令(默认双击复制) */ +export const copy: Directive = { + mounted(el: CopyEl, binding: DirectiveBinding) { + const { value } = binding; + if (value) { + el.copyValue = value; + const arg = binding.arg ?? "dblclick"; + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, arg, () => { + const success = copyTextToClipboard(el.copyValue); + success + ? message("复制成功", { type: "success" }) + : message("复制失败", { type: "error" }); + }); + } else { + throw new Error( + '[Directive: copy]: need value! Like v-copy="modelValue"' + ); + } + }, + updated(el: CopyEl, binding: DirectiveBinding) { + el.copyValue = binding.value; + } +}; diff --git a/src/directives/elResizeDetector/index.ts b/src/directives/elResizeDetector/index.ts deleted file mode 100644 index af089be7..00000000 --- a/src/directives/elResizeDetector/index.ts +++ /dev/null @@ -1,27 +0,0 @@ -import { Directive, type DirectiveBinding, type VNode } from "vue"; -import elementResizeDetectorMaker from "element-resize-detector"; -import type { Erd } from "element-resize-detector"; -import { emitter } from "@/utils/mitt"; - -const erd: Erd = elementResizeDetectorMaker({ - strategy: "scroll" -}); - -export const resize: Directive = { - mounted(el: HTMLElement, binding?: DirectiveBinding, vnode?: VNode) { - erd.listenTo(el, elem => { - const width = elem.offsetWidth; - const height = elem.offsetHeight; - if (binding?.instance) { - emitter.emit("resize", { detail: { width, height } }); - } else { - vnode.el.dispatchEvent( - new CustomEvent("resize", { detail: { width, height } }) - ); - } - }); - }, - unmounted(el: HTMLElement) { - erd.uninstall(el); - } -}; diff --git a/src/directives/index.ts b/src/directives/index.ts index d6d6592d..3be2c5c1 100644 --- a/src/directives/index.ts +++ b/src/directives/index.ts @@ -1,2 +1,5 @@ export * from "./auth"; -export * from "./elResizeDetector"; +export * from "./copy"; +export * from "./longpress"; +export * from "./optimize"; +export * from "./ripple"; diff --git a/src/directives/longpress/index.ts b/src/directives/longpress/index.ts new file mode 100644 index 00000000..54427844 --- /dev/null +++ b/src/directives/longpress/index.ts @@ -0,0 +1,63 @@ +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; +import { subBefore, subAfter, isFunction } from "@pureadmin/utils"; + +export const longpress: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const cb = binding.value; + if (cb && isFunction(cb)) { + let timer = null; + let interTimer = null; + let num = 500; + let interNum = null; + const isInter = binding?.arg?.includes(":") ?? false; + + if (isInter) { + num = Number(subBefore(binding.arg, ":")); + interNum = Number(subAfter(binding.arg, ":")); + } else if (binding.arg) { + num = Number(binding.arg); + } + + const clear = () => { + if (timer) { + clearTimeout(timer); + timer = null; + } + if (interTimer) { + clearInterval(interTimer); + interTimer = null; + } + }; + + const onDownInter = (ev: PointerEvent) => { + ev.preventDefault(); + if (interTimer === null) { + interTimer = setInterval(() => cb(), interNum); + } + }; + + const onDown = (ev: PointerEvent) => { + clear(); + ev.preventDefault(); + if (timer === null) { + timer = isInter + ? setTimeout(() => { + cb(); + onDownInter(ev); + }, num) + : setTimeout(() => cb(), num); + } + }; + + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener(el, "pointerdown", onDown); + useEventListener(el, "pointerup", clear); + useEventListener(el, "pointerleave", clear); + } else { + throw new Error( + '[Directive: longpress]: need callback and callback must be a function! Like v-longpress="callback"' + ); + } + } +}; diff --git a/src/directives/optimize/index.ts b/src/directives/optimize/index.ts new file mode 100644 index 00000000..395eb456 --- /dev/null +++ b/src/directives/optimize/index.ts @@ -0,0 +1,55 @@ +import { + isArray, + throttle, + debounce, + isObject, + isFunction +} from "@pureadmin/utils"; +import { useEventListener } from "@vueuse/core"; +import type { Directive, DirectiveBinding } from "vue"; + +/** 防抖(v-optimize或v-optimize:debounce)、节流(v-optimize:throttle)指令 */ +export const optimize: Directive = { + mounted(el: HTMLElement, binding: DirectiveBinding) { + const { value } = binding; + const optimizeType = binding.arg ?? "debounce"; + const type = ["debounce", "throttle"].find(t => t === optimizeType); + if (type) { + if (value && value.event && isFunction(value.fn)) { + let params = value?.params; + if (params) { + if (isArray(params) || isObject(params)) { + params = isObject(params) ? Array.of(params) : params; + } else { + throw new Error( + "[Directive: optimize]: `params` must be an array or object" + ); + } + } + // Register using addEventListener on mounted, and removeEventListener automatically on unmounted + useEventListener( + el, + value.event, + type === "debounce" + ? debounce( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 200, + value?.immediate ?? false + ) + : throttle( + params ? () => value.fn(...params) : value.fn, + value?.timeout ?? 1000 + ) + ); + } else { + throw new Error( + "[Directive: optimize]: `event` and `fn` are required, and `fn` must be a function" + ); + } + } else { + throw new Error( + "[Directive: optimize]: only `debounce` and `throttle` are supported" + ); + } + } +}; diff --git a/src/directives/ripple/index.scss b/src/directives/ripple/index.scss new file mode 100644 index 00000000..061c82c9 --- /dev/null +++ b/src/directives/ripple/index.scss @@ -0,0 +1,48 @@ +/* stylelint-disable-next-line scss/dollar-variable-colon-space-after */ +$ripple-animation-transition-in: + transform 0.4s cubic-bezier(0, 0, 0.2, 1), + opacity 0.2s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-transition-out: opacity 0.5s cubic-bezier(0, 0, 0.2, 1) !default; +$ripple-animation-visible-opacity: 0.25 !default; + +.v-ripple { + &__container { + position: absolute; + top: 0; + left: 0; + z-index: 0; + width: 100%; + height: 100%; + overflow: hidden; + pointer-events: none; + border-radius: inherit; + contain: strict; + } + + &__animation { + position: absolute; + top: 0; + left: 0; + overflow: hidden; + pointer-events: none; + background: currentcolor; + border-radius: 50%; + opacity: 0; + will-change: transform, opacity; + + &--enter { + opacity: 0; + transition: none; + } + + &--in { + opacity: $ripple-animation-visible-opacity; + transition: $ripple-animation-transition-in; + } + + &--out { + opacity: 0; + transition: $ripple-animation-transition-out; + } + } +} diff --git a/src/directives/ripple/index.ts b/src/directives/ripple/index.ts new file mode 100644 index 00000000..06ff25f2 --- /dev/null +++ b/src/directives/ripple/index.ts @@ -0,0 +1,234 @@ +import "./index.scss"; +import { isObject } from "@pureadmin/utils"; +import type { Directive, DirectiveBinding } from "vue"; + +interface RippleOptions { + class?: string; + center?: boolean; + circle?: boolean; +} + +export interface RippleDirectiveBinding + extends Omit { + value?: boolean | { class: string }; + modifiers: { + center?: boolean; + circle?: boolean; + }; +} + +function transform(el: HTMLElement, value: string) { + el.style.transform = value; + el.style.webkitTransform = value; +} + +const calculate = ( + e: PointerEvent, + el: HTMLElement, + value: RippleOptions = {} +) => { + const offset = el.getBoundingClientRect(); + + // 获取点击位置距离 el 的垂直和水平距离 + let localX = e.clientX - offset.left; + let localY = e.clientY - offset.top; + + let radius = 0; + let scale = 0.3; + // 计算点击位置到 el 顶点最远距离,即为圆的最大半径(勾股定理) + if (el._ripple?.circle) { + scale = 0.15; + radius = el.clientWidth / 2; + radius = value.center + ? radius + : radius + Math.sqrt((localX - radius) ** 2 + (localY - radius) ** 2) / 4; + } else { + radius = Math.sqrt(el.clientWidth ** 2 + el.clientHeight ** 2) / 2; + } + + // 中心点坐标 + const centerX = `${(el.clientWidth - radius * 2) / 2}px`; + const centerY = `${(el.clientHeight - radius * 2) / 2}px`; + + // 点击位置坐标 + const x = value.center ? centerX : `${localX - radius}px`; + const y = value.center ? centerY : `${localY - radius}px`; + + return { radius, scale, x, y, centerX, centerY }; +}; + +const ripples = { + show(e: PointerEvent, el: HTMLElement, value: RippleOptions = {}) { + if (!el?._ripple?.enabled) { + return; + } + + // 创建 ripple 元素和 ripple 父元素 + const container = document.createElement("span"); + const animation = document.createElement("span"); + + container.appendChild(animation); + container.className = "v-ripple__container"; + + if (value.class) { + container.className += ` ${value.class}`; + } + + const { radius, scale, x, y, centerX, centerY } = calculate(e, el, value); + + // ripple 圆大小 + const size = `${radius * 2}px`; + + animation.className = "v-ripple__animation"; + animation.style.width = size; + animation.style.height = size; + + el.appendChild(container); + + // 获取目标元素样式表 + const computed = window.getComputedStyle(el); + // 防止 position 被覆盖导致 ripple 位置有问题 + if (computed && computed.position === "static") { + el.style.position = "relative"; + el.dataset.previousPosition = "static"; + } + + animation.classList.add("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--visible"); + transform( + animation, + `translate(${x}, ${y}) scale3d(${scale},${scale},${scale})` + ); + animation.dataset.activated = String(performance.now()); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--enter"); + animation.classList.add("v-ripple__animation--in"); + transform(animation, `translate(${centerX}, ${centerY}) scale3d(1,1,1)`); + }, 0); + }, + + hide(el: HTMLElement | null) { + if (!el?._ripple?.enabled) return; + + const ripples = el.getElementsByClassName("v-ripple__animation"); + + if (ripples.length === 0) return; + const animation = ripples[ripples.length - 1] as HTMLElement; + + if (animation.dataset.isHiding) return; + else animation.dataset.isHiding = "true"; + + const diff = performance.now() - Number(animation.dataset.activated); + const delay = Math.max(250 - diff, 0); + + setTimeout(() => { + animation.classList.remove("v-ripple__animation--in"); + animation.classList.add("v-ripple__animation--out"); + + setTimeout(() => { + const ripples = el.getElementsByClassName("v-ripple__animation"); + if (ripples.length === 1 && el.dataset.previousPosition) { + el.style.position = el.dataset.previousPosition; + delete el.dataset.previousPosition; + } + + if (animation.parentNode?.parentNode === el) + el.removeChild(animation.parentNode); + }, 300); + }, delay); + } +}; + +function isRippleEnabled(value: any): value is true { + return typeof value === "undefined" || !!value; +} + +function rippleShow(e: PointerEvent) { + const value: RippleOptions = {}; + const element = e.currentTarget as HTMLElement | undefined; + + if (!element?._ripple || element._ripple.touched) return; + + value.center = element._ripple.centered; + if (element._ripple.class) { + value.class = element._ripple.class; + } + + ripples.show(e, element, value); +} + +function rippleHide(e: Event) { + const element = e.currentTarget as HTMLElement | null; + if (!element?._ripple) return; + + window.setTimeout(() => { + if (element._ripple) { + element._ripple.touched = false; + } + }); + ripples.hide(element); +} + +function updateRipple( + el: HTMLElement, + binding: RippleDirectiveBinding, + wasEnabled: boolean +) { + const { value, modifiers } = binding; + const enabled = isRippleEnabled(value); + if (!enabled) { + ripples.hide(el); + } + + el._ripple = el._ripple ?? {}; + el._ripple.enabled = enabled; + el._ripple.centered = modifiers.center; + el._ripple.circle = modifiers.circle; + if (isObject(value) && value.class) { + el._ripple.class = value.class; + } + + if (enabled && !wasEnabled) { + el.addEventListener("pointerdown", rippleShow); + el.addEventListener("pointerup", rippleHide); + } else if (!enabled && wasEnabled) { + removeListeners(el); + } +} + +function removeListeners(el: HTMLElement) { + el.removeEventListener("pointerdown", rippleShow); + el.removeEventListener("pointerup", rippleHide); +} + +function mounted(el: HTMLElement, binding: RippleDirectiveBinding) { + updateRipple(el, binding, false); +} + +function unmounted(el: HTMLElement) { + delete el._ripple; + removeListeners(el); +} + +function updated(el: HTMLElement, binding: RippleDirectiveBinding) { + if (binding.value === binding.oldValue) { + return; + } + + const wasEnabled = isRippleEnabled(binding.oldValue); + updateRipple(el, binding, wasEnabled); +} + +/** + * @description 指令 v-ripple + * @use 用法如下 + * 1. v-ripple 代表启用基本的 ripple 功能 + * 2. v-ripple="{ class: 'text-red' }" 代表自定义 ripple 颜色,支持 tailwindcss,生效样式是 color + * 3. v-ripple.center 代表从中心扩散 + */ +export const Ripple: Directive = { + mounted, + unmounted, + updated +}; diff --git a/src/layout/components/appMain.vue b/src/layout/components/appMain.vue deleted file mode 100644 index 3f2a2449..00000000 --- a/src/layout/components/appMain.vue +++ /dev/null @@ -1,148 +0,0 @@ - - - - - diff --git a/src/layout/components/lay-content/index.vue b/src/layout/components/lay-content/index.vue new file mode 100644 index 00000000..84d1396d --- /dev/null +++ b/src/layout/components/lay-content/index.vue @@ -0,0 +1,203 @@ + + + + + diff --git a/src/layout/components/lay-footer/index.vue b/src/layout/components/lay-footer/index.vue new file mode 100644 index 00000000..77631343 --- /dev/null +++ b/src/layout/components/lay-footer/index.vue @@ -0,0 +1,31 @@ + + + + + diff --git a/src/layout/components/lay-frame/index.vue b/src/layout/components/lay-frame/index.vue new file mode 100644 index 00000000..b2bb9d51 --- /dev/null +++ b/src/layout/components/lay-frame/index.vue @@ -0,0 +1,79 @@ + + diff --git a/src/layout/components/navbar.vue b/src/layout/components/lay-navbar/index.vue similarity index 73% rename from src/layout/components/navbar.vue rename to src/layout/components/lay-navbar/index.vue index 338bbdda..4e0cfcda 100644 --- a/src/layout/components/navbar.vue +++ b/src/layout/components/lay-navbar/index.vue @@ -1,10 +1,12 @@