lrf 7 tháng trước cách đây
commit
2a6edd1be8
100 tập tin đã thay đổi với 20357 bổ sung0 xóa
  1. 19 0
      .env.development
  2. 20 0
      .env.production
  3. 288 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. 6 0
      README.md
  9. 24 0
      index.html
  10. 10 0
      jsconfig.json
  11. 11586 0
      package-lock.json
  12. 53 0
      package.json
  13. 4069 0
      pnpm-lock.yaml
  14. BIN
      public/favicon.ico
  15. BIN
      public/images/401.gif
  16. BIN
      public/images/404.png
  17. BIN
      public/images/404_cloud.png
  18. BIN
      public/images/login-bg.jpg
  19. BIN
      public/images/logo-jilinbai.png
  20. BIN
      public/images/logo.png
  21. BIN
      public/images/video.mp4
  22. BIN
      public/logo.png
  23. 1 0
      public/logo.svg
  24. BIN
      public/导入模板 - 副本.xlsx
  25. BIN
      public/导入模板.xlsx
  26. 23 0
      src/App.vue
  27. 1 0
      src/assets/icons/close.svg
  28. 1 0
      src/assets/icons/close_all.svg
  29. 1 0
      src/assets/icons/close_left.svg
  30. 1 0
      src/assets/icons/close_other.svg
  31. 1 0
      src/assets/icons/close_right.svg
  32. 1 0
      src/assets/icons/language.svg
  33. 1 0
      src/assets/icons/refresh.svg
  34. 1 0
      src/assets/logo.svg
  35. 92 0
      src/components/Breadcrumb/index.vue
  36. 32 0
      src/components/LangSelect/index.vue
  37. 43 0
      src/components/SvgIcon/index.vue
  38. 63 0
      src/components/WangEditor/index.vue
  39. 40 0
      src/components/custom/custom-button-bar.vue
  40. 70 0
      src/components/custom/custom-desc.vue
  41. 197 0
      src/components/custom/custom-form.vue
  42. 150 0
      src/components/custom/custom-search-bar.vue
  43. 121 0
      src/components/custom/custom-table.vue
  44. 86 0
      src/components/custom/custom-upload.vue
  45. 7 0
      src/components/index.js
  46. 32 0
      src/lang/index.js
  47. 8 0
      src/lang/package/en.js
  48. 23 0
      src/lang/package/en/common.js
  49. 19 0
      src/lang/package/en/errorMessage.js
  50. 10 0
      src/lang/package/en/login.js
  51. 9 0
      src/lang/package/en/menus.js
  52. 6 0
      src/lang/package/en/navbar.js
  53. 55 0
      src/lang/package/en/pages.js
  54. 8 0
      src/lang/package/zh-cn.js
  55. 41 0
      src/lang/package/zh-cn/common.js
  56. 21 0
      src/lang/package/zh-cn/error.js
  57. 10 0
      src/lang/package/zh-cn/login.js
  58. 52 0
      src/lang/package/zh-cn/menus.js
  59. 6 0
      src/lang/package/zh-cn/navbar.js
  60. 696 0
      src/lang/package/zh-cn/pages.js
  61. 82 0
      src/layout/index.vue
  62. 84 0
      src/layout/parts/Header.vue
  63. 116 0
      src/layout/parts/Sidebar.vue
  64. 390 0
      src/layout/parts/Tagsbar.vue
  65. 48 0
      src/layout/parts/sidebar/items.vue
  66. 177 0
      src/layout/site.js
  67. 31 0
      src/main.js
  68. 123 0
      src/router/guard.js
  69. 82 0
      src/router/index.js
  70. 14 0
      src/router/modules/board.js
  71. 14 0
      src/router/modules/information.js
  72. 14 0
      src/router/modules/platform.js
  73. 14 0
      src/router/modules/system.js
  74. 14 0
      src/router/modules/user.js
  75. 120 0
      src/router/register.js
  76. 16 0
      src/settings.js
  77. 40 0
      src/store/api/log/opera.js
  78. 25 0
      src/store/api/login.js
  79. 40 0
      src/store/api/platform/achievement.js
  80. 40 0
      src/store/api/platform/demand.js
  81. 40 0
      src/store/api/platform/design.js
  82. 40 0
      src/store/api/platform/directory.js
  83. 40 0
      src/store/api/platform/footplate.js
  84. 49 0
      src/store/api/platform/friend.js
  85. 40 0
      src/store/api/platform/journal.js
  86. 40 0
      src/store/api/platform/match.js
  87. 40 0
      src/store/api/platform/news.js
  88. 40 0
      src/store/api/platform/notes.js
  89. 40 0
      src/store/api/platform/process.js
  90. 40 0
      src/store/api/platform/project.js
  91. 54 0
      src/store/api/platform/score.js
  92. 40 0
      src/store/api/platform/sign.js
  93. 40 0
      src/store/api/platform/supply.js
  94. 40 0
      src/store/api/platform/support.js
  95. 47 0
      src/store/api/system/dept.js
  96. 40 0
      src/store/api/system/dictData.js
  97. 40 0
      src/store/api/system/dictType.js
  98. 40 0
      src/store/api/system/menus.js
  99. 45 0
      src/store/api/system/message.js
  100. 0 0
      src/store/api/system/region.js

+ 19 - 0
.env.development

@@ -0,0 +1,19 @@
+## 开发环境
+NODE_ENV='development'
+# 是否启用加密,应与服务配合使用,否则会一端加密,一端不解密
+# VITE必须得写,不写就需要改默认配置.没必要
+VITE_USE_CRYPTO = false
+# 应用端口
+VITE_APP_PORT = 3000
+
+# 代理前缀
+VITE_APP_BASE_API = '/cxyy/api'
+
+VITE_APP_HOST = ""
+VITE_BASE_URL = "/cxyyAdmin"
+VITE_OUT_DIR = "cxyyAdmin"
+
+VITE_MQ_URL = 'ws://localhost:15674/ws'
+VITE_MQ_HOST = 'hxmsg'
+VITE_MQ_LOGIN = 'huaxin'
+VITE_MQ_PASSCODE = '1234qwerasdf'

+ 20 - 0
.env.production

@@ -0,0 +1,20 @@
+## 生产环境
+NODE_ENV='production'
+
+# 应用端口
+VITE_APP_PORT = 3000
+# 是否启用加密,应与服务配合使用,否则会一端加密,一端不解密
+VITE_USE_CRYPTO = false
+# 代理前缀
+VITE_APP_BASE_API = '/cxyy/api'
+
+VITE_APP_HOST = ""
+
+VITE_BASE_URL = "/cxyyAdmin"
+VITE_OUT_DIR = "cxyyAdmin"
+
+
+VITE_MQ_URL = '/ws'
+VITE_MQ_HOST = 'hxmsg'
+VITE_MQ_LOGIN = 'huaxin'
+VITE_MQ_PASSCODE = '1234qwerasdf'

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

@@ -0,0 +1,288 @@
+{
+  "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,
+    "useRouter": true,
+    "useI18n": 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: 200
+      }
+    ],
+    'prettier/prettier': [
+      'warn',
+      {
+        singleQuote: true,
+        bracketSpacing: true,
+        jsxBracketSameLine: true,
+        printWidth: 200
+      }
+    ]
+  }
+}

+ 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*
+cxyyAdmin*
+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": 400,
+  "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"
+  ]
+}

+ 6 - 0
README.md

@@ -0,0 +1,6 @@
+# web-template-vue3-js
+
+## 1.目录设置
+### 1.1 功能列表
+> 前端:主要是控制角色是否可以显示按钮
+> 服务:主要是用同一编码对接口的使用进行控制

+ 24 - 0
index.html

@@ -0,0 +1,24 @@
+<!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>产学研用协同创新数字化平台</title>
+  </head>
+  <body>
+    <div id="app" class="app"></div>
+    <script type="module" src="/src/main.js"></script>
+  </body>
+</html>
+<style>
+  body {
+    margin: 0;
+  }
+  #app {
+    height: 100vh;
+    width: 100vw;
+  }
+</style>
+</body>
+</html>

+ 10 - 0
jsconfig.json

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

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 11586 - 0
package-lock.json


+ 53 - 0
package.json

@@ -0,0 +1,53 @@
+{
+  "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",
+    "@stomp/stompjs": "^7.0.0",
+    "@vueuse/core": "^10.7.2",
+    "@vueuse/integrations": "^10.9.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "5.1.10",
+    "axios": "^1.6.7",
+    "crypto-js": "^4.2.0",
+    "echarts": "^5.5.0",
+    "element-plus": "^2.8.1",
+    "jsencrypt": "^3.3.2",
+    "lodash-es": "^4.17.21",
+    "moment": "^2.30.1",
+    "nprogress": "^0.2.0",
+    "path-browserify": "^1.0.1",
+    "path-to-regexp": "^6.2.1",
+    "pinia": "^2.1.7",
+    "stompjs": "^2.3.3",
+    "universal-cookie": "^7.1.0",
+    "vue": "^3.4.15",
+    "vue-i18n": "^9.9.1",
+    "vue-router": "^4.2.5",
+    "vue3-seamless-scroll": "^2.0.1"
+  },
+  "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",
+    "vite-plugin-svg-icons": "^2.0.1"
+  }
+}

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 4069 - 0
pnpm-lock.yaml


BIN
public/favicon.ico


BIN
public/images/401.gif


BIN
public/images/404.png


BIN
public/images/404_cloud.png


BIN
public/images/login-bg.jpg


BIN
public/images/logo-jilinbai.png


BIN
public/images/logo.png


BIN
public/images/video.mp4


BIN
public/logo.png


+ 1 - 0
public/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>

BIN
public/导入模板 - 副本.xlsx


BIN
public/导入模板.xlsx


+ 23 - 0
src/App.vue

@@ -0,0 +1,23 @@
+<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">
+#index {
+  width: 100%;
+  height: 100%;
+}
+.textOne {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>

+ 1 - 0
src/assets/icons/close.svg

@@ -0,0 +1 @@
+ <svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M19.41 18l8.29-8.29a1 1 0 0 0-1.41-1.41L18 16.59l-8.29-8.3a1 1 0 0 0-1.42 1.42l8.3 8.29l-8.3 8.29A1 1 0 1 0 9.7 27.7l8.3-8.29l8.29 8.29a1 1 0 0 0 1.41-1.41z" fill="currentColor"></path></svg>

+ 1 - 0
src/assets/icons/close_all.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 36 36"><path d="M26 17H10a1 1 0 0 0 0 2h16a1 1 0 0 0 0-2z" fill="currentColor"></path></svg>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/assets/icons/close_left.svg


+ 1 - 0
src/assets/icons/close_other.svg

@@ -0,0 +1 @@
+<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" aria-hidden="true" role="img" width="1em" height="1em" preserveAspectRatio="xMidYMid meet" viewBox="0 0 20 20"><path d="M3 5h14V3H3v2zm12 8V7H5v6h10zM3 17h14v-2H3v2z" fill="currentColor"></path></svg>

Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/assets/icons/close_right.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/assets/icons/language.svg


Những thai đổi đã bị hủy bỏ vì nó quá lớn
+ 1 - 0
src/assets/icons/refresh.svg


+ 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>

+ 92 - 0
src/components/Breadcrumb/index.vue

@@ -0,0 +1,92 @@
+<template>
+  <el-breadcrumb class="h-[50px] flex items-center">
+    <transition-group name="breadcrumb-transition">
+      <el-breadcrumb-item v-for="(item, index) in breadcrumbs" :key="item.path">
+        <span v-if="item.redirect === 'noredirect' || index === breadcrumbs.length - 1" class="text-[var(--el-disabled-text-color)]">
+          {{ translateRouteTitle(item.meta.title) }}
+        </span>
+        <a v-else @click.prevent="handleLink(item)">
+          {{ translateRouteTitle(item.meta.title) }}
+        </a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script setup>
+import { onBeforeMount, ref, watch } from 'vue'
+import { useRoute } from 'vue-router'
+import { compile } from 'path-to-regexp'
+import { translateRouteTitle } from '@/utils/i18n'
+import router from '@/router'
+
+const currentRoute = useRoute()
+const pathCompile = (path) => {
+  const { params } = currentRoute
+  const toPath = compile(path)
+  return toPath(params)
+}
+
+const breadcrumbs = ref([])
+
+function getBreadcrumb() {
+  let matched = currentRoute.matched.filter((item) => item.meta && item.meta.title)
+  const first = matched[0]
+  if (!isDashboard(first)) {
+    matched = [{ path: '/', meta: { title: 'home' } }].concat(matched)
+  }
+  breadcrumbs.value = matched.filter((item) => {
+    return item.meta && item.meta.title && item.meta.breadcrumb !== false
+  })
+}
+
+function isDashboard(route) {
+  const name = route && route.name
+  if (!name) {
+    return false
+  }
+  return name.toString().trim().toLocaleLowerCase() === 'home'.toLocaleLowerCase()
+}
+
+function handleLink(item) {
+  const { redirect, path, meta } = item
+  if (meta.type == '0') return
+  if (redirect) {
+    router.push(redirect).catch((err) => {
+      console.warn(err)
+    })
+    return
+  }
+  router.push(pathCompile(path)).catch((err) => {
+    console.warn(err)
+  })
+}
+
+watch(
+  () => currentRoute.path,
+  (path) => {
+    if (path.startsWith('/redirect/')) {
+      return
+    }
+    getBreadcrumb()
+  }
+)
+
+onBeforeMount(() => {
+  getBreadcrumb()
+})
+</script>
+<style lang="scss" scoped>
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  margin-left: 8px;
+  font-size: 14px;
+  line-height: 50px;
+}
+
+// 覆盖 element-plus 的样式
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+  font-weight: 400 !important;
+}
+</style>

+ 32 - 0
src/components/LangSelect/index.vue

