├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc.json ├── .gitignore ├── .prettierignore ├── .prettierrc.js ├── .stylelintignore ├── .stylelintrc.json ├── .vscode └── extensions.json ├── LICENSE ├── README.md ├── auto-imports.d.ts ├── build ├── cdn.ts ├── compress.ts ├── index.ts └── plugins.ts ├── index.html ├── mock ├── _createProductionServer.ts ├── analysis │ └── index.mock.ts ├── useTable │ └── index.mock.ts ├── user │ └── index.mock.ts └── utils.ts ├── package.json ├── pnpm-lock.yaml ├── src ├── App.vue ├── api │ ├── analysis │ │ └── index.ts │ ├── table │ │ ├── index.ts │ │ └── types.ts │ └── user │ │ ├── index.ts │ │ └── types.ts ├── assets │ ├── imgs │ │ ├── Avatar.png │ │ ├── VxLogo.png │ │ ├── avatar1.png │ │ ├── avatar2.png │ │ ├── avatar3.png │ │ ├── avatar4.png │ │ └── avatar5.png │ └── svgs │ │ ├── loading.svg │ │ └── login-banner.svg ├── axios │ ├── helper │ │ └── index.ts │ ├── index.ts │ └── types │ │ └── index.ts ├── components │ ├── Avatar │ │ ├── index.ts │ │ └── src │ │ │ ├── Avatar.scss │ │ │ └── Avatar.vue │ ├── Breadcrumb │ │ ├── index.ts │ │ └── src │ │ │ ├── Breadcrumb.scss │ │ │ ├── Breadcrumb.vue │ │ │ └── type.ts │ ├── Form │ │ ├── index.ts │ │ └── src │ │ │ ├── Form.vue │ │ │ ├── components │ │ │ ├── useRenderCheckbox.tsx │ │ │ ├── useRenderRadio.tsx │ │ │ └── useRenderSelect.tsx │ │ │ ├── helper │ │ │ ├── componentMap.ts │ │ │ └── index.ts │ │ │ └── types │ │ │ └── index.ts │ ├── Fullscreen │ │ ├── index.ts │ │ └── src │ │ │ ├── Fullscreen.scss │ │ │ └── Fullscreen.vue │ ├── LocaleSwitch │ │ ├── index.ts │ │ └── src │ │ │ ├── LocaleSwitch.scss │ │ │ └── LocaleSwitch.vue │ ├── Logo │ │ ├── index.ts │ │ └── src │ │ │ ├── Logo.scss │ │ │ └── Logo.vue │ ├── Menu │ │ ├── index.ts │ │ └── src │ │ │ ├── Menu.scss │ │ │ └── Menu.vue │ ├── Qrcode │ │ ├── index.ts │ │ └── src │ │ │ ├── Qrcode.scss │ │ │ ├── Qrcode.vue │ │ │ └── types │ │ │ └── index.ts │ ├── SearchMenus │ │ ├── index.ts │ │ └── src │ │ │ ├── SearchMenus.scss │ │ │ ├── SearchMenus.vue │ │ │ ├── helper │ │ │ └── index.ts │ │ │ └── types │ │ │ └── index.ts │ ├── StructureTypes │ │ └── index.ts │ ├── Table │ │ ├── index.ts │ │ └── src │ │ │ ├── Table.scss │ │ │ ├── Table.vue │ │ │ ├── components │ │ │ ├── TableSetting.scss │ │ │ └── TableSettings.vue │ │ │ ├── helper │ │ │ └── index.ts │ │ │ └── types │ │ │ └── index.ts │ ├── TagsView │ │ ├── index.ts │ │ └── src │ │ │ ├── TagsView.scss │ │ │ └── TagsView.vue │ ├── ThemeSwitch │ │ ├── index.ts │ │ └── src │ │ │ └── ThemeSwitch.vue │ ├── VxContainer │ │ ├── index.ts │ │ └── src │ │ │ ├── VxContainer.scss │ │ │ └── VxContainer.vue │ └── VxIcon │ │ ├── index.ts │ │ └── src │ │ ├── VxIcon.vue │ │ └── types │ │ └── index.ts ├── constants │ └── index.ts ├── directives │ └── auth │ │ ├── index.ts │ │ └── utils.ts ├── hooks │ ├── useEcharts.ts │ ├── useForm.ts │ ├── useI18n.ts │ ├── useIcon.ts │ ├── useLocale.ts │ ├── useProgress.ts │ ├── useStorage.ts │ ├── useStructure.ts │ ├── useTable.ts │ ├── useValidator.ts │ └── useX6.ts ├── layout │ ├── index.ts │ └── src │ │ ├── components │ │ ├── Horizontal.scss │ │ ├── Horizontal.vue │ │ ├── Vertical.scss │ │ ├── Vertical.vue │ │ └── index.ts │ │ ├── index.scss │ │ └── index.vue ├── locales │ └── lang │ │ ├── cn.ts │ │ └── en.ts ├── main.ts ├── plugins │ ├── echarts │ │ └── index.ts │ ├── elementPlus │ │ └── index.ts │ ├── vueI18n │ │ └── index.ts │ └── vxeTable │ │ └── index.ts ├── router │ ├── asyncRouterHelper.ts │ └── index.ts ├── store │ ├── index.ts │ └── modules │ │ ├── app.ts │ │ ├── locale.ts │ │ ├── router.ts │ │ └── tags.ts ├── styles │ ├── element │ │ ├── dark.scss │ │ └── index.scss │ ├── index.scss │ ├── mixin.scss │ ├── namespace.scss │ ├── reset.scss │ └── var.scss ├── utils │ ├── index.ts │ ├── is.ts │ ├── routerUtils.ts │ ├── theme.ts │ └── tsxUtils.ts ├── views │ ├── Authority │ │ └── ButtonPermissions.vue │ ├── Component │ │ ├── Echarts │ │ │ └── Echarts.vue │ │ ├── Form │ │ │ ├── DefaultForm.vue │ │ │ └── UseForm.vue │ │ ├── Qrcode │ │ │ └── Qrcode.vue │ │ └── Table │ │ │ ├── DefaultTable.vue │ │ │ ├── UseTable.vue │ │ │ └── VxeTable.vue │ ├── Dashboard │ │ ├── Analysis.scss │ │ ├── Analysis.vue │ │ ├── Workplace.scss │ │ └── Workplace.vue │ ├── Error │ │ └── 404.vue │ ├── Login │ │ ├── Login.scss │ │ ├── Login.vue │ │ └── components │ │ │ └── LoginForm.vue │ ├── Redirect │ │ └── Redirect.vue │ ├── System │ │ ├── Menu.vue │ │ ├── Role.vue │ │ └── User.vue │ └── Workflow │ │ └── useAntvX6.vue └── vite-env.d.ts ├── tsconfig.json ├── types ├── components.d.ts ├── gobal.d.ts └── router.d.ts └── vite.config.ts /.env.development: -------------------------------------------------------------------------------- 1 | # 项目标题,名称 2 | VITE_APP_TITLE = "VxAdmin" 3 | # 本地运行端口号(修改此处保存可立即重启vite开发服务器) 4 | VITE_PORT = 8080 5 | 6 | # 开发环境读取配置文件路径 7 | VITE_PUBLIC_PATH = / 8 | 9 | # 开发环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 10 | VITE_ROUTER_HISTORY = "hash" 11 | 12 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 13 | VITE_COMPRESSION = "none" 14 | #是否生成report.html报告文件 15 | VITE_REPORT = false -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | # 项目标题,名称 2 | VITE_APP_TITLE = "VxAdmin" 3 | 4 | # 线上环境平台打包路径 5 | VITE_PUBLIC_PATH = / 6 | 7 | # 线上环境路由历史模式(Hash模式传"hash"、HTML5模式传"h5"、Hash模式带base参数传"hash,base参数"、HTML5模式带base参数传"h5,base参数") 8 | VITE_ROUTER_HISTORY = "hash" 9 | 10 | # 压缩时删除原始文件的配置:gzip-clear、brotli-clear、both-clear(同时开启 gzip 与 brotli 压缩)、none(不开启压缩,默认) 11 | VITE_COMPRESSION = "gzip" 12 | 13 | # 是否在打包时使用cdn替换本地库 替换 true 不替换 false 14 | VITE_CDN = true 15 | #是否生成report.html报告文件 16 | VITE_REPORT = false 17 | -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | *.sh 2 | node_modules 3 | *.md 4 | *.woff 5 | *.ttf 6 | .vscode 7 | dist 8 | /public 9 | .local 10 | /bin 11 | /src/mock/* 12 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "root": true, 3 | "env": { 4 | "browser": true, 5 | "node": true, 6 | "es6": true 7 | }, 8 | // 指定如何解析语法 9 | "parser": "vue-eslint-parser", 10 | // 优先级低于 parse 的语法解析配置 11 | "parserOptions": { 12 | "parser": "@typescript-eslint/parser", 13 | "ecmaVersion": 2020, 14 | "sourceType": "module", 15 | "jsxPragma": "React", 16 | "ecmaFeatures": { 17 | "jsx": true 18 | } 19 | }, 20 | // 继承某些已有的规则 21 | "extends": ["plugin:vue/vue3-recommended", "plugin:@typescript-eslint/recommended", "prettier", "plugin:prettier/recommended"], 22 | /** 23 | * "off" 或 0 ==> 关闭规则 24 | * "warn" 或 1 ==> 打开的规则作为警告(不影响代码执行) 25 | * "error" 或 2 ==> 规则作为一个错误(代码不能执行,界面报错) 26 | */ 27 | "rules": { 28 | // eslint (http://eslint.cn/docs/rules) 29 | "no-var": "error", // 要求使用 let 或 const 而不是 var 30 | "no-multiple-empty-lines": ["error", { "max": 1 }], // 不允许多个空行 31 | "prefer-const": "off", // 使用 let 关键字声明但在初始分配后从未重新分配的变量,要求使用 const 32 | "no-use-before-define": "off", // 禁止在 函数/类/变量 定义之前使用它们 33 | 34 | // typeScript (https://typescript-eslint.io/rules) 35 | "@typescript-eslint/no-unused-vars": "error", // 禁止定义未使用的变量 36 | "@typescript-eslint/no-empty-function": "error", // 禁止空函数 37 | "@typescript-eslint/prefer-ts-expect-error": "error", // 禁止使用 @ts-ignore 38 | "@typescript-eslint/ban-ts-comment": "error", // 禁止 @ts- 使用注释或要求在指令后进行描述 39 | "@typescript-eslint/no-inferrable-types": "off", // 可以轻松推断的显式类型可能会增加不必要的冗长 40 | "@typescript-eslint/no-namespace": "off", // 禁止使用自定义 TypeScript 模块和命名空间 41 | "@typescript-eslint/no-explicit-any": "off", // 禁止使用 any 类型 42 | "@typescript-eslint/ban-types": "off", // 禁止使用特定类型 43 | "@typescript-eslint/no-var-requires": "off", // 允许使用 require() 函数导入模块 44 | "@typescript-eslint/no-non-null-assertion": "off", // 不允许使用后缀运算符的非空断言(!) 45 | 46 | // vue (https://eslint.vuejs.org/rules) 47 | "vue/script-setup-uses-vars": "error", // 防止 29 | 30 | 31 | -------------------------------------------------------------------------------- /mock/_createProductionServer.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer" 2 | 3 | const modules = import.meta.glob("./**/*.mock.ts", { 4 | import: "default", 5 | eager: true 6 | }) 7 | 8 | const mockModules: any[] = [] 9 | Object.keys(modules).forEach(async key => { 10 | if (key.includes("_")) { 11 | return 12 | } 13 | mockModules.push(...(modules[key] as any)) 14 | }) 15 | 16 | export function setupProdMockServer() { 17 | createProdMockServer(mockModules) 18 | } 19 | -------------------------------------------------------------------------------- /mock/analysis/index.mock.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock" 2 | 3 | const timeout = 500 4 | 5 | const List: any = { 6 | jingangDistrict: [ 7 | { 8 | name: "Project", 9 | num: "932", 10 | icon: "ant-design:project-filled", 11 | color: "#F07758" 12 | }, 13 | { 14 | name: "Inquiries", 15 | num: "1032", 16 | icon: "icon-park-outline:history-query", 17 | color: "#3CC2E0" 18 | }, 19 | { 20 | name: "Invesment", 21 | num: "102K", 22 | icon: "octicon:project-symlink-16", 23 | color: "#7D6AEB" 24 | }, 25 | { 26 | name: "Assets", 27 | num: "32K", 28 | icon: "fa-solid:project-diagram", 29 | color: "#FF626D" 30 | } 31 | ], 32 | projectStatistic: { 33 | xAxis: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 34 | yAxis1: [20, 39, 18, 70, 62, 58, 69], 35 | yAxis2: [40, 50, 62, 52, 21, 18, 69] 36 | }, 37 | email: [ 38 | { 39 | value: 763, 40 | name: "Primary" 41 | }, 42 | { 43 | value: 321, 44 | name: "Promotion" 45 | }, 46 | { 47 | value: 69, 48 | name: "Forum" 49 | }, 50 | { 51 | value: 154, 52 | name: "Socias" 53 | } 54 | ], 55 | statistic: { 56 | xAxis: ["Mon", "Tue", "Wed", "Thu", "Fri", "Sat", "Sun"], 57 | yAxis1: [312, 589, 438, 721, 654, 876, 543], 58 | yAxis2: [187, 496, 843, 519, 624, 357, 962] 59 | }, 60 | serverStatus: { 61 | progress: [ 62 | { 63 | id: 2, 64 | color: "#fe606b", 65 | title: "Mon", 66 | percentage: 63 67 | }, 68 | { 69 | id: 3, 70 | color: "#4cbb9a", 71 | title: "Tue", 72 | percentage: 37 73 | }, 74 | { 75 | id: 4, 76 | color: "#4cbb9a", 77 | title: "Wed", 78 | percentage: 55 79 | }, 80 | { 81 | id: 5, 82 | color: "#ecc567", 83 | title: "Thu", 84 | percentage: 99 85 | }, 86 | { 87 | id: 6, 88 | color: "#fd606b", 89 | title: "Fri", 90 | percentage: 80 91 | } 92 | ], 93 | statistics: [ 94 | { 95 | title: "Active Users", 96 | value: 15 97 | }, 98 | { 99 | title: "Active Group", 100 | value: 23 101 | }, 102 | { 103 | title: "Active Member", 104 | value: 50 105 | } 106 | ] 107 | }, 108 | marketPreviews: [ 109 | { title: "LTC/USD", color: "#f0c868", time: "March", increase: 1.24, num: 120.45 }, 110 | { title: "BTC/USD", color: "#ef6e4d", time: "January", increase: -1.27, num: 149.45 }, 111 | { title: "ETH/USD", color: "#ff606b", time: "January", increase: 1.43, num: 185.5 }, 112 | { title: "RPL/USD", color: "#ff606b", time: "January", increase: -2.32, num: 114.35 } 113 | ], 114 | project: { 115 | progress: [ 116 | { 117 | color: "#7d6aea", 118 | title: "Mon", 119 | percentage: 33, 120 | times: 17 121 | }, 122 | { 123 | color: "#ee6e4e", 124 | title: "Tue", 125 | percentage: 65, 126 | times: 32 127 | }, 128 | { 129 | color: "#3dc1df", 130 | title: "Wed", 131 | percentage: 55, 132 | times: 67 133 | }, 134 | { 135 | color: "#ecc567", 136 | title: "Thu", 137 | percentage: 99, 138 | times: 55 139 | }, 140 | { 141 | color: "#fd606b", 142 | title: "Fri", 143 | percentage: 37, 144 | times: 89 145 | } 146 | ], 147 | tags: [ 148 | { type: "", value: "#JavaScript" }, 149 | { type: "success", value: "#Vue" }, 150 | { type: "", value: "#React" }, 151 | { type: "", value: "#TypeScript" }, 152 | { type: "", value: "#Vueuse" } 153 | ] 154 | } 155 | } 156 | 157 | export default [ 158 | // worksplace数据接口 159 | { 160 | url: "/analysis/data", 161 | method: "get", 162 | timeout, 163 | response: () => { 164 | return { 165 | data: { 166 | code: 200, 167 | data: { 168 | list: List 169 | } 170 | } 171 | } 172 | } 173 | } 174 | ] as MockMethod[] 175 | -------------------------------------------------------------------------------- /mock/useTable/index.mock.ts: -------------------------------------------------------------------------------- 1 | import { MockMethod } from "vite-plugin-mock" 2 | import Mock from "mockjs" 3 | import { resultPageSuccess } from "../utils" 4 | 5 | const demoList = (() => { 6 | const result: any[] = [] 7 | for (let index = 0; index < 60; index++) { 8 | result.push({ 9 | id: index + 1, 10 | name: () => Mock.Random.cname(), 11 | "role|1": ["Develop", "Test", "PM"], 12 | "sex|1": ["Man", "Women"], 13 | "age|20-40": 1, 14 | address: () => Mock.Random.county(true) 15 | }) 16 | } 17 | return result 18 | })() 19 | 20 | export default [ 21 | { 22 | url: "/useTable/list", 23 | timeout: 2000, 24 | method: "post", 25 | response: ({ body }) => { 26 | const { page = 1, pageSize = 10 } = body 27 | 28 | return resultPageSuccess(page, pageSize, demoList) 29 | } 30 | } 31 | ] as MockMethod[] 32 | -------------------------------------------------------------------------------- /mock/utils.ts: -------------------------------------------------------------------------------- 1 | export function resultSuccess(result: T, { message = "ok" } = {}) { 2 | return { 3 | code: 200, 4 | result, 5 | message, 6 | type: "success" 7 | } 8 | } 9 | 10 | export function resultPageSuccess(page: number, pageSize: number, list: T[], { message = "ok" } = {}) { 11 | const pageData = pagination(page, pageSize, list) 12 | 13 | return { 14 | ...resultSuccess({ 15 | items: pageData, 16 | total: list.length 17 | }), 18 | message 19 | } 20 | } 21 | 22 | export function resultError(message = "Request failed", { code = 404, result = null } = {}) { 23 | return { 24 | code, 25 | result, 26 | message, 27 | type: "error" 28 | } 29 | } 30 | 31 | export function pagination(pageNo: number, pageSize: number, array: T[]): T[] { 32 | const offset = (pageNo - 1) * Number(pageSize) 33 | return offset + Number(pageSize) >= array.length 34 | ? array.slice(offset, array.length) 35 | : array.slice(offset, offset + Number(pageSize)) 36 | } 37 | 38 | export interface requestParams { 39 | method: string 40 | body: any 41 | headers?: { authorization?: string } 42 | query: any 43 | } 44 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vxadmin", 3 | "private": true, 4 | "version": "0.0.0", 5 | "type": "module", 6 | "scripts": { 7 | "dev": "set \"VITE_CJS_TRACE=true\" && vite dev", 8 | "build": "set \"VITE_CJS_TRACE=true\" && vue-tsc --noEmit --skipLibCheck && vite build", 9 | "preview": "set \"VITE_CJS_TRACE=true\" && vite preview" 10 | 11 | }, 12 | "dependencies": { 13 | "@antv/x6": "^2.18.1", 14 | "@intlify/unplugin-vue-i18n": "^2.0.0", 15 | "@jridgewell/sourcemap-codec": "^1.4.15", 16 | "@types/lodash": "^4.14.202", 17 | "@types/mockjs": "^1.0.10", 18 | "@vitejs/plugin-legacy": "^5.4.1", 19 | "@vueuse/core": "^10.6.1", 20 | "animate.css": "^4.1.1", 21 | "axios": "^1.6.2", 22 | "consola": "^3.2.3", 23 | "echarts": "^5.5.0", 24 | "element-plus": "^2.4.2", 25 | "lodash": "^4.17.21", 26 | "lodash-es": "^4.17.21", 27 | "mockjs": "^1.1.0", 28 | "nprogress": "^0.2.0", 29 | "pinia": "^2.1.7", 30 | "pinia-plugin-persist": "^1.0.0", 31 | "qrcode": "^1.5.3", 32 | "rollup-plugin-visualizer": "^5.12.0", 33 | "unplugin-auto-import": "^0.17.5", 34 | "vite-plugin-cdn-import": "^0.3.5", 35 | "vite-plugin-compression": "^0.5.1", 36 | "vite-plugin-style-import": "^2.0.0", 37 | "vue": "^3.4.14", 38 | "vue-demi": "^0.14.7", 39 | "vue-i18n": "^9.8.0", 40 | "vue-router": "^4.2.5", 41 | "vxe-table": "^4.5.18" 42 | }, 43 | "devDependencies": { 44 | "@iconify-icons/ep": "^1.2.12", 45 | "@iconify-icons/ri": "^1.2.10", 46 | "@iconify/vue": "^4.1.1", 47 | "@types/node": "^20.9.4", 48 | "@types/postcss-import": "^14.0.3", 49 | "@typescript-eslint/eslint-plugin": "^6.12.0", 50 | "@typescript-eslint/parser": "^6.12.0", 51 | "@vitejs/plugin-vue": "^4.5.0", 52 | "@vitejs/plugin-vue-jsx": "^3.1.0", 53 | "cssnano": "^7.0.1", 54 | "eslint": "^8.54.0", 55 | "eslint-config-prettier": "^9.0.0", 56 | "eslint-plugin-prettier": "^5.0.1", 57 | "eslint-plugin-vue": "^9.18.1", 58 | "postcss": "^8.4.38", 59 | "postcss-import": "^16.1.0", 60 | "postcss-load-config": "^6.0.1", 61 | "prettier": "3.0.3", 62 | "sass": "^1.69.5", 63 | "stylelint": "^15.11.0", 64 | "stylelint-config-html": "^1.1.0", 65 | "stylelint-config-recess-order": "^4.4.0", 66 | "stylelint-config-recommended": "^13.0.0", 67 | "stylelint-config-recommended-scss": "^13.1.0", 68 | "stylelint-config-recommended-vue": "^1.5.0", 69 | "stylelint-config-standard": "^34.0.0", 70 | "stylelint-config-standard-scss": "^11.1.0", 71 | "stylelint-order": "^6.0.3", 72 | "stylelint-prettier": "^4.0.2", 73 | "stylelint-scss": "^5.3.1", 74 | "typescript": "^5.4.3", 75 | "unplugin-vue-components": "^0.25.2", 76 | "vite": "^5.1.6", 77 | "vite-plugin-mock": "^2.9.8", 78 | "vue-tsc": "^1.8.22" 79 | }, 80 | "style": "stylelint \"src/**/*.(vue|scss|css)\" --fix" 81 | } 82 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 71 | 72 | 77 | -------------------------------------------------------------------------------- /src/api/analysis/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/axios" 2 | 3 | // analysis信息 4 | const getAnalysisData = async (): Promise => { 5 | return http.get("/analysis/data") 6 | } 7 | 8 | export { getAnalysisData } 9 | -------------------------------------------------------------------------------- /src/api/table/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/axios" 2 | import type { getUseTableListParams, getUseTableListReturns } from "./types" 3 | 4 | // 获取useTable list 5 | const getUseTableList = async (data: getUseTableListParams): Promise => { 6 | return http.post("/useTable/list", { data }) 7 | } 8 | 9 | export { getUseTableList } 10 | -------------------------------------------------------------------------------- /src/api/table/types.ts: -------------------------------------------------------------------------------- 1 | type getUseTableListParams = { 2 | page: number 3 | pageSize: number 4 | } 5 | 6 | type getUseTableListReturns = { 7 | result: { 8 | items: any[] 9 | total: number 10 | } 11 | [key: string]: any 12 | } 13 | 14 | export type { getUseTableListParams, getUseTableListReturns } 15 | -------------------------------------------------------------------------------- /src/api/user/index.ts: -------------------------------------------------------------------------------- 1 | import { http } from "@/axios" 2 | import type { 3 | UserLoginParams, 4 | UserReturns, 5 | UserListParams, 6 | getRoleListParams, 7 | RoleReturns, 8 | deleteRoleListParams, 9 | updateRoleParams 10 | } from "./types" 11 | 12 | // 登录 13 | const login = async (data: UserLoginParams): Promise => { 14 | return http.post("/user/login", { data }) 15 | } 16 | // 获取用户列表 17 | const userList = async (params: UserListParams): Promise => { 18 | return http.get("/user/list", { params }) 19 | } 20 | // 获取权限列表 21 | const roleList = async (data: getRoleListParams): Promise => { 22 | return http.post("/user/role", { data }) 23 | } 24 | // 获取权限列表 25 | const getRouterList = async (data: getRoleListParams): Promise => { 26 | return http.post("/user/routerList", { data }) 27 | } 28 | //根据roleId删除权限 29 | const deleteRoleList = async (data: deleteRoleListParams): Promise => { 30 | return http.post("/user/deleteRoleById", { data }) 31 | } 32 | //更新权限列表 33 | const updateRole = async (data: updateRoleParams): Promise => { 34 | return http.post("/user/updateRole", { data }) 35 | } 36 | // 退出登录 37 | const outLogin = async (): Promise => { 38 | return http.get("/user/loginOut") 39 | } 40 | export { login, userList, roleList, outLogin, getRouterList, deleteRoleList, updateRole } 41 | -------------------------------------------------------------------------------- /src/api/user/types.ts: -------------------------------------------------------------------------------- 1 | type UserLoginParams = { 2 | username?: string 3 | password?: string | number 4 | } 5 | 6 | type RouterMenusReturns = { 7 | result?: { 8 | path?: string 9 | redirect?: string 10 | name?: string 11 | component?: string 12 | meta?: { 13 | title?: string 14 | icon?: string 15 | role?: string[] 16 | } 17 | children?: RouterMenusReturns[] 18 | } 19 | } 20 | type UserReturns = { 21 | code: number 22 | data: { 23 | userId?: string | number 24 | username?: string 25 | password?: string 26 | roleId?: string | number 27 | // 角色 28 | roles?: string[] 29 | // 权限 30 | auths?: string[] 31 | routers?: RouterMenusReturns[] 32 | sex?: number 33 | age?: number 34 | nickname?: string 35 | [key: string]: any 36 | } 37 | } 38 | 39 | type RoleReturns = { 40 | code: number 41 | data: { 42 | roleId: number 43 | role: string 44 | remark: string 45 | createTime: string 46 | roleName: string 47 | [key: string]: any 48 | } 49 | } 50 | 51 | type getRoleListParams = { 52 | page?: number 53 | pageSize?: number 54 | role?: string 55 | roleName?: string 56 | creatTime?: string | number 57 | remark?: string 58 | } 59 | 60 | type UserListParams = { 61 | username: string 62 | page: number 63 | pageSize: number 64 | } 65 | 66 | type deleteRoleListParams = { 67 | roleId: string 68 | } 69 | 70 | type updateRoleParams = { 71 | roleId: string 72 | role?: string 73 | roleName?: string 74 | creatTime?: string | number 75 | remark?: string 76 | } 77 | 78 | export type { 79 | UserLoginParams, 80 | UserReturns, 81 | UserListParams, 82 | getRoleListParams, 83 | RoleReturns, 84 | deleteRoleListParams, 85 | updateRoleParams 86 | } 87 | -------------------------------------------------------------------------------- /src/assets/imgs/Avatar.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/Avatar.png -------------------------------------------------------------------------------- /src/assets/imgs/VxLogo.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/VxLogo.png -------------------------------------------------------------------------------- /src/assets/imgs/avatar1.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/avatar1.png -------------------------------------------------------------------------------- /src/assets/imgs/avatar2.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/avatar2.png -------------------------------------------------------------------------------- /src/assets/imgs/avatar3.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/avatar3.png -------------------------------------------------------------------------------- /src/assets/imgs/avatar4.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/avatar4.png -------------------------------------------------------------------------------- /src/assets/imgs/avatar5.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/assets/imgs/avatar5.png -------------------------------------------------------------------------------- /src/assets/svgs/loading.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/axios/helper/index.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@/hooks/useStorage" 2 | 3 | const { getStorage } = useStorage() 4 | 5 | export function getToken(): string | undefined { 6 | return getStorage("token") 7 | } 8 | -------------------------------------------------------------------------------- /src/axios/index.ts: -------------------------------------------------------------------------------- 1 | import Axios, { type AxiosInstance, type AxiosRequestConfig } from "axios" 2 | import type { HttpError, RequestMethods, HttpRequestConfig, HttpTypes } from "./types" 3 | import { baseURL } from "@/constants" 4 | 5 | // 相关配置请参考:www.axios-js.com/zh-cn/docs/#axios-request-config-1 6 | const defaultConfig: AxiosRequestConfig = baseURL 7 | ? { 8 | baseURL, 9 | timeout: 10000, 10 | headers: { 11 | baseURL, 12 | Accept: "application/json, text/plain, */*", 13 | "Content-Type": "application/json", 14 | "X-Requested-With": "XMLHttpRequest" 15 | } 16 | } 17 | : { 18 | timeout: 10000, 19 | headers: { 20 | baseURL, 21 | Accept: "application/json, text/plain, */*", 22 | "Content-Type": "application/json", 23 | "X-Requested-With": "XMLHttpRequest" 24 | } 25 | } 26 | 27 | class Http implements HttpTypes { 28 | constructor() { 29 | this.httpInterceptorsRequest() 30 | this.httpInterceptorsResponse() 31 | } 32 | 33 | /** token过期后,暂存待执行的请求 */ 34 | private static requests = [] 35 | 36 | /** 防止重复刷新token */ 37 | private static isRefreshing = false 38 | 39 | /** 初始化配置对象 */ 40 | private static initConfig: HttpRequestConfig = {} 41 | 42 | /** 保存当前Axios实例对象 */ 43 | private static axiosInstance: AxiosInstance = Axios.create(defaultConfig) 44 | 45 | /** 重连原始请求 */ 46 | private static retryOriginalRequest(config: HttpRequestConfig) { 47 | return new Promise(resolve => { 48 | Http.requests.push((token: string) => { 49 | config.headers["Authorization"] = token 50 | resolve(config) 51 | }) 52 | }) 53 | } 54 | 55 | /** 请求拦截 */ 56 | private httpInterceptorsRequest(): void { 57 | Http.axiosInstance.interceptors.request.use( 58 | async (config: HttpRequestConfig): Promise => { 59 | // 开启进度条动画 60 | // NProgress.start() 61 | // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调 62 | if (typeof config.beforeRequestCallback === "function") { 63 | config.beforeRequestCallback(config) 64 | return config 65 | } 66 | if (Http.initConfig.beforeRequestCallback) { 67 | Http.initConfig.beforeRequestCallback(config) 68 | return config 69 | } 70 | /** 请求白名单,放置一些不需要token的接口(通过设置请求白名单,防止token过期后再请求造成的死循环问题) */ 71 | const whiteList = ["/refresh-token", "/login"] 72 | return whiteList.find(url => url === config.url) 73 | ? config 74 | : new Promise(resolve => { 75 | resolve(config) 76 | }) 77 | }, 78 | error => { 79 | return Promise.reject(error) 80 | } 81 | ) 82 | } 83 | 84 | /** 响应拦截 */ 85 | private httpInterceptorsResponse(): void { 86 | const instance = Http.axiosInstance 87 | instance.interceptors.response.use( 88 | (response: any) => { 89 | const $config = response.config 90 | // 关闭进度条动画 91 | // NProgress.done() 92 | // 优先判断post/get等方法是否传入回调,否则执行初始化设置等回调 93 | if (typeof $config.beforeResponseCallback === "function") { 94 | $config.beforeResponseCallback(response) 95 | return response.data 96 | } 97 | if (Http.initConfig.beforeResponseCallback) { 98 | Http.initConfig.beforeResponseCallback(response) 99 | return response.data 100 | } 101 | return response.data 102 | }, 103 | (error: HttpError) => { 104 | const $error = error 105 | $error.isCancelRequest = Axios.isCancel($error) 106 | // 关闭进度条动画 107 | // NProgress.done() 108 | // 所有的响应异常 区分来源为取消请求/非取消请求 109 | return Promise.reject($error) 110 | } 111 | ) 112 | } 113 | 114 | /** 通用请求工具函数 */ 115 | public request( 116 | method: RequestMethods, 117 | url: string, 118 | param?: AxiosRequestConfig, 119 | axiosConfig?: HttpRequestConfig 120 | ): Promise { 121 | const config = { 122 | method, 123 | url, 124 | ...param, 125 | ...axiosConfig 126 | } as HttpRequestConfig 127 | 128 | // 单独处理自定义请求/响应回调 129 | return new Promise((resolve, reject) => { 130 | Http.axiosInstance 131 | .request(config) 132 | .then((response: any) => { 133 | resolve(response) 134 | }) 135 | .catch(error => { 136 | reject(error) 137 | }) 138 | }) 139 | } 140 | 141 | /** 单独抽离的post工具函数 */ 142 | public post(url: string, params?: AxiosRequestConfig, config?: HttpRequestConfig): Promise

