├── .devcontainer ├── Dockerfile └── devcontainer.json ├── .editorconfig ├── .env ├── .env-config.ts ├── .env.development ├── .env.production ├── .eslintignore ├── .eslintrc-auto-import.json ├── .eslintrc.cjs ├── .gitattributes ├── .gitignore ├── .husky ├── commit-msg └── pre-commit ├── .npmignore ├── .npmrc ├── .prettierrc.json ├── .vscode ├── clover.code-snippets ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── build ├── config │ ├── index.ts │ └── proxy.ts ├── index.ts ├── plugins │ ├── compress.ts │ ├── html.ts │ ├── index.ts │ ├── mock.ts │ ├── unplugin.ts │ └── visualizer.ts └── utils │ └── index.ts ├── commitlint.config.js ├── commitlint.config.ts ├── cypress.config.ts ├── cypress ├── e2e │ ├── example.cy.ts │ ├── spec.cy.ts │ └── tsconfig.json ├── fixtures │ └── example.json └── support │ ├── commands.ts │ └── e2e.ts ├── doc ├── git-commit-history-clean-cn.md └── pic │ ├── git-rebase.png │ ├── iconify-mdi.png │ ├── icons-root.png │ └── icons-src.png ├── docker ├── .dockerignore ├── Dockerfile └── nginx.conf ├── index.html ├── mock ├── api │ ├── auth.ts │ ├── example.ts │ └── index.ts └── index.ts ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public ├── favicon.ico ├── files │ └── test.xlsx └── resource │ └── loading.json ├── src ├── App.vue ├── assets │ ├── animation │ │ ├── 404-not-found.json │ │ ├── loading.json │ │ └── management.json │ ├── images │ │ └── example.png │ ├── logo.svg │ └── svg-icon │ │ ├── 403.svg │ │ ├── 404.svg │ │ ├── 500.svg │ │ ├── link-icon.svg │ │ ├── load-failed.svg │ │ ├── network-error.svg │ │ ├── no-content.svg │ │ ├── no-message.svg │ │ ├── page-error.svg │ │ └── verify.svg ├── components │ ├── __tests__ │ │ ├── ExceptionBase.spec.ts │ │ └── WebSiteLink.spec.ts │ ├── business │ │ ├── DragVerify.vue │ │ ├── ImgRotateVerify.vue │ │ ├── ImportExcel.vue │ │ ├── Loading.vue │ │ └── RandomNumVerify.vue │ ├── common │ │ ├── AdminLayout.vue │ │ ├── DarkModeContainer.vue │ │ ├── ElementProvider.vue │ │ ├── ExceptionBase.vue │ │ └── HoverContainer.vue │ ├── custom │ │ ├── BetterScroll.vue │ │ ├── CountTo.vue │ │ ├── GithubLink.vue │ │ ├── SvgIcon.vue │ │ └── WebSiteLink.vue │ └── icons │ │ └── IconLogo.vue ├── composables │ ├── echarts.ts │ ├── events.ts │ ├── excel.ts │ ├── index.ts │ ├── layout.ts │ ├── router.ts │ └── system.ts ├── config │ ├── index.ts │ ├── map-sdk.ts │ └── service.ts ├── constants │ ├── business.ts │ └── index.ts ├── context │ ├── count.ts │ ├── index.ts │ └── stepForm.ts ├── directives │ ├── clipboard.ts │ ├── index.ts │ └── permission.ts ├── enum │ ├── business.ts │ ├── common.ts │ └── index.ts ├── globalProperties.ts ├── hooks │ ├── business │ │ ├── index.ts │ │ └── useAliOSS.ts │ ├── common │ │ ├── index.ts │ │ ├── useBoolean.ts │ │ ├── useContext.ts │ │ └── useLoading.ts │ └── index.ts ├── layout │ ├── BasicLayout │ │ └── index.vue │ ├── BlankLayout │ │ └── index.vue │ ├── common │ │ ├── AppMain.vue │ │ ├── Footer │ │ │ └── index.vue │ │ ├── Header │ │ │ ├── components │ │ │ │ ├── Breadcrumb.vue │ │ │ │ ├── FullScreen.vue │ │ │ │ ├── GithubSite.vue │ │ │ │ ├── MenuCollapse.vue │ │ │ │ ├── ThemeModel.vue │ │ │ │ ├── UserAvatar.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ ├── Icon │ │ │ └── index.tsx │ │ ├── Logo │ │ │ └── index.vue │ │ ├── Sidebar │ │ │ ├── components │ │ │ │ ├── SidebarItem.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ ├── Tab │ │ │ ├── components │ │ │ │ ├── ReloadButton │ │ │ │ │ └── index.vue │ │ │ │ ├── TabDetail │ │ │ │ │ └── index.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ └── index.ts │ └── index.ts ├── main.ts ├── plugins │ ├── assets.ts │ └── index.ts ├── router │ ├── guard │ │ ├── dynamic.ts │ │ ├── index.ts │ │ └── permission.ts │ ├── helper │ │ ├── index.ts │ │ └── module.ts │ ├── index.ts │ ├── modules │ │ ├── about.ts │ │ ├── auth-demo.ts │ │ ├── component.ts │ │ ├── dashboard.ts │ │ ├── exception.ts │ │ ├── function.ts │ │ ├── index.ts │ │ ├── multi-menu.ts │ │ └── plugin.ts │ └── routes │ │ └── index.ts ├── sdk │ ├── index.ts │ └── oss.ts ├── service │ ├── api │ │ ├── auth.ts │ │ ├── example.ts │ │ └── index.ts │ ├── index.ts │ └── request │ │ ├── index.ts │ │ ├── instance.ts │ │ └── request.ts ├── stores │ ├── index.ts │ └── modules │ │ ├── app │ │ └── index.ts │ │ ├── auth │ │ └── index.ts │ │ ├── counter.ts │ │ ├── index.ts │ │ ├── route │ │ └── index.ts │ │ ├── tab │ │ ├── helpers.ts │ │ └── index.ts │ │ └── theme │ │ └── index.ts ├── styles │ ├── css │ │ ├── base.css │ │ ├── main.css │ │ ├── nprogress.css │ │ └── transition.css │ ├── less │ │ ├── common.less │ │ ├── element.less │ │ └── main.less │ └── scss │ │ ├── element.dark.scss │ │ ├── element.scss │ │ └── main.scss ├── typings │ ├── api.d.ts │ ├── auto-import.d.ts │ ├── business.d.ts │ ├── components.d.ts │ ├── env.d.ts │ ├── expose.d.ts │ ├── global.d.ts │ ├── package.d.ts │ ├── route.d.ts │ ├── router.d.ts │ ├── system.d.ts │ ├── utility.d.ts │ └── vue.d.ts ├── utils │ ├── auth │ │ ├── index.ts │ │ └── user.ts │ ├── common │ │ ├── index.ts │ │ ├── pattern.ts │ │ ├── responsibilitiesChain.ts │ │ └── typeof.ts │ ├── crypto │ │ └── index.ts │ ├── filters │ │ ├── business.ts │ │ └── index.ts │ ├── helper │ │ ├── index.ts │ │ └── tsxHelper.tsx │ ├── index.ts │ ├── router │ │ ├── auth.ts │ │ ├── breadcrumb.ts │ │ ├── cache.ts │ │ ├── helpers.ts │ │ ├── index.ts │ │ └── menus.ts │ ├── service │ │ ├── error.ts │ │ ├── handler.ts │ │ ├── index.ts │ │ ├── msg.ts │ │ └── transform.ts │ └── storage │ │ ├── index.ts │ │ ├── local.ts │ │ └── session.ts └── views │ ├── about │ └── index.vue │ ├── auth-demo │ ├── permission │ │ └── index.vue │ ├── super │ │ └── index.vue │ └── user │ │ └── index.vue │ ├── component │ ├── complex-form │ │ ├── components │ │ │ ├── BaseValidatorForm.vue │ │ │ ├── CustomValidatorForm.vue │ │ │ ├── ResponsibilityValidatorForm.vue │ │ │ └── index.ts │ │ └── index.vue │ ├── step-form │ │ ├── components │ │ │ ├── StepFour.vue │ │ │ ├── StepOne.vue │ │ │ ├── StepThree.vue │ │ │ ├── StepTwo.vue │ │ │ └── index.ts │ │ └── index.vue │ ├── table │ │ ├── components │ │ │ ├── BaseTable.vue │ │ │ └── index.ts │ │ └── index.vue │ └── verify │ │ ├── components │ │ ├── DragVerifyExample.vue │ │ ├── ImgRotateVerifyExample.vue │ │ ├── RandomVerifyExample.vue │ │ └── index.ts │ │ └── index.vue │ ├── dashboard │ └── analysis │ │ ├── components │ │ ├── BottomPart │ │ │ ├── components │ │ │ │ ├── HomeTable.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ ├── Charts │ │ │ └── index.vue │ │ ├── DataCard │ │ │ ├── components │ │ │ │ ├── GradientBg.vue │ │ │ │ └── index.ts │ │ │ └── index.vue │ │ └── index.ts │ │ └── index.vue │ ├── exception │ ├── 403 │ │ └── index.vue │ ├── 404 │ │ └── index.vue │ ├── 500 │ │ └── index.vue │ └── other │ │ └── index.vue │ ├── function │ └── request │ │ └── index.vue │ ├── login │ └── index.vue │ ├── multi-menu │ └── first │ │ ├── second-new │ │ └── third │ │ │ └── index.vue │ │ └── second │ │ └── index.vue │ ├── plugin │ ├── clipboard │ │ └── index.vue │ ├── echarts │ │ └── index.vue │ ├── editor │ │ ├── markdown │ │ │ └── index.vue │ │ └── quill │ │ │ └── index.vue │ ├── excel │ │ └── import │ │ │ └── index.vue │ ├── icon │ │ └── index.vue │ ├── map │ │ ├── components │ │ │ ├── BaiduMap.vue │ │ │ ├── GaodeMap.vue │ │ │ └── index.ts │ │ └── index.vue │ ├── oss │ │ ├── components │ │ │ ├── Example.vue │ │ │ ├── SharePic.vue │ │ │ └── index.ts │ │ └── index.vue │ ├── swiper │ │ └── index.vue │ └── video │ │ └── index.vue │ └── system-view │ ├── no-permission │ └── index.vue │ └── not-found │ └── index.vue ├── tsconfig.app.json ├── tsconfig.config.json ├── tsconfig.json ├── tsconfig.vitest.json ├── uno.config.ts ├── vite.config.ts └── vitest.config.ts /.devcontainer/Dockerfile: -------------------------------------------------------------------------------- 1 | # [Choice] Node.js version (use -bullseye variants on local arm64/Apple Silicon): 18, 16, 14, 18-bullseye, 16-bullseye, 14-bullseye, 18-buster, 16-buster, 14-buster 2 | ARG VARIANT=16 3 | FROM mcr.microsoft.com/devcontainers/javascript-node:0-${VARIANT} 4 | 5 | RUN su node -c "umask 0002 && npm install -g http-server @vue/cli @vue/cli-service-global" 6 | WORKDIR /app 7 | 8 | EXPOSE 8080 9 | 10 | # [Optional] Uncomment this section to install additional OS packages. 11 | # RUN apt-get update && export DEBIAN_FRONTEND=noninteractive \ 12 | # && apt-get -y install --no-install-recommends 13 | -------------------------------------------------------------------------------- /.devcontainer/devcontainer.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Vue (Community)", 3 | "build": { 4 | "dockerfile": "Dockerfile", 5 | "context": ".." 6 | }, 7 | 8 | // Configure tool-specific properties. 9 | "customizations": { 10 | // Configure properties specific to VS Code. 11 | "vscode": { 12 | // Add the IDs of extensions you want installed when the container is created. 13 | "extensions": [ 14 | "dbaeumer.vscode-eslint", 15 | "eamodio.gitlens", 16 | "afzalsayed96.icones", 17 | "antfu.iconify", 18 | "antfu.unocss", 19 | "christian-kohler.path-intellisense", 20 | "EditorConfig.EditorConfig", 21 | "esbenp.prettier-vscode", 22 | "formulahendry.auto-complete-tag", 23 | "formulahendry.auto-close-tag", 24 | "formulahendry.auto-rename-tag", 25 | "kisstkondoros.vscode-gutter-preview", 26 | "lokalise.i18n-ally", 27 | "mhutchie.git-graph", 28 | "mikestead.dotenv", 29 | "naumovs.color-highlight", 30 | "PKief.material-icon-theme", 31 | "Equinusocio.vsc-material-theme", 32 | "steoates.autoimport", 33 | "Vue.vscode-typescript-vue-plugin", 34 | "Vue.volar", 35 | "wix.vscode-import-cost", 36 | "pranaygp.vscode-css-peek", 37 | "Zignd.html-css-class-completion", 38 | "leizongmin.node-module-intellisense", 39 | "christian-kohler.npm-intellisense", 40 | "quicktype.quicktype", 41 | "whtouche.vscode-js-console-utils", 42 | "stylelint.vscode-stylelint", 43 | "syler.sass-indented", 44 | "bierner.color-info", 45 | "streetsidesoftware.code-spell-checker", 46 | "VisualStudioExptTeam.vscodeintellicode", 47 | "aaron-bond.better-comments", 48 | "Gruntfuggly.todo-tree", 49 | "VisualStudioExptTeam.intellicode-api-usage-examples", 50 | "xabikos.JavaScriptSnippets", 51 | "lottiefiles.vscode-lottie", 52 | "ZixuanChen.vitest-explorer", 53 | "GitHub.copilot", 54 | "intellsmi.comment-translate", 55 | "WhenSunset.chatgpt-china", 56 | "Tobermory.es6-string-html" 57 | ] 58 | } 59 | }, 60 | 61 | // Use 'forwardPorts' to make a list of ports inside the container available locally. 62 | "forwardPorts": [ 63 | 8080 64 | ], 65 | 66 | // Use 'postCreateCommand' to run commands after the container is created. 67 | // "postCreateCommand": "uname -a", 68 | 69 | // Set `remoteUser` to `root` to connect as root instead. More info: https://aka.ms/vscode-remote/containers/non-root. 70 | "remoteUser": "node", 71 | "features": { 72 | "ghcr.io/devcontainers/features/git:1": {}, 73 | "ghcr.io/devcontainers/features/github-cli:1": {}, 74 | "ghcr.io/devcontainers/features/node:1": {}, 75 | "ghcr.io/devcontainers/features/powershell:1": {}, 76 | "ghcr.io/devcontainers/features/sshd:1": {} 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /.editorconfig: -------------------------------------------------------------------------------- 1 | # Editor configuration, see http://editorconfig.org 2 | 3 | root = true 4 | 5 | [*] 6 | charset = utf-8 7 | indent_style = space 8 | indent_size = 2 9 | end_of_line = lf 10 | trim_trailing_whitespace = true 11 | insert_final_newline = true 12 | max_line_length = 100 13 | -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_BASE_URL=/ 2 | 3 | VITE_APP_NAME=CloverAdmin 4 | 5 | VITE_APP_TITLE=CloverAdmin管理系统 6 | 7 | VITE_APP_DESC=CloverAdmin是一个中后台管理系统模版 8 | 9 | # 路由首页(根路由重定向), 仅用于static模式的权限路由 10 | VITE_ROUTE_HOME_PATH=/dashboard/analysis 11 | -------------------------------------------------------------------------------- /.env-config.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // 上述代码是为了让本文件中的代码在编译前正确引用到./src/typings/env.d.ts 3 | // 即通过注释的方式, 告诉TS编译器去查找./src/typings/env.d.ts文件 4 | 5 | /** 请求服务的环境配置 */ 6 | type ServiceEnv = Record; 7 | 8 | /** 不同环境下的配置, 代理就写在这里 */ 9 | const serviceEnv: ServiceEnv = { 10 | dev: [ 11 | /** 12 | * 代理: 13 | * - 将http://127.0.0.1:5574/clover-api/xx代理到http://127.0.0.1:5976/api/xx 14 | */ 15 | { 16 | url: "http://server.wzc520pyf.cn", 17 | urlPattern: "/clover-api", 18 | rewritten: "/api", 19 | }, 20 | ], 21 | test: [], 22 | prod: [], 23 | }; 24 | 25 | /** 26 | * 获取当前环境下的请求服务配置 27 | * @param env 环境 28 | */ 29 | export function getServiceEnvConfig(env: ImportMetaEnv) { 30 | const { VITE_SERVICE_ENV = "dev" } = env; 31 | 32 | const config = serviceEnv[VITE_SERVICE_ENV]; 33 | 34 | return config; 35 | } 36 | -------------------------------------------------------------------------------- /.env.development: -------------------------------------------------------------------------------- 1 | VITE_HTTP_PROXY=Y 2 | -------------------------------------------------------------------------------- /.env.production: -------------------------------------------------------------------------------- 1 | VITE_VISUALIZER=Y 2 | 3 | VITE_COMPRESS=Y 4 | 5 | VITE_COMPRESS_TYPE=gzip -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | .vscode 2 | .idea 3 | .local 4 | !.env-config.ts 5 | components.d.ts 6 | auto-import.d.ts 7 | -------------------------------------------------------------------------------- /.eslintrc-auto-import.json: -------------------------------------------------------------------------------- 1 | { 2 | "globals": { 3 | "$": true, 4 | "$$": true, 5 | "$computed": true, 6 | "$customRef": true, 7 | "$ref": true, 8 | "$shallowRef": true, 9 | "$toRef": true, 10 | "EffectScope": true, 11 | "acceptHMRUpdate": true, 12 | "computed": true, 13 | "createApp": true, 14 | "createPinia": true, 15 | "customRef": true, 16 | "defineAsyncComponent": true, 17 | "defineComponent": true, 18 | "defineStore": true, 19 | "effectScope": true, 20 | "getActivePinia": true, 21 | "getCurrentInstance": true, 22 | "getCurrentScope": true, 23 | "h": true, 24 | "inject": true, 25 | "isProxy": true, 26 | "isReactive": true, 27 | "isReadonly": true, 28 | "isRef": true, 29 | "mapActions": true, 30 | "mapGetters": true, 31 | "mapState": true, 32 | "mapStores": true, 33 | "mapWritableState": true, 34 | "markRaw": true, 35 | "nextTick": true, 36 | "onActivated": true, 37 | "onBeforeMount": true, 38 | "onBeforeRouteLeave": true, 39 | "onBeforeRouteUpdate": true, 40 | "onBeforeUnmount": true, 41 | "onBeforeUpdate": true, 42 | "onDeactivated": true, 43 | "onErrorCaptured": true, 44 | "onMounted": true, 45 | "onRenderTracked": true, 46 | "onRenderTriggered": true, 47 | "onScopeDispose": true, 48 | "onServerPrefetch": true, 49 | "onUnmounted": true, 50 | "onUpdated": true, 51 | "provide": true, 52 | "reactive": true, 53 | "readonly": true, 54 | "ref": true, 55 | "resolveComponent": true, 56 | "resolveDirective": true, 57 | "setActivePinia": true, 58 | "setMapStoreSuffix": true, 59 | "shallowReactive": true, 60 | "shallowReadonly": true, 61 | "shallowRef": true, 62 | "storeToRefs": true, 63 | "toRaw": true, 64 | "toRef": true, 65 | "toRefs": true, 66 | "triggerRef": true, 67 | "unref": true, 68 | "useAttrs": true, 69 | "useCssModule": true, 70 | "useCssVars": true, 71 | "useLink": true, 72 | "useRoute": true, 73 | "useRouter": true, 74 | "useSlots": true, 75 | "watch": true, 76 | "watchEffect": true, 77 | "watchPostEffect": true, 78 | "watchSyncEffect": true 79 | } 80 | } -------------------------------------------------------------------------------- /.gitattributes: -------------------------------------------------------------------------------- 1 | "*.vue" eol=lf 2 | "*.js" eol=lf 3 | "*.ts" eol=lf 4 | "*.jsx" eol=lf 5 | "*.tsx" eol=lf 6 | "*.cjs" eol=lf 7 | "*.cts" eol=lf 8 | "*.mjs" eol=lf 9 | "*.mts" eol=lf 10 | "*.json" eol=lf 11 | "*.html" eol=lf 12 | "*.css" eol=lf 13 | "*.less" eol=lf 14 | "*.scss" eol=lf 15 | "*.sass" eol=lf 16 | "*.styl" eol=lf 17 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | pnpm-debug.log* 8 | lerna-debug.log* 9 | 10 | .pnpm-store 11 | node_modules 12 | .DS_Store 13 | dist 14 | dist-ssr 15 | stats.html 16 | coverage 17 | *.local 18 | 19 | /cypress/videos/ 20 | /cypress/screenshots/ 21 | 22 | # Editor directories and files 23 | .vscode/* 24 | !.vscode/extensions.json 25 | !.vscode/settings.json 26 | !.vscode/clover.code-snippets 27 | .idea 28 | *.suo 29 | *.ntvs* 30 | *.njsproj 31 | *.sln 32 | *.sw? 33 | 34 | /src/typings/components.d.ts 35 | /src/typings/auto-import.d.ts 36 | -------------------------------------------------------------------------------- /.husky/commit-msg: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm exec commitlint --config commitlint.config.js --edit "${1}" 5 | -------------------------------------------------------------------------------- /.husky/pre-commit: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | . "$(dirname "$0")/_/husky.sh" 3 | 4 | pnpm exec lint-staged 5 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | README.md 2 | test-project 3 | definition-manifest.json 4 | .vscode 5 | .npmignore -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | # see: https://pnpm.io/zh/npmrc 2 | # registry=https://registry.npmmirror.com/ 3 | auto-install-peers=true 4 | -------------------------------------------------------------------------------- /.prettierrc.json: -------------------------------------------------------------------------------- 1 | {} -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "dbaeumer.vscode-eslint", // 代码格式检查插件 4 | "eamodio.gitlens", // 大大增强vscode内使用git的体验 5 | "afzalsayed96.icones", // 方便地查找icon: Ctrl+Shift+P => Icons:Find icons => 即可在vsc内打开icons官网搜索图标 6 | "antfu.iconify", // 图标组件预览(直接将图标显示在你的HTML) 7 | "antfu.unocss", // unocss插件, 若使用unocss, 务必安装, 否则毫无体验可言 8 | "christian-kohler.path-intellisense", // 自动完成文件名 9 | "EditorConfig.EditorConfig", // 统一工作环境(例如统一工作区行尾符为LF) 10 | "esbenp.prettier-vscode", // 代码格式化插件 11 | "formulahendry.auto-complete-tag", // 标签自动完成 12 | "formulahendry.auto-close-tag", // 标签自动闭合 13 | "formulahendry.auto-rename-tag", // 标签自动重命名 14 | "kisstkondoros.vscode-gutter-preview", // 图片预览 15 | "lokalise.i18n-ally", // i18n国际化智能显示 16 | "mhutchie.git-graph", // 可视化Git提交图 17 | "mikestead.dotenv", // .env文件的语法高亮 18 | "naumovs.color-highlight", // 色彩值高亮显示 19 | "PKief.material-icon-theme", // 漂亮的图标主题 20 | "Equinusocio.vsc-material-theme", // 漂亮的色彩主题 21 | "steoates.autoimport", // 为TS提供代码提示 22 | "Vue.vscode-typescript-vue-plugin",// vue3 for ts 官方推荐插件 23 | "Vue.volar", // vue3官方推荐插件 24 | "Tobermory.es6-string-html", // 在内联的html字符串前加上/*html*/以获得高亮语法 25 | "ZixuanChen.vitest-explorer", // vitest官方插件 26 | "wix.vscode-import-cost", // 在编译器中显示导入包的大小 27 | "pranaygp.vscode-css-peek", // 允许在HTML内查看css定义, 且支持跳转 28 | "Zignd.html-css-class-completion", // 根据工作区定义的css, 在HTML中提供css智能提示 29 | "leizongmin.node-module-intellisense", // 当导入nodejs模块时智能提示模块名 30 | "christian-kohler.npm-intellisense", // 当导入npm模块时智能提示模块名 31 | "quicktype.quicktype", // 依据json生成TS的type定义: 1.copy一份json 2. Ctrl+Shift+P => 选择 Paste JSON as Code 32 | "whtouche.vscode-js-console-utils", // 快速插入console: 1. 选中要输出的变量 2. Ctrl+Shift+L 3. 快速删除当前文件内的所有console Ctrl+Shift+D 33 | "stylelint.vscode-stylelint", // 对css的语法检查 34 | "syler.sass-indented", // 针对sass的语法提示与格式化 35 | "bierner.color-info", // color工具, 帮助你检查和调整色彩, 也可以快速地切换rgb, hsl, hex等不同色彩表示法 36 | "streetsidesoftware.code-spell-checker", // 拼写检查, 减少你的拼写错误 37 | "VisualStudioExptTeam.vscodeintellicode", // 代码智能提示 38 | "aaron-bond.better-comments", // 高亮你的注释: 诸如 //? //! //* //TODO 39 | "Gruntfuggly.todo-tree", // 更方便地管理你的TODO 40 | "VisualStudioExptTeam.intellicode-api-usage-examples", // 帮助查看api的使用示例, 鼠标悬浮在api上, 你可以看到 See Real World Examples From GitHub 的可点击字样 41 | "xabikos.JavaScriptSnippets", // 提供一些js或ts的代码片段 42 | "lottiefiles.vscode-lottie", // 在vsocde里预览lottie动画(右击lottie的json文件, 选择View in Lottie Player) 43 | "intellsmi.comment-translate", // 悬浮翻译插件 44 | "WhenSunset.chatgpt-china", //! 近期很火的AI, 这个插件将他的功能结合在了vscode里, 支持中文, 你需要有国外帐号 45 | "GitHub.copilot" //! 强大的代码提示插件, 提高开发效率, 但它是付费的 46 | ] 47 | } 48 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 wzc520pyfm 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /build/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./proxy"; 2 | -------------------------------------------------------------------------------- /build/config/proxy.ts: -------------------------------------------------------------------------------- 1 | import type { ProxyOptions } from "vite"; 2 | 3 | /** 4 | * 创建proxy 5 | * @param isOpenProxy 是否启用proxy 6 | * @param envConfig 环境配置(不同环境有不用的环境配置) 7 | */ 8 | export function createViteProxy( 9 | isOpenProxy: boolean, 10 | envConfig: ServiceEnvConfigs 11 | ): Record | undefined { 12 | if (!isOpenProxy) return undefined; 13 | 14 | const proxy: Record = {}; 15 | 16 | for (const config of envConfig) { 17 | Reflect.set(proxy, config.urlPattern, { 18 | target: config.url, 19 | changeOrigin: true, 20 | rewrite: (path: string) => { 21 | if (config.rewritten) { 22 | return path.replace(new RegExp(`^${config.urlPattern}`), config.rewritten); 23 | } else { 24 | return path.replace(new RegExp(`^${config.urlPattern}`), ""); 25 | } 26 | }, 27 | }); 28 | } 29 | 30 | return proxy; 31 | } 32 | -------------------------------------------------------------------------------- /build/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./config"; 2 | export * from "./plugins"; 3 | -------------------------------------------------------------------------------- /build/plugins/compress.ts: -------------------------------------------------------------------------------- 1 | import { compression } from "vite-plugin-compression2"; 2 | 3 | // see: https://github.com/nonzzz/vite-compression-plugin#readme 4 | export default (viteEnv: ImportMetaEnv) => { 5 | const { VITE_COMPRESS_TYPE = "gzip" } = viteEnv; 6 | return compression({ 7 | algorithm: VITE_COMPRESS_TYPE, // 压缩算法 8 | // threshold: 10 * 1024, // 仅处理大于此大小的资源(以字节为单位) 9 | deleteOriginalAssets: false, // 是否删除原始资源 10 | }); 11 | }; 12 | -------------------------------------------------------------------------------- /build/plugins/html.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from "vite"; 2 | import { createHtmlPlugin } from "vite-plugin-html"; 3 | 4 | // see: https://github.com/vbenjs/vite-plugin-html/blob/main/README.zh_CN.md 5 | export default (viteEnv: ImportMetaEnv): PluginOption[] => { 6 | return createHtmlPlugin({ 7 | minify: true, 8 | inject: { 9 | data: { 10 | appName: viteEnv.VITE_APP_NAME, 11 | appTitle: viteEnv.VITE_APP_TITLE, 12 | appDescription: viteEnv.VITE_APP_DESC, 13 | }, 14 | }, 15 | }); 16 | }; 17 | -------------------------------------------------------------------------------- /build/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from "vite"; 2 | import { VitePWA } from "vite-plugin-pwa"; 3 | import unocss from "unocss/vite"; 4 | import unplugin from "./unplugin"; 5 | import mock from "./mock"; 6 | import html from "./html"; 7 | import compress from "./compress"; 8 | import visualizer from "./visualizer"; 9 | 10 | /** 11 | * vite插件 12 | */ 13 | export function setupVitePlugins(viteEnv: ImportMetaEnv): (PluginOption | PluginOption[])[] { 14 | const plugins = [ 15 | VitePWA(), // see: https://github.com/vite-pwa/vite-plugin-pwa 16 | html(viteEnv), 17 | ...unplugin(), 18 | unocss(), 19 | mock, 20 | ]; 21 | 22 | if (viteEnv.VITE_VISUALIZER === "Y") { 23 | plugins.push(visualizer()); 24 | } 25 | 26 | if (viteEnv.VITE_COMPRESS === "Y") { 27 | plugins.push(compress(viteEnv)); 28 | } 29 | 30 | return plugins; 31 | } 32 | -------------------------------------------------------------------------------- /build/plugins/mock.ts: -------------------------------------------------------------------------------- 1 | import { viteMockServe } from "vite-plugin-mock"; 2 | 3 | export default viteMockServe({ 4 | mockPath: "mock", 5 | injectCode: ` 6 | import { setupMockServer } from '../mock'; 7 | setupMockServer(); 8 | `, 9 | }); 10 | -------------------------------------------------------------------------------- /build/plugins/unplugin.ts: -------------------------------------------------------------------------------- 1 | import type { PluginOption } from "vite"; 2 | import VueMacros from "unplugin-vue-macros/vite"; 3 | import AutoImport from "unplugin-auto-import/vite"; 4 | import Components from "unplugin-vue-components/vite"; 5 | import Icons from "unplugin-icons/vite"; 6 | import IconsResolver from "unplugin-icons/resolver"; 7 | import { ElementPlusResolver } from "unplugin-vue-components/resolvers"; 8 | import { FileSystemIconLoader } from "unplugin-icons/loaders"; 9 | import ElementPlus from "unplugin-element-plus/vite"; 10 | import { createSvgIconsPlugin } from "vite-plugin-svg-icons"; 11 | import { getSrcPath } from "../utils"; 12 | import Vue from "@vitejs/plugin-vue"; 13 | import VueJsx from "@vitejs/plugin-vue-jsx"; 14 | import ReactivityTransform from "@vue-macros/reactivity-transform/vite"; 15 | 16 | export default function unplugin(): PluginOption[] { 17 | const srcPath = getSrcPath(); 18 | const localIconPath = `${srcPath}/assets/svg-icon`; 19 | 20 | /** 本地svg图标集合名称 */ 21 | const collectionName = "local"; 22 | 23 | return [ 24 | VueMacros({ 25 | plugins: { 26 | vue: Vue(), 27 | vueJsx: VueJsx(), 28 | }, 29 | }), // see: https://vue-macros.sxzz.moe/ 30 | ReactivityTransform(), 31 | AutoImport({ 32 | imports: ["vue", "vue-router", "pinia"], // 自动导入vue和vue-router相关函数 33 | dts: "src/typings/auto-import.d.ts", // 生成 `auto-import.d.ts` 全局声明 34 | resolvers: [ 35 | ElementPlusResolver(), // 自动导入element-plus相关组件 36 | IconsResolver({ 37 | // 自动导入图标组件 38 | prefix: "Icon", 39 | }), 40 | ], 41 | eslintrc: { 42 | enabled: false, // 自动生成全局声明文件, 不需要eslint检查(在.eslintrc-auto-import.json生成成功之后就可以改为false, 当你更新了导入配置后,将其改为true即可重新生成一次) 43 | filepath: "./.eslintrc-auto-import.json", 44 | globalsPropValue: true, 45 | }, 46 | }), 47 | Components({ 48 | dts: "src/typings/components.d.ts", 49 | resolvers: [ 50 | IconsResolver({ 51 | // 自动注册图标组件 how to use: 52 | enabledCollections: ["ep", "mdi"], // 'ep'是element图标集在https://iconify.design/ 里的集合名, 如果你引入或`使用了其他图标集, 需要在此把其集合名写上 53 | // 本地svg图标集合 54 | customCollections: [collectionName], 55 | // componentPrefix: "icon", // 与element-plus的prefix配置冲突(本地图标使用: ) 56 | }), 57 | ElementPlusResolver({ 58 | importStyle: "sass", 59 | }), // 自动导入element-plus相关组件 60 | ], 61 | }), 62 | 63 | // see: https://github.com/antfu/unplugin-icons 64 | Icons({ 65 | // 自动导入图标组件 图标来源: https://iconify.design/ 66 | autoInstall: true, 67 | // 本地图标 68 | compiler: "vue3", 69 | customCollections: { 70 | [collectionName]: FileSystemIconLoader(localIconPath), 71 | }, 72 | scale: 1, 73 | defaultClass: "inline-block", 74 | }), 75 | // 封装本地svg图标 76 | createSvgIconsPlugin({ 77 | iconDirs: [localIconPath], 78 | symbolId: `icon-local-[dir]-[name]`, 79 | inject: "body-last", 80 | customDomId: "__SVG_ICON_LOCAL__", 81 | }), 82 | ElementPlus({ useSource: true }), // 当需要手动导入Element组件时, 此plugin可帮助你自动导入其样式 83 | ]; 84 | } 85 | -------------------------------------------------------------------------------- /build/plugins/visualizer.ts: -------------------------------------------------------------------------------- 1 | import { visualizer } from "rollup-plugin-visualizer"; 2 | 3 | // see: https://www.npmjs.com/package/rollup-plugin-visualizer 4 | export default () => { 5 | return visualizer({ 6 | gzipSize: true, 7 | brotliSize: true, 8 | open: true, 9 | }); 10 | }; 11 | -------------------------------------------------------------------------------- /build/utils/index.ts: -------------------------------------------------------------------------------- 1 | import path from "path"; 2 | 3 | /** 4 | * 获取项目根路径 5 | * @description 末尾无斜杠 6 | */ 7 | export function getRootPath() { 8 | return path.resolve(process.cwd()); 9 | } 10 | 11 | /** 12 | * 获取项目src路径 13 | * @parma srcName - src目录名称 14 | * @description 末尾无斜杠 15 | */ 16 | export function getSrcPath(srcName = "src") { 17 | const rootPath = getRootPath(); 18 | return `${rootPath}/${srcName}`; 19 | } 20 | -------------------------------------------------------------------------------- /commitlint.config.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-undef */ 2 | // commitlint uses `ts-node` to load typescript config, it's too slow. So we replace it with `esbuild`. 3 | require("@esbuild-kit/cjs-loader"); 4 | module.exports = require("./commitlint.config.ts").default; 5 | -------------------------------------------------------------------------------- /cypress.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "cypress"; 2 | 3 | export default defineConfig({ 4 | projectId: "1zvgo9", 5 | e2e: { 6 | specPattern: "cypress/e2e/**/*.{cy,spec}.{js,jsx,ts,tsx}", 7 | baseUrl: "http://localhost:4173", 8 | }, 9 | }); 10 | -------------------------------------------------------------------------------- /cypress/e2e/example.cy.ts: -------------------------------------------------------------------------------- 1 | // https://docs.cypress.io/api/introduction/api.html 2 | 3 | describe("My Home Test", () => { 4 | it("visits the app root url", () => { 5 | cy.visit("/"); 6 | cy.contains("h2", "Clover管理系统"); 7 | }); 8 | }); 9 | -------------------------------------------------------------------------------- /cypress/e2e/spec.cy.ts: -------------------------------------------------------------------------------- 1 | describe("My First Test", () => { 2 | it("Dose not do much!", () => { 3 | expect(true).to.equal(true); // 最简单的测试例子(验证true是否为true) 4 | }); 5 | it("Visits the Kitchen Sink", () => { 6 | cy.visit("http://example.cypress.io"); // 访问http://example.cypress.io 7 | 8 | cy.contains("type"); // 在新页面上查找包含指定内容(type)的元素 9 | 10 | cy.contains("type").click(); // 点击找到的type(在这个测试页面中这是一个链接) 11 | 12 | cy.url().should("include", "/commands/actions"); // 点击type跳转到的页面的url应该包含/commands/actions 13 | 14 | cy.get(".action-email").type("fake@email.com"); // 获取到css类名为.action-email的input框, 并输入值fake@email.com 15 | 16 | // 验证输入框的值否按预期更新(断言判断其值是否是fake@email.com) 17 | cy.get(".action-email").should("have.value", "fake@email.com"); 18 | 19 | //! 不建议通过类名来选择元素, 但有时访问外部页面也不得不这么做, 最佳实践 @see: https://docs.cypress.io/guides/references/best-practices#Selecting-Elements 20 | }); 21 | }); 22 | -------------------------------------------------------------------------------- /cypress/e2e/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["./**/*", "../support/**/*"], 4 | "compilerOptions": { 5 | "isolatedModules": false, 6 | "target": "es5", 7 | "lib": ["es5", "dom"], 8 | "types": ["cypress"] 9 | } 10 | } 11 | -------------------------------------------------------------------------------- /cypress/fixtures/example.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "Using fixtures to represent data", 3 | "email": "hello@cypress.io", 4 | "body": "Fixtures are a great way to mock data for responses to routes" 5 | } 6 | -------------------------------------------------------------------------------- /cypress/support/commands.ts: -------------------------------------------------------------------------------- 1 | /// 2 | // *********************************************** 3 | // This example commands.ts shows you how to 4 | // create various custom commands and overwrite 5 | // existing commands. 6 | // 7 | // For more comprehensive examples of custom 8 | // commands please read more here: 9 | // https://on.cypress.io/custom-commands 10 | // *********************************************** 11 | // 12 | // 13 | // -- This is a parent command -- 14 | // Cypress.Commands.add('login', (email, password) => { ... }) 15 | // 16 | // 17 | // -- This is a child command -- 18 | // Cypress.Commands.add('drag', { prevSubject: 'element'}, (subject, options) => { ... }) 19 | // 20 | // 21 | // -- This is a dual command -- 22 | // Cypress.Commands.add('dismiss', { prevSubject: 'optional'}, (subject, options) => { ... }) 23 | // 24 | // 25 | // -- This will overwrite an existing command -- 26 | // Cypress.Commands.overwrite('visit', (originalFn, url, options) => { ... }) 27 | // 28 | // declare global { 29 | // namespace Cypress { 30 | // interface Chainable { 31 | // login(email: string, password: string): Chainable 32 | // drag(subject: string, options?: Partial): Chainable 33 | // dismiss(subject: string, options?: Partial): Chainable 34 | // visit(originalFn: CommandOriginalFn, url: string, options: Partial): Chainable 35 | // } 36 | // } 37 | // } 38 | 39 | export {}; 40 | -------------------------------------------------------------------------------- /cypress/support/e2e.ts: -------------------------------------------------------------------------------- 1 | // *********************************************************** 2 | // This example support/index.js is processed and 3 | // loaded automatically before your test files. 4 | // 5 | // This is a great place to put global configuration and 6 | // behavior that modifies Cypress. 7 | // 8 | // You can change the location of this file or turn off 9 | // automatically serving support files with the 10 | // 'supportFile' configuration option. 11 | // 12 | // You can read more here: 13 | // https://on.cypress.io/configuration 14 | // *********************************************************** 15 | 16 | // Import commands.js using ES2015 syntax: 17 | import "./commands"; 18 | 19 | // Alternatively you can use CommonJS syntax: 20 | // require('./commands') 21 | -------------------------------------------------------------------------------- /doc/pic/git-rebase.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/doc/pic/git-rebase.png -------------------------------------------------------------------------------- /doc/pic/iconify-mdi.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/doc/pic/iconify-mdi.png -------------------------------------------------------------------------------- /doc/pic/icons-root.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/doc/pic/icons-root.png -------------------------------------------------------------------------------- /doc/pic/icons-src.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/doc/pic/icons-src.png -------------------------------------------------------------------------------- /docker/.dockerignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | .npmrc 5 | .cache 6 | 7 | .local 8 | # local env files 9 | .env.local 10 | .env.*.local 11 | .eslintcache 12 | 13 | # Log files 14 | npm-debug.log* 15 | yarn-debug.log* 16 | yarn-error.log* 17 | pnpm-debug.log* 18 | 19 | # Editor directories and files 20 | .idea 21 | # .vscode 22 | *.suo 23 | *.ntvs* 24 | *.njsproj 25 | *.sln 26 | *.sw? 27 | yarn.lock 28 | pnpm-lock.yaml 29 | /vite-profile.cpuprofile 30 | -------------------------------------------------------------------------------- /docker/Dockerfile: -------------------------------------------------------------------------------- 1 | # 使用官方的Node.js镜像作为基础镜像, 为其指定别名为builder 2 | FROM node:16.18.0 as builder 3 | # 设置环境变量, 指定工作目录为/clover-admin-vue 4 | ENV WORKDIR=/clover-admin-vue 5 | # 指定新的工作目录并将值赋予变量$WORKDIR 6 | WORKDIR $WORKDIR 7 | # 将当前目录的文件复制到指定的工作目录 8 | COPY ./ $WORKDIR/ 9 | # 用于指定Dockfle中使用的版本号, 可在构建镜像时传递给Docker 10 | ARG version 11 | # 在Dockefile中定义环境变量 12 | ENV COMMITID=$version 13 | # 安装pnpm 14 | RUN npm i -g pnpm 15 | # 安装项目依赖 16 | RUN pnpm install 17 | # 项目打包(默认生产环境) 18 | RUN pnpm build 19 | # 使用nginx:alpine镜像作为基础镜像, 为其指定别名为prod 20 | FROM nginx:alpine as prod 21 | # 在当前容器创建文件夹 22 | RUN mkdir /clover 23 | # 从builder中复制/clover-admin-vue/dist到/clover-admin-vue 24 | COPY --from=builder /clover-admin-vue/dist /clover-admin-vue 25 | # 从builder中复制/clover-admin-vue/docker/nginx.conf到/etc/nginx/nginx.conf(此即服务器上的nginx配置文件位置) 26 | COPY --from=builder /clover-admin-vue/docker/nginx.conf /etc/nginx/nginx.conf 27 | # 向外界暴露容器的80端口 28 | EXPOSE 80 29 | -------------------------------------------------------------------------------- /docker/nginx.conf: -------------------------------------------------------------------------------- 1 | user nginx; # 指定Nginx服务器的用户名 2 | worker_processes 1; # 指定服务器上只有一个Nginx进程(避免多进程问题) 3 | error_log /var/log/nginx/error.log warn; # 指定Nginx日志目录和记录的错误级别(warn) 4 | pid /var/run/nginx.pid; # 指定Nginx进程的PID文件路径 5 | 6 | events { 7 | worker_connections 1024; # 指定服务器可同时处理1024个请求 8 | } 9 | 10 | http { 11 | include /etc/nginx/mime.types; # 让Nginx可以识别服务器上的文件(mime.types包含了网页服务器可以识别的文件类型) 12 | default_type application/octet-stream; # 指定默认的请求内容类型(此处使用二进制格式) 13 | log_format main '$remote_addr - $remote_user [$time_local] "$request" ' 14 | '$status $body_bytes_sent "$http_referer" ' 15 | '"$http_user_agent" "$http_x_forwarded_for"'; # 指定日志格式(命名为main), 依次记录: 客户端的IP地址、用户名、访问时间、请求URL、HTTP响应状态码、发送的字节数、来源URL、用户代理、X-Forwaeded-For头 16 | access_log /var/log/nginx/access.log main; # 指定Nginx的访问日志目录, 并指定日志格式为main 17 | sendfile on; # 允许Nginx服务器直接从磁盘读取文件(可提供Nginx服务器性能, 但会增加服务器负载) 18 | keepalive_timeout 65; # 如何客户端在65秒内没有发送下一个请求, Web服务器将断开连接 19 | 20 | server { 21 | listen 80; # 监听80端口 22 | server_name localhost; # 指定服务器的主机名或IP地址(设为localhost代表着只接受本地主机上的连接) 23 | 24 | location / { # URL匹配规则,告诉Nginx如何处理请求, 即将请求映射到待定的文件或目录 25 | # 不缓存html,防止程序更新后缓存继续生效 26 | if ($request_filename ~* .*\.(?:htm|html)$) { 27 | add_header Cache-Control "private, no-store, no-cache, must-revalidate, proxy-revalidate"; # 添加响应头 28 | access_log on; # 记录访问日志 29 | } 30 | root /clover-admin-vue/; # 根目录, 该目录下的所有文件和目录都可被访问 31 | index index.html index.htm; # 默认索引页面, 优先使用index.html, 若找不到则尝试使用index.htm 32 | try_files $uri $uri/ /index.html; # nginx会尝试使用请求的URI,然后尝试使用URI加上斜杠, 最后尝试使用index.html, 都尝试失败则返回404 33 | } 34 | 35 | location /clover-api/v1 { 36 | proxy_set_header Host $host; # 设置代理服务器发送到主机的HTTP请求头 37 | proxy_set_header X-Real-IP $remote_addr; # 设置X-Real-IP头, 包含客户端的真实IP地址 38 | proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; # 设置X-Forwarded-For头, 包含客户端的IP地址 39 | proxy_set_header REMOTE-HOST $remote_addr; # 设置REMOTE-HOST头, 值为客户端的远程主机地址 40 | 41 | proxy_pass http://server.wzc520pyf.cn/api/v1; # 后台接口地址 42 | proxy_redirect default; # 指定代理服务器如何重定向客户端请求(可以是URL或状态码) 43 | # 跨越设置 44 | add_header Access-Control-Allow-Origin *; # 允许所有源访问资源 45 | add_header Access-Control-Allow-Headers X-Requested-With; # 允许X-Requested-With头部(这个头部被用来标识Ajax请求)的请求通过跨域访问 46 | add_header Access-Control-Allow-Methods GET,POST,OPTIONS; # 允许的HTTP请求方法 47 | } 48 | 49 | error_page 500 502 503 504 /50x.html; # 指定发生错误时返回的html文件 50 | location = /50x.html { # 匹配规则 51 | root /usr/share/nginx/html; # 根目录 52 | } 53 | } 54 | } 55 | -------------------------------------------------------------------------------- /index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | <%= appName %> 8 | 9 | 10 | 11 |
12 |
13 | 14 |
15 |
16 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /mock/api/auth.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from "vite-plugin-mock"; 2 | 3 | /** 参数错误的状态码 */ 4 | const ERROR_PARAM_CODE = 10000; 5 | 6 | const ERROR_PARAM_MSG = "参数校验失败!"; 7 | 8 | const apis: MockMethod[] = [ 9 | // 用户帐密登录 10 | { 11 | url: "/mock/login", 12 | method: "post", 13 | statusCode: 200, 14 | response: () => { 15 | return { 16 | code: 200, 17 | message: "成功", 18 | data: { 19 | token: "这是Token", 20 | }, 21 | }; 22 | }, 23 | }, 24 | ]; 25 | 26 | export default apis; 27 | -------------------------------------------------------------------------------- /mock/api/example.ts: -------------------------------------------------------------------------------- 1 | import type { MockMethod } from "vite-plugin-mock"; 2 | 3 | /** 参数错误的状态码 */ 4 | const ERROR_PARAM_CODE = 10000; 5 | 6 | const ERROR_PARAM_MSG = "参数校验失败!"; 7 | 8 | const apis: MockMethod[] = [ 9 | // 404 10 | { 11 | url: "/mock/404", 12 | method: "post", 13 | statusCode: 404, 14 | response: () => { 15 | return { 16 | code: 404, 17 | message: "找不到资源!", 18 | data: "error", 19 | }; 20 | }, 21 | }, 22 | // 获取data 23 | { 24 | url: "/mock/example-data", 25 | method: "get", 26 | statusCode: 200, 27 | response: () => { 28 | return { 29 | code: 200, 30 | message: "成功", 31 | data: { 32 | list: [{ name: "jack" }], 33 | }, 34 | }; 35 | }, 36 | }, 37 | // 响应数据不包含data 38 | { 39 | url: "/mock/example-no-data", 40 | method: "get", 41 | statusCode: 200, 42 | response: () => { 43 | return { 44 | code: 200, 45 | message: "成功", 46 | }; 47 | }, 48 | }, 49 | // 获取headers 50 | { 51 | url: "/mock/example-headers", 52 | method: "get", 53 | statusCode: 200, 54 | response: () => { 55 | return { 56 | code: 200, 57 | message: "成功", 58 | data: {}, 59 | }; 60 | }, 61 | }, 62 | ]; 63 | 64 | export default apis; 65 | -------------------------------------------------------------------------------- /mock/api/index.ts: -------------------------------------------------------------------------------- 1 | import auth from "./auth"; 2 | import example from "./example"; 3 | 4 | export default [...auth, ...example]; 5 | -------------------------------------------------------------------------------- /mock/index.ts: -------------------------------------------------------------------------------- 1 | import { createProdMockServer } from "vite-plugin-mock/es/createProdMockServer"; 2 | import api from "./api"; 3 | 4 | export function setupMockServer() { 5 | createProdMockServer(api); 6 | } 7 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | ## This rule redirects to an external API, signing requests with a secret 2 | [[redirects]] 3 | from = "/clover-api/*" 4 | to = "http://server.wzc520pyf.cn/api/:splat" 5 | status = 200 6 | force = true # COMMENT: ensure that we always redirect 7 | headers = {X-From = "Netlify"} 8 | 9 | ## history 路由模式 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | -------------------------------------------------------------------------------- /public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/public/favicon.ico -------------------------------------------------------------------------------- /public/files/test.xlsx: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/public/files/test.xlsx -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 32 | 33 | 34 | -------------------------------------------------------------------------------- /src/assets/images/example.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/wzc520pyfm/clover-admin-vue/187c6536c565f0939dfac3e8bc986004d0850560/src/assets/images/example.png -------------------------------------------------------------------------------- /src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/svg-icon/link-icon.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | -------------------------------------------------------------------------------- /src/assets/svg-icon/verify.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /src/components/__tests__/ExceptionBase.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { shallowMount } from "@vue/test-utils"; 3 | import ExceptionBase from "../common/ExceptionBase.vue"; 4 | 5 | describe("ExceptionBase", () => { 6 | const wrapper = shallowMount(ExceptionBase); 7 | it("should have the correct component name", () => { 8 | expect(wrapper.vm.$options.name).toBe("ExceptionBase"); 9 | }); 10 | it("render the icon component for 403", async () => { 11 | await wrapper.setProps({ type: "403" }); 12 | expect(wrapper.find("i-local-403").exists()).toBe(true); 13 | }); 14 | it("render the icon component for 404", async () => { 15 | await wrapper.setProps({ type: "404" }); 16 | expect(wrapper.find("i-local-404").exists()).toBe(true); 17 | }); 18 | it("render the icon component for 500", () => { 19 | // 可以在测试用例中独立挂载 wrapper, 但这会导致额外开销, 推荐在外部挂载wrapper, 在用例中使用setProps()方法 20 | const wrapper = shallowMount(ExceptionBase, { propsData: { type: "500" } }); 21 | expect(wrapper.find("i-local-500").exists()).toBe(true); 22 | }); 23 | it("renders a router link to home page", () => { 24 | expect(wrapper.find("router-link").exists()).toBe(true); 25 | expect(wrapper.find("router-link").attributes("to")).toBe("/home"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/__tests__/WebSiteLink.spec.ts: -------------------------------------------------------------------------------- 1 | import { describe, expect, it } from "vitest"; 2 | import { shallowMount } from "@vue/test-utils"; 3 | import WebSiteLink from "../custom/WebSiteLink.vue"; 4 | 5 | describe("WebSiteLink.vue", () => { 6 | it("renders props.label when passed", () => { 7 | const label = "Google"; 8 | const link = "https://www.google.com"; 9 | const wrapper = shallowMount(WebSiteLink, { propsData: { label, link } }); 10 | expect(wrapper.text()).toMatch(label); 11 | }); 12 | 13 | it("renders props.link when passed", () => { 14 | const label = "Google"; 15 | const link = "https://www.google.com"; 16 | const wrapper = shallowMount(WebSiteLink, { propsData: { label, link } }); 17 | expect(wrapper.find("el-link").attributes("href")).toBe(link); 18 | }); 19 | 20 | it("renders props.isBlank when passed", () => { 21 | const label = "Google"; 22 | const link = "https://www.google.com"; 23 | const isBlank = true; 24 | const wrapper = shallowMount(WebSiteLink, { propsData: { label, link, isBlank } }); 25 | expect(wrapper.find("el-link").attributes("target")).toBe("_blank"); 26 | }); 27 | }); 28 | -------------------------------------------------------------------------------- /src/components/business/ImportExcel.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 72 | 73 | 74 | -------------------------------------------------------------------------------- /src/components/business/Loading.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/components/common/AdminLayout.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 43 | 44 | 56 | -------------------------------------------------------------------------------- /src/components/common/DarkModeContainer.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /src/components/common/ElementProvider.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/common/ExceptionBase.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 27 | 28 | 29 | -------------------------------------------------------------------------------- /src/components/common/HoverContainer.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 41 | 42 | 43 | -------------------------------------------------------------------------------- /src/components/custom/BetterScroll.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 48 | 49 | 50 | -------------------------------------------------------------------------------- /src/components/custom/CountTo.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 115 | 116 | 117 | -------------------------------------------------------------------------------- /src/components/custom/GithubLink.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/components/custom/SvgIcon.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 45 | 46 | 47 | -------------------------------------------------------------------------------- /src/components/custom/WebSiteLink.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/components/icons/IconLogo.vue: -------------------------------------------------------------------------------- 1 | 17 | -------------------------------------------------------------------------------- /src/composables/events.ts: -------------------------------------------------------------------------------- 1 | import { useTabStore } from "@/stores"; 2 | import { useEventListener } from "@vueuse/core"; 3 | 4 | /** 全局事件 */ 5 | export function useGlobalEvents() { 6 | const tab = useTabStore(); 7 | 8 | /** 页面离开时缓存多页签数据 */ 9 | useEventListener(window, "beforeunload", () => { 10 | tab.cacheTabRoutes(); 11 | }); 12 | } 13 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./router"; 2 | export * from "./system"; 3 | export * from "./echarts"; 4 | export * from "./layout"; 5 | export * from "./events"; 6 | export * from "./excel"; 7 | -------------------------------------------------------------------------------- /src/composables/layout.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from "@vueuse/core"; 2 | 3 | export function useBasicLayout() { 4 | const breakpoints = useBreakpoints(breakpointsTailwind); 5 | const isMobile = breakpoints.smaller("sm"); 6 | 7 | return { isMobile }; 8 | } 9 | -------------------------------------------------------------------------------- /src/composables/router.ts: -------------------------------------------------------------------------------- 1 | import { useRouter } from "vue-router"; 2 | import type { RouteLocationRaw } from "vue-router"; 3 | import { router as globalRouter } from "@/router"; 4 | /** 5 | * 路由跳转 6 | * @param inSetup - 是否在vue页面/组件的setup里面调用,在axios里面无法使用useRouter和useRoute 7 | */ 8 | export function useRouterPush(inSetup = true) { 9 | const router = inSetup ? useRouter() : globalRouter; 10 | const route = globalRouter.currentRoute; 11 | 12 | /** 13 | * 路由跳转 14 | * @param to - 需要跳转的路由 15 | * @param newTab - 是否在新的浏览器Tab标签打开 16 | */ 17 | function routerPush(to: RouteLocationRaw, newTab = false) { 18 | if (newTab) { 19 | const routerData = router.resolve(to); 20 | window.open(routerData.href, "_blank"); 21 | return Promise.resolve(); 22 | } else { 23 | router.push(to); 24 | } 25 | } 26 | 27 | /** 返回上一级路由 */ 28 | function routerBack() { 29 | router.go(-1); 30 | } 31 | 32 | return { 33 | routerPush, 34 | routerBack, 35 | }; 36 | } 37 | -------------------------------------------------------------------------------- /src/composables/system.ts: -------------------------------------------------------------------------------- 1 | import { useAuthStore } from "@/stores/modules/auth"; 2 | import { isArray, isString } from "@/utils"; 3 | 4 | /** 权限判断 */ 5 | export function usePermission() { 6 | const auth = useAuthStore(); 7 | 8 | function hasPermission(permission: Auth.RoleType | Auth.RoleType[]) { 9 | const { userRole } = auth.userInfo; 10 | 11 | let has = userRole === "super"; 12 | if (!has) { 13 | if (isArray(permission)) { 14 | has = (permission as Auth.RoleType[]).includes(userRole); 15 | } 16 | if (isString(permission)) { 17 | has = (permission as Auth.RoleType) === userRole; 18 | } 19 | } 20 | return has; 21 | } 22 | 23 | return { 24 | hasPermission, 25 | }; 26 | } 27 | -------------------------------------------------------------------------------- /src/config/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./map-sdk"; 2 | export * from "./service"; 3 | -------------------------------------------------------------------------------- /src/config/map-sdk.ts: -------------------------------------------------------------------------------- 1 | /** 高德地图sdk地址 */ 2 | export const GAODE_MAP_SDK_URL = 3 | "https://webapi.amap.com/maps?v=2.0&key=abacc5e3e24e0860f091ba6d5ae0cc8e"; 4 | -------------------------------------------------------------------------------- /src/config/service.ts: -------------------------------------------------------------------------------- 1 | /** 请求超时时间 */ 2 | export const REQUEST_TIMEOUT = 60 * 1000; 3 | 4 | /** 错误信息的显示时间 */ 5 | export const ERROR_MSG_DURATION = 3 * 1000; 6 | 7 | /** 默认的请求错误code */ 8 | export const DEFAULT_REQUEST_ERROR_CODE = "DEFAULT"; 9 | 10 | /** 默认的请求错误文本 */ 11 | export const DEFAULT_REQUEST_ERROR_MSG = "请求错误!"; 12 | 13 | /** 请求超时的错误code */ 14 | export const REQUEST_TIMEOUT_CODE = "ECONNABORTED"; 15 | 16 | /** 请求超时的错误文本 */ 17 | export const REQUEST_TIMEOUT_MSG = "请求超时!"; 18 | 19 | /** 网络不可用的code */ 20 | export const NETWORK_ERROR_CODE = "NETWORK_ERROR"; 21 | 22 | /** 网络不可用的错误文本 */ 23 | export const NETWORK_ERROR_MSG = "网络不可用!"; 24 | 25 | /** 请求不成功的状态码对应的错误 */ 26 | export const ERROR_STATUS = { 27 | 400: "400: 请求出现语法错误!", 28 | 401: "401: 未授权!", 29 | 403: "403: 禁止访问!", 30 | 404: "404: 请求的资源不存在!", 31 | 405: "405: 不被允许的请求方法!", 32 | 408: "408: 请求超时!", 33 | 500: "500: 服务器发生意外错误!", 34 | 501: "501: 暂未实施的请求!", 35 | 502: "502: 网关错误!", 36 | 503: "503: 服务不可用!", 37 | 504: "504: 网关超时!", 38 | 505: "505: 当前HTTP版本不支持该请求!", 39 | [DEFAULT_REQUEST_ERROR_CODE]: DEFAULT_REQUEST_ERROR_MSG, 40 | }; 41 | 42 | /** 不弹出错误信息的code */ 43 | export const NO_ERROR_MSG_CODE: (string | number)[] = []; 44 | -------------------------------------------------------------------------------- /src/constants/business.ts: -------------------------------------------------------------------------------- 1 | /** 用户性别 */ 2 | const GENDER: Record = { 3 | MALE: "男", 4 | FEMALE: "女", 5 | }; 6 | 7 | const GENDER_OPTIONS: Array> = [ 8 | { value: "MALE", label: GENDER["MALE"] }, 9 | { value: "FEMALE", label: GENDER["FEMALE"] }, 10 | ]; 11 | 12 | /** 用户状态 */ 13 | const USER_STATUS: Record = { 14 | ENABLE: "启用", 15 | DISABLE: "禁用", 16 | DELETED: "已删除", 17 | }; 18 | 19 | const USER_STATUS_OPTIONS: Array> = [ 20 | { value: "ENABLE", label: USER_STATUS["ENABLE"] }, 21 | { value: "DISABLE", label: USER_STATUS["DISABLE"] }, 22 | { value: "DELETED", label: USER_STATUS["DELETED"] }, 23 | ]; 24 | 25 | // 常量枚举 26 | const VALUE_CONSTANT = { 27 | GENDER: 1, 28 | USER_STATUS: 2, 29 | }; 30 | 31 | const VALUE_CONSTANT_MAPPING = [ 32 | { 33 | type: 1, 34 | data: GENDER_OPTIONS, 35 | }, 36 | { 37 | type: 2, 38 | data: USER_STATUS_OPTIONS, 39 | }, 40 | ]; 41 | 42 | const GLOBAL_PAGE_INDEX = 1; 43 | const GLOBAL_PAGE_SIZE = 10; 44 | const GLOBAL_MAX_PAGE_SIZE = 100; 45 | const SUCCESS = 200; 46 | const MEAN_LESS_NUM = -1; 47 | const MAX_NUMBER = 2 ** (32 - 1) - 1; 48 | 49 | export { 50 | GLOBAL_PAGE_INDEX, 51 | GLOBAL_PAGE_SIZE, 52 | GLOBAL_MAX_PAGE_SIZE, 53 | SUCCESS, 54 | MEAN_LESS_NUM, 55 | MAX_NUMBER, 56 | // 常量 57 | GENDER, 58 | GENDER_OPTIONS, 59 | USER_STATUS, 60 | USER_STATUS_OPTIONS, 61 | // 常量枚举 62 | VALUE_CONSTANT, 63 | VALUE_CONSTANT_MAPPING, 64 | }; 65 | -------------------------------------------------------------------------------- /src/constants/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./business"; 2 | -------------------------------------------------------------------------------- /src/context/count.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "@/hooks"; 2 | import type { Ref } from "vue"; 3 | 4 | interface CountContext { 5 | counts: Ref; 6 | setCounts: (count: number) => void; 7 | } 8 | 9 | const { useProvide: useCountProvide, useInject: useCountInject } = useContext(); 10 | 11 | export function useCountContext() { 12 | let counts = $ref(0); 13 | 14 | function setCounts(count: number) { 15 | counts = count; 16 | } 17 | 18 | const countContext: CountContext = { 19 | counts: $$(counts), 20 | setCounts, 21 | }; 22 | 23 | function useProvide() { 24 | return useCountProvide(countContext); 25 | } 26 | 27 | return { 28 | useProvide, 29 | useInject: useCountInject, 30 | }; 31 | } 32 | 33 | /** 34 | * 此示例展示了如何在组件间共享上下文: 35 | * A.vue为父组件, B.vue为子孙组件, C.vue为子孙组件 36 | * 37 | * A.vue 38 | * import { useCountContext } from "@/context"; 39 | * const { useProvide } = useCountContext(); 40 | * const { counts, setCounts } = useProvide(); 41 | * 42 | * B.vue和C.vue : 共享counts 43 | * import { useCountContext } from "@/context"; 44 | * const { useInject } = useCountContext(); 45 | * const { counts, setCounts } = useInject(); 46 | */ 47 | -------------------------------------------------------------------------------- /src/context/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./count"; 2 | export * from "./stepForm"; 3 | -------------------------------------------------------------------------------- /src/context/stepForm.ts: -------------------------------------------------------------------------------- 1 | import { useContext } from "@/hooks"; 2 | import type { Ref } from "vue"; 3 | 4 | type Step = 0 | 1 | 2 | 3; 5 | 6 | interface Person { 7 | name: string; 8 | age: number; 9 | isLikeBasketball: boolean; 10 | } 11 | 12 | interface StepFormContext { 13 | step: Ref; 14 | person: Ref; 15 | setStep: (value: Step) => void; 16 | addStep: () => void; 17 | } 18 | 19 | const { useProvide: useStepFormProvide, useInject: useStepFormInject } = 20 | useContext(); 21 | 22 | export function useStepFormContext() { 23 | let step = $ref(0); 24 | const person = $ref({ 25 | name: "", 26 | age: 18, 27 | isLikeBasketball: false, 28 | }); 29 | 30 | const stepFormContext: StepFormContext = { 31 | step: $$(step), 32 | person: $$(person), 33 | setStep, 34 | addStep, 35 | }; 36 | 37 | function setStep(value: Step) { 38 | step = value; 39 | } 40 | 41 | function addStep() { 42 | step++; 43 | } 44 | 45 | function useProvide() { 46 | return useStepFormProvide(stepFormContext); 47 | } 48 | 49 | return { 50 | useProvide, 51 | useInject: useStepFormInject, 52 | }; 53 | } 54 | -------------------------------------------------------------------------------- /src/directives/clipboard.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from "vue"; 2 | import { useClipboard } from "@vueuse/core"; 3 | 4 | export type ClipboardDirective = typeof clipboardDirective; 5 | 6 | let cache: string; 7 | 8 | function handleCopy(value: string) { 9 | const { copy, isSupported } = useClipboard(); 10 | if (!isSupported) { 11 | window.$message?.error("您的浏览器不支持Clipboard API!"); 12 | return; 13 | } 14 | if (!value) { 15 | window.$message?.warning("请输入要复制的内容"); 16 | return; 17 | } 18 | copy(value); 19 | window.$message?.success(`复制成功: ${value}`); 20 | } 21 | 22 | function onClick() { 23 | handleCopy(cache); 24 | } 25 | 26 | /** 27 | * 剪切板指令 28 | */ 29 | export const clipboardDirective: Directive = { 30 | beforeMount(el, binding) { 31 | cache = binding.value; 32 | el.addEventListener("click", onClick); 33 | }, 34 | beforeUpdate(el, binding) { 35 | cache = binding.value; 36 | }, 37 | }; 38 | 39 | export const VClipboard = "clipboard"; 40 | 41 | export default function setupClipboardDirective(app: App) { 42 | app.directive(VClipboard, clipboardDirective); 43 | } 44 | -------------------------------------------------------------------------------- /src/directives/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import setupPermissionDirective from "./permission"; 3 | import setupClipboardDirective from "./clipboard"; 4 | 5 | /** setup custom vue directives. */ 6 | export function setupDirectives(app: App) { 7 | setupPermissionDirective(app); 8 | setupClipboardDirective(app); 9 | } 10 | -------------------------------------------------------------------------------- /src/directives/permission.ts: -------------------------------------------------------------------------------- 1 | import type { App, Directive } from "vue"; 2 | import { usePermission } from "@/composables"; 3 | 4 | export type PermissionDirective = typeof permissionDirective; 5 | 6 | /** 7 | * 鉴权函数 8 | * @param el 指令绑定到的元素 9 | * @param permission 权限 10 | */ 11 | function updateElVisible(el: HTMLElement, permission: Auth.RoleType | Auth.RoleType[]) { 12 | const { hasPermission } = usePermission(); 13 | 14 | if (!permission) { 15 | throw new Error(`need roles: like v-permission="'admin'" or v-permission="['admin', 'test']"`); 16 | } 17 | if (!hasPermission(permission)) { 18 | el.parentElement?.removeChild(el); 19 | } 20 | } 21 | 22 | /** 23 | * 权限指令 24 | */ 25 | export const permissionDirective: Directive = { 26 | mounted(el, binding) { 27 | updateElVisible(el, binding.value); 28 | }, 29 | beforeUpdate(el, binding) { 30 | updateElVisible(el, binding.value); 31 | }, 32 | }; 33 | 34 | export const VPermission = "permission"; 35 | 36 | export default function setupPermissionDirective(app: App) { 37 | app.directive(VPermission, permissionDirective); 38 | } 39 | -------------------------------------------------------------------------------- /src/enum/business.ts: -------------------------------------------------------------------------------- 1 | /** 用户角色 */ 2 | export enum EnumUserRole { 3 | super = "超级管理员", 4 | admin = "管理员", 5 | user = "普通用户", 6 | } 7 | -------------------------------------------------------------------------------- /src/enum/common.ts: -------------------------------------------------------------------------------- 1 | /** http请求头的content-type类型 */ 2 | export enum EnumContentType { 3 | json = "application/json", 4 | formUrlencoded = "application/x-www-form-urlencoded", 5 | formData = "multipart/form-data", 6 | } 7 | 8 | /** 缓存的key */ 9 | export enum EnumStorageKey { 10 | /** 用户token */ 11 | "token" = "__TOKEN__", 12 | /** 用户刷新token */ 13 | "refresh-token" = "__REFRESH_TOKEN__", 14 | /** 用户信息 */ 15 | "user-info" = "__USER_INFO__", 16 | /** 多页签路由信息 */ 17 | "multi-tab-routes" = "__MULTI_TAB_ROUTES__", 18 | } 19 | 20 | /** 数据类型 */ 21 | export enum EnumDataType { 22 | number = "[object Number]", 23 | string = "[object String]", 24 | boolean = "[object Boolean]", 25 | null = "[object Null]", 26 | undefined = "[object Undefined]", 27 | object = "[object Object]", 28 | array = "[object Array]", 29 | date = "[object Date]", 30 | regexp = "[object RegExp]", 31 | set = "[object Set]", 32 | map = "[object Map]", 33 | file = "[object File]", 34 | function = "function", 35 | } 36 | -------------------------------------------------------------------------------- /src/enum/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./business"; 2 | export * from "./common"; 3 | -------------------------------------------------------------------------------- /src/globalProperties.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { isArray } from "./utils"; 3 | import * as filters from "./utils/filters"; 4 | 5 | /** setup custom vue globalProperties */ 6 | /** 7 | * how to define type? 8 | * - please go to /src/typing/vue.d.ts 9 | * how to use in setup? 10 | * - const vm = getCurrentInstance()!; 11 | * - const { $filters } = vm.appContext.config.globalProperties; 12 | * how to use in template? 13 | * -

{{ $filters }}

14 | */ 15 | export function setupGlobalProperties(app: App) { 16 | installGlobalProperties(filters, "$filters"); 17 | 18 | function installGlobalProperties(fn: T, name: string) { 19 | if (isArray(fn)) { 20 | app.config.globalProperties[name] = { ...fn }; 21 | } else { 22 | app.config.globalProperties[name] = fn; 23 | } 24 | 25 | return fn; 26 | } 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/business/index.ts: -------------------------------------------------------------------------------- 1 | import useAliOSS from "./useAliOSS"; 2 | 3 | export { useAliOSS }; 4 | -------------------------------------------------------------------------------- /src/hooks/business/useAliOSS.ts: -------------------------------------------------------------------------------- 1 | import { createOSSClient } from "@/sdk"; 2 | import type { PutObjectOptions } from "ali-oss"; 3 | 4 | /** 5 | * AliOSS 6 | */ 7 | export default async function useAliOSS() { 8 | const client = await createOSSClient(); 9 | if (!client) throw new Error("OSS client is null"); 10 | 11 | /** 12 | * 上传文件 13 | */ 14 | const upload = async (name: string, file: File, headers: PutObjectOptions) => { 15 | const result = await client.put(name, file, headers); 16 | return result; 17 | }; 18 | 19 | /** 20 | * 预览文件 21 | */ 22 | const preview = async (name: string) => { 23 | const result = await client.signatureUrl(name, { 24 | expires: 3600, 25 | process: "imm/previewdoc,copy_1", 26 | response: { 27 | "content-disposition": "inline", 28 | }, 29 | }); 30 | return result; 31 | }; 32 | 33 | /** 34 | * 下载文件 35 | */ 36 | const download = async (name: string) => { 37 | const result = await client.signatureUrl(name, { 38 | expires: 3600, 39 | response: { 40 | "content-disposition": "attachment", 41 | }, 42 | }); 43 | return result; 44 | }; 45 | 46 | /** 47 | * 删除文件 48 | */ 49 | const remove = async (name: string) => { 50 | const result = await client.delete(name); 51 | return result; 52 | }; 53 | 54 | /** 55 | * 列举文件 56 | */ 57 | const list = async (prefix: string) => { 58 | const result = await client.list( 59 | { 60 | prefix, 61 | "max-keys": 1000, 62 | }, 63 | { 64 | timeout: 60000, 65 | } 66 | ); 67 | return result; 68 | }; 69 | 70 | return { 71 | client, 72 | upload, 73 | preview, 74 | remove, 75 | download, 76 | list, 77 | }; 78 | } 79 | -------------------------------------------------------------------------------- /src/hooks/common/index.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from "./useBoolean"; 2 | import useLoading from "./useLoading"; 3 | import useContext from "./useContext"; 4 | 5 | export { useBoolean, useLoading, useContext }; 6 | -------------------------------------------------------------------------------- /src/hooks/common/useBoolean.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * boolean组合式函数 3 | * @param initValue 初始值 4 | */ 5 | export default function useBoolean(initValue = false) { 6 | const bool = ref(initValue); 7 | 8 | function setBool(value: boolean) { 9 | bool.value = value; 10 | } 11 | function setTrue() { 12 | setBool(true); 13 | } 14 | function setFalse() { 15 | setBool(false); 16 | } 17 | function toggle() { 18 | setBool(!bool.value); 19 | } 20 | 21 | return { 22 | bool, 23 | setBool, 24 | setTrue, 25 | setFalse, 26 | toggle, 27 | }; 28 | } 29 | -------------------------------------------------------------------------------- /src/hooks/common/useContext.ts: -------------------------------------------------------------------------------- 1 | import type { InjectionKey } from "vue"; 2 | 3 | /** 创建共享上下文状态 */ 4 | export default function useContext(contextName = "context") { 5 | const injectKey: InjectionKey = Symbol(contextName); 6 | 7 | function useProvide(context: T) { 8 | provide(injectKey, context); 9 | return context; 10 | } 11 | 12 | function useInject() { 13 | return inject(injectKey) as T; 14 | } 15 | 16 | return { 17 | useProvide, 18 | useInject, 19 | }; 20 | } 21 | -------------------------------------------------------------------------------- /src/hooks/common/useLoading.ts: -------------------------------------------------------------------------------- 1 | import useBoolean from "./useBoolean"; 2 | 3 | export default function useLoading(initValue = false) { 4 | const { bool: loading, setTrue: startLoading, setFalse: endLoading } = useBoolean(initValue); 5 | 6 | return { 7 | loading, 8 | startLoading, 9 | endLoading, 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /src/hooks/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./common"; 2 | export * from "./business"; 3 | -------------------------------------------------------------------------------- /src/layout/BasicLayout/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/layout/BlankLayout/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/layout/common/AppMain.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 38 | 39 | 40 | -------------------------------------------------------------------------------- /src/layout/common/Footer/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 12 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/Breadcrumb.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/FullScreen.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/GithubSite.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 14 | 15 | 16 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/MenuCollapse.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 17 | 18 | 19 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/ThemeModel.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 20 | 21 | 22 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/UserAvatar.vue: -------------------------------------------------------------------------------- 1 | 24 | 25 | 63 | 64 | 65 | -------------------------------------------------------------------------------- /src/layout/common/Header/components/index.ts: -------------------------------------------------------------------------------- 1 | import MenuCollapse from "./MenuCollapse.vue"; 2 | import Breadcrumb from "./Breadcrumb.vue"; 3 | import UserAvatar from "./UserAvatar.vue"; 4 | import FullScreen from "./FullScreen.vue"; 5 | import GithubSite from "./GithubSite.vue"; 6 | import ThemeModel from "./ThemeModel.vue"; 7 | 8 | export { MenuCollapse, Breadcrumb, UserAvatar, FullScreen, GithubSite, ThemeModel }; 9 | -------------------------------------------------------------------------------- /src/layout/common/Header/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 28 | 29 | 34 | -------------------------------------------------------------------------------- /src/layout/common/Icon/index.tsx: -------------------------------------------------------------------------------- 1 | import SvgIcon from "@/components/custom/SvgIcon.vue"; 2 | 3 | interface MenuIconProps { 4 | iconName: string | undefined; 5 | } 6 | 7 | export function LayoutIcon(props: MenuIconProps) { 8 | let vnode: JSX.Element; 9 | let { iconName } = props; 10 | iconName = iconName ?? "ep-eleme"; 11 | if (isLocalIcon(iconName)) { 12 | vnode = ; 13 | } else { 14 | vnode = ; 15 | } 16 | return vnode; 17 | } 18 | LayoutIcon.props = ["iconName"]; 19 | 20 | // 处理本地icon名 21 | function handleLocalIconName(iconName: string) { 22 | return iconName.split(/^local-/)[1]; 23 | } 24 | 25 | // 判断是否是本地icon名 26 | function isLocalIcon(iconName: string) { 27 | return iconName.startsWith("local-"); 28 | } 29 | -------------------------------------------------------------------------------- /src/layout/common/Logo/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 24 | 25 | 26 | -------------------------------------------------------------------------------- /src/layout/common/Sidebar/components/SidebarItem.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 34 | 35 | 43 | -------------------------------------------------------------------------------- /src/layout/common/Sidebar/components/index.ts: -------------------------------------------------------------------------------- 1 | import SidebarItem from "./SidebarItem.vue"; 2 | 3 | export { SidebarItem }; 4 | -------------------------------------------------------------------------------- /src/layout/common/Sidebar/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 37 | 38 | 85 | -------------------------------------------------------------------------------- /src/layout/common/Tab/components/ReloadButton/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 26 | 27 | 28 | -------------------------------------------------------------------------------- /src/layout/common/Tab/components/TabDetail/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 42 | 43 | 57 | -------------------------------------------------------------------------------- /src/layout/common/Tab/components/index.ts: -------------------------------------------------------------------------------- 1 | import TabDetail from "./TabDetail/index.vue"; 2 | import ReloadButton from "./ReloadButton/index.vue"; 3 | 4 | export { TabDetail, ReloadButton }; 5 | -------------------------------------------------------------------------------- /src/layout/common/Tab/index.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /src/layout/common/index.ts: -------------------------------------------------------------------------------- 1 | import Header from "./Header/index.vue"; 2 | import Tab from "./Tab/index.vue"; 3 | import Sidebar from "./Sidebar/index.vue"; 4 | import AppMain from "./AppMain.vue"; 5 | import Footer from "./Footer/index.vue"; 6 | import GlobalLogo from "./Logo/index.vue"; 7 | 8 | export { Header, Tab, Sidebar, AppMain, Footer, GlobalLogo }; 9 | -------------------------------------------------------------------------------- /src/layout/index.ts: -------------------------------------------------------------------------------- 1 | const Layout = () => import("./BasicLayout/index.vue"); 2 | const Blank = () => import("./BlankLayout/index.vue"); 3 | 4 | export { Layout, Blank }; 5 | export default Layout; 6 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import { createApp } from "vue"; 2 | 3 | import App from "./App.vue"; 4 | import { setupDirectives } from "./directives"; 5 | import { setupRouter } from "./router"; 6 | import { setupAssets } from "./plugins"; 7 | import { setupStore } from "./stores"; 8 | import { setupGlobalProperties } from "./globalProperties"; 9 | 10 | async function setupApp() { 11 | // import assets: js, css, images, fonts, etc. 12 | setupAssets(); 13 | 14 | const app = createApp(App); 15 | 16 | // setup vue store plugin: pinia. 17 | setupStore(app); 18 | 19 | // vue custom directives 20 | setupDirectives(app); 21 | 22 | // vue custom globalProperties 23 | setupGlobalProperties(app); 24 | 25 | // vue router 26 | await setupRouter(app); 27 | 28 | // mount app 29 | app.mount("#app"); 30 | } 31 | 32 | setupApp(); 33 | -------------------------------------------------------------------------------- /src/plugins/assets.ts: -------------------------------------------------------------------------------- 1 | import "uno.css"; 2 | import "swiper/css"; 3 | import "swiper/css/navigation"; 4 | import "swiper/css/pagination"; 5 | import "vue3-lottie/dist/style.css"; 6 | import "nprogress/nprogress.css"; // progress bar style 7 | import "element-plus/theme-chalk/dark/css-vars.css"; 8 | import "virtual:svg-icons-register"; 9 | import "../styles/css/main.css"; 10 | 11 | /** import static assets: css, js, font and so on. */ 12 | export default function setupAssets() {} 13 | -------------------------------------------------------------------------------- /src/plugins/index.ts: -------------------------------------------------------------------------------- 1 | import setupAssets from "./assets"; 2 | 3 | export { setupAssets }; 4 | -------------------------------------------------------------------------------- /src/router/guard/dynamic.ts: -------------------------------------------------------------------------------- 1 | import { useRouteStore } from "@/stores"; 2 | import { getToken } from "@/utils"; 3 | import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router"; 4 | 5 | /** 6 | * 动态路由 7 | */ 8 | export async function generateDynamicRoutes( 9 | to: RouteLocationNormalized, 10 | _from: RouteLocationNormalized, 11 | next: NavigationGuardNext 12 | ) { 13 | const route = useRouteStore(); 14 | const isLogin = Boolean(/** getToken() */ true); 15 | 16 | // 初始化权限路由 17 | if (!route.isInitAuthRoute) { 18 | // 未登录 19 | if (!isLogin) { 20 | const toName = to.name!; 21 | if (route.isValidConstantRoute(toName) && !to.meta.requiresAuth) { 22 | next(); 23 | } else { 24 | const redirect = to.fullPath; 25 | next({ name: "login", query: { redirect } }); 26 | } 27 | return false; 28 | } 29 | await route.initAuthRoute(); 30 | if (to.name === "not-found-page") { 31 | // 动态路由没有加载导致被not-found-page路由捕获,等待权限路由加载好了,回到之前的路由 32 | // 若路由是从根路由重定向过来的,重新回到根路由 33 | const ROOT_ROUTE_NAME = "root"; 34 | const path = to.redirectedFrom?.name === ROOT_ROUTE_NAME ? "/" : to.fullPath; 35 | next({ path, replace: true, query: to.query, hash: to.hash }); 36 | return false; 37 | } 38 | } 39 | 40 | // 权限路由已经加载完毕,仍然没有匹配到路由,跳转到404页面 41 | if (to.name === "not-found-page") { 42 | next({ name: "404", replace: true }); 43 | return false; 44 | } 45 | 46 | return true; 47 | } 48 | -------------------------------------------------------------------------------- /src/router/guard/index.ts: -------------------------------------------------------------------------------- 1 | import type { Router } from "vue-router"; 2 | import { handleRoutePermission } from "./permission"; 3 | 4 | /** 5 | * 路由守卫函数 6 | * @param router 路由实例 7 | */ 8 | export function setupRouterGuard(router: Router) { 9 | router.beforeEach(async (to, from, next) => { 10 | // 开始loading 11 | window.$loadingBar?.start(); 12 | // 页面跳转权限处理 13 | await handleRoutePermission(to, from, next); 14 | }); 15 | router.afterEach((to, from) => { 16 | // 结束loading 17 | window.$loadingBar?.done(); 18 | }); 19 | } 20 | -------------------------------------------------------------------------------- /src/router/guard/permission.ts: -------------------------------------------------------------------------------- 1 | import type { NavigationGuardNext, RouteLocationNormalized } from "vue-router"; 2 | import { generateDynamicRoutes } from "./dynamic"; 3 | 4 | /** 处理路由页面的权限 */ 5 | export async function handleRoutePermission( 6 | to: RouteLocationNormalized, 7 | from: RouteLocationNormalized, 8 | next: NavigationGuardNext 9 | ) { 10 | // 动态路由 11 | const permission = await generateDynamicRoutes(to, from, next); 12 | if (!permission) return; 13 | 14 | // 外链路由, 从新标签打开,返回上一个路由 15 | if (to.meta.href) { 16 | window.open(to.meta.href); 17 | next({ path: from.fullPath, replace: true, query: from.query }); 18 | return; 19 | } 20 | // ... 21 | next(); 22 | } 23 | -------------------------------------------------------------------------------- /src/router/helper/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./module"; 2 | -------------------------------------------------------------------------------- /src/router/helper/module.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由排序 3 | * @param routes - 路由 4 | */ 5 | export function sortRoutes(routes: AuthRoute.Route[]) { 6 | return routes.sort((next, pre) => Number(next.meta?.order ?? 0) - Number(pre.meta?.order ?? 0)); 7 | } 8 | 9 | /** 10 | * 处理路由模块 11 | * @param modules - 路由模块 12 | */ 13 | export function handleModuleRoutes(modules: Record) { 14 | const routes: AuthRoute.Route[] = []; 15 | Object.keys(modules).forEach((key) => { 16 | const route = modules[key].default; 17 | routes.push(route); 18 | }); 19 | 20 | return sortRoutes(routes.flat(1)); 21 | } 22 | -------------------------------------------------------------------------------- /src/router/index.ts: -------------------------------------------------------------------------------- 1 | import { createRouter, createWebHashHistory, createWebHistory } from "vue-router"; 2 | import { constantRoutes } from "./routes"; 3 | import type { App } from "vue"; 4 | import { setupRouterGuard } from "./guard"; 5 | 6 | const { VITE_HASH_ROUTE = "N", VITE_BASE_URL } = import.meta.env; 7 | 8 | export const router = createRouter({ 9 | history: 10 | VITE_HASH_ROUTE === "Y" ? createWebHashHistory(VITE_BASE_URL) : createWebHistory(VITE_BASE_URL), 11 | routes: [...constantRoutes], 12 | }); 13 | 14 | /** setup vue router. */ 15 | export async function setupRouter(app: App) { 16 | app.use(router); 17 | setupRouterGuard(router); 18 | await router.isReady(); 19 | } 20 | 21 | export * from "./routes"; 22 | export * from "./modules"; 23 | -------------------------------------------------------------------------------- /src/router/modules/about.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const about: AuthRoute.Route[] = [ 4 | { 5 | name: "about", 6 | path: "/about", 7 | component: Layout, 8 | redirect: "/about/index", 9 | meta: { title: "关于", hidden: true, order: 8 }, 10 | children: [ 11 | { 12 | path: "index", 13 | name: "about_index", 14 | component: () => import("@/views/about/index.vue"), 15 | meta: { 16 | title: "关于", 17 | icon: "ep-warning", 18 | }, 19 | }, 20 | ], 21 | }, 22 | ]; 23 | 24 | export default about; 25 | -------------------------------------------------------------------------------- /src/router/modules/auth-demo.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const authDemo = [ 4 | { 5 | name: "auth-demo", 6 | path: "/auth-demo", 7 | component: Layout, 8 | redirect: "/auth-demo/user", 9 | meta: { title: "权限示例", order: 4, icon: "ep-aim" }, 10 | children: [ 11 | { 12 | path: "user", 13 | name: "auth-demo-user", 14 | component: () => import("@/views/auth-demo/user/index.vue"), 15 | meta: { title: "普通用户可见", icon: "ep-user" }, 16 | }, 17 | { 18 | path: "super", 19 | name: "auth-demo-super", 20 | component: () => import("@/views/auth-demo/super/index.vue"), 21 | meta: { title: "超级管理员可见", icon: "ep-avatar", permissions: ["super"] }, 22 | }, 23 | { 24 | path: "permission", 25 | name: "auth-demo-permission", 26 | component: () => import("@/views/auth-demo/permission/index.vue"), 27 | meta: { title: "权限指令", icon: "ep-lock" }, 28 | }, 29 | ], 30 | }, 31 | ]; 32 | 33 | export default authDemo; 34 | -------------------------------------------------------------------------------- /src/router/modules/component.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const component = [ 4 | { 5 | name: "component", 6 | path: "/component", 7 | component: Layout, 8 | redirect: "/component/table", 9 | meta: { title: "组件示例", order: 5, icon: "ep-menu" }, 10 | children: [ 11 | { 12 | path: "table", 13 | name: "component_table", 14 | component: () => import("@/views/component/table/index.vue"), 15 | meta: { 16 | title: "表格", 17 | icon: "ep-grid", 18 | }, 19 | }, 20 | { 21 | path: "step-form", 22 | name: "component_step-form", 23 | component: () => import("@/views/component/step-form/index.vue"), 24 | meta: { 25 | title: "分步表单", 26 | icon: "ep-finished", 27 | }, 28 | }, 29 | { 30 | path: "complex-complex-form", 31 | name: "component_complex-form", 32 | component: () => import("@/views/component/complex-form/index.vue"), 33 | meta: { 34 | title: "复杂表单", 35 | icon: "mdi-form-select", 36 | }, 37 | }, 38 | { 39 | path: "complex-verify", 40 | name: "component_verify", 41 | component: () => import("@/views/component/verify/index.vue"), 42 | meta: { 43 | title: "验证组件", 44 | icon: "local-verify", 45 | }, 46 | }, 47 | ], 48 | }, 49 | ]; 50 | 51 | export default component; 52 | -------------------------------------------------------------------------------- /src/router/modules/dashboard.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const dashboard = [ 4 | { 5 | name: "dashboard", 6 | path: "/dashboard", 7 | component: Layout, 8 | redirect: "/dashboard/analysis", 9 | meta: { title: "仪表盘", order: 1, icon: "ep-house" }, 10 | children: [ 11 | { 12 | path: "analysis", 13 | name: "dashboard_analysis", 14 | component: () => import("@/views/dashboard/analysis/index.vue"), 15 | meta: { 16 | title: "分析页", 17 | key: "root", 18 | icon: "ep-house", 19 | }, 20 | }, 21 | ], 22 | }, 23 | ]; 24 | 25 | export default dashboard; 26 | -------------------------------------------------------------------------------- /src/router/modules/exception.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const exception = [ 4 | { 5 | name: "exception", 6 | path: "/exception", 7 | component: Layout, 8 | redirect: "/exception/403", 9 | meta: { title: "异常页", order: 3, icon: "ep-failed" }, 10 | children: [ 11 | { 12 | path: "403", 13 | name: "exception_403", 14 | component: () => import("@/views/exception/403/index.vue"), 15 | meta: { title: "403", icon: "ep-lock" }, 16 | }, 17 | { 18 | path: "404", 19 | name: "exception_404", 20 | component: () => import("@/views/exception/404/index.vue"), 21 | meta: { title: "404", icon: "ep-hide" }, 22 | }, 23 | { 24 | path: "500", 25 | name: "exception_500", 26 | component: () => import("@/views/exception/500/index.vue"), 27 | meta: { title: "500", icon: "ep-circle-close-filled" }, 28 | }, 29 | { 30 | path: "other", 31 | name: "exception_other", 32 | component: () => import("@/views/exception/other/index.vue"), 33 | meta: { title: "other", icon: "ep-more-filled" }, 34 | }, 35 | ], 36 | }, 37 | ]; 38 | 39 | export default exception; 40 | -------------------------------------------------------------------------------- /src/router/modules/function.ts: -------------------------------------------------------------------------------- 1 | import Layout from "@/layout"; 2 | 3 | const functionRoute = [ 4 | { 5 | name: "function", 6 | path: "/function", 7 | component: Layout, 8 | redirect: "/function/request", 9 | meta: { title: "功能示例", order: 6, icon: "ep-star-filled" }, 10 | children: [ 11 | { 12 | path: "request", 13 | name: "function_request", 14 | component: () => import("@/views/function/request/index.vue"), 15 | meta: { 16 | title: "网络请求", 17 | icon: "ep-promotion", 18 | }, 19 | }, 20 | ], 21 | }, 22 | ]; 23 | 24 | export default functionRoute; 25 | -------------------------------------------------------------------------------- /src/router/modules/index.ts: -------------------------------------------------------------------------------- 1 | import { handleModuleRoutes } from "../helper"; 2 | 3 | // see: https://cn.vitejs.dev/guide/features.html#glob-import 4 | const modules = import.meta.glob("./**/*.ts", { eager: true }) as Record< 5 | string, 6 | { default: AuthRoute.Route } 7 | >; 8 | 9 | const routes = handleModuleRoutes(modules); 10 | 11 | export { routes }; 12 | -------------------------------------------------------------------------------- /src/router/modules/multi-menu.ts: -------------------------------------------------------------------------------- 1 | import { Blank, Layout } from "@/layout"; 2 | 3 | const multiMenu: AuthRoute.Route[] = [ 4 | { 5 | name: "multi-menu", 6 | path: "/multi-menu", 7 | component: Layout, 8 | redirect: "/multi-menu/first", 9 | meta: { title: "多级菜单", icon: "ep-reading", order: 7 }, 10 | children: [ 11 | { 12 | path: "first", 13 | name: "multi-menu_first", 14 | component: Blank, 15 | redirect: "/multi-menu/first/second", 16 | meta: { title: "一级菜单", icon: "ep-reading" }, 17 | children: [ 18 | { 19 | path: "second", 20 | name: "multi-menu_first_second", 21 | component: () => import("@/views/multi-menu/first/second/index.vue"), 22 | meta: { 23 | title: "二级菜单", 24 | icon: "ep-reading", 25 | }, 26 | }, 27 | { 28 | path: "second-new", 29 | name: "multi-menu_first_second-new", 30 | component: Blank, 31 | redirect: "/multi-menu/first/second-new/third", 32 | meta: { 33 | title: "二级菜单(有子菜单)", 34 | icon: "ep-reading", 35 | }, 36 | children: [ 37 | { 38 | path: "third", 39 | name: "multi-menu_first_second-new_third", 40 | component: () => import("@/views/multi-menu/first/second-new/third/index.vue"), 41 | meta: { 42 | title: "三级菜单", 43 | icon: "ep-reading", 44 | }, 45 | }, 46 | ], 47 | }, 48 | ], 49 | }, 50 | ], 51 | }, 52 | ]; 53 | 54 | export default multiMenu; 55 | -------------------------------------------------------------------------------- /src/router/routes/index.ts: -------------------------------------------------------------------------------- 1 | /** 跟路由 */ 2 | export const ROOT_ROUTE = { 3 | name: "root", 4 | path: "/", 5 | redirect: import.meta.env.VITE_ROUTE_HOME_PATH, 6 | meta: { 7 | title: "Root", 8 | }, 9 | }; 10 | 11 | /** 固定路由 */ 12 | export const constantRoutes = [ 13 | ROOT_ROUTE, 14 | { 15 | name: "login", 16 | path: "/login", 17 | component: () => import("@/views/login/index.vue"), 18 | meta: { 19 | title: "登录", 20 | }, 21 | }, 22 | { 23 | name: "404", 24 | path: "/404", 25 | component: () => import("@/views/system-view/not-found/index.vue"), 26 | meta: { 27 | title: "未找到", 28 | singleLayout: "blank", 29 | }, 30 | }, 31 | // 匹配无效路径的路由 32 | { 33 | name: "not-found-page", 34 | path: "/:pathMatch(.*)*", 35 | component: () => import("@/views/system-view/not-found/index.vue"), 36 | meta: { 37 | title: "未找到", 38 | singleLayout: "blank", 39 | }, 40 | }, 41 | ]; 42 | -------------------------------------------------------------------------------- /src/sdk/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./oss"; 2 | -------------------------------------------------------------------------------- /src/sdk/oss.ts: -------------------------------------------------------------------------------- 1 | import OSS from "ali-oss"; 2 | import type { Credentials } from "ali-oss"; 3 | 4 | // 向服务器获取临时凭证 5 | function getSTS(): Promise { 6 | return new Promise((resolve, reject) => { 7 | // get STS token from server 8 | // see: https://help.aliyun.com/document_detail/32077.html 9 | fetch("/clover-api/v1/file/ali-oss-sts", { method: "post" }) 10 | .then((res) => res.json()) 11 | .then((result) => { 12 | resolve(result.data.list[0]); 13 | }) 14 | .catch((e) => { 15 | console.log(e); 16 | reject(e); 17 | }); 18 | }); 19 | } 20 | 21 | // 构建OSS客户端 22 | async function createOSSClient() { 23 | const sts = await getSTS().catch(console.log); 24 | if (!sts) return null; 25 | return new OSS({ 26 | // 使用自定义域名作为Endpoint。 27 | // see: https://help.aliyun.com/document_detail/64059.htm 28 | endpoint: "https://clover-oss.wzc520pyf.cn", 29 | // 使用自定义域名需开启cname 30 | cname: true, 31 | // yourRegion填写Bucket所在地域。以华东1(杭州)为例,Region填写为oss-cn-hangzhou。 32 | region: "oss-cn-hangzhou", 33 | // 从STS服务获取的临时访问密钥(AccessKey ID和AccessKey Secret)。 34 | accessKeyId: sts.AccessKeyId, 35 | accessKeySecret: sts.AccessKeySecret, 36 | // 从STS服务获取的安全令牌(SecurityToken)。 37 | stsToken: sts.SecurityToken, 38 | bucket: "clover-admin-oss", 39 | // 刷新临时访问凭证 40 | refreshSTSToken: async () => { 41 | // 向您搭建的STS服务获取临时访问凭证。 42 | const info = await getSTS(); 43 | return { 44 | accessKeyId: info.AccessKeyId, 45 | accessKeySecret: info.AccessKeySecret, 46 | stsToken: info.SecurityToken, 47 | }; 48 | }, 49 | // 刷新临时访问凭证的时间间隔,单位为毫秒。 50 | refreshSTSTokenInterval: 300000, 51 | }); 52 | } 53 | 54 | export { createOSSClient }; 55 | -------------------------------------------------------------------------------- /src/service/api/auth.ts: -------------------------------------------------------------------------------- 1 | import { mockRequest } from "../request"; 2 | 3 | /** 4 | * 帐密登录 5 | * @param username - 用户名 6 | * @param password - 密码 7 | * @returns Token 8 | */ 9 | export function fetchLogin(username: string, password: string) { 10 | return mockRequest.post("/login", { username, password }); 11 | } 12 | // mockRequest.post("/login", { username, password }, {}); 13 | // mockRequest.get("/login", { params: {} }); 14 | -------------------------------------------------------------------------------- /src/service/api/example.ts: -------------------------------------------------------------------------------- 1 | import { mockRequest } from "../request"; 2 | 3 | export function fetch404() { 4 | return mockRequest.post("/404"); 5 | } 6 | 7 | export function fetchExampleData() { 8 | return mockRequest.get("/example-data"); 9 | } 10 | 11 | export function fetchExampleNoData() { 12 | return mockRequest.get("/example-no-data"); 13 | } 14 | 15 | export function fetchExampleHeaders() { 16 | return mockRequest.get("/example-headers", { 17 | // axios: { headers: { "Content-Type": "FormData" } }, 18 | entries: ["data", "headers"], 19 | }); 20 | } 21 | -------------------------------------------------------------------------------- /src/service/api/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./example"; 3 | -------------------------------------------------------------------------------- /src/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./api"; 2 | -------------------------------------------------------------------------------- /src/service/request/index.ts: -------------------------------------------------------------------------------- 1 | import { createRequest } from "./request"; 2 | 3 | export const mockRequest = createRequest({ baseURL: "/mock" }); 4 | -------------------------------------------------------------------------------- /src/service/request/instance.ts: -------------------------------------------------------------------------------- 1 | import axios from "axios"; 2 | import type { AxiosError, AxiosHeaders, AxiosInstance, AxiosRequestConfig } from "axios"; 3 | import { 4 | getToken, 5 | handleAxiosError, 6 | handleBackendError, 7 | handleResponseError, 8 | handleServiceResult, 9 | transformRequestData, 10 | } from "@/utils"; 11 | 12 | export default class CustomAxiosInstance { 13 | private instance: AxiosInstance; 14 | 15 | private backendConfig: Service.BackendResultConfig; 16 | 17 | constructor( 18 | axiosConfig: AxiosRequestConfig, 19 | backendConfig: Service.BackendResultConfig = { 20 | codeField: "code", 21 | dataField: "data", 22 | msgField: "message", 23 | successCode: 200, 24 | } 25 | ) { 26 | this.backendConfig = backendConfig; 27 | this.instance = axios.create(axiosConfig); 28 | this.setInterceptor(); 29 | } 30 | 31 | /** 设置请求拦截器 */ 32 | private setInterceptor() { 33 | // 请求拦截器 34 | this.instance.interceptors.request.use( 35 | async (config) => { 36 | if (config.headers) { 37 | // 数据转换 38 | const contentType = (config.headers as AxiosHeaders).get("Content-Type") as string; 39 | config.data = await transformRequestData(config.data, contentType); 40 | // 设置token 41 | (config.headers as AxiosHeaders).set("Authorization", getToken()); 42 | } 43 | return config; 44 | }, 45 | (axiosError: AxiosError) => { 46 | const error = handleAxiosError(axiosError); 47 | return handleServiceResult(error, null); 48 | } 49 | ); 50 | // 响应拦截器 51 | this.instance.interceptors.response.use( 52 | async (response) => { 53 | const { status } = response; 54 | // http状态码成功 55 | if (status === 200 || status < 300 || status === 304) { 56 | const backend = response.data; 57 | const { codeField, dataField, successCode } = this.backendConfig; 58 | 59 | // 后端状态码返回请求成功 60 | if (backend[codeField] === successCode) { 61 | response.data = handleServiceResult(null, backend[dataField]); 62 | } else { 63 | // 后端状态码返回请求失败 64 | const error = handleBackendError(backend, this.backendConfig); 65 | response.data = handleServiceResult(error, null); 66 | } 67 | } else { 68 | // http状态码失败 69 | const error = handleResponseError(response); 70 | response.data = handleServiceResult(error, null); 71 | } 72 | 73 | return response; 74 | }, 75 | (axiosError: AxiosError) => { 76 | const error = handleAxiosError(axiosError); 77 | return handleServiceResult(error, null); 78 | } 79 | ); 80 | } 81 | 82 | public getInstance() { 83 | return this.instance; 84 | } 85 | 86 | public getBackendConfig() { 87 | return this.backendConfig; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/stores/index.ts: -------------------------------------------------------------------------------- 1 | import type { App } from "vue"; 2 | import { createPinia } from "pinia"; 3 | 4 | /** setup vue store plugin: pinia. */ 5 | export function setupStore(app: App) { 6 | const store = createPinia(); 7 | 8 | app.use(store); 9 | } 10 | 11 | export * from "./modules"; 12 | -------------------------------------------------------------------------------- /src/stores/modules/app/index.ts: -------------------------------------------------------------------------------- 1 | interface AppState { 2 | /** 重载页面(控制页面的显示) */ 3 | reloadFlag: boolean; 4 | /** 侧边栏折叠状态 */ 5 | sidebarCollapse: boolean; 6 | } 7 | 8 | export const useAppStore = defineStore("app-store", { 9 | state: (): AppState => ({ 10 | reloadFlag: true, 11 | sidebarCollapse: false, 12 | }), 13 | actions: { 14 | /** 15 | * 重载页面 16 | * - 被缓存的页面会触发activated, 未被缓存的页面会触发mounted 17 | * @param duration - 重载的延迟时间(ms) 18 | */ 19 | async reloadPage(duration = 0) { 20 | this.reloadFlag = false; 21 | await nextTick(); 22 | if (duration) { 23 | setTimeout(() => { 24 | this.reloadFlag = true; 25 | }, duration); 26 | } else { 27 | this.reloadFlag = true; 28 | } 29 | setTimeout(() => { 30 | document.documentElement.scrollTo({ left: 0, top: 0 }); 31 | }, 100); 32 | }, 33 | /** // TODO: 强制重载页面(即使是被缓存的页面也触发其mounted) */ 34 | async forceReloadPage() {}, 35 | /** 切换侧边栏折叠状态 */ 36 | toggleSidebarCollapse() { 37 | this.sidebarCollapse = !this.sidebarCollapse; 38 | }, 39 | }, 40 | }); 41 | -------------------------------------------------------------------------------- /src/stores/modules/auth/index.ts: -------------------------------------------------------------------------------- 1 | import { useRouteStore, useTabStore } from "@/stores"; 2 | import { getToken, getUserInfo, setUserInfo } from "@/utils/auth"; 3 | import { router as globalRouter } from "@/router"; 4 | 5 | interface AuthState { 6 | userInfo: Auth.UserInfo; 7 | token: string; 8 | } 9 | 10 | export const useAuthStore = defineStore("auth-store", { 11 | state: (): AuthState => ({ 12 | userInfo: getUserInfo(), 13 | token: getToken(), 14 | }), 15 | getters: { 16 | isLogin(state) { 17 | return Boolean(state.token); 18 | }, 19 | }, 20 | actions: { 21 | /** 登录 */ 22 | async login(userName: string, password: string) {}, 23 | /** 设置用户权限 */ 24 | setUserAuthRole(role: Auth.RoleType) { 25 | const { resetRouteStore, initAuthRoute } = useRouteStore(); 26 | const { initTabStore } = useTabStore(); 27 | 28 | const userInfo = { 29 | ...this.userInfo, 30 | userRole: role, 31 | }; 32 | setUserInfo(userInfo); 33 | this.userInfo = getUserInfo(); 34 | resetRouteStore(); 35 | initAuthRoute(); 36 | initTabStore(unref(globalRouter.currentRoute)); 37 | }, 38 | }, 39 | }); 40 | -------------------------------------------------------------------------------- /src/stores/modules/counter.ts: -------------------------------------------------------------------------------- 1 | import { computed, ref } from "vue"; 2 | import { defineStore } from "pinia"; 3 | 4 | // 一个pinia最基础的使用示例 5 | export const useCounterStore = defineStore("counter", () => { 6 | const count = ref(0); 7 | const doubleCount = computed(() => count.value * 2); 8 | function increment() { 9 | count.value++; 10 | } 11 | 12 | return { count, doubleCount, increment }; 13 | }); 14 | -------------------------------------------------------------------------------- /src/stores/modules/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./counter"; 2 | export * from "./route"; 3 | export * from "./app"; 4 | export * from "./tab"; 5 | export * from "./theme"; 6 | export * from "./auth"; 7 | -------------------------------------------------------------------------------- /src/stores/modules/tab/helpers.ts: -------------------------------------------------------------------------------- 1 | import type { RouteLocationNormalizedLoaded, RouteRecordNormalized } from "vue-router"; 2 | import { getLocal, setLocal } from "@/utils"; 3 | import { EnumStorageKey } from "@/enum"; 4 | 5 | /** 6 | * 根据vue-route路由获取tab路由 7 | * @param route 8 | */ 9 | export function getTabRouteByVueRoute( 10 | route: RouteRecordNormalized | RouteLocationNormalizedLoaded 11 | ) { 12 | const fullPath = hasFullPath(route) ? route.fullPath : route.path; 13 | const tabRoute: GlobalTabRoute = { 14 | name: route.name, 15 | fullPath, 16 | meta: route.meta, 17 | }; 18 | return tabRoute; 19 | } 20 | 21 | /** 22 | * 获取页签在多页签中的索引 23 | * @param tabs - 多页签数据 24 | * @param fullPath - 页签的路径 25 | */ 26 | export function getIndexInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) { 27 | return tabs.findIndex((tab) => tab.fullPath === fullPath); 28 | } 29 | 30 | /** 31 | * 根据路由名称获取该页签在多页签数据中的索引 32 | * @param tabs - 多页签数据 33 | * @param routeName - 路由名称 34 | */ 35 | export function getIndexInTabRoutesByRouteName(tabs: GlobalTabRoute[], routeName: string) { 36 | return tabs.findIndex((tab) => tab.name === routeName); 37 | } 38 | 39 | /** 40 | * 判断页签是否在多页签数据中 41 | * @param tabs - 多页签数据 42 | * @param fullPath - 页签的路径 43 | */ 44 | export function isInTabRoutes(tabs: GlobalTabRoute[], fullPath: string) { 45 | return getIndexInTabRoutes(tabs, fullPath) > -1; 46 | } 47 | 48 | /** 49 | * 判断路由是否有fullPath属性 50 | * @param route 路由 51 | */ 52 | function hasFullPath( 53 | route: RouteRecordNormalized | RouteLocationNormalizedLoaded 54 | ): route is RouteLocationNormalizedLoaded { 55 | return Boolean((route as RouteLocationNormalizedLoaded).fullPath); 56 | } 57 | 58 | /** 59 | * 获取缓存的多页签 60 | */ 61 | export function getTabRoutes() { 62 | const routes: GlobalTabRoute[] = []; 63 | const data = getLocal(EnumStorageKey["multi-tab-routes"]); 64 | if (data) { 65 | routes.push(...data); 66 | } 67 | return routes; 68 | } 69 | 70 | /** 缓存多页签数据 */ 71 | export function setTabRoutes(data: GlobalTabRoute[]) { 72 | setLocal(EnumStorageKey["multi-tab-routes"], data); 73 | } 74 | 75 | /** 清空多页签数据 */ 76 | export function clearTabRoutes() { 77 | setTabRoutes([]); 78 | } 79 | -------------------------------------------------------------------------------- /src/stores/modules/theme/index.ts: -------------------------------------------------------------------------------- 1 | import { createGlobalState, useDark, useToggle } from "@vueuse/core"; 2 | 3 | export const useThemeStore = createGlobalState(() => { 4 | // state 5 | const isDark = useDark(); 6 | const toggleDark = useToggle(isDark); 7 | 8 | // getters 9 | // ... 10 | 11 | // actions 12 | // ... 13 | 14 | return { isDark, toggleDark }; 15 | }); 16 | -------------------------------------------------------------------------------- /src/styles/css/base.css: -------------------------------------------------------------------------------- 1 | /* color palette from */ 2 | :root { 3 | --vt-c-white: #ffffff; 4 | --vt-c-white-soft: #f8f8f8; 5 | --vt-c-white-mute: #f2f2f2; 6 | 7 | --vt-c-black: #181818; 8 | --vt-c-black-soft: #222222; 9 | --vt-c-black-mute: #282828; 10 | 11 | --vt-c-indigo: #2c3e50; 12 | 13 | --vt-c-divider-light-1: rgba(60, 60, 60, 0.29); 14 | --vt-c-divider-light-2: rgba(60, 60, 60, 0.12); 15 | --vt-c-divider-dark-1: rgba(84, 84, 84, 0.65); 16 | --vt-c-divider-dark-2: rgba(84, 84, 84, 0.48); 17 | 18 | --vt-c-text-light-1: var(--vt-c-indigo); 19 | --vt-c-text-light-2: rgba(60, 60, 60, 0.66); 20 | --vt-c-text-dark-1: var(--vt-c-white); 21 | --vt-c-text-dark-2: rgba(235, 235, 235, 0.64); 22 | } 23 | 24 | /* semantic color variables for this project */ 25 | :root { 26 | --color-background: var(--vt-c-white); 27 | --color-background-soft: var(--vt-c-white-soft); 28 | --color-background-mute: var(--vt-c-white-mute); 29 | 30 | --color-border: var(--vt-c-divider-light-2); 31 | --color-border-hover: var(--vt-c-divider-light-1); 32 | 33 | --color-heading: var(--vt-c-text-light-1); 34 | --color-text: var(--vt-c-text-light-1); 35 | 36 | --section-gap: 160px; 37 | } 38 | 39 | @media (prefers-color-scheme: dark) { 40 | :root { 41 | --color-background: var(--vt-c-black); 42 | --color-background-soft: var(--vt-c-black-soft); 43 | --color-background-mute: var(--vt-c-black-mute); 44 | 45 | --color-border: var(--vt-c-divider-dark-2); 46 | --color-border-hover: var(--vt-c-divider-dark-1); 47 | 48 | --color-heading: var(--vt-c-text-dark-1); 49 | --color-text: var(--vt-c-text-dark-2); 50 | } 51 | } 52 | 53 | *, 54 | *::before, 55 | *::after { 56 | box-sizing: border-box; 57 | margin: 0; 58 | position: relative; 59 | font-weight: normal; 60 | } 61 | 62 | html { 63 | height: 100%; 64 | } 65 | 66 | body { 67 | height: 100%; 68 | color: var(--color-text); 69 | background: var(--color-background); 70 | transition: color 0.5s, background-color 0.5s; 71 | line-height: 1.6; 72 | font-family: Inter, -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, 73 | Cantarell, 'Fira Sans', 'Droid Sans', 'Helvetica Neue', sans-serif; 74 | font-size: 15px; 75 | text-rendering: optimizeLegibility; 76 | -webkit-font-smoothing: antialiased; 77 | -moz-osx-font-smoothing: grayscale; 78 | } 79 | -------------------------------------------------------------------------------- /src/styles/css/main.css: -------------------------------------------------------------------------------- 1 | /* 全局生效的css */ 2 | @import './base.css'; 3 | @import './transition.css'; 4 | @import './nprogress.css'; 5 | 6 | #app { 7 | width: 100%; 8 | height: 100%; 9 | margin: 0 auto; 10 | /* padding: 2rem; */ 11 | 12 | font-weight: normal; 13 | } 14 | 15 | a, 16 | .green { 17 | text-decoration: none; 18 | color: hsla(160, 100%, 37%, 1); 19 | transition: 0.4s; 20 | } 21 | 22 | @media (hover: hover) { 23 | a:hover { 24 | background-color: hsla(160, 100%, 37%, 0.2); 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/styles/css/nprogress.css: -------------------------------------------------------------------------------- 1 | #nprogress .bar { 2 | background-color: rgb(128, 250, 80); 3 | filter: drop-shadow(0, 0, 5px rgb(167, 241, 138)); 4 | } 5 | 6 | -------------------------------------------------------------------------------- /src/styles/css/transition.css: -------------------------------------------------------------------------------- 1 | /* fade */ 2 | .fade-enter-from, 3 | .fade-leave-to { 4 | transform: translateX(20px); 5 | opacity: 0; 6 | } 7 | 8 | .fade-enter-to, 9 | .fade-leave-from { 10 | opacity: 1; 11 | } 12 | 13 | .fade-enter-active { 14 | transition: all 0.7s ease; 15 | } 16 | 17 | .fade-leave-active { 18 | transition: all 0.3s cubic-bezier(1, 0.6, 0.6, 1); 19 | } 20 | 21 | -------------------------------------------------------------------------------- /src/styles/less/common.less: -------------------------------------------------------------------------------- 1 | // 自定义的常用less样式 2 | #cm() { 3 | 4 | .flex-center { 5 | display: flex; 6 | justify-content: center; 7 | align-items: center; 8 | } 9 | 10 | .flex-center-wrap { 11 | #cm.flex-center(); 12 | flex-wrap: wrap; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/styles/less/element.less: -------------------------------------------------------------------------------- 1 | // 自定义的element-plus样式 2 | #ep() { 3 | 4 | .el-card { 5 | border: 0; 6 | box-shadow: var(--el-box-shadow-lightest); 7 | } 8 | 9 | .el-card-rounded { 10 | #ep.el-card(); 11 | border-radius: 16px; 12 | } 13 | 14 | .el-card-no-divider { 15 | :deep(.el-card__body) { 16 | --el-card-padding: 0px 20px 20px 20px; 17 | } 18 | :deep(.el-card__header) { 19 | border-bottom: none; 20 | } 21 | } 22 | .el-card-header-bold { 23 | :deep(.el-card__header) { 24 | font-weight: bold; 25 | font-size: 22px; 26 | } 27 | } 28 | } 29 | -------------------------------------------------------------------------------- /src/styles/less/main.less: -------------------------------------------------------------------------------- 1 | // 这里的less定义可以被全局访问 2 | // 但这里定义的样式不会直接生效, 需要在页面中引入 3 | @import './element.less'; 4 | @import './common.less'; 5 | -------------------------------------------------------------------------------- /src/styles/scss/element.dark.scss: -------------------------------------------------------------------------------- 1 | // 在此覆盖elementUI的暗黑主题配置 2 | // elementUI暗黑主题变量see: https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/dark/var.scss 3 | @forward "element-plus/theme-chalk/src/dark/var.scss" with ( 4 | $colors: (), 5 | ); 6 | -------------------------------------------------------------------------------- /src/styles/scss/element.scss: -------------------------------------------------------------------------------- 1 | // 在此覆盖elementUI的主题配置 2 | // elementUI主题变量see: https://github.com/element-plus/element-plus/blob/dev/packages/theme-chalk/src/common/var.scss 3 | @forward "element-plus/theme-chalk/src/common/var.scss" with ( 4 | $colors: (), 5 | $menu: ( 6 | 'active-color': #409eff, 7 | 'hover-bg-color': #56565e1a, 8 | 'active-bg-color': #1890ff26, 9 | ), 10 | $box-shadow: ( 11 | 'lightest': ( 12 | 0px 1px 5px rgba(0, 0, 0, 0.05), 13 | ), 14 | ) 15 | ); 16 | -------------------------------------------------------------------------------- /src/styles/scss/main.scss: -------------------------------------------------------------------------------- 1 | @use "./element.scss" as *; 2 | @use "./element.dark.scss" as *; 3 | -------------------------------------------------------------------------------- /src/typings/api.d.ts: -------------------------------------------------------------------------------- 1 | // 后端接口返回的数据类型 2 | 3 | /** 用户权益相关 */ 4 | declare namespace ApiAuth { 5 | /** token */ 6 | interface Token { 7 | token: string; 8 | } 9 | /** 用户信息 */ 10 | type UserInfo = Auth.UserInfo; 11 | } 12 | 13 | declare namespace ApiUserManagement { 14 | interface User { 15 | /** 用户id */ 16 | id: string; 17 | /** 用户名 */ 18 | username: string | null; 19 | /** 用户年龄 */ 20 | age: number | null; 21 | /** 22 | * 用户性别 23 | * - MALE: 男 24 | * - FEMALE: 女 25 | */ 26 | gender: "MALE" | "FEMALE" | null; 27 | /** 用户手机号 */ 28 | phone: string; 29 | /** 30 | * 用户状态 31 | * - ENABLE: 启用 32 | * - DISABLE: 禁用 33 | * - DELETED: 已删除 34 | */ 35 | userStatus: "ENABLE" | "DISABLE" | "DELETED" | null; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/typings/business.d.ts: -------------------------------------------------------------------------------- 1 | /** 权限 */ 2 | declare namespace Auth { 3 | /** 用户角色类型 */ 4 | type RoleType = keyof typeof import("@/enum").EnumUserRole; 5 | 6 | interface UserInfo { 7 | userId: string; 8 | userName: string; 9 | userRole: RoleType; 10 | } 11 | } 12 | 13 | declare namespace UserManagement { 14 | interface User extends ApiUserManagement.User {} 15 | 16 | /** 17 | * 用户性别 18 | * - MALE: 男 19 | * - FEMALE: 女 20 | */ 21 | type GenderKey = NonNullable; 22 | 23 | /** 24 | * 用户状态 25 | * - ENABLE: 启用 26 | * - DISABLE: 禁用 27 | * - DELETED: 已删除 28 | */ 29 | type UserStatusKey = NonNullable; 30 | } 31 | -------------------------------------------------------------------------------- /src/typings/env.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | 3 | /** 4 | * 抉择 5 | * YES or NO 6 | */ 7 | type YorN = "Y" | "N"; 8 | 9 | /** 10 | * 服务的环境类型 11 | * - dev: 开发环境 12 | * - test: 测试环境 13 | * - prod: 生产环境 14 | */ 15 | type ServiceEnvType = "dev" | "test" | "prod"; 16 | 17 | /** 服务的环境配置 */ 18 | interface ServiceEnvConfig { 19 | /** 请求地址 */ 20 | url: string; 21 | /** 22 | * 匹配路径的正则字符串, 用于拦截地址作转发代理(可以是任意以/开头的字符串, 单个/不起作用) 23 | * - eg: urlPattern: /api, 路径/api/user会被匹配, 若不设置rewritten, 则会被转为/user 24 | */ 25 | urlPattern: `/${api}`; // 模板类型写法see: https://github.com/microsoft/TypeScript/pull/40336 26 | /** 重写后的路径, 如果需要将/api重写为/abc-api, 则需要配置为rewritten: /abc-api */ 27 | rewritten?: `/${api}`; 28 | } 29 | /** 服务的环境配置, 可能会有多个后端请求地址 */ 30 | type ServiceEnvConfigs = Array; 31 | 32 | interface ImportMetaEnv { 33 | /** 项目基本地址 */ 34 | readonly VITE_BASE_URL: string; 35 | /** 项目名称 */ 36 | readonly VITE_APP_NAME: string; 37 | /** 项目标题 */ 38 | readonly VITE_APP_TITLE: string; 39 | /** 项目描述 */ 40 | readonly VITE_APP_DESC: string; 41 | /** 是否开启打包文件大小结果分析 */ 42 | readonly VITE_VISUALIZER?: YorN; 43 | /** 是否开启打包压缩 */ 44 | readonly VITE_COMPRESS?: YorN; 45 | /** 压缩算法类型 */ 46 | readonly VITE_COMPRESS_TYPE?: "gzip" | "brotliCompress" | "deflate" | "deflateRaw"; 47 | /** 后端服务的环境类型 */ 48 | readonly VITE_SERVICE_ENV?: ServiceEnvType; 49 | /** 是否开启请求代理 */ 50 | readonly VITE_HTTP_PROXY?: YorN; 51 | /** hash路由模式 */ 52 | readonly VITE_HASH_ROUTE?: YorN; 53 | } 54 | 55 | interface ImportMeta { 56 | readonly env: ImportMetaEnv; 57 | } 58 | -------------------------------------------------------------------------------- /src/typings/expose.d.ts: -------------------------------------------------------------------------------- 1 | /** vue 的defineExpose导出的类型 */ 2 | declare namespace Expose { 3 | interface BetterScroll { 4 | instance: import("@better-scroll/core").BScrollInstance; 5 | } 6 | 7 | interface DragVerify { 8 | resume: () => void; 9 | } 10 | 11 | interface ImgRotateVerify { 12 | resume: () => void; 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /src/typings/global.d.ts: -------------------------------------------------------------------------------- 1 | interface Window { 2 | $loadingBar?: any; // progress bar 3 | $message?: typeof import("element-plus").ElMessage extends import("element-plus/es/utils").SFCInstallWithContext< 4 | infer P 5 | > 6 | ? P 7 | : never; // element-plus的ElMessage 8 | } 9 | 10 | type ConstantOptions = { 11 | value: T; 12 | label: string; 13 | }; 14 | 15 | /** 通用类型 */ 16 | declare namespace Common { 17 | /** 18 | * 策略模式 19 | * [状态, 为true时执行回调函数] 20 | */ 21 | type StrategyAction = [boolean, () => void]; 22 | } 23 | -------------------------------------------------------------------------------- /src/typings/package.d.ts: -------------------------------------------------------------------------------- 1 | /// 2 | declare module "nprogress"; // progress bar 3 | declare module "crypto-js"; // crypto-js 4 | declare module "canvas-datagrid"; // canvas-datagrid 5 | -------------------------------------------------------------------------------- /src/typings/route.d.ts: -------------------------------------------------------------------------------- 1 | /** 权限路由 */ 2 | declare namespace AuthRoute { 3 | /** 路由描述 */ 4 | interface RouteMeta { 5 | /** 路由标题(document.title或者菜单的名称) */ 6 | title: string; 7 | /** 权限, 为空则表示不需要权限 */ 8 | permissions?: Auth.RoleType[]; 9 | /** 缓存页面 */ 10 | keepAlive?: boolean; 11 | /** 图标 */ 12 | icon?: string; 13 | /** 需要登录权限 */ 14 | requiresAuth?: boolean; 15 | /** 是否在菜单中隐藏此项, 其子菜单不受影响 */ 16 | hidden?: boolean; 17 | /** 唯一标识, 独立于name的标识, 可用于标识主页路由 */ 18 | key?: string; 19 | /** 外链链接 */ 20 | href?: string; 21 | /** 路由顺序, 调整菜单排序 */ 22 | order?: number; 23 | } 24 | 25 | type Lazy = () => Promise; 26 | 27 | type RouteComponent = import("vue-router").RouteComponent; 28 | 29 | type RawRouteComponent = RouteComponent | Lazy; 30 | 31 | /** 路由类型结构 */ 32 | interface Route { 33 | /** 路由名称 */ 34 | name: string | symbol; 35 | /** 路由路径 */ 36 | path: string; 37 | /** 路由重定向 */ 38 | redirect?: string; 39 | /** 路由组件 */ 40 | component?: RawRouteComponent; 41 | /** 子路由 */ 42 | children?: Route[]; 43 | /** 路由属性 */ 44 | meta: RouteMeta; 45 | /** 是否在菜单中完全隐藏, 其子菜单也将隐藏 */ 46 | hidden?: boolean; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/typings/router.d.ts: -------------------------------------------------------------------------------- 1 | import "vue-router"; 2 | 3 | declare module "vue-router" { 4 | interface RouteMeta extends AuthRoute.RouteMeta {} 5 | } 6 | -------------------------------------------------------------------------------- /src/typings/system.d.ts: -------------------------------------------------------------------------------- 1 | /** 菜单项配置 */ 2 | type GlobalMenuOption = { 3 | key: string | symbol; 4 | label: string; 5 | routeName: string | symbol; 6 | routePath: string; 7 | icon?: string; 8 | children?: Array; 9 | }; 10 | 11 | /** 多页签Tab路由 */ 12 | type GlobalTabRoute = Pick< 13 | import("vue-router").RouteLocationNormalizedLoaded, 14 | "name" | "fullPath" | "meta" 15 | >; 16 | 17 | /** 请求相关类型 */ 18 | declare namespace Service { 19 | /** 20 | * 请求的错误类型 21 | * - axios: axios错误: 网络错误、请求超时、默认的兜底错误 22 | * - http:请求成功, 响应的http状态非200 23 | * - backend:请求成功,响应的http状态码为200,由后端定义的业务错误 24 | */ 25 | type RequestErrorType = "axios" | "http" | "backend"; 26 | 27 | /** 请求错误 */ 28 | interface RequestError { 29 | /** 错误类型 */ 30 | type: RequestErrorType; 31 | /** 错误码 */ 32 | code: string | number; 33 | /** 错误信息 */ 34 | msg: string; 35 | } 36 | 37 | /** 后端接口返回的数据结构配置 */ 38 | interface BackendResultConfig { 39 | /** 后端请求的返回状态码的属性字段 */ 40 | codeField: string; 41 | /** 后端请求的返回数据的属性字段 */ 42 | dataField: string; 43 | /** 后端请求的返回消息的属性字段 */ 44 | msgField: string; 45 | /** 后端业务上定义的成功请求的状态 */ 46 | successCode: number | string; 47 | } 48 | 49 | /** 自定义的请求成功结果 */ 50 | interface SuccessResult { 51 | /** 请求错误信息 */ 52 | error: null; 53 | /** 请求返回的数据 */ 54 | data: T; 55 | } 56 | 57 | /** 自定义的请求失败结果 */ 58 | interface FailedResult { 59 | /** 请求错误信息 */ 60 | error: RequestError; 61 | /** 请求返回的数据 */ 62 | data: null; 63 | } 64 | 65 | /** 自定义的请求结果 */ 66 | type RequestResult = SuccessResult | FailedResult; 67 | } 68 | -------------------------------------------------------------------------------- /src/typings/utility.d.ts: -------------------------------------------------------------------------------- 1 | /** 扩展TS的Utility Types集 */ 2 | declare namespace UT { 3 | type Nullable = T | null; 4 | type Nullishable = T | null | undefined; 5 | type MaybeRef = import("vue").Ref | T; 6 | } 7 | -------------------------------------------------------------------------------- /src/typings/vue.d.ts: -------------------------------------------------------------------------------- 1 | export {}; 2 | 3 | // see: https://github.com/vuejs/core/pull/3399 4 | declare module "vue" { 5 | // export interface ComponentCustomOptions { } 6 | 7 | /** 定义在vue实例上自定义的全局属性的类型 */ 8 | export interface ComponentCustomProperties { 9 | $filters: typeof import("@/utils/filters"); 10 | 11 | // 全局注册的自定义指令的类型(声明在此只是暂时的hack办法, 在未来应该声明在GlobalDirectives中), see: https://github.com/johnsoncodehk/volar/issues/465 12 | vPermission: import("@/directives/permission").PermissionDirective; 13 | vClipboard: import("@/directives/clipboard").ClipboardDirective; 14 | } 15 | // 暂不生效, see: https://github.com/johnsoncodehk/volar/issues/465 16 | // export interface GlobalDirectives { } 17 | 18 | // export interface GlobalComponents { } 19 | } 20 | -------------------------------------------------------------------------------- /src/utils/auth/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./user"; 2 | -------------------------------------------------------------------------------- /src/utils/auth/user.ts: -------------------------------------------------------------------------------- 1 | import { EnumStorageKey } from "@/enum"; 2 | import { getLocal, removeLocal, setLocal } from "../storage"; 3 | 4 | /** 设置token */ 5 | export function setToken(token: string) { 6 | setLocal(EnumStorageKey.token, token); 7 | } 8 | 9 | /** 获取token */ 10 | export function getToken() { 11 | return getLocal(EnumStorageKey.token) || ""; 12 | } 13 | 14 | /** 去除token */ 15 | export function removeToken() { 16 | removeLocal(EnumStorageKey.token); 17 | } 18 | 19 | /** 获取用户信息 */ 20 | export function getUserInfo() { 21 | const emptyInfo: Auth.UserInfo = { 22 | userId: "", 23 | userName: "", 24 | userRole: "super", // TODO: 更改为user, 暂时为方便调试: 默认权限为超级管理员 25 | }; 26 | const userInfo: Auth.UserInfo = getLocal(EnumStorageKey["user-info"]) || emptyInfo; 27 | return userInfo; 28 | } 29 | 30 | /** 设置用户信息 */ 31 | export function setUserInfo(userInfo: Auth.UserInfo) { 32 | setLocal(EnumStorageKey["user-info"], userInfo); 33 | } 34 | 35 | /** 去除用户信息 */ 36 | export function removeUserInfo() { 37 | removeLocal(EnumStorageKey["user-info"]); 38 | } 39 | 40 | /** 去除用户相关缓存 */ 41 | export function clearAuthStorage() { 42 | removeToken(); 43 | removeUserInfo(); 44 | } 45 | -------------------------------------------------------------------------------- /src/utils/common/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./pattern"; 2 | export * from "./typeof"; 3 | export * from "./responsibilitiesChain"; 4 | -------------------------------------------------------------------------------- /src/utils/common/pattern.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 策略模式 3 | * @param actions 符合条件时执行的操作 4 | */ 5 | export function exeStrategyActions(actions: Common.StrategyAction[]) { 6 | actions.some((item) => { 7 | const [flag, action] = item; 8 | if (flag) { 9 | action(); 10 | } 11 | return flag; 12 | }); 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/common/typeof.ts: -------------------------------------------------------------------------------- 1 | import { EnumDataType } from "@/enum"; 2 | 3 | export function isNumber(data: unknown) { 4 | return Object.prototype.toString.call(data) === EnumDataType.number; 5 | } 6 | export function isString(data: unknown) { 7 | return Object.prototype.toString.call(data) === EnumDataType.string; 8 | } 9 | export function isBoolean(data: unknown) { 10 | return Object.prototype.toString.call(data) === EnumDataType.boolean; 11 | } 12 | export function isNull(data: unknown) { 13 | return Object.prototype.toString.call(data) === EnumDataType.null; 14 | } 15 | export function isUndefined(data: unknown) { 16 | return Object.prototype.toString.call(data) === EnumDataType.undefined; 17 | } 18 | export function isObject(data: unknown) { 19 | return Object.prototype.toString.call(data) === EnumDataType.object; 20 | } 21 | export function isArray(data: unknown) { 22 | return Object.prototype.toString.call(data) === EnumDataType.array; 23 | } 24 | export function isDate(data: unknown) { 25 | return Object.prototype.toString.call(data) === EnumDataType.date; 26 | } 27 | export function isRegExp(data: unknown) { 28 | return Object.prototype.toString.call(data) === EnumDataType.regexp; 29 | } 30 | export function isSet(data: unknown) { 31 | return Object.prototype.toString.call(data) === EnumDataType.set; 32 | } 33 | export function isMap(data: unknown) { 34 | return Object.prototype.toString.call(data) === EnumDataType.map; 35 | } 36 | export function isFile(data: unknown) { 37 | return Object.prototype.toString.call(data) === EnumDataType.file; 38 | } 39 | export function isFunction(val: unknown) { 40 | return typeof val === EnumDataType.function; 41 | } 42 | -------------------------------------------------------------------------------- /src/utils/crypto/index.ts: -------------------------------------------------------------------------------- 1 | import CryptoJS from "crypto-js"; 2 | 3 | const CryptoSecret = "__CryptoJS_Secret__"; 4 | 5 | /** 6 | * 加密数据 7 | * @param data - 数据 8 | */ 9 | export function encrypto(data: any) { 10 | const newData = JSON.stringify(data); 11 | return CryptoJS.AES.encrypt(newData, CryptoSecret).toString(); 12 | } 13 | 14 | /** 15 | * 解密数据 16 | * @param cipherText - 密文 17 | */ 18 | export function decrypto(cipherText: string) { 19 | const bytes = CryptoJS.AES.decrypt(cipherText, CryptoSecret); 20 | const originalText = bytes.toString(CryptoJS.enc.Utf8); 21 | if (originalText) { 22 | return JSON.parse(originalText); 23 | } 24 | return null; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/filters/business.ts: -------------------------------------------------------------------------------- 1 | import { VALUE_CONSTANT_MAPPING } from "@/constants"; 2 | 3 | export function constantFormat(constantValue: any, constantType: number, symbol = "--") { 4 | let result = symbol; 5 | for (const element of VALUE_CONSTANT_MAPPING) { 6 | if (element.type === constantType) { 7 | const temp = element.data; 8 | for (const element of temp) { 9 | if (element.value === constantValue) { 10 | result = element.label; 11 | break; 12 | } 13 | } 14 | break; 15 | } 16 | } 17 | return result; 18 | } 19 | 20 | export function elTableFormatter(constantType: number, symbol?: string) { 21 | // eslint-disable-next-line @typescript-eslint/no-unused-vars 22 | return function (row: any, column: any, cellValue: any, index: any) { 23 | return constantFormat(cellValue, constantType, symbol); 24 | }; 25 | } 26 | -------------------------------------------------------------------------------- /src/utils/filters/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./business"; 2 | -------------------------------------------------------------------------------- /src/utils/helper/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./tsxHelper"; 2 | -------------------------------------------------------------------------------- /src/utils/helper/tsxHelper.tsx: -------------------------------------------------------------------------------- 1 | import type { Slots } from "vue"; 2 | import { isFunction } from "../common"; 3 | 4 | /** 5 | * @description: Get slot to prevent empty error 6 | */ 7 | export function getSlot(slots: Slots, slot = "default", data?: any) { 8 | if (!slots || !Reflect.has(slots, slot)) { 9 | return null; 10 | } 11 | if (!isFunction(slots[slot])) { 12 | console.error(`${slot} is not a function!`); 13 | return null; 14 | } 15 | const slotFn = slots[slot]; 16 | if (!slotFn) return null; 17 | return slotFn(data); 18 | } 19 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./auth"; 2 | export * from "./common"; 3 | export * from "./router"; 4 | export * from "./filters"; 5 | export * from "./helper"; 6 | export * from "./service"; 7 | export * from "./storage"; 8 | -------------------------------------------------------------------------------- /src/utils/router/auth.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 根据用户权限过滤路由 3 | * @param routes - 权限路由 4 | * @param permission - 权限 5 | */ 6 | export function filterAuthRoutesByUserPermission( 7 | routes: AuthRoute.Route[], 8 | permission: Auth.RoleType 9 | ): AuthRoute.Route[] { 10 | return routes.flatMap((route) => filterAuthRouteByUserPermission(route, permission)); 11 | } 12 | 13 | /** 14 | * 根据用户权限过滤单个路由 15 | * @param route - 单个路由 16 | * @param permission - 权限 17 | */ 18 | function filterAuthRouteByUserPermission( 19 | route: AuthRoute.Route, 20 | permission: Auth.RoleType 21 | ): AuthRoute.Route[] { 22 | const filterRoute = { ...route }; 23 | const hasPermission = 24 | /** 路由未声明权限,则默认允许查看 */ 25 | !route.meta.permissions || 26 | /** 超级管路元可查看所有路由 */ 27 | permission === "super" || 28 | /** 路由的权限包含用户的权限,则允许查看 */ 29 | route.meta.permissions.includes(permission); 30 | 31 | if (filterRoute.children) { 32 | const filterChildren = filterRoute.children.flatMap((item) => 33 | filterAuthRouteByUserPermission(item, permission) 34 | ); 35 | Object.assign(filterRoute, { children: filterChildren }); 36 | } 37 | return hasPermission ? [filterRoute] : []; 38 | } 39 | -------------------------------------------------------------------------------- /src/utils/router/breadcrumb.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 获取面包屑数据 3 | */ 4 | export function getBreadcrumbByRouteKey(activeKey: any, menus: any) { 5 | const breadcrumbMenu = getBreadcrumbMenu(activeKey, menus); 6 | return breadcrumbMenu; 7 | } 8 | 9 | /** 10 | * 根据菜单数据获取面包屑数据 11 | */ 12 | function getBreadcrumbMenu(activeKey: any, menus: any) { 13 | const breadcrumbMenu: any[] = []; 14 | menus.some((menu: any) => { 15 | const flag = activeKey.includes(menu.routeName); 16 | if (flag) { 17 | breadcrumbMenu.push(...getBreadcrumbMenuItem(activeKey, menu)); 18 | } 19 | return flag; 20 | }); 21 | return breadcrumbMenu; 22 | } 23 | 24 | /** 25 | * 根据单个菜单数据获取面包屑数据 26 | */ 27 | function getBreadcrumbMenuItem(activeKey: any, menu: any) { 28 | const breadcrumbMenu: any[] = []; 29 | if (activeKey === menu.routeName /** && !menu.meta.hidden */) { 30 | breadcrumbMenu.push({ 31 | key: menu.routeName, 32 | path: menu.routePath, 33 | title: menu.label, 34 | icon: menu.icon, 35 | }); 36 | } 37 | if (activeKey.includes(menu.routeName) && menu.children && menu.children.length) { 38 | /** !menu.meta.hidden && */ 39 | breadcrumbMenu.push( 40 | { 41 | key: menu.routeName, 42 | path: menu.routePath, 43 | title: menu.label, 44 | icon: menu.icon, 45 | }, 46 | ...menu.children.flatMap((item: any) => getBreadcrumbMenuItem(activeKey, item)) 47 | ); 48 | } 49 | return breadcrumbMenu; 50 | } 51 | -------------------------------------------------------------------------------- /src/utils/router/cache.ts: -------------------------------------------------------------------------------- 1 | import { camelCase, upperFirst } from "lodash"; 2 | /** 3 | * 获取缓存的路由对应组件的名称 4 | * @param routes - 转换后的vue路由 5 | */ 6 | export function getCacheRoutes(routes: AuthRoute.Route[]) { 7 | const cacheNames: string[] = []; 8 | routes.forEach((route) => { 9 | // 仅需获取二级路由的缓存的组件名 10 | if (hasChildren(route)) { 11 | (route.children as AuthRoute.Route[]).forEach((item) => { 12 | if (isKeepAlive(item)) { 13 | // Convert the name of the route to the name of the component 14 | // eg: plugin_echarts => PluginEcharts 15 | cacheNames.push(upperFirst(camelCase(item.name as string))); 16 | } 17 | }); 18 | } 19 | }); 20 | return cacheNames; 21 | } 22 | 23 | /** 24 | * 路由是否缓存 25 | * @param route 26 | */ 27 | function isKeepAlive(route: AuthRoute.Route) { 28 | return Boolean(route?.meta?.keepAlive); 29 | } 30 | /** 31 | * 是否有二级路由 32 | * @param route 33 | */ 34 | function hasChildren(route: AuthRoute.Route) { 35 | return Boolean(route.children && route.children.length); 36 | } 37 | -------------------------------------------------------------------------------- /src/utils/router/helpers.ts: -------------------------------------------------------------------------------- 1 | /** 将路由路径转换成路由名字 */ 2 | export function transformRoutePathToRouteName(path: string): string { 3 | if (path === "/") return "root"; 4 | 5 | const pathSplitMark = "/"; 6 | const routeSplitMark = "_"; 7 | 8 | const name = path.split(pathSplitMark).slice(1).join(routeSplitMark); 9 | 10 | return name; 11 | } 12 | 13 | /** 14 | * 获取所有固定路由的名称集合 15 | * @param routes 固定路由 16 | */ 17 | export function getConstantRouteNames(routes: AuthRoute.Route[]) { 18 | return routes.flatMap((route) => getConstantRouteName(route)); 19 | } 20 | 21 | /** 22 | * 获取所有固定路由的名称集合 23 | * @param routes 固定路由 24 | */ 25 | function getConstantRouteName(route: AuthRoute.Route) { 26 | const names = [route.name]; 27 | if (route.children?.length) { 28 | names.push(...route.children!.flatMap((item) => getConstantRouteName(item))); 29 | } 30 | return names; 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/router/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./breadcrumb"; 2 | export * from "./menus"; 3 | export * from "./helpers"; 4 | export * from "./auth"; 5 | export * from "./cache"; 6 | -------------------------------------------------------------------------------- /src/utils/router/menus.ts: -------------------------------------------------------------------------------- 1 | /** 2 | * 路由不转换为菜单, 其子路由也不转换 3 | */ 4 | function hiddenMenu(route: AuthRoute.Route) { 5 | return Boolean(route.hidden); 6 | } 7 | /** 8 | * 当前路由不转换为菜单, 子路由不受影响 9 | */ 10 | function hiddenMenuCurrent(route: AuthRoute.Route) { 11 | return Boolean(route.meta.hidden); 12 | } 13 | 14 | /** 15 | * 权限路由转换为菜单 16 | * @param routes - 路由 17 | */ 18 | export function transformRouteToMenu( 19 | routes: AuthRoute.Route[], 20 | superPath?: string 21 | ): GlobalMenuOption[] { 22 | const globalMenu: GlobalMenuOption[] = []; 23 | routes.forEach((route) => { 24 | const { name, path, meta } = route; 25 | let menuChildren: GlobalMenuOption[] | undefined; 26 | const fullPath = `${superPath ? `${superPath}/` : ""}${path}`; 27 | if (route.children) { 28 | menuChildren = transformRouteToMenu(route.children, fullPath); 29 | } 30 | const menuItem: GlobalMenuOption = { 31 | key: name, 32 | label: meta.title, 33 | routeName: name, 34 | routePath: fullPath, 35 | icon: meta.icon, 36 | children: menuChildren, 37 | }; 38 | if (!hiddenMenu(route)) { 39 | if (!hiddenMenuCurrent(route)) { 40 | globalMenu.push(menuItem); 41 | } else { 42 | globalMenu.push(...(menuChildren as GlobalMenuOption[])); 43 | } 44 | } 45 | }); 46 | 47 | return globalMenu; 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/service/error.ts: -------------------------------------------------------------------------------- 1 | import type { AxiosError, AxiosResponse } from "axios"; 2 | import { 3 | DEFAULT_REQUEST_ERROR_CODE, 4 | DEFAULT_REQUEST_ERROR_MSG, 5 | ERROR_STATUS, 6 | NETWORK_ERROR_CODE, 7 | NETWORK_ERROR_MSG, 8 | REQUEST_TIMEOUT_CODE, 9 | REQUEST_TIMEOUT_MSG, 10 | } from "@/config"; 11 | import { exeStrategyActions } from "../common"; 12 | import { showErrorMsg } from "./msg"; 13 | 14 | type ErrorStatus = keyof typeof ERROR_STATUS; 15 | 16 | /** 17 | * 处理axios请求失败的错误 18 | * @param axiosError 19 | * @returns 20 | */ 21 | export function handleAxiosError(axiosError: AxiosError) { 22 | const error: Service.RequestError = { 23 | type: "axios", 24 | code: DEFAULT_REQUEST_ERROR_CODE, 25 | msg: DEFAULT_REQUEST_ERROR_CODE, 26 | }; 27 | 28 | const actions: Common.StrategyAction[] = [ 29 | [ 30 | // 网络错误 31 | !window.navigator.onLine || axiosError.message === "Network Error", 32 | () => { 33 | Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG }); 34 | }, 35 | ], 36 | [ 37 | // 超时错误 38 | axiosError.code === REQUEST_TIMEOUT_CODE && axiosError.message.includes("timeout"), 39 | () => { 40 | Object.assign(error, { code: REQUEST_TIMEOUT_CODE, msg: REQUEST_TIMEOUT_MSG }); 41 | }, 42 | ], 43 | [ 44 | // 请求失败 45 | Boolean(axiosError.response), 46 | () => { 47 | const errorCode: ErrorStatus = (axiosError.response?.status as ErrorStatus) || "DEFAULT"; 48 | const msg = ERROR_STATUS[errorCode]; 49 | Object.assign(error, { code: errorCode, msg }); 50 | }, 51 | ], 52 | ]; 53 | 54 | exeStrategyActions(actions); 55 | 56 | showErrorMsg(error); 57 | 58 | return error; 59 | } 60 | 61 | /** 62 | * 处理请求成功后的错误 63 | * @param response 64 | */ 65 | export function handleResponseError(response: AxiosResponse) { 66 | const error: Service.RequestError = { 67 | type: "axios", 68 | code: DEFAULT_REQUEST_ERROR_CODE, 69 | msg: DEFAULT_REQUEST_ERROR_MSG, 70 | }; 71 | 72 | if (!window.navigator.onLine) { 73 | // 网络错误 74 | Object.assign(error, { code: NETWORK_ERROR_CODE, msg: NETWORK_ERROR_MSG }); 75 | } else { 76 | // 请求成功但状态码非200的错误 77 | const errorCode: ErrorStatus = response.status as ErrorStatus; 78 | const msg = ERROR_STATUS[errorCode] || DEFAULT_REQUEST_ERROR_MSG; 79 | Object.assign(error, { type: "http", code: errorCode, msg }); 80 | } 81 | 82 | showErrorMsg(error); 83 | 84 | return error; 85 | } 86 | 87 | /** 88 | * 处理后端返回的错误(业务错误) 89 | * @param backendResult 后端接口的响应数据 90 | * @param config 91 | * @returns 92 | */ 93 | export function handleBackendError( 94 | backendResult: Record, 95 | config: Service.BackendResultConfig 96 | ) { 97 | const { codeField, msgField } = config; 98 | const error: Service.RequestError = { 99 | type: "backend", 100 | code: backendResult[codeField], 101 | msg: backendResult[msgField], 102 | }; 103 | 104 | showErrorMsg(error); 105 | 106 | return error; 107 | } 108 | -------------------------------------------------------------------------------- /src/utils/service/handler.ts: -------------------------------------------------------------------------------- 1 | /** 统一失败和成功的请求结果的数据类型 */ 2 | export async function handleServiceResult(error: Service.RequestError | null, data: any) { 3 | if (error) { 4 | const fail: Service.FailedResult = { 5 | error, 6 | data: null, 7 | }; 8 | return fail; 9 | } 10 | const success: Service.SuccessResult> = { 11 | error: null, 12 | data: data ?? "success", 13 | }; 14 | return success; 15 | } 16 | -------------------------------------------------------------------------------- /src/utils/service/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./error"; 2 | export * from "./handler"; 3 | export * from "./transform"; 4 | -------------------------------------------------------------------------------- /src/utils/service/msg.ts: -------------------------------------------------------------------------------- 1 | import { ERROR_MSG_DURATION, NO_ERROR_MSG_CODE } from "@/config"; 2 | 3 | /** 错误信息栈, 避免同一错误同时出现 */ 4 | const errorMsgStack = new Map([]); 5 | 6 | function addErrorMsg(error: Service.RequestError) { 7 | errorMsgStack.set(error.code, error.msg); 8 | } 9 | function removeErrorMsg(error: Service.RequestError) { 10 | errorMsgStack.delete(error.code); 11 | } 12 | function hasErrorMsg(error: Service.RequestError) { 13 | return errorMsgStack.has(error.code); 14 | } 15 | 16 | /** 17 | * 显示错误信息 18 | */ 19 | export function showErrorMsg(error: Service.RequestError) { 20 | if (!error.msg || NO_ERROR_MSG_CODE.includes(error.code) || hasErrorMsg(error)) return; 21 | 22 | addErrorMsg(error); 23 | window.console.warn(error.code, error.msg); 24 | window.$message?.error({ 25 | message: error.msg, 26 | duration: ERROR_MSG_DURATION, 27 | }); 28 | setTimeout(() => { 29 | removeErrorMsg(error); 30 | }, ERROR_MSG_DURATION); 31 | } 32 | -------------------------------------------------------------------------------- /src/utils/service/transform.ts: -------------------------------------------------------------------------------- 1 | import qs from "qs"; 2 | import FormData from "form-data"; 3 | import { EnumContentType } from "@/enum"; 4 | import { isArray, isFile } from "../common"; 5 | 6 | /** 7 | * 请求数据转换 8 | * @param requestData - 请求数据 9 | * @param contentType - 请求头的Content-Type 10 | */ 11 | export async function transformRequestData(requestData: any, contentType?: string) { 12 | // application/json类型不作处理 13 | let data = requestData; 14 | // form类型转换 15 | if (contentType === EnumContentType.formUrlencoded) { 16 | data = qs.stringify(requestData); 17 | } 18 | // form-data类型转换 19 | if (contentType === EnumContentType.formData) { 20 | data = await handleFormData(requestData); 21 | } 22 | 23 | return data; 24 | } 25 | 26 | async function handleFormData(data: Record) { 27 | const formData = new FormData(); 28 | const entries = Object.entries(data); 29 | 30 | entries.forEach(async ([key, value]) => { 31 | const isFileType = isFile(value) || (isArray(value) && value.length && isFile(value[0])); 32 | 33 | if (isFileType) { 34 | await transformFile(formData, key, value); 35 | } else { 36 | formData.append(key, value); 37 | } 38 | }); 39 | 40 | return formData; 41 | } 42 | 43 | /** 44 | * 上传文件的数据转换 45 | * @param formData 46 | * @param key - 文件的属性名 47 | * @param file - 单文件或多文件 48 | */ 49 | async function transformFile(formData: FormData, key: string, file: File[] | File) { 50 | if (isArray(file)) { 51 | // 多文件 52 | await Promise.all( 53 | (file as File[]).map((item) => { 54 | formData.append(key, item); 55 | return true; 56 | }) 57 | ); 58 | } else { 59 | // 单文件 60 | formData.append(key, file); 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/utils/storage/index.ts: -------------------------------------------------------------------------------- 1 | export * from "./local"; 2 | export * from "./session"; 3 | -------------------------------------------------------------------------------- /src/utils/storage/local.ts: -------------------------------------------------------------------------------- 1 | import { decrypto, encrypto } from "../crypto"; 2 | 3 | interface StorageData { 4 | value: unknown; 5 | expire: number | null; 6 | } 7 | 8 | /** 默认缓存期限为7天 */ 9 | const DEFAULT_CACHE_TIME = 60 * 60 * 24 * 7; 10 | 11 | export function setLocal(key: string, value: unknown, expire: number | null = DEFAULT_CACHE_TIME) { 12 | const storageData: StorageData = { 13 | value, 14 | expire: expire !== null ? Date.now() + expire * 1000 : null, 15 | }; 16 | const json = encrypto(storageData); 17 | window.localStorage.setItem(key, json); 18 | } 19 | 20 | export function getLocal(key: string) { 21 | const json = window.localStorage.getItem(key); 22 | if (json) { 23 | let storageData: StorageData | null = null; 24 | try { 25 | storageData = decrypto(json); 26 | } catch { 27 | // 防止解析失败 28 | } 29 | if (storageData) { 30 | const { value, expire } = storageData; 31 | // 在有效期内直接返回 32 | if (expire === null || expire >= Date.now()) { 33 | return value as T; 34 | } 35 | } 36 | removeLocal(key); 37 | return null; 38 | } 39 | return null; 40 | } 41 | 42 | export function removeLocal(key: string) { 43 | window.localStorage.removeItem(key); 44 | } 45 | 46 | export function clearLocal() { 47 | window.localStorage.clear(); 48 | } 49 | -------------------------------------------------------------------------------- /src/utils/storage/session.ts: -------------------------------------------------------------------------------- 1 | import { decrypto, encrypto } from "../crypto"; 2 | 3 | export function setSession(key: string, value: unknown) { 4 | const json = encrypto(value); 5 | sessionStorage.setItem(key, json); 6 | } 7 | 8 | export function getSession(key: string) { 9 | const json = sessionStorage.getItem(key); 10 | let data: T | null = null; 11 | if (json) { 12 | try { 13 | data = decrypto(json); 14 | } catch { 15 | // 防止解析失败 16 | } 17 | } 18 | return data; 19 | } 20 | 21 | export function removeSession(key: string) { 22 | window.sessionStorage.removeItem(key); 23 | } 24 | 25 | export function clearSession() { 26 | window.sessionStorage.clear(); 27 | } 28 | -------------------------------------------------------------------------------- /src/views/about/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/auth-demo/permission/index.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 40 | 41 | 46 | -------------------------------------------------------------------------------- /src/views/auth-demo/super/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/auth-demo/user/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/component/complex-form/components/index.ts: -------------------------------------------------------------------------------- 1 | import BaseValidatorForm from "./BaseValidatorForm.vue"; 2 | import CustomValidatorForm from "./CustomValidatorForm.vue"; 3 | import ResponsibilityValidatorForm from "./ResponsibilityValidatorForm.vue"; 4 | 5 | export { BaseValidatorForm, CustomValidatorForm, ResponsibilityValidatorForm }; 6 | -------------------------------------------------------------------------------- /src/views/component/complex-form/index.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 18 | 19 | 24 | -------------------------------------------------------------------------------- /src/views/component/step-form/components/StepFour.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/views/component/step-form/components/StepOne.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/component/step-form/components/StepThree.vue: -------------------------------------------------------------------------------- 1 | 12 | 13 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/views/component/step-form/components/StepTwo.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 18 | 19 | 20 | -------------------------------------------------------------------------------- /src/views/component/step-form/components/index.ts: -------------------------------------------------------------------------------- 1 | import StepOne from "./StepOne.vue"; 2 | import StepTwo from "./StepTwo.vue"; 3 | import StepThree from "./StepThree.vue"; 4 | import StepFour from "./StepFour.vue"; 5 | 6 | export { StepOne, StepTwo, StepThree, StepFour }; 7 | -------------------------------------------------------------------------------- /src/views/component/step-form/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 31 | 32 | 37 | -------------------------------------------------------------------------------- /src/views/component/table/components/BaseTable.vue: -------------------------------------------------------------------------------- 1 | 43 | 44 | 78 | 79 | 84 | -------------------------------------------------------------------------------- /src/views/component/table/components/index.ts: -------------------------------------------------------------------------------- 1 | import BaseTable from "./BaseTable.vue"; 2 | 3 | export { BaseTable }; 4 | -------------------------------------------------------------------------------- /src/views/component/table/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 21 | -------------------------------------------------------------------------------- /src/views/component/verify/components/DragVerifyExample.vue: -------------------------------------------------------------------------------- 1 | 49 | 50 | 73 | 74 | 75 | -------------------------------------------------------------------------------- /src/views/component/verify/components/ImgRotateVerifyExample.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 19 | 20 | 21 | -------------------------------------------------------------------------------- /src/views/component/verify/components/RandomVerifyExample.vue: -------------------------------------------------------------------------------- 1 | 13 | 14 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/views/component/verify/components/index.ts: -------------------------------------------------------------------------------- 1 | import DragVerifyExample from "./DragVerifyExample.vue"; 2 | import ImgRotateVerifyExample from "./ImgRotateVerifyExample.vue"; 3 | import RandomVerifyExample from "./RandomVerifyExample.vue"; 4 | 5 | export { DragVerifyExample, ImgRotateVerifyExample, RandomVerifyExample }; 6 | -------------------------------------------------------------------------------- /src/views/component/verify/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 25 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/BottomPart/components/index.ts: -------------------------------------------------------------------------------- 1 | import HomeTable from "./HomeTable.vue"; 2 | 3 | export { HomeTable }; 4 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/BottomPart/index.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 41 | 42 | 47 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/DataCard/components/GradientBg.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 21 | 22 | 23 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/DataCard/components/index.ts: -------------------------------------------------------------------------------- 1 | import GradientBg from "./GradientBg.vue"; 2 | 3 | export { GradientBg }; 4 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/DataCard/index.vue: -------------------------------------------------------------------------------- 1 | 33 | 34 | 93 | 94 | 103 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/components/index.ts: -------------------------------------------------------------------------------- 1 | import DataCard from "./DataCard/index.vue"; 2 | import Charts from "./Charts/index.vue"; 3 | import BottomPart from "./BottomPart/index.vue"; 4 | 5 | export { DataCard, Charts, BottomPart }; 6 | -------------------------------------------------------------------------------- /src/views/dashboard/analysis/index.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 16 | 17 | 18 | -------------------------------------------------------------------------------- /src/views/exception/403/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/exception/404/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/exception/500/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/exception/other/index.vue: -------------------------------------------------------------------------------- 1 | 21 | 22 | 25 | 26 | 27 | -------------------------------------------------------------------------------- /src/views/function/request/index.vue: -------------------------------------------------------------------------------- 1 | 19 | 20 | 51 | 52 | 57 | -------------------------------------------------------------------------------- /src/views/login/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/multi-menu/first/second-new/third/index.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 49 | 50 | 55 | -------------------------------------------------------------------------------- /src/views/multi-menu/first/second/index.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 10 | 11 | 16 | -------------------------------------------------------------------------------- /src/views/plugin/clipboard/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 53 | 54 | 59 | -------------------------------------------------------------------------------- /src/views/plugin/echarts/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 50 | 51 | 56 | -------------------------------------------------------------------------------- /src/views/plugin/editor/markdown/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 43 | 44 | 49 | -------------------------------------------------------------------------------- /src/views/plugin/editor/quill/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 32 | 33 | 46 | -------------------------------------------------------------------------------- /src/views/plugin/excel/import/index.vue: -------------------------------------------------------------------------------- 1 | 23 | 24 | 69 | 70 | 80 | -------------------------------------------------------------------------------- /src/views/plugin/icon/index.vue: -------------------------------------------------------------------------------- 1 | 35 | 36 | 39 | 40 | 45 | -------------------------------------------------------------------------------- /src/views/plugin/map/components/BaiduMap.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/plugin/map/components/GaodeMap.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 49 | 50 | 51 | -------------------------------------------------------------------------------- /src/views/plugin/map/components/index.ts: -------------------------------------------------------------------------------- 1 | import GaodeMap from "./GaodeMap.vue"; 2 | import BaiduMap from "./BaiduMap.vue"; 3 | 4 | export { GaodeMap, BaiduMap }; 5 | -------------------------------------------------------------------------------- /src/views/plugin/map/index.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 46 | 47 | 56 | -------------------------------------------------------------------------------- /src/views/plugin/oss/components/index.ts: -------------------------------------------------------------------------------- 1 | import OssExample from "./Example.vue"; 2 | import OssSharePic from "./SharePic.vue"; 3 | 4 | export { OssExample, OssSharePic }; 5 | -------------------------------------------------------------------------------- /src/views/plugin/oss/index.vue: -------------------------------------------------------------------------------- 1 | 8 | 9 | 14 | 15 | 23 | -------------------------------------------------------------------------------- /src/views/plugin/video/index.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 37 | 38 | 43 | -------------------------------------------------------------------------------- /src/views/system-view/no-permission/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /src/views/system-view/not-found/index.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 8 | 9 | 10 | -------------------------------------------------------------------------------- /tsconfig.app.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.web.json", 3 | "include": ["build/**/*", "src/**/*", "src/**/*.vue", "src/**/*.json"], 4 | "exclude": ["src/**/__tests__/*"], 5 | "compilerOptions": { 6 | // "composite": true, 7 | "jsx": "preserve", 8 | "baseUrl": ".", 9 | "paths": { 10 | "@/*": ["./src/*"] 11 | }, 12 | "types": ["unplugin-vue-macros/macros-global", "@vue-macros/reactivity-transform/macros-global"] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.config.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@vue/tsconfig/tsconfig.node.json", 3 | "include": [ 4 | "vite.config.*", 5 | "vitest.config.*", 6 | "cypress.config.*", 7 | "playwright.config.*", 8 | ".env-config.*", 9 | "build/**/*" 10 | ], 11 | "compilerOptions": { 12 | "composite": true, 13 | "types": ["node"] 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "files": [], 3 | "references": [ 4 | { 5 | "path": "./tsconfig.config.json" 6 | }, 7 | { 8 | "path": "./tsconfig.app.json" 9 | }, 10 | { 11 | "path": "./tsconfig.vitest.json" 12 | } 13 | ] 14 | } 15 | -------------------------------------------------------------------------------- /tsconfig.vitest.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./tsconfig.app.json", 3 | "compilerOptions": { 4 | // "composite": true, 5 | "lib": ["ES2021", "DOM", "DOM.Iterable"], 6 | "types": ["node", "vitest/globals", "jsdom", "unplugin-vue-macros/macros-global"], 7 | "skipLibCheck": true 8 | }, 9 | "exclude": ["node_modules", "dist", "**/*.md"] 10 | } 11 | -------------------------------------------------------------------------------- /uno.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "unocss/vite"; 2 | import presetUno from "@unocss/preset-uno"; 3 | import { presetAttributify } from "unocss"; 4 | 5 | export default defineConfig({ 6 | exclude: [ 7 | "node_modules", 8 | "dist", 9 | ".git", 10 | ".husky", 11 | ".vscode", 12 | "public", 13 | "build", 14 | "mock", 15 | "./stats.html", 16 | ], 17 | presets: [presetUno({ dark: "class" }), presetAttributify()], // presetAttributify()开启无值的写法 18 | shortcuts: { 19 | "wh-full": "w-full h-full", 20 | "flex-center": "flex justify-center items-center", 21 | "flex-x-center": "flex justify-center", 22 | "flex-y-center": "flex items-center", 23 | "flex-x-end": "flex justify-end", 24 | "flex-1-hidden": "flex-1 overflow-hidden", 25 | "flex-col": "flex flex-col", 26 | "flex-col-center": "flex-center flex-col", 27 | "base-transition": "transition-all duration-300 ease-in-out", 28 | }, 29 | rules: [ 30 | [ 31 | /^wh-(\d+)px$/, 32 | ([, d]) => ({ 33 | width: `${d}px`, 34 | height: `${d}px`, 35 | }), 36 | ], 37 | ], 38 | // see: https://tailwindcss.com/docs/theme 39 | theme: { 40 | colors: { 41 | dark: "#18181c", 42 | dark_info: "#d5d5d6", 43 | }, 44 | }, 45 | }); 46 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import { URL, fileURLToPath } from "node:url"; 2 | import { defineConfig, loadEnv } from "vite"; 3 | import { createViteProxy, setupVitePlugins } from "./build"; 4 | import { getServiceEnvConfig } from "./.env-config"; 5 | import { resolve } from "node:path"; 6 | 7 | // https://vitejs.dev/config/ 8 | export default defineConfig(({ mode }) => { 9 | const viteEnv = loadEnv(mode, process.cwd()) as unknown as ImportMetaEnv; 10 | 11 | const isOpenProxy = viteEnv.VITE_HTTP_PROXY === "Y"; 12 | const envConfig = getServiceEnvConfig(viteEnv); 13 | 14 | return { 15 | base: viteEnv.VITE_BASE_URL, 16 | plugins: setupVitePlugins(viteEnv), 17 | resolve: { 18 | alias: { 19 | "@": fileURLToPath(new URL("./src", import.meta.url)), 20 | }, 21 | }, 22 | server: { 23 | port: 5574, 24 | open: true, 25 | proxy: createViteProxy(isOpenProxy, envConfig), 26 | }, 27 | // css配置 28 | css: { 29 | preprocessorOptions: { 30 | scss: { 31 | additionalData: `@use "./src/styles/scss/main.scss" as *;`, 32 | }, 33 | less: { 34 | // see: https://lesscss.org/usage/#less-options 35 | modifyVars: { 36 | // 全局导入 37 | // reference: 避免重复引用 38 | hack: `true; @import (reference) "${resolve("src/styles/less/main.less")}";`, 39 | }, 40 | }, 41 | }, 42 | }, 43 | // 依赖优化选项 44 | optimizeDeps: { 45 | include: [ 46 | "swiper", 47 | "swiper/vue", 48 | "@better-scroll/core", 49 | "echarts", 50 | "vditor", 51 | "xgplayer", 52 | "wangeditor", 53 | ], // 需要强制预构建的包 54 | }, 55 | // 构建选项 56 | build: { 57 | sourcemap: false, // 构建后是否生成source map 文件 58 | reportCompressedSize: false, // 启用/禁用 gzip 压缩大小报告, 压缩大型输出文件可能会很慢,因此禁用该功能可能会提高大型项目的构建性能 59 | }, 60 | }; 61 | }); 62 | -------------------------------------------------------------------------------- /vitest.config.ts: -------------------------------------------------------------------------------- 1 | import { defineConfig } from "vitest/config"; 2 | import Vue from "@vitejs/plugin-vue"; 3 | import VueJsx from "@vitejs/plugin-vue-jsx"; 4 | import VueMacros from "unplugin-vue-macros/vite"; 5 | 6 | export default defineConfig({ 7 | plugins: [ 8 | VueMacros({ 9 | setupComponent: false, 10 | setupSFC: false, 11 | plugins: { 12 | vue: Vue(), 13 | vueJsx: VueJsx(), 14 | }, 15 | }), 16 | ], 17 | optimizeDeps: { 18 | disabled: true, 19 | }, 20 | test: { 21 | clearMocks: true, 22 | environment: "jsdom", 23 | transformMode: { 24 | web: [/\.[jt]sx$/], 25 | }, 26 | }, 27 | }); 28 | --------------------------------------------------------------------------------