@@ -0,0 +1,32 @@
+<script setup>
+// 组件
+import { useI18n } from 'vue-i18n'
+import { useAppStore } from '@/store/modules/app'
+
+const appStore = useAppStore()
+const { locale } = useI18n()
+
+function handleLanguageChange(lang) {
+  locale.value = lang
+  appStore.changeLanguage(lang)
+  if (lang === 'en-us') {
+    ElMessage.success('Switch Language Successful!')
+  } else {
+    ElMessage.success('切换语言成功!')
+  }
+}
+</script>
+
+<template>
+  <el-dropdown trigger="click" @command="handleLanguageChange">
+    <div>
+      <SvgIcon icon-class="language"></SvgIcon>
+    </div>
+    <template #dropdown>
+      <el-dropdown-menu>
+        <el-dropdown-item :disabled="appStore.language === 'zh-cn'" command="zh-cn">中文</el-dropdown-item>
+        <el-dropdown-item :disabled="appStore.language === 'en-us'" command="en-us"> English</el-dropdown-item>
+      </el-dropdown-menu>
+    </template>
+  </el-dropdown>
+</template>

+ 43 - 0
src/components/SvgIcon/index.vue

@@ -0,0 +1,43 @@
+<template>
+  <svg aria-hidden="true" class="svg-icon" :style="'width:' + size + ';height:' + size">
+    <use :xlink:href="symbolId" :fill="color" />
+  </svg>
+</template>
+
+<script setup>
+const props = defineProps({
+  prefix: {
+    type: String,
+    default: 'icon'
+  },
+  iconClass: {
+    type: String,
+    required: false,
+    default: ''
+  },
+  color: {
+    type: String,
+    default: ''
+  },
+  size: {
+    type: String,
+    default: '1em'
+  }
+})
+
+const symbolId = computed(() => `#${props.prefix}-${props.iconClass}`)
+</script>
+
+<style scoped>
+.svg-icon {
+  display: inline-block;
+  width: 1em;
+  height: 1em;
+  overflow: hidden;
+  vertical-align: -0.15em;
+  /* 因icon大小被设置为和字体大小一致,而span等标签的下边缘会和字体的基线对齐,故需设置一个往下的偏移比例,来纠正视觉上的未对齐效果 */
+  outline: none;
+  fill: currentcolor;
+  /* 定义元素的颜色,currentColor是一个变量,这个变量的值就表示当前元素的color值,如果当前元素未设置color值,则从父元素继承 */
+}
+</style>

+ 63 - 0
src/components/WangEditor/index.vue

@@ -0,0 +1,63 @@
+<template>
+  <div class="editor-wrapper" style="width: 100%; border: 1px solid #dcdfe6; border-radius: 5px">
+    <!-- 工具栏 -->
+    <Toolbar id="toolbar-container" :editor="editorRef" :default-config="toolbarConfig" :mode="mode" />
+    <!-- 编辑器 -->
+    <Editor style="height: 350px" id="editor-container" v-model="modelValue" :default-config="editorConfig" :mode="mode" @on-change="handleChange" @on-created="handleCreated" />
+  </div>
+</template>
+
+<script setup>
+import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
+// API 引用
+import { uploadFileApi } from '@/utils/file'
+
+const props = defineProps({
+  modelValue: {
+    type: [String],
+    default: ''
+  }
+})
+
+const emit = defineEmits(['update:modelValue'])
+
+const modelValue = useVModel(props, 'modelValue', emit)
+
+const editorRef = shallowRef() // 编辑器实例,必须用 shallowRef
+const mode = ref('default') // 编辑器模式
+const toolbarConfig = ref({}) // 工具条配置
+// 编辑器配置
+const editorConfig = ref({
+  placeholder: '请输入内容...',
+  MENU_CONF: {
+    uploadImage: {
+      // 自定义图片上传
+      async customUpload(file, insertFn) {
+        uploadFileApi(file).then((response) => {
+          const { errcode, uri } = response.data
+          const url = `${import.meta.env.VITE_APP_HOST}${uri}`
+          if (errcode === 0) insertFn(url)
+        })
+      }
+    }
+  }
+})
+
+const handleCreated = (editor) => {
+  editorRef.value = editor // 记录 editor 实例,重要!
+}
+
+function handleChange(editor) {
+  modelValue.value = editor.getHtml()
+}
+
+// 组件销毁时,也及时销毁编辑器
+onBeforeUnmount(() => {
+  const editor = editorRef.value
+  if (editor == null) return
+  editor.destroy()
+})
+</script>
+
+<style src="@wangeditor/editor/dist/css/style.css"></style>

+ 40 - 0
src/components/custom/custom-button-bar.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-row style="padding: 5px">
+    <el-col :span="24" style="text-align: right">
+      <template v-for="b in fields" :key="b.method">
+        <el-button v-method="b.method" :type="getType(b)" @click="toClick(b)" :disabled="getDisabled(b)">{{ b.label }}</el-button>
+      </template>
+    </el-col>
+  </el-row>
+</template>
+
+<script setup>
+import { get, isFunction } from 'lodash-es'
+const { t } = useI18n()
+const props = defineProps({
+  fields: { type: Array, default: () => [] }
+})
+const emits = defineEmits([])
+const getType = (field) => {
+  return get(field, 'type', 'primary')
+}
+const toClick = (field) => {
+  const method = get(field, 'method')
+  if (!method) {
+    ElMessage({ type: 'error', message: t('common.no_method') })
+    return
+  }
+  emits(method)
+}
+const getDisabled = (field) => {
+  const disabled = get(field, 'disabled')
+  if (!disabled) return false
+  if (isFunction(disabled)) return disabled(field)
+  return disabled
+}
+</script>
+<style scoped>
+.el-button {
+  margin-top: 5px;
+}
+</style>

+ 70 - 0
src/components/custom/custom-desc.vue

@@ -0,0 +1,70 @@
+<template>
+  <el-descriptions :column="column" size="large" border>
+    <el-descriptions-item v-for="f in fields" :key="f.model" :label="f.label" align="center" label-align="center" :span="getSpan(f)">
+      <template v-if="f.type === 'image'">
+        <el-row>
+          <el-col :span="6" v-for="(i, index) in getArrayModel(f.model)">
+            <el-image :src="i.uri" :preview-src-list="getArrayModel(f.model)" :initial-index="index"></el-image>
+          </el-col>
+        </el-row>
+      </template>
+      <template v-else-if="f.type === 'editor'">
+        <span v-html="form[f.model]"></span>
+      </template>
+      <template v-else>
+        {{ getLabel(f) }}
+      </template>
+    </el-descriptions-item>
+  </el-descriptions>
+</template>
+
+<script setup>
+import { get, isFunction, isArray } from 'lodash-es'
+/**
+ * fields:
+ * [
+ *  {
+ *    label:'字段中文',
+ *    model:'字段名',
+ *    format:Function 格式化输出内容函数 可选,
+ *    span: 占用列 可选 默认1,
+ *  }
+ * ]
+ */
+const props = defineProps({
+  modelValue: { type: Object },
+  fields: { type: Array, default: () => [] },
+  column: { type: Number, default: 3 },
+  useSave: { type: Boolean, default: true },
+  span: { type: Number, default: 24 } // 限制两侧的距离,24就是整行全用
+})
+const emits = defineEmits(['update:modelValue'])
+const form = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    console.log(value)
+    emits('update:modelValue', value)
+  }
+})
+const getSpan = (field) => {
+  const span = get(field, 'span', 1)
+  return span
+}
+const getLabel = (field) => {
+  const format = get(field, 'format')
+  if (!format || !isFunction(format)) return get(form, `value.${field.model}`)
+  const val = get(form, `value.${field.model}`)
+  if (!val) return
+  const fval = format(val)
+  if (fval) return fval
+}
+
+const getArrayModel = (modelName) => {
+  let data = get(form, `value.${modelName}`, [])
+  if (!isArray(data)) data = []
+  return data
+}
+</script>
+<style scoped></style>

+ 197 - 0
src/components/custom/custom-form.vue

@@ -0,0 +1,197 @@
+<template>
+  <div id="custom-form">
+    <el-form ref="formRef" :model="form" :rules="rules" :label-width="labelWidth" class="form" @submit.prevent :disabled="disabled">
+      <el-col :span="span" v-for="(item, index) in fields" :key="index">
+        <el-form-item v-if="display(item)" :key="`form-field-${item.model}`" :label="getField('label', item)" :prop="item.model" :required="item.required">
+          <template v-if="item.custom">
+            <slot :name="item.model" v-bind="{ item }"></slot>
+          </template>
+          <template v-else>
+            <template v-if="item.type === 'textarea'">
+              <el-input
+                clearable
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('placeholder', item)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                show-word-limit
+              ></el-input>
+            </template>
+            <template v-else-if="item.type === 'numbers'">
+              <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" @change="dataChange(item.model)" style="width: 100%" />
+            </template>
+            <template v-else-if="item.type === 'radio'">
+              <el-radio-group v-model="form[item.model]" :type="item.type" v-bind="item.options" @change="dataChange(item.model)">
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-radio-group>
+            </template>
+            <template v-else-if="item.type === 'checkbox'">
+              <el-checkbox-group v-model="form[item.model]" :type="item.type" v-bind="item.options">
+                <slot :name="item.model" v-bind="{ item }"></slot>
+              </el-checkbox-group>
+            </template>
+            <template v-else-if="item.type === 'select'">
+              <el-tooltip effect="dark" content="可输入文本搜索选项" placement="top-start">
+                <el-select
+                  clearable
+                  filterable
+                  allow-create
+                  default-first-option
+                  v-model="form[item.model]"
+                  :type="item.type"
+                  :placeholder="getField('selectplaceholder', item)"
+                  v-bind="item.options"
+                  @change="dataChange(item.model)"
+                  style="width: 100%"
+                >
+                  <slot :name="item.model" v-bind="{ item }"></slot>
+                </el-select>
+              </el-tooltip>
+            </template>
+            <template v-else-if="item.type === 'selectMany'">
+              <el-tooltip effect="dark" content="可输入文本搜索选项" placement="top-start">
+                <el-select
+                  filterable
+                  clearable
+                  multiple
+                  collapse-tags
+                  v-model="form[item.model]"
+                  :type="item.type"
+                  :placeholder="getField('selectplaceholder', item)"
+                  v-bind="item.options"
+                  @change="dataChange(item.model)"
+                  style="width: 100%"
+                >
+                  <slot :name="item.model" v-bind="{ item }"></slot>
+                </el-select>
+              </el-tooltip>
+            </template>
+            <template v-else-if="item.type === `year` || item.type == 'month' || item.type == 'date' || item.type == 'daterange' || item.type == 'datetime' || item.type == 'datetimerange'">
+              <el-date-picker
+                v-model="form[item.model]"
+                :type="item.type"
+                :placeholder="getField('selectplaceholder', item)"
+                :format="getDateFormat(item.type)"
+                :value-format="getDateFormat(item.type)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                range-separator="至"
+                style="width: 100%"
+              >
+              </el-date-picker>
+            </template>
+            <template v-else-if="item.type === `time`">
+              <el-time-picker
+                v-model="form[item.model]"
+                :placeholder="getField('selectplaceholder', item)"
+                :format="getDateFormat(item.type)"
+                :value-format="getDateFormat(item.type)"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+                style="width: 100%"
+              >
+              </el-time-picker>
+            </template>
+            <template v-else-if="item.type === `inputnumber`">
+              <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" v-bind="item.options" @change="dataChange(item.model)" style="width: 100%"></el-input-number>
+            </template>
+            <template v-else>
+              <el-input
+                clearable
+                v-model="form[item.model]"
+                :type="getField('type', item)"
+                :placeholder="getField('placeholder', item)"
+                :show-password="getField('type', item) === 'password'"
+                v-bind="item.options"
+                @change="dataChange(item.model)"
+              ></el-input>
+            </template>
+          </template>
+        </el-form-item>
+      </el-col>
+      <el-col :span="24" label="" class="btn" v-if="useSave">
+        <slot name="submit">
+          <el-button type="primary" @click="save(formRef)">{{ submitText || submitTextDefault }}</el-button>
+        </slot>
+      </el-col>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+import { get, isFunction } from 'lodash-es'
+const { t } = useI18n()
+const submitTextDefault = t('common.save')
+const props = defineProps({
+  modelValue: { type: Object },
+  rules: { type: Array, default: () => {} },
+  labelWidth: { type: String, default: 'auto' },
+  disabled: { type: Boolean, default: false },
+  fields: { type: Array, default: () => [] },
+  submitText: { type: String },
+  useSave: { type: Boolean, default: true },
+  span: { type: Number, default: 24 } // 限制两侧的距离,24就是整行全用
+})
+const emits = defineEmits(['update:modelValue', 'dataChange', 'save'])
+const formRef = ref()
+const form = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    console.log(value)
+    emits('update:modelValue', value)
+  }
+})
+const save = async (formEl) => {
+  if (!formEl) return
+  await formEl.validate((valid, fields) => {
+    if (valid) {
+      emits('save', form.value)
+    } else {
+      console.log('error submit!', fields)
+    }
+  })
+}
+const getField = (item, data) => {
+  let res = get(data, item, null)
+  if (item === 'type') res = res === null ? `text` : res
+  if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res
+  if (item === `selectplaceholder`) res = res === null ? `请选择${data.label}` : res
+  if (item === 'required') res = res === null ? false : res
+  if (item === `error`) res = res === null ? `${data.label}错误` : res
+  return res
+}
+const dataChange = (model) => {
+  const value = form.value[model]
+  emits('dataChange', { model, value })
+}
+const display = (field) => {
+  let dis = get(field, `display`)
+  if (!isFunction(dis)) return true
+  else {
+    return dis(field, form)
+  }
+}
+const getDateFormat = (e) => {
+  if (e === 'year') return 'YYYY'
+  if (e === 'month') return 'MM'
+  if (e === 'date') return 'YYYY-MM-DD'
+  if (e === 'daterange') return 'YYYY-MM-DD'
+  if (e === 'datetime') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'datetimerange') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'time') return 'HH:mm:ss'
+}
+</script>
+<style scoped>
+.btn {
+  text-align: center;
+}
+
+.form {
+  display: flex;
+  flex-direction: row;
+  flex-wrap: wrap;
+}
+</style>