{ 143 | return this.request

("post", url, params, config) 144 | } 145 | 146 | /** 单独抽离的get工具函数 */ 147 | public get(url: string, params?: AxiosRequestConfig, config?: HttpRequestConfig): Promise

{ 148 | return this.request

("get", url, params, config) 149 | } 150 | } 151 | 152 | export const http = new Http() 153 | -------------------------------------------------------------------------------- /src/axios/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { Method, AxiosError, AxiosResponse, AxiosRequestConfig, AxiosRequestHeaders } from "axios" 2 | 3 | export type resultType = { 4 | accessToken?: string 5 | } 6 | 7 | export type RequestMethods = Extract 8 | 9 | export interface HttpError extends AxiosError { 10 | isCancelRequest?: boolean 11 | } 12 | 13 | export interface HttpResponse extends AxiosResponse { 14 | config: { 15 | headers: AxiosRequestHeaders 16 | } 17 | } 18 | 19 | export interface HttpRequestConfig extends AxiosRequestConfig { 20 | beforeRequestCallback?: (request: HttpRequestConfig) => void 21 | beforeResponseCallback?: (response: HttpResponse) => void 22 | } 23 | 24 | export interface HttpTypes { 25 | request(method: RequestMethods, url: string, param?: AxiosRequestConfig, axiosConfig?: HttpRequestConfig): Promise 26 | post(url: string, params?: T, config?: HttpRequestConfig): Promise

27 | get(url: string, params?: T, config?: HttpRequestConfig): Promise

28 | } 29 | -------------------------------------------------------------------------------- /src/components/Avatar/index.ts: -------------------------------------------------------------------------------- 1 | import Avatar from "./src/Avatar.vue" 2 | 3 | export { Avatar } 4 | -------------------------------------------------------------------------------- /src/components/Avatar/src/Avatar.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-avatar { 3 | height: inherit; 4 | .vx-dropdown { 5 | height: var(--header-global-height); 6 | padding: 0 15px 0 5px; 7 | &__username { 8 | width: unset; 9 | margin-right: 10px; 10 | overflow: hidden; 11 | font-size: 13px; 12 | color: var(--theme-text-color); 13 | text-overflow: ellipsis; 14 | white-space: nowrap; 15 | } 16 | &__username--fold { 17 | width: 20px; 18 | margin-right: 10px; 19 | overflow: hidden; 20 | font-size: 13px; 21 | color: var(--theme-text-color); 22 | text-overflow: ellipsis; 23 | white-space: nowrap; 24 | } 25 | 26 | @include flex(center, center, nowrap); 27 | } 28 | } 29 | .vx-dropdown-item__text { 30 | margin-left: 5px; 31 | } 32 | -------------------------------------------------------------------------------- /src/components/Avatar/src/Avatar.vue: -------------------------------------------------------------------------------- 1 | 66 | 96 | 99 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/index.ts: -------------------------------------------------------------------------------- 1 | import Breadcrumb from "./src/Breadcrumb.vue" 2 | 3 | export { Breadcrumb } 4 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/src/Breadcrumb.scss: -------------------------------------------------------------------------------- 1 | .vx-breadcrumb { 2 | margin-left: 10px; 3 | } 4 | .animate__animated.animate__fadeInRight { 5 | animation-duration: .3s; 6 | } -------------------------------------------------------------------------------- /src/components/Breadcrumb/src/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 75 | 90 | 93 | -------------------------------------------------------------------------------- /src/components/Breadcrumb/src/type.ts: -------------------------------------------------------------------------------- 1 | import { RouteRecordRaw } from "vue-router" 2 | 3 | type BreadcrumbList = { 4 | currentTarget: string 5 | route: RouteRecordRaw[] 6 | } 7 | 8 | export type { BreadcrumbList } 9 | -------------------------------------------------------------------------------- /src/components/Form/index.ts: -------------------------------------------------------------------------------- 1 | import Form from "./src/Form.vue" 2 | import type { FormSchema, FormSetProps } from "./src/types" 3 | export type { 4 | ComponentNameEnum, 5 | ComponentName, 6 | InputComponentProps, 7 | AutocompleteComponentProps, 8 | InputNumberComponentProps, 9 | SelectOption, 10 | SelectComponentProps, 11 | SelectV2ComponentProps, 12 | CascaderComponentProps, 13 | SwitchComponentProps, 14 | RateComponentProps, 15 | ColorPickerComponentProps, 16 | TransferComponentProps, 17 | RadioOption, 18 | RadioGroupComponentProps, 19 | RadioButtonComponentProps, 20 | CheckboxOption, 21 | CheckboxGroupComponentProps, 22 | DividerComponentProps, 23 | DatePickerComponentProps, 24 | DateTimePickerComponentProps, 25 | TimePickerComponentProps, 26 | TimeSelectComponentProps, 27 | ColProps, 28 | FormSetProps, 29 | FormItemProps, 30 | FormSchema, 31 | FormProps, 32 | PlaceholderModel, 33 | InputPasswordComponentProps, 34 | TreeSelectComponentProps 35 | } from "./src/types" 36 | 37 | export interface FormExpose { 38 | setValues: (data: Recordable) => void 39 | setProps: (props: Recordable) => void 40 | delSchema: (field: string) => void 41 | addSchema: (formSchema: FormSchema, index?: number) => void 42 | setSchema: (schemaProps: FormSetProps[]) => void 43 | formModel: Recordable 44 | getComponentExpose: (field: string) => any 45 | getFormItemExpose: (field: string) => any 46 | } 47 | 48 | export { Form } 49 | -------------------------------------------------------------------------------- /src/components/Form/src/components/useRenderCheckbox.tsx: -------------------------------------------------------------------------------- 1 | import { FormSchema, ComponentNameEnum, CheckboxGroupComponentProps } from "../types" 2 | import { ElCheckbox, ElCheckboxButton } from "element-plus" 3 | import { defineComponent } from "vue" 4 | 5 | export const useRenderCheckbox = () => { 6 | const renderCheckboxOptions = (item: FormSchema) => { 7 | // 如果有别名,就取别名 8 | const componentProps = item?.componentProps as CheckboxGroupComponentProps 9 | const valueAlias = componentProps?.props?.value || "value" 10 | const labelAlias = componentProps?.props?.label || "label" 11 | const disabledAlias = componentProps?.props?.disabled || "disabled" 12 | const Com = (item.component === ComponentNameEnum.CHECKBOX_GROUP ? ElCheckbox : ElCheckboxButton) as ReturnType< 13 | typeof defineComponent 14 | > 15 | return componentProps?.options?.map(option => { 16 | const { ...other } = option 17 | return ( 18 | 19 | {option[labelAlias || "label"]} 20 | 21 | ) 22 | }) 23 | } 24 | 25 | return { 26 | renderCheckboxOptions 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/components/Form/src/components/useRenderRadio.tsx: -------------------------------------------------------------------------------- 1 | import { FormSchema, ComponentNameEnum, RadioGroupComponentProps } from "../types" 2 | import { ElRadio, ElRadioButton } from "element-plus" 3 | import { defineComponent } from "vue" 4 | 5 | export const useRenderRadio = () => { 6 | const renderRadioOptions = (item: FormSchema) => { 7 | // 如果有别名,就取别名 8 | const componentProps = item?.componentProps as RadioGroupComponentProps 9 | const valueAlias = componentProps?.props?.value || "value" 10 | const labelAlias = componentProps?.props?.label || "label" 11 | const disabledAlias = componentProps?.props?.disabled || "disabled" 12 | const Com = (item.component === ComponentNameEnum.RADIO_GROUP ? ElRadio : ElRadioButton) as ReturnType 13 | return componentProps?.options?.map(option => { 14 | const { ...other } = option 15 | return ( 16 | 17 | {option[labelAlias || "label"]} 18 | 19 | ) 20 | }) 21 | } 22 | 23 | return { 24 | renderRadioOptions 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/components/Form/src/components/useRenderSelect.tsx: -------------------------------------------------------------------------------- 1 | import { ElOption, ElOptionGroup } from "element-plus" 2 | import { FormSchema, SelectComponentProps, SelectOption } from "../types" 3 | 4 | export const useRenderSelect = () => { 5 | // 渲染 select options 6 | const renderSelectOptions = (item: FormSchema) => { 7 | const componentsProps = item?.componentProps as SelectComponentProps 8 | const optionGroupDefaultSlot = componentsProps?.slots?.optionGroupDefault 9 | // 如果有别名,就取别名 10 | const labelAlias = componentsProps?.props?.label 11 | const keyAlias = componentsProps?.props?.key 12 | return componentsProps?.options?.map(option => { 13 | if (option?.options?.length) { 14 | return optionGroupDefaultSlot ? ( 15 | optionGroupDefaultSlot(option) 16 | ) : ( 17 | 18 | {{ 19 | default: () => 20 | option?.options?.map(v => { 21 | return renderSelectOptionItem(item, v) 22 | }) 23 | }} 24 | 25 | ) 26 | } else { 27 | return renderSelectOptionItem(item, option) 28 | } 29 | }) 30 | } 31 | 32 | // 渲染 select option item 33 | const renderSelectOptionItem = (item: FormSchema, option: SelectOption) => { 34 | // 如果有别名,就取别名 35 | const componentsProps = item.componentProps as SelectComponentProps 36 | const labelAlias = componentsProps?.props?.label 37 | const valueAlias = componentsProps?.props?.value 38 | const keyAlias = componentsProps?.props?.key 39 | const optionDefaultSlot = componentsProps.slots?.optionDefault 40 | 41 | return ( 42 | 48 | {{ 49 | default: () => (optionDefaultSlot ? optionDefaultSlot(option) : void 0) 50 | }} 51 | 52 | ) 53 | } 54 | 55 | return { 56 | renderSelectOptions 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/components/Form/src/helper/componentMap.ts: -------------------------------------------------------------------------------- 1 | import { 2 | ElCascader, 3 | ElCheckboxGroup, 4 | ElColorPicker, 5 | ElDatePicker, 6 | ElInput, 7 | ElInputNumber, 8 | ElRadioGroup, 9 | ElRate, 10 | ElSelect, 11 | ElSelectV2, 12 | ElSlider, 13 | ElSwitch, 14 | ElTimePicker, 15 | ElTimeSelect, 16 | ElTransfer, 17 | ElAutocomplete, 18 | ElDivider, 19 | ElTreeSelect, 20 | ElUpload 21 | } from "element-plus" 22 | import { Component } from "vue" 23 | import type { ComponentName } from "../types" 24 | 25 | const componentMap: Recordable = { 26 | RadioGroup: ElRadioGroup, 27 | RadioButton: ElRadioGroup, 28 | CheckboxGroup: ElCheckboxGroup, 29 | CheckboxButton: ElCheckboxGroup, 30 | Input: ElInput, 31 | Autocomplete: ElAutocomplete, 32 | InputNumber: ElInputNumber, 33 | Select: ElSelect, 34 | Cascader: ElCascader, 35 | Switch: ElSwitch, 36 | Slider: ElSlider, 37 | TimePicker: ElTimePicker, 38 | DatePicker: ElDatePicker, 39 | Rate: ElRate, 40 | ColorPicker: ElColorPicker, 41 | Transfer: ElTransfer, 42 | Divider: ElDivider, 43 | TimeSelect: ElTimeSelect, 44 | SelectV2: ElSelectV2, 45 | TreeSelect: ElTreeSelect, 46 | Upload: ElUpload 47 | } 48 | 49 | export { componentMap } 50 | -------------------------------------------------------------------------------- /src/components/Form/src/helper/index.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@/hooks/useI18n" 2 | import { PlaceholderModel, FormSchema, ComponentNameEnum, ColProps } from "../types" 3 | import { isFunction } from "@/utils/is" 4 | import { firstUpperCase, humpToDash } from "@/utils" 5 | import { set, get } from "lodash-es" 6 | 7 | const { t } = useI18n() 8 | 9 | /** 10 | * 11 | * @param schema 对应组件数据 12 | * @returns 返回提示信息对象 13 | * @description 用于自动设置placeholder 14 | */ 15 | export const setTextPlaceholder = (schema: FormSchema): PlaceholderModel => { 16 | const textMap = [ComponentNameEnum.INPUT, ComponentNameEnum.AUTOCOMPLETE, ComponentNameEnum.INPUT_NUMBER] 17 | const selectMap = [ 18 | ComponentNameEnum.SELECT, 19 | ComponentNameEnum.TIME_PICKER, 20 | ComponentNameEnum.DATE_PICKER, 21 | ComponentNameEnum.TIME_SELECT, 22 | ComponentNameEnum.SELECT_V2 23 | ] 24 | if (textMap.includes(schema?.component as ComponentNameEnum)) { 25 | return { 26 | placeholder: "请输入" 27 | } 28 | } 29 | if (selectMap.includes(schema?.component as ComponentNameEnum)) { 30 | // 一些范围选择器 31 | const twoTextMap = ["datetimerange", "daterange", "monthrange", "datetimerange", "daterange"] 32 | if (twoTextMap.includes(((schema?.componentProps as any)?.type || (schema?.componentProps as any)?.isRange) as string)) { 33 | return { 34 | startPlaceholder: t("common.startTimeText"), 35 | endPlaceholder: t("common.endTimeText"), 36 | rangeSeparator: "-" 37 | } 38 | } else { 39 | return { 40 | placeholder: t("common.selectText") 41 | } 42 | } 43 | } 44 | return {} 45 | } 46 | 47 | /** 48 | * 49 | * @param col 内置栅格 50 | * @returns 返回栅格属性 51 | * @description 合并传入进来的栅格属性 52 | */ 53 | export const setGridProp = (col: ColProps = {}): ColProps => { 54 | const colProps: ColProps = { 55 | // 如果有span,代表用户优先级更高,所以不需要默认栅格 56 | ...(col.span 57 | ? {} 58 | : { 59 | xs: 24, 60 | sm: 12, 61 | md: 12, 62 | lg: 12, 63 | xl: 12 64 | }), 65 | ...col 66 | } 67 | return colProps 68 | } 69 | 70 | /** 71 | * 72 | * @param item 传入的组件属性 73 | * @returns 默认添加 clearable 属性 74 | */ 75 | export const setComponentProps = (item: FormSchema): Recordable => { 76 | // const notNeedClearable = ['ColorPicker'] 77 | // 拆分事件并组合 78 | const onEvents = (item?.componentProps as any)?.on || {} 79 | const newOnEvents: Recordable = {} 80 | 81 | for (const key in onEvents) { 82 | if (onEvents[key]) { 83 | newOnEvents[`on${firstUpperCase(key)}`] = (...args: any[]) => { 84 | onEvents[key](...args) 85 | } 86 | } 87 | } 88 | 89 | const componentProps: Recordable = { 90 | clearable: true, 91 | ...item.componentProps, 92 | ...newOnEvents 93 | } 94 | // 需要删除额外的属性 95 | if (componentProps.slots) { 96 | delete componentProps.slots 97 | } 98 | if (componentProps.on) { 99 | delete componentProps.on 100 | } 101 | return componentProps 102 | } 103 | 104 | /** 105 | * 106 | * @param formModel 表单数据 107 | * @param slotsProps 插槽属性 108 | */ 109 | export const setItemComponentSlots = (slotsProps: Recordable = {}): Recordable => { 110 | const slotObj: Recordable = {} 111 | for (const key in slotsProps) { 112 | if (slotsProps[key]) { 113 | if (isFunction(slotsProps[key])) { 114 | slotObj[humpToDash(key)] = (...args: any[]) => { 115 | return slotsProps[key]?.(...args) 116 | } 117 | } else { 118 | slotObj[humpToDash(key)] = () => { 119 | return slotsProps[key] 120 | } 121 | } 122 | } 123 | } 124 | return slotObj 125 | } 126 | 127 | /** 128 | * 129 | * @param schema Form表单结构化数组 130 | * @param formModel FormMoel 131 | * @returns FormMoel 132 | * @description 生成对应的formModel 133 | */ 134 | export const initModel = (schema: FormSchema[], formModel: Recordable) => { 135 | const model: Recordable = { ...formModel } 136 | schema.map(v => { 137 | if (v.remove) { 138 | delete model[v.field] 139 | } else if (v.component !== "Divider") { 140 | const hasField = get(model, v.field) 141 | // 如果先前已经有值存在,则不进行重新赋值,而是采用现有的值 142 | set(model, v.field, hasField !== void 0 ? get(model, v.field) : v.value !== void 0 ? v.value : void 0) 143 | } 144 | }) 145 | return model 146 | } 147 | -------------------------------------------------------------------------------- /src/components/Fullscreen/index.ts: -------------------------------------------------------------------------------- 1 | import Fullscreen from "./src/Fullscreen.vue" 2 | 3 | export { Fullscreen } -------------------------------------------------------------------------------- /src/components/Fullscreen/src/Fullscreen.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-fullscreen { 3 | height: var(--header-global-height); 4 | padding: 0 5px; 5 | &__icon { 6 | @include cursor; 7 | } 8 | &__icon:hover { 9 | color: var(--logo-bg-active-color); 10 | } 11 | 12 | @include flex(center, center, nowrap); 13 | } 14 | -------------------------------------------------------------------------------- /src/components/Fullscreen/src/Fullscreen.vue: -------------------------------------------------------------------------------- 1 | 11 | 22 | 23 | 26 | -------------------------------------------------------------------------------- /src/components/LocaleSwitch/index.ts: -------------------------------------------------------------------------------- 1 | import LocaleSwitch from "./src/LocaleSwitch.vue" 2 | 3 | export { LocaleSwitch } 4 | -------------------------------------------------------------------------------- /src/components/LocaleSwitch/src/LocaleSwitch.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-locale-switch { 3 | height: var(--header-global-height); 4 | padding: 0 5px; 5 | background-color: var(--theme-div-color); 6 | :deep(.el-dropdown-link > svg) { 7 | outline: none !important; 8 | } 9 | 10 | @include transition(background-color); 11 | @include flex(center, center, nowrap); 12 | } 13 | .dropdown-menu { 14 | padding: 8px 0; 15 | .dropdown-item { 16 | width: 100%; 17 | padding: 4px 16px 4px 6px; 18 | & span { 19 | margin-left: 5px; 20 | } 21 | 22 | @include flex(start, center, nowrap); 23 | @include cursor; 24 | } 25 | .dropdown-item--active { 26 | /* stylelint-disable */ 27 | background-color: var(--el-dropdown-menuItem-hover-fill); 28 | color: var(--theme-color); 29 | /* stylelint-enable */ 30 | } 31 | .vx-icon--visible { 32 | opacity: 0; 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/components/LocaleSwitch/src/LocaleSwitch.vue: -------------------------------------------------------------------------------- 1 | 33 | 53 | 56 | -------------------------------------------------------------------------------- /src/components/Logo/index.ts: -------------------------------------------------------------------------------- 1 | import Logo from "./src/Logo.vue" 2 | 3 | export { Logo } 4 | -------------------------------------------------------------------------------- /src/components/Logo/src/Logo.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-logo { 3 | height: var(--header-global-height); 4 | background-color: var(--logo-bg-color); 5 | transition-timing-function: ease-in; 6 | &:hover { 7 | background-color: var(--logo-bg-active-color); 8 | } 9 | &__image { 10 | width: 50px; 11 | height: 50px; 12 | 13 | @include flex(center, center, nowrap); 14 | & img { 15 | width: 32px; 16 | height: 32px; 17 | } 18 | } 19 | &__title { 20 | padding-left: 4px; 21 | 22 | @include transition(all); 23 | & span { 24 | font-size: var(--text-middle-size); 25 | font-weight: bold; 26 | color: var(--logo-text-color); 27 | 28 | @include transition(color); 29 | &:hover { 30 | color: var(--logo-active-color); 31 | } 32 | } 33 | } 34 | 35 | @include transition(all); 36 | @include cursor(default); 37 | @include flex(start, center, nowrap); 38 | } 39 | -------------------------------------------------------------------------------- /src/components/Logo/src/Logo.vue: -------------------------------------------------------------------------------- 1 | 25 | 35 | 41 | -------------------------------------------------------------------------------- /src/components/Menu/index.ts: -------------------------------------------------------------------------------- 1 | import Menu from "./src/Menu.vue" 2 | 3 | export { Menu } 4 | -------------------------------------------------------------------------------- /src/components/Menu/src/Menu.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-menu { 3 | border-right: 1px solid var(--left-menu-bg-color); 4 | :deep(.el-menu) { 5 | width: 100% !important; 6 | .is-active { 7 | & > .el-sub-menu__title { 8 | color: var(--left-menu-text-active-color) !important; 9 | } 10 | } 11 | 12 | // 设置子菜单悬停的高亮和背景色 13 | .el-sub-menu__title, 14 | .el-menu-item { 15 | &:hover { 16 | color: var(--left-menu-text-active-color) !important; 17 | background-color: var(--left-menu-bg-color) !important; 18 | } 19 | } 20 | 21 | // 设置选中时的高亮背景和高亮颜色 22 | .el-menu-item.is-active { 23 | color: var(--left-menu-text-active-color) !important; 24 | background: var(--left-menu-bg-active-color) !important; 25 | &:hover { 26 | background: var(--left-menu-bg-active-color) !important; 27 | } 28 | } 29 | 30 | // 设置子菜单的背景颜色 31 | .el-menu { 32 | .el-sub-menu__title, 33 | .el-menu-item:not(.is-active) { 34 | background-color: var(--left-menu-bg-light-color) !important; 35 | } 36 | } 37 | } 38 | 39 | // 设置没有子目录的菜单选中样式 40 | :deep(.el-menu-item.is-active) { 41 | color: var(--left-menu-text-active-color) !important; 42 | background: var(--left-menu-bg-active-color) !important; 43 | } 44 | 45 | // 设置仅有二级菜单的未选中的子样式 46 | :deep(.el-sub-menu .el-menu-item) { 47 | background-color: var(--left-menu-bg-light-color) !important; 48 | } 49 | :deep(.el-sub-menu .el-menu-item:hover) { 50 | background-color: var(--left-menu-bg-light-color) !important; 51 | } 52 | :deep(.el-menu .el-sub-menu__title:hover) { 53 | background-color: var(--el-menu-hover-bg-color) !important; 54 | } 55 | :deep(.el-menu .is-active .el-sub-menu__title) { 56 | color: var(--left-menu-text-active-color) !important; 57 | } 58 | :deep(.el-menu-item-group__title) { 59 | height: 50px; 60 | 61 | @include flex(flex-start, center, nowrap); 62 | @include cursor(default); 63 | } 64 | 65 | // 重置padding 66 | :deep(.el-sub-menu__title) { 67 | padding: 0 16px; 68 | } 69 | &--horizontal { 70 | width: calc(100vw - 465px) !important; 71 | height: var(--header-global-height); 72 | } 73 | } 74 | .vx-scrollbar { 75 | height: calc(100vh - var(--header-global-height)) !important; 76 | } 77 | -------------------------------------------------------------------------------- /src/components/Qrcode/index.ts: -------------------------------------------------------------------------------- 1 | import Qrcode from "./src/Qrcode.vue" 2 | 3 | export { Qrcode } 4 | -------------------------------------------------------------------------------- /src/components/Qrcode/src/Qrcode.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-qrcode { 3 | position: relative; 4 | &__cover { 5 | position: absolute; 6 | top: 0; 7 | left: 0; 8 | width: 100%; 9 | height: 100%; 10 | background-color: rgba(255, 255, 255, 0.9); 11 | 12 | @include flex(center, center, nowrap); 13 | .cover-inset { 14 | flex-direction: column; 15 | font-weight: bold; 16 | cursor: default; 17 | 18 | @include flex(center, center, nowrap); 19 | } 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /src/components/Qrcode/src/types/index.ts: -------------------------------------------------------------------------------- 1 | import type { QRCodeRenderersOptions } from "qrcode" 2 | 3 | /** 4 | * @description: qrcode components props type 5 | */ 6 | type QrcodePropsType = { 7 | render?: "canvas" | "img" 8 | value?: string 9 | options?: QRCodeRenderersOptions 10 | width?: number 11 | logo?: QrcodeOptionType | string 12 | disabled?: boolean 13 | disabledText?: string 14 | } 15 | 16 | /** 17 | * @description: qrcode options type 18 | */ 19 | type QrcodeOptionType = { 20 | src?: string 21 | logoSize?: number 22 | bgColor?: string 23 | borderSize?: number 24 | crossOrigin?: string 25 | borderRadius?: number 26 | logoRadius?: number 27 | } 28 | 29 | export type { QrcodePropsType, QrcodeOptionType } 30 | -------------------------------------------------------------------------------- /src/components/SearchMenus/index.ts: -------------------------------------------------------------------------------- 1 | import SearchMenus from "./src/SearchMenus.vue" 2 | 3 | export { SearchMenus } 4 | -------------------------------------------------------------------------------- /src/components/SearchMenus/src/SearchMenus.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-search-menus { 3 | height: inherit; 4 | padding: 0 10px; 5 | 6 | @include flex(center, center, nowrap); 7 | } 8 | .search-icon { 9 | transform: rotate(270deg); 10 | 11 | @include cursor; 12 | } 13 | .search-footer { 14 | height: inherit; 15 | .keyboard-tips { 16 | margin-right: 10px; 17 | font-size: 14px; 18 | 19 | @include flex(flex-start, center, nowrap); 20 | } 21 | 22 | @include flex(flex-start, center, nowrap); 23 | } 24 | .card-inside { 25 | &-text { 26 | @include cursor(default); 27 | } 28 | 29 | @include cursor(default); 30 | @include flex(flex-start, center, nowrap); 31 | } 32 | -------------------------------------------------------------------------------- /src/components/SearchMenus/src/helper/index.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hytql8/vue-vx-admin/07be380bc67bf7a04d58a048c64dc0b7bb9ffd1a/src/components/SearchMenus/src/helper/index.ts -------------------------------------------------------------------------------- /src/components/SearchMenus/src/types/index.ts: -------------------------------------------------------------------------------- 1 | type SearchMenusType = { 2 | path: string 3 | name: string 4 | icon: string 5 | isCheck?: boolean 6 | } 7 | 8 | export type { SearchMenusType } 9 | -------------------------------------------------------------------------------- /src/components/StructureTypes/index.ts: -------------------------------------------------------------------------------- 1 | import { TableColumnParameterTypes } from "@/components/Table/src/types" 2 | import { FormSchema } from "@/components/Form" 3 | 4 | type TableColumnConfig = Omit 5 | type FormItemConfig = Omit 6 | 7 | /** 8 | * @description 构造表格表单的配置数据类型 tableConfig formConfig需要必传,如果不需要form table同时显示,则无需用此数据结构 9 | * @param field 表格/表单的绑定的数据key 10 | * @param label 表头/表单标题 文字 11 | * @param tableConfig table配置 12 | * @param formConfig form 配置 13 | */ 14 | type StructureConfig = { 15 | field: string 16 | label?: string 17 | tableConfig: TableColumnConfig 18 | formConfig: FormItemConfig 19 | [key: string]: any 20 | } 21 | 22 | export type { StructureConfig } 23 | -------------------------------------------------------------------------------- /src/components/Table/index.ts: -------------------------------------------------------------------------------- 1 | import Table from "./src/Table.vue" 2 | 3 | export { Table } 4 | -------------------------------------------------------------------------------- /src/components/Table/src/Table.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-table { 3 | display: flex; 4 | flex-direction: column; 5 | height: 100%; 6 | background-color: var(--theme-div-color); 7 | &__fullscreen { 8 | display: flex; 9 | flex-direction: column; 10 | height: 100%; 11 | background-color: var(--theme-div-color); 12 | } 13 | &__pagination { 14 | height: var(--pagination-global-height); 15 | line-height: var(--pagination-global-height); 16 | background-color: var(--theme-div-color); 17 | } 18 | &__prefix { 19 | flex-direction: column; 20 | align-content: start; 21 | 22 | @include flex(start, start, wrap); 23 | } 24 | .fullscreen-padding { 25 | padding: 20px 20px 10px; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/components/Table/src/components/TableSetting.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-table-setting-style { 3 | width: 100%; 4 | 5 | @include flex(space-between, center, unset); 6 | .setting-slot { 7 | flex: 1; 8 | } 9 | .setting-option { 10 | min-width: 150px; 11 | min-height: 30px; 12 | 13 | @include flex(flex-end, center, unset); 14 | } 15 | .vx-icon { 16 | margin-right: 5px; 17 | outline: 0 !important; 18 | 19 | @include cursor; 20 | } 21 | } 22 | .column-selection { 23 | &__header { 24 | height: 30px; 25 | .header-total { 26 | height: inherit; 27 | &-sum { 28 | margin-left: 5px; 29 | } 30 | 31 | @include flex(flex-start, center, nowrap); 32 | } 33 | 34 | @include flex(space-between, center, nowrap); 35 | } 36 | } 37 | .treenode-group { 38 | width: 100%; 39 | 40 | @include flex(space-between, center, unset); 41 | .icon-group { 42 | .icon-left { 43 | margin-right: 5px; 44 | } 45 | 46 | @include flex(flex-start, center, nowrap); 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/components/Table/src/helper/index.ts: -------------------------------------------------------------------------------- 1 | import { omitSetting } from "@/constants" 2 | import type { TableColumnParameterTypes } from "@/components/Table/src/types" 3 | 4 | // 筛选TableSetting 需要排除的表单项配置 5 | 6 | const filterBySetting = (data: TableColumnParameterTypes[]) => { 7 | console.log() 8 | const settingSet = new Set(omitSetting) 9 | const result = [] as TableColumnParameterTypes[] 10 | for (let v of data) { 11 | if (v.filed) { 12 | console.log(v) 13 | } 14 | } 15 | return data 16 | } 17 | 18 | export { filterBySetting } 19 | -------------------------------------------------------------------------------- /src/components/TagsView/index.ts: -------------------------------------------------------------------------------- 1 | import TagsView from "./src/TagsView.vue" 2 | import { computed, unref } from "vue" 3 | import { router } from "@/router" 4 | import { useTagsStoreWithOut } from "@/store/modules/tags" 5 | 6 | const tagsStore = useTagsStoreWithOut() 7 | const tagsList = computed(() => tagsStore.getTagsList) 8 | 9 | // 初始化tags 10 | const tagsViewInit = (): string => { 11 | const redirect = router.getRoutes().find(v => v.path === "/").redirect 12 | const defaultCurRoute = router.getRoutes().find(v => v.path === redirect) 13 | if (!unref(tagsList)?.length) { 14 | const arr: TagsList[] = [] 15 | arr.push( 16 | Object.assign(unref(tagsList)[0] ?? {}, { 17 | title: defaultCurRoute.meta.title, 18 | path: defaultCurRoute.path, 19 | icon: defaultCurRoute.meta.icon, 20 | isFixed: true, 21 | current: true 22 | }) 23 | ) 24 | tagsStore.setTagsList(arr) 25 | return defaultCurRoute.path 26 | } 27 | return void 0 28 | } 29 | 30 | export { TagsView, tagsViewInit } 31 | -------------------------------------------------------------------------------- /src/components/TagsView/src/TagsView.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-tags { 3 | width: 100%; 4 | background-color: var(--theme-div-color); 5 | border-bottom: 1px solid var(--divider-color); 6 | 7 | @include transition(background-color); 8 | &-list-pre { 9 | height: 32px; 10 | padding: 0 8px; 11 | background-color: var(--theme-div-color); 12 | border-right: 1px solid var(--divider-color); 13 | border-left: 1px solid var(--divider-color); 14 | 15 | @include flex(center, center, nowrap); 16 | @include transition(background-color); 17 | } 18 | & .first-pre { 19 | border-left: unset; 20 | } 21 | &-list-suf { 22 | height: 32px; 23 | padding: 0 8px; 24 | background-color: var(--theme-div-color); 25 | border-right: 1px solid var(--divider-color); 26 | 27 | @include transition(background-color); 28 | @include flex(center, center, nowrap); 29 | } 30 | :deep(.el-scrollbar) { 31 | flex: 1; 32 | } 33 | &-list { 34 | width: 100%; 35 | padding: 2px 0; 36 | 37 | @include flex(start, center, nowrap); 38 | &__item { 39 | height: 28px; 40 | margin: 0 3px; 41 | font-size: var(--text-small-size); 42 | color: var(--theme-text-color); 43 | border-radius: 2px; 44 | &--current { 45 | color: #ffffff; 46 | white-space: nowrap; 47 | background-color: var(--el-color-primary); 48 | 49 | @include flex(start, center, nowrap); 50 | &__left { 51 | height: inherit; 52 | padding-top: 2px; 53 | padding-left: 10px; 54 | 55 | @include flex(space-between, center, nowrap); 56 | } 57 | &__right { 58 | height: inherit; 59 | padding: 0 4px; 60 | color: #ffffff; 61 | & > .seat { 62 | width: 10px; 63 | } 64 | 65 | @include flex(center, center, nowrap); 66 | } 67 | } 68 | &--normal { 69 | white-space: nowrap; 70 | background-color: var(--theme-div-color); 71 | border: 1px solid var(--divider-color); 72 | 73 | @include flex(start, center, nowrap); 74 | &:hover { 75 | color: var(--el-color-primary); 76 | } 77 | &__left { 78 | padding-left: 10px; 79 | 80 | @include flex(start, center, nowrap); 81 | } 82 | &__right { 83 | height: inherit; 84 | padding: 0 4px; 85 | & > .seat { 86 | width: 10px; 87 | } 88 | 89 | @include flex(center, center, nowrap); 90 | } 91 | } 92 | 93 | @include cursor; 94 | @include flex; 95 | } 96 | } 97 | 98 | @include flex(start, center, nowrap); 99 | } 100 | .dropItem__span { 101 | margin-left: 5px; 102 | } 103 | -------------------------------------------------------------------------------- /src/components/ThemeSwitch/index.ts: -------------------------------------------------------------------------------- 1 | import ThemeSwitch from "./src/ThemeSwitch.vue" 2 | export { ThemeSwitch } 3 | -------------------------------------------------------------------------------- /src/components/ThemeSwitch/src/ThemeSwitch.vue: -------------------------------------------------------------------------------- 1 | 44 | 54 | -------------------------------------------------------------------------------- /src/components/VxContainer/index.ts: -------------------------------------------------------------------------------- 1 | import VxContainer from "./src/VxContainer.vue" 2 | 3 | export { VxContainer } 4 | -------------------------------------------------------------------------------- /src/components/VxContainer/src/VxContainer.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-container { 3 | width: 100%; 4 | height: 100%; 5 | &__scrollbar { 6 | height: inherit; 7 | padding: 15px; 8 | color: var(--theme-text-color); 9 | background-color: var(--theme-div-color); 10 | border-radius: 5px; 11 | box-shadow: var(--el-box-shadow); 12 | 13 | @include transition(background-color); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/components/VxContainer/src/VxContainer.vue: -------------------------------------------------------------------------------- 1 | 6 | 13 | 14 | 17 | -------------------------------------------------------------------------------- /src/components/VxIcon/index.ts: -------------------------------------------------------------------------------- 1 | import VxIcon from "./src/VxIcon.vue" 2 | 3 | export { VxIcon } 4 | -------------------------------------------------------------------------------- /src/components/VxIcon/src/VxIcon.vue: -------------------------------------------------------------------------------- 1 | 40 | 47 | -------------------------------------------------------------------------------- /src/components/VxIcon/src/types/index.ts: -------------------------------------------------------------------------------- 1 | interface IconTypes { 2 | icon: string 3 | color?: string 4 | size?: number 5 | hoverColor?: string 6 | } 7 | 8 | export type { IconTypes } 9 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | // app的常量 2 | //路由白名单 3 | export const routerWhiteList = [] 4 | // 渲染菜单排除项 name 5 | export const menuWhiteList = ["Login", "Root", "Redirect", "404"] 6 | // 定义 axios 请求头 7 | export const baseURL = "" 8 | // TableSetting 需要排除的表单项配置 9 | export const omitSetting = ["#", "index", "expand"] 10 | -------------------------------------------------------------------------------- /src/directives/auth/index.ts: -------------------------------------------------------------------------------- 1 | // 按钮级别权限 2 | import { hasAuth } from "./utils" 3 | import type { Directive, DirectiveBinding } from "vue" 4 | import { App } from "vue" 5 | 6 | export const auth: Directive = { 7 | mounted(el: HTMLElement, binding: DirectiveBinding) { 8 | const { value } = binding 9 | if (value) { 10 | !hasAuth(value) && el.parentNode?.removeChild(el) 11 | } else { 12 | throw new Error("[Directive: auth]: need auths! Like v-auth=\"['create', 'read', 'update', 'delete']\"") 13 | } 14 | } 15 | } 16 | 17 | export const setupAuthDirective = (app: App) => { 18 | app.directive("auth", auth) 19 | } 20 | -------------------------------------------------------------------------------- /src/directives/auth/utils.ts: -------------------------------------------------------------------------------- 1 | import { useStorage } from "@/hooks/useStorage" 2 | import { isArray, isString } from "@/utils/is" 3 | 4 | const { getStorage } = useStorage("localStorage") 5 | // 设置默认权限,默认仅有读的权限,实际应该更改 6 | const { auths } = getStorage("user") ?? { 7 | auths: ["read"] 8 | } 9 | export const hasAuth = (auth: string[] | string) => { 10 | if (isArray(auth)) { 11 | for (let v of auth) { 12 | return auths ? auths.includes(v) : false 13 | } 14 | } else if (isString(auth)) { 15 | return auths ? auths.includes[auth] : false 16 | } 17 | return false 18 | } 19 | -------------------------------------------------------------------------------- /src/hooks/useEcharts.ts: -------------------------------------------------------------------------------- 1 | import type { EChartsOption } from "echarts" 2 | import type { Ref } from "vue" 3 | import { tryOnUnmounted, useDebounceFn } from "@vueuse/core" 4 | import { unref, nextTick, watch, ref, isRef } from "vue" 5 | import echarts from "@/plugins/echarts" 6 | import { useAppStoreWithOut } from "@/store/modules/app" 7 | 8 | const appStore = useAppStoreWithOut() 9 | 10 | export const useECharts = (elRef: Ref) => { 11 | // 获取当前主题 12 | const isDark = ref(appStore.getIsDark ? "dark" : "light") 13 | // echarts实例ref对象 14 | let echartsInstance: Nullable = null 15 | // 接受的options对象缓存 16 | const cacheOptions = ref({}) as Ref 17 | // 移除resize监听方法 18 | // eslint-disable-next-line @typescript-eslint/no-empty-function 19 | let removeResizeFn: Fn = () => {} 20 | // 图表根据父容器大小resize 21 | const resize = useDebounceFn(() => { 22 | echartsInstance && 23 | echartsInstance.resize({ 24 | animation: { 25 | duration: 200, 26 | easing: "quadraticIn" 27 | } 28 | }) 29 | }, 200) 30 | // 监听主题变化 31 | watch( 32 | () => appStore.getIsDark, 33 | async (val: boolean) => { 34 | isDark.value = val ? "dark" : "light" 35 | if (echartsInstance) { 36 | disposeCharts() 37 | await setOptions(unref(cacheOptions)) 38 | } 39 | }, 40 | { 41 | immediate: true 42 | } 43 | ) 44 | // 监听是否折叠菜单 45 | watch( 46 | () => appStore.getIsFold, 47 | () => { 48 | resize() 49 | }, 50 | { 51 | immediate: true 52 | } 53 | ) 54 | 55 | // 初始化echart 56 | const initCharts = async (theme: string = "light") => { 57 | await nextTick() 58 | const el = unref(elRef) 59 | if (!el || !unref(el)) { 60 | console.warn("echarts is null") 61 | return 62 | } 63 | echartsInstance = echarts.init(el, theme) 64 | window.addEventListener("resize", resize) 65 | removeResizeFn = () => { 66 | window.removeEventListener("resize", resize) 67 | } 68 | } 69 | // 销毁当前的 echarts 实例 70 | const disposeCharts = () => { 71 | echartsInstance?.dispose() 72 | echartsInstance = null 73 | } 74 | // 设置echarts options方法 75 | const setOptions = async (options: EChartsOption | Ref) => { 76 | let activeOptions = isRef(options) ? unref(options) : options 77 | cacheOptions.value = activeOptions 78 | if (!echartsInstance) { 79 | await initCharts(unref(isDark)) 80 | } 81 | echartsInstance.setOption(activeOptions) 82 | } 83 | // echarts 对象实例 84 | const getInstance = async (): Promise> => { 85 | if (!echartsInstance) { 86 | await initCharts(unref(isDark)) 87 | } 88 | return echartsInstance as Nullable 89 | } 90 | // 组件卸载,图表相关一样卸载 91 | tryOnUnmounted(() => { 92 | if (!echartsInstance) return 93 | removeResizeFn() 94 | disposeCharts() 95 | echartsInstance = null 96 | }) 97 | 98 | return { 99 | setOptions, 100 | resize, 101 | echarts, 102 | getInstance 103 | } 104 | } 105 | -------------------------------------------------------------------------------- /src/hooks/useForm.ts: -------------------------------------------------------------------------------- 1 | import type { Form, FormExpose } from "@/components/Form" 2 | import type { ElForm, ElFormItem } from "element-plus" 3 | import { ref, unref, nextTick } from "vue" 4 | import { FormSchema, FormSetProps, FormProps } from "@/components/Form" 5 | import { isEmptyVal, isObject } from "@/utils/is" 6 | 7 | export const useForm = () => { 8 | // From实例 9 | const formRef = ref() 10 | 11 | // ElForm实例 12 | const elFormRef = ref>() 13 | 14 | /** 15 | * @param ref Form实例 16 | * @param elRef ElForm实例 17 | */ 18 | const register = (ref: typeof Form & FormExpose, elRef: InstanceType) => { 19 | formRef.value = ref 20 | elFormRef.value = elRef 21 | } 22 | 23 | const getForm = async () => { 24 | await nextTick() 25 | const form = unref(formRef) 26 | if (!form) { 27 | console.log("没有找到form实例") 28 | } 29 | return form 30 | } 31 | 32 | // 一些内置的方法 33 | const methods = { 34 | /** 35 | * @description 设置form组件的props 36 | * @param props form组件的props 37 | */ 38 | setProps: async (props: FormProps = {}) => { 39 | const form = await getForm() 40 | form?.setProps(props) 41 | if (props.model) { 42 | form?.setValues(props.model) 43 | } 44 | }, 45 | 46 | /** 47 | * @description 设置form的值 48 | * @param data 需要设置的数据 49 | */ 50 | setValues: async (data: Recordable) => { 51 | const form = await getForm() 52 | form?.setValues(data) 53 | }, 54 | 55 | /** 56 | * @description 设置schema 57 | * @param schemaProps 需要设置的schemaProps 58 | */ 59 | setSchema: async (schemaProps: FormSetProps[]) => { 60 | const form = await getForm() 61 | form?.setSchema(schemaProps) 62 | }, 63 | 64 | /** 65 | * @description 新增schema 66 | * @param formSchema 需要新增数据 67 | * @param index 在哪里新增 68 | */ 69 | addSchema: async (formSchema: FormSchema, index?: number) => { 70 | const form = await getForm() 71 | form?.addSchema(formSchema, index) 72 | }, 73 | 74 | /** 75 | * @description 删除schema 76 | * @param field 删除哪个数据 77 | */ 78 | delSchema: async (field: string) => { 79 | const form = await getForm() 80 | form?.delSchema(field) 81 | }, 82 | 83 | /** 84 | * @description 获取表单数据 85 | * @returns form data 86 | */ 87 | getFormData: async (filterEmptyVal = true): Promise => { 88 | const form = await getForm() 89 | const model = form?.formModel as any 90 | if (filterEmptyVal) { 91 | // 使用reduce过滤空值,并返回一个新对象 92 | return Object.keys(model).reduce((prev, next) => { 93 | const value = model[next] 94 | if (!isEmptyVal(value)) { 95 | if (isObject(value)) { 96 | if (Object.keys(value).length > 0) { 97 | prev[next] = value 98 | } 99 | } else { 100 | prev[next] = value 101 | } 102 | } 103 | return prev 104 | }, {}) as T 105 | } else { 106 | return model as T 107 | } 108 | }, 109 | 110 | /** 111 | * @description 获取表单组件的实例 112 | * @param field 表单项唯一标识 113 | * @returns component instance 114 | */ 115 | getComponentExpose: async (field: string) => { 116 | const form = await getForm() 117 | return form?.getComponentExpose(field) 118 | }, 119 | 120 | /** 121 | * @description 获取formItem组件的实例 122 | * @param field 表单项唯一标识 123 | * @returns formItem instance 124 | */ 125 | getFormItemExpose: async (field: string) => { 126 | const form = await getForm() 127 | return form?.getFormItemExpose(field) as InstanceType 128 | }, 129 | 130 | /** 131 | * @description 获取ElForm组件的实例 132 | * @returns ElForm instance 133 | */ 134 | getElFormExpose: async () => { 135 | await getForm() 136 | return unref(elFormRef) 137 | }, 138 | 139 | getFormExpose: async () => { 140 | await getForm() 141 | return unref(formRef) 142 | } 143 | } 144 | 145 | return { 146 | formRegister: register, 147 | formMethods: methods 148 | } 149 | } 150 | -------------------------------------------------------------------------------- /src/hooks/useI18n.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/plugins/vueI18n" 2 | //解决element plus内的rule国际化 3 | const getKey = (namespace: string | undefined, key: string) => { 4 | if (!namespace) { 5 | return key 6 | } 7 | if (key.startsWith(namespace)) { 8 | return key 9 | } 10 | return `${namespace}.${key}` 11 | } 12 | 13 | /** 14 | * @param namespace 指定翻译哪一个模块的,比如common,就只翻译common对象内的字段 15 | */ 16 | export const useI18n = (namespace?: string) => { 17 | const normalFn = { 18 | t: (key: string) => { 19 | return getKey(namespace, key) 20 | } 21 | } 22 | 23 | if (!i18n) { 24 | return normalFn 25 | } 26 | 27 | const { t, ...methods } = i18n.global 28 | 29 | const tFn: any = (key: string, ...arg: any[]) => { 30 | if (!key) return "" 31 | if (!key.includes(".") && !namespace) return key 32 | return (t as any)(getKey(namespace, key), ...(arg as any)) 33 | } 34 | return { 35 | ...methods, 36 | t: tFn 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/hooks/useIcon.ts: -------------------------------------------------------------------------------- 1 | import { h } from "vue" 2 | import type { VNode } from "vue" 3 | import { VxIcon } from "@/components/VxIcon" 4 | import type { IconTypes } from "@/components/VxIcon/src/types" 5 | 6 | export const useIcon = (props: IconTypes): VNode => { 7 | return h(VxIcon, props) 8 | } 9 | -------------------------------------------------------------------------------- /src/hooks/useLocale.ts: -------------------------------------------------------------------------------- 1 | import { i18n } from "@/plugins/vueI18n" 2 | import { Ref } from "vue" 3 | 4 | // 设置html的语言属性 lang 5 | export const setHtmlLang = (currentLocale: CurrentLocale) => { 6 | document.querySelector("html")?.setAttribute("lang", currentLocale.lang) 7 | } 8 | 9 | const setI18nLanguage = (locale: LocaleMap) => { 10 | ;(i18n.global.locale as Ref).value = locale.lang 11 | } 12 | 13 | export const useCustomLocale = () => { 14 | const changeLocale = async (locale: LocaleMap) => { 15 | const globalI18n = i18n.global 16 | const langModule = await import(`../locales/lang/${locale.lang === "en" ? "en" : "cn"}.ts`) 17 | 18 | globalI18n.setLocaleMessage(locale.lang, langModule.default) 19 | setI18nLanguage(locale) 20 | } 21 | 22 | return { 23 | changeLocale 24 | } 25 | } 26 | 27 | // 此方法用于配合i18n-Ally翻译 28 | export const t = (key: string) => key 29 | -------------------------------------------------------------------------------- /src/hooks/useProgress.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, unref } from "vue" 2 | import type { NProgressOptions } from "nprogress" 3 | import NProgress from "nprogress" 4 | import "nprogress/nprogress.css" 5 | import { useCssVar } from "@vueuse/core" 6 | 7 | const primaryColor = useCssVar("--el-color-primary", document.documentElement) 8 | 9 | export const useNProgress = () => { 10 | NProgress.configure({ showSpinner: false } as NProgressOptions) 11 | 12 | const initColor = async () => { 13 | await nextTick() 14 | const bar = document.getElementById("nprogress")?.getElementsByClassName("bar")[0] as HTMLElement 15 | if (bar) { 16 | bar.style.background = unref(primaryColor.value) 17 | } 18 | } 19 | 20 | initColor() 21 | 22 | const start = () => { 23 | NProgress.start() 24 | } 25 | 26 | const done = () => { 27 | NProgress.done() 28 | } 29 | 30 | return { 31 | start, 32 | done 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/hooks/useStorage.ts: -------------------------------------------------------------------------------- 1 | // 获取传入的值的类型 2 | const getValueType = (value: any) => { 3 | const type = Object.prototype.toString.call(value) 4 | return type.slice(8, -1) 5 | } 6 | 7 | export const useStorage = (type: "sessionStorage" | "localStorage" = "sessionStorage") => { 8 | const setStorage = (key: string, value: any) => { 9 | const valueType = getValueType(value) 10 | window[type].setItem(key, JSON.stringify({ type: valueType, value })) 11 | } 12 | 13 | const getStorage = (key: string) => { 14 | const value = window[type].getItem(key) 15 | if (value) { 16 | const { value: val } = JSON.parse(value) 17 | return val 18 | } else { 19 | return value 20 | } 21 | } 22 | 23 | const removeStorage = (key: string) => { 24 | window[type].removeItem(key) 25 | } 26 | 27 | const clear = (excludes?: string[]) => { 28 | // 获取排除项 29 | const keys = Object.keys(window[type]) 30 | const defaultExcludes = ["dynamicRouter", "serverDynamicRouter"] 31 | const excludesArr = excludes ? [...excludes, ...defaultExcludes] : defaultExcludes 32 | const excludesKeys = excludesArr ? keys.filter(key => !excludesArr.includes(key)) : keys 33 | // 排除项不清除 34 | excludesKeys.forEach(key => { 35 | window[type].removeItem(key) 36 | }) 37 | } 38 | 39 | return { 40 | setStorage, 41 | getStorage, 42 | removeStorage, 43 | clear 44 | } 45 | } 46 | -------------------------------------------------------------------------------- /src/hooks/useStructure.ts: -------------------------------------------------------------------------------- 1 | import type { StructureConfig } from "@/components/StructureTypes" 2 | 3 | export const useStructure = (structure: StructureConfig[]) => { 4 | if (!structure.length) return 5 | 6 | const formItems = structure.map((config: StructureConfig) => { 7 | return { 8 | field: config.field, 9 | label: config.label, 10 | ...config.formConfig 11 | } 12 | }) 13 | 14 | const tableColumns = structure.map((config: StructureConfig) => { 15 | return { 16 | field: config.field, 17 | label: config.label, 18 | ...config.tableConfig 19 | } 20 | }) 21 | return { 22 | formItems, 23 | tableColumns 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/hooks/useTable.ts: -------------------------------------------------------------------------------- 1 | import { ref, onMounted, unref, nextTick, watch } from "vue" 2 | import { Table } from "@/components/Table" 3 | import type { TableParameterTypes, TableColumnParameterTypes, TableSetProps, TableExpose } from "@/components/Table/src/types" 4 | import { ElTable, ElMessageBox, ElMessage } from "element-plus" 5 | import { t } from "@/hooks/useLocale" 6 | 7 | /** 8 | * @param immediate 是否初始化的时候请求一次 9 | * @param getDataApi 请求数据的Api 10 | * @param delDataApi 删除数据的Api 11 | */ 12 | interface UseTableConfig { 13 | immediate?: boolean 14 | getDataApi: () => Promise<{ 15 | list: any[] 16 | total?: number 17 | }> 18 | delDataApi?: () => Promise 19 | } 20 | 21 | export const useTable = (config: UseTableConfig) => { 22 | const { immediate = true } = config 23 | 24 | const loading = ref(false) 25 | const currentPage = ref(1) 26 | const pageSize = ref(10) 27 | const total = ref(0) 28 | const dataList = ref([]) 29 | 30 | onMounted(() => { 31 | if (immediate) { 32 | methods.getList() 33 | } 34 | }) 35 | 36 | const tableRef = ref & TableExpose>() 37 | const elTableRef = ref>() 38 | 39 | // 注册方法实例 40 | const register = (ref: InstanceType & TableExpose, elRef: InstanceType) => { 41 | tableRef.value = ref 42 | elTableRef.value = unref(elRef) 43 | } 44 | // 获取tableRef 45 | const getTable = async () => { 46 | await nextTick() 47 | const table = unref(tableRef) 48 | // if (!table) { 49 | // console.warn("table注册失败") 50 | // } 51 | return table 52 | } 53 | //监听变化的pageSize和currentPage 54 | watch( 55 | () => unref(pageSize), 56 | (val: number) => { 57 | pageSize.value = val 58 | methods.getList() 59 | } 60 | ) 61 | 62 | watch( 63 | () => unref(currentPage), 64 | (val: number) => { 65 | currentPage.value = val 66 | methods.getList() 67 | } 68 | ) 69 | 70 | const methods = { 71 | // 请求table数据 72 | getList: async () => { 73 | loading.value = true 74 | try { 75 | const res = await config?.getDataApi() 76 | console.log("获取数据成功!", res) 77 | if (res) { 78 | dataList.value = res.list 79 | total.value = res.total || 0 80 | } 81 | } catch (err) { 82 | console.log("获取数据失败!") 83 | } finally { 84 | loading.value = false 85 | } 86 | }, 87 | /** 88 | * @description 设置table组件的props 89 | * @param props table组件的props 90 | */ 91 | setProps: async (props: TableParameterTypes = {}) => { 92 | const table = await getTable() 93 | table?.setProps(props) 94 | }, 95 | 96 | /** 97 | * @description 设置column 98 | * @param columnProps 需要设置的列 99 | */ 100 | setColumn: async (columnProps: TableSetProps[]) => { 101 | const table = await getTable() 102 | table?.setColumn(columnProps) 103 | }, 104 | 105 | /** 106 | * @description 新增column 107 | * @param tableColumn 需要新增数据 108 | * @param index 在哪里新增 109 | */ 110 | addColumn: async (tableColumn: TableColumnParameterTypes, index?: number) => { 111 | const table = await getTable() 112 | table?.addColumn(tableColumn, index) 113 | }, 114 | 115 | /** 116 | * @description 删除column 117 | * @param field 删除哪个数据 118 | */ 119 | delColumn: async (field: string) => { 120 | const table = await getTable() 121 | table?.delColumn(field) 122 | }, 123 | 124 | /** 125 | * @description 获取ElTable组件的实例 126 | * @returns ElTable instance 127 | */ 128 | getElTableExpose: async () => { 129 | await getTable() 130 | return unref(elTableRef) 131 | }, 132 | 133 | refresh: () => { 134 | methods.getList() 135 | }, 136 | 137 | // 删除数据 138 | delList: async (idsLength: number) => { 139 | const { delDataApi } = config 140 | if (!delDataApi) { 141 | console.warn("delDataApi is undefined") 142 | return 143 | } 144 | ElMessageBox.confirm(t("layout.test"), t("layout.test"), { 145 | confirmButtonText: t("layout.test"), 146 | cancelButtonText: t("layout.test"), 147 | type: "warning" 148 | }).then(async () => { 149 | const res = await delDataApi() 150 | if (res) { 151 | ElMessage.success(t("layout.test")) 152 | 153 | // 计算出临界点 154 | const current = 155 | unref(total) % unref(pageSize) === idsLength || unref(pageSize) === 1 156 | ? unref(currentPage) > 1 157 | ? unref(currentPage) - 1 158 | : unref(currentPage) 159 | : unref(currentPage) 160 | 161 | currentPage.value = current 162 | methods.getList() 163 | } 164 | }) 165 | } 166 | } 167 | 168 | return { 169 | tableRegister: register, 170 | tableMethods: methods, 171 | tableState: { 172 | currentPage, 173 | pageSize, 174 | total, 175 | dataList, 176 | loading 177 | } 178 | } 179 | } 180 | -------------------------------------------------------------------------------- /src/hooks/useValidator.ts: -------------------------------------------------------------------------------- 1 | import { useI18n } from "@/hooks/useI18n" 2 | import { FormItemRule } from "element-plus" 3 | 4 | const { t } = useI18n() 5 | 6 | interface LengthRange { 7 | min: number 8 | max: number 9 | message?: string 10 | } 11 | 12 | export const useValidator = () => { 13 | const required = (message?: string): FormItemRule => { 14 | return { 15 | required: true, 16 | message: message || t("common.required") 17 | } 18 | } 19 | 20 | const lengthRange = (options: LengthRange): FormItemRule => { 21 | const { min, max, message } = options 22 | 23 | return { 24 | min, 25 | max, 26 | message: message || t("common.lengthRange", { min, max }) 27 | } 28 | } 29 | 30 | const notSpace = (message?: string): FormItemRule => { 31 | return { 32 | validator: (_, val, callback) => { 33 | if (val?.indexOf(" ") !== -1) { 34 | callback(new Error(message || t("common.notSpace"))) 35 | } else { 36 | callback() 37 | } 38 | } 39 | } 40 | } 41 | 42 | const notSpecialCharacters = (message?: string): FormItemRule => { 43 | return { 44 | validator: (_, val, callback) => { 45 | if (/[`~!@#$%^&*()_+<>?:"{},.\/;'[\]]/gi.test(val)) { 46 | callback(new Error(message || t("common.notSpecialCharacters"))) 47 | } else { 48 | callback() 49 | } 50 | } 51 | } 52 | } 53 | 54 | const phone = (message?: string): FormItemRule => { 55 | return { 56 | validator: (_, val, callback) => { 57 | if (!val) return callback() 58 | if (!/^1[3456789]\d{9}$/.test(val)) { 59 | callback(new Error(message || "请输入正确的手机号码")) 60 | } else { 61 | callback() 62 | } 63 | } 64 | } 65 | } 66 | 67 | const email = (message?: string): FormItemRule => { 68 | return { 69 | validator: (_, val, callback) => { 70 | if (!val) return callback() 71 | if (!/^(\w-*\.*)+@(\w-?)+(\.\w{2,})+$/.test(val)) { 72 | callback(new Error(message || "请输入正确的邮箱")) 73 | } else { 74 | callback() 75 | } 76 | } 77 | } 78 | } 79 | 80 | const maxlength = (max: number): FormItemRule => { 81 | return { 82 | max, 83 | message: "长度不能超过" + max + "个字符" 84 | } 85 | } 86 | 87 | const check = (message?: string): FormItemRule => { 88 | return { 89 | validator: (_, val, callback) => { 90 | if (!val) { 91 | callback(new Error(message || t("common.required"))) 92 | } else { 93 | callback() 94 | } 95 | } 96 | } 97 | } 98 | 99 | return { 100 | required, 101 | lengthRange, 102 | notSpace, 103 | notSpecialCharacters, 104 | phone, 105 | email, 106 | maxlength, 107 | check 108 | } 109 | } 110 | -------------------------------------------------------------------------------- /src/hooks/useX6.ts: -------------------------------------------------------------------------------- 1 | import { nextTick, unref, isRef } from "vue" 2 | import type { Ref } from "vue" 3 | import { Graph } from "@antv/x6" 4 | 5 | export const useX6 = (elRef: Ref, ...args: any) => { 6 | // antv x6实例对象 7 | let instance: Nullable = null 8 | // 初始化 9 | const init = async () => { 10 | await nextTick() 11 | if (!elRef || !unref(elRef)) { 12 | console.log("antv is null") 13 | return 14 | } 15 | instance = new Graph( 16 | Object.assign( 17 | { 18 | container: unref(elRef) 19 | }, 20 | ...args 21 | ) 22 | ) 23 | } 24 | // 设置参数data 25 | const setOptions = async (data: any) => { 26 | let activeOptions = isRef(data) ? unref(data) : data 27 | if (!instance) { 28 | await init() 29 | } 30 | instance.fromJSON(activeOptions) 31 | } 32 | 33 | // 获取当前antv x6实例 34 | const getInstance = async () => { 35 | if (!instance) { 36 | await init() 37 | } 38 | return instance as Nullable 39 | } 40 | // 添加node 41 | const addNode = async (data: any) => { 42 | if (!instance) { 43 | await init() 44 | } 45 | return instance.addNode(data) 46 | } 47 | // 添加edge 48 | const addEdge = async (data: any) => { 49 | if (!instance) { 50 | await init() 51 | } 52 | instance.addEdge(data) 53 | } 54 | // 删除node 55 | const removeNode = async (node: any) => { 56 | if (!instance) { 57 | await init() 58 | } 59 | instance.removeNode(node) 60 | } 61 | // 删除edge 62 | const removeEdge = async (data: any) => { 63 | if (!instance) { 64 | await init() 65 | } 66 | instance.removeEdge(data) 67 | } 68 | return { 69 | setOptions, 70 | getInstance, 71 | addNode, 72 | addEdge, 73 | removeNode, 74 | removeEdge 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/layout/index.ts: -------------------------------------------------------------------------------- 1 | import Layout from "./src/index.vue" 2 | 3 | export { Layout } 4 | -------------------------------------------------------------------------------- /src/layout/src/components/Horizontal.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-container { 3 | height: 100%; 4 | :deep(.el-header) { 5 | padding: 0 !important; 6 | } 7 | :deep(.el-main) { 8 | padding: unset; 9 | } 10 | .vx-header { 11 | width: 100%; 12 | height: var(--header-global-height); 13 | overflow: hidden; 14 | background-color: var(--theme-div-color); 15 | border-bottom: 1px solid var(--divider-color); 16 | &__logo { 17 | width: 210px; 18 | height: inherit; 19 | } 20 | &__menu { 21 | width: calc(100vw - 465px); 22 | height: inherit; 23 | overflow: hidden; 24 | :deep(.el-menu .el-sub-menu) { 25 | width: 180px; 26 | } 27 | :deep(.el-menu) { 28 | max-width: calc(100vw - 465px); 29 | } 30 | } 31 | &__domain { 32 | width: 250px; 33 | height: inherit; 34 | 35 | @include flex(flex-end, center, nowrap); 36 | } 37 | 38 | @include flex(space-between, center, nowrap); 39 | } 40 | } 41 | .vx-main { 42 | &__container { 43 | height: calc(100vh - var(--header-global-height) - var(--tags-view-global-height) - 1px); 44 | padding: 10px; 45 | } 46 | } 47 | -------------------------------------------------------------------------------- /src/layout/src/components/Horizontal.vue: -------------------------------------------------------------------------------- 1 | 32 | 33 | 62 | 63 | 69 | -------------------------------------------------------------------------------- /src/layout/src/components/Vertical.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-container { 3 | height: 100%; 4 | :deep(.el-header) { 5 | padding: 0 !important; 6 | } 7 | :deep(.el-main) { 8 | padding: unset; 9 | } 10 | .vx-header { 11 | width: 100%; 12 | height: var(--header-global-height); 13 | &__nav { 14 | position: relative; 15 | flex: 1; 16 | height: inherit; 17 | background-color: var(--theme-div-color); 18 | 19 | @include flex(space-between, center, nowrap); 20 | @include transition(background-color); 21 | &::after { 22 | position: absolute; 23 | bottom: 0; 24 | width: 100%; 25 | content: ""; 26 | border-bottom: 1px solid var(--divider-color); 27 | } 28 | } 29 | &__menu-switch { 30 | padding: 0 0 0 5px; 31 | 32 | @include cursor; 33 | @include flex(start, center, nowrap); 34 | } 35 | &__info { 36 | @include flex(start, center, nowrap); 37 | } 38 | 39 | @include flex(start, center, nowrap); 40 | } 41 | .vx-aside { 42 | overflow: hidden; 43 | background-color: var(--left-menu-bg-color); 44 | border-right: 1px solid var(--divider-color); 45 | 46 | @include transition(all); 47 | &__menu { 48 | width: inherit; 49 | height: 100%; 50 | background-color: var(--left-menu-bg-color); 51 | } 52 | } 53 | .vx-main { 54 | &__container { 55 | height: calc(100vh - var(--header-global-height) - var(--tags-view-global-height) - 1px); 56 | padding: 10px; 57 | } 58 | 59 | // &:deep(.vx-table) { 60 | // height: calc(100vh - var(--header-global-height) - var(--tags-view-global-height) - var(--pagination-global-height) - 1px); 61 | // } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/layout/src/components/Vertical.vue: -------------------------------------------------------------------------------- 1 | 55 | 56 | 90 | 91 | 97 | -------------------------------------------------------------------------------- /src/layout/src/components/index.ts: -------------------------------------------------------------------------------- 1 | import Horizontal from "./Horizontal.vue" 2 | import Vertical from "./Vertical.vue" 3 | 4 | export { Horizontal, Vertical } 5 | -------------------------------------------------------------------------------- /src/layout/src/index.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .layout { 3 | color: var(--theme-text-color); 4 | background-color: var(--theme-bg-color); 5 | &-vertical { 6 | height: inherit; 7 | } 8 | &-setting { 9 | position: fixed; 10 | top: 50%; 11 | right: 0; 12 | z-index: 999; 13 | transform: translateY(-50%); 14 | } 15 | &-inset { 16 | &__switch { 17 | width: 100%; 18 | 19 | @include flex(center, center, nowrap); 20 | } 21 | &__position { 22 | .normal-mode { 23 | width: 45px; 24 | height: 35px; 25 | overflow: hidden; 26 | border-radius: 3px; 27 | box-shadow: var(--el-box-shadow-light); 28 | .normal-left { 29 | width: 30%; 30 | height: 100%; 31 | .normal-left-t { 32 | width: 100%; 33 | height: 20%; 34 | background-color: #0f2438; 35 | } 36 | .normal-left-b { 37 | width: 100%; 38 | height: 80%; 39 | background-color: #0f2438; 40 | } 41 | } 42 | .normal-right { 43 | width: 70%; 44 | height: 100%; 45 | .normal-right-t { 46 | width: 100%; 47 | height: 20%; 48 | background-color: #ffffff; 49 | } 50 | .normal-right-b { 51 | width: 100%; 52 | height: 80%; 53 | background-color: #e0e3e6; 54 | } 55 | } 56 | 57 | @include cursor; 58 | @include flex(flex-start, center, nowrap); 59 | } 60 | .group-mode { 61 | width: 45px; 62 | height: 35px; 63 | margin-left: 20px; 64 | overflow: hidden; 65 | border-radius: 3px; 66 | box-shadow: var(--el-box-shadow-light); 67 | .group-left { 68 | width: 30%; 69 | height: 100%; 70 | .group-left-t { 71 | width: 100%; 72 | height: 20%; 73 | background-color: #001529; 74 | } 75 | .group-left-b { 76 | flex-direction: column; 77 | width: 100%; 78 | height: 80%; 79 | background-color: #001529; 80 | 81 | @include flex(flex-start, center, nowrap); 82 | &-i { 83 | width: 100%; 84 | height: 30%; 85 | background-color: #27343f; 86 | } 87 | &-y { 88 | width: 100%; 89 | height: 40%; 90 | background-color: #2b3641; 91 | transform: translateY(5px); 92 | } 93 | } 94 | } 95 | .group-right { 96 | width: 70%; 97 | height: 100%; 98 | .group-right-t { 99 | width: 100%; 100 | height: 20%; 101 | background-color: #ffffff; 102 | } 103 | .group-right-b { 104 | width: 100%; 105 | height: 80%; 106 | background-color: #e0e3e6; 107 | } 108 | } 109 | 110 | @include cursor; 111 | @include flex(flex-start, center, nowrap); 112 | } 113 | .top-mode { 114 | width: 45px; 115 | height: 35px; 116 | margin-left: 20px; 117 | overflow: hidden; 118 | border-radius: 3px; 119 | box-shadow: var(--el-box-shadow-light); 120 | .top-left { 121 | width: 30%; 122 | height: 100%; 123 | .top-left-t { 124 | width: 100%; 125 | height: 20%; 126 | background-color: #0f2438; 127 | } 128 | .top-left-b { 129 | width: 100%; 130 | height: 80%; 131 | background-color: #e0e3e6; 132 | } 133 | } 134 | .top-right { 135 | width: 70%; 136 | height: 100%; 137 | .top-right-t { 138 | width: 100%; 139 | height: 20%; 140 | background-color: #0f2438; 141 | } 142 | .top-right-b { 143 | width: 100%; 144 | height: 80%; 145 | background-color: #e0e3e6; 146 | } 147 | } 148 | 149 | @include cursor; 150 | @include flex(flex-start, center, nowrap); 151 | } 152 | .mode-active { 153 | border: 2px solid var(--theme-color); 154 | } 155 | 156 | @include flex(center, center, nowrap); 157 | } 158 | &__theme { 159 | @include flex(flex-start, center, nowrap); 160 | 161 | width: 100%; 162 | .color-box { 163 | width: 20px; 164 | height: 20px; 165 | margin-right: 15px; 166 | border-radius: 2px; 167 | 168 | @include cursor; 169 | } 170 | .mode-active { 171 | border: 2px solid var(--theme-color); 172 | } 173 | } 174 | &__config { 175 | width: 100%; 176 | .config-inset { 177 | width: inherit; 178 | font-size: 14px; 179 | 180 | @include flex(space-between, center, nowrap); 181 | } 182 | } 183 | } 184 | } 185 | .vx-setting { 186 | width: 32px; 187 | height: 32px; 188 | background-color: var(--theme-color); 189 | border-top-left-radius: 5px; 190 | border-bottom-left-radius: 5px; 191 | 192 | @include flex(center, center, nowrap); 193 | @include cursor; 194 | } 195 | -------------------------------------------------------------------------------- /src/locales/lang/cn.ts: -------------------------------------------------------------------------------- 1 | import vxeCn from "vxe-table/lib/locale/lang/zh-CN" 2 | 3 | export default { 4 | layout: { 5 | test: "测试语言" 6 | }, 7 | routes: { 8 | login: "登录", 9 | dashboard: "Dashboard", 10 | welcome: "欢迎页", 11 | workplace: "工作台", 12 | analysis: "分析页", 13 | notfound: "找不到页面", 14 | system: "系统管理", 15 | user: "用户管理", 16 | role: "角色管理", 17 | menu: "菜单管理", 18 | component: "组件", 19 | form: "表单", 20 | table: "表格", 21 | chart: "图表", 22 | defaultTable: "默认表格", 23 | useTable: "UseTable", 24 | VxeTable: "VxeTable", 25 | defaultForm: "默认表单", 26 | useForm: "useForm表单", 27 | authority: "权限", 28 | btnPerm: "按钮权限", 29 | workflow: "工作流", 30 | useX6: "使用X6", 31 | qrcode: "二维码" 32 | }, 33 | tagsOperations: { 34 | noTitle: "无标题", 35 | reload: "重新加载", 36 | closeTag: "关闭标签页", 37 | closeLeftTag: "关闭左侧标签页", 38 | closeRightTag: "关闭右侧标签页", 39 | closeOtherTag: "关闭其他标签页", 40 | closeAllTag: "关闭全部标签页" 41 | }, 42 | personalCenter: { 43 | personalCenter: "个人中心", 44 | changePassword: "修改密码", 45 | loginOut: "退出登录" 46 | }, 47 | size: { 48 | default: "默认", 49 | small: "紧凑", 50 | large: "中等" 51 | }, 52 | common: { 53 | required: "该项为必填项", 54 | lengthRange: "长度在 {min} 到 {max} 个字符", 55 | notSpace: "不能包含空格", 56 | notSpecialCharacters: "不能包含特殊字符", 57 | inputText: "请输入", 58 | selectText: "请选择", 59 | startTimeText: "开始时间", 60 | endTimeText: "结束时间" 61 | }, 62 | login: { 63 | login: "登录", 64 | username: "用户名", 65 | password: "密码", 66 | outDesp: "确定要退出登录吗?", 67 | confirm: "确认", 68 | cancel: "取消", 69 | outSuccess: "已成功注销", 70 | warn: "提示", 71 | tabQrLogin: "二维码登录", 72 | tips: "一款基于Typescript + vue3 + vite + vueuse的后台管理系统框架" 73 | }, 74 | formDemo: { 75 | input: "输入框", 76 | inputNumber: "数字输入框", 77 | default: "默认", 78 | icon: "图标", 79 | mixed: "复合型", 80 | password: "密码框", 81 | textarea: "多行文本", 82 | remoteSearch: "远程搜索", 83 | slot: "插槽", 84 | position: "位置", 85 | autocomplete: "自动补全", 86 | select: "选择器", 87 | optionSlot: "选项插槽", 88 | selectGroup: "选项分组", 89 | selectV2: "虚拟列表选择器", 90 | cascader: "级联选择器", 91 | switch: "开关", 92 | rate: "评分", 93 | colorPicker: "颜色选择器", 94 | transfer: "穿梭框", 95 | render: "渲染器", 96 | radio: "单选框", 97 | radioGroup: "单选框组", 98 | button: "按钮", 99 | checkbox: "多选框", 100 | checkboxButton: "多选框按钮", 101 | checkboxGroup: "多选框组", 102 | slider: "滑块", 103 | datePicker: "日期选择器", 104 | shortcuts: "快捷选项", 105 | today: "今天", 106 | yesterday: "昨天", 107 | aWeekAgo: "一周前", 108 | week: "周", 109 | year: "年", 110 | month: "月", 111 | dates: "日期", 112 | daterange: "日期范围", 113 | monthrange: "月份范围", 114 | dateTimePicker: "日期时间选择器", 115 | dateTimerange: "日期时间范围", 116 | timePicker: "时间选择器", 117 | timeSelect: "时间选择", 118 | inputPassword: "密码输入框", 119 | passwordStrength: "密码强度", 120 | defaultForm: "全部示例", 121 | formDes: "基于 ElementPlus 的 Form 组件二次封装,实现数据驱动,支持所有 Form 参数", 122 | example: "示例", 123 | operate: "操作", 124 | change: "更改", 125 | restore: "还原", 126 | disabled: "禁用", 127 | disablement: "解除禁用", 128 | delete: "删除", 129 | add: "添加", 130 | setValue: "设置值", 131 | resetValue: "重置值", 132 | set: "设置", 133 | subitem: "子项", 134 | formValidation: "表单验证", 135 | verifyReset: "验证重置", 136 | form: "表单", 137 | focus: "聚焦", 138 | treeSelect: "树形选择器", 139 | showCheckbox: "显示复选框", 140 | selectAnyLevel: "选择任意级别", 141 | multiple: "多选", 142 | filterable: "可筛选", 143 | customContent: "自定义内容", 144 | lazyLoad: "懒加载", 145 | upload: "上传", 146 | userAvatar: "用户头像", 147 | iconPicker: "图标选择器", 148 | iAgree: "我同意" 149 | }, 150 | result: { 151 | notfound: "抱歉,您访问的页面不存在。" 152 | }, 153 | ...vxeCn 154 | } 155 | -------------------------------------------------------------------------------- /src/locales/lang/en.ts: -------------------------------------------------------------------------------- 1 | import vxeEn from "vxe-table/lib/locale/lang/en-US" 2 | 3 | export default { 4 | layout: { 5 | test: "test Lang" 6 | }, 7 | routes: { 8 | login: "Login", 9 | dashboard: "Dashboard", 10 | welcome: "Welcome", 11 | workplace: "Workplace", 12 | analysis: "Analysis", 13 | notfound: "not found", 14 | system: "System Management", 15 | user: "User Management", 16 | role: "Role Management", 17 | menu: "Menu Management", 18 | component: "Component Element", 19 | form: "Form", 20 | table: "Table", 21 | chart: "Echarts", 22 | defaultTable: "Default Table", 23 | useTable: "UseTable", 24 | VxeTable: "VxeTable", 25 | defaultForm: "Default Form", 26 | useForm: "UseForm", 27 | authority: "Authority", 28 | btnPerm: "Button Permissions", 29 | workflow: "Workflow", 30 | useX6: "Use X6", 31 | qrcode: "Qrcode" 32 | }, 33 | tagsOperations: { 34 | noTitle: "No Title", 35 | reload: "Reload", 36 | closeTag: "Close Tab", 37 | closeLeftTag: "Close The Left Tab", 38 | closeRightTag: "Close The Right Tab", 39 | closeOtherTag: "Close Other Tabs", 40 | closeAllTag: "Close All Tabs" 41 | }, 42 | personalCenter: { 43 | personalCenter: "Personal Center", 44 | changePassword: "Change Password", 45 | loginOut: "Login Out" 46 | }, 47 | size: { 48 | default: "Default", 49 | small: "Compact", 50 | large: "Medium" 51 | }, 52 | common: { 53 | required: "This is required", 54 | lengthRange: "The length should be between {min} and {max}", 55 | notSpace: "Spaces are not allowed", 56 | notSpecialCharacters: "Special characters are not allowed", 57 | inputText: "Please input", 58 | selectText: "Please select", 59 | startTimeText: "Start time", 60 | endTimeText: "End time" 61 | }, 62 | login: { 63 | login: "Login", 64 | username: "Username", 65 | password: "Password", 66 | outDesp: "Are you sure you want to log out?", 67 | confirm: "Confirm", 68 | cancel: "Cancel", 69 | outSuccess: "Successfully logged out", 70 | warn: "Warning", 71 | tabQrLogin: "Qrcode Login", 72 | tips: "A backend management system framework based on Typescript+Vue3+vite+Vueuse" 73 | }, 74 | formDemo: { 75 | input: "Input", 76 | inputNumber: "InputNumber", 77 | default: "Default", 78 | icon: "Icon", 79 | mixed: "Mixed", 80 | password: "Password", 81 | textarea: "Textarea", 82 | remoteSearch: "Remote search", 83 | slot: "Slot", 84 | position: "Position", 85 | autocomplete: "Autocomplete", 86 | select: "Select", 87 | optionSlot: "Option Slot", 88 | selectGroup: "Select Group", 89 | selectV2: "SelectV2", 90 | cascader: "Cascader", 91 | switch: "Switch", 92 | rate: "Rate", 93 | colorPicker: "Color Picker", 94 | transfer: "Transfer", 95 | render: "Render", 96 | radio: "Radio", 97 | radioGroup: "Radio Group", 98 | button: "Button", 99 | checkbox: "Checkbox", 100 | checkboxButton: "Checkbox Button", 101 | checkboxGroup: "Checkbox Group", 102 | slider: "Slider", 103 | datePicker: "Date Picker", 104 | shortcuts: "Shortcuts", 105 | today: "Today", 106 | yesterday: "Yesterday", 107 | aWeekAgo: "A week ago", 108 | week: "Week", 109 | year: "Year", 110 | month: "Month", 111 | dates: "Dates", 112 | daterange: "Date Range", 113 | monthrange: "Month Range", 114 | dateTimePicker: "DateTimePicker", 115 | dateTimerange: "Datetime Range", 116 | timePicker: "Time Picker", 117 | timeSelect: "Time Select", 118 | inputPassword: "input Password", 119 | passwordStrength: "Password Strength", 120 | defaultForm: "All examples", 121 | formDes: 122 | "The secondary encapsulation of form components based on ElementPlus realizes data-driven and supports all Form parameters", 123 | example: "example", 124 | operate: "operate", 125 | change: "Change", 126 | restore: "Restore", 127 | disabled: "Disabled", 128 | disablement: "Disablement", 129 | delete: "Delete", 130 | add: "Add", 131 | setValue: "Set value", 132 | resetValue: "Reset value", 133 | set: "Set", 134 | subitem: "Subitem", 135 | formValidation: "Form validation", 136 | verifyReset: "Verify reset", 137 | form: "Form", 138 | focus: "Focus", 139 | treeSelect: "Tree Select", 140 | showCheckbox: "Show Checkbox", 141 | selectAnyLevel: "Select Any Level", 142 | multiple: "Multiple", 143 | filterable: "Filterable", 144 | customContent: "Custom content", 145 | lazyLoad: "Lazy load", 146 | upload: "Upload", 147 | userAvatar: "User avatar", 148 | iconPicker: "Icon picker", 149 | iAgree: "I agree" 150 | }, 151 | result: { 152 | notfound: "Sorry, the page you visited does not exist." 153 | }, 154 | ...vxeEn 155 | } 156 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue" 2 | import App from "./App.vue" 3 | import { router, setupRouter } from "@/router" 4 | import { setupStore } from "@/store" 5 | import { setupI18n } from "@/plugins/vueI18n" 6 | import { setupVxeTable } from "./plugins/vxeTable" 7 | import { setupAuthDirective } from "./directives/auth" 8 | 9 | import "@/router/asyncRouterHelper" 10 | 11 | import "animate.css" 12 | import "./styles/reset.scss" 13 | import "./styles/index.scss" 14 | 15 | const setupApp = async () => { 16 | const app = createApp(App) 17 | setupI18n(app) 18 | setupRouter(app) 19 | // 在页面显示之前先等待router加载完毕 20 | await router.isReady() 21 | if (process.env.NODE_ENV === "production" && import.meta.env?.VITE_CDN) { 22 | const { setupElementPlus } = await import("./plugins/elementPlus") 23 | setupElementPlus(app) 24 | } 25 | setupStore(app) 26 | setupVxeTable(app) 27 | setupAuthDirective(app) 28 | app.mount("#app") 29 | } 30 | setupApp() 31 | -------------------------------------------------------------------------------- /src/plugins/echarts/index.ts: -------------------------------------------------------------------------------- 1 | import * as echarts from "echarts/core" 2 | 3 | import { BarChart, LineChart, PieChart, MapChart, PictorialBarChart, RadarChart, ScatterChart } from "echarts/charts" 4 | 5 | import { 6 | TitleComponent, 7 | TooltipComponent, 8 | GridComponent, 9 | PolarComponent, 10 | AriaComponent, 11 | ParallelComponent, 12 | LegendComponent, 13 | RadarComponent, 14 | ToolboxComponent, 15 | DataZoomComponent, 16 | VisualMapComponent, 17 | TimelineComponent, 18 | CalendarComponent, 19 | GraphicComponent 20 | } from "echarts/components" 21 | 22 | import { SVGRenderer } from "echarts/renderers" 23 | 24 | echarts.use([ 25 | LegendComponent, 26 | TitleComponent, 27 | TooltipComponent, 28 | GridComponent, 29 | PolarComponent, 30 | AriaComponent, 31 | ParallelComponent, 32 | BarChart, 33 | LineChart, 34 | PieChart, 35 | MapChart, 36 | RadarChart, 37 | SVGRenderer, 38 | PictorialBarChart, 39 | RadarComponent, 40 | ToolboxComponent, 41 | DataZoomComponent, 42 | VisualMapComponent, 43 | TimelineComponent, 44 | CalendarComponent, 45 | GraphicComponent, 46 | ScatterChart 47 | ]) 48 | 49 | export default echarts 50 | -------------------------------------------------------------------------------- /src/plugins/elementPlus/index.ts: -------------------------------------------------------------------------------- 1 | // element plus全局引入部分组件,比如loading,scrollbar,解决cdn pro环境下全局样式失效问题,如果不需要cdn加速,非cdn的按需引入已配置 2 | import { App, Component } from "vue" 3 | import { 4 | ElText, 5 | ElTag, 6 | ElAffix, 7 | ElSkeleton, 8 | ElBreadcrumb, 9 | ElBreadcrumbItem, 10 | ElScrollbar, 11 | ElSubMenu, 12 | ElButton, 13 | ElCol, 14 | ElRow, 15 | ElSpace, 16 | ElDivider, 17 | ElCard, 18 | ElDropdown, 19 | ElDialog, 20 | ElMenu, 21 | ElMenuItem, 22 | ElDropdownItem, 23 | ElDropdownMenu, 24 | ElIcon, 25 | ElInput, 26 | ElForm, 27 | ElFormItem, 28 | ElHeader, 29 | ElContainer, 30 | ElMain, 31 | ElAside, 32 | ElProgress, 33 | ElPopover, 34 | ElPopper, 35 | ElTooltip, 36 | ElDrawer, 37 | ElPagination, 38 | ElAlert, 39 | ElRadio, 40 | ElRadioButton, 41 | ElRadioGroup, 42 | ElDescriptions, 43 | ElDescriptionsItem, 44 | ElBacktop, 45 | ElSwitch, 46 | ElBadge, 47 | ElTabs, 48 | ElTabPane, 49 | ElAvatar, 50 | ElEmpty, 51 | ElCollapse, 52 | ElCollapseItem, 53 | ElTable, 54 | ElTableColumn, 55 | ElLink, 56 | ElColorPicker, 57 | ElSelect, 58 | ElOption, 59 | ElTimeline, 60 | ElTimelineItem, 61 | ElResult, 62 | ElSteps, 63 | ElStep, 64 | ElTree, 65 | ElTreeV2, 66 | ElPopconfirm, 67 | ElCheckbox, 68 | ElCheckboxGroup, 69 | ElConfigProvider, //全局配置 70 | ElLoading, // v-loading 指令 71 | ElInfiniteScroll, // v-infinite-scroll 指令 72 | ElPopoverDirective, // v-popover 指令 73 | ElMessage, // $message 全局属性对象globalProperties 74 | ElMessageBox, // $msgbox、$alert、$confirm、$prompt 全局属性对象globalProperties 75 | ElNotification // $notify 全局属性对象globalProperties 76 | } from "element-plus" 77 | 78 | const components = [ 79 | ElText, 80 | ElTag, 81 | ElAffix, 82 | ElSkeleton, 83 | ElBreadcrumb, 84 | ElBreadcrumbItem, 85 | ElScrollbar, 86 | ElSubMenu, 87 | ElButton, 88 | ElCol, 89 | ElRow, 90 | ElSpace, 91 | ElDivider, 92 | ElCard, 93 | ElDropdown, 94 | ElDialog, 95 | ElMenu, 96 | ElMenuItem, 97 | ElDropdownItem, 98 | ElDropdownMenu, 99 | ElIcon, 100 | ElInput, 101 | ElForm, 102 | ElFormItem, 103 | ElHeader, 104 | ElContainer, 105 | ElMain, 106 | ElAside, 107 | ElProgress, 108 | ElPopover, 109 | ElPopper, 110 | ElTooltip, 111 | ElDrawer, 112 | ElPagination, 113 | ElAlert, 114 | ElRadio, 115 | ElRadioButton, 116 | ElRadioGroup, 117 | ElDescriptions, 118 | ElDescriptionsItem, 119 | ElBacktop, 120 | ElSwitch, 121 | ElBadge, 122 | ElTabs, 123 | ElTabPane, 124 | ElAvatar, 125 | ElEmpty, 126 | ElCollapse, 127 | ElCollapseItem, 128 | ElTree, 129 | ElTreeV2, 130 | ElPopconfirm, 131 | ElCheckbox, 132 | ElCheckboxGroup, 133 | ElTable, 134 | ElTableColumn, 135 | ElLink, 136 | ElColorPicker, 137 | ElSelect, 138 | ElOption, 139 | ElTimeline, 140 | ElTimelineItem, 141 | ElResult, 142 | ElSteps, 143 | ElStep 144 | ] 145 | 146 | const plugins = [ElConfigProvider, ElLoading, ElInfiniteScroll, ElPopoverDirective, ElMessage, ElMessageBox, ElNotification] 147 | 148 | export function setupElementPlus(app: App) { 149 | // 注册组件 150 | components.forEach((component: Component) => { 151 | app.component(component.name, component) 152 | }) 153 | // 全局注册 154 | plugins.forEach(plugin => { 155 | app.use(plugin) 156 | }) 157 | } 158 | -------------------------------------------------------------------------------- /src/plugins/vueI18n/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import { createI18n } from "vue-i18n" 3 | import { useLocaleStoreWithOut } from "@/store/modules/locale" 4 | import type { I18n, I18nOptions } from "vue-i18n" 5 | import { setHtmlLang } from "@/hooks/useLocale" 6 | 7 | export let i18n: ReturnType 8 | const createI18nOptions = async (): Promise => { 9 | const localeStore = useLocaleStoreWithOut() 10 | const locale = localeStore.getCurrentLocale 11 | const localeMap = localeStore.getLocaleMap 12 | const defaultLocal = await import(`../../locales/lang/${locale.lang === "en" ? "en" : "cn"}.ts`) 13 | const message = defaultLocal.default ?? {} 14 | 15 | setHtmlLang(locale) 16 | 17 | localeStore.setCurrentLocale({ 18 | lang: locale.lang 19 | }) 20 | 21 | return { 22 | legacy: false, 23 | locale: locale.lang, 24 | fallbackLocale: locale.lang, 25 | messages: { 26 | [locale.lang]: message 27 | }, 28 | availableLocales: localeMap.map(v => v.lang), 29 | sync: true, 30 | silentTranslationWarn: true, 31 | missingWarn: false, 32 | silentFallbackWarn: true 33 | } 34 | } 35 | 36 | export const setupI18n = async (app: App) => { 37 | const options = await createI18nOptions() 38 | i18n = createI18n(options) as I18n 39 | app.use(i18n) 40 | } 41 | -------------------------------------------------------------------------------- /src/plugins/vxeTable/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import { i18n } from "@/plugins/vueI18n" 3 | import VXETable from "vxe-table" 4 | export function setupVxeTable(app: App) { 5 | VXETable.config({ 6 | // @ts-expect-error 详情请见https://vxetable.cn/#/table/start/i18n 7 | i18n: (key, args) => i18n.global.t(key, args) 8 | }) 9 | app.use(VXETable) 10 | } 11 | -------------------------------------------------------------------------------- /src/router/asyncRouterHelper.ts: -------------------------------------------------------------------------------- 1 | import { unref, computed } from "vue" 2 | import type { RouteRecordRaw } from "vue-router" 3 | import { router, staticRouter } from "@/router" 4 | import { useRoutersStoreWithOut } from "@/store/modules/router" 5 | import { useAppStoreWithOut } from "@/store/modules/app" 6 | import { generateDynamicRouters } from "@/utils/routerUtils" 7 | import { useNProgress } from "@/hooks/useProgress" 8 | import { useStorage } from "@/hooks/useStorage" 9 | import { menuWhiteList } from "@/constants" 10 | 11 | const { start, done } = useNProgress() 12 | const routersStore = useRoutersStoreWithOut() 13 | const appStore = useAppStoreWithOut() 14 | const { getStorage } = useStorage("localStorage") 15 | 16 | const mode = computed(() => appStore.getRouterMode) 17 | 18 | const cashRoutes = routersStore.getRouters.length ? routersStore.getRouters : staticRouter 19 | // 创建路由 20 | export const createRouter = (asyncRouters: RouteRecordRaw[] = cashRoutes) => { 21 | let asyncFinalRouter = [] as RouteRecordRaw[] 22 | let newAsyncFinalRouter = [] as RouteRecordRaw[] 23 | asyncFinalRouter = generateDynamicRouters(asyncRouters, unref(mode), routersStore.user) 24 | 25 | newAsyncFinalRouter = asyncFinalRouter.filter((route: RouteRecordRaw) => { 26 | return !menuWhiteList.includes(route.name as string) 27 | }) 28 | routersStore.setMenu(newAsyncFinalRouter) 29 | router.getRoutes().map((v: RouteRecordRaw) => { 30 | router.removeRoute(v.name) 31 | }) 32 | asyncFinalRouter.map((v: RouteRecordRaw) => { 33 | router.addRoute(v) 34 | }) 35 | } 36 | 37 | // 如果未登录过默认不执行,实际可换成token (本地无user缓存和pinia中无路由表视为未登录) 38 | const isAuth = computed(() => (getStorage("user") || routersStore.getRouters.length ? true : false)) 39 | if (unref(isAuth)) { 40 | createRouter(cashRoutes) 41 | } 42 | 43 | router.beforeEach((to, from, next) => { 44 | start() 45 | if (to.path !== "/login" && !unref(isAuth)) { 46 | next("/login") // 如果未验证,跳转到登录页 47 | } else { 48 | next() // 继续路由导航 49 | } 50 | }) 51 | 52 | router.afterEach(() => { 53 | done() 54 | }) 55 | -------------------------------------------------------------------------------- /src/store/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue" 2 | import { createPinia } from "pinia" 3 | import piniaPersist from "pinia-plugin-persist" 4 | 5 | const store = createPinia() 6 | 7 | store.use(piniaPersist) 8 | 9 | export const setupStore = (app: App) => { 10 | app.use(store) 11 | } 12 | 13 | export { store } 14 | -------------------------------------------------------------------------------- /src/store/modules/locale.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | import { useStorage } from "@/hooks/useStorage" 3 | import { store } from "../index" 4 | import zhCn from "element-plus/es/locale/lang/zh-cn" 5 | import en from "element-plus/es/locale/lang/en" 6 | 7 | const { setStorage, getStorage } = useStorage() 8 | 9 | const elLocaleMap = { 10 | "zh-CN": zhCn, 11 | en: en 12 | } 13 | interface localeState { 14 | currentLocale: CurrentLocale 15 | localeMap: LocaleMap[] 16 | } 17 | 18 | export const useLocaleStore = defineStore("locale", { 19 | state: (): localeState => { 20 | return { 21 | currentLocale: { 22 | lang: getStorage("lang") || "zh-CN", 23 | elLang: elLocaleMap[getStorage("lang") || "zh-CN"] 24 | }, 25 | localeMap: [ 26 | { 27 | lang: "zh-CN", 28 | name: "简体中文" 29 | }, 30 | { 31 | lang: "en", 32 | name: "English" 33 | } 34 | ] 35 | } 36 | }, 37 | getters: { 38 | getCurrentLocale(): CurrentLocale { 39 | return this.currentLocale 40 | }, 41 | getLocaleMap(): LocaleMap[] { 42 | return this.localeMap 43 | } 44 | }, 45 | actions: { 46 | setCurrentLocale(localeMap: CurrentLocale) { 47 | this.currentLocale.lang = localeMap?.lang 48 | this.currentLocale.elLang = elLocaleMap[localeMap?.lang] 49 | setStorage("lang", localeMap?.lang) 50 | } 51 | } 52 | }) 53 | 54 | export const useLocaleStoreWithOut = () => { 55 | return useLocaleStore(store) 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/router.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | import { store } from "../index" 3 | import type { RouteRecordRaw } from "vue-router" 4 | import { useStorage } from "@/hooks/useStorage" 5 | 6 | /** 7 | * @param user 用户的登录信息 8 | * @param routers 动态路由且为请求的原始路由,前端处理之前 9 | * @param menu 菜单所需要读取的路由,前端处理之后的路由 10 | */ 11 | type RoutersState = { 12 | user: Recordable 13 | routers: RouteRecordRaw[] 14 | menu: RouteRecordRaw[] 15 | } 16 | 17 | const { setStorage, getStorage } = useStorage("localStorage") 18 | 19 | export const useRoutersStore = defineStore("routers", { 20 | state: (): RoutersState => { 21 | return { 22 | user: getStorage("user") || {}, 23 | routers: getStorage("asyncRouters") || [], 24 | menu: getStorage("menu") || [] 25 | } 26 | }, 27 | getters: { 28 | getUser(): Recordable { 29 | return this.user 30 | }, 31 | getRouters(): RouteRecordRaw[] { 32 | return this.routers 33 | }, 34 | getMenu(): RouteRecordRaw[] { 35 | return this.menu 36 | } 37 | }, 38 | actions: { 39 | setUser(user: Recordable) { 40 | this.user = user 41 | setStorage("user", user) 42 | }, 43 | setRouters(routers: RouteRecordRaw[]) { 44 | this.routers = routers 45 | setStorage("asyncRouters", this.routers) 46 | }, 47 | setMenu(menu: RouteRecordRaw[]) { 48 | this.menu = menu 49 | setStorage("menu", this.menu) 50 | } 51 | } 52 | }) 53 | 54 | export const useRoutersStoreWithOut = () => { 55 | return useRoutersStore(store) 56 | } 57 | -------------------------------------------------------------------------------- /src/store/modules/tags.ts: -------------------------------------------------------------------------------- 1 | import { defineStore } from "pinia" 2 | import { useStorage } from "@/hooks/useStorage" 3 | import { RouteRecordRaw } from "vue-router" 4 | import { toRaw } from "vue" 5 | import { store } from "../index" 6 | import { t } from "@/hooks/useLocale" 7 | 8 | const { getStorage, setStorage } = useStorage("localStorage") 9 | 10 | type tagsState = { 11 | tagsList: TagsList[] 12 | } 13 | 14 | export const useTagsStore = defineStore("tags", { 15 | state: (): tagsState => { 16 | return { 17 | tagsList: getStorage("tagsList") || [] 18 | } 19 | }, 20 | getters: { 21 | getTagsList(): TagsList[] { 22 | return this.tagsList 23 | }, 24 | // 获取当前选中的tag 25 | getCurrentTag(): TagsList { 26 | return this.tagsList.filter((v: TagsList) => v.current)[0] 27 | } 28 | }, 29 | actions: { 30 | // 设置整个tagsList 31 | setTagsList(tagsList: TagsList[]) { 32 | this.tagsList = tagsList 33 | setStorage("tagsList", tagsList) 34 | }, 35 | // 添加tags 36 | addTags(route: RouteRecordRaw) { 37 | if (this.tagsList.some((v: TagsList) => v.path === route.path)) { 38 | this.updateTagsByRoute(route) 39 | return 40 | } 41 | // 添加之前先干掉所有样式 42 | this.tagsList.forEach((v: TagsList) => Object.assign(v, { current: false, isFixed: false })) 43 | this.tagsList.push( 44 | Object.assign({ 45 | current: true, 46 | icon: route.meta.icon, 47 | isFixed: true, 48 | path: route.path, 49 | title: route.meta.title ?? t("tagsOperations.noTitle") 50 | }) 51 | ) 52 | setStorage("tagsList", this.tagsList) 53 | }, 54 | // 根据当前路由更新tags 55 | updateTagsByRoute(route: RouteRecordRaw) { 56 | this.tagsList.forEach((v: TagsList) => Object.assign(v, { current: false, isFixed: false })) 57 | for (let v of this.tagsList) { 58 | if (v.path === route.path && v.path !== "/login" && v.name !== "Redirect") { 59 | v = Object.assign(v, { 60 | current: true, 61 | icon: route.meta.icon, 62 | isFixed: true, 63 | path: route.path, 64 | title: route.meta.title ?? t("tagsOperations.noTitle") 65 | }) 66 | break 67 | } 68 | } 69 | setStorage("tagsList", this.tagsList) 70 | }, 71 | // 根据传入Tags更新tags 72 | updateTagsByTags(tag: TagsList) { 73 | this.tagsList.forEach((v: TagsList) => Object.assign(v, { current: false, isFixed: false })) 74 | for (let v of this.tagsList) { 75 | if (v.path === tag.path) { 76 | v = Object.assign(v, tag, { current: true, isFixed: true }) 77 | break 78 | } 79 | } 80 | setStorage("tagsList", this.tagsList) 81 | }, 82 | // 根据传入Tags删除tags 83 | delTagsByTags(tag: TagsList): string { 84 | let index = this.tagsList.findIndex((v: TagsList, index: number) => (v.path === tag.path ? index : -1)) 85 | for (let [i, v] of this.tagsList.entries()) { 86 | if (tag.path === v.path) { 87 | this.tagsList.splice(i, 1) 88 | break 89 | } 90 | } 91 | // 删完立马缓存(缓存交给update方法) 92 | // setStorage("tagsList", this.tagsList) 93 | // 接下来解决路由跳转问题,默认跳转到tags在 tagsList的前一个,找不到则为最后一个tag(前一个添加的tag,因为template中已经加以限制,能显示删除的tagsList length最少为2) 94 | let path = index > 0 ? this.tagsList[index - 1].path : index === 0 ? this.tagsList[this.tagsList.length - 1].path : "/404" 95 | this.updateTagsByTags(toRaw(this.tagsList.filter((v: TagsList) => v.path === path)[0])) 96 | return path 97 | }, 98 | // 关闭当前标签页 99 | delCurrentTags(): string { 100 | return this.delTagsByTags(toRaw(this.getCurrentTag)) 101 | }, 102 | // 关闭全部标签页 103 | delOtherTags() { 104 | const currentTag = toRaw(this.getCurrentTag) as TagsList 105 | this.tagsList.splice(0, this.tagsList.length) 106 | this.tagsList.push(currentTag) 107 | setStorage("tagsList", this.tagsList) 108 | }, 109 | // 关闭左侧标签页 110 | delLeftTags() { 111 | const currentTag = toRaw(this.getCurrentTag) as TagsList 112 | for (let [i, v] of toRaw(this.getTagsList).entries()) { 113 | if (v.path === currentTag.path) { 114 | this.tagsList.splice(0, i) 115 | break 116 | } 117 | } 118 | setStorage("tagsList", this.tagsList) 119 | }, 120 | // 关闭右侧标签页 121 | delRightTags() { 122 | const currentTag = toRaw(this.getCurrentTag) as TagsList 123 | for (let [i, v] of toRaw(this.getTagsList).entries()) { 124 | if (v.path === currentTag.path) { 125 | this.tagsList.splice(i + 1, this.tagsList.length) 126 | break 127 | } 128 | } 129 | setStorage("tagsList", this.tagsList) 130 | }, 131 | delAllTags() { 132 | this.tagsList.splice(0, this.tagsList.length) 133 | setStorage("tagsList", this.tagsList) 134 | } 135 | } 136 | }) 137 | 138 | export const useTagsStoreWithOut = () => { 139 | return useTagsStore(store) 140 | } 141 | -------------------------------------------------------------------------------- /src/styles/element/dark.scss: -------------------------------------------------------------------------------- 1 | $--colors: ( 2 | "primary": ( 3 | "base": #3a6ee8 4 | ) 5 | ); 6 | 7 | @forward "element-plus/theme-chalk/src/dark/var.scss" with ( 8 | $colors: $--colors 9 | ); 10 | -------------------------------------------------------------------------------- /src/styles/element/index.scss: -------------------------------------------------------------------------------- 1 | // 默认element的主题 2 | // $--colors: ( 3 | // "primary": ( 4 | // "base": green 5 | // ), 6 | // "success": ( 7 | // "base": #21ba45 8 | // ), 9 | // "warning": ( 10 | // "base": #f2711c 11 | // ), 12 | // "danger": ( 13 | // "base": #db2828 14 | // ), 15 | // "error": ( 16 | // "base": #db2828 17 | // ), 18 | // "info": ( 19 | // "base": #42b8dd 20 | // ) 21 | // ); 22 | 23 | // 添加一个custom namespace, 默认是 'el' 24 | @forward "element-plus/theme-chalk/src/mixins/config.scss" with ( 25 | $namespace: "el" 26 | ); 27 | 28 | // 设置element默认样式,这里设置的是按钮padding和border-radius 29 | @forward "element-plus/theme-chalk/src/common/var.scss" with ( 30 | $button-padding-horizontal: ( 31 | "default": 20px 32 | ), 33 | $button-border-radius: ( 34 | "default": 4px 35 | ) 36 | ); 37 | 38 | @use "./dark.scss"; 39 | -------------------------------------------------------------------------------- /src/styles/index.scss: -------------------------------------------------------------------------------- 1 | // 导入暗黑模式 2 | @use "element-plus/theme-chalk/src/dark/css-vars.scss" as *; 3 | 4 | // 全局样式 5 | @use "var.scss"; 6 | -------------------------------------------------------------------------------- /src/styles/mixin.scss: -------------------------------------------------------------------------------- 1 | @mixin flex($justify: space-between, $align: center, $wrap: nowrap) { 2 | display: flex; 3 | flex-wrap: $wrap; 4 | align-items: $align; 5 | justify-content: $justify !important; 6 | } 7 | 8 | @mixin border($color) { 9 | border: 1px solid $color; 10 | } 11 | 12 | @mixin cursor($pointer: pointer) { 13 | cursor: $pointer; 14 | } 15 | 16 | @mixin transition($attr: color) { 17 | transition: $attr 0.3s; 18 | } 19 | -------------------------------------------------------------------------------- /src/styles/namespace.scss: -------------------------------------------------------------------------------- 1 | // VXAdmin css前缀 2 | $vx-prefix: "vx"; 3 | 4 | // element plus css前缀 5 | $el-prefix: "el"; 6 | -------------------------------------------------------------------------------- /src/styles/reset.scss: -------------------------------------------------------------------------------- 1 | // 重置样式 2 | *, 3 | ::before, 4 | ::after { 5 | box-sizing: border-box; 6 | border-color: currentColor; 7 | border-style: solid; 8 | border-width: 0; 9 | } 10 | #app { 11 | width: 100%; 12 | height: 100%; 13 | } 14 | html { 15 | box-sizing: border-box; 16 | width: 100%; 17 | height: 100%; 18 | line-height: 1.5; 19 | tab-size: 4; 20 | text-size-adjust: 100%; 21 | } 22 | body { 23 | width: 100%; 24 | height: 100%; 25 | margin: 0; 26 | font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", "微软雅黑", Arial, sans-serif; 27 | line-height: inherit; 28 | -moz-osx-font-smoothing: grayscale; 29 | -webkit-font-smoothing: antialiased; 30 | text-rendering: optimizelegibility; 31 | } 32 | hr { 33 | height: 0; 34 | color: inherit; 35 | border-top-width: 1px; 36 | } 37 | abbr:where([title]) { 38 | text-decoration: underline dotted; 39 | } 40 | a { 41 | color: inherit; 42 | text-decoration: inherit; 43 | } 44 | b, 45 | strong { 46 | font-weight: bolder; 47 | } 48 | code, 49 | kbd, 50 | samp, 51 | pre { 52 | font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; 53 | font-size: 1em; 54 | } 55 | small { 56 | font-size: 80%; 57 | } 58 | sub, 59 | sup { 60 | position: relative; 61 | font-size: 75%; 62 | line-height: 0; 63 | vertical-align: baseline; 64 | } 65 | sub { 66 | bottom: -0.25em; 67 | } 68 | sup { 69 | top: -0.5em; 70 | } 71 | table { 72 | text-indent: 0; 73 | border-collapse: collapse; 74 | border-color: inherit; 75 | } 76 | button, 77 | input, 78 | optgroup, 79 | select, 80 | textarea { 81 | padding: 0; 82 | margin: 0; 83 | font-family: inherit; 84 | font-size: 100%; 85 | line-height: inherit; 86 | color: inherit; 87 | } 88 | button, 89 | select { 90 | text-transform: none; 91 | } 92 | button, 93 | [type="button"], 94 | [type="reset"], 95 | [type="submit"] { 96 | background-image: none; 97 | } 98 | :-moz-focusring { 99 | outline: auto; 100 | } 101 | :-moz-ui-invalid { 102 | box-shadow: none; 103 | } 104 | progress { 105 | vertical-align: baseline; 106 | } 107 | ::-webkit-inner-spin-button, 108 | ::-webkit-outer-spin-button { 109 | height: auto; 110 | } 111 | [type="search"] { 112 | outline-offset: -2px; 113 | } 114 | ::-webkit-file-upload-button { 115 | font: inherit; 116 | } 117 | summary { 118 | display: list-item; 119 | } 120 | blockquote, 121 | dl, 122 | dd, 123 | h1, 124 | h2, 125 | h3, 126 | h4, 127 | h5, 128 | h6, 129 | hr, 130 | figure, 131 | p, 132 | pre { 133 | margin: 0; 134 | } 135 | fieldset { 136 | padding: 0; 137 | margin: 0; 138 | } 139 | legend { 140 | padding: 0; 141 | } 142 | ol, 143 | ul, 144 | menu { 145 | padding: 0; 146 | margin: 0; 147 | list-style: none; 148 | } 149 | textarea { 150 | resize: vertical; 151 | } 152 | input::placeholder, 153 | textarea::placeholder { 154 | color: #9ca3af; 155 | opacity: 1; 156 | } 157 | button, 158 | [role="button"] { 159 | cursor: pointer; 160 | } 161 | :disabled { 162 | cursor: default; 163 | } 164 | img, 165 | svg, 166 | video, 167 | canvas, 168 | audio, 169 | iframe, 170 | embed, 171 | object { 172 | display: block; 173 | } 174 | img, 175 | video { 176 | max-width: 100%; 177 | height: auto; 178 | } 179 | [hidden] { 180 | display: none; 181 | } 182 | .dark { 183 | color-scheme: dark; 184 | } 185 | label { 186 | font-weight: 700; 187 | } 188 | *, 189 | *::before, 190 | *::after { 191 | box-sizing: inherit; 192 | } 193 | a:focus, 194 | a:active { 195 | outline: none; 196 | } 197 | a, 198 | a:focus, 199 | a:hover { 200 | color: inherit; 201 | text-decoration: none; 202 | cursor: pointer; 203 | } 204 | div:focus { 205 | outline: none; 206 | } 207 | .clearfix { 208 | &::after { 209 | display: block; 210 | height: 0; 211 | clear: both; 212 | font-size: 0; 213 | visibility: hidden; 214 | content: " "; 215 | } 216 | } 217 | 218 | /* 隐藏滚动条 */ 219 | body::-webkit-scrollbar { 220 | width: 0; 221 | } 222 | body::-webkit-scrollbar-track { 223 | background-color: transparent; 224 | } 225 | body::-webkit-scrollbar-thumb { 226 | background-color: transparent; 227 | } 228 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | // 修改全局变量方法 2 | export const setCssVar = (prop: string, val: any, dom = document.documentElement) => { 3 | dom.style.setProperty(prop, val) 4 | } 5 | // 将驼峰css变量转换为普通格式css变量 例如:elPrimaryColor --> --el-primary-color 6 | export const toCssVariable = (variable: string): string => { 7 | return `--${variable.replace(/[A-Z]/g, "-$&").toLowerCase()}` 8 | } 9 | // 静态资源路径转换(生产环境不生效,暂时弃用) 10 | export const getStaticSource = (sourceUrl: string): string | undefined => { 11 | // 当前import.meta.url是当前index.ts的路径,需要提取此url src及以前的部分 12 | const parseUrl = new URL(import.meta.url) 13 | 14 | const baseUrl = parseUrl.origin + parseUrl.pathname.split("/src")[0] 15 | 16 | // 正则表达式匹配规则 17 | const regex: RegExp = /^(@\/|\.{0,2}\/|[a-zA-Z]+:\/\/).+\.(jpg|jpeg|png|gif)$/ 18 | 19 | if (regex.test(sourceUrl)) { 20 | return new URL(sourceUrl.replace("@/", "src/"), baseUrl).href 21 | } 22 | 23 | return void 0 24 | } 25 | 26 | // Map 数据结构转obj 27 | export const transMapToObject = (map: Map) => { 28 | const target = {} as Recordable 29 | map.forEach((value, key) => { 30 | target[key] = value 31 | }) 32 | return target 33 | } 34 | 35 | // 刷新页面 36 | export const reload = () => { 37 | window.location.reload() 38 | } 39 | // 驼峰转横杠 40 | export const humpToDash = (str: string): string => { 41 | return str.replace(/([A-Z])/g, "-$1").toLowerCase() 42 | } 43 | // 首字母大写 44 | export function firstUpperCase(str: string) { 45 | return str.toLowerCase().replace(/( |^)[a-z]/g, L => L.toUpperCase()) 46 | } 47 | 48 | /** 49 | * 查找数组对象的某个下标 50 | * @param {Array} ary 查找的数组 51 | * @param {Functon} fn 判断的方法 52 | */ 53 | export const findIndex = (ary: Array, fn: Fn): number => { 54 | if (ary.findIndex) { 55 | return ary.findIndex(fn) 56 | } 57 | let index = -1 58 | ary.some((item: T, i: number, ary: Array) => { 59 | const ret: T = fn(item, i, ary) 60 | if (ret) { 61 | index = i 62 | return ret 63 | } 64 | }) 65 | return index 66 | } 67 | -------------------------------------------------------------------------------- /src/utils/is.ts: -------------------------------------------------------------------------------- 1 | const toString = Object.prototype.toString 2 | 3 | export const is = (val: unknown, type: string) => { 4 | return toString.call(val) === `[object ${type}]` 5 | } 6 | 7 | export const isDef = (val?: T): val is T => { 8 | return typeof val !== "undefined" 9 | } 10 | 11 | export const isUnDef = (val?: T): val is T => { 12 | return !isDef(val) 13 | } 14 | 15 | export const isObject = (val: any): val is Record => { 16 | return val !== null && is(val, "Object") 17 | } 18 | 19 | export const isEmpty = (val: T): val is T => { 20 | if (isArray(val) || isString(val)) { 21 | return val.length === 0 22 | } 23 | 24 | if (val instanceof Map || val instanceof Set) { 25 | return val.size === 0 26 | } 27 | 28 | if (isObject(val)) { 29 | return Object.keys(val).length === 0 30 | } 31 | 32 | return false 33 | } 34 | 35 | export const isDate = (val: unknown): val is Date => { 36 | return is(val, "Date") 37 | } 38 | 39 | export const isNull = (val: unknown): val is null => { 40 | return val === null 41 | } 42 | 43 | export const isNullAndUnDef = (val: unknown): val is null | undefined => { 44 | return isUnDef(val) && isNull(val) 45 | } 46 | 47 | export const isNullOrUnDef = (val: unknown): val is null | undefined => { 48 | return isUnDef(val) || isNull(val) 49 | } 50 | 51 | export const isNumber = (val: unknown): val is number => { 52 | return is(val, "Number") 53 | } 54 | 55 | export const isPromise = (val: unknown): val is Promise => { 56 | return is(val, "Promise") && isObject(val) && isFunction(val.then) && isFunction(val.catch) 57 | } 58 | 59 | export const isString = (val: unknown): val is string => { 60 | return is(val, "String") 61 | } 62 | 63 | export const isFunction = (val: unknown): val is Function => { 64 | return typeof val === "function" 65 | } 66 | 67 | export const isBoolean = (val: unknown): val is boolean => { 68 | return is(val, "Boolean") 69 | } 70 | 71 | export const isRegExp = (val: unknown): val is RegExp => { 72 | return is(val, "RegExp") 73 | } 74 | 75 | export const isArray = (val: any): val is Array => { 76 | return val && Array.isArray(val) 77 | } 78 | 79 | export const isWindow = (val: any): val is Window => { 80 | return typeof window !== "undefined" && is(val, "Window") 81 | } 82 | 83 | export const isElement = (val: unknown): val is Element => { 84 | return isObject(val) && !!val.tagName 85 | } 86 | 87 | export const isMap = (val: unknown): val is Map => { 88 | return is(val, "Map") 89 | } 90 | 91 | export const isServer = typeof window === "undefined" 92 | 93 | export const isClient = !isServer 94 | 95 | export const isUrl = (path: string): boolean => { 96 | const reg = 97 | /(((^https?:(?:\/\/)?)(?:[-:&=\+\$,\w]+@)?[A-Za-z0-9.-]+(?::\d+)?|(?:www.|[-:&=\+\$,\w]+@)[A-Za-z0-9.-]+)((?:\/[\+~%\/.\w-_]*)?\??(?:[-\+=&%@.\w_]*)#?(?:[\w]*))?)$/ 98 | return reg.test(path) 99 | } 100 | 101 | export const isDark = (): boolean => { 102 | return window.matchMedia("(prefers-color-scheme: dark)").matches 103 | } 104 | 105 | // 是否是图片链接 106 | export const isImgPath = (path: string): boolean => { 107 | return /(https?:\/\/|data:image\/).*?\.(png|jpg|jpeg|gif|svg|webp|ico)/gi.test(path) 108 | } 109 | 110 | export const isEmptyVal = (val: any): boolean => { 111 | return val === "" || val === null || val === undefined 112 | } 113 | -------------------------------------------------------------------------------- /src/utils/theme.ts: -------------------------------------------------------------------------------- 1 | // 此文件代表暗黑模式和正常模式配置,appStore读取此文件中的配置信息,建议与var.scss中保持一致 2 | const normalTheme: ThemeTypes = { 3 | themeColor: "#3a6ee8", 4 | elColorPrimary: "#3a6ee8", 5 | themeTextColor: "#252525", 6 | themeBgColor: "#f5f7f9", 7 | themeDivColor: "#fff", 8 | dividerColor: "#dbdcdd", 9 | leftMenuBgColor: "#001529", 10 | leftMenuBgLightColor: "#0f2438", 11 | leftMenuBgActiveColor: "var(--el-color-primary)", 12 | leftMenuTextColor: "#bfcbd9", 13 | leftMenuTextActiveColor: "#ffffff", 14 | leftMenuCollapseBgActiveColor: "var(--el-color-primary)" 15 | } 16 | 17 | const darkTheme: ThemeTypes = { 18 | themeColor: "#3a6ee8", 19 | elColorPrimary: "#3a6ee8", 20 | themeTextColor: "#fff", 21 | themeBgColor: "#1b1b1f", 22 | themeDivColor: "#2e2e32", 23 | dividerColor: "#4C4D4F", 24 | leftMenuBgColor: "#191b24", 25 | leftMenuBgLightColor: "#282a33", 26 | leftMenuBgActiveColor: "var(--el-color-primary)", 27 | leftMenuTextColor: "#bfcbd9", 28 | leftMenuTextActiveColor: "#ffffff", 29 | leftMenuCollapseBgActiveColor: "var(--el-color-primary)" 30 | } 31 | 32 | export { normalTheme, darkTheme } 33 | -------------------------------------------------------------------------------- /src/utils/tsxUtils.ts: -------------------------------------------------------------------------------- 1 | import { Slots } from "vue" 2 | import { isFunction } from "./is" 3 | 4 | export const getSlot = (slots: Slots, slot = "default", data?: Recordable) => { 5 | // Reflect.has 判断一个对象是否存在某个属性 6 | if (!slots || !Reflect.has(slots, slot)) { 7 | return null 8 | } 9 | const slotFn = slots[slot] 10 | if (!slotFn || !isFunction(slotFn)) return null 11 | return slotFn(data) 12 | } 13 | -------------------------------------------------------------------------------- /src/views/Authority/ButtonPermissions.vue: -------------------------------------------------------------------------------- 1 | 26 | 64 | -------------------------------------------------------------------------------- /src/views/Component/Echarts/Echarts.vue: -------------------------------------------------------------------------------- 1 | 67 | 86 | -------------------------------------------------------------------------------- /src/views/Component/Form/DefaultForm.vue: -------------------------------------------------------------------------------- 1 | 103 | 108 | -------------------------------------------------------------------------------- /src/views/Component/Qrcode/Qrcode.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 65 | 66 | 67 | -------------------------------------------------------------------------------- /src/views/Component/Table/DefaultTable.vue: -------------------------------------------------------------------------------- 1 | 92 | 125 | 132 | -------------------------------------------------------------------------------- /src/views/Dashboard/Workplace.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .workplace-header { 3 | width: 100%; 4 | 5 | @include flex(space-between, center, nowrap); 6 | } 7 | .header-left { 8 | &__avatar { 9 | min-width: 50px; 10 | } 11 | &__tips { 12 | width: fit-content; 13 | margin-left: 15px; 14 | & > .tips-greeting { 15 | font-size: 18px; 16 | 17 | @include flex(flex-start, center, nowrap); 18 | } 19 | & > .tips-weather { 20 | font-size: 12px; 21 | 22 | @include flex(flex-start, center, nowrap); 23 | } 24 | } 25 | 26 | @include flex(flex-start, center, nowrap); 27 | } 28 | .header-right { 29 | & > .static { 30 | margin-right: 20px; 31 | } 32 | 33 | @include flex(flex-end, center, nowrap); 34 | } 35 | .static-title { 36 | & > span { 37 | margin-left: 5px; 38 | } 39 | 40 | @include flex(flex-start, center, nowrap); 41 | } 42 | .workplace-module { 43 | align-content: flex-start; 44 | .module-box { 45 | flex-direction: column; 46 | & > span { 47 | margin-top: 10px; 48 | 49 | @include cursor(default); 50 | } 51 | 52 | @include flex(center, center, nowrap); 53 | } 54 | 55 | @include flex(space-between, center, wrap); 56 | } 57 | .module-card { 58 | width: calc((100% - 120px) / 5); 59 | min-width: 150px; 60 | margin-top: 20px; 61 | } 62 | .workplace-active { 63 | margin-top: 20px; 64 | 65 | @include flex(space-between, center, nowrap); 66 | .active-card { 67 | width: 49%; 68 | .member-list { 69 | height: 50px; 70 | border-bottom: 1px solid var(--divider-color); 71 | &__left { 72 | @include flex(flex-start, center, nowrap); 73 | } 74 | 75 | @include flex(space-between, center, nowrap); 76 | } 77 | } 78 | } 79 | .user-message { 80 | flex-direction: column; 81 | margin-left: 16px; 82 | .username { 83 | font-size: 15px; 84 | color: var(--theme-text-color); 85 | 86 | @include cursor(default); 87 | } 88 | &__detail { 89 | .scr { 90 | font-size: 12px; 91 | } 92 | .time { 93 | margin-left: 20px; 94 | font-size: 12px; 95 | } 96 | 97 | @include flex(flex-start, center, nowrap); 98 | } 99 | 100 | @include flex(flex-start, flex-start, nowrap); 101 | } 102 | 103 | @media (width <= 768px) { 104 | .workplace-header { 105 | flex-direction: column; 106 | width: 100%; 107 | 108 | @include flex(flex-start, center, nowrap); 109 | } 110 | } 111 | 112 | @media (width <= 835px) { 113 | .module-card { 114 | width: 48%; 115 | } 116 | .workplace-active { 117 | align-content: flex-start; 118 | 119 | @include flex(space-between, center, wrap); 120 | .active-card { 121 | width: 100%; 122 | margin-top: 20px; 123 | } 124 | } 125 | } 126 | -------------------------------------------------------------------------------- /src/views/Error/404.vue: -------------------------------------------------------------------------------- 1 | 8 | 17 | -------------------------------------------------------------------------------- /src/views/Login/Login.scss: -------------------------------------------------------------------------------- 1 | @import "@/styles/mixin.scss"; 2 | .vx-login { 3 | height: 100vh; 4 | background-color: var(--theme-div-color); 5 | 6 | @include transition(all); 7 | &__avatar { 8 | @include flex(flex-end, center, nowrap); 9 | } 10 | &__main { 11 | width: 100%; 12 | .loginBox { 13 | @include transition(all); 14 | &__title { 15 | // margin-bottom: 5px; 16 | & > img { 17 | width: 45px; 18 | height: 45px; 19 | } 20 | & > span { 21 | position: relative; 22 | z-index: 0; 23 | margin-left: 16px; 24 | font-size: 33px; 25 | font-weight: 600; 26 | &::before { 27 | position: absolute; 28 | z-index: -1; 29 | font-size: 33px; 30 | font-weight: 600; 31 | color: #cccccc; 32 | content: "VxAdmin"; 33 | filter: blur(3px); 34 | transform: skew(50deg) scaleY(0.5) translate(-13px, 11px); 35 | } 36 | } 37 | 38 | @include flex(center, center, nowrap); 39 | } 40 | &__desp { 41 | margin-block: 12px 20px; 42 | font-size: 14px; 43 | color: var(--theme-text-color); 44 | 45 | @include transition(all); 46 | } 47 | &__container { 48 | max-width: 375px; 49 | } 50 | &__tabs { 51 | width: inherit; 52 | .qrcode-login { 53 | @include flex(center, center, nowrap); 54 | width: 400px; 55 | } 56 | } 57 | 58 | flex-direction: column; 59 | 60 | @include flex(flex-end, center, nowrap); 61 | } 62 | 63 | @include flex(center, flex-start, nowrap); 64 | } 65 | } 66 | -------------------------------------------------------------------------------- /src/views/Login/Login.vue: -------------------------------------------------------------------------------- 1 | 25 | 61 | 64 | -------------------------------------------------------------------------------- /src/views/Login/components/LoginForm.vue: -------------------------------------------------------------------------------- 1 | 111 | 114 | -------------------------------------------------------------------------------- /src/views/Redirect/Redirect.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 53 | -------------------------------------------------------------------------------- /src/views/System/Menu.vue: -------------------------------------------------------------------------------- 1 | 133 | 154 | -------------------------------------------------------------------------------- /src/views/Workflow/useAntvX6.vue: -------------------------------------------------------------------------------- 1 | 179 | 184 | 190 | -------------------------------------------------------------------------------- /src/vite-env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "esnext", // 生成代码的 ECMAScript 目标版本 4 | "module": "esnext", // 生成代码的模块标准 5 | "moduleResolution": "node", // 模块解析策略 6 | "strict": false, // 启用所有严格类型检查 7 | "jsx": "preserve", // 在 .tsx 文件里支持 JSX: 'preserve', 'react-native', or 'react' 8 | "importHelpers": true, // 通过 tslib 引入辅助工具函数 9 | "experimentalDecorators": true, // 启用实验性的装饰器特性 10 | "strictFunctionTypes": false, // 严格检查函数的类型 11 | "skipLibCheck": true, // 是否跳过检查库文件。 12 | "esModuleInterop": true, // 通过创建命名空间实现 CommonJS 兼容性 13 | "isolatedModules": true, // 控制是否将每个文件作为单独的模块处理。 14 | "allowSyntheticDefaultImports": true, 15 | "forceConsistentCasingInFileNames": true, 16 | "sourceMap": true, // 是否生成 .map 文件 17 | "baseUrl": ".", // 解析使用非相对路径导入模块时的基地址 18 | "allowJs": false, // 是否编译 JS 文件 19 | "resolveJsonModule": true, // 自动解析JSON文件 20 | "checkJs": false, // 是否在 JS 文件中报告错误 不处理js的错误 21 | "lib": ["dom", "esnext"], // 编译过程中需要引入的库文件的列表 22 | // 指定模块路径别名。 23 | "paths": { 24 | "@/*": ["src/*"], 25 | "@build/*": ["build/*"] 26 | }, 27 | "types": ["node", "vite/client", "pinia-plugin-persist"] 28 | }, 29 | //需要编译的 30 | "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "types/**/*.d.ts", "types/*.d.ts", "vite.config.ts", "mock/**/*.ts"], 31 | "exclude": ["dist", "**/*.js", "node_modules"] //需要排除的 32 | } 33 | -------------------------------------------------------------------------------- /types/components.d.ts: -------------------------------------------------------------------------------- 1 | /* eslint-disable */ 2 | /* prettier-ignore */ 3 | // @ts-nocheck 4 | // Generated by unplugin-vue-components 5 | // Read more: https://github.com/vuejs/core/pull/3399 6 | export {} 7 | 8 | declare module 'vue' { 9 | export interface GlobalComponents { 10 | Avatar: typeof import('./../src/components/Avatar/src/Avatar.vue')['default'] 11 | Breadcrumb: typeof import('./../src/components/Breadcrumb/src/Breadcrumb.vue')['default'] 12 | ElAvatar: typeof import('element-plus/es')['ElAvatar'] 13 | ElBreadcrumb: typeof import('element-plus/es')['ElBreadcrumb'] 14 | ElBreadcrumbItem: typeof import('element-plus/es')['ElBreadcrumbItem'] 15 | ElButton: typeof import('element-plus/es')['ElButton'] 16 | ElCard: typeof import('element-plus/es')['ElCard'] 17 | ElColorPicker: typeof import('element-plus/es')['ElColorPicker'] 18 | ElConfigProvider: typeof import('element-plus/es')['ElConfigProvider'] 19 | ElDatePicker: typeof import('element-plus/es')['ElDatePicker'] 20 | ElDialog: typeof import('element-plus/es')['ElDialog'] 21 | ElDivider: typeof import('element-plus/es')['ElDivider'] 22 | ElDrawer: typeof import('element-plus/es')['ElDrawer'] 23 | ElDropdown: typeof import('element-plus/es')['ElDropdown'] 24 | ElForm: typeof import('element-plus/es')['ElForm'] 25 | ElFormItem: typeof import('element-plus/es')['ElFormItem'] 26 | ElInput: typeof import('element-plus/es')['ElInput'] 27 | ElPagination: typeof import('element-plus/es')['ElPagination'] 28 | ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm'] 29 | ElScrollbar: typeof import('element-plus/es')['ElScrollbar'] 30 | ElStatistic: typeof import('element-plus/es')['ElStatistic'] 31 | ElSwitch: typeof import('element-plus/es')['ElSwitch'] 32 | ElTable: typeof import('element-plus/es')['ElTable'] 33 | ElTableColumn: typeof import('element-plus/es')['ElTableColumn'] 34 | ElTag: typeof import('element-plus/es')['ElTag'] 35 | ElText: typeof import('element-plus/es')['ElText'] 36 | ElTimeline: typeof import('element-plus/es')['ElTimeline'] 37 | ElTimelineItem: typeof import('element-plus/es')['ElTimelineItem'] 38 | Form: typeof import('./../src/components/Form/src/Form.vue')['default'] 39 | Fullscreen: typeof import('./../src/components/Fullscreen/src/Fullscreen.vue')['default'] 40 | LocaleSwitch: typeof import('./../src/components/LocaleSwitch/src/LocaleSwitch.vue')['default'] 41 | Logo: typeof import('./../src/components/Logo/src/Logo.vue')['default'] 42 | Menu: typeof import('./../src/components/Menu/src/Menu.vue')['default'] 43 | Qrcode: typeof import('./../src/components/Qrcode/src/Qrcode.vue')['default'] 44 | RouterLink: typeof import('vue-router')['RouterLink'] 45 | RouterView: typeof import('vue-router')['RouterView'] 46 | SearchMenus: typeof import('./../src/components/SearchMenus/src/SearchMenus.vue')['default'] 47 | Table: typeof import('./../src/components/Table/src/Table.vue')['default'] 48 | TableSettings: typeof import('./../src/components/Table/src/components/TableSettings.vue')['default'] 49 | TagsView: typeof import('./../src/components/TagsView/src/TagsView.vue')['default'] 50 | ThemeSwitch: typeof import('./../src/components/ThemeSwitch/src/ThemeSwitch.vue')['default'] 51 | VxContainer: typeof import('./../src/components/VxContainer/src/VxContainer.vue')['default'] 52 | VxIcon: typeof import('./../src/components/VxIcon/src/VxIcon.vue')['default'] 53 | } 54 | export interface ComponentCustomProperties { 55 | vLoading: typeof import('element-plus/es')['ElLoadingDirective'] 56 | } 57 | } 58 | -------------------------------------------------------------------------------- /types/gobal.d.ts: -------------------------------------------------------------------------------- 1 | import { Language } from "element-plus/es/locale" 2 | 3 | declare global { 4 | /** 5 | * 打包压缩格式的类型声明 6 | */ 7 | type ViteCompression = "none" | "gzip" | "brotli" | "both" | "gzip-clear" | "brotli-clear" | "both-clear" 8 | 9 | // 全局定义vite环境变量type 对应.env.dev 10 | interface ViteEnv { 11 | VITE_APP_TITLE: string 12 | VITE_PORT: number 13 | VITE_PUBLIC_PATH: string 14 | VITE_ROUTER_HISTORY: string 15 | VITE_CDN: boolean 16 | VITE_COMPRESSION: ViteCompression 17 | VITE_REPORT: boolean 18 | } 19 | 20 | type Recordable = Record 21 | 22 | type Nullable = T | null 23 | 24 | type IsDark = "auto" | "dark" 25 | 26 | type LayoutType = "vertical" | "horizontal" | "group" 27 | 28 | interface ThemeTypes { 29 | themeColor: string 30 | elColorPrimary: string 31 | themeTextColor: string 32 | themeBgColor: string 33 | themeDivColor: string 34 | dividerColor: string 35 | leftMenuBgColor: string 36 | leftMenuBgLightColor: string 37 | leftMenuBgActiveColor: string 38 | leftMenuTextColor: string 39 | leftMenuTextActiveColor: string 40 | leftMenuCollapseBgActiveColor: string 41 | } 42 | 43 | type Locales = "zh-CN" | "en" 44 | 45 | type CurrentLocale = { 46 | lang: Locales 47 | elLang?: Language 48 | } 49 | 50 | type LocaleMap = { 51 | lang: Locales 52 | name: string 53 | } 54 | 55 | type ReturnType any> = T extends (...args: any) => infer R ? R : any 56 | 57 | type TagsList = { 58 | title: string 59 | path: string 60 | icon: string 61 | isFixed: boolean 62 | current: boolean 63 | } 64 | 65 | interface Fn { 66 | (...arg: T[]): R 67 | } 68 | 69 | type ElementPlusInfoType = "success" | "info" | "warning" | "danger" 70 | } 71 | 72 | export {} 73 | // 需要使用 var 不要使用 const 和 let 74 | // 必须要添加 export {} 否则会报错:全局范围的扩大仅可直接嵌套在外部模块中或环境模块声明中 75 | // global 包含的文件范围只以 tsconfig.json 为依据的 76 | // tsconfig.json 作为根目录,下面的全部.ts 文件都共享这个 global 77 | -------------------------------------------------------------------------------- /types/router.d.ts: -------------------------------------------------------------------------------- 1 | import { RouteComponent, RouteRecordName, RouteRecordRedirectOption } from "vue-router" 2 | // 目的,利用ts约束路由的每一项的类型,降低出错概率 3 | // 这里暂且将路由分为一级和其他级别(原因是希望一级路由允许有redirect,其他级别不允许有) 4 | 5 | declare global { 6 | /** 自定义 meta 类型 */ 7 | type CustomizeRouteMeta = { 8 | title?: string 9 | icon?: string 10 | keepAlive?: boolean 11 | hidden?: boolean 12 | role?: string[] 13 | } 14 | /** App Router详情 */ 15 | type VxRouteRecordRaw = { 16 | path: string 17 | name?: RouteRecordName 18 | redirect?: RouteRecordRedirectOption 19 | component?: RouteComponent 20 | children?: OtherLevelRouterRecord[] 21 | meta?: CustomizeRouteMeta 22 | } 23 | /** Router其他路由 */ 24 | type OtherLevelRouterRecord = Omit 25 | /** 26 | * @description 路由模式 27 | * @param static 读取静态路由,此模式下用户需要在前端静态路由上定义权限,然后根据后端返回的角色权限,前端判断路由显隐 28 | * @param async 异步路由,此模式下框架默认不加载本地静态路由,而是改为直接获取后端返回的路由,后端默认返回全部路由,前端根据权限或者部分其他特殊情况过滤出应该显示的路由 29 | * @param dynamic 动态路由,此模式下框架默认不加载本地静态路由,后端在登录之后返回角色对应的应该渲染的路由,前端展示拿到的路由 30 | */ 31 | type RouterMode = "static" | "async" | "dynamic" 32 | } 33 | // 想约束router meta 暂时不行,只能拓展,ts给个语法提示 34 | declare module "vue-router" { 35 | interface RouteMeta extends CustomizeRouteMeta {} 36 | // interface RouteRecordRaw extends VxRouteRecordRaw {} 37 | } 38 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from "path" 2 | import { loadEnv } from "vite" 3 | import { readEnv } from "./build/index" 4 | import type { UserConfig, ConfigEnv } from "vite" 5 | import { getPluginsList } from "./build/plugins" 6 | import type { AcceptedPlugin } from "postcss" 7 | import postcssImport from "postcss-import" 8 | import cssnano from "cssnano" 9 | 10 | /** 当前执行node命令时文件夹的地址(工作目录) */ 11 | const root: string = process.cwd() 12 | 13 | /** 路径查找 */ 14 | const pathResolve = (dir: string): string => { 15 | return resolve(root, ".", dir) 16 | } 17 | 18 | /** 设置别名 */ 19 | const alias: Record = { 20 | "@": pathResolve("src"), 21 | "@build": pathResolve("build") 22 | } 23 | 24 | export default ({ mode }: ConfigEnv): UserConfig => { 25 | const { VITE_PORT, VITE_CDN, VITE_PUBLIC_PATH, VITE_REPORT, VITE_COMPRESSION } = readEnv(loadEnv(mode, root)) 26 | return { 27 | base: VITE_PUBLIC_PATH, 28 | root, 29 | plugins: getPluginsList(VITE_CDN, VITE_REPORT, VITE_COMPRESSION), 30 | // 解决路径 31 | resolve: { 32 | alias 33 | }, 34 | // vue 3.4去除控制台warning 35 | define: { 36 | __VUE_PROD_HYDRATION_MISMATCH_DETAILS__: "true" 37 | }, 38 | // css配置 39 | css: { 40 | preprocessorOptions: { 41 | scss: { 42 | additionalData: `@use "@/styles/element/index.scss" as *;` 43 | } 44 | }, 45 | postcss: { 46 | plugins: [postcssImport as AcceptedPlugin, ...(mode === "production" ? [cssnano] : [])] 47 | } 48 | }, 49 | // 打包 50 | build: { 51 | sourcemap: false, 52 | // 消除打包大小超过500kb警告 默认500kb 这里改为4000kb 53 | chunkSizeWarningLimit: 4000, 54 | rollupOptions: { 55 | input: { 56 | index: pathResolve("index.html") 57 | }, 58 | // 静态资源分类打包 59 | output: { 60 | chunkFileNames: "static/js/[name]-[hash].js", 61 | entryFileNames: "static/js/[name]-[hash].js", 62 | assetFileNames: "static/[ext]/[name]-[hash].[ext]" 63 | } 64 | } 65 | }, 66 | // 服务 67 | server: { 68 | port: VITE_PORT, 69 | proxy: {}, 70 | hmr: { 71 | overlay: false 72 | }, 73 | host: "0.0.0.0" 74 | }, 75 | // 预构建 include构建 exclude排除 76 | optimizeDeps: { 77 | include: [ 78 | "vue", 79 | "pinia", 80 | "vue-i18n", 81 | "vue-router", 82 | "@vueuse/core", 83 | "element-plus", 84 | "element-plus/es/locale/lang/zh-cn", 85 | "element-plus/es/locale/lang/en", 86 | "axios", 87 | "echarts", 88 | "vxe-table", 89 | "lodash-es" 90 | ], 91 | exclude: ["@iconify-icons/ep", "@iconify-icons/ri", "@iconify/vue"] 92 | } 93 | } 94 | } 95 | --------------------------------------------------------------------------------