├── .editorconfig ├── .env ├── .env.development ├── .env.production ├── .env.test ├── .eslintignore ├── .eslintrc.js ├── .gitignore ├── .husky └── pre-commit ├── .npmrc ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.js ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── index.html ├── lint-staged.config.js ├── mock ├── global.ts └── user.ts ├── package.json ├── pnpm-lock.yaml ├── public └── favicon.ico ├── src ├── @types │ ├── config.settings.d.ts │ ├── i18n.d.ts │ ├── nprogress.d.ts │ ├── utils.request.d.ts │ ├── vite-env.d.ts │ └── vue-router.d.ts ├── App.vue ├── assets │ ├── css │ │ ├── element-plus.scss │ │ ├── global.scss │ │ ├── mixin.scss │ │ ├── normalize.css │ │ └── variables.scss │ ├── iconsvg │ │ ├── arrow-down.svg │ │ ├── arrow-left.svg │ │ ├── arrow-right.svg │ │ ├── arrow-up.svg │ │ ├── close.svg │ │ ├── fold.svg │ │ ├── language-outline.svg │ │ ├── lock.svg │ │ ├── menu-anomaly.svg │ │ ├── menu-detail.svg │ │ ├── menu-edit.svg │ │ ├── menu-home.svg │ │ ├── menu-link.svg │ │ ├── menu-list.svg │ │ ├── menu-permission.svg │ │ ├── menu-result.svg │ │ ├── moon.svg │ │ ├── more.svg │ │ ├── pwd.svg │ │ ├── router.svg │ │ ├── search.svg │ │ ├── set.svg │ │ ├── sun.svg │ │ ├── theme.svg │ │ ├── unfold.svg │ │ ├── unlock.svg │ │ └── user.svg │ └── images │ │ ├── bg.svg │ │ └── logo.png ├── components │ ├── ALink │ │ └── index.vue │ ├── IconSvg │ │ └── index.vue │ ├── PageLoading │ │ └── index.vue │ ├── Permission │ │ └── index.vue │ ├── Result │ │ ├── index.vue │ │ └── svg │ │ │ ├── error.vue │ │ │ ├── info.vue │ │ │ ├── noFound.vue │ │ │ ├── serverError.vue │ │ │ ├── success.vue │ │ │ ├── unauthorized.vue │ │ │ └── warning.vue │ ├── ScreenTable │ │ ├── data.d.ts │ │ ├── index.vue │ │ └── search.vue │ ├── SelectLang │ │ └── index.vue │ └── Spin │ │ └── index.vue ├── composables │ ├── useEcharts.ts │ ├── useI18n.ts │ ├── useMenuLayout.ts │ ├── useMenuStyle.ts │ ├── useTheme.ts │ └── useTitle.ts ├── config │ ├── router.ts │ ├── settings.ts │ └── store.ts ├── directives │ └── vPermission.ts ├── enums │ └── utils.request.enum.ts ├── layouts │ ├── MemberLayout │ │ ├── components │ │ │ ├── BreadCrumbs.vue │ │ │ ├── LeftSider.vue │ │ │ ├── Main.vue │ │ │ ├── RightTop.vue │ │ │ ├── RightTopTabNav.vue │ │ │ ├── RightTopUser.vue │ │ │ ├── Settings.vue │ │ │ ├── SiderMenu.vue │ │ │ └── SiderMenuItem.vue │ │ ├── css │ │ │ ├── index.scss │ │ │ └── variables.scss │ │ ├── index.vue │ │ ├── locales │ │ │ ├── en-US.ts │ │ │ ├── index.ts │ │ │ ├── zh-CN.ts │ │ │ └── zh-TW.ts │ │ ├── routes.ts │ │ └── store │ │ │ └── rightTopTabNav.ts │ ├── SecurityLayout.vue │ └── UserLayout │ │ ├── index.vue │ │ └── routes.ts ├── locales │ ├── en-US.ts │ ├── index.ts │ ├── zh-CN.ts │ └── zh-TW.ts ├── main.ts ├── pages │ ├── 404 │ │ └── index.vue │ ├── detail │ │ ├── basic │ │ │ └── index.vue │ │ ├── module │ │ │ └── index.vue │ │ └── table │ │ │ └── index.vue │ ├── exception │ │ ├── 403 │ │ │ └── index.vue │ │ ├── 404 │ │ │ └── index.vue │ │ └── 500 │ │ │ └── index.vue │ ├── form │ │ ├── base │ │ │ ├── data.d.ts │ │ │ └── index.vue │ │ └── step │ │ │ └── index.vue │ ├── home │ │ ├── components │ │ │ ├── ArticleChartCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── ArticleHitCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── HotSearchCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── HotTagsCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── LinksChartCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── TopicsChartCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ ├── WorksChartCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ │ └── WorksHitCard │ │ │ │ ├── data.d.ts │ │ │ │ ├── index.vue │ │ │ │ └── service.ts │ │ └── index.vue │ ├── list │ │ ├── basic │ │ │ ├── data.d.ts │ │ │ ├── index.vue │ │ │ └── service.ts │ │ ├── filter │ │ │ ├── components │ │ │ │ └── TypeSelect │ │ │ │ │ └── index.vue │ │ │ ├── data.d.ts │ │ │ ├── index.vue │ │ │ └── service.ts │ │ ├── highlyAdaptive │ │ │ ├── data.d.ts │ │ │ ├── index.vue │ │ │ └── service.ts │ │ └── highlyAdaptive2 │ │ │ ├── components │ │ │ └── TypeSelect │ │ │ │ └── index.vue │ │ │ ├── data.d.ts │ │ │ ├── index.vue │ │ │ └── service.ts │ ├── permission │ │ ├── all │ │ │ └── index.vue │ │ ├── test │ │ │ └── index.vue │ │ └── user │ │ │ └── index.vue │ ├── result │ │ ├── fail │ │ │ └── index.vue │ │ └── success │ │ │ └── index.vue │ ├── routeMetaExtend │ │ └── breadcrumb │ │ │ └── index.vue │ └── user │ │ └── login │ │ ├── data.d.ts │ │ ├── index.vue │ │ └── server.ts ├── services │ └── user.ts ├── store │ ├── global.ts │ ├── i18n.ts │ └── user.ts └── utils │ ├── i18n.ts │ ├── is.ts │ ├── localToken.ts │ ├── object.ts │ ├── request.ts │ └── router.ts ├── svgo.config.js ├── tsconfig.json ├── tsconfig.node.json └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | # @see: http://editorconfig.org 2 | root = true 3 | 4 | [*] # 表示所有文件适用 5 | charset = utf-8 # 设置文件字符集为 utf-8 6 | indent_style = tab # 缩进风格(tab | space) 7 | indent_size = 2 # 缩进大小 8 | max_line_length = 120 # 最大行长度 9 | end_of_line = crlf # 控制换行类型(lf | cr | crlf) 10 | trim_trailing_whitespace = true # 开启末尾空格修剪 11 | insert_final_newline = true # 始终在文件末尾插入一个新行 12 | 13 | [*.md] 14 | trim_trailing_whitespace = false # 关闭末尾空格修剪 15 | max_line_length = off # 关闭最大行长度限制 16 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | # port 2 | VITE_APP_PORT = 3001 3 | 4 | # open 运行 pnpm run dev 时自动打开浏览器 5 | VITE_APP_OPEN = true 6 | 7 | # 是否生成包预览文件 8 | VITE_APP_REPORT = false 9 | 10 | # 是否开启gzip压缩 11 | VITE_APP_BUILD_GZIP = false 12 | 13 | # mock 是否开启 true|false 14 | VITE_APP_MOCK = false 15 | 16 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | # 本地环境 2 | NODE_ENV = development 3 | 4 | # mock 是否开启 true|false 5 | VITE_APP_MOCK = true 6 | 7 | # 接口地址 8 | VITE_APP_API_URL = /api 9 | 10 | # VITE_APP_API_URL对应的代理地址,空则不启用 (开发环境vite使用) 11 | VITE_APP_API_URL_PROXY = http://yapi.liqingsong.cc/mock/11 12 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 线上环境 2 | NODE_ENV = production 3 | 4 | # 接口地址 5 | VITE_APP_API_URL = /api 6 | 7 | # VITE_APP_API_URL对应的代理地址,空则不启用 (开发环境vite使用) 8 | # 如果是正式环境,可以用服务端代理,如:ng。或者不用代理,把VITE_APP_API_URL设置成绝对地址,如:http://api.xxx.com 9 | VITE_APP_API_URL_PROXY = 10 | -------------------------------------------------------------------------------- /.env.test: -------------------------------------------------------------------------------- 1 | # 测试环境 2 | NODE_ENV = test 3 | 4 | # 测试环境接口地址 5 | VITE_APP_API_URL = /api 6 | 7 | # VITE_APP_API_URL对应的代理地址,空则不启用 (开发环境vite使用) 8 | VITE_APP_API_URL_PROXY = 9 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | .idea 8 | dist 9 | /public 10 | /docs 11 | .husky 12 | .local 13 | .npmrc 14 | /bin 15 | .eslintrc.js 16 | .prettierrc.js 17 | lint-staged.config.js 18 | svgo.config.js 19 | /mock/* 20 | 21 | -------------------------------------------------------------------------------- /.eslintrc.js: -------------------------------------------------------------------------------- 1 | // @see: http://eslint.cn 2 | module.exports = { 3 | root: true, 4 | env: { 5 | browser: true, 6 | node: true, 7 | es6: true, 8 | }, 9 | // 指定如何解析语法 10 | parser: "vue-eslint-parser", 11 | // 优先级低于 parse 的语法解析配置 12 | parserOptions: { 13 | parser: "@typescript-eslint/parser", 14 | ecmaVersion: 2020, 15 | sourceType: "module", 16 | jsxPragma: "React", 17 | ecmaFeatures: { 18 | jsx: true, 19 | }, 20 | }, 21 | // 继承某些已有的规则 22 | extends: [ 23 | "prettier", 24 | "plugin:prettier/recommended", 25 | "plugin:vue/vue3-recommended", 26 | "plugin:@typescript-eslint/recommended", 27 | ], 28 | // 插件 29 | plugins: ["eslint-plugin-prettier"], 30 | /* 31 | * "off" 或 0 ==> 关闭规则 32 | * "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行) 33 | * "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错) 34 | */ 35 | rules: { 36 | // eslint (@see http://eslint.cn/docs/rules) 37 | "no-var": "error", // 要求使用 let 或 const 而不是 var 38 | "no-multiple-empty-lines": ["error", { max: 1 }], // 不允许多个空行 39 | "no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们 40 | "prefer-const": "off", // 此规则旨在标记使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const 41 | "no-irregular-whitespace": "off", // 禁止不规则的空白 42 | quotes: "off", // 禁止使用一致的反勾号、双引号或单引号 43 | 44 | // typeScript (@see https://typescript-eslint.io/rules) 45 | "@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量 46 | "@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长 47 | "@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间。 48 | "@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型 49 | "@typescript-eslint/ban-ts-ignore": "off", // 禁止使用 @ts-ignore 50 | "@typescript-eslint/ban-types": "off", // 禁止使用特定类型 51 | "@typescript-eslint/explicit-function-return-type": "off", // 不允许对初始化为数字、字符串或布尔值的变量或参数进行显式类型声明 52 | "@typescript-eslint/no-var-requires": "off", // 不允许在 import 语句中使用 require 语句 53 | "@typescript-eslint/no-empty-function": "off", // 禁止空函数 54 | "@typescript-eslint/no-use-before-define": "off", // 禁止在变量定义之前使用它们 55 | "@typescript-eslint/ban-ts-comment": "off", // 禁止 @ts- 使用注释或要求在指令后进行描述 56 | "@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!) 57 | "@typescript-eslint/explicit-module-boundary-types": "off", // 要求导出函数和类的公共类方法的显式返回和参数类型 58 | "@typescript-eslint/quotes": ["error", "double"], // 要求尽可能地使用双引号 59 | 60 | // vue (@see https://eslint.vuejs.org/rules) 61 | "vue/no-v-html": "off", // 禁止使用 v-html 62 | "vue/script-setup-uses-vars": "error", // 防止 12 | 13 | 14 | -------------------------------------------------------------------------------- /lint-staged.config.js: -------------------------------------------------------------------------------- 1 | // @see: https://github.com/okonet/lint-staged 2 | module.exports = { 3 | "*.{js,jsx,ts,tsx}": ["eslint --fix", "prettier --write"], 4 | "{!(package)*.json,*.code-snippets,.!(browserslist)*rc}": ["prettier --write--parser json"], 5 | "package.json": ["prettier --write"], 6 | "*.vue": ["eslint --fix", "prettier --write", "stylelint --fix"], 7 | "*.{scss,less,styl,html}": ["stylelint --fix", "prettier --write"], 8 | "*.md": ["prettier --write"], 9 | }; 10 | -------------------------------------------------------------------------------- /mock/global.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock"; 2 | 3 | export default [ 4 | { 5 | url: "/api/test", 6 | method: "get", 7 | response: ({ headers, body }) => { 8 | return { 9 | code: 0, 10 | data: "测试mock接口功能", 11 | }; 12 | }, 13 | }, 14 | { 15 | url: "/api/uploads", 16 | method: "POST", 17 | response: () => { 18 | return { 19 | code: 0, 20 | data: { 21 | id: 1, 22 | url: "http://uploads.liqingsong.cc/20200531/583057e8-8bab-4eee-b5a0-bec915089c0c.jpg", 23 | name: "xcx.jpg", 24 | }, 25 | }; 26 | }, 27 | }, 28 | { 29 | url: "/api/500", 30 | method: "get", 31 | // statusCode: 401, 32 | response: ({ headers, body }) => { 33 | return { 34 | timestamp: 1513932555104, 35 | status: 500, 36 | error: "error", 37 | message: "error", 38 | path: "/500", 39 | }; 40 | }, 41 | }, 42 | ] as MockMethod[]; 43 | -------------------------------------------------------------------------------- /mock/user.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock"; 2 | const ajaxHeadersTokenKey = "x-token"; 3 | export default [ 4 | { 5 | url: "/api/user/login", 6 | method: "post", 7 | response: ({ headers, body }) => { 8 | const { password, username } = body; 9 | const send = { code: 0, data: {}, msg: "" }; 10 | if (username === "admin" && password === "123456") { 11 | send["data"] = { 12 | token: "admin", 13 | }; 14 | } else if (username === "user" && password === "123456") { 15 | send["data"] = { 16 | token: "user", 17 | }; 18 | } else if (username === "test" && password === "123456") { 19 | send["data"] = { 20 | token: "test", 21 | }; 22 | } else { 23 | send["code"] = 201; 24 | send["msg"] = "Wrong username or password"; 25 | } 26 | return send; 27 | }, 28 | }, 29 | { 30 | url: "/api/user/info", 31 | method: "get", 32 | response: ({ headers, body }) => { 33 | if (headers[ajaxHeadersTokenKey] === "admin") { 34 | return { 35 | code: 0, 36 | data: { 37 | id: 1, 38 | name: "Admins-mock", 39 | avatar: "", 40 | roles: ["admin"], 41 | }, 42 | }; 43 | } else if (headers[ajaxHeadersTokenKey] === "user") { 44 | return { 45 | code: 0, 46 | data: { 47 | id: 2, 48 | name: "Users", 49 | avatar: "", 50 | roles: ["user"], 51 | }, 52 | }; 53 | } else if (headers[ajaxHeadersTokenKey] === "test") { 54 | return { 55 | code: 0, 56 | data: { 57 | id: 3, 58 | name: "Tests", 59 | avatar: "", 60 | roles: ["test"], 61 | }, 62 | }; 63 | } else { 64 | return { 65 | code: 10002, 66 | data: {}, 67 | msg: "未登录", 68 | }; 69 | } 70 | }, 71 | }, 72 | ] as MockMethod[]; 73 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "admin-element-vue-vite-ts", 3 | "description": "Vue3.x Element-Plus Pinia Vite Admin", 4 | "private": true, 5 | "version": "2.0.0", 6 | "author": "LiQingSong <957698457@qq.com>", 7 | "scripts": { 8 | "dev": "vite", 9 | "build": "vue-tsc --noEmit && vite build", 10 | "build:dev": "vue-tsc --noEmit && vite build --mode development", 11 | "build:test": "vue-tsc --noEmit && vite build --mode test", 12 | "preview": "vite preview", 13 | "svgo": "svgo -f src/assets/iconsvg", 14 | "lint:eslint": "eslint --fix --ext .js,.ts,.vue ./src", 15 | "lint:prettier": "prettier --write --loglevel warn \"src/**/*.{js,ts,json,tsx,css,less,scss,vue,html,md}\"", 16 | "lint:stylelint": "stylelint --cache --fix \"**/*.{vue,less,postcss,css,scss}\" --cache --cache-location node_modules/.cache/stylelint/", 17 | "lint:lint-staged": "lint-staged", 18 | "prepare": "husky install", 19 | "preinstall": "only-allow pnpm" 20 | }, 21 | "dependencies": { 22 | "axios": "^1.2.2", 23 | "echarts": "^5.4.1", 24 | "element-plus": "^2.2.28", 25 | "lodash": "^4.17.21", 26 | "nprogress": "^0.2.0", 27 | "path-to-regexp": "^6.2.1", 28 | "pinia": "^2.0.28", 29 | "qs": "^6.11.0", 30 | "vue": "^3.2.45", 31 | "vue-router": "^4.1.6" 32 | }, 33 | "devDependencies": { 34 | "@types/lodash": "^4.14.191", 35 | "@types/node": "^18.11.18", 36 | "@types/qs": "^6.9.7", 37 | "@typescript-eslint/eslint-plugin": "^5.48.0", 38 | "@typescript-eslint/parser": "^5.48.0", 39 | "@vitejs/plugin-vue": "^3.2.0", 40 | "eslint": "^8.31.0", 41 | "eslint-config-prettier": "^8.6.0", 42 | "eslint-plugin-prettier": "^4.2.1", 43 | "eslint-plugin-vue": "^9.8.0", 44 | "husky": "^8.0.2", 45 | "less": "^4.1.3", 46 | "lint-staged": "^13.1.0", 47 | "mockjs": "^1.1.0", 48 | "only-allow": "^1.1.1", 49 | "postcss": "^8.4.20", 50 | "postcss-html": "^1.5.0", 51 | "prettier": "^2.8.1", 52 | "rollup-plugin-visualizer": "^5.9.0", 53 | "sass": "^1.57.1", 54 | "stylelint": "^14.16.1", 55 | "stylelint-config-html": "^1.1.0", 56 | "stylelint-config-prettier": "^9.0.4", 57 | "stylelint-config-recess-order": "^3.1.0", 58 | "stylelint-config-recommended-less": "^1.0.4", 59 | "stylelint-config-recommended-scss": "^8.0.0", 60 | "stylelint-config-recommended-vue": "^1.4.0", 61 | "stylelint-config-standard": "^29.0.0", 62 | "stylelint-config-standard-scss": "^6.1.0", 63 | "stylelint-less": "^1.0.6", 64 | "svgo": "^3.0.2", 65 | "typescript": "^4.9.4", 66 | "vite": "^3.2.5", 67 | "vite-plugin-compression": "^0.5.1", 68 | "vite-plugin-eslint": "^1.8.1", 69 | "vite-plugin-mock": "^2.9.6", 70 | "vite-plugin-svg-icons": "^2.0.1", 71 | "vue-tsc": "^1.0.19" 72 | }, 73 | "engines": { 74 | "node": ">= 14.18.0" 75 | }, 76 | "keywords": [ 77 | "vite", 78 | "vitejs", 79 | "vite3", 80 | "vue", 81 | "vue3", 82 | "vue3.0", 83 | "vue3.x", 84 | "pinia", 85 | "typescript", 86 | "frame", 87 | "template" 88 | ] 89 | } 90 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqsong/admin-element-vue/9cbde5dd98d41a11d40d29ef93f10824694eca94/public/favicon.ico -------------------------------------------------------------------------------- /src/@types/config.settings.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 站点配置 ts定义 3 | * @author LiQingSong 4 | */ 5 | 6 | /** 7 | * @description: 站点名称 8 | */ 9 | export type TSiteTitle = string; 10 | 11 | /** 12 | * @description: 首页路由Path 13 | */ 14 | export type THomePath = string; 15 | 16 | /** 17 | * @description: 站点本地存储Token 的 Key值 18 | */ 19 | export type TSiteTokenKey = string; 20 | 21 | /** 22 | * @description: Ajax请求头发送Token 的 Key值 23 | */ 24 | export type TAjaxHeadersTokenKey = string; 25 | 26 | /** 27 | * @description: Ajax返回值不参加统一验证的api地址 28 | */ 29 | export type TAjaxResponseNoVerifyUrl = string[]; 30 | 31 | /** 32 | * @description: Layout 模板主题 33 | */ 34 | export type TTheme = "dark" | "light"; 35 | 36 | /** 37 | * @description: Layout 模板主题本地存储(localStorage)的key名称 38 | */ 39 | export type TThemeStorageKey = string; 40 | 41 | /** 42 | * @description: Layout 菜单导航布局 43 | */ 44 | export type TMenuLayout = "vertical" | "horizontal"; 45 | 46 | /** 47 | * @description: Layout 菜单导航布局本地存储(localStorage)的key名称 48 | */ 49 | export type TMenuLayoutStorageKey = string; 50 | 51 | /** 52 | * @description: Layout 菜单导航风格 53 | */ 54 | export type TMenuStyle = "dark" | "light"; 55 | 56 | /** 57 | * @description: Layout 菜单导航风格本地存储(localStorage)的key名称 58 | */ 59 | export type TMenuStyleStorageKey = string; 60 | -------------------------------------------------------------------------------- /src/@types/i18n.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义i18n ts定义 3 | * @author LiQingSong 4 | */ 5 | 6 | /** 7 | * @description: 语言名类型 8 | */ 9 | export type TI18nKey = "zh-CN" | "zh-TW" | "en-US"; 10 | 11 | /** 12 | * @description: 语言值类型 13 | */ 14 | export interface II18nVal { 15 | [key: string]: string; 16 | } 17 | 18 | /** 19 | * @description: 语言包格式 20 | */ 21 | export type TI18n = { 22 | [key in TI18nKey]?: II18nVal; 23 | }; 24 | 25 | /** 26 | * @description: 语言内部变量格式替换字段 27 | */ 28 | export type TUseFormat = (string | number)[] | { [key in string]: number | string }; 29 | -------------------------------------------------------------------------------- /src/@types/nprogress.d.ts: -------------------------------------------------------------------------------- 1 | declare module "nprogress"; 2 | -------------------------------------------------------------------------------- /src/@types/utils.request.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义 request 网络请求工具 ts定义 3 | * @author LiQingSong 4 | */ 5 | import { AxiosRequestConfig } from "axios"; 6 | import { ContentTypeEnum } from "@/enums/utils.request.enum"; 7 | 8 | /** 9 | * @description: ajax 配置参数类型 10 | */ 11 | export interface IAxiosRequestConfig extends AxiosRequestConfig { 12 | contentType?: ContentTypeEnum; 13 | } 14 | 15 | /** 16 | * @description: 请求返回数据类型 17 | */ 18 | export interface IResponseData { 19 | code: number; 20 | data?: T; 21 | msg?: string; 22 | } 23 | 24 | /** 25 | * @description: 状态码对应内容信息 26 | */ 27 | export interface ICodeMessage { 28 | [key: number]: string; 29 | } 30 | -------------------------------------------------------------------------------- /src/@types/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * @description: vue 文件定义 5 | */ 6 | declare module "*.vue" { 7 | import type { DefineComponent } from "vue"; 8 | const component: DefineComponent<{}, {}, any>; 9 | export default component; 10 | } 11 | 12 | /** 13 | * @description: vite import.meta.env 变量 14 | */ 15 | interface ImportMetaEnv { 16 | readonly VITE_APP_API_URL: string; // api接口域名 17 | // 更多环境变量... 18 | } 19 | 20 | /** 21 | * @description: vite import.meta.env 变量 22 | */ 23 | interface ImportMeta { 24 | readonly env: ImportMetaEnv; 25 | } 26 | -------------------------------------------------------------------------------- /src/@types/vue-router.d.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 扩展路由类型 ts定义 3 | * @author LiQingSong 4 | */ 5 | import { RouteRecordRaw } from "vue-router"; 6 | import "vue-router"; 7 | 8 | /** 9 | * @description: 面包屑类型 10 | */ 11 | export interface IBreadcrumb { 12 | // 标题,路由在菜单、浏览器title 或 面包屑中展示的文字 13 | title: string; 14 | // 路由地址或外链 15 | path: string; 16 | } 17 | 18 | /** 19 | * tab导航存储规则类型 20 | */ 21 | export type TTabNavType = "path" | "querypath"; 22 | 23 | /** 24 | * @description: 扩展 vue-router 25 | */ 26 | declare module "vue-router" { 27 | // 扩展meta字段 - Layout 可以根据以下参数做些定制化功能,如菜单栏 28 | interface RouteMeta { 29 | // 标题,路由在菜单、浏览器title 或 面包屑中展示的文字 30 | title?: string; 31 | // 图标的名称,显示在菜单标题前 32 | icon?: string; 33 | // 所有父元素的path,下标key按照父元素的顺序,若不设置则根据路由自动生成 34 | parentPath?: string[]; 35 | /** 36 | * 左侧菜单选中,如果设置路径,侧栏将突出显示你设置的路径对应的侧栏导航 37 | * 1、(默认 route.path),此参数是为了满足特殊页面特殊需求, 38 | * 2、如:详情页等选中侧栏导航或在模块A下面的页面,想选模块B为导航选中状态 39 | */ 40 | selectLeftMenu?: string; 41 | /** 42 | * 所属顶级菜单,当顶级菜单存在时,用于选中顶部菜单,与侧栏菜单切换 43 | * 1、三级路由此参数的作用是选中顶级菜单 44 | * 2、二级路由此参数的作用是所属某个顶级菜单的下面,两个层级的必须同时填写一致,如果path设置的是外链,此参数必填 45 | * 3、(默认不设置 path.split('/')[0]),此参数是为了满足特殊页面特殊需求 46 | */ 47 | selectTopMenu?: string; 48 | /** 49 | * 面包屑自定义内容: 50 | * 1、默认不配置按照路由自动读取; 51 | * 2、设置为 false , 按照路由自动读取并不读当前自己; 52 | * 3、配置对应的面包屑格式如下: 53 | */ 54 | breadcrumb?: IBreadcrumb[] | false; 55 | // 菜单中是否隐藏 56 | hidden?: boolean; 57 | // 权限控制,页面角色(您可以设置多个角色) 58 | roles?: string[]; 59 | /** 60 | * 设置tab导航存储规则类型 61 | * 1、默认不配置按照path(route.path)规则 62 | * 2、querypath:path + query (route.path+route.query) 规则 63 | * 比如:详情页可设置querypath 64 | */ 65 | tabNavType?: TTabNavType; 66 | /** 67 | * 设置该字段,则在关闭当前tab页时,作为关闭前的钩子函数 68 | * @param close 关闭回调函数 69 | */ 70 | tabNavCloseBefore?: (close: () => void) => void; 71 | /** 72 | * 当启用tabNav时,此导航对应页面组件,是否缓存。 73 | * 注意:如果设置true,route.name必须也有值且唯一,才会有效(并且对应的页面也需要声明相同name)。 74 | * 比如:发布页、编辑页可设置此参数看效果 75 | */ 76 | isKeepAlive?: boolean; 77 | } 78 | } 79 | 80 | /** 81 | * @description: json path key 路由类型 82 | */ 83 | export interface IPathKeyRouter { 84 | [path: string]: RouteRecordRaw; 85 | } 86 | 87 | /** 88 | * @description: 路由类型 RouteRecordRaw 与 json path key 路由类型 集合 89 | */ 90 | export interface IRouterPathKeyRouter { 91 | router: RouteRecordRaw[]; 92 | pathKeyRouter: IPathKeyRouter; 93 | } 94 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 28 | 29 | 30 | -------------------------------------------------------------------------------- /src/assets/css/element-plus.scss: -------------------------------------------------------------------------------- 1 | @import "element-plus/dist/index.css"; 2 | @import "element-plus/theme-chalk/dark/css-vars.css"; 3 | 4 | // @import "element-plus/theme-chalk/src/mixins/config.scss"; 5 | -------------------------------------------------------------------------------- /src/assets/css/global.scss: -------------------------------------------------------------------------------- 1 | @import "./normalize.css"; 2 | @import "./variables.scss"; 3 | @import "./mixin.scss"; 4 | @import "./element-plus.scss"; 5 | html, 6 | body { 7 | font-family: var(--ft-font-family); 8 | font-size: var(--ft-font-size); 9 | font-weight: var(--ft-font-weight); 10 | line-height: 1.4; 11 | text-size-adjust: 100%; 12 | text-rendering: optimizelegibility; 13 | -webkit-font-smoothing: antialiased; 14 | -webkit-tap-highlight-color: transparent; 15 | -moz-osx-font-smoothing: grayscale; 16 | } 17 | body { 18 | width: 100%; 19 | min-height: 100vh; 20 | color: var(--ft-text-color); 21 | background-color: var(--ft-bg-color); 22 | } 23 | a { 24 | font-size: var(--ft-a-font-size); 25 | font-weight: var(--ft-a-font-weight); 26 | color: var(--ft-a-text-color); 27 | text-decoration: none; 28 | cursor: pointer; 29 | background-color: transparent; 30 | outline: none; 31 | &:active, 32 | &:hover { 33 | text-decoration: none; 34 | outline: 0; 35 | } 36 | &:active { 37 | color: var(--ft-a-text-color); 38 | } 39 | &:hover { 40 | color: var(--ft-a-hover-text-color); 41 | } 42 | &.primary { 43 | --ft-a-text-color: var(--ft-color-primary); 44 | --ft-a-hover-text-color: var(--ft-text-color); 45 | } 46 | } 47 | .flex-wrap-wrap { 48 | flex-wrap: wrap; 49 | } 50 | 51 | /* text */ 52 | .text-align-right { 53 | text-align: right; 54 | } 55 | 56 | /* float */ 57 | .float-right { 58 | float: right; 59 | } 60 | 61 | /* cursor */ 62 | .cursor-pointer { 63 | cursor: pointer; 64 | } 65 | 66 | /* margin */ 67 | .margin-l15 { 68 | margin-left: 15px; 69 | } 70 | .margin-b16 { 71 | margin-bottom: 16px; 72 | } 73 | 74 | /* padding */ 75 | .padding-t10 { 76 | padding-top: 10px; 77 | } 78 | .padding-lr16 { 79 | padding-right: 16px; 80 | padding-left: 16px; 81 | } 82 | 83 | /* 详情表格样式 */ 84 | .ft-detail { 85 | position: relative; 86 | box-sizing: border-box; 87 | display: flex; 88 | flex-direction: column; 89 | &.border { 90 | border-right: solid 1px var(--ft-cell-border-color); 91 | border-bottom: solid 1px var(--ft-cell-border-color); 92 | } 93 | .ft-detail-row { 94 | position: relative; 95 | box-sizing: border-box; 96 | display: flex; 97 | flex: 1; 98 | flex-direction: row; 99 | .left, 100 | .right { 101 | position: relative; 102 | box-sizing: border-box; 103 | display: flex; 104 | flex: 1; 105 | .cell { 106 | padding: 8px 11px; 107 | font-size: 14px; 108 | line-height: 22px; 109 | word-break: break-all; 110 | } 111 | &.width200 { 112 | flex: none; 113 | width: 200px; 114 | } 115 | &.border { 116 | border-top: solid 1px var(--ft-cell-border-color); 117 | border-left: solid 1px var(--ft-cell-border-color); 118 | } 119 | } 120 | } 121 | 122 | // 标题 123 | .th { 124 | background: var(--ft-cell-hover-bg-color); 125 | } 126 | } 127 | 128 | /* 基于element table 重置样式 */ 129 | .ft-el-table-header { 130 | th { 131 | background-color: var(--ft-cell-hover-bg-color) !important; 132 | } 133 | } 134 | -------------------------------------------------------------------------------- /src/assets/css/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin scrollbar( 2 | $thumb-background: hsla(0, 0%, 100%, 0.2), 3 | $thumb-shadow: hsla(0, 0%, 100%, 0.05), 4 | $track-background: hsla(0, 0%, 100%, 0.15), 5 | $track-shadow: rgba(37, 37, 37, 0.05) 6 | ) { 7 | ::-webkit-scrollbar { 8 | width: 6px; 9 | height: 6px; 10 | } 11 | ::-webkit-scrollbar-thumb { 12 | background: $thumb-background; 13 | border-radius: 3px; 14 | box-shadow: inset 0 0 5px $thumb-shadow; 15 | } 16 | ::-webkit-scrollbar-track { 17 | background: $track-background; 18 | border-radius: 3px; 19 | box-shadow: inset 0 0 5px $track-shadow; 20 | } 21 | } 22 | 23 | @mixin scrollbar-light { 24 | @include scrollbar(hsla(0, 0%, 0%, 0.2), hsla(0, 0%, 0%, 0.05), hsla(0, 0%, 0%, 0.15), rgba(255, 255, 255, 0.05)); 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/css/variables.scss: -------------------------------------------------------------------------------- 1 | :root { 2 | // 白天模式 3 | color-scheme: light; 4 | 5 | // 颜色 6 | --ft-color-white: #ffffff; 7 | --ft-color-white-1: #f0f2f5; 8 | --ft-color-white-2: #f3f3f3; 9 | --ft-color-white-rgb-1: rgb(255 255 255 / 87%); 10 | --ft-color-white-rgb-2: rgb(255 255 255 / 65%); 11 | --ft-color-black: #000000; 12 | --ft-color-black-1: #1d1e1f; 13 | --ft-color-black-2: #303133; 14 | --ft-color-black-rgb-1: rgb(0 0 0 / 10%); 15 | --ft-color-black-rgb-2: rgb(60 60 60 / 12%); 16 | --ft-color-black-rgb-3: rgb(84 84 84 / 65%); 17 | --ft-color-black-primary: #000c17; 18 | --ft-color-black-primary-1: #001529; 19 | --ft-color-primary: #1890ff; 20 | --ft-color-primary-1: #e6f7ff; 21 | 22 | // 字体大小 23 | --ft-font-size-base: 14px; // 基础 24 | 25 | // 字体粗细 26 | --ft-font-weight-base: 400; // 基础 27 | 28 | // 网页 - 背景色 29 | --ft-bg-color: var(--ft-color-white); 30 | 31 | // 网页 - 文本颜色 32 | --ft-text-color: var(--ft-color-black-2); 33 | 34 | // 网页 - 字体 35 | --ft-font-family: "Helvetica Neue", helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", 36 | "\5fae\8f6f\96c5\9ed1", arial, sans-serif; 37 | 38 | // 网页 - 字体大小 39 | --ft-font-size: var(--ft-font-size-base); 40 | 41 | // 网页 - 字体粗细 42 | --ft-font-weight: var(--ft-font-weight-base); 43 | 44 | // a 链接 45 | --ft-a-font-size: var(--ft-font-size-base); 46 | --ft-a-font-weight: var(--ft-font-weight-base); 47 | --ft-a-text-color: var(--ft-text-color); 48 | --ft-a-hover-text-color: var(--ft-color-primary); 49 | 50 | // 分割器 51 | --ft-divider-color: var(--ft-color-black-rgb-2); 52 | 53 | // shadow color 54 | --ft-shadow-color-1: var(--ft-color-black-rgb-1); 55 | 56 | // card 57 | --ft-card-bg-color: var(--ft-color-white); 58 | 59 | // 单元格 60 | --ft-cell-hover-bg-color: #f5f7fa; 61 | --ft-cell-border-color: #ebeef5; 62 | } 63 | 64 | /* 暗黑主题 */ 65 | html.dark { 66 | // 黑夜模式 67 | color-scheme: dark; 68 | 69 | // 网页 - 背景色 70 | --ft-bg-color: var(--ft-color-black-1); 71 | 72 | // 网页 - 文本颜色 73 | --ft-text-color: var(--ft-color-white-rgb-1); 74 | 75 | // 分割器 76 | --ft-divider-color: var(--ft-color-black-rgb-3); 77 | 78 | // shadow color 79 | --ft-shadow-color-1: var(--ft-color-white-rgb-1); 80 | 81 | // card 82 | --ft-card-bg-color: var(--ft-color-black-1); 83 | 84 | // 单元格 85 | --ft-cell-hover-bg-color: #262727; 86 | --ft-cell-border-color: #363637; 87 | } 88 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-down.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-left.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-right.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/arrow-up.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/close.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/fold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/language-outline.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/lock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-anomaly.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-detail.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-edit.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-home.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-link.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-list.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-permission.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/menu-result.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/moon.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/more.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/pwd.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/router.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/search.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/set.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/sun.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/theme.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/unfold.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/unlock.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/iconsvg/user.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/images/logo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/lqsong/admin-element-vue/9cbde5dd98d41a11d40d29ef93f10824694eca94/src/assets/images/logo.png -------------------------------------------------------------------------------- /src/components/ALink/index.vue: -------------------------------------------------------------------------------- 1 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /src/components/IconSvg/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 15 | 24 | -------------------------------------------------------------------------------- /src/components/PageLoading/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 9 | 17 | -------------------------------------------------------------------------------- /src/components/Permission/index.vue: -------------------------------------------------------------------------------- 1 | 17 | 29 | -------------------------------------------------------------------------------- /src/components/Result/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 40 | 92 | -------------------------------------------------------------------------------- /src/components/Result/svg/error.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/Result/svg/info.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/Result/svg/success.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/Result/svg/warning.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/components/ScreenTable/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IPaginationConfig { 2 | layout?: string; 3 | total: number; 4 | current: number; 5 | pageSize: number; 6 | sizes?: number[]; 7 | sizeChange: (size: number) => void; 8 | onChange: (page: number) => void; 9 | } 10 | 11 | /** 12 | * 表格列显示在popver中设置的项 13 | */ 14 | export interface IPopoverTableColumnItem { 15 | // 名称标题 16 | label: string; 17 | // 字段名称,传回数据库 18 | key: string; 19 | // 是否显示 20 | checked: boolean; 21 | // 是否禁用 22 | disabled?: boolean; 23 | /* 以下是显示表格的时候会用到 */ 24 | // 是否fixed 25 | fixed?: boolean | string; 26 | // prop 27 | prop?: string; 28 | //列宽 29 | width?: string; 30 | minWidth?: string; 31 | // 其他 32 | [key in string]?: any; 33 | } 34 | 35 | // 暴露类型 36 | export interface IDefineExpose { 37 | setSearchDrawerVisible: (v: boolean) => void; 38 | setPopoverColumnAllVal: () => void; 39 | } 40 | -------------------------------------------------------------------------------- /src/components/ScreenTable/search.vue: -------------------------------------------------------------------------------- 1 | 6 | 10 | 27 | -------------------------------------------------------------------------------- /src/components/SelectLang/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 55 | 62 | -------------------------------------------------------------------------------- /src/components/Spin/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 19 | 96 | -------------------------------------------------------------------------------- /src/composables/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import { onMounted, onBeforeUnmount, Ref, ShallowRef, shallowRef } from "vue"; 2 | import { debounce } from "lodash"; 3 | import * as echarts from "echarts"; 4 | 5 | export type EChartsOption = echarts.EChartsOption; 6 | 7 | /** 8 | * Echarts composables 9 | * @param labRef Ref HTMLDivElement ref 10 | * @param initOption EChartOption echarts option init 11 | * @param cb Function|undefined 回调函数 读取数据 12 | * @param theme string|undefined 使用的主题 13 | * @returns 14 | * @author LiQingSong 15 | */ 16 | export default function useEcharts( 17 | labRef: Ref, 18 | initOption: EChartsOption, 19 | cb?: (ec: echarts.ECharts) => any, 20 | theme = "", 21 | ): { 22 | echart: ShallowRef; 23 | cb: () => void; 24 | } { 25 | const chart = shallowRef(); 26 | 27 | const resizeHandler = debounce(() => { 28 | chart.value?.resize(); 29 | }, 100); 30 | 31 | const callback = () => { 32 | if (typeof cb === "function" && chart.value) { 33 | cb(chart.value); 34 | } 35 | }; 36 | 37 | onMounted(() => { 38 | if (labRef.value) { 39 | chart.value = echarts.init(labRef.value, theme); 40 | chart.value.setOption(initOption); 41 | callback(); 42 | } 43 | 44 | window.addEventListener("resize", resizeHandler); 45 | }); 46 | 47 | onBeforeUnmount(() => { 48 | chart.value?.dispose(); 49 | window.removeEventListener("resize", resizeHandler); 50 | }); 51 | 52 | return { 53 | echart: chart, 54 | cb: callback, 55 | }; 56 | } 57 | -------------------------------------------------------------------------------- /src/composables/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { useI18nStore } from "@/store/i18n"; 2 | import { TI18n } from "@/@types/i18n"; 3 | 4 | /** 5 | * @description 引入语言包 6 | * @param locales 当前本地(文件夹下)语言包 7 | * @returns (key: string, format?: TUseFormat) => string 8 | * @author LiQingSong 9 | */ 10 | export const useI18n = (locales?: TI18n) => { 11 | const i18n = useI18nStore(); 12 | return i18n.use(locales); 13 | }; 14 | -------------------------------------------------------------------------------- /src/composables/useMenuLayout.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeMount, watch, ref, Ref } from "vue"; 2 | import { menuLayout, menuLayoutStorageKey } from "@/config/settings"; 3 | import { useGlobalStore } from "@/store/global"; 4 | import { TMenuLayout } from "@/@types/config.settings"; 5 | 6 | /** 7 | * @description:设置 菜单导航布局 8 | * @author LiQingSong 9 | */ 10 | export const useMenuLayout = (): Ref => { 11 | const globalStore = useGlobalStore(); 12 | const menuLayoutStorage = (localStorage.getItem(menuLayoutStorageKey) || menuLayout) as TMenuLayout; 13 | const ml = ref(menuLayoutStorage); 14 | 15 | const setMenuLayout = () => { 16 | localStorage.setItem(menuLayoutStorageKey, ml.value); 17 | globalStore.menuLayout = ml.value; 18 | }; 19 | 20 | watch(ml, () => { 21 | setMenuLayout(); 22 | }); 23 | 24 | onBeforeMount(() => { 25 | setMenuLayout(); 26 | }); 27 | 28 | return ml; 29 | }; 30 | -------------------------------------------------------------------------------- /src/composables/useMenuStyle.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeMount, watch, ref, Ref } from "vue"; 2 | import { menuStyle, menuStyleStorageKey } from "@/config/settings"; 3 | import { useGlobalStore } from "@/store/global"; 4 | import { TMenuStyle } from "@/@types/config.settings"; 5 | 6 | /** 7 | * @description:设置 菜单导航风格 8 | * @author LiQingSong 9 | */ 10 | export const useMenuStyle = (): Ref => { 11 | const globalStore = useGlobalStore(); 12 | const menuStyleStorage = (localStorage.getItem(menuStyleStorageKey) || menuStyle) as TMenuStyle; 13 | const ms = ref(menuStyleStorage); 14 | 15 | const setMenuStyle = () => { 16 | localStorage.setItem(menuStyleStorageKey, ms.value); 17 | globalStore.menuStyle = ms.value; 18 | }; 19 | 20 | watch(ms, () => { 21 | setMenuStyle(); 22 | }); 23 | 24 | onBeforeMount(() => { 25 | setMenuStyle(); 26 | }); 27 | 28 | return ms; 29 | }; 30 | -------------------------------------------------------------------------------- /src/composables/useTheme.ts: -------------------------------------------------------------------------------- 1 | import { onBeforeMount, watch, ref, Ref } from "vue"; 2 | import { theme, themeStorageKey } from "@/config/settings"; 3 | import { useGlobalStore } from "@/store/global"; 4 | 5 | /** 6 | * @description:设置 模板主题 7 | * @author LiQingSong 8 | */ 9 | export const useTheme = (): Ref => { 10 | const globalStore = useGlobalStore(); 11 | const themeStorage = localStorage.getItem(themeStorageKey) || theme; 12 | const isDark = ref(themeStorage === "dark"); 13 | 14 | const setDark = () => { 15 | if (isDark.value === true) { 16 | localStorage.setItem(themeStorageKey, "dark"); 17 | document.querySelector("html")?.classList.add("dark"); 18 | globalStore.theme = "dark"; 19 | } else { 20 | localStorage.setItem(themeStorageKey, "light"); 21 | document.querySelector("html")?.classList.remove("dark"); 22 | globalStore.theme = "light"; 23 | } 24 | }; 25 | 26 | watch(isDark, () => { 27 | setDark(); 28 | }); 29 | 30 | onBeforeMount(() => { 31 | setDark(); 32 | }); 33 | 34 | return isDark; 35 | }; 36 | -------------------------------------------------------------------------------- /src/composables/useTitle.ts: -------------------------------------------------------------------------------- 1 | import { ComputedRef, onMounted, watch } from "vue"; 2 | import { RouteRecordRaw } from "vue-router"; 3 | import { siteTitle } from "@/config/settings"; 4 | import { TUseFormat } from "@/@types/i18n"; 5 | 6 | /** 7 | * @description:设置 html Title composables 8 | * @param routeItem 当前路由item 9 | * @author LiQingSong 10 | */ 11 | export const useTitle = ( 12 | routeItem: ComputedRef, 13 | t: (key: string, format?: TUseFormat | undefined) => string = (key: string) => key, 14 | ): void => { 15 | const setTitle = (title: string): void => { 16 | document.title = `${t(title)} - ${siteTitle}`; 17 | }; 18 | 19 | watch(routeItem, () => { 20 | setTitle(routeItem.value?.meta?.title || ""); 21 | }); 22 | 23 | onMounted(() => { 24 | setTitle(routeItem.value?.meta?.title || ""); 25 | }); 26 | }; 27 | -------------------------------------------------------------------------------- /src/config/router.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 路由配置入口 3 | * @author LiQingSong 4 | */ 5 | import NProgress from "nprogress"; // progress bar 6 | import "nprogress/nprogress.css"; // progress bar style 7 | NProgress.configure({ showSpinner: false }); // NProgress Configuration 8 | 9 | import { createRouter, createWebHashHistory, RouteRecordRaw } from "vue-router"; 10 | 11 | /* SecurityLayout */ 12 | import SecurityLayout from "@/layouts/SecurityLayout.vue"; 13 | 14 | /* MemberLayout */ 15 | import MemberLayoutRoutes from "@/layouts/MemberLayout/routes"; 16 | import MemberLayout from "@/layouts/MemberLayout/index.vue"; 17 | 18 | /* UserLayout */ 19 | import UserLayoutRoutes from "@/layouts/UserLayout/routes"; 20 | import UserLayout from "@/layouts/UserLayout/index.vue"; 21 | 22 | /* 请求消除器 */ 23 | import { requestCanceler } from "@/utils/request"; 24 | 25 | // 配置路由 26 | const routes: RouteRecordRaw[] = [ 27 | // MemberLayout 必须放在最上方,因为 redirect: "/home" 28 | { 29 | path: "/", 30 | component: SecurityLayout, 31 | children: [ 32 | { 33 | path: "/", 34 | redirect: "/home", 35 | component: MemberLayout, 36 | children: MemberLayoutRoutes, 37 | }, 38 | ], 39 | }, 40 | 41 | { 42 | path: "/", 43 | component: UserLayout, 44 | children: UserLayoutRoutes, 45 | }, 46 | 47 | { 48 | path: "/:pathMatch(.*)*", 49 | component: () => import("@/pages/404/index.vue"), 50 | }, 51 | ]; 52 | 53 | const router = createRouter({ 54 | scrollBehavior() { 55 | return { left: 0, top: 0 }; 56 | }, 57 | history: createWebHashHistory(import.meta.env.BASE_URL), 58 | routes: routes, 59 | }); 60 | 61 | /** 62 | * @description 路由前置,拦截 63 | */ 64 | router.beforeEach((to, from, next) => { 65 | // start progress bar 66 | NProgress.start(); 67 | 68 | // 在跳转之前,清除所有ajax请求 69 | requestCanceler.removeAllPending(); 70 | 71 | // 跳转到对应路由 72 | next(); 73 | }); 74 | 75 | /** 76 | * @description 路由后置,跳转结束 77 | */ 78 | router.afterEach(() => { 79 | // finish progress bar 80 | NProgress.done(); 81 | }); 82 | 83 | export default router; 84 | -------------------------------------------------------------------------------- /src/config/settings.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 站点配置 3 | * @author LiQingSong 4 | */ 5 | import { 6 | TAjaxHeadersTokenKey, 7 | TAjaxResponseNoVerifyUrl, 8 | THomePath, 9 | TMenuLayout, 10 | TMenuLayoutStorageKey, 11 | TMenuStyle, 12 | TMenuStyleStorageKey, 13 | TSiteTitle, 14 | TSiteTokenKey, 15 | TTheme, 16 | TThemeStorageKey, 17 | } from "@/@types/config.settings.d"; 18 | 19 | /** 20 | * @description: 站点名称 21 | */ 22 | export const siteTitle: TSiteTitle = "Admin-Element-Vue"; 23 | 24 | /** 25 | * @description: 首页路由path 26 | */ 27 | export const homePath: THomePath = "/home"; 28 | 29 | /** 30 | * @description: 站点本地存储Token 的 Key值 31 | */ 32 | export const siteTokenKey: TSiteTokenKey = "admin-element-vue-token"; 33 | 34 | /** 35 | * @description: Ajax请求头发送Token 的 Key值 36 | */ 37 | export const ajaxHeadersTokenKey: TAjaxHeadersTokenKey = "x-token"; 38 | 39 | /** 40 | * @description: Ajax返回值不参加统一报错的api地址 41 | */ 42 | export const ajaxResponseNoVerifyUrl: TAjaxResponseNoVerifyUrl = ["/user/login", "/user/info"]; 43 | 44 | /** 45 | * @description: Layout 模板主题 46 | */ 47 | export const theme: TTheme = "light"; 48 | 49 | /** 50 | * @description: Layout 模板主题本地存储(localStorage)的key名称 51 | */ 52 | export const themeStorageKey: TThemeStorageKey = "admin-element-vue-theme"; 53 | 54 | /** 55 | * @description: Layout 菜单导航布局 56 | */ 57 | export const menuLayout: TMenuLayout = "vertical"; 58 | 59 | /** 60 | * @description: Layout 菜单导航布局本地存储(localStorage)的key名称 61 | */ 62 | export const menuLayoutStorageKey: TMenuLayoutStorageKey = "admin-element-vue-memu-layout"; 63 | 64 | /** 65 | * @description: Layout 菜单导航风格 66 | */ 67 | export const menuStyle: TMenuStyle = "dark"; 68 | 69 | /** 70 | * @description: Layout 菜单导航风格本地存储(localStorage)的key名称 71 | */ 72 | export const menuStyleStorageKey: TMenuStyleStorageKey = "admin-element-vue-memu-style"; 73 | 74 | /** 75 | * @description: Layout 是否启用多标签Tab页 76 | */ 77 | export const isTabsNav: boolean = true; 78 | 79 | /** 80 | * @description: Layout 多标签Tab页白名单,不用在tabNav组件中显示的路由 81 | */ 82 | export const tabsNavWhiteList: string[] = ["/403", "/500"]; 83 | 84 | /** 85 | * @description: Layout 是否启用底部 86 | */ 87 | export const isLayoutFooter: boolean = true; 88 | -------------------------------------------------------------------------------- /src/config/store.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: Store 配置入口,可以做些公共的配置,如数据持久化等 3 | * @author LiQingSong 4 | */ 5 | import { createPinia } from "pinia"; 6 | 7 | const pinia = createPinia(); 8 | 9 | export default pinia; 10 | -------------------------------------------------------------------------------- /src/directives/vPermission.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 自定义指令 权限验证 3 | * @author LiQingSong 4 | * 使用Demo: 5 | * 8 | * 12 | */ 13 | import type { ObjectDirective, DirectiveBinding } from "vue"; 14 | import { useUserStore } from "@/store/user"; 15 | import { hasPermissionRoles } from "@/utils/router"; 16 | 17 | const vPermission: ObjectDirective = { 18 | // 在绑定元素的父组件 19 | // 及他自己的所有子节点都挂载完成后调用 20 | mounted(el: HTMLElement, binding: DirectiveBinding) { 21 | const { value } = binding; 22 | if (value) { 23 | const userStroe = useUserStore(); 24 | if (!hasPermissionRoles(userStroe.roles, value)) { 25 | el.parentNode && el.parentNode.removeChild(el); 26 | } 27 | } else { 28 | throw new Error("need roles! Like v-permission=\"['admin','test']\" or v-permission=\"'test'\""); 29 | } 30 | }, 31 | }; 32 | 33 | export default vPermission; 34 | -------------------------------------------------------------------------------- /src/enums/utils.request.enum.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义 request 网络请求枚举配置 3 | * @author LiQingSong 4 | */ 5 | 6 | /** 7 | * @description: 自定义状态码配置 8 | */ 9 | export enum ResultCodeEnum { 10 | SUCCESS = 0, // 成功 11 | LOGININVALID = 10002, // 登入信息失效 12 | } 13 | 14 | /** 15 | * @description: 常用的contentTyp类型 16 | */ 17 | export enum ContentTypeEnum { 18 | // json 19 | JSON = "application/json;charset=UTF-8", 20 | // text 21 | TEXT = "text/plain;charset=UTF-8", 22 | // form-data 一般配合qs 23 | FORM_URLENCODED = "application/x-www-form-urlencoded;charset=UTF-8", 24 | // form-data 上传 25 | FORM_DATA = "multipart/form-data;charset=UTF-8", 26 | } 27 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/BreadCrumbs.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 34 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/LeftSider.vue: -------------------------------------------------------------------------------- 1 | 22 | 23 | 39 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/Main.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 34 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/RightTop.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 85 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/RightTopUser.vue: -------------------------------------------------------------------------------- 1 | 42 | 62 | 78 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/SiderMenu.vue: -------------------------------------------------------------------------------- 1 | 36 | 37 | 48 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/components/SiderMenuItem.vue: -------------------------------------------------------------------------------- 1 | 29 | 55 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/css/variables.scss: -------------------------------------------------------------------------------- 1 | @import "../../../assets/css/variables.scss"; 2 | :root { 3 | // 白天模式 4 | color-scheme: light; 5 | 6 | // 框架 - 左边宽度 7 | --ft-mb-layout-left-side-width: 200px; 8 | --ft-mb-layout-left-side-collapsed-width: 54px; 9 | 10 | // 框架 - 左边 - 菜单宽度 11 | --ft-mb-layout-left-menu-width: var(--ft-mb-layout-left-side-width); 12 | 13 | // 框架 - 菜单 - item高度 14 | --ft-mb-layout-menu-item-height: 40px; 15 | 16 | // 框架 - 菜单 - item字体颜色 17 | --ft-mb-layout-menu-item-color: var(--ft-color-white-rgb-2); 18 | 19 | // 框架 - 菜单 - item字体颜色(选中) 20 | --ft-mb-layout-menu-item-active-color: var(--ft-color-white); 21 | 22 | // 框架 - 菜单 - item背景色(选中) 23 | --ft-mb-layout-menu-item-active-bg-color: var(--ft-color-primary); 24 | 25 | // 框架 - 菜单 - 背景色 26 | --ft-mb-layout-menu-bg-color: var(--ft-color-black-primary-1); 27 | 28 | // 框架 - 菜单 - 子菜单 - 背景色 29 | --ft-mb-layout-submenu-bg-color: var(--ft-color-black-primary); 30 | 31 | // 框架 - 主窗口 - 背景色 32 | --ft-mb-layout-main-bg-color: var(--ft-color-white-1); 33 | 34 | // 框架 - 右侧 - 头部高度 35 | --ft-mb-layout-header-height: 48px; 36 | 37 | // 框架 - 右侧 - 头部Tab - 导航高度 38 | --ft-mb-layout-header-tab-nav-height: 36px; 39 | 40 | // 框架 - 右侧 - 头部Tab - 背景颜色 41 | --ft-mb-layout-header-tab-nav-bg-color: var(--ft-color-white-2); 42 | 43 | // 框架 - 右侧 - 头部Tab - item(选中背景) 44 | --ft-mb-layout-header-tab-nav-item-active-bg-color: var(--ft-color-white); 45 | 46 | // 框架 - 右侧 - 头部Tab - item(hover背景) 47 | --ft-mb-layout-header-tab-nav-item-hover-bg-color: var(--ft-color-white-1); 48 | 49 | // 框架 - 右侧 - 头部 - 右侧 - item文字颜色 50 | --ft-mb-layout-header-right-item-color: var(--ft-text-color); 51 | 52 | // 框架 - 右侧 - 头部 - 右侧 - item背景色(hover) 53 | --ft-mb-layout-header-right-item-hover-bg-color: var(--ft-color-white-1); 54 | 55 | // 框架 - 右侧 - 主窗口 - 间隔 56 | --ft-mb-layout-main-padding: 16px; 57 | } 58 | 59 | /* 暗黑主题 */ 60 | html.dark { 61 | // 黑夜模式 62 | color-scheme: dark; 63 | 64 | // 框架 - 菜单 - item背景色(选中) 65 | --ft-mb-layout-menu-item-active-bg-color: var(--ft-color-black-2); 66 | 67 | // 框架 - 菜单 - 背景色 68 | --ft-mb-layout-menu-bg-color: var(--ft-color-black-1); 69 | 70 | // 框架 - 菜单 - 子菜单 - 背景色 71 | --ft-mb-layout-submenu-bg-color: var(--ft-color-black); 72 | 73 | // 框架 - 主窗口 - 背景色 74 | --ft-mb-layout-main-bg-color: var(--ft-color-black-rgb-3); 75 | 76 | // 框架 - 右侧 - 头部Tab - 背景颜色 77 | --ft-mb-layout-header-tab-nav-bg-color: var(--ft-color-black-1); 78 | 79 | // 框架 - 右侧 - 头部Tab - item(选中背景) 80 | --ft-mb-layout-header-tab-nav-item-active-bg-color: var(--ft-color-black); 81 | 82 | // 框架 - 右侧 - 头部Tab - item(hover背景) 83 | --ft-mb-layout-header-tab-nav-item-hover-bg-color: var(--ft-color-black-2); 84 | 85 | // 框架 - 右侧 - 头部 - 右侧 - item背景色(hover) 86 | --ft-mb-layout-header-right-item-hover-bg-color: var(--ft-color-black-primary); 87 | } 88 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/index.vue: -------------------------------------------------------------------------------- 1 | 36 | 62 | 65 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "member-layout.topmenu.userinfo": "Personal Info", 3 | "member-layout.topmenu.logout": "Logout", 4 | 5 | "member-layout.menu.home": "Workplace", 6 | 7 | "member-layout.menu.link": "External link", 8 | "member-layout.menu.link.github": "Github repository", 9 | "member-layout.menu.link.gitee": "Gitee repository", 10 | "member-layout.menu.link.docs": "Use document", 11 | 12 | "member-layout.menu.form": "Form page", 13 | "member-layout.menu.form.basic": "Base form", 14 | "member-layout.menu.form.step": "Step form", 15 | 16 | "member-layout.menu.list": "List page", 17 | "member-layout.menu.list.basic": "Base list", 18 | "member-layout.menu.list.filter": "Filter list", 19 | "member-layout.menu.list.highlyAdaptive": "Highly adaptive list", 20 | "member-layout.menu.list.highlyAdaptive2": "Highly adaptive list2", 21 | 22 | "member-layout.menu.detail": "Detail page", 23 | "member-layout.menu.detail.basic": "Basic details", 24 | "member-layout.menu.detail.module": "Module details", 25 | "member-layout.menu.detail.table": "Table details", 26 | 27 | "member-layout.menu.result": "Result page", 28 | "member-layout.menu.result.success": "Success page", 29 | "member-layout.menu.result.fail": "Failure page", 30 | 31 | "member-layout.menu.exception": "Exception page", 32 | "member-layout.menu.exception.403": "403", 33 | "member-layout.menu.exception.404": "404", 34 | "member-layout.menu.exception.500": "500", 35 | 36 | "member-layout.menu.permission": "Permission", 37 | "member-layout.menu.permission.all": "All users", 38 | "member-layout.menu.permission.user": "User", 39 | "member-layout.menu.permission.test": "Test", 40 | 41 | "member-layout.menu.routeMetaExtend": "RouteMeta", 42 | "member-layout.menu.routeMetaExtend.breadcrumb": "Custom breadcrumbs", 43 | }; 44 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/locales/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * MemberLayout locale 入口 3 | * @author LiQingSong 4 | */ 5 | 6 | import { TI18n } from "@/@types/i18n.d"; 7 | 8 | import zhCN from "./zh-CN"; 9 | import zhTW from "./zh-TW"; 10 | import enUS from "./en-US"; 11 | 12 | const locales: TI18n = { 13 | "zh-CN": zhCN, 14 | "zh-TW": zhTW, 15 | "en-US": enUS, 16 | }; 17 | 18 | export default locales; 19 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "member-layout.topmenu.userinfo": "个人信息", 3 | "member-layout.topmenu.logout": "退出", 4 | 5 | "member-layout.menu.home": "工作台", 6 | 7 | "member-layout.menu.link": "外部链接", 8 | "member-layout.menu.link.github": "Github 仓库", 9 | "member-layout.menu.link.gitee": "Gitee 仓库", 10 | "member-layout.menu.link.docs": "使用文档", 11 | 12 | "member-layout.menu.form": "表单页", 13 | "member-layout.menu.form.basic": "基础表单", 14 | "member-layout.menu.form.step": "分步表单", 15 | 16 | "member-layout.menu.list": "列表页", 17 | "member-layout.menu.list.basic": "基础列表", 18 | "member-layout.menu.list.filter": "筛选列表", 19 | "member-layout.menu.list.highlyAdaptive": "高度自适应列表", 20 | "member-layout.menu.list.highlyAdaptive2": "高度自适应列表2", 21 | 22 | "member-layout.menu.detail": "详情页", 23 | "member-layout.menu.detail.basic": "基础详情", 24 | "member-layout.menu.detail.module": "模块详情", 25 | "member-layout.menu.detail.table": "表格详情", 26 | 27 | "member-layout.menu.result": "结果页", 28 | "member-layout.menu.result.success": "成功页", 29 | "member-layout.menu.result.fail": "失败页", 30 | 31 | "member-layout.menu.exception": "异常页", 32 | "member-layout.menu.exception.403": "403", 33 | "member-layout.menu.exception.404": "404", 34 | "member-layout.menu.exception.500": "500", 35 | 36 | "member-layout.menu.permission": "权限验证", 37 | "member-layout.menu.permission.all": "所有用户都有权限", 38 | "member-layout.menu.permission.user": "User用户有权限", 39 | "member-layout.menu.permission.test": "Test用户有权限", 40 | 41 | "member-layout.menu.routeMetaExtend": "RouteMeta扩展", 42 | "member-layout.menu.routeMetaExtend.breadcrumb": "自定义面包屑", 43 | }; 44 | -------------------------------------------------------------------------------- /src/layouts/MemberLayout/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "member-layout.topmenu.userinfo": "個人信息", 3 | "member-layout.topmenu.logout": "退出", 4 | 5 | "member-layout.menu.home": "工作臺", 6 | 7 | "member-layout.menu.link": "外部鏈接", 8 | "member-layout.menu.link.github": "Github 倉庫", 9 | "member-layout.menu.link.gitee": "Gitee 倉庫", 10 | "member-layout.menu.link.docs": "使用文檔", 11 | 12 | "member-layout.menu.form": "表單頁", 13 | "member-layout.menu.form.basic": "基礎表單", 14 | "member-layout.menu.form.step": "分步表單", 15 | 16 | "member-layout.menu.list": "列表頁", 17 | "member-layout.menu.list.basic": "基礎列表", 18 | "member-layout.menu.list.filter": "篩選列表", 19 | "member-layout.menu.list.highlyAdaptive": "高度自適應列表", 20 | "member-layout.menu.list.highlyAdaptive2": "高度自適應列表2", 21 | 22 | "member-layout.menu.detail": "詳情頁", 23 | "member-layout.menu.detail.basic": "基礎詳情", 24 | "member-layout.menu.detail.module": "模塊詳情", 25 | "member-layout.menu.detail.table": "表格詳情", 26 | 27 | "member-layout.menu.result": "結果頁", 28 | "member-layout.menu.result.success": "成功頁", 29 | "member-layout.menu.result.fail": "失敗頁", 30 | 31 | "member-layout.menu.exception": "異常頁", 32 | "member-layout.menu.exception.403": "403", 33 | "member-layout.menu.exception.404": "404", 34 | "member-layout.menu.exception.500": "500", 35 | 36 | "member-layout.menu.permission": "權限驗證", 37 | "member-layout.menu.permission.all": "所有用戶都有權限", 38 | "member-layout.menu.permission.user": "User用戶有權限", 39 | "member-layout.menu.permission.test": "Test用戶有權限", 40 | 41 | "member-layout.menu.routeMetaExtend": "RouteMeta擴展", 42 | "member-layout.menu.routeMetaExtend.breadcrumb": "自定義面包屑", 43 | }; 44 | -------------------------------------------------------------------------------- /src/layouts/SecurityLayout.vue: -------------------------------------------------------------------------------- 1 | 43 | 47 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 28 | 42 | -------------------------------------------------------------------------------- /src/layouts/UserLayout/routes.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router"; 2 | 3 | const UserLayoutRoutes: RouteRecordRaw[] = [ 4 | { 5 | path: "/user", 6 | redirect: "/user/login", 7 | children: [ 8 | { 9 | meta: { 10 | title: "登录", 11 | }, 12 | path: "login", 13 | component: () => import("@/pages/user/login/index.vue"), 14 | }, 15 | ], 16 | }, 17 | ]; 18 | 19 | export default UserLayoutRoutes; 20 | -------------------------------------------------------------------------------- /src/locales/en-US.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "app.empty": "empty", 3 | "app.global.nodata": "No Data", 4 | "app.global.menu.notfound": "Not Found", 5 | "app.global.form.validatefields.catch": "The validation did not pass, please check the input", 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/index.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 全局 locale 入口 3 | * @author LiQingSong 4 | */ 5 | import { TI18n } from "@/@types/i18n.d"; 6 | 7 | import zhCN from "./zh-CN"; 8 | import zhTW from "./zh-TW"; 9 | import enUS from "./en-US"; 10 | 11 | const locales: TI18n = { 12 | "zh-CN": zhCN, 13 | "zh-TW": zhTW, 14 | "en-US": enUS, 15 | }; 16 | 17 | export default locales; 18 | -------------------------------------------------------------------------------- /src/locales/zh-CN.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "app.empty": "empty", 3 | "app.global.nodata": "暂无数据", 4 | "app.global.menu.notfound": "Not Found", 5 | "app.global.form.validatefields.catch": "验证不通过,请检查输入", 6 | }; 7 | -------------------------------------------------------------------------------- /src/locales/zh-TW.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | "app.empty": "empty", 3 | "app.global.nodata": "暫無數據", 4 | "app.global.menu.notfound": "Not Found", 5 | "app.global.form.validatefields.catch": "驗證不通過,請檢查輸入", 6 | }; 7 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | // 全局样式 4 | import "@/assets/css/global.scss"; 5 | // ElementPlus UI组件 6 | import ElementPlus from "element-plus"; 7 | // App 8 | import App from "@/App.vue"; 9 | // vue router 10 | import router from "@/config/router"; 11 | // pinia store 12 | import store from "@/config/store"; 13 | // Register icon sprite 14 | import "virtual:svg-icons-register"; 15 | 16 | const app = createApp(App); 17 | app.use(router); 18 | app.use(store); 19 | app.use(ElementPlus); 20 | app.mount("#app"); 21 | -------------------------------------------------------------------------------- /src/pages/404/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/pages/detail/basic/index.vue: -------------------------------------------------------------------------------- 1 | 75 | 139 | 152 | -------------------------------------------------------------------------------- /src/pages/detail/module/index.vue: -------------------------------------------------------------------------------- 1 | 75 | 143 | 156 | -------------------------------------------------------------------------------- /src/pages/exception/403/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/pages/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/pages/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 9 | 10 | 19 | -------------------------------------------------------------------------------- /src/pages/form/base/data.d.ts: -------------------------------------------------------------------------------- 1 | import type { ModelValueType } from "element-plus"; 2 | export interface IFormData { 3 | name: string; 4 | date: ModelValueType; 5 | select: string; 6 | radio1: string; 7 | radio2: string; 8 | checkbox: string[]; 9 | remark: string; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/form/base/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 67 | 68 | 142 | 143 | 144 | -------------------------------------------------------------------------------- /src/pages/form/step/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 79 | 80 | 157 | 158 | 159 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IArticleChartData { 2 | total: number; 3 | num: number; 4 | week: number; 5 | day: number; 6 | } 7 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleChartCard/index.vue: -------------------------------------------------------------------------------- 1 | 34 | 69 | 104 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export async function dailynewArticles(): Promise { 4 | return request({ 5 | url: "/home/articles/dailynew", 6 | method: "get", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleHitCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | sort?: number; 5 | } 6 | 7 | export interface IPaginationConfig { 8 | total: number; 9 | current: number; 10 | pageSize: number; 11 | } 12 | 13 | export interface ITableListItem { 14 | id: number; 15 | title: string; 16 | hit: number; 17 | } 18 | 19 | export interface ITableData { 20 | loading: boolean; 21 | list: ITableListItem[]; 22 | pagination: IPaginationConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleHitCard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 83 | -------------------------------------------------------------------------------- /src/pages/home/components/ArticleHitCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/home/articles", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/HotSearchCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | sort?: number; 5 | } 6 | 7 | export interface IPaginationConfig { 8 | total: number; 9 | current: number; 10 | pageSize: number; 11 | } 12 | 13 | export interface ITableListItem { 14 | id: number; 15 | title: string; 16 | hit: number; 17 | } 18 | 19 | export interface ITableData { 20 | loading: boolean; 21 | list: ITableListItem[]; 22 | pagination: IPaginationConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/home/components/HotSearchCard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 83 | -------------------------------------------------------------------------------- /src/pages/home/components/HotSearchCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/home/searchs/keywords", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/HotTagsCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | sort?: number; 5 | } 6 | 7 | export interface IPaginationConfig { 8 | total: number; 9 | current: number; 10 | pageSize: number; 11 | } 12 | 13 | export interface ITableListItem { 14 | id: number; 15 | name: string; 16 | hit: number; 17 | pinyin?: string; 18 | } 19 | 20 | export interface ITableData { 21 | loading: boolean; 22 | list: ITableListItem[]; 23 | pagination: IPaginationConfig; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/home/components/HotTagsCard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 83 | -------------------------------------------------------------------------------- /src/pages/home/components/HotTagsCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/home/tags", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/LinksChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IChartData { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface IWorksChartData { 7 | total: number; 8 | num: number; 9 | chart: IChartData; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/LinksChartCard/index.vue: -------------------------------------------------------------------------------- 1 | 90 | 109 | 135 | -------------------------------------------------------------------------------- /src/pages/home/components/LinksChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export async function annualnewLinks(): Promise { 4 | return request({ 5 | url: "/home/links/annualnew", 6 | method: "get", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/home/components/TopicsChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IChartData { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface IWorksChartData { 7 | total: number; 8 | num: number; 9 | chart: IChartData; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/TopicsChartCard/index.vue: -------------------------------------------------------------------------------- 1 | 111 | 130 | 156 | -------------------------------------------------------------------------------- /src/pages/home/components/TopicsChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export async function monthnewTopics(): Promise { 4 | return request({ 5 | url: "/home/topics/monthnew", 6 | method: "get", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksChartCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IChartData { 2 | day: string[]; 3 | num: number[]; 4 | } 5 | 6 | export interface IWorksChartData { 7 | total: number; 8 | num: number; 9 | chart: IChartData; 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksChartCard/index.vue: -------------------------------------------------------------------------------- 1 | 108 | 127 | 153 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksChartCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export async function weeknewWorks(): Promise { 4 | return request({ 5 | url: "/home/works/weeknew", 6 | method: "get", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksHitCard/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | sort?: number; 5 | } 6 | 7 | export interface IPaginationConfig { 8 | total: number; 9 | current: number; 10 | pageSize: number; 11 | } 12 | 13 | export interface ITableListItem { 14 | id: number; 15 | title: string; 16 | hit: number; 17 | } 18 | 19 | export interface ITableData { 20 | loading: boolean; 21 | list: ITableListItem[]; 22 | pagination: IPaginationConfig; 23 | } 24 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksHitCard/index.vue: -------------------------------------------------------------------------------- 1 | 40 | 83 | -------------------------------------------------------------------------------- /src/pages/home/components/WorksHitCard/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/home/works", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/pages/home/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 44 | 45 | 79 | 80 | 81 | -------------------------------------------------------------------------------- /src/pages/list/basic/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface IPaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | } 11 | 12 | export interface ITableListItem { 13 | id: number; 14 | name: string; 15 | desc: string; 16 | href: string; 17 | type: string; 18 | } 19 | 20 | export interface ITableData { 21 | loading: boolean; 22 | list: ITableListItem[]; 23 | pagination: IPaginationConfig; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/list/basic/index.vue: -------------------------------------------------------------------------------- 1 | 43 | 115 | -------------------------------------------------------------------------------- /src/pages/list/basic/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams, ITableListItem } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/pages/list", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: "/pages/list", 15 | method: "POST", 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: "PUT", 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: "delete", 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({ url: `/pages/list/${id}` }); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/list/filter/components/TypeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 36 | -------------------------------------------------------------------------------- /src/pages/list/filter/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface IQueryParams { 2 | page: number; 3 | per: number; 4 | } 5 | 6 | export interface IPaginationConfig { 7 | total: number; 8 | current: number; 9 | pageSize: number; 10 | } 11 | 12 | export interface ITableListItem { 13 | id: number; 14 | name: string; 15 | desc: string; 16 | href: string; 17 | type: string; 18 | } 19 | 20 | export interface ITableData { 21 | loading: boolean; 22 | list: ITableListItem[]; 23 | pagination: IPaginationConfig; 24 | } 25 | -------------------------------------------------------------------------------- /src/pages/list/filter/index.vue: -------------------------------------------------------------------------------- 1 | 75 | 178 | -------------------------------------------------------------------------------- /src/pages/list/filter/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams, ITableListItem } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/pages/list", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: "/pages/list", 15 | method: "POST", 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: "PUT", 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: "delete", 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({ url: `/pages/list/${id}` }); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive/data.d.ts: -------------------------------------------------------------------------------- 1 | import { IPaginationConfig } from "@/components/ScreenTable/data.d"; 2 | export interface IQueryParams { 3 | page: number; 4 | per: number; 5 | } 6 | 7 | export interface ITableListItem { 8 | id: number; 9 | name: string; 10 | desc: string; 11 | href: string; 12 | type: string; 13 | } 14 | 15 | export interface ITableData { 16 | loading: boolean; 17 | list: ITableListItem[]; 18 | pagination: IPaginationConfig; 19 | } 20 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive/index.vue: -------------------------------------------------------------------------------- 1 | 56 | 89 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams, ITableListItem } from "./data.d"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/pages/list", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: "/pages/list", 15 | method: "POST", 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: "PUT", 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: "delete", 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({ url: `/pages/list/${id}` }); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive2/components/TypeSelect/index.vue: -------------------------------------------------------------------------------- 1 | 29 | 36 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive2/data.d.ts: -------------------------------------------------------------------------------- 1 | import { IPaginationConfig } from "@/components/ScreenTable/data.d"; 2 | 3 | export interface IQueryParams { 4 | page: number; 5 | per: number; 6 | } 7 | 8 | export interface ITableListItem { 9 | id: number; 10 | name: string; 11 | desc: string; 12 | href: string; 13 | type: string; 14 | } 15 | 16 | export interface ITableData { 17 | loading: boolean; 18 | list: ITableListItem[]; 19 | pagination: IPaginationConfig; 20 | } 21 | -------------------------------------------------------------------------------- /src/pages/list/highlyAdaptive2/service.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { IQueryParams, ITableListItem } from "./data"; 3 | 4 | export async function queryList(params?: IQueryParams): Promise { 5 | return request({ 6 | url: "/pages/list", 7 | method: "get", 8 | params, 9 | }); 10 | } 11 | 12 | export async function createData(params: Omit): Promise { 13 | return request({ 14 | url: "/pages/list", 15 | method: "POST", 16 | data: params, 17 | }); 18 | } 19 | 20 | export async function updateData(id: number, params: Omit): Promise { 21 | return request({ 22 | url: `/pages/list/${id}`, 23 | method: "PUT", 24 | data: params, 25 | }); 26 | } 27 | 28 | export async function removeData(id: number): Promise { 29 | return request({ 30 | url: `/pages/list/${id}`, 31 | method: "delete", 32 | }); 33 | } 34 | 35 | export async function detailData(id: number): Promise { 36 | return request({ url: `/pages/list/${id}` }); 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/permission/all/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 130 | -------------------------------------------------------------------------------- /src/pages/permission/test/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/pages/permission/user/index.vue: -------------------------------------------------------------------------------- 1 | 2 | 5 | -------------------------------------------------------------------------------- /src/pages/result/fail/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/pages/result/success/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 13 | -------------------------------------------------------------------------------- /src/pages/routeMetaExtend/breadcrumb/index.vue: -------------------------------------------------------------------------------- 1 | 30 | 39 | -------------------------------------------------------------------------------- /src/pages/user/login/data.d.ts: -------------------------------------------------------------------------------- 1 | export interface LoginParamsType { 2 | username: string; 3 | password: string; 4 | } 5 | -------------------------------------------------------------------------------- /src/pages/user/login/index.vue: -------------------------------------------------------------------------------- 1 | 59 | 60 | 105 | 106 | 128 | -------------------------------------------------------------------------------- /src/pages/user/login/server.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | import { LoginParamsType } from "./data"; 3 | 4 | export async function accountLogin(params: LoginParamsType): Promise { 5 | return request({ 6 | url: "/user/login", 7 | method: "POST", 8 | data: params, 9 | }); 10 | } 11 | -------------------------------------------------------------------------------- /src/services/user.ts: -------------------------------------------------------------------------------- 1 | import request from "@/utils/request"; 2 | 3 | export async function queryUserInfo(): Promise { 4 | return request({ 5 | url: "/user/info", 6 | method: "get", 7 | }); 8 | } 9 | -------------------------------------------------------------------------------- /src/store/global.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 全局 store 3 | * @author LiQingSong 4 | */ 5 | import { defineStore } from "pinia"; 6 | import { theme, menuLayout, menuStyle, isTabsNav, isLayoutFooter } from "@/config/settings"; 7 | import { TTheme, TMenuLayout, TMenuStyle } from "@/@types/config.settings"; 8 | 9 | // state ts类型 10 | export interface IGlobalState { 11 | /* 以下是针对所有 Layout 扩展字段 */ 12 | // 左侧展开收起 13 | collapsed: boolean; 14 | // 模板主题 15 | theme: TTheme; 16 | 17 | /* 以下是针对 MemberLayout 扩展字段 */ 18 | // 菜单导航布局 19 | menuLayout: TMenuLayout; 20 | // 菜单导航风格 21 | menuStyle: TMenuStyle; 22 | // 是否启用多标签Tab页 23 | isTabsNav: boolean; 24 | // 是否启用底部 25 | isLayoutFooter: boolean; 26 | } 27 | 28 | export const useGlobalStore = defineStore("useGlobalStore", { 29 | state: (): IGlobalState => { 30 | return { 31 | collapsed: false, 32 | theme: theme, 33 | menuLayout: menuLayout, 34 | menuStyle: menuStyle, 35 | isTabsNav: isTabsNav, 36 | isLayoutFooter: isLayoutFooter, 37 | }; 38 | }, 39 | getters: {}, 40 | actions: {}, 41 | }); 42 | -------------------------------------------------------------------------------- /src/store/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义I18n store 3 | * @author LiQingSong 4 | */ 5 | import { defineStore } from "pinia"; 6 | import { isArray, isObject } from "@/utils/is"; 7 | import { getLocale, defaultLang } from "@/utils/i18n"; 8 | 9 | import { TI18n, TI18nKey, TUseFormat } from "@/@types/i18n"; 10 | 11 | // 导入 element-plus 语言包 12 | import { zhCn, zhTw, en, Language } from "element-plus/es/locale/index"; 13 | const elementPlusMessages = { 14 | "zh-CN": zhCn, 15 | "zh-TW": zhTw, 16 | "en-US": en, 17 | }; 18 | 19 | // 导入全局自定义语言 20 | import globalLocales from "@/locales"; 21 | 22 | // state ts类型 23 | export interface II18nState { 24 | // 语言名 25 | locale: TI18nKey; 26 | // 语言包 27 | messages: TI18n; 28 | // element-plus 语言内容 29 | elementPlusLocale: Language; 30 | } 31 | 32 | // 获取当前系统或存储的语言 33 | const sysLocale = getLocale(); 34 | 35 | export const useI18nStore = defineStore("useI18nStore", { 36 | state: (): II18nState => { 37 | return { 38 | locale: globalLocales[sysLocale] ? sysLocale : defaultLang, 39 | messages: globalLocales, 40 | elementPlusLocale: elementPlusMessages[sysLocale], 41 | }; 42 | }, 43 | getters: {}, 44 | actions: { 45 | /** 46 | * @description 引入语言包 47 | * @param locales 当前本地(文件夹下)语言包 48 | * @returns (key: string, format?: TUseFormat) => string 49 | */ 50 | use(locales?: TI18n) { 51 | return (key: string, format?: TUseFormat) => { 52 | const i18nMessage = this.messages[this.locale] || {}; 53 | const locale = locales ? locales[this.locale] || {} : {}; 54 | let str = i18nMessage[key] || locale[key] || key; 55 | 56 | if (isObject(format) || isArray(format)) { 57 | const newFormat: any = format; 58 | for (const key in newFormat) { 59 | if (Object.prototype.hasOwnProperty.call(newFormat, key)) { 60 | const pattern = `\\{${key}\\}`; 61 | str = str.replace(new RegExp(pattern, "ig"), newFormat[key]); 62 | } 63 | } 64 | } 65 | 66 | return str; 67 | }; 68 | }, 69 | 70 | /** 71 | * @description 设置 element-plus 多语言 72 | * @param locale 语言名称 73 | */ 74 | setElementPlusLocale(locale?: TI18nKey) { 75 | const l = locale || this.locale; 76 | this.elementPlusLocale = elementPlusMessages[l]; 77 | }, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /src/store/user.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 登录用户信息 store 3 | * @author LiQingSong 4 | */ 5 | import { defineStore } from "pinia"; 6 | import { ResultCodeEnum } from "@/enums/utils.request.enum"; 7 | import { IResponseData } from "@/@types/utils.request"; 8 | import { queryUserInfo } from "@/services/user"; 9 | 10 | // state ts类型 11 | export interface IUserState { 12 | // 用户id 13 | id: number; 14 | // 用户名 15 | name: string; 16 | // 用户权限角色 17 | roles: string[]; 18 | } 19 | 20 | export const useUserStore = defineStore("useUserStore", { 21 | state: (): IUserState => { 22 | return { 23 | id: 0, 24 | name: "", 25 | roles: [], 26 | }; 27 | }, 28 | getters: { 29 | // 是否登录 30 | isLogin({ id }) { 31 | return id > 0; 32 | }, 33 | }, 34 | actions: { 35 | /** 36 | * @description: 获取用户信息 37 | * @returns result code 0 已登录并且获取用户信息成功,1 未登录, 2 后端返回的其他错误,999 服务器错误 38 | */ 39 | async getInfo() { 40 | const result = { code: 0, msg: "" }; 41 | if (this.id > 0) { 42 | // 如果用户已经登录了,就不要请求了 43 | return result; 44 | } 45 | 46 | try { 47 | const response: IResponseData = await queryUserInfo(); 48 | const data = response.data; 49 | this.id = data.id || 0; 50 | this.name = data.name || ""; 51 | this.roles = data.roles || []; 52 | } catch (error: any) { 53 | console.log("error", error); 54 | if (error.message && error.message === "CustomError") { 55 | const response = error.response || { data: { code: ResultCodeEnum.LOGININVALID, msg: "" } }; 56 | const { code, msg } = response.data; 57 | if (code === ResultCodeEnum.LOGININVALID) { 58 | result.code = 1; 59 | } else { 60 | result.code = 2; 61 | } 62 | result.msg = msg; 63 | } else { 64 | result.code = 999; 65 | result.msg = error; 66 | } 67 | } 68 | return result; 69 | }, 70 | /** 71 | * @description: 重置用户信息 72 | */ 73 | reset() { 74 | this.id = 0; 75 | this.name = ""; 76 | this.roles = []; 77 | }, 78 | }, 79 | }); 80 | -------------------------------------------------------------------------------- /src/utils/i18n.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义 i18n 方法 3 | * @author LiQingSong 4 | */ 5 | 6 | import { TI18nKey } from "@/@types/i18n.d"; 7 | 8 | /** 9 | * @description: window.localStorage 存储key 10 | */ 11 | export const localeKey = "locale"; 12 | 13 | /** 14 | * @description: 默认语言 15 | */ 16 | export const defaultLang: TI18nKey = "zh-CN"; 17 | 18 | /** 19 | * @description: 验证语言命名规则 zh-CN 20 | * @returns boolen 21 | */ 22 | export const localeNameExp = (lang: string): boolean => { 23 | const localeExp = /^([a-z]{2})-?([A-Z]{2})?$/; 24 | return localeExp.test(lang); 25 | }; 26 | 27 | /** 28 | * @description: 设置 html lang 属性值 29 | * @param lang 语言的 TI18nKey 30 | */ 31 | export const setHtmlLang = (lang: TI18nKey) => { 32 | /** 33 | * axios.defaults.headers.common['Accept-Language'] = locale 34 | */ 35 | const htmlSelector = document.querySelector("html"); 36 | if (htmlSelector) { 37 | htmlSelector.setAttribute("lang", lang); 38 | } 39 | }; 40 | 41 | /** 42 | * @description: 获取当前选择的语言 43 | * @returns string 44 | */ 45 | export const getLocale = (): TI18nKey => { 46 | const lang = typeof window.localStorage !== "undefined" ? window.localStorage.getItem(localeKey) : ""; 47 | const isNavigatorLanguageValid = typeof navigator !== "undefined" && typeof navigator.language === "string"; 48 | const browserLang = isNavigatorLanguageValid ? navigator.language.split("-").join("-") : ""; 49 | return (lang || browserLang || defaultLang) as TI18nKey; 50 | }; 51 | 52 | /** 53 | * @description: 切换语言 54 | * @param lang 语言的 TI18nKey 55 | * @param realReload 是否刷新页面,默认刷新 56 | */ 57 | export const setLocale = (lang: TI18nKey, realReload?: boolean, callback?: Function) => { 58 | if (lang !== undefined && !localeNameExp(lang)) { 59 | // for reset when lang === undefined 60 | throw new Error("setLocale lang format error"); 61 | } 62 | if (getLocale() !== lang) { 63 | if (typeof window.localStorage !== "undefined") { 64 | window.localStorage.setItem(localeKey, lang || ""); 65 | } 66 | 67 | if (realReload === true) { 68 | window.location.reload(); 69 | } else { 70 | setHtmlLang(lang); 71 | 72 | if (typeof callback === "function") { 73 | callback(); 74 | } 75 | } 76 | } 77 | }; 78 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义 is 方法 3 | * @author LiQingSong 4 | */ 5 | 6 | /** 7 | * @description: 判断值是否未某个类型 8 | */ 9 | export function is(val: unknown, type: string) { 10 | return Object.prototype.toString.call(val) === `[object ${type}]`; 11 | } 12 | 13 | /** 14 | * @description: 是否为函数 15 | */ 16 | export function isFunction(val: unknown): val is T { 17 | return is(val, "Function"); 18 | } 19 | 20 | /** 21 | * @description: 是否已定义 22 | */ 23 | export const isDef = (val?: T): val is T => { 24 | return typeof val !== "undefined"; 25 | }; 26 | 27 | export const isUnDef = (val?: T): val is T => { 28 | return !isDef(val); 29 | }; 30 | 31 | /** 32 | * @description: 是否为对象 33 | */ 34 | export const isObject = (val: any): val is Record => { 35 | return val !== null && is(val, "Object"); 36 | }; 37 | 38 | /** 39 | * @description: 是否为时间 40 | */ 41 | export function isDate(val: unknown): val is Date { 42 | return is(val, "Date"); 43 | } 44 | 45 | /** 46 | * @description: 是否为数值 47 | */ 48 | export function isNumber(val: unknown): val is number { 49 | return is(val, "Number"); 50 | } 51 | 52 | /** 53 | * @description: 是否为AsyncFunction 54 | */ 55 | export function isAsyncFunction(val: unknown): val is Promise { 56 | return is(val, "AsyncFunction"); 57 | } 58 | 59 | /** 60 | * @description: 是否为promise 61 | */ 62 | export function isPromise(val: unknown): val is Promise { 63 | return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch); 64 | } 65 | 66 | /** 67 | * @description: 是否为字符串 68 | */ 69 | export function isString(val: unknown): val is string { 70 | return is(val, "String"); 71 | } 72 | 73 | /** 74 | * @description: 是否为boolean类型 75 | */ 76 | export function isBoolean(val: unknown): val is boolean { 77 | return is(val, "Boolean"); 78 | } 79 | 80 | /** 81 | * @description: 是否为数组 82 | */ 83 | export function isArray(val: any): val is Array { 84 | return val && Array.isArray(val); 85 | } 86 | 87 | /** 88 | * @description: 是否为null 89 | */ 90 | export function isNull(val: unknown): val is null { 91 | return val === null; 92 | } 93 | 94 | /** 95 | * @description: 判断是否是外链 96 | * @param {string} path 97 | * @returns {Boolean} 98 | */ 99 | export const isExternal = (path: string): boolean => /^(https?:|mailto:|tel:)/.test(path); 100 | -------------------------------------------------------------------------------- /src/utils/localToken.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * @description: 自定义 token 操作 3 | * @author LiQingSong 4 | */ 5 | import { siteTokenKey } from "@/config/settings"; 6 | 7 | /** 8 | * @description: 获取本地Token 9 | */ 10 | export const getToken = () => localStorage.getItem(siteTokenKey); 11 | 12 | /** 13 | * @description: 设置存储本地Token 14 | */ 15 | export const setToken = (token: string) => { 16 | localStorage.setItem(siteTokenKey, token); 17 | }; 18 | 19 | /** 20 | * @description: 移除本地Token 21 | */ 22 | export const removeToken = () => { 23 | localStorage.removeItem(siteTokenKey); 24 | }; 25 | -------------------------------------------------------------------------------- /src/utils/object.ts: -------------------------------------------------------------------------------- 1 | import { LocationQuery } from "vue-router"; 2 | 3 | /** 4 | * 浅比较两个object, json的key是否一致 5 | * @param obj1 6 | * @param obj2 7 | * @returns 8 | * @author LiQingSong 9 | */ 10 | export function equalObjectKey(obj1: Object, obj2: Object): boolean { 11 | const obj1Keys: string[] = Object.keys(obj1); 12 | const obj2Keys: string[] = Object.keys(obj2); 13 | const obj1KeysLen: number = obj1Keys.length; 14 | if (obj1KeysLen !== obj2Keys.length) { 15 | return false; 16 | } 17 | let is = true; 18 | for (let index = 0; index < obj1KeysLen; index++) { 19 | const element: string = obj1Keys[index]; 20 | if (!Object.prototype.hasOwnProperty.call(obj2, element)) { 21 | is = false; 22 | break; 23 | } 24 | } 25 | return is; 26 | } 27 | 28 | /** 29 | * 浅比较两个对象是否相等,这两个对象的值只能是数字或字符串 30 | * @param obj1 31 | * @param obj2 32 | * @author LiQingSong 33 | * @returns 34 | */ 35 | export function equalObject(obj1: LocationQuery, obj2: LocationQuery): boolean { 36 | const obj1Keys: string[] = Object.keys(obj1); 37 | const obj2Keys: string[] = Object.keys(obj2); 38 | const obj1KeysLen: number = obj1Keys.length; 39 | const obj2KeysLen: number = obj2Keys.length; 40 | if (obj1KeysLen !== obj2KeysLen) { 41 | return false; 42 | } 43 | 44 | if (obj1KeysLen === 0 && obj2KeysLen === 0) { 45 | return true; 46 | } 47 | 48 | return !obj1Keys.some((key) => obj1[key] != obj2[key]); 49 | } 50 | -------------------------------------------------------------------------------- /svgo.config.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | plugins: [ 3 | { 4 | name: "removeAttrs", 5 | params: { 6 | attrs: "(fill|fill-rule)", 7 | }, 8 | }, 9 | ], 10 | }; 11 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "ESNext", 4 | "useDefineForClassFields": true, 5 | "module": "ESNext", 6 | "moduleResolution": "Node", 7 | "strict": true, 8 | "jsx": "preserve", 9 | "sourceMap": true, 10 | "resolveJsonModule": true, 11 | "isolatedModules": true, 12 | "esModuleInterop": true, 13 | "lib": ["ESNext", "DOM"], 14 | "types": ["vite/client","element-plus/global"], 15 | // 跳过库检查,解决打包失败 16 | "skipLibCheck": true, 17 | // 解析非相对模块名的基准目录 18 | "baseUrl": ".", 19 | // 模块名到基于 baseUrl的路径映射的列表。 20 | "paths": { 21 | "@/*": ["src/*"] 22 | } 23 | }, 24 | "include": ["src/**/*.ts", "src/**/*.d.ts", "src/**/*.tsx", "src/**/*.vue"], 25 | "exclude": ["node_modules", "dist", "**/*.js"], 26 | "references": [{ "path": "./tsconfig.node.json" }] 27 | } 28 | -------------------------------------------------------------------------------- /tsconfig.node.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "composite": true, 4 | "module": "ESNext", 5 | "moduleResolution": "Node", 6 | "allowSyntheticDefaultImports": true 7 | }, 8 | "include": ["vite.config.ts"] 9 | } 10 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path"; 2 | import { defineConfig, loadEnv, PluginOption } from "vite"; 3 | import vue from "@vitejs/plugin-vue"; 4 | import eslintPlugin from "vite-plugin-eslint"; 5 | import viteCompression from "vite-plugin-compression"; 6 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 7 | import { visualizer } from "rollup-plugin-visualizer"; 8 | import { viteMockServe } from "vite-plugin-mock"; 9 | 10 | // @see: https://vitejs.dev/config/ 11 | export default defineConfig(({ command, mode }) => { 12 | const root = process.cwd(); 13 | const env = loadEnv(mode, root); 14 | const { 15 | VITE_APP_PORT, 16 | VITE_APP_MOCK, 17 | VITE_APP_REPORT, 18 | VITE_APP_BUILD_GZIP, 19 | VITE_APP_OPEN, 20 | VITE_APP_API_URL_PROXY, 21 | VITE_APP_API_URL, 22 | } = env; 23 | 24 | const isBuild = command === "build"; 25 | 26 | // vite 插件 27 | const vitePlugins: (PluginOption | PluginOption[])[] = [ 28 | vue(), 29 | // 使用 svg 图标 30 | createSvgIconsPlugin({ 31 | iconDirs: [resolve(__dirname, "./src/assets/iconsvg")], 32 | symbolId: "icon-[name]", 33 | }), 34 | // EsLint 报错信息显示在浏览器界面上 35 | eslintPlugin(), 36 | ]; 37 | 38 | // vite-plugin-compression gzip compress 39 | if (VITE_APP_BUILD_GZIP === "true") { 40 | vitePlugins.push( 41 | viteCompression({ 42 | verbose: true, 43 | disable: false, 44 | threshold: 10240, 45 | algorithm: "gzip", 46 | ext: ".gz", 47 | }), 48 | ); 49 | } 50 | 51 | // rollup-plugin-visualizer 是否生成包预览(分析依赖包大小,方便做优化处理) 52 | if (VITE_APP_REPORT === "true") { 53 | vitePlugins.push(visualizer()); 54 | } 55 | 56 | // vite-plugin-mock 57 | if (VITE_APP_MOCK === "true") { 58 | vitePlugins.push( 59 | viteMockServe({ 60 | mockPath: "mock", 61 | supportTs: true, 62 | watchFiles: true, 63 | localEnabled: !isBuild, 64 | prodEnabled: isBuild, 65 | logger: true, 66 | }), 67 | ); 68 | } 69 | 70 | // proxy 71 | const proxy = {}; 72 | if (!isBuild) { 73 | // 不是生产环境 74 | if (VITE_APP_API_URL_PROXY && VITE_APP_API_URL_PROXY !== "") { 75 | // VITE_APP_API_URL_PROXY存在,启用;如果VITE_APP_MOCK启用且mock中有相同url,则mock优先 76 | proxy[VITE_APP_API_URL] = { 77 | target: VITE_APP_API_URL_PROXY, 78 | rewrite: (path) => path.replace(VITE_APP_API_URL, ""), 79 | changeOrigin: true, 80 | }; 81 | } 82 | } 83 | 84 | return { 85 | root, 86 | server: { 87 | host: "0.0.0.0", 88 | port: Number(VITE_APP_PORT || 3001), 89 | open: VITE_APP_OPEN === "true", 90 | cors: true, 91 | proxy, 92 | }, 93 | resolve: { 94 | alias: [ 95 | { 96 | find: /^~/, 97 | replacement: `${resolve(__dirname, "./node_modules")}/`, 98 | }, 99 | { 100 | find: /@\//, 101 | replacement: `${resolve(__dirname, "./src")}/`, 102 | }, 103 | ], 104 | }, 105 | plugins: vitePlugins, 106 | css: { 107 | preprocessorOptions: { 108 | less: { 109 | javascriptEnabled: true, 110 | }, 111 | }, 112 | }, 113 | }; 114 | }); 115 | --------------------------------------------------------------------------------