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": [
"",
- "\t\n",
- "\t
",
+ "\ttest
",
"\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",
"",
- "\t\n",
- "\t
",
+ "\ttest
",
"\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",
+ "",
+ "\ttest
",
+ "\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 @@
-
-
+
-
-
+