+ 150 - 0
src/components/custom/custom-search-bar.vue

@@ -0,0 +1,150 @@
+<template>
+  <div id="custom-search-bar">
+    <el-form ref="formRef" :model="form" :label-width="labelWidth" class="form" @submit.prevent :inline="true">
+      <el-form-item v-for="item in fields" :key="`form-field-${item.model}`" :label="getField('label', item)" :prop="item.model" :required="item.required">
+        <template v-if="item.custom">
+          <slot :name="item.model" v-bind="{ item }"></slot>
+        </template>
+        <template v-else>
+          <template v-if="item.type === 'numbers'">
+            <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" @change="dataChange(item.model)" style="width: 100%" />
+          </template>
+          <template v-else-if="item.type === 'radio'">
+            <el-radio-group v-model="form[item.model]" :type="item.type" v-bind="item.options" @change="dataChange(item.model)">
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-radio-group>
+          </template>
+          <template v-else-if="item.type === 'checkbox'">
+            <el-checkbox-group v-model="form[item.model]" :type="item.type" v-bind="item.options">
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-checkbox-group>
+          </template>
+          <template v-else-if="item.type === 'select'">
+            <el-select
+              clearable
+              filterable
+              allow-create
+              default-first-option
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%; min-width: 200px"
+            >
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-select>
+          </template>
+          <template v-else-if="item.type === 'selectMany'">
+            <el-select
+              filterable
+              clearable
+              multiple
+              collapse-tags
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%"
+            >
+              <slot :name="item.model" v-bind="{ item }"></slot>
+            </el-select>
+          </template>
+          <template v-else-if="item.type === `year` || item.type == 'month' || item.type == 'date' || item.type == 'daterange' || item.type == 'datetime' || item.type == 'datetimerange'">
+            <el-date-picker
+              v-model="form[item.model]"
+              :type="item.type"
+              :placeholder="getField('selectplaceholder', item)"
+              :format="getDateFormat(item.type)"
+              :value-format="getDateFormat(item.type)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              range-separator="至"
+              style="width: 100%"
+            >
+            </el-date-picker>
+          </template>
+          <template v-else-if="item.type === `time`">
+            <el-time-picker
+              v-model="form[item.model]"
+              :placeholder="getField('selectplaceholder', item)"
+              :format="getDateFormat(item.type)"
+              :value-format="getDateFormat(item.type)"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+              style="width: 100%"
+            >
+            </el-time-picker>
+          </template>
+          <template v-else-if="item.type === `inputnumber`">
+            <el-input-number v-model="form[item.model]" :placeholder="getField('placeholder', item)" v-bind="item.options" @change="dataChange(item.model)" style="width: 100%"></el-input-number>
+          </template>
+          <template v-else>
+            <el-input
+              clearable
+              v-model="form[item.model]"
+              :type="getField('type', item)"
+              :placeholder="getField('placeholder', item)"
+              :show-password="getField('type', item) === 'password'"
+              v-bind="item.options"
+              @change="dataChange(item.model)"
+            ></el-input>
+          </template>
+        </template>
+      </el-form-item>
+      <el-form-item v-if="fields.length > 0">
+        <el-button type="primary" @click="toClick">{{ $t('common.search') }}</el-button>
+        <el-button type="default" @click="toReset">{{ $t('common.reset') }}</el-button>
+      </el-form-item>
+    </el-form>
+  </div>
+</template>
+
+<script setup>
+import { get } from 'lodash-es'
+const props = defineProps({
+  modelValue: { type: Object },
+  fields: { type: Array, default: () => [] }
+})
+const emits = defineEmits(['update:modelValue', 'dataChange', 'search'])
+const formRef = ref()
+const form = computed({
+  get() {
+    return props.modelValue
+  },
+  set(value) {
+    console.log(value)
+    emits('update:modelValue', value)
+  }
+})
+const toClick = () => {
+  emits('search')
+}
+const toReset = () => {
+  emits('reset')
+}
+const getField = (item, data) => {
+  let res = get(data, item, null)
+  if (item === 'type') res = res === null ? `text` : res
+  if (item === 'placeholder') res = res === null ? `请输入${data.label}` : res
+  if (item === `selectplaceholder`) res = res === null ? `请选择${data.label}` : res
+  if (item === 'required') res = res === null ? false : res
+  if (item === `error`) res = res === null ? `${data.label}错误` : res
+  return res
+}
+const dataChange = (model) => {
+  const value = form.value[model]
+  emits('dataChange', { model, value })
+}
+const getDateFormat = (e) => {
+  if (e === 'year') return 'YYYY'
+  if (e === 'month') return 'MM'
+  if (e === 'date') return 'YYYY-MM-DD'
+  if (e === 'daterange') return 'YYYY-MM-DD'
+  if (e === 'datetime') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'datetimerange') return 'YYYY-MM-DD HH:mm:ss'
+  if (e === 'time') return 'HH:mm:ss'
+}
+</script>
+<style scoped></style>

+ 121 - 0
src/components/custom/custom-table.vue

@@ -0,0 +1,121 @@
+<template>
+  <el-row>
+    <el-col>
+      <el-table :data="data" border :height="height" @selection-change="toSelect">
+        <el-table-column type="selection" width="55" v-if="select"> </el-table-column>
+        <template v-for="f in fields" :key="f.model">
+          <el-table-column v-if="f.custom" :label="f.label" :prop="f.model" align="center" v-bind="f.options">
+            <template v-slot="{ row }">
+              <slot :name="f.model" v-bind="{ f, row }"></slot>
+            </template>
+          </el-table-column>
+          <el-table-column v-else :label="f.label" :prop="f.model" align="center" :formatter="toFormatter"></el-table-column>
+        </template>
+        <el-table-column :label="$t('common.opera')" align="center" v-if="opera.length > 0">
+          <template v-slot="{ row, $index }">
+            <slot v-bind="{ row }">
+              <template v-for="f in opera">
+                <template v-if="display(f, row)">
+                  <el-link :key="f.method" :type="f.type || 'primary'" size="small" :underline="false" class="link" v-method="f.method" @click="handleOpera(f, row, $index)">
+                    {{ f.label }}
+                  </el-link>
+                </template>
+              </template>
+            </slot>
+          </template>
+        </el-table-column>
+      </el-table>
+    </el-col>
+  </el-row>
+  <el-row justify="end">
+    <el-pagination
+      background
+      layout="total, prev, pager, next"
+      :page-sizes="[10, 20, 50, 100, 200]"
+      :total="total"
+      :page-size="limit"
+      v-model:current-page="currentPage"
+      @current-change="changePage"
+      @size-change="sizeChange"
+    >
+    </el-pagination>
+  </el-row>
+</template>
+<script setup>
+import { isFunction, get, isString, cloneDeep } from 'lodash-es'
+const props = defineProps({
+  data: { type: Array, default: () => [] },
+  height: { type: String, default: '55vh' },
+  fields: { type: Array, default: () => [] },
+  opera: { type: Array, default: () => [] },
+  total: { type: Number, default: 0 },
+  limit: { type: Number, default: 10 },
+  select: { type: Boolean, default: false }
+})
+const emit = defineEmits(['query', 'toSelect'])
+const toSelect = (val) => {
+  emit(`toSelect`, val)
+}
+
+const handleOpera = (field, data, index) => {
+  let { method, confirm = false, methodZh, label, confirmWord } = cloneDeep(field)
+  if (isFunction(methodZh)) methodZh = methodZh(data)
+  else if (isString(methodZh)) {
+    methodZh = label
+  }
+  if (confirm) {
+    let word = methodZh ? methodZh : `您确认${label}该数据?`
+    if (confirmWord) word = confirmWord
+    ElMessageBox.confirm(word, '提示', { confirmButtonText: '确定', cancelButtonText: '取消', type: 'warning' })
+      .then(() => {
+        emit(method, cloneDeep(data), index)
+      })
+      .catch(() => {})
+  } else emit(method, cloneDeep(data), index)
+}
+
+/**
+ * 根据field中的 format函数 格式化该单元格数据
+ * @param {Object} row 本行数据
+ * @param {Object} column 本列实例
+ * @param {any} cellValue 该单元格原数据
+ */
+const toFormatter = (row, column, cellValue) => {
+  // 先找到field
+  const fields = get(props, 'fields')
+  if (!fields) return cellValue
+  let this_field = fields.find((fil) => fil.model === column.property)
+  if (!this_field) return cellValue
+  // 再找field中format函数
+  let format = get(this_field, `format`, false)
+  if (!format) return cellValue
+  if (isFunction(format)) {
+    const formatResult = format(cellValue, row, this_field)
+    return formatResult
+  }
+}
+const display = (field, row) => {
+  let display = get(field, `display`, true)
+  if (display === true) return true
+  else {
+    let res = display(row)
+    return res
+  }
+}
+const currentPage = ref(1)
+// 分页
+const changePage = (page = currentPage.value) => {
+  emit('query', { skip: (page - 1) * props.limit, limit: props.limit })
+}
+</script>
+<style scoped>
+.el-row {
+  padding-top: 20px;
+}
+.link {
+  padding: 0 5px 0 0;
+}
+.page {
+  margin: 10px 0 0 0;
+}
+</style>

+ 86 - 0
src/components/custom/custom-upload.vue

@@ -0,0 +1,86 @@
+<template>
+  <div id="c-upload">
+    <el-upload v-if="url" ref="upload" :action="url" :limit="limit" :accept="accept" :file-list="list" :list-type="listType" :on-exceed="outLimit" :on-preview="filePreview" :on-success="onSuccess" :before-remove="onRemove" :show-file-list="showList">
+      <el-button type="primary">{{ $t('common.upload_btn') }}</el-button>
+      <template #tip v-if="tip">
+        <p style="color: #ff0000">{{ tip }}</p>
+      </template>
+    </el-upload>
+    <el-dialog v-model="dialog.show" append-to-body>
+      <img width="100%" :src="dialog.url" alt="" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { ElMessage } from 'element-plus'
+import { omit, cloneDeep, isArray } from 'lodash-es'
+let dialog = ref({ show: false, url: '' })
+const props = defineProps({
+  url: { type: String, default: () => '' },
+  limit: { type: Number, default: () => 6 },
+  accept: { type: String, default: () => 'image/png, image/jpeg' },
+  listType: { type: String, default: () => 'text' }, //'text' | 'picture' | 'picture-card'
+  tip: { type: String, default: () => undefined },
+  list: { type: Array, default: () => [] },
+  model: { type: String, default: () => '' },
+  showList: { type: Boolean, default: true }
+})
+// 图片上传地址
+const { url } = toRefs(props)
+// 可上传文件数目
+const { limit } = toRefs(props)
+// 接收上传的文件类型
+const { accept } = toRefs(props)
+// 文件列表的类型--picture-card---picture
+const { listType } = toRefs(props)
+// 文件提醒
+const { tip } = toRefs(props)
+// 已有数据,赋值,预览
+const { list } = toRefs(props)
+const { model } = toRefs(props)
+// const list = ref<UploadUserFile[]>([]);
+
+const emit = defineEmits(['change'])
+// 图片预览
+const filePreview = (file) => {
+  // this.dialog = { show: true, url: file.url };
+  window.open(file.url)
+}
+// 只允许上传多少个文件
+const outLimit = () => {
+  ElMessage.error(`只允许上传${limit.value}个文件`)
+}
+// 上传成功,response:成功信息,file:图片信息,fileList:图片列表
+const onSuccess = (response, file) => {
+  console.log(response)
+  if (response.errcode !== 0) {
+    ElMessage({ type: 'error', message: '删除成功' })
+    return
+  }
+  let ponse = omit(response, ['errcode', 'errmsg'])
+  let arr = cloneDeep(list)
+  if (isArray(list.value)) {
+    arr.value.push({ ...ponse, name: file.name, url: `${response.uri}` })
+  } else {
+    arr.value = [{ ...ponse, name: file.name, url: `${response.uri}` }]
+  }
+  emit('change', { model: model.value, value: arr.value })
+}
+// 删除图片
+// file: { id: any; uri: string }, fileList: any
+const onRemove = () => {
+  // let arr: Ref<ListItem[]> = _.cloneDeep(list);
+  // let info = arr.value.filter((f) => f.id != file.id);
+  // emit('change', info);
+  return true
+}
+
+// #endregion
+</script>
+
+<style lang="scss" scoped>
+#c-upload {
+  width: 100%;
+}
+</style>

+ 7 - 0
src/components/index.js

@@ -0,0 +1,7 @@
+export default function globalComponents(app) {
+  const components = import.meta.glob('./**/**.{vue,tsx}', { eager: true }) //获取文件夹及其嵌套的多级子文件夹
+  for (let [key, value] of Object.entries(components)) {
+    const name = key.replace('./', '').split('/')[0]
+    app.component(value.default.name || name, value.default)
+  }
+}

+ 32 - 0
src/lang/index.js

@@ -0,0 +1,32 @@
+import { createI18n } from 'vue-i18n'
+import { useCookies } from '@vueuse/integrations/useCookies'
+import defaultSettings from '@/settings'
+const cookies = useCookies()
+// 本地语言包
+// tm('key') 取ref对象 再用toRaw转换成普通对象
+// t('key.target')取值
+import enLocale from './package/en'
+import zhCnLocale from './package/zh-cn'
+const messages = {
+  'zh-cn': {
+    ...zhCnLocale
+  },
+  'en-us': {
+    ...enLocale
+  }
+}
+
+let lang = cookies.get('locale')
+if (!lang) {
+  lang = defaultSettings.language
+  cookies.set('locale', defaultSettings.language, { path: '/' })
+}
+
+const i18n = createI18n({
+  legacy: false,
+  locale: lang,
+  messages: messages,
+  globalInjection: true
+})
+
+export default i18n

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

@@ -0,0 +1,8 @@
+const allModules = import.meta.glob('./en/*.js', { eager: true })
+const keys = Object.keys(allModules)
+const packages = {}
+for (const key of keys) {
+  const name = key.substring(key.lastIndexOf('/') + 1, key.lastIndexOf('.js'))
+  packages[name] = allModules[key].default
+}
+export default packages

