lrf 1 year ago
commit
4070952d91
51 changed files with 4785 additions and 0 deletions
  1. 11 0
      .env.development
  2. 9 0
      .env.production
  3. 286 0
      .eslintrc-auto-import.json
  4. 28 0
      .eslintrc.cjs
  5. 30 0
      .gitignore
  6. 8 0
      .prettierrc.json
  7. 8 0
      .vscode/extensions.json
  8. 35 0
      README.md
  9. 13 0
      index.html
  10. 10 0
      jsconfig.json
  11. 37 0
      package.json
  12. 2380 0
      pnpm-lock.yaml
  13. BIN
      public/favicon.ico
  14. 21 0
      src/App.vue
  15. BIN
      src/assets/images/401.gif
  16. BIN
      src/assets/images/404.png
  17. BIN
      src/assets/images/404_cloud.png
  18. 1 0
      src/assets/logo.svg
  19. 44 0
      src/components/HelloWorld.vue
  20. 88 0
      src/components/TheWelcome.vue
  21. 86 0
      src/components/WelcomeItem.vue
  22. 7 0
      src/components/icons/IconCommunity.vue
  23. 7 0
      src/components/icons/IconDocumentation.vue
  24. 7 0
      src/components/icons/IconEcosystem.vue
  25. 7 0
      src/components/icons/IconSupport.vue
  26. 19 0
      src/components/icons/IconTooling.vue
  27. 22 0
      src/lang/index.js
  28. 40 0
      src/lang/package/en.js
  29. 40 0
      src/lang/package/zh-cn.js
  30. 59 0
      src/layout/index.vue
  31. 76 0
      src/layout/parts/Header.vue
  32. 113 0
      src/layout/parts/Sidebar.vue
  33. 22 0
      src/layout/parts/breadcrumb.vue
  34. 169 0
      src/layout/site.js
  35. 17 0
      src/main.js
  36. 28 0
      src/router/index.js
  37. 16 0
      src/settings.js
  38. 25 0
      src/store/api/login.js
  39. 15 0
      src/store/index.js
  40. 89 0
      src/store/modules/app.js
  41. 6 0
      src/store/user.js
  42. 128 0
      src/utils/axios-wrapper.js
  43. 50 0
      src/utils/error-code.js
  44. 12 0
      src/utils/i18n.js
  45. 35 0
      src/utils/util-methods.js
  46. 105 0
      src/views/error-page/401.vue
  47. 256 0
      src/views/error-page/404.vue
  48. 9 0
      src/views/home/index.vue
  49. 49 0
      src/views/login/index.js
  50. 109 0
      src/views/login/index.vue
  51. 153 0
      vite.config.js

+ 11 - 0
.env.development

@@ -0,0 +1,11 @@
+## 开发环境
+NODE_ENV='development'
+
+# 应用端口
+VITE_APP_PORT = 3000
+
+# 代理前缀
+VITE_APP_BASE_API = '/dev-api'
+
+# 代理地址
+VITE_APP_PROXY_TARGET = 'http://localhost:8989'

+ 9 - 0
.env.production

@@ -0,0 +1,9 @@
+## 生产环境
+NODE_ENV='production'
+
+# 应用端口
+VITE_APP_PORT = 3000
+
+# 代理前缀
+VITE_APP_BASE_API = '/prod-api'
+

+ 286 - 0
.eslintrc-auto-import.json

@@ -0,0 +1,286 @@
+{
+  "globals": {
+    "Component": true,
+    "ComponentPublicInstance": true,
+    "ComputedRef": true,
+    "EffectScope": true,
+    "ElMessage": true,
+    "ElMessageBox": true,
+    "ElNotification": true,
+    "InjectionKey": true,
+    "PropType": true,
+    "Ref": true,
+    "VNode": true,
+    "asyncComputed": true,
+    "autoResetRef": true,
+    "computed": true,
+    "computedAsync": true,
+    "computedEager": true,
+    "computedInject": true,
+    "computedWithControl": true,
+    "controlledComputed": true,
+    "controlledRef": true,
+    "createApp": true,
+    "createEventHook": true,
+    "createGlobalState": true,
+    "createInjectionState": true,
+    "createReactiveFn": true,
+    "createReusableTemplate": true,
+    "createSharedComposable": true,
+    "createTemplatePromise": true,
+    "createUnrefFn": true,
+    "customRef": true,
+    "debouncedRef": true,
+    "debouncedWatch": true,
+    "defineAsyncComponent": true,
+    "defineComponent": true,
+    "eagerComputed": true,
+    "effectScope": true,
+    "extendRef": true,
+    "getCurrentInstance": true,
+    "getCurrentScope": true,
+    "h": true,
+    "ignorableWatch": true,
+    "inject": true,
+    "isDefined": true,
+    "isProxy": true,
+    "isReactive": true,
+    "isReadonly": true,
+    "isRef": true,
+    "makeDestructurable": true,
+    "markRaw": true,
+    "nextTick": true,
+    "onActivated": true,
+    "onBeforeMount": true,
+    "onBeforeUnmount": true,
+    "onBeforeUpdate": true,
+    "onClickOutside": true,
+    "onDeactivated": true,
+    "onErrorCaptured": true,
+    "onKeyStroke": true,
+    "onLongPress": true,
+    "onMounted": true,
+    "onRenderTracked": true,
+    "onRenderTriggered": true,
+    "onScopeDispose": true,
+    "onServerPrefetch": true,
+    "onStartTyping": true,
+    "onUnmounted": true,
+    "onUpdated": true,
+    "pausableWatch": true,
+    "provide": true,
+    "reactify": true,
+    "reactifyObject": true,
+    "reactive": true,
+    "reactiveComputed": true,
+    "reactiveOmit": true,
+    "reactivePick": true,
+    "readonly": true,
+    "ref": true,
+    "refAutoReset": true,
+    "refDebounced": true,
+    "refDefault": true,
+    "refThrottled": true,
+    "refWithControl": true,
+    "resolveComponent": true,
+    "resolveRef": true,
+    "resolveUnref": true,
+    "shallowReactive": true,
+    "shallowReadonly": true,
+    "shallowRef": true,
+    "syncRef": true,
+    "syncRefs": true,
+    "templateRef": true,
+    "throttledRef": true,
+    "throttledWatch": true,
+    "toRaw": true,
+    "toReactive": true,
+    "toRef": true,
+    "toRefs": true,
+    "toValue": true,
+    "triggerRef": true,
+    "tryOnBeforeMount": true,
+    "tryOnBeforeUnmount": true,
+    "tryOnMounted": true,
+    "tryOnScopeDispose": true,
+    "tryOnUnmounted": true,
+    "unref": true,
+    "unrefElement": true,
+    "until": true,
+    "useActiveElement": true,
+    "useAnimate": true,
+    "useArrayDifference": true,
+    "useArrayEvery": true,
+    "useArrayFilter": true,
+    "useArrayFind": true,
+    "useArrayFindIndex": true,
+    "useArrayFindLast": true,
+    "useArrayIncludes": true,
+    "useArrayJoin": true,
+    "useArrayMap": true,
+    "useArrayReduce": true,
+    "useArraySome": true,
+    "useArrayUnique": true,
+    "useAsyncQueue": true,
+    "useAsyncState": true,
+    "useAttrs": true,
+    "useBase64": true,
+    "useBattery": true,
+    "useBluetooth": true,
+    "useBreakpoints": true,
+    "useBroadcastChannel": true,
+    "useBrowserLocation": true,
+    "useCached": true,
+    "useClipboard": true,
+    "useCloned": true,
+    "useColorMode": true,
+    "useConfirmDialog": true,
+    "useCounter": true,
+    "useCssModule": true,
+    "useCssVar": true,
+    "useCssVars": true,
+    "useCurrentElement": true,
+    "useCycleList": true,
+    "useDark": true,
+    "useDateFormat": true,
+    "useDebounce": true,
+    "useDebounceFn": true,
+    "useDebouncedRefHistory": true,
+    "useDeviceMotion": true,
+    "useDeviceOrientation": true,
+    "useDevicePixelRatio": true,
+    "useDevicesList": true,
+    "useDisplayMedia": true,
+    "useDocumentVisibility": true,
+    "useDraggable": true,
+    "useDropZone": true,
+    "useElementBounding": true,
+    "useElementByPoint": true,
+    "useElementHover": true,
+    "useElementSize": true,
+    "useElementVisibility": true,
+    "useEventBus": true,
+    "useEventListener": true,
+    "useEventSource": true,
+    "useEyeDropper": true,
+    "useFavicon": true,
+    "useFetch": true,
+    "useFileDialog": true,
+    "useFileSystemAccess": true,
+    "useFocus": true,
+    "useFocusWithin": true,
+    "useFps": true,
+    "useFullscreen": true,
+    "useGamepad": true,
+    "useGeolocation": true,
+    "useIdle": true,
+    "useImage": true,
+    "useInfiniteScroll": true,
+    "useIntersectionObserver": true,
+    "useInterval": true,
+    "useIntervalFn": true,
+    "useKeyModifier": true,
+    "useLastChanged": true,
+    "useLocalStorage": true,
+    "useMagicKeys": true,
+    "useManualRefHistory": true,
+    "useMediaControls": true,
+    "useMediaQuery": true,
+    "useMemoize": true,
+    "useMemory": true,
+    "useMounted": true,
+    "useMouse": true,
+    "useMouseInElement": true,
+    "useMousePressed": true,
+    "useMutationObserver": true,
+    "useNavigatorLanguage": true,
+    "useNetwork": true,
+    "useNow": true,
+    "useObjectUrl": true,
+    "useOffsetPagination": true,
+    "useOnline": true,
+    "usePageLeave": true,
+    "useParallax": true,
+    "useParentElement": true,
+    "usePerformanceObserver": true,
+    "usePermission": true,
+    "usePointer": true,
+    "usePointerLock": true,
+    "usePointerSwipe": true,
+    "usePreferredColorScheme": true,
+    "usePreferredContrast": true,
+    "usePreferredDark": true,
+    "usePreferredLanguages": true,
+    "usePreferredReducedMotion": true,
+    "usePrevious": true,
+    "useRafFn": true,
+    "useRefHistory": true,
+    "useResizeObserver": true,
+    "useScreenOrientation": true,
+    "useScreenSafeArea": true,
+    "useScriptTag": true,
+    "useScroll": true,
+    "useScrollLock": true,
+    "useSessionStorage": true,
+    "useShare": true,
+    "useSlots": true,
+    "useSorted": true,
+    "useSpeechRecognition": true,
+    "useSpeechSynthesis": true,
+    "useStepper": true,
+    "useStorage": true,
+    "useStorageAsync": true,
+    "useStyleTag": true,
+    "useSupported": true,
+    "useSwipe": true,
+    "useTemplateRefsList": true,
+    "useTextDirection": true,
+    "useTextSelection": true,
+    "useTextareaAutosize": true,
+    "useThrottle": true,
+    "useThrottleFn": true,
+    "useThrottledRefHistory": true,
+    "useTimeAgo": true,
+    "useTimeout": true,
+    "useTimeoutFn": true,
+    "useTimeoutPoll": true,
+    "useTimestamp": true,
+    "useTitle": true,
+    "useToNumber": true,
+    "useToString": true,
+    "useToggle": true,
+    "useTransition": true,
+    "useUrlSearchParams": true,
+    "useUserMedia": true,
+    "useVModel": true,
+    "useVModels": true,
+    "useVibrate": true,
+    "useVirtualList": true,
+    "useWakeLock": true,
+    "useWebNotification": true,
+    "useWebSocket": true,
+    "useWebWorker": true,
+    "useWebWorkerFn": true,
+    "useWindowFocus": true,
+    "useWindowScroll": true,
+    "useWindowSize": true,
+    "watch": true,
+    "watchArray": true,
+    "watchAtMost": true,
+    "watchDebounced": true,
+    "watchDeep": true,
+    "watchEffect": true,
+    "watchIgnorable": true,
+    "watchImmediate": true,
+    "watchOnce": true,
+    "watchPausable": true,
+    "watchPostEffect": true,
+    "watchSyncEffect": true,
+    "watchThrottled": true,
+    "watchTriggerable": true,
+    "watchWithFilter": true,
+    "whenever": true,
+    "defineStore": true,
+    "useRoute": true
+  }
+}

