├── .editorconfig ├── .env ├── .eslintignore ├── .eslintrc ├── .gitignore ├── .npmrc ├── .vscode ├── extensions.json └── settings.json ├── LICENSE ├── README.md ├── README.zh-CN.md ├── index.html ├── netlify.toml ├── package.json ├── pnpm-lock.yaml ├── public ├── _headers └── favicon.svg ├── src ├── App.vue ├── api │ ├── table.ts │ └── user.ts ├── assets │ ├── 403.svg │ ├── 404.svg │ ├── 500.svg │ ├── login-bg.svg │ └── preview.jpeg ├── auto-imports.d.ts ├── components.d.ts ├── components │ ├── LayoutAside.vue │ ├── LayoutAsideMenu.vue │ ├── LayoutBreadcrumb.vue │ ├── LayoutHeader.vue │ ├── LayoutPageHeader.vue │ ├── LayoutTabs.vue │ └── README.md ├── composables │ ├── breakpoints.ts │ ├── dark.ts │ ├── descriptions.ts │ ├── form.ts │ ├── index.ts │ ├── table.ts │ └── useAsync.ts ├── layouts │ ├── 404.vue │ ├── README.md │ ├── default.vue │ └── empty.vue ├── main.ts ├── mocks │ └── index.js ├── modules │ ├── README.md │ ├── nprogress.ts │ ├── pinia.ts │ └── user.ts ├── pages │ ├── README.md │ ├── [...all].vue │ ├── dashboard │ │ ├── analysis.vue │ │ ├── monitor.vue │ │ └── workplace.vue │ ├── exception │ │ ├── 403.vue │ │ ├── 404.vue │ │ └── 500.vue │ ├── form │ │ ├── advanced-form.vue │ │ ├── basic-form.vue │ │ └── step-form.vue │ ├── icons │ │ ├── ant-design.vue │ │ └── element-plus.vue │ ├── list │ │ ├── table-pro-layout.vue │ │ ├── table-pro.vue │ │ └── table.vue │ ├── multi-window │ │ ├── index.vue │ │ └── page2.vue │ ├── permission │ │ └── index.vue │ ├── profile │ │ ├── advanced.vue │ │ └── basic.vue │ ├── result │ │ ├── fail.vue │ │ └── success.vue │ └── user │ │ └── login.vue ├── router.ts ├── shims.d.ts ├── stores │ ├── layout.ts │ ├── permission.ts │ └── user.ts ├── styles │ └── main.css ├── types.ts └── utils │ ├── index.ts │ └── request.ts ├── tsconfig.json ├── unocss.config.ts └── vite.config.ts /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | charset = utf-8 5 | indent_style = space 6 | indent_size = 2 7 | end_of_line = lf 8 | insert_final_newline = true 9 | trim_trailing_whitespace = true -------------------------------------------------------------------------------- /.env: -------------------------------------------------------------------------------- 1 | VITE_APP_NAME=MDAdmin -------------------------------------------------------------------------------- /.eslintignore: -------------------------------------------------------------------------------- 1 | dist 2 | public 3 | mocks -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "@antfu", 3 | "rules": { 4 | "@typescript-eslint/brace-style": ["error", "1tbs"], 5 | "curly": ["error", "all"] 6 | } 7 | } 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .vite-ssg-dist 3 | .vite-ssg-temp 4 | *.local 5 | dist 6 | dist-ssr 7 | node_modules 8 | .idea/ 9 | *.log 10 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.vscode/extensions.json: -------------------------------------------------------------------------------- 1 | { 2 | "recommendations": [ 3 | "antfu.iconify", 4 | "antfu.unocss", 5 | "antfu.vite", 6 | "csstools.postcss", 7 | "dbaeumer.vscode-eslint", 8 | "johnsoncodehk.volar", 9 | "lokalise.i18n-ally" 10 | ] 11 | } 12 | -------------------------------------------------------------------------------- /.vscode/settings.json: -------------------------------------------------------------------------------- 1 | { 2 | "cSpell.words": [ 3 | "antfu", 4 | "composables", 5 | "demi", 6 | "iconify", 7 | "intlify", 8 | "pinia", 9 | "pnpm", 10 | "unocss", 11 | "unplugin", 12 | "Vite", 13 | "vitejs", 14 | "Vitesse", 15 | "vitest", 16 | "vueuse" 17 | ], 18 | "i18n-ally.sourceLanguage": "en", 19 | "i18n-ally.keystyle": "nested", 20 | "i18n-ally.localesPaths": "locales", 21 | "i18n-ally.sortKeys": true, 22 | "prettier.enable": false, 23 | "editor.codeActionsOnSave": { 24 | "source.fixAll.eslint": true, 25 | }, 26 | "files.associations": { 27 | "*.css": "postcss", 28 | }, 29 | "editor.formatOnSave": false, 30 | "iconify.excludes": [ 31 | "el" 32 | ], 33 | } 34 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2022 hminghe 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 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 |

2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
19 |

20 | 21 |

22 | Mocking up web app with MDAdmin(speed)
23 |

24 | 25 |
26 | 27 |

28 | Live Demo 29 |

30 | 31 |
32 | 33 |

34 | English | 简体中文 35 | 36 |