+ 23 - 0
src/lang/package/en/common.js

@@ -0,0 +1,23 @@
+export default {
+  opera: 'opera',
+  add: 'add',
+  update: 'update',
+  delete: 'delete',
+  delete_confirm: 'Are you sure you want to delete this data?',
+  search: 'search',
+  view: 'view',
+  save: 'save',
+  submit: 'submit',
+  is_use_abled: 'abled',
+  is_use_disabled: 'disabled',
+  yes: 'yes',
+  no: 'no',
+  no_method: 'The function is not yet open!',
+  warning: 'warning',
+  confirm: 'confirm',
+  cancel: 'cancel',
+  user_confirm: 'User confirmation',
+  re_login: 'Login again',
+  opera_success: 'opera success',
+  opera_fail: 'opera fail'
+}

+ 19 - 0
src/lang/package/en/errorMessage.js

@@ -0,0 +1,19 @@
+export default {
+  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'
+}

+ 10 - 0
src/lang/package/en/login.js

@@ -0,0 +1,10 @@
+// 登录页面国际化
+export default {
+  title: 'Information Technology Incubation Platform',
+  username: 'Username',
+  password: 'Password',
+  login: 'Login',
+  captchaCode: 'Verify Code',
+  placeholder1: 'please select your username',
+  placeholder2: 'please select your password'
+}

+ 9 - 0
src/lang/package/en/menus.js

@@ -0,0 +1,9 @@
+export default {
+  home: 'home',
+  system: 'system',
+  system_menus: 'system_menus',
+  system_role: 'system_role',
+  system_parameter: 'system_parameter',
+  system_dict: 'system_dict',
+  system_dict_data: 'system_dictData'
+}

+ 6 - 0
src/lang/package/en/navbar.js

@@ -0,0 +1,6 @@
+// 导航栏国际化
+export default {
+  dashboard: 'Dashboard',
+  logout: 'Logout',
+  my: 'Personal Center'
+}

+ 55 - 0
src/lang/package/en/pages.js

@@ -0,0 +1,55 @@
+export default {
+  menus: {
+    dialogTitle: '目录信息',
+    is_default: '是否默认',
+    name: '目录名称',
+    namePh: '请填写目录名称',
+    route_name: '路由名称',
+    route_namePh: '请填写路由名称',
+    i18n_code: '国际化编码',
+    i18n_codePh: '请填写国际化编码并确保与国际化文件中的编码一致',
+    parentName: '父级目录',
+    icon: '图标',
+    iconPh: '请选择图标',
+    order_num: '顺序',
+    path: '路由地址',
+    pathPh: '请填写路由地址',
+    component: '组件地址',
+    componentPh: '请填写组件地址',
+    type: '目录类型',
+    typePh: '请选择目录类型',
+    is_use: '状态',
+    remark: '备注',
+    remarkPh: '请输入备注',
+    addNext: '添加下一级',
+    baseInfo: '基本信息',
+    configInfo: '功能列表',
+    config_zh: '功能说明',
+    config_code: '功能编码',
+    add_config: '添加功能'
+  },
+  role: {
+    dialogTitle: '角色信息',
+    name: '角色名称',
+    namePh: '请填写角色名称',
+    code: '角色代码',
+    codePh: '请填写角色代码',
+    brief: '简介',
+    menu: '权限配置',
+    is_use: '是否启用'
+  },
+  admin: {
+    dialogTitle: '用户信息',
+    account: '账号',
+    nick_name: '名称',
+    role: '角色',
+    is_super: '是否是超级管理员',
+    is_use: '是否启用',
+    bind: '绑定用户',
+    rp: '重置密码',
+    rpConfirm: '您确定要重置密码?',
+    changeToAbled: '您确定要启用该用户?',
+    changeToDisabled: '您确定要禁用该用户?',
+    password: '密码'
+  }
+}

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

@@ -0,0 +1,8 @@
+const allModules = import.meta.glob('./zh-cn/*.js', { eager: true })
+const keys = Object.keys(allModules)
+const packages = {}
+for (const key of keys) {
+  const name = key.substring(key.lastIndexOf('/') + 1, key.lastIndexOf('.js'))
+  packages[name] = allModules[key].default
+}
+export default packages

+ 41 - 0
src/lang/package/zh-cn/common.js

@@ -0,0 +1,41 @@
+export default {
+  route_loading: '页面加载中...',
+  opera: '操作',
+  back: '返回',
+  create: '添加',
+  select: '批量删除',
+  update: '修改',
+  delete: '删除',
+  detail: '详情',
+  exam: '审核',
+  dict: '字典数据',
+  sign: '报名',
+  score: '分数',
+  process: '流程',
+  journal: '期刊',
+  directory: '目录',
+  delete_confirm: '您确定删除该数据?',
+  search: '查询',
+  reset: '重置',
+  view: '查看',
+  save: '保存',
+  submit: '提交',
+  is_use_abled: '启用',
+  is_use_disabled: '禁用',
+  yes: '是',
+  no: '否',
+  no_method: '功能暂未开放',
+  warning: '注意',
+  confirm: '确定',
+  cancel: '取消',
+  user_confirm: '用户确认',
+  re_login: '重新登录',
+  reload: '重新加载',
+  opera_success: '操作成功',
+  opera_fail: '操作失败',
+  token_error: '用户验证失败,请重新登录',
+  upload_btn: '选择文件',
+  has_error: '发生错误',
+  agree: '通过',
+  refuse: '拒绝'
+}

+ 21 - 0
src/lang/package/zh-cn/error.js

@@ -0,0 +1,21 @@
+export default {
+  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: '业务错误',
+  REQUEST_ERROR: '接口请求发生错误',
+  USER_PERMISSION_ERROR: '用户权限异常,请重新登陆或联系管理员!'
+}

+ 10 - 0
src/lang/package/zh-cn/login.js

@@ -0,0 +1,10 @@
+// 登录页面国际化
+export default {
+  title: '产学研用协同创新数字化平台',
+  username: '用户名',
+  password: '密码',
+  login: '登 录',
+  captchaCode: '验证码',
+  placeholder1: '请输入用户名',
+  placeholder2: '请输入密码'
+}

+ 52 - 0
src/lang/package/zh-cn/menus.js

@@ -0,0 +1,52 @@
+export default {
+  home: '首页',
+  system: '系统管理',
+  platform: '平台设置',
+  information: '信息管理',
+  user: '用户管理',
+  board: '数据看板',
+  center: '个人中心',
+  password: '修改密码',
+
+  // system_menus: '菜单设置',
+  // system_userMenus: '用户目录',
+  // system_role: '角色设置',
+  // system_parameter: '系统参数',
+  // system_dict: '字典管理',
+  // system_dict_data: '字典数据',
+  // system_tags: '标签设置',
+  // system_design: '平台设置',
+  // system_dept: '部门管理',
+  // system_sector: '产业设置',
+  // system_region: '地区设置',
+  // system_friend: '合作伙伴设置',
+  // user_admin: '管理员用户',
+  // user_user: '平台用户',
+  // platform: '信息管理',
+  // platform_policy: '政策信息',
+  // platform_news: '新闻通知',
+  // platform_trends: '行业动态',
+  // demand: '供需管理',
+  // platform_match: '赛事管理',
+  // platform_achievement: '成果管理',
+  // demand_supply: '供给信息管理',
+  // demand_demand: '需求信息管理',
+  // match: '赛事管理',
+  // match_info: '赛事信息管理',
+  // match_sign: '报名管理',
+  // match_process: '流程管理',
+  // match_score: '打分管理',
+  // achievement: '成果管理',
+  // project: '项目管理',
+  // footplate: '平台管理',
+  // support: '服务管理',
+  // journal: '行研产研管理',
+  // notes: '期刊管理',
+  // directory: '目录管理',
+  // incubator: '全省孵化基地情况',
+  // elevenHatch: '孵化基地情况',
+  // import: '数据导入',
+  // log: '日志管理',
+  // log_opera: '操作日志',
+  // system_message: '站内消息'
+}

+ 6 - 0
src/lang/package/zh-cn/navbar.js

@@ -0,0 +1,6 @@
+// 导航栏国际化
+export default {
+  dashboard: '首页',
+  logout: '注销',
+  my: '个人中心'
+}

+ 696 - 0
src/lang/package/zh-cn/pages.js