+ 28 - 0
.eslintrc.cjs

@@ -0,0 +1,28 @@
+/* eslint-env node */
+require('@rushstack/eslint-patch/modern-module-resolution')
+
+module.exports = {
+  root: true,
+  extends: ['plugin:vue/vue3-essential', 'eslint:recommended', '@vue/eslint-config-prettier/skip-formatting', './.eslintrc-auto-import.json'],
+  parserOptions: {
+    ecmaVersion: 'latest'
+  },
+  rules: {
+    'vue/multi-word-component-names': 0,
+    'max-len': [
+      'warn',
+      {
+        code: 150
+      }
+    ],
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 150
+      }
+    ]
+  }
+}

+ 30 - 0
.gitignore

@@ -0,0 +1,30 @@
+# Logs
+logs
+*.log
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+pnpm-debug.log*
+lerna-debug.log*
+
+node_modules
+.DS_Store
+dist
+dist-ssr
+coverage
+*.local
+
+/cypress/videos/
+/cypress/screenshots/
+
+# Editor directories and files
+.vscode/*
+!.vscode/extensions.json
+.idea
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.sw?
+
+*.tsbuildinfo

+ 8 - 0
.prettierrc.json

@@ -0,0 +1,8 @@
+{
+  "$schema": "https://json.schemastore.org/prettierrc",
+  "semi": false,
+  "tabWidth": 2,
+  "singleQuote": true,
+  "printWidth": 100,
+  "trailingComma": "none"
+}

+ 8 - 0
.vscode/extensions.json

@@ -0,0 +1,8 @@
+{
+  "recommendations": [
+    "Vue.volar",
+    "Vue.vscode-typescript-vue-plugin",
+    "dbaeumer.vscode-eslint",
+    "esbenp.prettier-vscode"
+  ]
+}

+ 35 - 0
README.md

@@ -0,0 +1,35 @@
+# web-template-vue3-js
+
+This template should help get you started developing with Vue 3 in Vite.
+
+## Recommended IDE Setup
+
+[VSCode](https://code.visualstudio.com/) + [Volar](https://marketplace.visualstudio.com/items?itemName=Vue.volar) (and disable Vetur) + [TypeScript Vue Plugin (Volar)](https://marketplace.visualstudio.com/items?itemName=Vue.vscode-typescript-vue-plugin).
+
+## Customize configuration
+
+See [Vite Configuration Reference](https://vitejs.dev/config/).
+
+## Project Setup
+
+```sh
+pnpm install
+```
+
+### Compile and Hot-Reload for Development
+
+```sh
+pnpm dev
+```
+
+### Compile and Minify for Production
+
+```sh
+pnpm build
+```
+
+### Lint with [ESLint](https://eslint.org/)
+
+```sh
+pnpm lint
+```

+ 13 - 0
index.html

@@ -0,0 +1,13 @@
+<!DOCTYPE html>
+<html lang="en">
+  <head>
+    <meta charset="UTF-8">
+    <link rel="icon" href="/favicon.ico">
+    <meta name="viewport" content="width=device-width, initial-scale=1.0">
+    <title>Vite App</title>
+  </head>
+  <body>
+    <div id="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>

+ 10 - 0
jsconfig.json

@@ -0,0 +1,10 @@
+{
+  "compilerOptions": {
+    "baseUrl": "./",
+    "paths": {
+      "@/*": ["./src/*"]
+    }
+  },
+  "exclude": ["node_modules", "dist"],
+  "include": ["src/**/*.vue","**/*.js"]
+}

+ 37 - 0
package.json

@@ -0,0 +1,37 @@
+{
+  "name": "web-template-vue3-js",
+  "version": "0.0.0",
+  "private": true,
+  "type": "module",
+  "scripts": {
+    "dev": "vite --mode development",
+    "build": "vite build --mode production",
+    "preview": "vite preview",
+    "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs --fix --ignore-path .gitignore",
+    "format": "prettier --write src/"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@vueuse/core": "^10.7.2",
+    "element-plus": "^2.5.6",
+    "lodash-es": "^4.17.21",
+    "pinia": "^2.1.7",
+    "vue": "^3.4.15",
+    "vue-i18n": "^9.9.1",
+    "vue-router": "^4.2.5"
+  },
+  "devDependencies": {
+    "@rushstack/eslint-patch": "^1.3.3",
+    "@vitejs/plugin-vue": "^5.0.3",
+    "@vue/eslint-config-prettier": "^8.0.0",
+    "eslint": "^8.49.0",
+    "eslint-plugin-vue": "^9.17.0",
+    "prettier": "^3.0.3",
+    "sass": "^1.71.0",
+    "unplugin-auto-import": "^0.17.5",
+    "unplugin-icons": "^0.18.5",
+    "unplugin-vue-components": "^0.26.0",
+    "vite": "^5.0.11",
+    "vite-plugin-inspect": "^0.8.3"
+  }
+}

File diff suppressed because it is too large
+ 2380 - 0
pnpm-lock.yaml


BIN
public/favicon.ico


+ 21 - 0
src/App.vue

@@ -0,0 +1,21 @@
+<script setup>
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+</script>
+
+<template>
+  <el-config-provider :locale="appStore.locale" :size="appStore.size">
+    <router-view />
+  </el-config-provider>
+</template>
+
+<style lang="scss">
+body {
+  margin: 0;
+}
+.w_1200 {
+  width: 1200px;
+  margin: 0 auto;
+}
+</style>

BIN
src/assets/images/401.gif


BIN
src/assets/images/404.png


BIN
src/assets/images/404_cloud.png


+ 1 - 0
src/assets/logo.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 261.76 226.69"><path d="M161.096.001l-30.225 52.351L100.647.001H-.005l130.877 226.688L261.749.001z" fill="#41b883"/><path d="M161.096.001l-30.225 52.351L100.647.001H52.346l78.526 136.01L209.398.001z" fill="#34495e"/></svg>

+ 44 - 0
src/components/HelloWorld.vue

@@ -0,0 +1,44 @@
+<script setup>
+defineProps({
+  msg: {
+    type: String,
+    required: true
+  }
+})
+</script>
+
+<template>
+  <div class="greetings">
+    <h1 class="green">{{ msg }}</h1>
+    <h3>
+      You’ve successfully created a project with
+      <a href="https://vitejs.dev/" target="_blank" rel="noopener">Vite</a> +
+      <a href="https://vuejs.org/" target="_blank" rel="noopener">Vue 3</a>.
+    </h3>
+  </div>
+</template>
+
+<style scoped>
+h1 {
+  font-weight: 500;
+  font-size: 2.6rem;
+  position: relative;
+  top: -10px;
+}
+
+h3 {
+  font-size: 1.2rem;
+}
+
+.greetings h1,
+.greetings h3 {
+  text-align: center;
+}
+
+@media (min-width: 1024px) {
+  .greetings h1,
+  .greetings h3 {
+    text-align: left;
+  }
+}
+</style>

+ 88 - 0
src/components/TheWelcome.vue

@@ -0,0 +1,88 @@
+<script setup>
+import WelcomeItem from './WelcomeItem.vue'
+import DocumentationIcon from './icons/IconDocumentation.vue'
+import ToolingIcon from './icons/IconTooling.vue'
+import EcosystemIcon from './icons/IconEcosystem.vue'
+import CommunityIcon from './icons/IconCommunity.vue'
+import SupportIcon from './icons/IconSupport.vue'
+</script>
+
+<template>
+  <WelcomeItem>
+    <template #icon>
+      <DocumentationIcon />
+    </template>
+    <template #heading>Documentation</template>
+
+    Vue’s
+    <a href="https://vuejs.org/" target="_blank" rel="noopener">official documentation</a>
+    provides you with all information you need to get started.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <ToolingIcon />
+    </template>
+    <template #heading>Tooling</template>
+
+    This project is served and bundled with
+    <a href="https://vitejs.dev/guide/features.html" target="_blank" rel="noopener">Vite</a>. The
+    recommended IDE setup is
+    <a href="https://code.visualstudio.com/" target="_blank" rel="noopener">VSCode</a> +
+    <a href="https://github.com/johnsoncodehk/volar" target="_blank" rel="noopener">Volar</a>. If
+    you need to test your components and web pages, check out
+    <a href="https://www.cypress.io/" target="_blank" rel="noopener">Cypress</a> and
+    <a href="https://on.cypress.io/component" target="_blank" rel="noopener"
+      >Cypress Component Testing</a
+    >.
+
+    <br />
+
+    More instructions are available in <code>README.md</code>.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <EcosystemIcon />
+    </template>
+    <template #heading>Ecosystem</template>
+
+    Get official tools and libraries for your project:
+    <a href="https://pinia.vuejs.org/" target="_blank" rel="noopener">Pinia</a>,
+    <a href="https://router.vuejs.org/" target="_blank" rel="noopener">Vue Router</a>,
+    <a href="https://test-utils.vuejs.org/" target="_blank" rel="noopener">Vue Test Utils</a>, and
+    <a href="https://github.com/vuejs/devtools" target="_blank" rel="noopener">Vue Dev Tools</a>. If
+    you need more resources, we suggest paying
+    <a href="https://github.com/vuejs/awesome-vue" target="_blank" rel="noopener">Awesome Vue</a>
+    a visit.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <CommunityIcon />
+    </template>
+    <template #heading>Community</template>
+
+    Got stuck? Ask your question on
+    <a href="https://chat.vuejs.org" target="_blank" rel="noopener">Vue Land</a>, our official
+    Discord server, or
+    <a href="https://stackoverflow.com/questions/tagged/vue.js" target="_blank" rel="noopener"
+      >StackOverflow</a
+    >. You should also subscribe to
+    <a href="https://news.vuejs.org" target="_blank" rel="noopener">our mailing list</a> and follow
+    the official
+    <a href="https://twitter.com/vuejs" target="_blank" rel="noopener">@vuejs</a>
+    twitter account for latest news in the Vue world.
+  </WelcomeItem>
+
+  <WelcomeItem>
+    <template #icon>
+      <SupportIcon />
+    </template>
+    <template #heading>Support Vue</template>
+
+    As an independent project, Vue relies on community backing for its sustainability. You can help
+    us by
+    <a href="https://vuejs.org/sponsor/" target="_blank" rel="noopener">becoming a sponsor</a>.
+  </WelcomeItem>
+</template>

+ 86 - 0
src/components/WelcomeItem.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="item">
+    <i>
+      <slot name="icon"></slot>
+    </i>
+    <div class="details">
+      <h3>
+        <slot name="heading"></slot>
+      </h3>
+      <slot></slot>
+    </div>
+  </div>
+</template>
+
+<style scoped>
+.item {
+  margin-top: 2rem;
+  display: flex;
+  position: relative;
+}
+
+.details {
+  flex: 1;
+  margin-left: 1rem;
+}
+
+i {
+  display: flex;
+  place-items: center;
+  place-content: center;
+  width: 32px;
+  height: 32px;
+  color: var(--color-text);
+}
+
+h3 {
+  font-size: 1.2rem;
+  font-weight: 500;
+  margin-bottom: 0.4rem;
+  color: var(--color-heading);
+}
+
+@media (min-width: 1024px) {
+  .item {
+    margin-top: 0;
+    padding: 0.4rem 0 1rem calc(var(--section-gap) / 2);
+  }
+
+  i {
+    top: calc(50% - 25px);
+    left: -26px;
+    position: absolute;
+    border: 1px solid var(--color-border);
+    background: var(--color-background);
+    border-radius: 8px;
+    width: 50px;
+    height: 50px;
+  }
+
+  .item:before {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    bottom: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:after {
+    content: ' ';
+    border-left: 1px solid var(--color-border);
+    position: absolute;
+    left: 0;
+    top: calc(50% + 25px);
+    height: calc(50% - 25px);
+  }
+
+  .item:first-of-type:before {
+    display: none;
+  }
+
+  .item:last-of-type:after {
+    display: none;
+  }
+}
+</style>

File diff suppressed because it is too large
+ 7 - 0
src/components/icons/IconCommunity.vue


File diff suppressed because it is too large
+ 7 - 0
src/components/icons/IconDocumentation.vue


File diff suppressed because it is too large
+ 7 - 0
src/components/icons/IconEcosystem.vue


+ 7 - 0
src/components/icons/IconSupport.vue

@@ -0,0 +1,7 @@
+<template>
+  <svg xmlns="http://www.w3.org/2000/svg" width="20" height="20" fill="currentColor">
+    <path
+      d="M10 3.22l-.61-.6a5.5 5.5 0 0 0-7.666.105 5.5 5.5 0 0 0-.114 7.665L10 18.78l8.39-8.4a5.5 5.5 0 0 0-.114-7.665 5.5 5.5 0 0 0-7.666-.105l-.61.61z"
+    />
+  </svg>
+</template>

+ 19 - 0
src/components/icons/IconTooling.vue

@@ -0,0 +1,19 @@
+<!-- This icon is from <https://github.com/Templarian/MaterialDesign>, distributed under Apache 2.0 (https://www.apache.org/licenses/LICENSE-2.0) license-->
+<template>
+  <svg
+    xmlns="http://www.w3.org/2000/svg"
+    xmlns:xlink="http://www.w3.org/1999/xlink"
+    aria-hidden="true"
+    role="img"
+    class="iconify iconify--mdi"
+    width="24"
+    height="24"
+    preserveAspectRatio="xMidYMid meet"
+    viewBox="0 0 24 24"
+  >
+    <path
+      d="M20 18v-4h-3v1h-2v-1H9v1H7v-1H4v4h16M6.33 8l-1.74 4H7v-1h2v1h6v-1h2v1h2.41l-1.74-4H6.33M9 5v1h6V5H9m12.84 7.61c.1.22.16.48.16.8V18c0 .53-.21 1-.6 1.41c-.4.4-.85.59-1.4.59H4c-.55 0-1-.19-1.4-.59C2.21 19 2 18.53 2 18v-4.59c0-.32.06-.58.16-.8L4.5 7.22C4.84 6.41 5.45 6 6.33 6H7V5c0-.55.18-1 .57-1.41C7.96 3.2 8.44 3 9 3h6c.56 0 1.04.2 1.43.59c.39.41.57.86.57 1.41v1h.67c.88 0 1.49.41 1.83 1.22l2.34 5.39z"
+      fill="currentColor"
+    ></path>
+  </svg>
+</template>

+ 22 - 0
src/lang/index.js

@@ -0,0 +1,22 @@
+import { createI18n } from 'vue-i18n'
+// 本地语言包
+import enLocale from './package/en'
+import zhCnLocale from './package/zh-cn'
+
+const messages = {
+  'zh-cn': {
+    ...zhCnLocale
+  },
+  en: {
+    ...enLocale
+  }
+}
+
+const i18n = createI18n({
+  legacy: false,
+  locale: 'zh-cn',
+  messages: messages,
+  globalInjection: true
+})
+
+export default i18n

+ 40 - 0
src/lang/package/en.js

@@ -0,0 +1,40 @@
+export default {
+  // 路由国际化
+  route: {
+    dashboard: 'Dashboard',
+    document: 'Document'
+  },
+  // 登录页面国际化
+  login: {
+    username: 'Username',
+    password: 'Password',
+    login: 'Login',
+    captchaCode: 'Verify Code'
+  },
+  // 导航栏国际化
+  navbar: {
+    dashboard: 'Dashboard',
+    logout: 'Logout',
+    document: 'Document',
+    gitee: 'Gitee'
+  },
+  ErrorMessage: {
+    UNKNOW: 'unknow',
+    BADPARAM: 'bad param',
+    NETWORK: 'network',
+    JSON_ERROR: 'json error',
+    USER_NOT_EXIST: 'user not exist',
+    BAD_PASSWORD: 'bad password',
+    NOT_LOGIN: 'not login',
+    ACCESS_DENIED: 'access denied',
+    DATA_NOT_EXIST: 'data not exist',
+    DATA_EXISTED: 'data existed',
+    DATA_INVALID: 'data invalid',
+    VERIFYCODE_INVALID: 'verifycode invalid',
+    SERVICE_FAULT: 'service fault',
+    DATABASE_FAULT: 'database fault',
+    FILE_FAULT: 'file fault',
+    USER_NOT_BIND: 'user not bind',
+    BUSINESS: 'business error'
+  }
+}

+ 40 - 0
src/lang/package/zh-cn.js

@@ -0,0 +1,40 @@
+export default {
+  // 路由国际化
+  route: {
+    dashboard: '首页',
+    document: '项目文档'
+  },
+  // 登录页面国际化
+  login: {
+    username: '用户名',
+    password: '密码',
+    login: '登 录',
+    captchaCode: '验证码'
+  },
+  // 导航栏国际化
+  navbar: {
+    dashboard: '首页',
+    logout: '注销',
+    document: '项目文档',
+    gitee: '码云'
+  },
+  ErrorMessage: {
+    UNKNOW: '系统错误',
+    BADPARAM: '参数错误',
+    NETWORK: '网络错误',
+    JSON_ERROR: 'JSON错误',
+    USER_NOT_EXIST: '用户不存在',
+    BAD_PASSWORD: '密码错误',
+    NOT_LOGIN: '未登录',
+    ACCESS_DENIED: '禁止访问',
+    DATA_NOT_EXIST: '数据不存在',
+    DATA_EXISTED: '数据已存在',
+    DATA_INVALID: '无效数据',
+    VERIFYCODE_INVALID: '验证码无效',
+    SERVICE_FAULT: '服务错误',
+    DATABASE_FAULT: '数据库错误',
+    FILE_FAULT: '文件错误',
+    USER_NOT_BIND: '用户未绑定',
+    BUSINESS: '业务错误'
+  }
+}

+ 59 - 0
src/layout/index.vue

@@ -0,0 +1,59 @@
+<template>
+  <el-container class="main">
+    <el-header :style="{ padding: 0 }"> <component :is="cHeader"></component></el-header>
+    <el-container>
+      <el-aside width="200px" :style="{ 'background-color': '#242f42' }"><component :is="cAside"></component></el-aside>
+      <el-main>
+        <div class="content-box" :class="{ 'content-collapse': collapse }">
+          <el-col :span="24" class="content">
+            <transition name="move" mode="out-in">
+              <el-row>
+                <component :is="cBreadcrumb" :breadcrumbTitle="route.meta.title"></component>
+                <el-col :span="24" class="container" :style="{ padding: '10px 0 0 0' }"><router-view :style="operaViewStyle"></router-view></el-col>
+              </el-row>
+            </transition>
+            <el-backtop target=".content"></el-backtop>
+          </el-col>
+        </div>
+      </el-main>
+    </el-container>
+  </el-container>
+</template>
+
+<script setup>
+// 组件
+import cHeader from './parts/Header.vue'
+import cAside from './parts/Sidebar.vue'
+import cBreadcrumb from './parts/breadcrumb.vue'
+// import { useRoute } from 'vue-router'
+// import { ref } from 'vue'
+
+// 路由
+const route = useRoute()
+// const breadcrumbTitle: Ref<any> = ref();
+let collapse = ref(false)
+const operaViewStyle = ref({
+  height: '85vh',
+  background: '#ffffff',
+  'overflow-x': 'hidden',
+  'overflow-y': 'auto',
+  border: '1px solid #f0f0f0',
+  padding: '10px'
+})
+</script>
+<style scoped lang="scss">
+.main {
+  background-color: #f0f0f0;
+  .el-header {
+    border-bottom: 1px solid;
+  }
+  .el-aside {
+    height: 93vh;
+    overflow-x: auto;
+    overflow-y: auto;
+  }
+  .el-main {
+    padding: 10px;
+  }
+}
+</style>

+ 76 - 0
src/layout/parts/Header.vue

@@ -0,0 +1,76 @@
+<template>
+  <div id="Header">
+    <el-row>
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="main header">
+          <el-col :span="24" class="one">
+            <el-col :span="12" class="left">
+              <span>
+                <i v-if="!collapse" class="el-icon-s-fold"></i>
+                <i v-else class="el-icon-s-unfold"></i>
+              </span>
+              <span>{{ siteInfo.zhTitle }}-管理中心</span>
+            </el-col>
+            <el-col :span="12" class="right">
+              <span>
+                <el-icon><UserFilled /></el-icon>
+                {{ user && user._id ? user.nick_name : '游客' }}
+              </span>
+              <el-button type="danger" size="small" @click="logout">退出登录</el-button>
+            </el-col>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { siteInfo } from '@/layout/site'
+import { UserStore } from '@/store/user'
+import { ref } from 'vue'
+const userStore = UserStore()
+const user = computed(() => userStore.user)
+let collapse = ref(false)
+const route = useRoute()
+// 退出登录
+const logout = () => {
+  localStorage.removeItem('token')
+  route.to('/login')
+}
+</script>
+<style scoped lang="scss">
+.main {
+  background-color: #242f42;
+  .one {
+    height: 60px;
+    border-bottom: 1px solid #f1f1f1;
+    padding: 0 10px;
+    display: flex;
+    .left {
+      line-height: 60px;
+      span {
+        display: inline-block;
+        margin: 0 10px;
+        font-size: 22px;
+        color: #ffffff;
+        font-weight: bold;
+        font-family: cursive;
+      }
+    }
+    .right {
+      text-align: right;
+      padding: 16px 0;
+      span {
+        padding: 0 8px;
+        color: #ffffff;
+        position: relative;
+        top: 1px;
+      }
+      .el-icon {
+        top: 2px;
+      }
+    }
+  }
+}
+</style>

+ 113 - 0
src/layout/parts/Sidebar.vue

@@ -0,0 +1,113 @@
+<!-- eslint-disable vue/no-deprecated-slot-attribute -->
+<template>
+  <div id="Sidebar">
+    <el-row class="sidebar">
+      <el-col :span="24" class="main">
+        <el-col :span="24" class="first">
+          <el-menu
+            class="sidebar-el-menu"
+            :default-active="onRoutes"
+            unique-opened
+            router
+            :background-color="styleInfo.backColor"
+            :text-color="styleInfo.textColor"
+            :active-text-color="styleInfo.activeColor"
+          >
+            <template v-for="item in items">
+              <template v-if="item.type === '0'">
+                <el-sub-menu :index="item._id" :key="item._id">
+                  <template #title>
+                    <i :class="['iconfont', item.icon]"></i>
+                    <span>{{ item.name }}</span>
+                  </template>
+                  <template v-for="subItem in item.children">
+                    <!-- TODO:这里有问题需要改成自引用输出方式.实现无线嵌套.目前只是最多三级 -->
+                    <template v-if="subItem.type === '0'">
+                      <el-sub-menu v-if="subItem.children && subItem.children.length > 0" :index="subItem._id" :key="subItem._id">
+                        <template #title>
+                          <i :class="['iconfont', subItem.icon]"></i>
+                          <span>{{ subItem.name }}</span>
+                        </template>
+                        <el-menu-item v-for="(threeItem, i) in subItem.children" :key="i" :index="threeItem.path">
+                          <i :class="['iconfont', threeItem.icon]"></i>
+                          <span>{{ threeItem.name }}</span>
+                        </el-menu-item>
+                      </el-sub-menu>
+                    </template>
+                    <el-menu-item v-else-if="subItem.type === '1'" :index="subItem.path" :key="subItem.path">
+                      <i :class="['iconfont', subItem.icon]"></i>
+                      <span>{{ subItem.name }}</span>
+                    </el-menu-item>
+                  </template>
+                </el-sub-menu>
+              </template>
+              <template v-else>
+                <el-menu-item :index="item.path" :key="item.path">
+                  <i :class="['iconfont', item.icon]"></i>
+                  <span>{{ item.name }}</span>
+                </el-menu-item>
+              </template>
+            </template>
+          </el-menu>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { menuInfo } from '@/layout/site'
+import { ElMessage } from 'element-plus'
+import { UserStore } from '@/store/user'
+import { ref, watch } from 'vue'
+
+const route = useRoute()
+const userStore = UserStore()
+const user = computed(() => userStore.user)
+let onRoutes = route.path
+const styleInfo = ref(menuInfo.info)
+let items = ref(menuInfo.menuList)
+
+const getMenu = async () => {
+  const menus = user.value.menus
+  const newMenus = [...menus]
+  items.value = newMenus
+}
+
+watch(
+  user,
+  (newVal) => {
+    if (newVal && newVal._id) {
+      if (newVal && newVal._id) getMenu()
+      else ElMessage({ message: `暂无用户信息,无法获取菜单信息`, type: 'error' })
+    }
+  },
+  {
+    deep: true
+  }
+)
+</script>
+<style scoped lang="scss">
+.sidebar::-webkit-scrollbar {
+  width: 0;
+}
+
+.sidebar > ul {
+  height: 100%;
+}
+
+.main {
+  .first {
+    padding: 0 2px 0 0;
+
+    .el-menu {
+      border-right: none;
+    }
+
+    .iconfont {
+      font-size: 18px;
+      margin: 0 5px 0 0;
+    }
+  }
+}
+</style>

+ 22 - 0
src/layout/parts/breadcrumb.vue

@@ -0,0 +1,22 @@
+<template>
+  <div id="breadcrumb">
+    <el-row>
+      <el-col :span="24" class="crumbs">
+        <el-breadcrumb separator="/">
+          <el-breadcrumb-item> <i class="el-icon-s-grid"></i> {{ breadcrumbTitle }} </el-breadcrumb-item>
+        </el-breadcrumb>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+// #region 参数传递
+const props = defineProps({
+  breadcrumbTitle: { type: String, default: () => '' }
+})
+const { breadcrumbTitle } = toRefs(props)
+
+// #endregion
+</script>
+<style lang="scss" scoped></style>

+ 169 - 0
src/layout/site.js

@@ -0,0 +1,169 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '食堂管理平台',
+  logo_url: '/src/assets/logo.png'
+}
+// 菜单设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  },
+  menuList: [
+    // {
+    //   _id: '65a479b4969adc42d8feea73',
+    //   name: '首页',
+    //   order_num: 1,
+    //   path: '/homeIndex',
+    //   component: '/home/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea71',
+    //   name: '系统设置',
+    //   order_num: 2,
+    //   type: '0',
+    //   is_use: '0',
+    //   for_platform: '0',
+    //   children: [
+    //     {
+    //       _id: '65a479b4969adc42d8feea75',
+    //       name: '菜单设置',
+    //       parent_id: '65a479b4969adc42d8feea71',
+    //       order_num: 1,
+    //       path: '/system/menus',
+    //       component: '/system/menus/index',
+    //       type: '1',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '系统设置'
+    //     },
+    //     {
+    //       _id: '65a479b4969adc42d8feea76',
+    //       name: '角色管理',
+    //       parent_id: '65a479b4969adc42d8feea71',
+    //       order_num: 2,
+    //       path: '/system/role',
+    //       component: '/system/role/index',
+    //       type: '1',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '系统设置'
+    //     },
+    //     {
+    //       _id: '65a479b4969adc42d8feea77',
+    //       name: '字典管理',
+    //       parent_id: '65a479b4969adc42d8feea71',
+    //       order_num: 3,
+    //       path: '/system/dict',
+    //       component: '/system/dict/index',
+    //       type: '1',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '系统设置'
+    //     },
+    //     {
+    //       _id: '65a479b4969adc42d8feea78',
+    //       name: '字典数据',
+    //       parent_id: '65a479b4969adc42d8feea71',
+    //       order_num: 4,
+    //       path: '/system/dictData',
+    //       component: '/system/dictData/index',
+    //       type: '2',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '系统设置'
+    //     }
+    //   ]
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea72',
+    //   name: '用户管理',
+    //   order_num: 3,
+    //   type: '0',
+    //   is_use: '0',
+    //   for_platform: '0',
+    //   children: [
+    //     {
+    //       _id: '65a479b4969adc42d8feea7a',
+    //       name: '管理员用户',
+    //       parent_id: '65a479b4969adc42d8feea72',
+    //       order_num: 1,
+    //       path: '/user/admin',
+    //       component: '/user/admin/index',
+    //       type: '1',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '用户管理'
+    //     },
+    //     {
+    //       _id: '65a479b4969adc42d8feea7b',
+    //       name: '平台用户',
+    //       parent_id: '65a479b4969adc42d8feea72',
+    //       order_num: 2,
+    //       path: '/user/user',
+    //       component: '/user/user/index',
+    //       type: '1',
+    //       is_use: '0',
+    //       for_platform: '0',
+    //       parent_name: '用户管理'
+    //     }
+    //   ]
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea7e',
+    //   name: '菜品管理',
+    //   order_num: 5,
+    //   path: '/menu',
+    //   component: '/menu/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea7e',
+    //   name: '安排管理',
+    //   order_num: 6,
+    //   path: '/arrange',
+    //   component: '/arrange/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea7f',
+    //   name: '订单管理',
+    //   order_num: 7,
+    //   path: '/order',
+    //   component: '/order/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea80',
+    //   name: '新闻管理',
+    //   order_num: 8,
+    //   path: '/news',
+    //   component: '/news/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // },
+    // {
+    //   _id: '65a479b4969adc42d8feea7c',
+    //   name: '修改密码',
+    //   order_num: 999,
+    //   path: '/acccount/updatepd',
+    //   component: '/acccount/updatepd/index',
+    //   type: '1',
+    //   is_use: '0',
+    //   for_platform: '0'
+    // }
+  ]
+}

+ 17 - 0
src/main.js

@@ -0,0 +1,17 @@
+import { createApp } from 'vue'
+import { setupStore } from '@/store'
+import i18n from '@/lang/index'
+
+import App from './App.vue'
+import router from './router'
+
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+// 国际化
+
+const app = createApp(App)
+setupStore(app)
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app.use(router).use(i18n).mount('#app')

+ 28 - 0
src/router/index.js

@@ -0,0 +1,28 @@
+import { createRouter, createWebHistory } from 'vue-router'
+export const homeIndex = () => import('@/views/home/index.vue')
+
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: [
+    {
+      path: '/login',
+      name: 'login',
+      meta: { title: '账号登录' },
+      component: () => import('@/views/login/index.vue')
+    },
+    {
+      path: '/',
+      meta: { title: '系统首页' },
+      component: () => import('@/layout/index.vue'),
+      children: [
+        {
+          path: '/',
+          meta: { title: '系统首页' },
+          component: () => import('@/views/home/index.vue')
+        }
+      ]
+    }
+  ]
+})
+
+export default router

+ 16 - 0
src/settings.js

@@ -0,0 +1,16 @@
+const defaultSettings = {
+  title: 'admin-template',
+  version: 'v0.0.1',
+  showSettings: true,
+  tagsView: true,
+  fixedHeader: false,
+  sidebarLogo: true,
+  layout: 'left',
+  theme: 'light',
+  size: 'default',
+  language: 'zh-cn',
+  themeColor: '#409EFF',
+  watermark: { enabled: false, content: '福瑞科技' }
+}
+
+export default defaultSettings

+ 25 - 0
src/store/api/login.js

@@ -0,0 +1,25 @@
+import { defineStore } from 'pinia'
+import { get, omit } from 'lodash'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+const axios = new AxiosWrapper()
+export const LoginStore = defineStore('login', () => {
+  const login = async (payload) => {
+    const type = get(payload, 'type')
+    const np = omit(payload, 'type')
+    const res = await axios.$post(`/login/${type}`, np)
+    return res
+  }
+  const rp = async (payload) => {
+    const type = get(payload, 'type')
+    const np = omit(payload, 'type')
+    const res = await axios.$post(`/login/updatePwd/${type}`, np)
+    return res
+  }
+  const rpNoNewPassword = async (payload) => {
+    const type = get(payload, 'type')
+    const np = omit(payload, 'type')
+    const res = await axios.$post(`/login/resetPwd/${type}`, np)
+    return res
+  }
+  return { login, rp, rpNoNewPassword }
+})

+ 15 - 0
src/store/index.js

@@ -0,0 +1,15 @@
+import { createPinia } from 'pinia'
+
+const store = createPinia()
+
+// 全局注册 store
+export function setupStore(app) {
+  app.use(store)
+}
+
+export * from './modules/app'
+// export * from './modules/permission'
+// export * from './modules/settings'
+// export * from './modules/tagsView'
+// export * from './modules/user'
+export { store }

+ 89 - 0
src/store/modules/app.js

@@ -0,0 +1,89 @@
+import defaultSettings from '@/settings'
+// import { useStorage } from '@vueuse/core'
+
+// 导入 Element Plus 中英文语言包
+import zhCn from 'element-plus/es/locale/lang/zh-cn'
+import en from 'element-plus/es/locale/lang/en'
+
+// setup
+export const useAppStore = defineStore('app', () => {
+  // state
+  const device = useStorage('device', 'desktop')
+  const size = useStorage('size', defaultSettings.size)
+  const language = useStorage('language', defaultSettings.language)
+
+  const sidebarStatus = useStorage('sidebarStatus', 'closed')
+
+  const sidebar = reactive({
+    opened: sidebarStatus.value !== 'closed',
+    withoutAnimation: false
+  })
+  const activeTopMenuPath = useStorage('activeTopMenuPath', '')
+  /**
+   * 根据语言标识读取对应的语言包
+   */
+  const locale = computed(() => {
+    if (language?.value == 'en') {
+      return en
+    } else {
+      return zhCn
+    }
+  })
+
+  // actions
+  function toggleSidebar() {
+    sidebar.opened = !sidebar.opened
+    if (sidebar.opened) {
+      sidebarStatus.value = 'opened'
+    } else {
+      sidebarStatus.value = 'closed'
+    }
+  }
+
+  function closeSideBar() {
+    sidebar.opened = false
+    sidebarStatus.value = 'closed'
+  }
+
+  function openSideBar() {
+    sidebar.opened = true
+    sidebarStatus.value = 'opened'
+  }
+
+  function toggleDevice(val) {
+    device.value = val
+  }
+
+  function changeSize(val) {
+    size.value = val
+  }
+  /**
+   * 切换语言
+   *
+   * @param val
+   */
+  function changeLanguage(val) {
+    language.value = val
+  }
+  /**
+   * 混合模式顶部切换
+   */
+  function activeTopMenu(val) {
+    activeTopMenuPath.value = val
+  }
+  return {
+    device,
+    sidebar,
+    language,
+    locale,
+    size,
+    activeTopMenu,
+    toggleDevice,
+    changeSize,
+    changeLanguage,
+    toggleSidebar,
+    closeSideBar,
+    openSideBar,
+    activeTopMenuPath
+  }
+})