37 | 38 |
39 | 40 | ## Features 41 | 42 | - ⚡️ [Vue 3](https://github.com/vuejs/core), [Vite 2](https://github.com/vitejs/vite), [pnpm](https://pnpm.js.org/), [ESBuild](https://github.com/evanw/esbuild) - born with fastness 43 | 44 | - 🗂 [File based routing](./src/pages) 45 | 46 | - 📦 [Components auto importing](./src/components) 47 | 48 | - 🍍 [State Management via Pinia](https://pinia.esm.dev/) 49 | 50 | - 📑 [Layout system](./src/layouts) 51 | 52 | - 🎨 [UnoCSS](https://github.com/antfu/unocss) - the instant on-demand atomic CSS engine 53 | 54 | - 😃 [Use icons from any icon sets with classes](https://github.com/antfu/unocss/tree/main/packages/preset-icons) 55 | 56 | - 🔥 Use the [new ` 18 | 19 | 20 |
21 | 22 | 23 | 24 | -------------------------------------------------------------------------------- /netlify.toml: -------------------------------------------------------------------------------- 1 | [build.environment] 2 | # bypass npm auto install 3 | NPM_FLAGS = "--version" 4 | NODE_VERSION = "16" 5 | 6 | [build] 7 | publish = "dist" 8 | command = "npx pnpm i --store=node_modules/.pnpm-store && npx pnpm run build" 9 | 10 | [[redirects]] 11 | from = "/*" 12 | to = "/index.html" 13 | status = 200 14 | 15 | [[headers]] 16 | for = "/manifest.webmanifest" 17 | [headers.values] 18 | Content-Type = "application/manifest+json" -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "packageManager": "pnpm@6.32.3", 4 | "scripts": { 5 | "build": "vite build", 6 | "dev": "vite --port 3333", 7 | "lint": "eslint .", 8 | "preview": "vite preview", 9 | "preview-https": "serve dist", 10 | "typecheck": "vue-tsc --noEmit" 11 | }, 12 | "dependencies": { 13 | "@vue-plus/element-plus": "^0.0.7", 14 | "@vue-plus/multi-window": "^0.0.2", 15 | "@vueuse/core": "^8.5.0", 16 | "axios": "^0.26.1", 17 | "echarts": "^5.3.3", 18 | "element-plus": "^2.2.9", 19 | "nprogress": "^0.2.0", 20 | "numeral": "^2.0.6", 21 | "omit": "^1.0.1", 22 | "pinia": "^2.0.16", 23 | "vue": "^3.2.37", 24 | "vue-demi": "^0.12.5", 25 | "vue-echarts": "^6.2.3", 26 | "vue-router": "^4.1.2" 27 | }, 28 | "devDependencies": { 29 | "@antfu/eslint-config": "^0.20.7", 30 | "@iconify-json/ant-design": "^1.1.3", 31 | "@iconify-json/ep": "^1.1.6", 32 | "@types/nprogress": "^0.2.0", 33 | "@types/numeral": "^2.0.2", 34 | "@types/omit": "^1.0.0", 35 | "@unocss/reset": "^0.32.13", 36 | "@vitejs/plugin-vue": "^3.0.1", 37 | "@vitejs/plugin-vue-jsx": "^2.0.0", 38 | "@vue/test-utils": "^2.0.2", 39 | "cross-env": "^7.0.3", 40 | "eslint": "^8.20.0", 41 | "https-localhost": "^4.7.1", 42 | "less": "^4.1.3", 43 | "mockjs": "^1.1.0", 44 | "pnpm": "^7.5.2", 45 | "typescript": "^4.7.4", 46 | "unocss": "^0.44.4", 47 | "unplugin-auto-import": "^0.9.3", 48 | "unplugin-vue-components": "^0.21.1", 49 | "vite": "^3.0.2", 50 | "vite-plugin-inspect": "^0.6.0", 51 | "vite-plugin-pages": "^0.25.0", 52 | "vite-plugin-vue-layouts": "^0.6.0", 53 | "vue-tsc": "^0.34.17" 54 | } 55 | } 56 | -------------------------------------------------------------------------------- /public/_headers: -------------------------------------------------------------------------------- 1 | /assets/* 2 | cache-control: max-age=31536000 3 | cache-control: immutable 4 | -------------------------------------------------------------------------------- /public/favicon.svg: -------------------------------------------------------------------------------- 1 | 2 | 13 | 14 | 15 | -------------------------------------------------------------------------------- /src/App.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 12 | -------------------------------------------------------------------------------- /src/api/table.ts: -------------------------------------------------------------------------------- 1 | import { type TableBaseResult } from '@/composables' 2 | import request, { get } from '@/utils/request' 3 | 4 | export interface TableListResponse { 5 | id: number 6 | name: string 7 | desc: string 8 | count: number 9 | status: number 10 | lastTime: string 11 | } 12 | 13 | export interface TableListRequest { 14 | name: string 15 | } 16 | 17 | export function getTableList(query: TableListRequest): Promise> { 18 | return get('/tableList', query) 19 | } 20 | 21 | export function tableListDelete(ids: number[]): Promise { 22 | return request.delete(`/tableList?ids=${ids.join(',')}`) 23 | } 24 | -------------------------------------------------------------------------------- /src/api/user.ts: -------------------------------------------------------------------------------- 1 | import { get, post } from '@/utils/request' 2 | import { type PermissionState } from '@/stores/permission' 3 | 4 | export interface UserInfoResponse { 5 | token: string 6 | username: string 7 | nickname: string 8 | avatar: string 9 | permission: PermissionState 10 | } 11 | 12 | export function getUserInfo(): Promise { 13 | return get('/user/info') 14 | } 15 | 16 | export function login(username: string, password: string): Promise { 17 | return post('/user/login', { 18 | username, 19 | password, 20 | }) 21 | } 22 | 23 | export function logout(): Promise { 24 | return post('/user/logout') 25 | } 26 | -------------------------------------------------------------------------------- /src/assets/403.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/404.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /src/assets/500.svg: -------------------------------------------------------------------------------- 1 | cancel -------------------------------------------------------------------------------- /src/assets/login-bg.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | Group 21 5 | Created with Sketch. 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | 65 | 66 | 67 | 68 | 69 | -------------------------------------------------------------------------------- /src/assets/preview.jpeg: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/hminghe/md-admin-element-plus/f6c4f470536a671067ce2253b0902bc5669581b9/src/assets/preview.jpeg -------------------------------------------------------------------------------- /src/auto-imports.d.ts: -------------------------------------------------------------------------------- 1 | // Generated by 'unplugin-auto-import' 2 | export {} 3 | declare global { 4 | const $$: typeof import('vue/macros')['$$'] 5 | const $: typeof import('vue/macros')['$'] 6 | const $computed: typeof import('vue/macros')['$computed'] 7 | const $customRef: typeof import('vue/macros')['$customRef'] 8 | const $ref: typeof import('vue/macros')['$ref'] 9 | const $shallowRef: typeof import('vue/macros')['$shallowRef'] 10 | const $toRef: typeof import('vue/macros')['$toRef'] 11 | const EffectScope: typeof import('vue')['EffectScope'] 12 | const ElMessage: typeof import('element-plus/es')['ElMessage'] 13 | const asyncComputed: typeof import('@vueuse/core')['asyncComputed'] 14 | const autoResetRef: typeof import('@vueuse/core')['autoResetRef'] 15 | const computed: typeof import('vue')['computed'] 16 | const computedAsync: typeof import('@vueuse/core')['computedAsync'] 17 | const computedEager: typeof import('@vueuse/core')['computedEager'] 18 | const computedInject: typeof import('@vueuse/core')['computedInject'] 19 | const computedWithControl: typeof import('@vueuse/core')['computedWithControl'] 20 | const controlledComputed: typeof import('@vueuse/core')['controlledComputed'] 21 | const controlledRef: typeof import('@vueuse/core')['controlledRef'] 22 | const createApp: typeof import('vue')['createApp'] 23 | const createEventHook: typeof import('@vueuse/core')['createEventHook'] 24 | const createGlobalState: typeof import('@vueuse/core')['createGlobalState'] 25 | const createInjectionState: typeof import('@vueuse/core')['createInjectionState'] 26 | const createReactiveFn: typeof import('@vueuse/core')['createReactiveFn'] 27 | const createSharedComposable: typeof import('@vueuse/core')['createSharedComposable'] 28 | const createUnrefFn: typeof import('@vueuse/core')['createUnrefFn'] 29 | const customRef: typeof import('vue')['customRef'] 30 | const debouncedRef: typeof import('@vueuse/core')['debouncedRef'] 31 | const debouncedWatch: typeof import('@vueuse/core')['debouncedWatch'] 32 | const defineAsyncComponent: typeof import('vue')['defineAsyncComponent'] 33 | const defineComponent: typeof import('vue')['defineComponent'] 34 | const eagerComputed: typeof import('@vueuse/core')['eagerComputed'] 35 | const effectScope: typeof import('vue')['effectScope'] 36 | const extendRef: typeof import('@vueuse/core')['extendRef'] 37 | const getCurrentInstance: typeof import('vue')['getCurrentInstance'] 38 | const getCurrentScope: typeof import('vue')['getCurrentScope'] 39 | const h: typeof import('vue')['h'] 40 | const ignorableWatch: typeof import('@vueuse/core')['ignorableWatch'] 41 | const inject: typeof import('vue')['inject'] 42 | const isDefined: typeof import('@vueuse/core')['isDefined'] 43 | const isProxy: typeof import('vue')['isProxy'] 44 | const isReactive: typeof import('vue')['isReactive'] 45 | const isReadonly: typeof import('vue')['isReadonly'] 46 | const isRef: typeof import('vue')['isRef'] 47 | const logicAnd: typeof import('@vueuse/core')['logicAnd'] 48 | const logicNot: typeof import('@vueuse/core')['logicNot'] 49 | const logicOr: typeof import('@vueuse/core')['logicOr'] 50 | const makeDestructurable: typeof import('@vueuse/core')['makeDestructurable'] 51 | const markRaw: typeof import('vue')['markRaw'] 52 | const nextTick: typeof import('vue')['nextTick'] 53 | const onActivated: typeof import('vue')['onActivated'] 54 | const onBeforeMount: typeof import('vue')['onBeforeMount'] 55 | const onBeforeUnmount: typeof import('vue')['onBeforeUnmount'] 56 | const onBeforeUpdate: typeof import('vue')['onBeforeUpdate'] 57 | const onClickOutside: typeof import('@vueuse/core')['onClickOutside'] 58 | const onDeactivated: typeof import('vue')['onDeactivated'] 59 | const onErrorCaptured: typeof import('vue')['onErrorCaptured'] 60 | const onKeyStroke: typeof import('@vueuse/core')['onKeyStroke'] 61 | const onLongPress: typeof import('@vueuse/core')['onLongPress'] 62 | const onMounted: typeof import('vue')['onMounted'] 63 | const onRenderTracked: typeof import('vue')['onRenderTracked'] 64 | const onRenderTriggered: typeof import('vue')['onRenderTriggered'] 65 | const onScopeDispose: typeof import('vue')['onScopeDispose'] 66 | const onServerPrefetch: typeof import('vue')['onServerPrefetch'] 67 | const onStartTyping: typeof import('@vueuse/core')['onStartTyping'] 68 | const onUnmounted: typeof import('vue')['onUnmounted'] 69 | const onUpdated: typeof import('vue')['onUpdated'] 70 | const pausableWatch: typeof import('@vueuse/core')['pausableWatch'] 71 | const provide: typeof import('vue')['provide'] 72 | const reactify: typeof import('@vueuse/core')['reactify'] 73 | const reactifyObject: typeof import('@vueuse/core')['reactifyObject'] 74 | const reactive: typeof import('vue')['reactive'] 75 | const reactiveComputed: typeof import('@vueuse/core')['reactiveComputed'] 76 | const reactiveOmit: typeof import('@vueuse/core')['reactiveOmit'] 77 | const reactivePick: typeof import('@vueuse/core')['reactivePick'] 78 | const readonly: typeof import('vue')['readonly'] 79 | const ref: typeof import('vue')['ref'] 80 | const refAutoReset: typeof import('@vueuse/core')['refAutoReset'] 81 | const refDebounced: typeof import('@vueuse/core')['refDebounced'] 82 | const refDefault: typeof import('@vueuse/core')['refDefault'] 83 | const refThrottled: typeof import('@vueuse/core')['refThrottled'] 84 | const refWithControl: typeof import('@vueuse/core')['refWithControl'] 85 | const resolveComponent: typeof import('vue')['resolveComponent'] 86 | const resolveRef: typeof import('@vueuse/core')['resolveRef'] 87 | const resolveUnref: typeof import('@vueuse/core')['resolveUnref'] 88 | const shallowReactive: typeof import('vue')['shallowReactive'] 89 | const shallowReadonly: typeof import('vue')['shallowReadonly'] 90 | const shallowRef: typeof import('vue')['shallowRef'] 91 | const syncRef: typeof import('@vueuse/core')['syncRef'] 92 | const syncRefs: typeof import('@vueuse/core')['syncRefs'] 93 | const templateRef: typeof import('@vueuse/core')['templateRef'] 94 | const throttledRef: typeof import('@vueuse/core')['throttledRef'] 95 | const throttledWatch: typeof import('@vueuse/core')['throttledWatch'] 96 | const toRaw: typeof import('vue')['toRaw'] 97 | const toReactive: typeof import('@vueuse/core')['toReactive'] 98 | const toRef: typeof import('vue')['toRef'] 99 | const toRefs: typeof import('vue')['toRefs'] 100 | const triggerRef: typeof import('vue')['triggerRef'] 101 | const tryOnBeforeMount: typeof import('@vueuse/core')['tryOnBeforeMount'] 102 | const tryOnBeforeUnmount: typeof import('@vueuse/core')['tryOnBeforeUnmount'] 103 | const tryOnMounted: typeof import('@vueuse/core')['tryOnMounted'] 104 | const tryOnScopeDispose: typeof import('@vueuse/core')['tryOnScopeDispose'] 105 | const tryOnUnmounted: typeof import('@vueuse/core')['tryOnUnmounted'] 106 | const unref: typeof import('vue')['unref'] 107 | const unrefElement: typeof import('@vueuse/core')['unrefElement'] 108 | const until: typeof import('@vueuse/core')['until'] 109 | const useActiveElement: typeof import('@vueuse/core')['useActiveElement'] 110 | const useAsyncQueue: typeof import('@vueuse/core')['useAsyncQueue'] 111 | const useAsyncState: typeof import('@vueuse/core')['useAsyncState'] 112 | const useAttrs: typeof import('vue')['useAttrs'] 113 | const useBase64: typeof import('@vueuse/core')['useBase64'] 114 | const useBattery: typeof import('@vueuse/core')['useBattery'] 115 | const useBluetooth: typeof import('@vueuse/core')['useBluetooth'] 116 | const useBreakpoints: typeof import('@vueuse/core')['useBreakpoints'] 117 | const useBroadcastChannel: typeof import('@vueuse/core')['useBroadcastChannel'] 118 | const useBrowserLocation: typeof import('@vueuse/core')['useBrowserLocation'] 119 | const useCached: typeof import('@vueuse/core')['useCached'] 120 | const useClamp: typeof import('@vueuse/core')['useClamp'] 121 | const useClipboard: typeof import('@vueuse/core')['useClipboard'] 122 | const useColorMode: typeof import('@vueuse/core')['useColorMode'] 123 | const useConfirmDialog: typeof import('@vueuse/core')['useConfirmDialog'] 124 | const useCounter: typeof import('@vueuse/core')['useCounter'] 125 | const useCssModule: typeof import('vue')['useCssModule'] 126 | const useCssVar: typeof import('@vueuse/core')['useCssVar'] 127 | const useCssVars: typeof import('vue')['useCssVars'] 128 | const useCurrentElement: typeof import('@vueuse/core')['useCurrentElement'] 129 | const useCycleList: typeof import('@vueuse/core')['useCycleList'] 130 | const useDark: typeof import('@vueuse/core')['useDark'] 131 | const useDateFormat: typeof import('@vueuse/core')['useDateFormat'] 132 | const useDebounce: typeof import('@vueuse/core')['useDebounce'] 133 | const useDebounceFn: typeof import('@vueuse/core')['useDebounceFn'] 134 | const useDebouncedRefHistory: typeof import('@vueuse/core')['useDebouncedRefHistory'] 135 | const useDeviceMotion: typeof import('@vueuse/core')['useDeviceMotion'] 136 | const useDeviceOrientation: typeof import('@vueuse/core')['useDeviceOrientation'] 137 | const useDevicePixelRatio: typeof import('@vueuse/core')['useDevicePixelRatio'] 138 | const useDevicesList: typeof import('@vueuse/core')['useDevicesList'] 139 | const useDisplayMedia: typeof import('@vueuse/core')['useDisplayMedia'] 140 | const useDocumentVisibility: typeof import('@vueuse/core')['useDocumentVisibility'] 141 | const useDraggable: typeof import('@vueuse/core')['useDraggable'] 142 | const useDropZone: typeof import('@vueuse/core')['useDropZone'] 143 | const useElementBounding: typeof import('@vueuse/core')['useElementBounding'] 144 | const useElementByPoint: typeof import('@vueuse/core')['useElementByPoint'] 145 | const useElementHover: typeof import('@vueuse/core')['useElementHover'] 146 | const useElementSize: typeof import('@vueuse/core')['useElementSize'] 147 | const useElementVisibility: typeof import('@vueuse/core')['useElementVisibility'] 148 | const useEventBus: typeof import('@vueuse/core')['useEventBus'] 149 | const useEventListener: typeof import('@vueuse/core')['useEventListener'] 150 | const useEventSource: typeof import('@vueuse/core')['useEventSource'] 151 | const useEyeDropper: typeof import('@vueuse/core')['useEyeDropper'] 152 | const useFavicon: typeof import('@vueuse/core')['useFavicon'] 153 | const useFetch: typeof import('@vueuse/core')['useFetch'] 154 | const useFileDialog: typeof import('@vueuse/core')['useFileDialog'] 155 | const useFileSystemAccess: typeof import('@vueuse/core')['useFileSystemAccess'] 156 | const useFocus: typeof import('@vueuse/core')['useFocus'] 157 | const useFocusWithin: typeof import('@vueuse/core')['useFocusWithin'] 158 | const useFps: typeof import('@vueuse/core')['useFps'] 159 | const useFullscreen: typeof import('@vueuse/core')['useFullscreen'] 160 | const useGamepad: typeof import('@vueuse/core')['useGamepad'] 161 | const useGeolocation: typeof import('@vueuse/core')['useGeolocation'] 162 | const useIdle: typeof import('@vueuse/core')['useIdle'] 163 | const useImage: typeof import('@vueuse/core')['useImage'] 164 | const useInfiniteScroll: typeof import('@vueuse/core')['useInfiniteScroll'] 165 | const useIntersectionObserver: typeof import('@vueuse/core')['useIntersectionObserver'] 166 | const useInterval: typeof import('@vueuse/core')['useInterval'] 167 | const useIntervalFn: typeof import('@vueuse/core')['useIntervalFn'] 168 | const useKeyModifier: typeof import('@vueuse/core')['useKeyModifier'] 169 | const useLastChanged: typeof import('@vueuse/core')['useLastChanged'] 170 | const useLocalStorage: typeof import('@vueuse/core')['useLocalStorage'] 171 | const useMagicKeys: typeof import('@vueuse/core')['useMagicKeys'] 172 | const useManualRefHistory: typeof import('@vueuse/core')['useManualRefHistory'] 173 | const useMediaControls: typeof import('@vueuse/core')['useMediaControls'] 174 | const useMediaQuery: typeof import('@vueuse/core')['useMediaQuery'] 175 | const useMemoize: typeof import('@vueuse/core')['useMemoize'] 176 | const useMemory: typeof import('@vueuse/core')['useMemory'] 177 | const useMounted: typeof import('@vueuse/core')['useMounted'] 178 | const useMouse: typeof import('@vueuse/core')['useMouse'] 179 | const useMouseInElement: typeof import('@vueuse/core')['useMouseInElement'] 180 | const useMousePressed: typeof import('@vueuse/core')['useMousePressed'] 181 | const useMutationObserver: typeof import('@vueuse/core')['useMutationObserver'] 182 | const useNavigatorLanguage: typeof import('@vueuse/core')['useNavigatorLanguage'] 183 | const useNetwork: typeof import('@vueuse/core')['useNetwork'] 184 | const useNow: typeof import('@vueuse/core')['useNow'] 185 | const useObjectUrl: typeof import('@vueuse/core')['useObjectUrl'] 186 | const useOffsetPagination: typeof import('@vueuse/core')['useOffsetPagination'] 187 | const useOnline: typeof import('@vueuse/core')['useOnline'] 188 | const usePageLeave: typeof import('@vueuse/core')['usePageLeave'] 189 | const useParallax: typeof import('@vueuse/core')['useParallax'] 190 | const usePermission: typeof import('@vueuse/core')['usePermission'] 191 | const usePointer: typeof import('@vueuse/core')['usePointer'] 192 | const usePointerSwipe: typeof import('@vueuse/core')['usePointerSwipe'] 193 | const usePreferredColorScheme: typeof import('@vueuse/core')['usePreferredColorScheme'] 194 | const usePreferredDark: typeof import('@vueuse/core')['usePreferredDark'] 195 | const usePreferredLanguages: typeof import('@vueuse/core')['usePreferredLanguages'] 196 | const useRafFn: typeof import('@vueuse/core')['useRafFn'] 197 | const useRefHistory: typeof import('@vueuse/core')['useRefHistory'] 198 | const useResizeObserver: typeof import('@vueuse/core')['useResizeObserver'] 199 | const useRoute: typeof import('vue-router')['useRoute'] 200 | const useRouter: typeof import('vue-router')['useRouter'] 201 | const useScreenOrientation: typeof import('@vueuse/core')['useScreenOrientation'] 202 | const useScreenSafeArea: typeof import('@vueuse/core')['useScreenSafeArea'] 203 | const useScriptTag: typeof import('@vueuse/core')['useScriptTag'] 204 | const useScroll: typeof import('@vueuse/core')['useScroll'] 205 | const useScrollLock: typeof import('@vueuse/core')['useScrollLock'] 206 | const useSessionStorage: typeof import('@vueuse/core')['useSessionStorage'] 207 | const useShare: typeof import('@vueuse/core')['useShare'] 208 | const useSlots: typeof import('vue')['useSlots'] 209 | const useSpeechRecognition: typeof import('@vueuse/core')['useSpeechRecognition'] 210 | const useSpeechSynthesis: typeof import('@vueuse/core')['useSpeechSynthesis'] 211 | const useStepper: typeof import('@vueuse/core')['useStepper'] 212 | const useStorage: typeof import('@vueuse/core')['useStorage'] 213 | const useStorageAsync: typeof import('@vueuse/core')['useStorageAsync'] 214 | const useStyleTag: typeof import('@vueuse/core')['useStyleTag'] 215 | const useSwipe: typeof import('@vueuse/core')['useSwipe'] 216 | const useTemplateRefsList: typeof import('@vueuse/core')['useTemplateRefsList'] 217 | const useTextSelection: typeof import('@vueuse/core')['useTextSelection'] 218 | const useTextareaAutosize: typeof import('@vueuse/core')['useTextareaAutosize'] 219 | const useThrottle: typeof import('@vueuse/core')['useThrottle'] 220 | const useThrottleFn: typeof import('@vueuse/core')['useThrottleFn'] 221 | const useThrottledRefHistory: typeof import('@vueuse/core')['useThrottledRefHistory'] 222 | const useTimeAgo: typeof import('@vueuse/core')['useTimeAgo'] 223 | const useTimeout: typeof import('@vueuse/core')['useTimeout'] 224 | const useTimeoutFn: typeof import('@vueuse/core')['useTimeoutFn'] 225 | const useTimeoutPoll: typeof import('@vueuse/core')['useTimeoutPoll'] 226 | const useTimestamp: typeof import('@vueuse/core')['useTimestamp'] 227 | const useTitle: typeof import('@vueuse/core')['useTitle'] 228 | const useToggle: typeof import('@vueuse/core')['useToggle'] 229 | const useTransition: typeof import('@vueuse/core')['useTransition'] 230 | const useUrlSearchParams: typeof import('@vueuse/core')['useUrlSearchParams'] 231 | const useUserMedia: typeof import('@vueuse/core')['useUserMedia'] 232 | const useVModel: typeof import('@vueuse/core')['useVModel'] 233 | const useVModels: typeof import('@vueuse/core')['useVModels'] 234 | const useVibrate: typeof import('@vueuse/core')['useVibrate'] 235 | const useVirtualList: typeof import('@vueuse/core')['useVirtualList'] 236 | const useWakeLock: typeof import('@vueuse/core')['useWakeLock'] 237 | const useWebNotification: typeof import('@vueuse/core')['useWebNotification'] 238 | const useWebSocket: typeof import('@vueuse/core')['useWebSocket'] 239 | const useWebWorker: typeof import('@vueuse/core')['useWebWorker'] 240 | const useWebWorkerFn: typeof import('@vueuse/core')['useWebWorkerFn'] 241 | const useWindowFocus: typeof import('@vueuse/core')['useWindowFocus'] 242 | const useWindowScroll: typeof import('@vueuse/core')['useWindowScroll'] 243 | const useWindowSize: typeof import('@vueuse/core')['useWindowSize'] 244 | const watch: typeof import('vue')['watch'] 245 | const watchArray: typeof import('@vueuse/core')['watchArray'] 246 | const watchAtMost: typeof import('@vueuse/core')['watchAtMost'] 247 | const watchDebounced: typeof import('@vueuse/core')['watchDebounced'] 248 | const watchEffect: typeof import('vue')['watchEffect'] 249 | const watchIgnorable: typeof import('@vueuse/core')['watchIgnorable'] 250 | const watchOnce: typeof import('@vueuse/core')['watchOnce'] 251 | const watchPausable: typeof import('@vueuse/core')['watchPausable'] 252 | const watchPostEffect: typeof import('vue')['watchPostEffect'] 253 | const watchSyncEffect: typeof import('vue')['watchSyncEffect'] 254 | const watchThrottled: typeof import('@vueuse/core')['watchThrottled'] 255 | const watchTriggerable: typeof import('@vueuse/core')['watchTriggerable'] 256 | const watchWithFilter: typeof import('@vueuse/core')['watchWithFilter'] 257 | const whenever: typeof import('@vueuse/core')['whenever'] 258 | } 259 | -------------------------------------------------------------------------------- /src/components.d.ts: -------------------------------------------------------------------------------- 1 | // generated by unplugin-vue-components 2 | // We suggest you to commit this file into source control 3 | // Read more: https://github.com/vuejs/core/pull/3399 4 | import '@vue/runtime-core' 5 | 6 | export {} 7 | 8 | declare module '@vue/runtime-core' { 9 | export interface GlobalComponents { 10 | LayoutAside: typeof import('./components/LayoutAside.vue')['default'] 11 | LayoutAsideMenu: typeof import('./components/LayoutAsideMenu.vue')['default'] 12 | LayoutBreadcrumb: typeof import('./components/LayoutBreadcrumb.vue')['default'] 13 | LayoutHeader: typeof import('./components/LayoutHeader.vue')['default'] 14 | LayoutPageHeader: typeof import('./components/LayoutPageHeader.vue')['default'] 15 | LayoutTabs: typeof import('./components/LayoutTabs.vue')['default'] 16 | MultiWindowKeepAlive: typeof import('./components/multi-window/components/MultiWindowKeepAlive.vue')['default'] 17 | MultiWindowRouter: typeof import('./components/multi-window/components/MultiWindowRouter.vue')['default'] 18 | MultiWindowRouterTransition: typeof import('./components/multi-window/components/MultiWindowRouterTransition.vue')['default'] 19 | RouterLink: typeof import('vue-router')['RouterLink'] 20 | RouterView: typeof import('vue-router')['RouterView'] 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/components/LayoutAside.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 46 | 47 | 70 | -------------------------------------------------------------------------------- /src/components/LayoutAsideMenu.vue: -------------------------------------------------------------------------------- 1 | 87 | 88 | 102 | -------------------------------------------------------------------------------- /src/components/LayoutBreadcrumb.vue: -------------------------------------------------------------------------------- 1 | 37 | 38 | 48 | -------------------------------------------------------------------------------- /src/components/LayoutHeader.vue: -------------------------------------------------------------------------------- 1 | 25 | 26 | 83 | 84 | 135 | -------------------------------------------------------------------------------- /src/components/LayoutPageHeader.vue: -------------------------------------------------------------------------------- 1 | 9 | 10 | 23 | 24 | 48 | -------------------------------------------------------------------------------- /src/components/LayoutTabs.vue: -------------------------------------------------------------------------------- 1 | 103 | 104 | 142 | 143 | 176 | -------------------------------------------------------------------------------- /src/components/README.md: -------------------------------------------------------------------------------- 1 | ## Components 2 | 3 | Components in this dir will be auto-registered and on-demand, powered by [`unplugin-vue-components`](https://github.com/antfu/unplugin-vue-components). 4 | 5 | 6 | ### Icons 7 | 8 | You can use icons from almost any icon sets by the power of [Iconify](https://iconify.design/). 9 | 10 | It will only bundle the icons you use. Check out [`unplugin-icons`](https://github.com/antfu/unplugin-icons) for more details. 11 | -------------------------------------------------------------------------------- /src/composables/breakpoints.ts: -------------------------------------------------------------------------------- 1 | import { breakpointsTailwind, useBreakpoints } from '@vueuse/core' 2 | 3 | // sm: 640px; 4 | // md: 768px; 5 | // lg: 1024px; 6 | // xl: 1280px; 7 | // '2xl': 1536px; 8 | export const breakpoints = useBreakpoints(breakpointsTailwind) 9 | -------------------------------------------------------------------------------- /src/composables/dark.ts: -------------------------------------------------------------------------------- 1 | // these APIs are auto-imported from @vueuse/core 2 | export const isDark = useDark() 3 | export const toggleDark = useToggle(isDark) 4 | -------------------------------------------------------------------------------- /src/composables/descriptions.ts: -------------------------------------------------------------------------------- 1 | import { computed } from 'vue' 2 | import { breakpoints } from '@/composables' 3 | 4 | export const descriptionsColumn = computed(() => { 5 | if (breakpoints.lg.value) { 6 | return 3 7 | } 8 | if (breakpoints.md.value) { 9 | return 2 10 | } 11 | return 1 12 | }) 13 | -------------------------------------------------------------------------------- /src/composables/form.ts: -------------------------------------------------------------------------------- 1 | import { type FormInstance } from 'element-plus' 2 | 3 | export function useForm>(data: Data) { 4 | const form$ = ref() 5 | const form = ref({ 6 | ...data, 7 | }) 8 | 9 | function resetForm() { 10 | resetFields() 11 | form.value = { 12 | ...data, 13 | } 14 | } 15 | 16 | function resetFields() { 17 | return form$.value?.resetFields() 18 | } 19 | 20 | function validate() { 21 | return form$.value?.validate() 22 | } 23 | 24 | return { 25 | form$, 26 | form, 27 | resetForm, 28 | validate, 29 | resetFields, 30 | } 31 | } 32 | -------------------------------------------------------------------------------- /src/composables/index.ts: -------------------------------------------------------------------------------- 1 | export * from './dark' 2 | export * from './useAsync' 3 | export * from './table' 4 | export * from './form' 5 | export * from './breakpoints' 6 | export * from './descriptions' 7 | -------------------------------------------------------------------------------- /src/composables/table.ts: -------------------------------------------------------------------------------- 1 | import { type Sort } from 'element-plus/lib/components/table/src/table/defaults' 2 | import { type Ref, computed, ref } from 'vue' 3 | 4 | export interface TableBaseResult { 5 | list: T[] 6 | total: number 7 | } 8 | 9 | export interface useTableOptions { 10 | defaultPageSize?: number 11 | defaultSort?: Sort 12 | } 13 | 14 | // interface UseTableListReturn extends UsePaginationReturn, UseSortReturn, UseSelectionReturn { 15 | // data: Ref> 16 | // isLoading: Ref 17 | // refresh: () => void 18 | // } 19 | 20 | // 分页 21 | export function usePagination(defaultPageSize = 15) { 22 | const currentPage = refWithControl(1) 23 | const pageSize = ref(defaultPageSize) 24 | 25 | return { 26 | currentPage, 27 | pageSize, 28 | } 29 | } 30 | 31 | // 排序 32 | export function useSort(defaultSort: Sort = { prop: '', order: 'descending' }) { 33 | const sort = ref({ 34 | ...defaultSort, 35 | }) 36 | 37 | function sortChange({ prop, order }: Sort) { 38 | sort.value = { prop, order } 39 | } 40 | 41 | return { 42 | sort, 43 | sortChange, 44 | } 45 | } 46 | 47 | // 表格选择 48 | export function useSelection() { 49 | const selection = ref([]) as Ref 50 | const selectionCount = computed(() => selection.value?.length || 0) 51 | const selectionChange = (value: T[]) => { 52 | selection.value = value 53 | } 54 | 55 | return { 56 | selection, 57 | selectionCount, 58 | selectionChange, 59 | } 60 | } 61 | 62 | export function useTable( 63 | getTableData: (query: any) => Promise>, 64 | searchData: Record | Ref>, 65 | { 66 | defaultPageSize, 67 | defaultSort, 68 | }: useTableOptions = {}, 69 | ) { 70 | const { 71 | currentPage, 72 | pageSize, 73 | } = usePagination(defaultPageSize) 74 | 75 | const { 76 | sort, 77 | sortChange, 78 | } = useSort(defaultSort) 79 | 80 | const { 81 | selection, 82 | selectionCount, 83 | selectionChange, 84 | } = useSelection() 85 | 86 | const { state: data, execute, isLoading } = useAsyncState( 87 | () => getTableData({ 88 | pageSize: pageSize.value, 89 | currentPage: currentPage.value, 90 | sort: sort.value.prop ? sort.value.prop : null, 91 | sortOrder: sort.value.prop ? sort.value.order : null, 92 | ...unref(searchData), 93 | }), 94 | { list: [], total: 0 }, 95 | { resetOnExecute: false, delay: 1 }, 96 | ) 97 | 98 | const fetchData = useDebounceFn(execute, 1) 99 | 100 | // 刷新 101 | function refresh(page?: number) { 102 | if (page) { 103 | currentPage.silentSet(1) 104 | } 105 | fetchData() 106 | } 107 | 108 | watch(() => [pageSize.value, currentPage.value, sort.value, unref(searchData)], () => { 109 | fetchData() 110 | }) 111 | 112 | return { 113 | data, 114 | isLoading, 115 | refresh, 116 | 117 | currentPage, 118 | pageSize, 119 | 120 | sort, 121 | sortChange, 122 | 123 | selection, 124 | selectionCount, 125 | selectionChange, 126 | } 127 | } 128 | -------------------------------------------------------------------------------- /src/composables/useAsync.ts: -------------------------------------------------------------------------------- 1 | import type { Ref } from 'vue' 2 | import { ref } from 'vue' 3 | 4 | export interface UseAsyncReturn { 5 | isLoading: Ref 6 | execute: Callback 7 | } 8 | 9 | interface UseAsyncOptions { 10 | 11 | immediate?: boolean 12 | 13 | onError?: (e: unknown) => void 14 | 15 | loadingService?: () => () => void 16 | 17 | isLoading?: Ref 18 | 19 | } 20 | 21 | export function useAsync Promise)>( 22 | callback: Callback, 23 | options: UseAsyncOptions = {}, 24 | ): UseAsyncReturn { 25 | const isLoading = ref(false) 26 | 27 | let loadingServiceClose: () => void 28 | watch(isLoading, (isLoading) => { 29 | if (options.isLoading) { 30 | options.isLoading.value = isLoading 31 | } 32 | 33 | if (options.loadingService) { 34 | if (isLoading) { 35 | loadingServiceClose = options.loadingService() 36 | } else if (loadingServiceClose) { 37 | loadingServiceClose() 38 | } 39 | } 40 | }) 41 | 42 | async function execute(...args: any[]) { 43 | try { 44 | isLoading.value = true 45 | // eslint-disable-next-line @typescript-eslint/ban-ts-comment 46 | // @ts-expect-error 47 | const result = await callback(...args) 48 | isLoading.value = false 49 | return result 50 | } catch (error) { 51 | isLoading.value = false 52 | if (options.onError) { 53 | options.onError(error) 54 | } 55 | throw error 56 | } 57 | } 58 | 59 | if (options.immediate) { 60 | execute() 61 | } 62 | 63 | return { 64 | isLoading, 65 | execute: execute as unknown as Callback, 66 | } 67 | } 68 | -------------------------------------------------------------------------------- /src/layouts/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | -------------------------------------------------------------------------------- /src/layouts/README.md: -------------------------------------------------------------------------------- 1 | ## Layouts 2 | 3 | Vue components in this dir are used as layouts. 4 | 5 | By default, `default.vue` will be used unless an alternative is specified in the route meta. 6 | 7 | With [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) and [`vite-plugin-vue-layouts`](https://github.com/JohnCampionJr/vite-plugin-vue-layouts), you can specify the layout in the page's SFCs like this: 8 | 9 | ```html 10 | 11 | meta: 12 | layout: home 13 | 14 | ``` 15 | -------------------------------------------------------------------------------- /src/layouts/default.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 30 | -------------------------------------------------------------------------------- /src/layouts/empty.vue: -------------------------------------------------------------------------------- 1 | 4 | -------------------------------------------------------------------------------- /src/main.ts: -------------------------------------------------------------------------------- 1 | import App from './App.vue' 2 | import router from './router' 3 | import type { UserModule } from './types' 4 | 5 | import 'element-plus/theme-chalk/dark/css-vars.css' 6 | import 'element-plus/es/components/message/style/css' 7 | // import '@unocss/reset/tailwind.css' 8 | import './styles/main.css' 9 | import 'uno.css' 10 | 11 | import './mocks/index' 12 | 13 | const app = createApp(App) 14 | app.use(router) 15 | 16 | Object.values(import.meta.glob<{ install: UserModule }>('./modules/*.ts', { eager: true })) 17 | .forEach(i => i.install?.({ 18 | app, 19 | router, 20 | })) 21 | 22 | app.mount('#app') 23 | -------------------------------------------------------------------------------- /src/mocks/index.js: -------------------------------------------------------------------------------- 1 | import Mock from 'mockjs'; 2 | 3 | function createMenu(name, icon, path, children) { 4 | return { 5 | name, 6 | icon, 7 | path, 8 | children, 9 | }; 10 | } 11 | 12 | const userMap = { 13 | admin: { 14 | username: 'admin', 15 | nickname: 'Admin', 16 | token: 'admin-token', 17 | avatar: 'https://iph.href.lu/100x100?text=MD&fg=FFFFFF&bg=000000', 18 | permission: { 19 | identifyList: ['*'], 20 | menuList: [ 21 | createMenu('仪表盘', 'i-ep-odometer', '/dashboard', [ 22 | createMenu('工作台', '', '/dashboard/workplace'), 23 | createMenu('分析页', '', '/dashboard/analysis'), 24 | createMenu('监控页', '', '/dashboard/monitor'), 25 | ]), 26 | createMenu('表单页', 'i-ep-edit', '/form', [ 27 | createMenu('基础表单', '', '/form/basic-form'), 28 | createMenu('分步表单', '', '/form/step-form'), 29 | createMenu('高级表单', '', '/form/advanced-form'), 30 | ]), 31 | createMenu('列表页', 'i-ep-grid', '/list', [ 32 | createMenu('查询列表', '', '/list/table'), 33 | createMenu('TablePro', '', '/list/table-pro'), 34 | createMenu('自定义布局(TablePro)', '', '/list/table-pro-layout'), 35 | ]), 36 | createMenu('详情页', 'i-ep-document', '/profile', [ 37 | createMenu('基础详情页', '', '/profile/basic'), 38 | createMenu('高级详情页', '', '/profile/advanced'), 39 | ]), 40 | createMenu('结果页', 'i-ep-reading', '/result', [ 41 | createMenu('成功页', '', '/result/success'), 42 | createMenu('失败页', '', '/result/fail'), 43 | ]), 44 | createMenu('异常页', 'i-ep-data-board', '/exception', [ 45 | createMenu('403', '', '/exception/403'), 46 | createMenu('404', '', '/exception/404'), 47 | createMenu('500', '', '/exception/500'), 48 | ]), 49 | createMenu('图标集', 'i-ep-brush', '/icons', [ 50 | createMenu('Element Plus', '', '/icons/element-plus'), 51 | createMenu('Ant Design', '', '/icons/ant-design'), 52 | createMenu('更多图标', '', 'https://icones.netlify.app/'), 53 | ]), 54 | createMenu('多窗口演示', 'i-ep-refresh-right', '/multi-window', [ 55 | createMenu('演示', '', '/multi-window'), 56 | createMenu('详细页面演示', '', '/multi-window/page2'), 57 | ]), 58 | createMenu('权限演示', 'i-ep-flag', '/permission'), 59 | ], 60 | }, 61 | }, 62 | test: { 63 | username: 'test', 64 | nickname: '测试人员', 65 | token: 'test-token', 66 | avatar: 'https://iph.href.lu/100x100?text=MD&fg=FFFFFF&bg=000000', 67 | permission: { 68 | identifyList: ['test'], 69 | menuList: [ 70 | createMenu('仪表盘', 'i-ep-odometer', '/dashboard', [ 71 | createMenu('工作台', '', '/dashboard/workplace'), 72 | createMenu('分析页', '', '/dashboard/analysis'), 73 | createMenu('监控页', '', '/dashboard/monitor'), 74 | ]), 75 | createMenu('表单页', 'i-ep-edit', '/form', [ 76 | createMenu('基础表单', '', '/form/basic-form'), 77 | createMenu('分步表单', '', '/form/step-form'), 78 | createMenu('高级表单', '', '/form/advanced-form'), 79 | ]), 80 | createMenu('列表页', 'i-ep-grid', '/list', [ 81 | createMenu('查询列表', '', '/list/table'), 82 | ]), 83 | createMenu('详情页', 'i-ep-document', '/profile', [ 84 | createMenu('基础详情页', '', '/profile/basic'), 85 | createMenu('高级详情页', '', '/profile/advanced'), 86 | ]), 87 | createMenu('权限演示', 'i-ep-flag', '/permission'), 88 | ], 89 | }, 90 | }, 91 | 92 | dev: { 93 | username: 'dev', 94 | nickname: '开发人员', 95 | token: 'dev-token', 96 | avatar: 'https://iph.href.lu/100x100?text=MD&fg=FFFFFF&bg=000000', 97 | permission: { 98 | identifyList: ['dev'], 99 | menuList: [ 100 | createMenu('权限演示', 'i-ep-flag', '/permission'), 101 | ], 102 | }, 103 | }, 104 | }; 105 | function getUser(username) { 106 | if (userMap[username]) { 107 | return { 108 | ...userMap[username], 109 | errCode: 0, 110 | errMsg: 'success', 111 | }; 112 | } else { 113 | return { 114 | errCode: 'no-login', 115 | errMsg: '请先登录', 116 | }; 117 | } 118 | } 119 | 120 | Mock.mock(new RegExp('/user/login'), (request) => { 121 | const data = JSON.parse(request.body); 122 | return getUser(data.username); 123 | 124 | // return { 125 | // errCode: '01', 126 | // errMsg: '账号或者密码出错', 127 | // }; 128 | }); 129 | 130 | Mock.mock(new RegExp('/user/info'), function (request) { 131 | const user = localStorage.getItem('MDAdminToken')?.split('-')[0] 132 | 133 | return getUser(user); 134 | }); 135 | 136 | Mock.mock(new RegExp('/tableList'), { 137 | errCode: 0, 138 | errMsg: 'success', 139 | total: 100, 140 | ['list|15']: [ 141 | { 142 | 'id|+1': 1, 143 | 'name|1-10': '★', 144 | 'desc|5-10': '★', 145 | 'count|100-1000': 10, 146 | 'status|1-4': 1, 147 | 'lastTime': () => Mock.mock('@datetime'), 148 | }, 149 | ], 150 | }); 151 | 152 | function getUserName() { 153 | 154 | } 155 | -------------------------------------------------------------------------------- /src/modules/README.md: -------------------------------------------------------------------------------- 1 | ## Modules 2 | 3 | A custom user module system. Place a `.ts` file with the following template, it will be installed automatically. 4 | 5 | ```ts 6 | import { type UserModule } from '~/types' 7 | 8 | export const install: UserModule = ({ app, router }) => { 9 | // do something 10 | } 11 | ``` 12 | -------------------------------------------------------------------------------- /src/modules/nprogress.ts: -------------------------------------------------------------------------------- 1 | import NProgress from 'nprogress' 2 | import { type UserModule } from '@/types' 3 | 4 | export const install: UserModule = ({ router }) => { 5 | router.beforeEach((to, from) => { 6 | if (to.path !== from.path) { 7 | NProgress.start() 8 | } 9 | }) 10 | router.afterEach(() => { 11 | NProgress.done() 12 | }) 13 | } 14 | -------------------------------------------------------------------------------- /src/modules/pinia.ts: -------------------------------------------------------------------------------- 1 | import { createPinia } from 'pinia' 2 | import { type UserModule } from '@/types' 3 | 4 | // Setup Pinia 5 | // https://pinia.esm.dev/ 6 | export const install: UserModule = ({ app }) => { 7 | const pinia = createPinia() 8 | app.use(pinia) 9 | } 10 | -------------------------------------------------------------------------------- /src/modules/user.ts: -------------------------------------------------------------------------------- 1 | import { useUserStore } from '@/stores/user' 2 | import { usePermissionStore } from '@/stores/permission' 3 | import { type UserModule } from '@/types' 4 | 5 | export const loginPath = '/user/login' 6 | 7 | export const install: UserModule = ({ router }) => { 8 | router.beforeEach(async(to, from, next) => { 9 | const userStore = useUserStore() 10 | const permissionStore = usePermissionStore() 11 | 12 | if (!to.meta.layout || to.path === '/') { 13 | // 未获取用户信息 14 | if (!userStore.username) { 15 | if (!userStore.isLogin) { 16 | return next(loginPath) 17 | } 18 | 19 | await userStore.initUser() 20 | } 21 | 22 | // 判断菜单权限 23 | if (to.meta.isMenu && !permissionStore.hasMenu(to.path)) { 24 | return next('/403') 25 | } 26 | 27 | if (to.path === '/') { 28 | let menu = permissionStore.allMenu[0] 29 | while (menu.children) { 30 | menu = menu.children[0] 31 | } 32 | return next(menu.path) 33 | } 34 | } 35 | next() 36 | }) 37 | } 38 | -------------------------------------------------------------------------------- /src/pages/README.md: -------------------------------------------------------------------------------- 1 | ## File-based Routing 2 | 3 | Routes will be auto-generated for Vue files in this dir with the same file structure. 4 | Check out [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) for more details. 5 | 6 | 这个目录的文件会自动生成为路由配置。详情 [`vite-plugin-pages`](https://github.com/hannoeru/vite-plugin-pages) 7 | 8 | ### Path Aliasing 9 | 10 | `@/` is aliased to `./src/` folder. 11 | 12 | `@/` 是 `./src/` 的别名. 13 | 14 | For example, instead of having 15 | 16 | ```ts 17 | import { isDark } from '../../../../composables' 18 | ``` 19 | 20 | now, you can use 21 | 22 | ```ts 23 | import { isDark } from '@/composables' 24 | ``` 25 | -------------------------------------------------------------------------------- /src/pages/[...all].vue: -------------------------------------------------------------------------------- 1 | 3 | 4 | 9 | 10 | 11 | meta: 12 | layout: 404 13 | 14 | -------------------------------------------------------------------------------- /src/pages/dashboard/analysis.vue: -------------------------------------------------------------------------------- 1 | 114 | 115 | 243 | 244 | 249 | 250 | 251 | meta: 252 | name: 分析页 253 | 254 | -------------------------------------------------------------------------------- /src/pages/dashboard/monitor.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 10 | -------------------------------------------------------------------------------- /src/pages/dashboard/workplace.vue: -------------------------------------------------------------------------------- 1 | 20 | 21 | 117 | 118 | 139 | 140 | 141 | meta: 142 | name: 工作台 143 | 144 | -------------------------------------------------------------------------------- /src/pages/exception/403.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 20 | meta: 21 | multiWindow: false 22 | 23 | -------------------------------------------------------------------------------- /src/pages/exception/404.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 20 | meta: 21 | multiWindow: false 22 | 23 | -------------------------------------------------------------------------------- /src/pages/exception/500.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 18 | 19 | 20 | meta: 21 | multiWindow: false 22 | 23 | -------------------------------------------------------------------------------- /src/pages/form/advanced-form.vue: -------------------------------------------------------------------------------- 1 | 101 | 102 | 291 | 292 | 301 | 302 | 303 | meta: 304 | name: 高级表单 305 | 306 | -------------------------------------------------------------------------------- /src/pages/form/basic-form.vue: -------------------------------------------------------------------------------- 1 | 50 | 51 | 110 | 111 | 112 | meta: 113 | name: 基础表单 114 | 115 | -------------------------------------------------------------------------------- /src/pages/form/step-form.vue: -------------------------------------------------------------------------------- 1 | 56 | 57 | 148 | 149 | 150 | meta: 151 | name: 分步表单 152 | 153 | -------------------------------------------------------------------------------- /src/pages/icons/ant-design.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 66 | 67 | 72 | 73 | 74 | meta: 75 | name: Ant Design Icons 76 | 77 | -------------------------------------------------------------------------------- /src/pages/icons/element-plus.vue: -------------------------------------------------------------------------------- 1 | 301 | 302 | 333 | 334 | 339 | 340 | 341 | meta: 342 | name: Element Plus Icons 343 | 344 | -------------------------------------------------------------------------------- /src/pages/list/table-pro-layout.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 114 | 115 | 116 | meta: 117 | name: 自定义布局(TablePro) 118 | 119 | -------------------------------------------------------------------------------- /src/pages/list/table-pro.vue: -------------------------------------------------------------------------------- 1 | 38 | 39 | 107 | 108 | 109 | meta: 110 | name: TablePro 111 | 112 | -------------------------------------------------------------------------------- /src/pages/list/table.vue: -------------------------------------------------------------------------------- 1 | 54 | 55 | 230 | 231 | 232 | meta: 233 | name: 查询列表 234 | 235 | -------------------------------------------------------------------------------- /src/pages/multi-window/index.vue: -------------------------------------------------------------------------------- 1 | 63 | 64 | 103 | 104 | 105 | meta: 106 | name: 多窗口测试 107 | 108 | -------------------------------------------------------------------------------- /src/pages/multi-window/page2.vue: -------------------------------------------------------------------------------- 1 | 6 | 7 | 18 | 19 | 20 | meta: 21 | name: 多窗口测试 22 | 23 | -------------------------------------------------------------------------------- /src/pages/permission/index.vue: -------------------------------------------------------------------------------- 1 | 18 | 19 | 59 | 60 | 61 | meta: 62 | name: 权限演示 63 | 64 | -------------------------------------------------------------------------------- /src/pages/profile/advanced.vue: -------------------------------------------------------------------------------- 1 | 77 | 78 | 278 | 279 | 295 | 296 | 297 | meta: 298 | name: 高级详情页 299 | 300 | -------------------------------------------------------------------------------- /src/pages/profile/basic.vue: -------------------------------------------------------------------------------- 1 | 121 | 122 | 211 | 212 | 219 | 220 | 221 | meta: 222 | name: 基础详情页 223 | 224 | -------------------------------------------------------------------------------- /src/pages/result/fail.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 20 | 21 | 22 | meta: 23 | multiWindow: false 24 | 25 | -------------------------------------------------------------------------------- /src/pages/result/success.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 22 | 23 | 24 | meta: 25 | multiWindow: false 26 | 27 | -------------------------------------------------------------------------------- /src/pages/user/login.vue: -------------------------------------------------------------------------------- 1 | 34 | 35 | 89 | 90 | 98 | 99 | 100 | meta: 101 | layout: empty 102 | 103 | -------------------------------------------------------------------------------- /src/router.ts: -------------------------------------------------------------------------------- 1 | import generatedRoutes from 'virtual:generated-pages' 2 | import { setupLayouts } from 'virtual:generated-layouts' 3 | import { createRouter, createWebHistory } from 'vue-router' 4 | import { setupMultiWindow } from '@vue-plus/multi-window' 5 | 6 | const routes = setupMultiWindow(setupLayouts(generatedRoutes)) 7 | 8 | const router = createRouter({ 9 | history: createWebHistory(), 10 | routes, 11 | }) 12 | 13 | export default router 14 | -------------------------------------------------------------------------------- /src/shims.d.ts: -------------------------------------------------------------------------------- 1 | declare interface Window { 2 | // extend the window 3 | } 4 | 5 | // with vite-plugin-md, markdowns can be treat as Vue components 6 | declare module '*.md' { 7 | import { type DefineComponent } from 'vue' 8 | const component: DefineComponent<{}, {}, any> 9 | export default component 10 | } 11 | 12 | declare module '*.vue' { 13 | import { type DefineComponent } from 'vue' 14 | const component: DefineComponent<{}, {}, any> 15 | export default component 16 | } 17 | -------------------------------------------------------------------------------- /src/stores/layout.ts: -------------------------------------------------------------------------------- 1 | import { acceptHMRUpdate, defineStore } from 'pinia' 2 | import { reactive, watch } from 'vue' 3 | import { breakpoints } from '@/composables' 4 | 5 | export const useLayoutStore = defineStore('layout', () => { 6 | const route = useRoute() 7 | const menu = reactive({ 8 | isCollapse: false, 9 | isDrawer: false, 10 | }) 11 | watch(() => route.fullPath, () => { 12 | if (menu.isDrawer) { 13 | menu.isCollapse = true 14 | } 15 | }) 16 | watch(breakpoints.lg, (lg) => { 17 | menu.isCollapse = !lg 18 | }, { immediate: true }) 19 | watch(breakpoints.sm, (sm) => { 20 | menu.isDrawer = !sm 21 | }, { immediate: true }) 22 | 23 | return { 24 | menu, 25 | } 26 | }) 27 | 28 | if (import.meta.hot) { 29 | import.meta.hot.accept(acceptHMRUpdate(useLayoutStore, import.meta.hot)) 30 | } 31 | -------------------------------------------------------------------------------- /src/stores/permission.ts: -------------------------------------------------------------------------------- 1 | import { acceptHMRUpdate, defineStore } from 'pinia' 2 | import { shallowReactive, toRefs } from 'vue' 3 | 4 | export interface PermissionState { 5 | menuList: Menu[] // 菜单树 6 | allMenu: Menu[] 7 | identifyList: string[] 8 | } 9 | 10 | export interface Menu { 11 | name: string 12 | path: string 13 | icon?: string 14 | children?: Menu[] 15 | parent?: Menu | null 16 | } 17 | 18 | export const usePermissionStore = defineStore('permission', () => { 19 | const state = shallowReactive({ 20 | menuList: [], // 菜单 21 | allMenu: [], 22 | identifyList: [], // 权限标识 23 | }) 24 | 25 | const setMenuList = (menuList: Menu[]) => { 26 | menuList = JSON.parse(JSON.stringify(menuList)) 27 | const allMenu: Menu[] = [] 28 | 29 | const recursive = (list: Menu[], parent: Menu | null = null) => list.forEach((menu) => { 30 | menu.parent = parent 31 | allMenu.push(menu) 32 | 33 | if (menu.children) { 34 | recursive(menu.children, menu) 35 | } 36 | }) 37 | recursive(menuList) 38 | 39 | state.allMenu = allMenu 40 | state.menuList = menuList 41 | } 42 | 43 | const setIdentifyList = (identifyList: string[]) => { 44 | state.identifyList = identifyList 45 | } 46 | 47 | // 判断菜单 48 | const hasMenu = (path: string) => state.allMenu.findIndex(menu => menu.path === path) > -1 49 | 50 | // 判断权限标识 51 | const hasIdentify = (identify: string) => state.identifyList[0] === '*' || state.identifyList.includes(identify) 52 | 53 | // 判断权限标识 or 菜单 54 | const hasPermission = (permission: string | string[]) => 55 | (typeof permission === 'string' ? [permission] : permission).every(value => hasIdentify(value) || hasMenu(value)) 56 | 57 | return { 58 | ...toRefs(state), 59 | 60 | setMenuList, 61 | hasMenu, 62 | setIdentifyList, 63 | hasIdentify, 64 | hasPermission, 65 | } 66 | }) 67 | 68 | if (import.meta.hot) { 69 | import.meta.hot.accept(acceptHMRUpdate(usePermissionStore, import.meta.hot)) 70 | } 71 | -------------------------------------------------------------------------------- /src/stores/user.ts: -------------------------------------------------------------------------------- 1 | import { acceptHMRUpdate, defineStore } from 'pinia' 2 | import { computed, reactive, toRefs } from 'vue' 3 | import { usePermissionStore } from './permission' 4 | import { type UserInfoResponse, getUserInfo } from '@/api/user' 5 | 6 | export interface UserState { 7 | token: string 8 | username: string 9 | nickname: string 10 | avatar: string 11 | } 12 | 13 | export const useUserStore = defineStore('user', () => { 14 | const { setIdentifyList, setMenuList } = usePermissionStore() 15 | 16 | const state = reactive({ 17 | token: localStorage.getItem('MDAdminToken') || '', 18 | username: '', 19 | nickname: '', 20 | avatar: '', 21 | }) 22 | 23 | const isLogin = computed(() => { 24 | return state.token 25 | }) 26 | 27 | function setUserInfo(payload: UserInfoResponse) { 28 | const { permission, ...user } = payload 29 | if (user.token !== state.token) { 30 | localStorage.setItem('MDAdminToken', user.token) 31 | } 32 | 33 | Object.assign(state, user) 34 | 35 | setIdentifyList(permission.identifyList) 36 | setMenuList(permission.menuList) 37 | } 38 | 39 | function initUser() { 40 | return getUserInfo() 41 | .then(setUserInfo) 42 | } 43 | 44 | function logout() { 45 | localStorage.removeItem('MDAdminToken') 46 | } 47 | 48 | return { 49 | ...toRefs(state), 50 | 51 | isLogin, 52 | setUserInfo, 53 | 54 | initUser, 55 | logout, 56 | } 57 | }) 58 | 59 | if (import.meta.hot) { 60 | import.meta.hot.accept(acceptHMRUpdate(useUserStore, import.meta.hot)) 61 | } 62 | -------------------------------------------------------------------------------- /src/styles/main.css: -------------------------------------------------------------------------------- 1 | *, 2 | ::before, 3 | ::after { 4 | border-width: 0; 5 | border-style: solid; 6 | border-color: var(--el-border-color); 7 | } 8 | 9 | html { 10 | background-color: #fff; 11 | } 12 | 13 | a { 14 | color: inherit; 15 | text-decoration: inherit; 16 | } 17 | 18 | .link { 19 | color: var(--el-color-primary); 20 | } 21 | 22 | a:active, a:hover { 23 | text-decoration: none; 24 | outline: 0; 25 | } 26 | 27 | html, 28 | body, 29 | #app { 30 | height: 100%; 31 | margin: 0; 32 | padding: 0; 33 | } 34 | 35 | #app { 36 | font-size: 14px; 37 | } 38 | 39 | html.dark { 40 | background: #121212; 41 | } 42 | 43 | #nprogress { 44 | pointer-events: none; 45 | } 46 | 47 | #nprogress .bar { 48 | background: rgb(13,148,136); 49 | opacity: 0.75; 50 | position: fixed; 51 | z-index: 1031; 52 | top: 0; 53 | left: 0; 54 | width: 100%; 55 | height: 2px; 56 | } 57 | 58 | flex { 59 | display: flex; 60 | } 61 | 62 | .color-p { 63 | color: var(--el-text-color-primary); 64 | } 65 | 66 | .color-r { 67 | color: var(--el-text-color-regular); 68 | } 69 | 70 | .color-s { 71 | color: var(--el-text-color-secondary); 72 | } 73 | 74 | .el-empty .el-empty__image{ 75 | max-width: 80vw; 76 | } 77 | 78 | .el-tabs.tabs-content-none { 79 | 80 | } 81 | 82 | .el-tabs.tabs-content-none .el-tabs__header { 83 | margin: 0; 84 | } 85 | 86 | .el-tabs.tabs-content-none .el-tabs__content { 87 | display: none; 88 | } 89 | 90 | .el-tabs.tabs-content-none .el-tabs__nav-wrap::after { 91 | display: none; 92 | } -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | import type { App } from 'vue' 2 | import type { Router } from 'vue-router' 3 | 4 | export type UserModule = (ctx: { app: App; router: Router }) => void 5 | -------------------------------------------------------------------------------- /src/utils/index.ts: -------------------------------------------------------------------------------- 1 | import numeral from 'numeral' 2 | import { type TableColumnCtx } from 'element-plus/es/components/table/src/table-column/defaults' 3 | 4 | export interface Dict { 5 | value: number | string 6 | label: string 7 | } 8 | export function dict2Map(dictList: T[]): Record { 9 | const map: Record = {} 10 | dictList.forEach((dict) => { 11 | map[dict.value] = dict 12 | }) 13 | 14 | return map 15 | } 16 | 17 | export function formatNumber(value: string | number, format = '0,0'): string { 18 | return numeral(value).format(format) 19 | } 20 | 21 | export function formatAmount(value: string | number, format = '0,0.00'): string { 22 | return formatNumber(value, format) 23 | } 24 | 25 | export function createTableSummaryMethod>(sumColumn: string[], amountColumn: string[] = []) { 26 | return ({ columns, data }: { 27 | columns: TableColumnCtx[] 28 | data: T[] 29 | }) => { 30 | return columns.map((column, index) => { 31 | if (index === 0) { 32 | return '合计' 33 | } 34 | 35 | if (sumColumn.includes(column.property)) { 36 | const values = data.map(item => item[column.property]) 37 | 38 | const sum = values.reduce((prev, curr) => { 39 | const value = Number(curr) 40 | if (!isNaN(value)) { 41 | return prev + curr 42 | } else { 43 | return prev 44 | } 45 | }, 0) 46 | 47 | return amountColumn.includes(column.property) ? formatAmount(sum) : formatNumber(sum) 48 | } else { 49 | return '' 50 | } 51 | }) 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/utils/request.ts: -------------------------------------------------------------------------------- 1 | import axios, { type AxiosRequestConfig, type AxiosResponse } from 'axios' 2 | 3 | import { ElMessage } from 'element-plus' 4 | import omit from 'omit' 5 | import { useUserStore } from '@/stores/user' 6 | 7 | export interface BaseResponseResult { 8 | errCode: string | number 9 | errMsg: string 10 | } 11 | 12 | const service = axios.create({ 13 | baseURL: 'http://yapi.smart-xwork.cn/mock/100200/api/', 14 | timeout: 10000, 15 | }) 16 | 17 | // 请求拦截器 18 | service.interceptors.request.use((config) => { 19 | if (!config.headers) { 20 | config.headers = {} 21 | } 22 | config.headers['Content-Type'] = 'application/json;charset=utf-8' 23 | 24 | const store = useUserStore() 25 | if (store.token) { 26 | // config.url += `${config.url?.indexOf('?') === -1 ? '?' : '&'}token=${store.token}` 27 | config.headers.token = store.token 28 | } 29 | 30 | console.log('request', config.method, config.url, config.data) 31 | 32 | return config 33 | }, (error) => { 34 | console.log(error) 35 | Promise.reject(error) 36 | }) 37 | 38 | // 响应拦截器 39 | service.interceptors.response.use((res: AxiosResponse) => { 40 | if (res.data.errCode === 0) { 41 | return omit(['errCode', 'errMsg'], res.data) 42 | } else { 43 | ElMessage({ 44 | message: res.data.errMsg, 45 | type: 'error', 46 | duration: 5 * 1000, 47 | }) 48 | return Promise.reject(res.data) 49 | } 50 | }, 51 | (error) => { 52 | console.error(error) 53 | let { message } = error 54 | if (message === 'Network Error') { 55 | message = '后端接口连接异常' 56 | } else if (message.includes('timeout')) { 57 | message = '系统接口请求超时' 58 | } else if (message.includes('Request failed with status code')) { 59 | message = `系统接口${message.substr(message.length - 3)}异常` 60 | } 61 | 62 | ElMessage({ 63 | message, 64 | type: 'error', 65 | duration: 5 * 1000, 66 | }) 67 | return error 68 | }, 69 | ) 70 | 71 | export default service 72 | 73 | export function get(url: string, params?: P, config?: AxiosRequestConfig): Promise { 74 | if (params) { 75 | url += '?' 76 | for (const key in params) { 77 | const value = params[key] 78 | if (typeof value !== 'object' && value !== null && value !== undefined) { 79 | url += `${encodeURIComponent(key)}=${typeof value === 'string' ? encodeURIComponent(value) : value}&` 80 | } 81 | } 82 | url = url.substring(0, url.length - 1) 83 | } 84 | return service.get(url, config) 85 | } 86 | 87 | export const post = service.post.bind(service) 88 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "baseUrl": ".", 4 | "module": "ESNext", 5 | "target": "es2016", 6 | "lib": ["DOM", "ESNext"], 7 | "strict": true, 8 | "jsx": "preserve", 9 | "esModuleInterop": true, 10 | "incremental": false, 11 | "skipLibCheck": true, 12 | "moduleResolution": "node", 13 | "resolveJsonModule": true, 14 | "noUnusedLocals": true, 15 | "strictNullChecks": true, 16 | "forceConsistentCasingInFileNames": true, 17 | "types": [ 18 | "vite/client", 19 | "vue/ref-macros", 20 | "vite-plugin-pages/client", 21 | "vite-plugin-vue-layouts/client", 22 | "element-plus/global" 23 | ], 24 | "paths": { 25 | "@/*": ["src/*"], 26 | } 27 | }, 28 | "exclude": ["dist", "node_modules", "cypress", "mocks"] 29 | } 30 | -------------------------------------------------------------------------------- /unocss.config.ts: -------------------------------------------------------------------------------- 1 | import { 2 | defineConfig, 3 | presetAttributify, 4 | presetIcons, 5 | presetTypography, 6 | // presetUno, 7 | presetWebFonts, 8 | presetWind, 9 | transformerDirectives, 10 | transformerVariantGroup, 11 | } from 'unocss' 12 | 13 | import epIcons from '@iconify-json/ep' 14 | 15 | const iconPrefix = 'i-' 16 | 17 | export default defineConfig({ 18 | shortcuts: [ 19 | 20 | ], 21 | variants: [ 22 | { 23 | match: (s) => { 24 | if (s.startsWith(iconPrefix)) { 25 | return { 26 | matcher: s, 27 | selector: (s) => { 28 | return s.startsWith('.') ? `${s.slice(1)},${s}` : s 29 | }, 30 | } 31 | } 32 | }, 33 | }, 34 | ], 35 | presets: [ 36 | // presetUno(), 37 | presetWind(), 38 | presetAttributify(), 39 | presetIcons({ 40 | scale: 1.2, 41 | warn: true, 42 | prefix: iconPrefix, 43 | }), 44 | presetTypography(), 45 | presetWebFonts({ 46 | fonts: { 47 | sans: 'DM Sans', 48 | serif: 'DM Serif Display', 49 | mono: 'DM Mono', 50 | }, 51 | }), 52 | ], 53 | transformers: [ 54 | transformerDirectives(), 55 | transformerVariantGroup(), 56 | ], 57 | safelist: [ 58 | ...'prose prose-sm m-auto text-left'.split(' '), 59 | ...Object.keys(epIcons.icons.icons).map(name => `${iconPrefix}${epIcons.icons.prefix}-${name}`), 60 | ], 61 | theme: { 62 | colors: { 63 | primary: 'var(--el-color-primary)', 64 | }, 65 | }, 66 | }) 67 | -------------------------------------------------------------------------------- /vite.config.ts: -------------------------------------------------------------------------------- 1 | import path from 'path' 2 | import { defineConfig } from 'vite' 3 | import Vue from '@vitejs/plugin-vue' 4 | import vueJsx from '@vitejs/plugin-vue-jsx' 5 | import Pages from 'vite-plugin-pages' 6 | import Layouts from 'vite-plugin-vue-layouts' 7 | import Components from 'unplugin-vue-components/vite' 8 | import { ElementPlusResolver } from 'unplugin-vue-components/resolvers' 9 | import AutoImport from 'unplugin-auto-import/vite' 10 | // import { VitePWA } from 'vite-plugin-pwa' 11 | import Inspect from 'vite-plugin-inspect' 12 | import Unocss from 'unocss/vite' 13 | 14 | export default defineConfig({ 15 | resolve: { 16 | alias: { 17 | '@/': `${path.resolve(__dirname, 'src')}/`, 18 | }, 19 | }, 20 | 21 | plugins: [ 22 | Vue({ 23 | include: [/\.vue$/], 24 | reactivityTransform: true, 25 | template: { 26 | compilerOptions: { 27 | isCustomElement: tag => ['flex'].includes(tag), 28 | }, 29 | }, 30 | }), 31 | 32 | vueJsx(), 33 | 34 | // https://github.com/hannoeru/vite-plugin-pages 35 | Pages({ 36 | extensions: ['vue'], 37 | }), 38 | 39 | // https://github.com/JohnCampionJr/vite-plugin-vue-layouts 40 | Layouts(), 41 | 42 | // https://github.com/antfu/unplugin-auto-import 43 | AutoImport({ 44 | resolvers: [ElementPlusResolver()], 45 | imports: [ 46 | 'vue', 47 | 'vue-router', 48 | 'vue/macros', 49 | '@vueuse/core', 50 | ], 51 | dts: 'src/auto-imports.d.ts', 52 | }), 53 | 54 | // https://github.com/antfu/unplugin-vue-components 55 | Components({ 56 | // 生产环境按需导入 57 | resolvers: process.env.NODE_ENV === 'production' 58 | ? ElementPlusResolver() 59 | : undefined, 60 | // allow auto load markdown components under `./src/components/` 61 | extensions: ['vue'], 62 | // allow auto import and register components used in markdown 63 | include: [/\.vue$/, /\.vue\?vue/], 64 | dts: 'src/components.d.ts', 65 | }), 66 | 67 | // 开发环境完整引入element-plus 68 | { 69 | name: 'dev-auto-import-element-plus', 70 | transform(code, id) { 71 | if (process.env.NODE_ENV !== 'production' && /src\/main.ts$/.test(id)) { 72 | return { 73 | code: `${code};import ElementPlus from 'element-plus';import 'element-plus/dist/index.css';app.use(ElementPlus);`, 74 | map: null, 75 | } 76 | } 77 | }, 78 | }, 79 | 80 | // https://github.com/antfu/unocss 81 | // see unocss.config.ts for config 82 | Unocss(), 83 | 84 | // https://github.com/antfu/vite-plugin-inspect 85 | // Visit http://localhost:3333/__inspect/ to see the inspector 86 | Inspect(), 87 | ], 88 | 89 | build: { 90 | // cssCodeSplit: false, 91 | rollupOptions: { 92 | output: { 93 | manualChunks(id) { 94 | if (id.includes('element-plus/es')) { 95 | return 'element-plus' 96 | } 97 | }, 98 | }, 99 | }, 100 | }, 101 | }) 102 | --------------------------------------------------------------------------------