@@ -0,0 +1,696 @@
+export default {
+  menus: {
+    dialogTitle: '目录信息',
+    is_default: '是否默认',
+    name: '目录名称',
+    namePh: '请填写目录名称',
+    route_name: '路由名称',
+    route_namePh: '请填写路由名称',
+    i18n_code: '国际化编码',
+    i18n_codePh: '请填写国际化编码并确保与国际化文件中的编码一致',
+    parentName: '父级目录',
+    icon: '图标',
+    iconPh: '请选择图标',
+    order_num: '顺序',
+    path: '路由地址',
+    pathPh: '请填写路由地址',
+    component: '组件地址',
+    componentPh: '请填写组件地址',
+    type: '目录类型',
+    typePh: '请选择目录类型',
+    is_use: '状态',
+    remark: '备注',
+    remarkPh: '请输入备注',
+    addNext: '添加下一级',
+    baseInfo: '基本信息',
+    configInfo: '功能列表',
+    config_zh: '功能说明',
+    config_code: '按钮编码',
+    config_controller_code: '接口编码',
+    add_config: '添加功能'
+  },
+  role: {
+    is_default: '是否默认',
+    dialogTitle: '角色信息',
+    name: '角色名称',
+    namePh: '请填写角色名称',
+    code: '角色代码',
+    codePh: '请填写角色代码',
+    brief: '简介',
+    menu: '权限配置',
+    is_use: '是否启用',
+    is_admin_role: '是否是管理员使用的角色'
+  },
+  admin: {
+    dialogTitle: '用户信息',
+    account: '账号',
+    nick_name: '名称',
+    role: '角色',
+    is_super: '是否是超级管理员',
+    is_use: '是否启用',
+    bind: '绑定用户',
+    rp: '重置密码',
+    rpConfirm: '您确定要重置密码?',
+    changeToAbled: '您确定要启用该用户?',
+    changeToDisabled: '您确定要禁用该用户?',
+    password: '密码'
+  },
+  user: {
+    dialogTitle: '用户信息',
+    examDialogTitle: '用户审核',
+    openid: '微信唯一标识',
+    account: '账号',
+    password: '密码',
+    industry: '所属产业',
+    nick_name: '昵称',
+    gender: '性别',
+    phone: '手机号',
+    email: '电子邮箱',
+    role: '角色',
+    is_audit: '审核状态',
+    status: '状态'
+  },
+  dict: {
+    addDialogTitle: '新增字典类型',
+    upDialogTitle: '修改字典类型',
+    title: '字典名称',
+    code: '编码',
+    is_use: '是否启用',
+    remark: '备注',
+    titleMessage: '请输入字典名称',
+    codeMessage: '请输入编码'
+  },
+  dictData: {
+    dialogTitle: '管理字典数据',
+    codeDialogTitle: '字典数据',
+    label: '数据显示值',
+    value: '数据选择值',
+    sort: '排序',
+    is_use: '是否启用',
+    labelMessage: '请输入数据显示值',
+    valueMessage: '请输入数据选择值'
+  },
+  tags: {
+    addDialogTitle: '新增导航目录',
+    upDialogTitle: '修改导航目录',
+    title: '标题',
+    English: '英文标题',
+    href: '路由',
+    children: '子目录',
+    type: '是否有子目录',
+    is_use: '是否启用',
+    remark: '备注',
+    sort: '排序',
+    titleMessage: '请输入标题',
+    hrefMessage: '请输入路由',
+    sortfMessage: '请输入排序'
+  },
+  region: {
+    dialogTitle: '地区信息',
+    code: '编码',
+    name: '名称',
+    parent_code: '上级编码',
+    level: '等级',
+    is_use: '是否启用',
+    codeMessage: '请输入编码',
+    nameMessage: '请输入名称',
+    levelfMessage: '请输入等级'
+  },
+  sector: {
+    addDialogTitle: '新增产业',
+    upDialogTitle: '修改产业',
+    title: '标题',
+    brief: '简介',
+    file: '背景图片',
+    href: '路由',
+    partner: '合作伙伴',
+    is_use: '是否启用',
+    remark: '备注',
+    sort: '排序',
+    titleMessage: '请输入标题',
+    hrefMessage: '请输入路由',
+    sortfMessage: '请输入排序'
+  },
+  news: {
+    addDialogTitle: '新增',
+    upDialogTitle: '修改',
+    examDialogTitle: '审核',
+    title: '标题',
+    person: '发布人',
+    time: '发布时间',
+    number: '浏览次数',
+    content: '内容',
+    tags: '标签',
+    logo: '封面',
+    is_show: '是否在首页显示',
+    order_num: '显示顺序',
+    is_use: '是否启用',
+    status: '审核状态',
+    titleMessage: '请输入标题'
+  },
+  demand: {
+    addDialogTitle: '新增需求',
+    upDialogTitle: '修改需求',
+    examDialogTitle: '审核需求',
+    name: '需求名称',
+    tags: '标签',
+    field: '行业领域',
+    urgent: '需求紧急度',
+    method: '合作方式',
+    time: '有效期',
+    money: '价格(万元)',
+    area: '需求地区',
+    brief: '简介',
+    status: '审核状态',
+    industry: '所属产业',
+    company: '所属企业',
+    company_brief: '企业简况',
+    contacts: '联系人',
+    tel: '联系电话',
+    year: '年份',
+    month: '月份',
+    tec_name: '技术需求名称',
+    question: '待解决问题',
+    is_use: '是否启用',
+    demand_status: '需求状态',
+    titleMessage: '请输入需求名称'
+  },
+  supply: {
+    addDialogTitle: '新增供给',
+    upDialogTitle: '修改供给',
+    examDialogTitle: '审核供给',
+    name: '供给名称',
+    tags: '标签',
+    field: '行业领域',
+    urgent: '供给紧急度',
+    method: '合作方式',
+    time: '有效期',
+    money: '价格(万元)',
+    area: '供给地区',
+    brief: '简介',
+    industry: '所属产业',
+    source: '项目来源',
+    status: '审核状态',
+    is_use: '是否启用',
+    supply_status: '供给状态',
+    titleMessage: '请输入供给名称'
+  },
+  match: {
+    addDialogTitle: '新增需赛事',
+    upDialogTitle: '修改赛事',
+    examDialogTitle: '审核赛事',
+    name: '赛事名称',
+    tags: '标签',
+    type: '类型',
+    match_type: '赛事类型',
+    scale: '赛事规模',
+    video: '活动视频',
+    href: '外部链接',
+    work: '组织单位',
+    industry: '所属产业',
+    form: '类别',
+    file: '封面',
+    time: '有效期',
+    money: '奖金(万元)',
+    rules: '赛事规则',
+    brief: '常见问题',
+    status: '审核状态',
+    match_status: '赛事状态',
+    is_use: '是否启用',
+    is_show: '是否在首页显示',
+    order_num: '显示顺序',
+    titleMessage: '请输入需求名称',
+    rules1: '大赛背景',
+    rules2: '大赛主题和目标',
+    rules3: '大赛基本情况介绍',
+    rules4: '赛题任务',
+    rules5: '赛程安排',
+    rules6: '赛制阶段',
+    rules7: '参赛资格',
+    rules8: '参赛报名',
+    rules9: '奖项设置与奖励办法',
+    rules10: '组织单位',
+    rules11: '赛事联络',
+    rules12: '赛事交流'
+  },
+  achievement: {
+    addDialogTitle: '新增成果',
+    upDialogTitle: '修改成果',
+    examDialogTitle: '审核成果',
+    name: '成果名称',
+    industry: '所属产业',
+    tags: '标签',
+    patent: '专利号',
+    field: '行业领域',
+    type: '类型',
+    attribute: '属性',
+    mature: '成熟度',
+    sell: '出让方式',
+    technology: '技术分类',
+    time: '发布时间',
+    money: '价格(万元)',
+    area: '成果地区',
+    brief: '简介',
+    source: '项目来源',
+    person: '联系人',
+    tel: '联系电话',
+    file: '附件',
+    status: '审核状态',
+    is_use: '是否启用',
+    achievement_status: '成果状态',
+    titleMessage: '请输入成果名称'
+  },
+  project: {
+    addDialogTitle: '新增项目',
+    upDialogTitle: '修改项目',
+    examDialogTitle: '审核项目',
+    name: '项目名称',
+    tags: '标签',
+    field: '行业领域',
+    type: '行业分类',
+    skill: '技术类型',
+    maturity: '成熟度',
+    cooperate: '合作类型',
+    time: '发布时间',
+    main: '项目主体',
+    area: '项目地区',
+    progress: '项目进展',
+    track_unit: '跟踪支持单位',
+    source: '项目来源',
+    industry: '所属产业',
+    brief: '简介',
+    file: '附件',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入项目名称'
+  },
+  footplate: {
+    addDialogTitle: '新增平台',
+    upDialogTitle: '修改平台',
+    examDialogTitle: '审核平台',
+    file: '封面',
+    user: '用户',
+    tags: '标签',
+    industry: '所属产业',
+    name: '名称',
+    build: '建设主体',
+    operate: '运营主体',
+    field: '服务产业领域',
+    area: '所在地区',
+    address: '地址',
+    contacts: '联系人',
+    phone: '联系电话',
+    brief: '简介',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入平台名称'
+  },
+  support: {
+    addDialogTitle: '新增服务',
+    upDialogTitle: '修改服务',
+    examDialogTitle: '审核服务',
+    user: '用户',
+    file: '封面',
+    tags: '标签',
+    industry: '所属产业',
+    name: '名称',
+    field: '服务领域',
+    time: '登记时间',
+    area: '所在地区',
+    address: '地址',
+    contacts: '联系人',
+    phone: '联系电话',
+    brief: '简介',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入服务名称'
+  },
+  journal: {
+    addDialogTitle: '新增行研产研',
+    upDialogTitle: '修改行研产研',
+    examDialogTitle: '审核行研产研',
+    file: '封面',
+    name: '名称',
+    sort: '排序',
+    brief: '简介',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入行研产研名称'
+  },
+  notes: {
+    addDialogTitle: '新增期刊',
+    upDialogTitle: '修改期刊',
+    examDialogTitle: '审核期刊',
+    journal: '所属期刊',
+    file: '封面',
+    name: '名称',
+    brief: '简介',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入期刊名称'
+  },
+  directory: {
+    addDialogTitle: '新增目录',
+    upDialogTitle: '修改目录',
+    examDialogTitle: '审核目录',
+    journal: '所属行研产研',
+    notes: '所属期刊',
+    client: '委托方',
+    partner: '合作方',
+    name: '名称',
+    brief: '简介',
+    detail: '详情',
+    status: '审核状态',
+    is_use: '是否启用',
+    titleMessage: '请输入期刊名称'
+  },
+  sign: {
+    name: '姓名',
+    phone: '手机号',
+    cardType: '证件类型',
+    card: '证件号码',
+    time: '报名时间',
+    communication: '微信/QQ',
+    email: '电子邮箱',
+    remark: '备注',
+    DialogTitle: '查看报名情况'
+  },
+  process: {
+    addDialogTitle: '新增流程',
+    upDialogTitle: '修改流程',
+    name: '流程名称',
+    time: '时间',
+    signPerson: '报名人员',
+    brief: '简介',
+    order_num: '顺序',
+    is_use: '是否启用',
+    nameMessage: '请输入流程名称'
+  },
+  score: {
+    addDialogTitle: '新增分数',
+    upDialogTitle: '修改分数',
+    match: '赛事',
+    matchPath: '流程',
+    sign: '选手',
+    time: '时间',
+    score: '分数',
+    signMessage: '请选择选手',
+    scoreMessage: '请填写分数'
+  },
+  dept: {
+    dialogTitle: '部门信息',
+    rDialogTitle: '部门资源',
+    leftTreeTitle: '部门架构图',
+    name: '部门名称',
+    order_num: '顺序',
+    partent: '上级部门',
+    status: '使用状态',
+    resource: '资源管理'
+  },
+  friend: {
+    dialogTitle: '合作伙伴信息',
+    code: '编码',
+    name: '名称',
+    file: 'logo',
+    brief: '简介',
+    parent_code: '上级编码',
+    is_use: '是否启用',
+    codeMessage: '请输入编码',
+    nameMessage: '请输入名称',
+    order_num: '显示顺序(降序)'
+  },
+  log_opera: {
+    operator_id: '操作人数据id',
+    operator_name: '操作人名称',
+    opera: '操作业务',
+    ip: 'ip',
+    time: '时间',
+    referer: '操作地址',
+    path: '接口路径',
+    dialogTitle: '日志详情',
+    device: '设备信息',
+    params: '地址参数',
+    query: '请求参数',
+    body: '方法体参数',
+    origin_data: '原数据',
+    new_data: '新数据',
+    viewTabs1: '记录内容',
+    viewTabs2: '请求参数',
+    viewTabs3: '数据对比',
+    key: '键名',
+    value: '值'
+  },
+  message: {
+    content: '消息内容',
+    type: '消息类型',
+    to: '发送对象',
+    view: '查看接收情况',
+    contentRuleMessage: '请输入消息内容',
+    typeRuleMessage: '请选择消息类型',
+    dialogTitle: '消息详情',
+    addDialogTitle: '消息添加',
+    inputNamePla: '请输入要发送的用户名称',
+    to_user: '接收用户名称',
+    has_user: '已添加该用户',
+    is_read: '是否已读',
+    viewTitle: '接收情况',
+    remind: '重提示',
+    remindSuccess: '重提示成功'
+  },
+  investment: {
+    name: '姓名',
+    cardType: '证件类型',
+    card: '证件号码',
+    money: '出资额',
+    type: '出资方式',
+    address: '地址',
+    phone: '联系电话',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  association: {
+    name: '名称',
+    address: '地址',
+    person: '负责人',
+    person_phone: '负责人电话',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  state: {
+    name: '名称',
+    address: '地址',
+    person: '负责人',
+    person_phone: '负责人电话',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  school: {
+    name: '名称',
+    address: '地址',
+    person: '负责人',
+    person_phone: '负责人电话',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  company: {
+    name: '企业名称',
+    name_pla: '请输入企业名称',
+    type: '所属行业',
+    type_pla: '请选择所属行业',
+    code: '企业统一信用代码',
+    code_pla: '请输入统一信用代码',
+    representative: '法定代表人',
+    representative_pla: '请输入法定代表人名称',
+    phone: '联系电话',
+    email: '电子邮箱',
+    email_pla: '请输入电子邮箱',
+    status: '状态',
+    logo: '企业Logo',
+    pattern: '企业类型',
+    pattern_pla: '请选择企业类型',
+    scale: '企业规模',
+    scale_pla: '请选择企业规模',
+    area: '所在地区',
+    area_pla: '请选择所在地区',
+    person: '员工人数',
+    person_pla: '请输入员工人数',
+    register: '注册资本(万元)',
+    register_pla: '请输入注册资本(万元)',
+    graduate_num: '硕士研究生人数',
+    graduate_num_pla: '请输入硕士研究生人数',
+    doctor_num: '博士人数',
+    doctor_num_pla: '请输入博士人数',
+    returnee_num: '海归人数',
+    returnee_num_pla: '请输入海归人数',
+    knowledge: '知识产权数',
+    knowledge_pla: '请输入知识产权数',
+    patent: '发明专利数',
+    patent_pla: '请输入发明专利数',
+    utility: '实用新型数',
+    utility_pla: '请输入实用新型数',
+    copyright: '软件著作权数',
+    copyright_pla: '请输入软件著作权数',
+    is_tech: '是否为高新技术企业',
+    is_new: '是否为专精特新企业',
+    address: '企业地址',
+    address_pla: '请输入企业地址',
+    is_show: '是否公开',
+    create_time: '成立时间',
+    create_time_pla: '请选择成立时间',
+    products: '企业产品',
+    products_pla: '请输入企业产品',
+    brief: '简介',
+    brief_pla: '请输入简介',
+    status: '审核状态',
+    tabsBasic: '基本信息',
+    tabsSec: '年度信息',
+    year: '年度',
+    year_pla: '请选择年度',
+    esincome_money: '预计营业收入(万元)',
+    esprofit_money: '预计利润(万元)',
+    estax_money: '预计税金(万元)',
+    essearch_money: '预计研发费用(万元)',
+    is_use: '是否使用',
+    time: '填写时间',
+    status: '状态',
+    status_pla: '请选择状态',
+    no_data: '暂无数据',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核',
+    yearInfoTitle: '发布年度信息',
+    yearInfoUpdateTitle: '修改年度信息',
+    yearInfoStatusTitle: '年度信息审核'
+  },
+  expert: {
+    icon: '头像',
+    name: '专家姓名',
+    name_pla: '请输入专家姓名',
+    birth: '出生年月',
+    birth_pla: '请选择出生年月',
+    cardType: '证件类型',
+    cardType_pla: '请选择证件类型',
+    card: '证件号码',
+    card_pla: '请输入证件号码',
+    field: '擅长领域',
+    field_pla: '请选择擅长领域',
+    statusSelectPlaceholder: '请选择状态',
+    direction: '研究方向',
+    direction_pla: '请输入研究方向',
+    education: '最高学历',
+    education_pla: '请选择最高学历',
+    area: '所在地区',
+    area_pla: '请选择所在地区',
+    work: '工作单位',
+    work_pla: '请输入工作单位',
+    is_show: '是否公开',
+    brief: '简介',
+    brief_pla: '请输入简介',
+    status: '状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  unit: {
+    name: '名称',
+    address: '地址',
+    person: '负责人',
+    person_phone: '负责人电话',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核'
+  },
+  incubator: {
+    logo: 'logo',
+    name: '孵化基地名称',
+    name_pla: '请输入孵化基地名称',
+    unit: '运营单位名称',
+    unit_pla: '请输入运营单位名称',
+    person: '负责人姓名',
+    person_pla: '请输入负责人姓名',
+    person_phone: '负责人电话',
+    person_phone_pla: '请输入负责人电话',
+    area: '所在地区',
+    area_pla: '请选择所在地区',
+    address: '地址',
+    address_pla: '请输入地址',
+    cover_area: '占地面积(平方米)',
+    cover_area_pla: '请输入占地面积(平方米)',
+    build_area: '建筑面积(平方米)',
+    build_area_pla: '请输入建筑面积(平方米)',
+    residue_area: '剩余面积(平方米)',
+    residue_area_pla: '请输入剩余面积(平方米)',
+    rent: '房租(元/平方米/月)',
+    rent_pla: '请输入房租(元/平方米/月)',
+    wy_money: '物业费(元/平方米/月)',
+    wy_money_pla: '请输入物业费(元/平方米/月)',
+    is_have: '是否具备中试场地',
+    unit_num: '载体运营单位人数',
+    unit_num_pla: '请输入载体运营单位人数',
+    teacher_num: '载体运营单位的省级以上导师数',
+    teacher_num_pla: '请输入载体运营单位的省级以上导师数',
+    site_area: '中试场地面积(平方米)',
+    site_area_pla: '请输入中试场地面积(平方米)',
+    activity_num: '累计参加活动人次',
+    activity_num_pla: '请输入累计参加活动人次',
+    actCompany_num: '累计参加活动企业数量',
+    actCompany_num_pla: '请输入累计参加活动企业数量',
+    actCity_num: '市级以上活动数',
+    actCity_num_pla: '请输入市级以上活动数',
+    actTrain_num: '培训辅导类活动数',
+    actTrain_num_pla: '请输入培训辅导类活动数',
+    actSchool_num: '高校院所类活动数',
+    actSchool_num_pla: '请输入高校院所类活动数',
+    actInstitution_num: '投资机构类活动数',
+    actInstitution_num_pla: '请输入投资机构类活动数',
+    actService_num: '中介服务类活动数',
+    actService_num_pla: '请输入中介服务类活动数',
+    company_num: '入驻企业数',
+    company_num_pla: '请输入入驻企业数',
+    cooperate: '是否和平台合作',
+    file: '基地风采',
+    brief: '简介',
+    is_show: '是否公开',
+    status: '状态',
+    status_pla: '请选择状态',
+    year: '年度',
+    year_pla: '请选择年度',
+    act_num: '活动总数',
+    time: '填写时间',
+    income_money: '企业实现收入(万元)',
+    profit_money: '企业利润(万元)',
+    tax_money: '企业税金(万元)',
+    esincome_money: '企业预计收入(万元)',
+    esprofit_money: '企业预计利润(万元)',
+    estax_money: '企业预计税金(万元)',
+    content: '获得荣誉情况',
+    is_use: '是否使用',
+    no_data: '暂无数据',
+    statusSelectPlaceholder: '请选择状态',
+    updateTitle: '信息修改',
+    examTitle: '信息审核',
+    yearInfoTitle: '发布年度信息',
+    yearInfoUpdateTitle: '修改年度信息',
+    yearInfoStatusTitle: '年度信息审核',
+    tabsBasic: '基本信息',
+    tabsSec: '年度信息'
+  }
+}

+ 82 - 0
src/layout/index.vue

@@ -0,0 +1,82 @@
+<template>
+  <el-container class="layout-container-demo">
+    <el-aside width="210px" :style="{ 'background-color': '#59b2e4' }">
+      <component :is="cAside"></component>
+    </el-aside>
+    <el-main>
+      <div class="content-box">
+        <el-col :span="24" class="content">
+          <transition name="move" mode="out-in">
+            <el-row>
+              <el-col :span="24" class="header">
+                <component :is="cHeader"></component>
+              </el-col>
+              <el-col :span="24" class="crumb">
+                <component :is="tagsBar"></component>
+              </el-col>
+              {{ pagesLoading }}
+              <el-col :span="24" class="container" :style="{ padding: '10px' }">
+                <router-view :style="viewStyle"></router-view>
+              </el-col>
+            </el-row>
+          </transition>
+          <el-backtop target=".content"></el-backtop>
+        </el-col>
+      </div>
+    </el-main>
+  </el-container>
+</template>
+
+<script setup>
+// 组件
+import cHeader from './parts/Header.vue'
+import cAside from './parts/Sidebar.vue'
+import tagsBar from './parts/Tagsbar.vue'
+// 路由
+const route = useRoute()
+const viewStyle = ref({})
+// 请求
+onMounted(async () => {
+  await updateStyle()
+})
+// 多选
+const updateStyle = () => {
+  if (route.name == 'incubator' || route.name == 'elevenHatch') viewStyle.value = {}
+  else {
+    viewStyle.value = {
+      width: '98.8%',
+      height: '86vh',
+      background: '#ffffff',
+      'overflow-x': 'hidden',
+      'overflow-y': 'auto',
+      border: '1px solid #f0f0f0',
+      padding: '10px'
+    }
+  }
+}
+watch(
+  route,
+  () => {
+    updateStyle()
+  },
+  {
+    deep: true
+  }
+)
+</script>
+
+<style scoped lang="scss">
+.layout-container-demo {
+  height: 100%;
+  width: 100%;
+
+  .el-header {
+    padding: 0;
+  }
+
+  .el-main {
+    padding: 0;
+    background-color: #f0f0f0;
+  }
+}
+</style>

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

@@ -0,0 +1,84 @@
+<template>
+  <div id="Header">
+    <el-row>
+      <el-col :span="24" class="main">
+        <div class="left">
+          <Breadcrumb></Breadcrumb>
+        </div>
+        <div class="right">
+          <!-- <LangSelect class="navbar-item"></LangSelect> -->
+          <el-dropdown>
+            <el-icon style="margin-right: 8px; margin-top: 1px">
+              <setting />
+            </el-icon>
+            <template #dropdown>
+              <el-dropdown-menu>
+                <el-dropdown-item @click="center">{{ $t('navbar.my') }}</el-dropdown-item>
+                <el-dropdown-item @click="logout">{{ $t('navbar.logout') }}</el-dropdown-item>
+              </el-dropdown-menu>
+            </template>
+          </el-dropdown>
+          <span>
+            {{ user && user.id ? user.nick_name : '游客' }}
+          </span>
+        </div>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { UserStore } from '@/store/user'
+import { useTagsViewStore } from '@/store'
+const userStore = UserStore()
+const tagsViewStore = useTagsViewStore()
+const user = computed(() => userStore.user)
+
+const router = useRouter()
+// 退出登录
+const logout = () => {
+  userStore.logOut()
+  tagsViewStore.delAllViews()
+  window.location.href = `${import.meta.env.VITE_BASE_URL}/login`
+}
+// 个人中心
+const center = () => {
+  router.push('/center')
+}
+</script>
+<style scoped lang="scss">
+.main {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 0 20px;
+  height: 50px;
+  background: #ffffff;
+
+  .left {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+  }
+
+  .right {
+    display: inline-flex;
+    align-items: center;
+    justify-content: center;
+
+    .navbar-item {
+      display: inline-block;
+      width: 30px;
+      height: 50px;
+      line-height: 50px;
+      color: var(--el-text-color);
+      text-align: center;
+      cursor: pointer;
+
+      &:hover {
+        background: rgb(0 0 0 / 10%);
+      }
+    }
+  }
+}
+</style>

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

@@ -0,0 +1,116 @@
+<!-- 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">
+          <img src="/logo.png" class="logo-image" />
+          <div class="logo-title">{{ $t('login.title') }}</div>
+        </el-col>
+        <el-col :span="24" class="second">
+          <el-menu class="sidebar-el-menu" :default-active="onRoutes" unique-opened router background-color="#304156" text-color="#bfcbd9" active-text-color="#409eff">
+            <menu-item :items="items"></menu-item>
+          </el-menu>
+        </el-col>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script setup>
+import { UserStore } from '@/store/user'
+import menuItem from './sidebar/items.vue'
+import { useRoute } from 'vue-router'
+const route = useRoute()
+
+const onRoutes = ref(route.path)
+const userStore = UserStore()
+let items = ref(userStore.menus)
+const user = computed(() => userStore.user)
+
+const getMenu = async () => {
+  console.log('in')
+  const menus = user.value.menus
+  const newMenus = [...menus]
+  console.log(newMenus)
+  items.value = newMenus
+}
+
+// watch(
+//   user,
+//   (newVal) => {
+//     if (newVal && newVal.id) {
+//       if (newVal && newVal.id) getMenu()
+//       else ElMessage({ message: `暂无用户信息,无法获取目录信息`, type: 'error' })
+//     }
+//   },
+//   {
+//     deep: true
+//   }
+// )
+watch(
+  route,
+  (newVal) => {
+    if (newVal && newVal.path) onRoutes.value = newVal.path
+  },
+  {
+    immediate: true //初始化立即执行
+  }
+)
+</script>
+<style scoped lang="scss">
+.sidebar::-webkit-scrollbar {
+  width: 0;
+}
+
+.sidebar > ul {
+  height: 100%;
+}
+
+.main {
+  .first {
+    display: flex;
+    flex-direction: column;
+    align-items: center;
+    justify-content: center;
+    background-color: #29a2e6;
+    padding: 5px 0;
+
+    .logo-image {
+      width: 135px;
+      height: 40px;
+    }
+
+    .logo-title {
+      margin-left: 10px;
+      font-size: 14px;
+      font-weight: bold;
+      color: #000;
+    }
+  }
+
+  .second {
+    padding: 0 2px 0 0;
+
+    .el-menu {
+      border-right: none;
+    }
+
+    .iconfont {
+      font-size: 18px;
+      margin: 0 5px 0 0;
+    }
+    :deep(.el-menu-item.is-active) {
+      color: #ffff !important;
+    }
+    :deep(.el-menu-item) {
+      background-color: #59b2e4 !important;
+      color: #000 !important;
+    }
+    :deep(.el-sub-menu__title) {
+      background-color: #59b2e4 !important;
+      color: #000 !important;
+    }
+  }
+}
+</style>

+ 390 - 0
src/layout/parts/Tagsbar.vue

@@ -0,0 +1,390 @@
+<template>
+  <div class="tags-container">
+    <el-scrollbar class="scroll-container" :vertical="false" @wheel.prevent="handleScroll">
+      <router-link
+        ref="tagRef"
+        v-for="tag in visitedViews"
+        :key="tag.fullPath"
+        :class="'tags-item ' + (isActive(tag) ? 'active' : '')"
+        :to="{ path: tag.path, query: tag.query }"
+      >
+      <!-- @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
+        @contextmenu.prevent="openContentMenu(tag, $event)" -->
+        {{ translateRouteTitle(tag.title) }}
+        <SvgIcon class="close-icon" icon-class="close" v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)"></SvgIcon>
+      </router-link>
+    </el-scrollbar>
+    <!-- tag标签操作目录 -->
+    <ul v-show="contentMenuVisible" class="contextmenu" :style="{ left: left + 'px', top: top + 'px' }">
+      <li @click="refreshSelectedTag(selectedTag)">
+        <SvgIcon icon-class="refresh"></SvgIcon>
+        刷新
+      </li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
+        <SvgIcon icon-class="close"></SvgIcon>
+        关闭
+      </li>
+      <li @click="closeOtherTags">
+        <SvgIcon icon-class="close_other"></SvgIcon>
+        关闭其它
+      </li>
+      <li v-if="!isFirstView()" @click="closeLeftTags">
+        <SvgIcon icon-class="close_left"></SvgIcon>
+        关闭左侧
+      </li>
+      <li v-if="!isLastView()" @click="closeRightTags">
+        <SvgIcon icon-class="close_right"></SvgIcon>
+        关闭右侧
+      </li>
+      <li @click="closeAllTags(selectedTag)">
+        <SvgIcon icon-class="close_all"></SvgIcon>
+        关闭所有
+      </li>
+    </ul>
+  </div>
+</template>
+
+<script setup>
+import { ref, watch, onMounted, getCurrentInstance, computed } from 'vue'
+import { useRoute, useRouter } from 'vue-router'
+import defaultSettings from '@/settings'
+import { storeToRefs } from 'pinia'
+import { resolve } from 'path-browserify'
+import { translateRouteTitle } from '@/utils/i18n'
+import { useTagsViewStore } from '@/store'
+
+const { proxy } = getCurrentInstance()
+const router = useRouter()
+const route = useRoute()
+
+const tagsViewStore = useTagsViewStore()
+
+const { visitedViews } = storeToRefs(tagsViewStore)
+const layout = computed(() => defaultSettings.layout)
+const { t } = useI18n()
+const selectedTag = ref({
+  path: '',
+  fullPath: '',
+  name: '',
+  title: '',
+  affix: false,
+  keepAlive: false
+})
+
+const affixTags = ref([])
+const left = ref(0)
+const top = ref(0)
+
+watch(
+  route,
+  () => {
+    addTags()
+    moveToCurrentTag()
+  },
+  {
+    immediate: true //初始化立即执行
+  }
+)
+
+const contentMenuVisible = ref(false) // 右键目录是否显示
+watch(contentMenuVisible, (value) => {
+  if (value) {
+    document.body.addEventListener('click', closeContentMenu)
+  } else {
+    document.body.removeEventListener('click', closeContentMenu)
+  }
+})
+
+/**
+ * 过滤出需要固定的标签
+ */
+function filterAffixTags(routes, basePath = '/') {
+  let tags = []
+  routes.forEach((route) => {
+    const tagPath = resolve(basePath, route.path)
+    if (route.meta?.affix) {
+      tags.push({
+        path: tagPath,
+        fullPath: tagPath,
+        name: String(route.name),
+        title: route.meta?.title || 'no-name',
+        affix: route.meta?.affix,
+        keepAlive: route.meta?.keepAlive
+      })
+    }
+    if (route.children) {
+      const tempTags = filterAffixTags(route.children, basePath + route.path)
+      if (tempTags.length >= 1) {
+        tags = [...tags, ...tempTags]
+      }
+    }
+  })
+  return tags
+}
+
+function initTags() {
+  const tags = filterAffixTags([])
+  affixTags.value = tags
+  for (const tag of tags) {
+    // Must have tag name
+    if (tag.name) {
+      tagsViewStore.addVisitedView(tag)
+    }
+  }
+}
+
+function addTags() {
+  if (route.meta.title) {
+    tagsViewStore.addView({
+      name: route.name,
+      title: t(route.meta.title),
+      path: route.path,
+      fullPath: route.fullPath,
+      affix: route.meta?.affix,
+      keepAlive: route.meta?.keepAlive
+    })
+  }
+}
+
+function moveToCurrentTag() {
+  // 使用 nextTick() 的目的是确保在更新 tagsView 组件之前,scrollPaneRef 对象已经滚动到了正确的位置。
+  nextTick(() => {
+    for (const tag of visitedViews.value) {
+      if (tag.path === route.path) {
+        if (tag.fullPath !== route.fullPath) {
+          tagsViewStore.updateVisitedView({
+            name: route.name,
+            title: route.meta.title || '',
+            path: route.path,
+            fullPath: route.fullPath,
+            affix: route.meta?.affix,
+            keepAlive: route.meta?.keepAlive
+          })
+        }
+      }
+    }
+  })
+}
+
+function isActive(tag) {
+  return tag.path === route.path
+}
+
+function isAffix(tag) {
+  return tag?.affix
+}
+
+function isFirstView() {
+  try {
+    return selectedTag.value.path === '/dashboard' || selectedTag.value.fullPath === tagsViewStore.visitedViews[1].fullPath
+  } catch (err) {
+    return false
+  }
+}
+
+function isLastView() {
+  try {
+    return selectedTag.value.fullPath === tagsViewStore.visitedViews[tagsViewStore.visitedViews.length - 1].fullPath
+  } catch (err) {
+    return false
+  }
+}
+
+function refreshSelectedTag(view) {
+  tagsViewStore.delCachedView(view)
+  const { fullPath } = view
+  nextTick(() => {
+    router.replace({ path: '/redirect' + fullPath })
+  })
+}
+
+function toLastView(visitedViews, view) {
+  const latestView = visitedViews.slice(-1)[0]
+  if (latestView && latestView.fullPath) {
+    router.push(latestView.fullPath)
+  } else {
+    if (view?.name === 'home') {
+      router.replace({ path: '/redirect' + view.fullPath })
+    } else {
+      router.push('/')
+    }
+  }
+}
+
+function closeSelectedTag(view) {
+  tagsViewStore.delView(view).then((res) => {
+    if (isActive(view)) {
+      toLastView(res.visitedViews, view)
+    }
+  })
+}
+
+function closeLeftTags() {
+  tagsViewStore.delLeftViews(selectedTag.value).then((res) => {
+    if (!res.visitedViews.find((item) => item.path === route.path)) {
+      toLastView(res.visitedViews)
+    }
+  })
+}
+function closeRightTags() {
+  tagsViewStore.delRightViews(selectedTag.value).then((res) => {
+    if (!res.visitedViews.find((item) => item.path === route.path)) {
+      toLastView(res.visitedViews)
+    }
+  })
+}
+
+function closeOtherTags() {
+  router.push(selectedTag.value)
+  tagsViewStore.delOtherViews(selectedTag.value).then(() => {
+    moveToCurrentTag()
+  })
+}
+
+function closeAllTags(view) {
+  tagsViewStore.delAllViews().then((res) => {
+    toLastView(res.visitedViews, view)
+  })
+}
+
+/**
+ * 打开右键目录
+ */
+function openContentMenu(tag, e) {
+  const menuMinWidth = 105
+  const offsetLeft = proxy?.$el.getBoundingClientRect().left // container margin left
+  const offsetWidth = proxy?.$el.offsetWidth // container width
+  const maxLeft = offsetWidth - menuMinWidth // left boundary
+  const l = e.clientX - offsetLeft + 15 // 15: margin right
+
+  if (l > maxLeft) {
+    left.value = maxLeft
+  } else {
+    left.value = l
+  }
+
+  // 混合模式下,需要减去顶部目录(fixed)的高度
+  if (layout.value === 'mix') {
+    top.value = e.clientY - 50
+  } else {
+    top.value = e.clientY
+  }
+
+  contentMenuVisible.value = true
+  selectedTag.value = tag
+}
+
+/**
+ * 关闭右键目录
+ */
+function closeContentMenu() {
+  contentMenuVisible.value = false
+}
+
+/**
+ * 滚动事件
+ */
+function handleScroll() {
+  closeContentMenu()
+}
+onMounted(() => {
+  initTags()
+})
+</script>
+<style lang="scss" scoped>
+.tags-container {
+  height: 35px;
+  background-color: var(--el-bg-color);
+  border: 1px solid var(--el-border-color-light);
+  box-shadow: 0 1px 1px var(--el-box-shadow-light);
+
+  .tags-item {
+    display: inline-block;
+    padding: 3px 8px;
+    margin: 4px 0 0 5px;
+    font-size: 12px;
+    cursor: pointer;
+    border: 1px solid var(--el-border-color-light);
+    text-decoration: none;
+
+    &:hover {
+      color: var(--el-color-primary);
+    }
+
+    &:first-of-type {
+      margin-left: 15px;
+    }
+
+    &:last-of-type {
+      margin-right: 15px;
+    }
+
+    .close-icon {
+      width: 1em;
+      height: 1em;
+      color: #000;
+      border-radius: 50%;
+
+      &:hover {
+        color: #fff;
+        background-color: var(--el-color-primary);
+      }
+    }
+
+    &.active {
+      color: #fff;
+      background-color: var(--el-color-primary);
+
+      &::before {
+        display: inline-block;
+        width: 8px;
+        height: 8px;
+        margin-right: 5px;
+        content: '';
+        background: #fff;
+        border-radius: 50%;
+      }
+      .close-icon {
+        color: #fff;
+      }
+      .close-icon:hover {
+        color: var(--el-color-primary);
+        background-color: var(--el-fill-color-light);
+      }
+    }
+  }
+}
+
+.contextmenu {
+  position: absolute;
+  z-index: 99;
+  font-size: 12px;
+  background: var(--el-bg-color-overlay);
+  border-radius: 4px;
+  box-shadow: var(--el-box-shadow-light);
+
+  li {
+    padding: 8px 16px 8px 0;
+    cursor: pointer;
+
+    &:hover {
+      background: var(--el-fill-color-light);
+    }
+  }
+}
+
+.scroll-container {
+  position: relative;
+  width: 100%;
+  overflow: hidden;
+  white-space: nowrap;
+
+  .el-scrollbar__bar {
+    bottom: 0;
+  }
+
+  .el-scrollbar__wrap {
+    height: 49px;
+  }
+}
+</style>

+ 48 - 0
src/layout/parts/sidebar/items.vue

@@ -0,0 +1,48 @@
+<template>
+  <template v-for="item in items" :key="item.id">
+    <template v-if="item.type === '0'">
+      <el-menu-item :index="item.path" :key="item.path">
+        <component class="icon" :is="item.icon"></component>
+        <span>{{ $t(item.name) }}</span>
+      </el-menu-item>
+    </template>
+  </template>
+</template>
+
+<script>
+export default {
+  name: 'menuItem',
+  props: {
+    items: { type: Array, default: () => [] }
+  }
+}
+</script>
+
+<style scoped lang="scss">
+.icon {
+  display: inline-block;
+  width: 1em;
+  height: 1em;
+  overflow: hidden;
+  vertical-align: -0.15em;
+  outline: none;
+  margin-right: 5px;
+}
+.el-menu {
+  background-color: #59b2e4 !important;
+  color: #000 !important;
+}
+.sidebar-el-menu {
+  background-color: #59b2e4 !important;
+  color: #000 !important;
+}
+.el-menu-item.is-active {
+  background-color: #59b2e4 !important;
+  color: #fff !important;
+}
+.el-menu-item:focus,
+.el-menu-item:hover {
+  background-color: #59b2e4 !important;
+  color: #000 !important;
+}
+</style>

+ 177 - 0
src/layout/site.js

@@ -0,0 +1,177 @@
+// 网站基本设置
+export const siteInfo = {
+  display: false,
+  zhTitle: '产学研用协同创新数字化平台',
+  logo_url: `${import.meta.env.VITE_BASE_URL}/src/assets/logo-jilinbai.png`
+}
+// 目录设置
+export const menuInfo = {
+  info: {
+    display: false,
+    mode: 'horizontal',
+    backColor: '#242f42',
+    textColor: '#ffffff'
+  },
+  menuList: [
+    {
+      id: '65b7633cf546132d55d63958',
+      name: 'dashboard',
+      order_num: 1,
+      path: '/',
+      component: '/home/index',
+      type: '1',
+      is_use: '0'
+    },
+    {
+      id: '65b7633cf546132d55d63956',
+      name: 'system',
+      order_num: 2,
+      type: '0',
+      is_use: '0',
+      children: [
+        {
+          id: '65b7633cf546132d55d6395a',
+          name: 'menus',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 1,
+          path: '/system/menus',
+          component: '/system/menus/index',
+          type: '1',
+          is_use: '0',
+          parent_name: 'system'
+        },
+        {
+          id: '65b7633cf546132d55d6395b',
+          name: 'role',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 2,
+          path: '/system/role',
+          component: '/system/role/index',
+          type: '1',
+          is_use: '0',
+          parent_name: 'system'
+        },
+        {
+          id: '65b7633cf546132d55d6395c',
+          name: 'dict',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 3,
+          path: '/system/dict',
+          component: '/system/dict/index',
+          type: '1',
+          is_use: '0',
+          parent_name: 'system'
+        },
+        {
+          id: '65b7633cf546132d55d6395d',
+          name: 'dictData',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 4,
+          path: '/system/dictData',
+          component: '/system/dictData/index',
+          type: '2',
+          is_use: '0',
+          parent_name: 'system'
+        },
+        {
+          id: '65b7633cf546132d55d6395e',
+          name: 'config',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 5,
+          path: '/system/config',
+          component: '/system/config/index',
+          type: '1',
+          is_use: '0',
+          parent_name: 'system'
+        },
+        {
+          id: '65b88ca947ad35b235c046a9',
+          name: 'module',
+          parent_id: '65b7633cf546132d55d63956',
+          order_num: 6,
+          path: '/system/module',
+          component: '/system/module/index',
+          type: '1',
+          is_use: '0',
+          parent_name: 'system'
+        }
+      ]
+    },
+    {
+      id: '65b7633cf546132d55d63957',
+      name: '用户管理',
+      order_num: 3,
+      type: '0',
+      is_use: '0',
+      children: [
+        {
+          id: '65b7633cf546132d55d63960',
+          name: '管理员用户',
+          parent_id: '65b7633cf546132d55d63957',
+          order_num: 1,
+          path: '/user/admin',
+          component: '/user/admin/index',
+          type: '1',
+          is_use: '0',
+          parent_name: '用户管理'
+        },
+        {
+          id: '65b7633cf546132d55d63961',
+          name: '平台用户',
+          parent_id: '65b7633cf546132d55d63957',
+          order_num: 2,
+          path: '/user/user',
+          component: '/user/user/index',
+          type: '1',
+          is_use: '0',
+          parent_name: '用户管理'
+        }
+      ]
+    },
+    {
+      id: '65b7633cf546132d55d63963',
+      name: '公证员管理',
+      order_num: 5,
+      path: '/personnel',
+      component: '/personnel/index',
+      type: '1',
+      is_use: '0'
+    },
+    {
+      id: '65b7633cf546132d55d63964',
+      name: '申办流程管理',
+      order_num: 6,
+      path: '/path',
+      component: '/path/index',
+      type: '1',
+      is_use: '0'
+    },
+    {
+      id: '65b7633cf546132d55d63965',
+      name: '业务管理',
+      order_num: 7,
+      path: '/business',
+      component: '/business/index',
+      type: '1',
+      is_use: '0'
+    },
+    {
+      id: '65b7633cf546132d55d63966',
+      name: '申请记录管理',
+      order_num: 8,
+      path: '/apply',
+      component: '/apply/index',
+      type: '1',
+      is_use: '0'
+    },
+    {
+      id: '65b7633cf546132d55d63962',
+      name: '修改密码',
+      order_num: 999,
+      path: '/acccount/updatepd',
+      component: '/acccount/updatepd/index',
+      type: '1',
+      is_use: '0'
+    }
+  ]
+}

+ 31 - 0
src/main.js

@@ -0,0 +1,31 @@
+import { createApp } from 'vue'
+import { setupStore } from '@/store'
+
+import App from './App.vue'
+import router from './router'
+import * as ElementPlusIconsVue from '@element-plus/icons-vue'
+
+// 本地SVG图标
+import 'virtual:svg-icons-register'
+// 国际化
+import i18n from '@/lang/index'
+// 请求检查函数
+import { InitCheckResult } from './utils/checkResult'
+import { InitVariable } from './utils/variable'
+// 组件
+import globalComponents from '@/components'
+// 指令
+import { InitDirective } from './utils/directives'
+// 自动滚动
+import vue3SeamlessScroll from 'vue3-seamless-scroll'
+const app = createApp(App)
+globalComponents(app)
+setupStore(app)
+for (const [key, component] of Object.entries(ElementPlusIconsVue)) {
+  app.component(key, component)
+}
+app.use(i18n).use(router).mount('#app')
+app.use(vue3SeamlessScroll, { name: 'vue3SeamlessScroll' })
+InitCheckResult(app)
+InitVariable(app)
+InitDirective(app)

+ 123 - 0
src/router/guard.js

@@ -0,0 +1,123 @@
+import { get } from 'lodash-es'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { UserStore } from '@/store/user'
+
+const whiteList = ['/redirect', '/login', '/401', '/404', '/route/loading', '/test']
+NProgress.configure({ showSpinner: false }) // 进度条
+const dontRedirectList = ['/login', '/', '/401', '/404']
+const getRedirectUri = (route) => {
+  const res = dontRedirectList.find((f) => f === route.path)
+  if (res) return
+  else return route.fullPath
+}
+
+// 注册前置守卫
+export const registerBeforeRouter = async (router) => {
+  router.beforeEach(async (to, from, next) => {
+    NProgress.start() //开启进度条
+    const token = localStorage.getItem('token')
+    NProgress.inc()
+    if (whiteList.includes(to.path)) {
+      next()
+      return
+    }
+    if (!token) {
+      // 没有登录信息,返回login,并记录当前路由
+      console.log('no token & not in whiteList')
+      next({ path: '/login', query: { redirectPath: getRedirectUri(to) } })
+      return
+    } else {
+      // token兑换信息
+      const userStore = UserStore()
+      const result = await userStore.tokenView(token)
+      if (result.errcode === 0) {
+        userStore.setUser(result.data)
+        const menus = [
+          {
+            id: 1,
+            name: '首页',
+            order_num: 1,
+            path: '/',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+          {
+            id: 2,
+            name: '系统管理',
+            order_num: 2,
+            path: '/system/index',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+          {
+            id: 3,
+            name: '平台设置',
+            order_num: 3,
+            path: '/platform/index',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+          {
+            id: 4,
+            name: '信息管理',
+            order_num: 4,
+            path: '/information/index',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+          {
+            id: 5,
+            name: '用户管理',
+            order_num: 1,
+            path: '/user/index',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+          {
+            id: 6,
+            name: '数据看板',
+            order_num: 6,
+            path: '/board/index',
+            type: '0',
+            is_default: '0',
+            is_use: '0'
+          },
+        ]
+        userStore.setMenus(menus)
+      }
+      next()
+      // const userStore = UserStore()
+      // const userMenus = toRaw(userStore.menus)
+      // if (!userMenus || userMenus.length <= 0) {
+      //   // 没有目录,说明路由没有注册,需要注册路由,记录当前去哪里再跳转至路由
+      //   next({ path: '/route/loading', query: { redirectPath: getRedirectUri(to) } })
+      //   return
+      // } else {
+      //   // 路由注册了,直接gogogo
+      //   // 检查有没有redirectPath
+      //   let redirectPath = get(to, 'query.redirectPath')
+      //   if (redirectPath) next(redirectPath)
+      //   next()
+      //   return
+      // }
+    }
+  })
+}
+
+// 注册路由后置守卫
+
+export const registerAfterRouter = async (router) => {
+  router.afterEach(async (to) => {
+    NProgress.done() //完成进度条
+    if (to.path === '/login') {
+      return
+    }
+    // 请求该页面的权限
+  })
+}

+ 82 - 0
src/router/index.js

@@ -0,0 +1,82 @@
+import { createRouter, createWebHistory } from 'vue-router'
+import { registerBeforeRouter, registerAfterRouter } from './guard'
+import { routes as system_routes } from './modules/system'
+import { routes as platform_routes } from './modules/platform'
+import { routes as information_routes } from './modules/information'
+import { routes as user_routes } from './modules/user'
+import { routes as board_routes } from './modules/board'
+export const homeIndex = () => import('@/views/home/index.vue')
+export const Layout = () => import('@/layout/index.vue')
+
+import i18n from '@/lang'
+// 静态路由
+export const constantRoutes = [
+  {
+    path: '/test',
+    component: () => import('@/views/test.vue'),
+    meta: { hidden: true }
+  },
+  {
+    path: '/redirect',
+    component: Layout,
+    meta: { hidden: true },
+    children: [
+      {
+        path: '/redirect/:path(.*)',
+        component: () => import('@/views/redirect/index.vue')
+      }
+    ]
+  },
+  {
+    path: '/login',
+    name: 'Login',
+    meta: { title: '系统登录' },
+    component: () => import('@/views/login/index.vue')
+  },
+  {
+    path: '/',
+    name: 'Layout',
+    component: Layout,
+    children: [
+      {
+        path: '/',
+        name: 'home',
+        meta: {
+          title: i18n.global.t('menus.home'),
+          affix: true,
+          keepAlive: true,
+          alwaysShow: false
+        },
+        component: () => import('@/views/home/index.vue')
+      },
+      {
+        path: '401',
+        component: () => import('@/views/error-page/401.vue'),
+        meta: { hidden: true }
+      },
+      {
+        path: '404',
+        component: () => import('@/views/error-page/404.vue'),
+        meta: { hidden: true }
+      },
+      ...system_routes,
+      ...platform_routes,
+      ...information_routes,
+      ...user_routes,
+      ...board_routes
+    ]
+  }
+]
+
+/**
+ * 创建路由
+ */
+const router = createRouter({
+  history: createWebHistory(import.meta.env.BASE_URL),
+  routes: constantRoutes,
+  // 刷新时,滚动条位置还原
+  scrollBehavior: () => ({ left: 0, top: 0 })
+})
+registerBeforeRouter(router)
+registerAfterRouter(router)
+export default router

+ 14 - 0
src/router/modules/board.js

@@ -0,0 +1,14 @@
+import i18n from '@/lang'
+export const routes = [
+  {
+    path: '/board/index',
+    name: 'board_index',
+    meta: {
+      title: i18n.global.t('menus.board'),
+      affix: true,
+      keepAlive: true,
+      alwaysShow: false
+    },
+    component: () => import('@/views/board/index.vue')
+  }
+]

+ 14 - 0
src/router/modules/information.js

@@ -0,0 +1,14 @@
+import i18n from '@/lang'
+export const routes = [
+  {
+    path: '/information/index',
+    name: 'information_index',
+    meta: {
+      title: i18n.global.t('menus.information'),
+      affix: true,
+      keepAlive: true,
+      alwaysShow: false
+    },
+    component: () => import('@/views/information/index.vue')
+  }
+]

+ 14 - 0
src/router/modules/platform.js

@@ -0,0 +1,14 @@
+import i18n from '@/lang'
+export const routes = [
+  {
+    path: '/platform/index',
+    name: 'platform_index',
+    meta: {
+      title: i18n.global.t('menus.platform'),
+      affix: true,
+      keepAlive: true,
+      alwaysShow: false
+    },
+    component: () => import('@/views/platform/index.vue')
+  }
+]

+ 14 - 0
src/router/modules/system.js

@@ -0,0 +1,14 @@
+import i18n from '@/lang'
+export const routes = [
+  {
+    path: '/system/index',
+    name: 'system_index',
+    meta: {
+      title: i18n.global.t('menus.system'),
+      affix: true,
+      keepAlive: true,
+      alwaysShow: false
+    },
+    component: () => import('@/views/system/index.vue')
+  }
+]

+ 14 - 0
src/router/modules/user.js

@@ -0,0 +1,14 @@
+import i18n from '@/lang'
+export const routes = [
+  {
+    path: '/user/index',
+    name: 'user_index',
+    meta: {
+      title: i18n.global.t('menus.user'),
+      affix: true,
+      keepAlive: true,
+      alwaysShow: false
+    },
+    component: () => import('@/views/user/index.vue')
+  }
+]

+ 120 - 0
src/router/register.js

@@ -0,0 +1,120 @@
+import { UserStore } from '@/store/user'
+import { cloneDeep, isArray, omit } from 'lodash-es'
+
+// 获取用户信息,返回目录
+export const getUserMeta = async (token) => {
+  const userStore = UserStore()
+  const result = await userStore.tokenView(token)
+  if (result.errcode === 0) {
+    userStore.setUser(result.data)
+    const resetMenusResult = resetMenus(result.data.menus)
+    const storeMenus = toRaw(userStore.menus)
+    if (storeMenus.length <= 0) {
+      userStore.setMenus(resetMenusResult)
+    }
+    return { menus: result.data.menus, errcode: 0 }
+  }
+}
+
+// 检查路由是否存在
+export const hasNecessaryRoute = (to, router) => {
+  // 将默认注册的路由平铺成一维数组
+  const routesOneDimensional = toOneDimensional(router.getRoutes())
+  return routesOneDimensional.find((f) => f.path === to.path)
+}
+
+/**
+ * 将路由数组一维化
+ * @param {Array} routes 路由数组
+ * @returns 一维路由数组
+ */
+const toOneDimensional = (routes) => {
+  const result = []
+  for (const r of routes) {
+    const { children = [], ...others } = r
+    result.push(others)
+    if (children.length > 0) result.push(...toOneDimensional(children))
+  }
+  return result
+}
+
+// 添加路由
+export const addUserRoutes = (menus, router) => {
+  return new Promise((resolve) => {
+    // 将用户目录转换成普通对象
+    const menuArr = toRaw(menus)
+    // 将用户目录平铺成一维数组,并将目录过滤出去.目录不需要注册,不是组件
+    const menuOneDimensional = toOneDimensional(menuArr)
+    // 将默认注册的路由平铺成一维数组
+    const routesOneDimensional = toOneDimensional(router.getRoutes())
+    routesRegister(menuOneDimensional, routesOneDimensional, router)
+    resolve()
+  })
+}
+
+/**
+ * 注册路由
+ * @param {Array} menus 一维数组目录
+ * @param {Array} defaultRoutes 默认路由一维数组
+ * @param {*} router 路由实例
+ */
+const routesRegister = (menus, defaultRoutes, router) => {
+  // 默认注册位置
+  const __def = 'Layout'
+  const loadComponent = import.meta.glob('../views/**/*.vue')
+  for (const route of menus) {
+    const { type, route_name, path, component, parent_id } = route
+    if (!path) {
+      // console.log(route)
+    }
+    // 检查路由是否已存在,存在跳过
+    const hasRoute = defaultRoutes.find((f) => f.path === path)
+    if (hasRoute) continue
+    if (type === '0') {
+      // 目录, 默认使用 `/${路由名称}`
+      const route = {
+        path: `/${route_name}`,
+        name: route_name,
+        meta: { title: route_name, type }
+      }
+      router.addRoute(__def, route)
+    } else if (type === '1' || type === '2') {
+      // 目录 或 子目录
+      const route = {
+        path: path,
+        name: route_name,
+        meta: {
+          title: route_name,
+          type
+        },
+        component: loadComponent[`../views${component}.vue`]
+      }
+      if (parent_id) {
+        const parent = menus.find((f) => f.id === parent_id)
+        if (!parent) continue
+        const pos = parent.route_name
+        router.addRoute(pos, route)
+        // try {
+        // } catch (error) {
+        //   console.log(pos, route)
+        //   console.error(error)
+        // }
+      } else {
+        router.addRoute(__def, route)
+      }
+    }
+  }
+}
+
+const resetMenus = (menus) => {
+  if (!isArray(menus) || menus.length <= 0) return []
+  const cMenus = cloneDeep(menus)
+  const result = []
+  for (const m of cMenus) {
+    const mid = omit(m, ['is_use', 'order_num', 'in_admin_frame'])
+    const { children } = mid
+    if (children) mid.children = resetMenus(children)
+    result.push(mid)
+  }
+  return result
+}

+ 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

+ 40 - 0
src/store/api/log/opera.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/log/opera'
+const axios = new AxiosWrapper()
+
+export const OperaLogsStore = defineStore('OperaLogs', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

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

@@ -0,0 +1,25 @@
+import { defineStore } from 'pinia'
+import { get, omit } from 'lodash-es'
+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 }
+})

+ 40 - 0
src/store/api/platform/achievement.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/achievement'
+const axios = new AxiosWrapper()
+
+export const AchievementStore = defineStore('achievement', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/demand.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/demand'
+const axios = new AxiosWrapper()
+
+export const DemandStore = defineStore('demand', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/design.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/design'
+const axios = new AxiosWrapper()
+
+export const DesignStore = defineStore('design', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/directory.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/directory'
+const axios = new AxiosWrapper()
+
+export const DirectoryStore = defineStore('directory', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/footplate.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/footplate'
+const axios = new AxiosWrapper()
+
+export const FootplateStore = defineStore('footplate', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 49 - 0
src/store/api/platform/friend.js

@@ -0,0 +1,49 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/friend'
+const axios = new AxiosWrapper()
+
+export const FriendStore = defineStore('friend', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const list = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}/list`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    list,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/journal.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/journal'
+const axios = new AxiosWrapper()
+
+export const JournalStore = defineStore('journal', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/match.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/match'
+const axios = new AxiosWrapper()
+
+export const MatchStore = defineStore('match', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/news.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/news'
+const axios = new AxiosWrapper()
+
+export const NewsStore = defineStore('news', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/notes.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/notes'
+const axios = new AxiosWrapper()
+
+export const NotesStore = defineStore('notes', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/process.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/matchPath'
+const axios = new AxiosWrapper()
+
+export const ProcessStore = defineStore('process', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/project.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/project'
+const axios = new AxiosWrapper()
+
+export const ProjectStore = defineStore('project', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 54 - 0
src/store/api/platform/score.js

@@ -0,0 +1,54 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/score'
+const axios = new AxiosWrapper()
+
+export const ScoreStore = defineStore('score', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const list = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}/list`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const detail = async (payload) => {
+    const res = await axios.$get(`${url}/detail/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, '_id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    list,
+    fetch,
+    detail,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/sign.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/sign'
+const axios = new AxiosWrapper()
+
+export const SignStore = defineStore('sign', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/supply.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/supply'
+const axios = new AxiosWrapper()
+
+export const SupplyStore = defineStore('supply', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/platform/support.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/support'
+const axios = new AxiosWrapper()
+
+export const SupportStore = defineStore('support', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 47 - 0
src/store/api/system/dept.js

@@ -0,0 +1,47 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get, omit } from 'lodash-es'
+const url = '/dept'
+const axios = new AxiosWrapper()
+
+export const DeptStore = defineStore('dept', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  const nextLevel = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const querys = omit(payload, ['id'])
+    const res = await axios.$get(`${url}/nextLevel/${id}`, querys)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    nextLevel
+  }
+})

+ 40 - 0
src/store/api/system/dictData.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/dictData'
+const axios = new AxiosWrapper()
+
+export const DictDataStore = defineStore('dictData', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/system/dictType.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/dictType'
+const axios = new AxiosWrapper()
+
+export const DictTypeStore = defineStore('dictType', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 40 - 0
src/store/api/system/menus.js

@@ -0,0 +1,40 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/menus'
+const axios = new AxiosWrapper()
+
+export const MenusStore = defineStore('menus', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del
+  }
+})

+ 45 - 0
src/store/api/system/message.js

@@ -0,0 +1,45 @@
+import { defineStore } from 'pinia'
+import { AxiosWrapper } from '@/utils/axios-wrapper'
+import { get } from 'lodash-es'
+const url = '/message'
+const axios = new AxiosWrapper()
+
+export const MessageStore = defineStore('message', () => {
+  const query = async ({ skip = 0, limit = undefined, ...info } = {}) => {
+    let cond = {}
+    if (skip) cond.skip = skip
+    if (limit) cond.limit = limit
+    cond = { ...cond, ...info }
+    const res = await axios.$get(`${url}`, cond)
+    return res
+  }
+  const fetch = async (payload) => {
+    const res = await axios.$get(`${url}/${payload}`)
+    return res
+  }
+  const create = async (payload) => {
+    const res = await axios.$post(`${url}`, payload)
+    return res
+  }
+  const update = async (payload) => {
+    const id = get(payload, 'id', get(payload, 'id'))
+    const res = await axios.$post(`${url}/${id}`, payload)
+    return res
+  }
+  const del = async (payload) => {
+    const res = await axios.$delete(`${url}/${payload}`)
+    return res
+  }
+  const remind = async (payload) => {
+    const res = await axios.$post(`${url}/remind/${payload}`)
+    return res
+  }
+  return {
+    query,
+    fetch,
+    create,
+    update,
+    del,
+    remind
+  }
+})

+ 0 - 0
src/store/api/system/region.js


Một số tệp đã không được hiển thị bởi vì quá nhiều tập tin thay đổi trong này khác