+ 6 - 0
src/store/user.js

@@ -0,0 +1,6 @@
+import { defineStore } from 'pinia'
+export const UserStore = defineStore('user', () => {
+  const user = ref({})
+  const setUser = computed((payload) => (user.value = payload))
+  return { user, setUser }
+})

+ 128 - 0
src/utils/axios-wrapper.js

@@ -0,0 +1,128 @@
+/* eslint-disable no-console */
+/* eslint-disable no-param-reassign */
+
+import { get, isObject } from 'lodash-es'
+import Axios from 'axios'
+import { trimData, isNullOrUndefined } from './util-methods'
+import { ErrorCode } from './error-code'
+
+let currentRequests = 0
+
+export class AxiosWrapper {
+  constructor({ baseUrl = import.meta.env.VITE_REQUEST_BASE, unwrap = true } = {}) {
+    this.baseUrl = baseUrl
+    this.unwrap = unwrap
+  }
+  baseUrl
+  unwrap
+
+  // 替换uri中的参数变量
+  static merge(uri, query) {
+    if (!uri.includes(':')) {
+      return uri
+    }
+    const keys = []
+    const regexp = /\/:([a-z0-9_]+)/gi
+    let res
+    // eslint-disable-next-line no-cond-assign
+    while ((res = regexp.exec(uri)) != null) {
+      keys.push(res[1])
+    }
+    keys.forEach((key) => {
+      const val = get(query, key)
+      if (!isNullOrUndefined(val)) {
+        uri = uri.replace(`:${key}`, `${val}`)
+      }
+    })
+    return uri
+  }
+
+  $get(uri, query, options) {
+    return this.$request(uri, undefined, query, options)
+  }
+
+  $post(uri, data = {}, query, options) {
+    return this.$request(uri, data, query, options)
+  }
+  $delete(uri, data = {}, query, options = {}) {
+    options = { ...options, method: 'delete' }
+    return this.$request(uri, data, query, options)
+  }
+  async $request(uri, data, query, options) {
+    if (query && isObject(query)) {
+      const keys = Object.keys(query)
+      for (const key of keys) {
+        const val = get(query, key)
+        if (val === '') {
+          delete query[key]
+        }
+      }
+    }
+    if (isObject(query) && isObject(options)) {
+      options = { ...options, params: query, method: 'get' }
+    } else if (isObject(query) && !query.params) {
+      options = { params: query }
+    } else if (isObject(query) && query.params) {
+      options = query
+    }
+    if (!options) options = {}
+    if (options.params) options.params = trimData(options.params, null, null)
+    const params = get(options, 'params')
+    const url = AxiosWrapper.merge(uri, params)
+    currentRequests += 1
+    // Indicator.open({
+    //   spinnerType: 'fading-circle',
+    // });
+    try {
+      let returnData
+      const axios = Axios.create({
+        baseURL: this.baseUrl
+      })
+      // if (util.token && util.token !== null) axios.defaults.headers.common.Authorization = util.token;
+      const token = localStorage.getItem('token')
+      const apiToken = localStorage.getItem('apiToken')
+      if (token) axios.defaults.headers.common['token'] = token
+      if (apiToken) axios.defaults.headers.common['api-token'] = apiToken
+      const res = await axios.request({
+        method: isNullOrUndefined(data) ? 'get' : 'post',
+        url,
+        data,
+        responseType: 'json',
+        ...options
+      })
+      const returnRes = res.data
+      const { errcode, errmsg, details } = returnRes
+      if (errcode) {
+        console.warn(`[${uri}] fail: ${errcode}-${errmsg} ${details}`)
+        return returnRes
+      }
+      // unwrap data
+      if (this.unwrap) {
+        returnData = returnRes
+      }
+      // 处理apiToken
+      const { apiToken: at, ...others } = returnData
+      if (at) localStorage.setItem('apiToken', at)
+      return others
+    } catch (err) {
+      let errmsg = '接口请求失败,请稍后重试'
+      if (err.response) {
+        const { status } = err.response
+        if (status === 401) errmsg = '用户认证失败,请重新登录'
+        if (status === 403) errmsg = '当前用户不允许执行该操作'
+      }
+      console.error(
+        `[AxiosWrapper] 接口请求失败: ${err.config && err.config.url} - 
+        ${err.message}`
+      )
+      return { errcode: ErrorCode.SERVICE_FAULT, errmsg, details: err.message }
+    } finally {
+      /* eslint-disable */
+      currentRequests -= 1
+      if (currentRequests <= 0) {
+        currentRequests = 0
+        // Indicator.close();
+      }
+    }
+  }
+}

+ 50 - 0
src/utils/error-code.js

@@ -0,0 +1,50 @@
+import i18n from '@/lang/index'
+
+const UNKNOW = 'UNKNOW'
+const BADPARAM = 'BADPARAM'
+const NETWORK = 'NETWORK'
+const JSON_ERROR = 'JSON_ERROR'
+const USER_NOT_EXIST = 'USER_NOT_EXIST'
+const BAD_PASSWORD = 'BAD_PASSWORD'
+const NOT_LOGIN = 'NOT_LOGIN'
+const ACCESS_DENIED = 'ACCESS_DENIED'
+const DATA_NOT_EXIST = 'DATA_NOT_EXIST'
+const DATA_EXISTED = 'DATA_EXISTED'
+const DATA_INVALID = 'DATA_INVALID'
+const VERIFYCODE_INVALID = 'VERIFYCODE_INVALID'
+const SERVICE_FAULT = 'SERVICE_FAULT'
+const DATABASE_FAULT = 'DATABASE_FAULT'
+const FILE_FAULT = 'FILE_FAULT'
+const USER_NOT_BIND = 'USER_NOT_BIND'
+const BUSINESS = 'BUSINESS'
+
+const ErrorCode = {
+  [UNKNOW]: -1,
+  [BADPARAM]: -2,
+  [NETWORK]: -3,
+  [JSON_ERROR]: -4,
+  [USER_NOT_EXIST]: -5,
+  [BAD_PASSWORD]: -6,
+  [NOT_LOGIN]: -7,
+  [ACCESS_DENIED]: -8,
+  [DATA_NOT_EXIST]: -9,
+  [DATA_EXISTED]: -10,
+  [DATA_INVALID]: -11,
+  [VERIFYCODE_INVALID]: -12,
+  [SERVICE_FAULT]: -13,
+  [DATABASE_FAULT]: -14,
+  [FILE_FAULT]: -15,
+  [USER_NOT_BIND]: -16,
+  [BUSINESS]: -100
+}
+
+const errmsg = (err) => {
+  const ErrorMessage = i18n.global.t('ErrorMessage')
+  for (const key in ErrorMessage) {
+    if (ErrorCode[key] === err) {
+      return ErrorMessage[key]
+    }
+  }
+}
+
+export { ErrorCode, errmsg }

+ 12 - 0
src/utils/i18n.js

@@ -0,0 +1,12 @@
+// translate router.meta.title, be used in breadcrumb sidebar tagsview
+import i18n from '@/lang/index'
+
+export function translateRouteTitle(title) {
+  // 判断是否存在国际化配置,如果没有原生返回
+  const hasKey = i18n.global.te('route.' + title)
+  if (hasKey) {
+    const translatedTitle = i18n.global.t('route.' + title)
+    return translatedTitle
+  }
+  return title
+}

+ 35 - 0
src/utils/util-methods.js

@@ -0,0 +1,35 @@
+import { isUndefined, isString, isNumber, isBoolean } from 'lodash-es'
+export const trimData = (data, exclude = [], include) => {
+  if (data === null || data === undefined) {
+    return data
+  }
+
+  for (const key in data) {
+    if (isUndefined(data[key]) || (exclude && exclude.indexOf(key) !== -1)) delete data[key]
+    if (include && include.indexOf(key) === -1) delete data[key]
+  }
+  return data
+}
+// 检查email格式
+export const isEmail = function (val) {
+  // 允许汉字、英文字母、数字
+  return /^[A-Za-z0-9\u4e00-\u9fa5]+@[a-zA-Z0-9_-]+(\.[a-zA-Z0-9_-]+)+$/.test(val)
+}
+
+// 是否为空
+export const isNullOrUndefined = function (val) {
+  return val === null || val === undefined
+}
+
+// 转换为boolean
+export const toBoolean = function (val) {
+  if (isString(val) && (val.toLowerCase() === 'true' || val.toLowerCase() !== '0')) {
+    val = true
+  } else if (isNumber(val) && val !== 0) {
+    val = true
+  } else {
+    val = isBoolean(val) && val
+  }
+
+  return val
+}

+ 105 - 0
src/views/error-page/401.vue

@@ -0,0 +1,105 @@
+<script setup>
+import { reactive, toRefs } from 'vue'
+import { useRouter } from 'vue-router'
+
+import { defineComponent } from 'vue'
+
+defineComponent({
+  name: 'Page401'
+})
+
+const state = reactive({
+  errGif: new URL(`../../assets/images/401.gif`, import.meta.url).href,
+
+  ewizardClap: 'https://wpimg.wallstcn.com/007ef517-bafd-4066-aae4-6883632d9646',
+  dialogVisible: false
+})
+
+const { errGif, ewizardClap, dialogVisible } = toRefs(state)
+
+const router = useRouter()
+
+function back() {
+  router.back()
+}
+</script>
+
+<template>
+  <div class="errPage-container">
+    <el-button icon="el-icon-arrow-left" class="pan-back-btn" @click="back"> 返回 </el-button>
+    <el-row>
+      <el-col :span="12">
+        <h1 class="text-jumbo text-ginormous">Oops!</h1>
+        gif来源<a href="https://zh.airbnb.com/" target="_blank">airbnb</a> 页面
+        <h2>你没有权限去该页面</h2>
+        <h6>如有不满请联系你领导</h6>
+        <ul class="list-unstyled">
+          <li>或者你可以去:</li>
+          <li class="link-type">
+            <router-link to="/dashboard"> 回首页 </router-link>
+          </li>
+          <li class="link-type">
+            <a href="https://www.taobao.com/">随便看看</a>
+          </li>
+          <li>
+            <a href="#" @click.prevent="dialogVisible = true">点我看图</a>
+          </li>
+        </ul>
+      </el-col>
+      <el-col :span="12">
+        <img :src="errGif" width="313" height="428" alt="Girl has dropped her ice cream." />
+      </el-col>
+    </el-row>
+    <el-dialog v-model="dialogVisible" title="随便看">
+      <img :src="ewizardClap" class="pan-img" />
+    </el-dialog>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.errPage-container {
+  width: 800px;
+  max-width: 100%;
+  margin: 100px auto;
+
+  .pan-back-btn {
+    color: #fff;
+    background: #008489;
+    border: none !important;
+  }
+
+  .pan-gif {
+    display: block;
+    margin: 0 auto;
+  }
+
+  .pan-img {
+    display: block;
+    width: 100%;
+    margin: 0 auto;
+  }
+
+  .text-jumbo {
+    font-size: 60px;
+    font-weight: 700;
+    color: #484848;
+  }
+
+  .list-unstyled {
+    font-size: 14px;
+
+    li {
+      padding-bottom: 5px;
+    }
+
+    a {
+      color: #008489;
+      text-decoration: none;
+
+      &:hover {
+        text-decoration: underline;
+      }
+    }
+  }
+}
+</style>

+ 256 - 0
src/views/error-page/404.vue

@@ -0,0 +1,256 @@
+<!-- setup 无法设置组件名称,组件名称keepAlive必须 -->
+<script>
+export default {
+  name: 'Page404'
+}
+</script>
+
+<script setup lang="ts">
+function message() {
+  return 'The webmaster said that you can not enter this page...'
+}
+</script>
+
+<template>
+  <div class="wscn-http404-container">
+    <div class="wscn-http404">
+      <div class="pic-404">
+        <img class="pic-404__parent" src="@/assets/images/404.png" alt="404" />
+        <img class="pic-404__child left" src="@/assets/images/404_cloud.png" alt="404" />
+        <img class="pic-404__child mid" src="@/assets/images/404_cloud.png" alt="404" />
+        <img class="pic-404__child right" src="@/assets/images/404_cloud.png" alt="404" />
+      </div>
+      <div class="bullshit">
+        <div class="bullshit__oops">OOPS!</div>
+        <div class="bullshit__info">
+          All rights reserved
+          <a style="color: #20a0ff" href="https://wallstreetcn.com" target="_blank">wallstreetcn</a>
+        </div>
+        <div class="bullshit__headline">{{ message() }}</div>
+        <div class="bullshit__info">Please check that the URL you entered is correct, or click the button below to return to the homepage.</div>
+        <a href="" class="bullshit__return-home">Back to home</a>
+      </div>
+    </div>
+  </div>
+</template>
+
+<style lang="scss" scoped>
+.wscn-http404-container {
+  position: absolute;
+  top: 40%;
+  left: 50%;
+  transform: translate(-50%, -50%);
+}
+
+.wscn-http404 {
+  position: relative;
+  width: 1200px;
+  padding: 0 50px;
+  overflow: hidden;
+
+  .pic-404 {
+    position: relative;
+    float: left;
+    width: 600px;
+    overflow: hidden;
+
+    &__parent {
+      width: 100%;
+    }
+
+    &__child {
+      position: absolute;
+
+      &.left {
+        top: 17px;
+        left: 220px;
+        width: 80px;
+        opacity: 0;
+        animation-name: cloudLeft;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-delay: 1s;
+        animation-fill-mode: forwards;
+      }
+
+      &.mid {
+        top: 10px;
+        left: 420px;
+        width: 46px;
+        opacity: 0;
+        animation-name: cloudMid;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-delay: 1.2s;
+        animation-fill-mode: forwards;
+      }
+
+      &.right {
+        top: 100px;
+        left: 500px;
+        width: 62px;
+        opacity: 0;
+        animation-name: cloudRight;
+        animation-duration: 2s;
+        animation-timing-function: linear;
+        animation-delay: 1s;
+        animation-fill-mode: forwards;
+      }
+
+      @keyframes cloudLeft {
+        0% {
+          top: 17px;
+          left: 220px;
+          opacity: 0;
+        }
+
+        20% {
+          top: 33px;
+          left: 188px;
+          opacity: 1;
+        }
+
+        80% {
+          top: 81px;
+          left: 92px;
+          opacity: 1;
+        }
+
+        100% {
+          top: 97px;
+          left: 60px;
+          opacity: 0;
+        }
+      }
+
+      @keyframes cloudMid {
+        0% {
+          top: 10px;
+          left: 420px;
+          opacity: 0;
+        }
+
+        20% {
+          top: 40px;
+          left: 360px;
+          opacity: 1;
+        }
+
+        70% {
+          top: 130px;
+          left: 180px;
+          opacity: 1;
+        }
+
+        100% {
+          top: 160px;
+          left: 120px;
+          opacity: 0;
+        }
+      }
+
+      @keyframes cloudRight {
+        0% {
+          top: 100px;
+          left: 500px;
+          opacity: 0;
+        }
+
+        20% {
+          top: 120px;
+          left: 460px;
+          opacity: 1;
+        }
+
+        80% {
+          top: 180px;
+          left: 340px;
+          opacity: 1;
+        }
+
+        100% {
+          top: 200px;
+          left: 300px;
+          opacity: 0;
+        }
+      }
+    }
+  }
+
+  .bullshit {
+    position: relative;
+    float: left;
+    width: 300px;
+    padding: 30px 0;
+    overflow: hidden;
+
+    &__oops {
+      margin-bottom: 20px;
+      font-size: 32px;
+      font-weight: bold;
+      line-height: 40px;
+      color: #1482f0;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-fill-mode: forwards;
+    }
+
+    &__headline {
+      margin-bottom: 10px;
+      font-size: 20px;
+      font-weight: bold;
+      line-height: 24px;
+      color: #222;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.1s;
+      animation-fill-mode: forwards;
+    }
+
+    &__info {
+      margin-bottom: 30px;
+      font-size: 13px;
+      line-height: 21px;
+      color: grey;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.2s;
+      animation-fill-mode: forwards;
+    }
+
+    &__return-home {
+      display: block;
+      float: left;
+      width: 110px;
+      height: 36px;
+      font-size: 14px;
+      line-height: 36px;
+      color: #fff;
+      text-align: center;
+      cursor: pointer;
+      background: #1482f0;
+      border-radius: 100px;
+      opacity: 0;
+      animation-name: slideUp;
+      animation-duration: 0.5s;
+      animation-delay: 0.3s;
+      animation-fill-mode: forwards;
+    }
+
+    @keyframes slideUp {
+      0% {
+        opacity: 0;
+        transform: translateY(60px);
+      }
+
+      100% {
+        opacity: 1;
+        transform: translateY(0);
+      }
+    }
+  }
+}
+</style>

+ 9 - 0
src/views/home/index.vue

@@ -0,0 +1,9 @@
+<template>
+  <div id="index" style="">
+    <p>index</p>
+  </div>
+</template>
+
+<script setup></script>
+
+<style scoped></style>

+ 49 - 0
src/views/login/index.js

@@ -0,0 +1,49 @@
+import { ref, reactive } from 'vue'
+import { ElMessage } from 'element-plus'
+import router from '@/router'
+// 接口
+import { LoginStore } from '@/stores/login'
+const loginStore = LoginStore()
+
+// 表单
+/**
+ * 表单对象
+ */
+const formRef = ref()
+/**
+ * 表单model
+ */
+const form = ref({ type: 'Admin' })
+/**
+ * 输入规则
+ */
+const rules = reactive({
+  account: [{ required: true, message: '请输入登录账号', trigger: 'blur' }],
+  password: [{ required: true, message: '请输入账号密码', trigger: 'blur' }]
+})
+
+/**
+ * 提交登录
+ */
+const toSave = async (formEl) => {
+  if (!formEl) return
+  await formEl.validate((valid) => {
+    if (valid) {
+      toLogin(form.value)
+    } else {
+      console.log('error submit!')
+    }
+  })
+}
+const toLogin = async (data) => {
+  const res = await loginStore.login(data)
+  if (res.errcode == '0') {
+    ElMessage({ message: `登录成功`, type: 'success' })
+    localStorage.setItem('token', `${res.data}`)
+    // 路由
+    router.push({ path: '/' })
+  } else {
+    ElMessage({ message: `${res.errmsg}`, type: 'error' })
+  }
+}
+export { toSave, formRef, form, rules }

+ 109 - 0
src/views/login/index.vue

@@ -0,0 +1,109 @@
+<template>
+  <div id="index">
+    <el-row>
+      <el-col :span="24" class="main animate__animated animate__backInRight">
+        <el-col :span="24" class="one w_1200">
+          <el-col :span="24" class="one_1">{{ siteInfo.zhTitle }}</el-col>
+          <el-col :span="24" class="one_2">
+            <div class="login">
+              <el-col :span="24" class="login_1"> 管理登录 </el-col>
+              <el-col :span="24" class="login_2">
+                <el-col :span="24" class="form">
+                  <el-form ref="formRef" :model="form" :rules="rules" label-width="auto">
+                    <!-- <el-form-item>
+                      <el-radio-group v-model="form.type" style="padding-left: 15px">
+                        <el-radio label="SelfUser">个人用户</el-radio>
+                        <el-radio label="Unit">企业用户</el-radio>
+                        <el-radio label="Research">科研单位</el-radio>
+                        <el-radio label="Admin">管理员</el-radio>
+                      </el-radio-group>
+                    </el-form-item> -->
+                    <el-form-item label="" prop="account">
+                      <el-input v-model="form.account" placeholder="请输入登录账号">
+                        <template #prefix>
+                          <el-icon>
+                            <User />
+                          </el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-form-item label="" prop="password">
+                      <el-input v-model="form.password" type="password" show-password placeholder="请输入登录密码">
+                        <template #prefix>
+                          <el-icon>
+                            <Unlock />
+                          </el-icon>
+                        </template>
+                      </el-input>
+                    </el-form-item>
+                    <el-col :span="24" class="btn">
+                      <el-button type="primary" @click="toSave(formRef)">提交登录</el-button>
+                    </el-col>
+                  </el-form>
+                </el-col>
+              </el-col>
+            </div>
+          </el-col>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup lang="ts">
+import { toSave, formRef, form, rules } from './index';
+import { siteInfo } from '@/layout/site';
+</script>
+<style scoped lang="scss">
+.main {
+  display: flex;
+  flex-direction: column;
+  width: 100vw;
+  height: 100vh;
+  overflow: hidden;
+  background: url('@/assets/bglogin.jpg');
+  background-repeat: no-repeat;
+  background-size: 100% 100%;
+
+  .one {
+    .one_1 {
+      text-align: center;
+      color: #ffffff;
+      font-size: 30px;
+      margin: 5% 0 40px 0;
+    }
+
+    .one_2 {
+      text-align: center;
+      display: flex;
+      justify-content: center;
+
+      .login {
+        width: 500px;
+        height: 500px;
+        background-color: #ffffff;
+        border-radius: 5px;
+        padding: 10px;
+
+        .login_1 {
+          text-align: center;
+          margin: 30px 0;
+          font-size: 25px;
+          color: #409eff;
+          font-weight: 700;
+        }
+
+        .login_2 {
+          .type {
+            margin: 0 0 10px 0;
+          }
+        }
+      }
+    }
+  }
+}
+
+:deep(.el-input-group__append, .el-input-group__prepend) {
+  padding: 0;
+}
+</style>

+ 153 - 0
vite.config.js

@@ -0,0 +1,153 @@
+import AutoImport from 'unplugin-auto-import/vite'
+import Components from 'unplugin-vue-components/vite'
+import Icons from 'unplugin-icons/vite'
+import IconsResolver from 'unplugin-icons/resolver'
+import { ElementPlusResolver } from 'unplugin-vue-components/resolvers'
+import { defineConfig, loadEnv } from 'vite'
+import vue from '@vitejs/plugin-vue'
+import Inspect from 'vite-plugin-inspect'
+import path from 'path'
+const pathSrc = path.resolve(__dirname, 'src')
+// https://vitejs.dev/config/
+export default defineConfig(({ mode }) => {
+  const env = loadEnv(mode, process.cwd())
+  return {
+    server: {
+      // 允许IP访问
+      host: '0.0.0.0',
+      // 应用端口 (默认:3000)
+      port: Number(env.VITE_APP_PORT),
+      // 运行是否自动打开浏览器
+      open: true,
+      proxy: {
+        /**
+         * env.VITE_APP_BASE_API: /dev-api
+         */
+        [env.VITE_APP_BASE_API]: {
+          changeOrigin: true,
+          target: 'http://localhost:8989'
+        }
+      }
+    },
+    resolve: {
+      alias: {
+        '@': pathSrc
+      }
+    },
+    css: {
+      // CSS 预处理器
+      preprocessorOptions: {
+        // 定义全局 SCSS 变量
+        scss: {
+          javascriptEnabled: true
+          // additionalData: `
+          //   @use "@/styles/variables.scss" as *;
+          // `
+        }
+      }
+    },
+    plugins: [
+      vue(),
+      // // 自动导入参考: https://github.com/sxzz/element-plus-best-practices/blob/main/vite.config.ts
+      AutoImport({
+        // 自动导入 Vue 相关函数,如:ref, reactive, toRef 等
+        imports: ['vue', '@vueuse/core', 'pinia', 'vue-router', 'vue-i18n'],
+        // 自动导入 Element Plus 相关函数,如:ElMessage, ElMessageBox... (带样式)
+        resolvers: [ElementPlusResolver(), IconsResolver({})],
+        eslintrc: {
+          enabled: false,
+          filepath: './.eslintrc-auto-import.json',
+          globalsPropValue: true
+        },
+        vueTemplate: true
+        // 配置文件生成位置(false:关闭自动生成)
+        //dts: false,
+        // dts: path.resolve(pathSrc, 'auto-imports.d.ts')
+      }),
+      Components({
+        resolvers: [
+          // 自动导入 Element Plus 组件
+          ElementPlusResolver(),
+          // 自动注册图标组件
+          IconsResolver({ enabledCollections: ['ep'] })
+        ],
+        // 指定自定义组件位置(默认:src/components)
+        dirs: ['src/components', 'src/**/components']
+        // 配置文件位置 (false:关闭自动生成)
+        //dts: false,
+        // dts: path.resolve(pathSrc, 'components.d.ts')
+      }),
+      Icons({
+        autoInstall: true
+      }),
+      Inspect()
+    ],
+    // 预加载项目必需的组件
+    optimizeDeps: {
+      include: [
+        'vue',
+        'vue-router',
+        'pinia',
+        // 'axios',
+        'element-plus/es/components/form/style/css',
+        'element-plus/es/components/form-item/style/css',
+        'element-plus/es/components/button/style/css',
+        'element-plus/es/components/input/style/css',
+        'element-plus/es/components/input-number/style/css',
+        'element-plus/es/components/switch/style/css',
+        'element-plus/es/components/upload/style/css',
+        'element-plus/es/components/menu/style/css',
+        'element-plus/es/components/col/style/css',
+        'element-plus/es/components/icon/style/css',
+        'element-plus/es/components/row/style/css',
+        'element-plus/es/components/tag/style/css',
+        'element-plus/es/components/dialog/style/css',
+        'element-plus/es/components/loading/style/css',
+        'element-plus/es/components/radio/style/css',
+        'element-plus/es/components/radio-group/style/css',
+        'element-plus/es/components/popover/style/css',
+        'element-plus/es/components/scrollbar/style/css',
+        'element-plus/es/components/tooltip/style/css',
+        'element-plus/es/components/dropdown/style/css',
+        'element-plus/es/components/dropdown-menu/style/css',
+        'element-plus/es/components/dropdown-item/style/css',
+        'element-plus/es/components/sub-menu/style/css',
+        'element-plus/es/components/menu-item/style/css',
+        'element-plus/es/components/divider/style/css',
+        'element-plus/es/components/card/style/css',
+        'element-plus/es/components/link/style/css',
+        'element-plus/es/components/breadcrumb/style/css',
+        'element-plus/es/components/breadcrumb-item/style/css',
+        'element-plus/es/components/table/style/css',
+        'element-plus/es/components/tree-select/style/css',
+        'element-plus/es/components/table-column/style/css',
+        'element-plus/es/components/select/style/css',
+        'element-plus/es/components/option/style/css',
+        'element-plus/es/components/pagination/style/css',
+        'element-plus/es/components/tree/style/css',
+        'element-plus/es/components/alert/style/css',
+        'element-plus/es/components/radio-button/style/css',
+        'element-plus/es/components/checkbox-group/style/css',
+        'element-plus/es/components/checkbox/style/css',
+        'element-plus/es/components/tabs/style/css',
+        'element-plus/es/components/tab-pane/style/css',
+        'element-plus/es/components/rate/style/css',
+        'element-plus/es/components/date-picker/style/css',
+        'element-plus/es/components/notification/style/css',
+        'element-plus/es/components/image/style/css',
+        'element-plus/es/components/statistic/style/css',
+        'element-plus/es/components/watermark/style/css',
+        'element-plus/es/components/config-provider/style/css',
+        'vue-i18n',
+        'element-plus/es/components/text/style/css'
+        // '@vueuse/core',
+        // 'sortablejs',
+        // 'path-to-regexp',
+        // 'echarts',
+        // '@wangeditor/editor',
+        // '@wangeditor/editor-for-vue',
+        // 'path-browserify'
+      ]
+    }
+  }
+})