xld 3 долоо хоног өмнө
parent
commit
328a45f74f
100 өөрчлөгдсөн 5095 нэмэгдсэн , 2 устгасан
  1. 1 0
      .dockerignore
  2. 11 0
      .env.development
  3. 7 0
      .env.production
  4. 5 0
      .gitignore
  5. 12 0
      .prettierrc
  6. 25 0
      Dockerfile
  7. 105 2
      README.md
  8. 4 0
      babel.config.js
  9. 29 0
      eslint.config.mjs
  10. 115 0
      index.html
  11. 10 0
      jsconfig.json
  12. 37 0
      limit.js
  13. 20 0
      openDocument.js
  14. 75 0
      package.json
  15. 6 0
      postcss.config.js
  16. BIN
      public/favicon.ico
  17. BIN
      public/logo.png
  18. 41 0
      src/App.vue
  19. 176 0
      src/api/api.js
  20. 26 0
      src/api/attachmentCategory.js
  21. 85 0
      src/api/authority.js
  22. 25 0
      src/api/authorityBtn.js
  23. 198 0
      src/api/autoCode.js
  24. 43 0
      src/api/breakpoint.js
  25. 32 0
      src/api/casbin.js
  26. 80 0
      src/api/customer.js
  27. 14 0
      src/api/email.js
  28. 128 0
      src/api/exportTemplate.js
  29. 67 0
      src/api/fileUploadAndDownload.js
  30. 19 0
      src/api/github.js
  31. 27 0
      src/api/initdb.js
  32. 14 0
      src/api/jwt.js
  33. 113 0
      src/api/menu.js
  34. 80 0
      src/api/sysDictionary.js
  35. 80 0
      src/api/sysDictionaryDetail.js
  36. 48 0
      src/api/sysOperationRecord.js
  37. 111 0
      src/api/sysParams.js
  38. 55 0
      src/api/system.js
  39. 181 0
      src/api/user.js
  40. BIN
      src/assets/404.png
  41. 0 0
      src/assets/background.svg
  42. BIN
      src/assets/banner.jpg
  43. BIN
      src/assets/banner2.jpg
  44. BIN
      src/assets/dashboard.png
  45. BIN
      src/assets/docs.png
  46. BIN
      src/assets/flipped-aurora.png
  47. BIN
      src/assets/github.png
  48. 1 0
      src/assets/icons/ai-gva.svg
  49. 1 0
      src/assets/icons/customer-gva.svg
  50. BIN
      src/assets/kefu.png
  51. BIN
      src/assets/login_background.jpg
  52. 33 0
      src/assets/login_background.svg
  53. 10 0
      src/assets/login_left.svg
  54. BIN
      src/assets/login_right_banner.jpg
  55. BIN
      src/assets/logo.jpg
  56. BIN
      src/assets/logo.png
  57. BIN
      src/assets/logo_login.png
  58. BIN
      src/assets/nav_logo.png
  59. BIN
      src/assets/noBody.png
  60. BIN
      src/assets/notFound.png
  61. BIN
      src/assets/qm.png
  62. BIN
      src/assets/video.png
  63. 67 0
      src/components/arrayCtrl/arrayCtrl.vue
  64. 44 0
      src/components/bottomInfo/bottomInfo.vue
  65. 54 0
      src/components/charts/index.vue
  66. 196 0
      src/components/commandMenu/index.vue
  67. 90 0
      src/components/customPic/index.vue
  68. 75 0
      src/components/exportExcel/exportExcel.vue
  69. 40 0
      src/components/exportExcel/exportTemplate.vue
  70. 45 0
      src/components/exportExcel/importExcel.vue
  71. 31 0
      src/components/office/docx.vue
  72. 36 0
      src/components/office/excel.vue
  73. 49 0
      src/components/office/index.vue
  74. 39 0
      src/components/office/pdf.vue
  75. 86 0
      src/components/richtext/rich-edit.vue
  76. 58 0
      src/components/richtext/rich-view.vue
  77. 87 0
      src/components/selectFile/selectFile.vue
  78. 86 0
      src/components/selectImage/selectComponent.vue
  79. 453 0
      src/components/selectImage/selectImage.vue
  80. 32 0
      src/components/svgIcon/svgIcon.vue
  81. 65 0
      src/components/upload/QR-code.vue
  82. 90 0
      src/components/upload/common.vue
  83. 237 0
      src/components/upload/cropper.vue
  84. 102 0
      src/components/upload/image.vue
  85. 33 0
      src/components/warningBar/warningBar.vue
  86. 53 0
      src/core/config.js
  87. 27 0
      src/core/gin-vue-admin.js
  88. 59 0
      src/core/global.js
  89. 40 0
      src/directive/auth.js
  90. 18 0
      src/hooks/charts.js
  91. 35 0
      src/hooks/responsive.js
  92. 23 0
      src/hooks/use-windows-resize.js
  93. 21 0
      src/main.js
  94. 70 0
      src/pathInfo.json
  95. 147 0
      src/permission.js
  96. 8 0
      src/pinia/index.js
  97. 133 0
      src/pinia/modules/app.js
  98. 41 0
      src/pinia/modules/dictionary.js
  99. 31 0
      src/pinia/modules/params.js
  100. 144 0
      src/pinia/modules/router.js

+ 1 - 0
.dockerignore

@@ -0,0 +1 @@
+node_modules/

+ 11 - 0
.env.development

@@ -0,0 +1,11 @@
+ENV = 'development'
+VITE_CLI_PORT = 8080
+VITE_SERVER_PORT = 8888
+VITE_BASE_API = /api
+VITE_FILE_API = /api
+VITE_BASE_PATH = http://127.0.0.1
+VITE_POSITION = close
+VITE_EDITOR = vscode
+// VITE_EDITOR = webstorm 如果使用webstorm开发且要使用dom定位到代码行功能 请先自定添加 webstorm到环境变量 再将VITE_EDITOR值修改为webstorm
+// 如果使用docker-compose开发模式,设置为下面的地址或本机主机IP
+//VITE_BASE_PATH = http://177.7.0.12

+ 7 - 0
.env.production

@@ -0,0 +1,7 @@
+ENV = 'production'
+
+#下方为上线需要用到的程序代理前缀,一般用于nginx代理转发
+VITE_BASE_API = /api
+VITE_FILE_API = /api
+#下方修改为你的线上ip(如果需要在线使用表单构建工具时使用,其余情况无需使用以下环境变量)
+VITE_BASE_PATH = https://demo.gin-vue-admin.com

+ 5 - 0
.gitignore

@@ -0,0 +1,5 @@
+node_modules/*
+package-lock.json
+yarn.lock
+bun.lockb
+config.yaml

+ 12 - 0
.prettierrc

@@ -0,0 +1,12 @@
+{
+  "printWidth": 80,
+  "tabWidth": 2,
+  "useTabs": false,
+  "semi": false,
+  "singleQuote": true,
+  "trailingComma": "none",
+  "bracketSpacing": true,
+  "arrowParens": "always",
+  "vueIndentScriptAndStyle": true,
+  "endOfLine": "lf"
+}

+ 25 - 0
Dockerfile

@@ -0,0 +1,25 @@
+# 如果需要用 cicd ,请设置环境变量:
+# variables:
+#     DOCKER_BUILDKIT: 1
+
+FROM node:20-slim AS base
+ENV PNPM_HOME="/pnpm"
+ENV PATH="$PNPM_HOME:$PATH"
+RUN corepack enable
+COPY . /app
+WORKDIR /app
+
+
+FROM base AS prod-deps
+RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install --prod
+
+FROM base AS build
+COPY --from=prod-deps /app/node_modules /app/node_modules
+RUN --mount=type=cache,id=pnpm,target=/pnpm/store pnpm install && pnpm run build
+
+
+FROM nginx:alpine
+LABEL MAINTAINER="[email protected]"
+COPY --from=base  /app/.docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
+COPY --from=build  /app/dist /usr/share/nginx/html
+RUN ls -al /usr/share/nginx/html

+ 105 - 2
README.md

@@ -1,3 +1,106 @@
-# mzk-web
+# gin-vue-admin web
 
-媒资库 web前段
+## Project setup
+
+```
+npm install
+```
+
+### Compiles and hot-reloads for development
+
+```
+npm run serve
+```
+
+### Compiles and minifies for production
+
+```
+npm run build
+```
+
+### Run your tests
+
+```
+npm run test
+```
+
+### Lints and fixes files
+
+```
+npm run lint
+```
+
+整理代码结构
+
+```lua
+web
+ ├── babel.config.js
+ ├── Dockerfile
+ ├── favicon.ico
+ ├── index.html                 -- 主页面
+ ├── limit.js                   -- 助手代码
+ ├── package.json               -- 包管理器代码
+ ├── src                        -- 源代码
+ │   ├── api                    -- api 组
+ │   ├── App.vue                -- 主页面
+ │   ├── assets                 -- 静态资源
+ │   ├── components             -- 全局组件
+ │   ├── core                   -- gva 组件包
+ │   │   ├── config.js          -- gva网站配置文件
+ │   │   ├── gin-vue-admin.js   -- 注册欢迎文件
+ │   │   └── global.js          -- 统一导入文件
+ │   ├── directive              -- v-auth 注册文件
+ │   ├── main.js                -- 主文件
+ │   ├── permission.js          -- 路由中间件
+ │   ├── pinia                  -- pinia 状态管理器,取代vuex
+ │   │   ├── index.js           -- 入口文件
+ │   │   └── modules            -- modules
+ │   │       ├── dictionary.js
+ │   │       ├── router.js
+ │   │       └── user.js
+ │   ├── router                 -- 路由声明文件
+ │   │   └── index.js
+ │   ├── style                  -- 全局样式
+ │   │   ├── base.scss
+ │   │   ├── basics.scss
+ │   │   ├── element_visiable.scss  -- 此处可以全局覆盖 element-plus 样式
+ │   │   ├── iconfont.css           -- 顶部几个icon的样式文件
+ │   │   ├── main.scss
+ │   │   ├── mobile.scss
+ │   │   └── newLogin.scss
+ │   ├── utils                  -- 方法包库
+ │   │   ├── asyncRouter.js     -- 动态路由相关
+ │   │   ├── bus.js             -- 全局mitt声明文件
+ │   │   ├── date.js            -- 日期相关
+ │   │   ├── dictionary.js      -- 获取字典方法
+ │   │   ├── downloadImg.js     -- 下载图片方法
+ │   │   ├── format.js          -- 格式整理相关
+ │   │   ├── image.js           -- 图片相关方法
+ │   │   ├── page.js            -- 设置页面标题
+ │   │   ├── request.js         -- 请求
+ │   │   └── stringFun.js       -- 字符串文件
+ |   ├── view -- 主要view代码
+ |   |   ├── about -- 关于我们
+ |   |   ├── dashboard -- 面板
+ |   |   ├── error -- 错误
+ |   |   ├── example --上传案例
+ |   |   ├── iconList -- icon列表
+ |   |   ├── init -- 初始化数据
+ |   |   |   ├── index -- 新版本
+ |   |   |   ├── init -- 旧版本
+ |   |   ├── layout  --  layout约束页面
+ |   |   |   ├── aside
+ |   |   |   ├── bottomInfo     -- bottomInfo
+ |   |   |   ├── screenfull     -- 全屏设置
+ |   |   |   ├── setting        -- 系统设置
+ |   |   |   └── index.vue      -- base 约束
+ |   |   ├── login              --登录
+ |   |   ├── person             --个人中心
+ |   |   ├── superAdmin         -- 超级管理员操作
+ |   |   ├── system             -- 系统检测页面
+ |   |   ├── systemTools        -- 系统配置相关页面
+ |   |   └── routerHolder.vue   -- page 入口页面
+ ├── vite.config.js             -- vite 配置文件
+ └── yarn.lock
+
+```

+ 4 - 0
babel.config.js

@@ -0,0 +1,4 @@
+module.exports = {
+  presets: [],
+  plugins: []
+}

+ 29 - 0
eslint.config.mjs

@@ -0,0 +1,29 @@
+import js from '@eslint/js'
+import pluginVue from 'eslint-plugin-vue'
+import globals from 'globals'
+
+export default [
+  js.configs.recommended,
+  ...pluginVue.configs['flat/essential'],
+  {
+    name: 'app/files-to-lint',
+    files: ['**/*.{js,mjs,jsx,vue}'],
+    languageOptions: {
+      ecmaVersion: 'latest',
+      sourceType: 'module',
+      globals: globals.node
+    },
+    rules: {
+      'vue/max-attributes-per-line': 0,
+      'vue/no-v-model-argument': 0,
+      'vue/multi-word-component-names': 'off',
+      'no-lone-blocks': 'off',
+      'no-extend-native': 'off',
+      'no-unused-vars': ['error', { argsIgnorePattern: '^_' }]
+    }
+  },
+  {
+    name: 'app/files-to-ignore',
+    ignores: ['**/dist/**', '**/build/*.js', '**/src/assets/**', '**/public/**']
+  }
+]

+ 115 - 0
index.html

@@ -0,0 +1,115 @@
+<!doctype html>
+<html lang="zh-cn" class="transition-colors">
+  <head>
+    <meta charset="utf-8" />
+    <meta http-equiv="X-UA-Compatible" content="IE=edge" />
+    <meta name="viewport" content="width=device-width,initial-scale=1.0" />
+    <meta
+      content="Gin,Vue,Admin.Gin-Vue-Admin,GVA,gin-vue-admin,后台管理框架,vue后台管理框架,gin-vue-admin文档,gin-vue-admin首页,gin-vue-admin"
+      name="keywords"
+    />
+    <link rel="icon" href="favicon.ico" />
+    <title></title>
+    <style>
+      .transition-colors {
+        transition-property: color, background-color, border-color,
+          text-decoration-color, fill, stroke;
+        transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1);
+        transition-duration: 150ms;
+      }
+      body {
+        margin: 0;
+        --64f90c3645474bd5: #409eff;
+      }
+      #gva-loading-box {
+        position: relative;
+        display: flex;
+        align-items: center;
+        justify-content: center;
+        height: 100vh;
+        width: 100vw;
+      }
+      #loading-text {
+        position: absolute;
+        bottom: calc(50% - 100px);
+        left: 0;
+        width: 100%;
+        text-align: center;
+        color: #666;
+        font-size: 14px;
+      }
+      #loading {
+        position: absolute;
+        top: calc(50% - 20px);
+        left: calc(50% - 20px);
+      }
+      @keyframes loader {
+        0% {
+          left: -100px;
+        }
+        100% {
+          left: 110%;
+        }
+      }
+      #box {
+        width: 50px;
+        height: 50px;
+        background: var(--64f90c3645474bd5);
+        animation: animate 0.5s linear infinite;
+        position: absolute;
+        top: 0;
+        left: 0;
+        border-radius: 3px;
+      }
+      @keyframes animate {
+        17% {
+          border-bottom-right-radius: 3px;
+        }
+        25% {
+          transform: translateY(9px) rotate(22.5deg);
+        }
+        50% {
+          transform: translateY(18px) scale(1, 0.9) rotate(45deg);
+          border-bottom-right-radius: 40px;
+        }
+        75% {
+          transform: translateY(9px) rotate(67.5deg);
+        }
+        100% {
+          transform: translateY(0) rotate(90deg);
+        }
+      }
+      #shadow {
+        width: 50px;
+        height: 5px;
+        background: #000;
+        opacity: 0.1;
+        position: absolute;
+        top: 59px;
+        left: 0;
+        border-radius: 50%;
+        animation: shadow 0.5s linear infinite;
+      }
+      .dark #shadow {
+        background: #fff;
+      }
+      @keyframes shadow {
+        50% {
+          transform: scale(1.2, 1);
+        }
+      }
+    </style>
+  </head>
+
+  <body>
+    <div id="gva-loading-box">
+      <div id="loading">
+        <div id="shadow"></div>
+        <div id="box"></div>
+      </div>
+      <div id="loading-text">系统正在加载中,请稍候...</div>
+    </div>
+    <div id="app"></div>
+    <script type="module" src="./src/main.js"></script>
+  </body>
+</html>

+ 10 - 0
jsconfig.json

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

+ 37 - 0
limit.js

@@ -0,0 +1,37 @@
+// 运行项目前通过node执行此脚本 (此脚本与 node_modules 目录同级)
+const fs = require('fs')
+const path = require('path')
+const wfPath = path.resolve(__dirname, './node_modules/.bin')
+
+fs.readdir(wfPath, (err, files) => {
+  if (err) {
+    console.log(err)
+  } else {
+    if (files.length !== 0) {
+      files.forEach((item) => {
+        if (item.split('.')[1] === 'cmd') {
+          replaceStr(`${wfPath}/${item}`, /"%_prog%"/, '%_prog%')
+        }
+      })
+    }
+  }
+})
+
+// 参数:[文件路径、 需要修改的字符串、修改后的字符串] (替换对应文件内字符串的公共函数)
+function replaceStr(filePath, sourceRegx, targetSrt) {
+  fs.readFile(filePath, (err, data) => {
+    if (err) {
+      console.log(err)
+    } else {
+      let str = data.toString()
+      str = str.replace(sourceRegx, targetSrt)
+      fs.writeFile(filePath, str, (err) => {
+        if (err) {
+          console.log(err)
+        } else {
+          console.log('\x1B[42m%s\x1B[0m', '文件修改成功')
+        }
+      })
+    }
+  })
+}

+ 20 - 0
openDocument.js

@@ -0,0 +1,20 @@
+/*
+此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。
+     未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。
+*/
+
+var child_process = require('child_process')
+
+var url = 'https://www.gin-vue-admin.com'
+var cmd = ''
+switch (process.platform) {
+  case 'win32':
+    cmd = 'start'
+    child_process.exec(cmd + ' ' + url)
+    break
+
+  case 'darwin':
+    cmd = 'open'
+    child_process.exec(cmd + ' ' + url)
+    break
+}

+ 75 - 0
package.json

@@ -0,0 +1,75 @@
+{
+  "name": "gin-vue-admin",
+  "version": "2.8.0",
+  "private": true,
+  "scripts": {
+    "serve": "node openDocument.js && vite --host --mode development",
+    "build": "vite build --mode production",
+    "limit-build": "npm install increase-memory-limit-fixbug cross-env -g && npm run fix-memory-limit && node ./limit && npm run build",
+    "preview": "vite preview",
+    "fix-memory-limit": "cross-env LIMIT=4096 increase-memory-limit"
+  },
+  "dependencies": {
+    "@element-plus/icons-vue": "^2.3.1",
+    "@form-create/designer": "^3.2.6",
+    "@form-create/element-ui": "^3.2.10",
+    "@vue-office/docx": "^1.6.2",
+    "@vue-office/excel": "^1.7.11",
+    "@vue-office/pdf": "^2.0.2",
+    "@vueuse/core": "^11.0.3",
+    "@vueuse/integrations": "^12.0.0",
+    "@wangeditor/editor": "^5.1.23",
+    "@wangeditor/editor-for-vue": "^5.1.12",
+    "ace-builds": "^1.36.4",
+    "axios": "^1.7.7",
+    "chokidar": "^4.0.0",
+    "core-js": "^3.38.1",
+    "echarts": "5.5.1",
+    "element-plus": "^2.8.5",
+    "highlight.js": "^11.10.0",
+    "marked": "14.1.1",
+    "marked-highlight": "^2.1.4",
+    "mitt": "^3.0.1",
+    "nprogress": "^0.2.0",
+    "path": "^0.12.7",
+    "pinia": "^2.2.2",
+    "qs": "^6.13.0",
+    "screenfull": "^6.0.2",
+    "sortablejs": "^1.15.3",
+    "spark-md5": "^3.0.2",
+    "tailwindcss": "^3.4.10",
+    "universal-cookie": "^7",
+    "vform3-builds": "^3.0.10",
+    "vite-auto-import-svg": "^1.1.0",
+    "vue": "^3.5.7",
+    "vue-cropper": "^1.1.4",
+    "vue-echarts": "^7.0.3",
+    "vue-qr": "^4.0.9",
+    "vue-router": "^4.4.3",
+    "vue3-ace-editor": "^2.2.4",
+    "vuedraggable": "^4.1.0"
+  },
+  "devDependencies": {
+    "@babel/eslint-parser": "^7.25.1",
+    "@eslint/js": "^9.14.0",
+    "@vitejs/plugin-legacy": "^5.4.2",
+    "@vitejs/plugin-vue": "^5.1.4",
+    "@vue/cli-plugin-babel": "~5.0.8",
+    "@vue/cli-plugin-eslint": "~5.0.8",
+    "@vue/cli-plugin-router": "~5.0.8",
+    "@vue/cli-plugin-vuex": "~5.0.8",
+    "@vue/cli-service": "~5.0.8",
+    "@vue/compiler-sfc": "^3.5.1",
+    "babel-plugin-import": "^1.13.8",
+    "chalk": "^5.3.0",
+    "dotenv": "^16.4.5",
+    "eslint": "^9.14.0",
+    "eslint-plugin-vue": "^9.30.0",
+    "sass": "^1.78.0",
+    "terser": "^5.31.6",
+    "vite": "^5.4.3",
+    "vite-plugin-banner": "^0.8.0",
+    "vite-plugin-importer": "^0.2.5",
+    "vite-plugin-vue-devtools": "^7.4.4"
+  }
+}

+ 6 - 0
postcss.config.js

@@ -0,0 +1,6 @@
+module.exports = {
+  plugins: {
+    tailwindcss: {},
+    autoprefixer: {}
+  }
+}

BIN
public/favicon.ico


BIN
public/logo.png


+ 41 - 0
src/App.vue

@@ -0,0 +1,41 @@
+<template>
+  <div
+    id="app"
+    class="bg-gray-50 text-slate-700 dark:text-slate-500 dark:bg-slate-800"
+  >
+    <el-config-provider :locale="zhCn">
+      <router-view />
+    </el-config-provider>
+  </div>
+</template>
+
+<script setup>
+  import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+  import { useAppStore } from '@/pinia'
+  useAppStore()
+  defineOptions({
+    name: 'App'
+  })
+</script>
+<style lang="scss">
+  // 引入初始化样式
+  #app {
+    height: 100vh;
+    overflow: hidden;
+    font-weight: 400 !important;
+  }
+  .el-button {
+    font-weight: 400 !important;
+  }
+
+  .gva-body-h {
+    min-height: calc(100% - 3rem);
+  }
+
+  .gva-container {
+    height: calc(100% - 2.5rem);
+  }
+  .gva-container2 {
+    height: calc(100% - 4.5rem);
+  }
+</style>

+ 176 - 0
src/api/api.js

@@ -0,0 +1,176 @@
+import service from '@/utils/request'
+
+// @Tags api
+// @Summary 分页获取角色列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body modelInterface.PageInfo true "分页获取用户列表"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /api/getApiList [post]
+// {
+//  page     int
+//	pageSize int
+// }
+export const getApiList = (data) => {
+  return service({
+    url: '/api/getApiList',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags Api
+// @Summary 创建基础api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateApiParams true "创建api"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /api/createApi [post]
+export const createApi = (data) => {
+  return service({
+    url: '/api/createApi',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags menu
+// @Summary 根据id获取菜单
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.GetById true "根据id获取菜单"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /menu/getApiById [post]
+export const getApiById = (data) => {
+  return service({
+    url: '/api/getApiById',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags Api
+// @Summary 更新api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateApiParams true "更新api"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /api/updateApi [post]
+export const updateApi = (data) => {
+  return service({
+    url: '/api/updateApi',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags Api
+// @Summary 更新api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateApiParams true "更新api"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /api/setAuthApi [post]
+export const setAuthApi = (data) => {
+  return service({
+    url: '/api/setAuthApi',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags Api
+// @Summary 获取所有的Api 不分页
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /api/getAllApis [post]
+export const getAllApis = (data) => {
+  return service({
+    url: '/api/getAllApis',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags Api
+// @Summary 删除指定api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body dbModel.Api true "删除api"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /api/deleteApi [post]
+export const deleteApi = (data) => {
+  return service({
+    url: '/api/deleteApi',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysApi
+// @Summary 删除选中Api
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "ID"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /api/deleteApisByIds [delete]
+export const deleteApisByIds = (data) => {
+  return service({
+    url: '/api/deleteApisByIds',
+    method: 'delete',
+    data
+  })
+}
+
+// FreshCasbin
+// @Tags      SysApi
+// @Summary   刷新casbin缓存
+// @accept    application/json
+// @Produce   application/json
+// @Success   200   {object}  response.Response{msg=string}  "刷新成功"
+// @Router    /api/freshCasbin [get]
+export const freshCasbin = () => {
+  return service({
+    url: '/api/freshCasbin',
+    method: 'get'
+  })
+}
+
+export const syncApi = () => {
+  return service({
+    url: '/api/syncApi',
+    method: 'get'
+  })
+}
+
+export const getApiGroups = () => {
+  return service({
+    url: '/api/getApiGroups',
+    method: 'get'
+  })
+}
+
+export const ignoreApi = (data) => {
+  return service({
+    url: '/api/ignoreApi',
+    method: 'post',
+    data
+  })
+}
+
+export const enterSyncApi = (data) => {
+  return service({
+    url: '/api/enterSyncApi',
+    method: 'post',
+    data
+  })
+}

+ 26 - 0
src/api/attachmentCategory.js

@@ -0,0 +1,26 @@
+import service from '@/utils/request'
+// 分类列表
+export const getCategoryList = () => {
+    return service({
+        url: '/attachmentCategory/getCategoryList',
+        method: 'get',
+    })
+}
+
+// 添加/编辑分类
+export const addCategory = (data) => {
+    return service({
+        url: '/attachmentCategory/addCategory',
+        method: 'post',
+        data
+    })
+}
+
+// 删除分类
+export const deleteCategory = (data) => {
+    return service({
+        url: '/attachmentCategory/deleteCategory',
+        method: 'post',
+        data
+    })
+}

+ 85 - 0
src/api/authority.js

@@ -0,0 +1,85 @@
+import service from '@/utils/request'
+// @Router /authority/getAuthorityList [post]
+export const getAuthorityList = (data) => {
+  return service({
+    url: '/authority/getAuthorityList',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 删除角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body {authorityId uint} true "删除角色"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /authority/deleteAuthority [post]
+export const deleteAuthority = (data) => {
+  return service({
+    url: '/authority/deleteAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 创建角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateAuthorityPatams true "创建角色"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /authority/createAuthority [post]
+export const createAuthority = (data) => {
+  return service({
+    url: '/authority/createAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags authority
+// @Summary 拷贝角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateAuthorityPatams true "拷贝角色"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"拷贝成功"}"
+// @Router /authority/copyAuthority [post]
+export const copyAuthority = (data) => {
+  return service({
+    url: '/authority/copyAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 设置角色资源权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body sysModel.SysAuthority true "设置角色资源权限"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
+// @Router /authority/setDataAuthority [post]
+export const setDataAuthority = (data) => {
+  return service({
+    url: '/authority/setDataAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 修改角色
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysAuthority true "修改角色"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
+// @Router /authority/setDataAuthority [post]
+export const updateAuthority = (data) => {
+  return service({
+    url: '/authority/updateAuthority',
+    method: 'put',
+    data
+  })
+}

+ 25 - 0
src/api/authorityBtn.js

@@ -0,0 +1,25 @@
+import service from '@/utils/request'
+
+export const getAuthorityBtnApi = (data) => {
+  return service({
+    url: '/authorityBtn/getAuthorityBtn',
+    method: 'post',
+    data
+  })
+}
+
+export const setAuthorityBtnApi = (data) => {
+  return service({
+    url: '/authorityBtn/setAuthorityBtn',
+    method: 'post',
+    data
+  })
+}
+
+export const canRemoveAuthorityBtnApi = (params) => {
+  return service({
+    url: '/authorityBtn/canRemoveAuthorityBtn',
+    method: 'post',
+    params
+  })
+}

+ 198 - 0
src/api/autoCode.js

@@ -0,0 +1,198 @@
+import service from '@/utils/request'
+
+export const preview = (data) => {
+  return service({
+    url: '/autoCode/preview',
+    method: 'post',
+    data
+  })
+}
+
+export const createTemp = (data) => {
+  return service({
+    url: '/autoCode/createTemp',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysApi
+// @Summary 获取当前所有数据库
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /autoCode/getDatabase [get]
+export const getDB = (params) => {
+  return service({
+    url: '/autoCode/getDB',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysApi
+// @Summary 获取当前数据库所有表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /autoCode/getTables [get]
+export const getTable = (params) => {
+  return service({
+    url: '/autoCode/getTables',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysApi
+// @Summary 获取当前数据库所有表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /autoCode/getColumn [get]
+export const getColumn = (params) => {
+  return service({
+    url: '/autoCode/getColumn',
+    method: 'get',
+    params
+  })
+}
+
+export const getSysHistory = (data) => {
+  return service({
+    url: '/autoCode/getSysHistory',
+    method: 'post',
+    data
+  })
+}
+
+export const rollback = (data) => {
+  return service({
+    url: '/autoCode/rollback',
+    method: 'post',
+    data
+  })
+}
+
+export const getMeta = (data) => {
+  return service({
+    url: '/autoCode/getMeta',
+    method: 'post',
+    data
+  })
+}
+
+export const delSysHistory = (data) => {
+  return service({
+    url: '/autoCode/delSysHistory',
+    method: 'post',
+    data
+  })
+}
+
+export const createPackageApi = (data) => {
+  return service({
+    url: '/autoCode/createPackage',
+    method: 'post',
+    data
+  })
+}
+
+export const getPackageApi = () => {
+  return service({
+    url: '/autoCode/getPackage',
+    method: 'post'
+  })
+}
+
+export const deletePackageApi = (data) => {
+  return service({
+    url: '/autoCode/delPackage',
+    method: 'post',
+    data
+  })
+}
+
+export const getTemplatesApi = () => {
+  return service({
+    url: '/autoCode/getTemplates',
+    method: 'get'
+  })
+}
+
+export const installPlug = (data) => {
+  return service({
+    url: '/autoCode/installPlug',
+    method: 'post',
+    data
+  })
+}
+
+export const pubPlug = (params) => {
+  return service({
+    url: '/autoCode/pubPlug',
+    method: 'post',
+    params
+  })
+}
+
+export const llmAuto = (data) => {
+  return service({
+    url: '/autoCode/llmAuto',
+    method: 'post',
+    data: { ...data, mode: 'ai' },
+    timeout: 1000 * 60 * 10,
+    loadingOption: {
+      lock: true,
+      fullscreen: true,
+      text: `小淼正在思考,请稍候...`
+    }
+  })
+}
+
+export const butler = (data) => {
+  return service({
+    url: '/autoCode/llmAuto',
+    method: 'post',
+    data: { ...data, mode: 'butler' },
+    timeout: 1000 * 60 * 10
+  })
+}
+
+
+export const eye = (data) => {
+  return service({
+    url: '/autoCode/llmAuto',
+    method: 'post',
+    data: { ...data, mode: 'eye' },
+    timeout: 1000 * 60 * 10
+  })
+}
+
+
+export const addFunc = (data) => {
+  return service({
+    url: '/autoCode/addFunc',
+    method: 'post',
+    data
+  })
+}
+
+export const initMenu = (data) => {
+  return service({
+    url: '/autoCode/initMenu',
+    method: 'post',
+    data
+  })
+}
+
+export const initAPI = (data) => {
+  return service({
+    url: '/autoCode/initAPI',
+    method: 'post',
+    data
+  })
+}

+ 43 - 0
src/api/breakpoint.js

@@ -0,0 +1,43 @@
+import service from '@/utils/request'
+// @Summary 设置角色资源权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body sysModel.SysAuthority true "设置角色资源权限"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"设置成功"}"
+// @Router /authority/setDataAuthority [post]
+
+export const findFile = (params) => {
+  return service({
+    url: '/fileUploadAndDownload/findFile',
+    method: 'get',
+    params
+  })
+}
+
+export const breakpointContinue = (data) => {
+  return service({
+    url: '/fileUploadAndDownload/breakpointContinue',
+    method: 'post',
+    donNotShowLoading: true,
+    headers: { 'Content-Type': 'multipart/form-data' },
+    data
+  })
+}
+
+export const breakpointContinueFinish = (params) => {
+  return service({
+    url: '/fileUploadAndDownload/breakpointContinueFinish',
+    method: 'post',
+    params
+  })
+}
+
+export const removeChunk = (data, params) => {
+  return service({
+    url: '/fileUploadAndDownload/removeChunk',
+    method: 'post',
+    data,
+    params
+  })
+}

+ 32 - 0
src/api/casbin.js

@@ -0,0 +1,32 @@
+import service from '@/utils/request'
+// @Tags authority
+// @Summary 更改角色api权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateAuthorityPatams true "更改角色api权限"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /casbin/UpdateCasbin [post]
+export const UpdateCasbin = (data) => {
+  return service({
+    url: '/casbin/updateCasbin',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags casbin
+// @Summary 获取权限列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.CreateAuthorityPatams true "获取权限列表"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /casbin/getPolicyPathByAuthorityId [post]
+export const getPolicyPathByAuthorityId = (data) => {
+  return service({
+    url: '/casbin/getPolicyPathByAuthorityId',
+    method: 'post',
+    data
+  })
+}

+ 80 - 0
src/api/customer.js

@@ -0,0 +1,80 @@
+import service from '@/utils/request'
+// @Tags SysApi
+// @Summary 删除客户
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body dbModel.ExaCustomer true "删除客户"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /customer/customer [post]
+export const createExaCustomer = (data) => {
+  return service({
+    url: '/customer/customer',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysApi
+// @Summary 更新客户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body dbModel.ExaCustomer true "更新客户信息"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /customer/customer [put]
+export const updateExaCustomer = (data) => {
+  return service({
+    url: '/customer/customer',
+    method: 'put',
+    data
+  })
+}
+
+// @Tags SysApi
+// @Summary 创建客户
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body dbModel.ExaCustomer true "创建客户"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /customer/customer [delete]
+export const deleteExaCustomer = (data) => {
+  return service({
+    url: '/customer/customer',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysApi
+// @Summary 获取单一客户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body dbModel.ExaCustomer true "获取单一客户信息"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /customer/customer [get]
+export const getExaCustomer = (params) => {
+  return service({
+    url: '/customer/customer',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysApi
+// @Summary 获取权限客户列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body modelInterface.PageInfo true "获取权限客户列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /customer/customerList [get]
+export const getExaCustomerList = (params) => {
+  return service({
+    url: '/customer/customerList',
+    method: 'get',
+    params
+  })
+}

+ 14 - 0
src/api/email.js

@@ -0,0 +1,14 @@
+import service from '@/utils/request'
+// @Tags email
+// @Summary 发送测试邮件
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
+// @Router /email/emailTest [post]
+export const emailTest = (data) => {
+  return service({
+    url: '/email/emailTest',
+    method: 'post',
+    data
+  })
+}

+ 128 - 0
src/api/exportTemplate.js

@@ -0,0 +1,128 @@
+import service from '@/utils/request'
+
+// @Tags SysExportTemplate
+// @Summary 创建导出模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysExportTemplate true "创建导出模板"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /sysExportTemplate/createSysExportTemplate [post]
+export const createSysExportTemplate = (data) => {
+  return service({
+    url: '/sysExportTemplate/createSysExportTemplate',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysExportTemplate
+// @Summary 删除导出模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysExportTemplate true "删除导出模板"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysExportTemplate/deleteSysExportTemplate [delete]
+export const deleteSysExportTemplate = (data) => {
+  return service({
+    url: '/sysExportTemplate/deleteSysExportTemplate',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysExportTemplate
+// @Summary 批量删除导出模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "批量删除导出模板"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysExportTemplate/deleteSysExportTemplate [delete]
+export const deleteSysExportTemplateByIds = (data) => {
+  return service({
+    url: '/sysExportTemplate/deleteSysExportTemplateByIds',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysExportTemplate
+// @Summary 更新导出模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysExportTemplate true "更新导出模板"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /sysExportTemplate/updateSysExportTemplate [put]
+export const updateSysExportTemplate = (data) => {
+  return service({
+    url: '/sysExportTemplate/updateSysExportTemplate',
+    method: 'put',
+    data
+  })
+}
+
+// @Tags SysExportTemplate
+// @Summary 用id查询导出模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query model.SysExportTemplate true "用id查询导出模板"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /sysExportTemplate/findSysExportTemplate [get]
+export const findSysExportTemplate = (params) => {
+  return service({
+    url: '/sysExportTemplate/findSysExportTemplate',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysExportTemplate
+// @Summary 分页获取导出模板列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query request.PageInfo true "分页获取导出模板列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysExportTemplate/getSysExportTemplateList [get]
+export const getSysExportTemplateList = (params) => {
+  return service({
+    url: '/sysExportTemplate/getSysExportTemplateList',
+    method: 'get',
+    params
+  })
+}
+
+
+// ExportExcel 导出表格token
+// @Tags SysExportTemplate
+// @Summary 导出表格
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Router /sysExportTemplate/exportExcel [get]
+export const exportExcel = (params) => {
+  return service({
+    url: '/sysExportTemplate/exportExcel',
+    method: 'get',
+    params
+  })
+}
+
+// ExportTemplate 导出表格模板
+// @Tags SysExportTemplate
+// @Summary 导出表格模板
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Router /sysExportTemplate/exportTemplate [get]
+export const exportTemplate = (params) => {
+  return service({
+    url: '/sysExportTemplate/exportTemplate',
+    method: 'get',
+    params
+  })
+}

+ 67 - 0
src/api/fileUploadAndDownload.js

@@ -0,0 +1,67 @@
+import service from '@/utils/request'
+// @Tags FileUploadAndDownload
+// @Summary 分页文件列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body modelInterface.PageInfo true "分页获取文件户列表"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /fileUploadAndDownload/getFileList [post]
+export const getFileList = (data) => {
+  return service({
+    url: '/fileUploadAndDownload/getFileList',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags FileUploadAndDownload
+// @Summary 删除文件
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Param data body dbModel.FileUploadAndDownload true "传入文件里面id即可"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"返回成功"}"
+// @Router /fileUploadAndDownload/deleteFile [post]
+export const deleteFile = (data) => {
+  return service({
+    url: '/fileUploadAndDownload/deleteFile',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 编辑文件名或者备注
+ * @param data
+ * @returns {*}
+ */
+export const editFileName = (data) => {
+  return service({
+    url: '/fileUploadAndDownload/editFileName',
+    method: 'post',
+    data
+  })
+}
+
+/**
+ * 导入URL
+ * @param data
+ * @returns {*}
+ */
+export const importURL = (data) => {
+  return service({
+    url: '/fileUploadAndDownload/importURL',
+    method: 'post',
+    data
+  })
+}
+
+
+// 上传文件 暂时用于头像上传
+export const uploadFile = (data) => {
+  return service({
+    url: "/fileUploadAndDownload/upload",
+    method: "post",
+    data,
+  });
+};

+ 19 - 0
src/api/github.js

@@ -0,0 +1,19 @@
+import axios from 'axios'
+
+const service = axios.create()
+
+export function Commits(page) {
+  return service({
+    url:
+      'https://api.github.com/repos/flipped-aurora/gin-vue-admin/commits?page=' +
+      page,
+    method: 'get'
+  })
+}
+
+export function Members() {
+  return service({
+    url: 'https://api.github.com/orgs/FLIPPED-AURORA/members',
+    method: 'get'
+  })
+}

+ 27 - 0
src/api/initdb.js

@@ -0,0 +1,27 @@
+import service from '@/utils/request'
+// @Tags InitDB
+// @Summary 初始化用户数据库
+// @Produce  application/json
+// @Param data body request.InitDB true "初始化数据库参数"
+// @Success 200 {string} string "{"code":0,"data":{},"msg":"自动创建数据库成功"}"
+// @Router /init/initdb [post]
+export const initDB = (data) => {
+  return service({
+    url: '/init/initdb',
+    method: 'post',
+    data,
+    donNotShowLoading: true
+  })
+}
+
+// @Tags CheckDB
+// @Summary 初始化用户数据库
+// @Produce  application/json
+// @Success 200 {string} string "{"code":0,"data":{},"msg":"探测完成"}"
+// @Router /init/checkdb [post]
+export const checkDB = () => {
+  return service({
+    url: '/init/checkdb',
+    method: 'post'
+  })
+}

+ 14 - 0
src/api/jwt.js

@@ -0,0 +1,14 @@
+import service from '@/utils/request'
+// @Tags jwt
+// @Summary jwt加入黑名单
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"拉黑成功"}"
+// @Router /jwt/jsonInBlacklist [post]
+export const jsonInBlacklist = () => {
+  return service({
+    url: '/jwt/jsonInBlacklist',
+    method: 'post'
+  })
+}

+ 113 - 0
src/api/menu.js

@@ -0,0 +1,113 @@
+import service from '@/utils/request'
+// @Summary 用户登录 获取动态路由
+// @Produce  application/json
+// @Param 可以什么都不填 调一下即可
+// @Router /menu/getMenu [post]
+export const asyncMenu = () => {
+  return service({
+    url: '/menu/getMenu',
+    method: 'post'
+  })
+}
+
+// @Summary 获取menu列表
+// @Produce  application/json
+// @Param {
+//  page     int
+//	pageSize int
+// }
+// @Router /menu/getMenuList [post]
+export const getMenuList = (data) => {
+  return service({
+    url: '/menu/getMenuList',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 新增基础menu
+// @Produce  application/json
+// @Param menu Object
+// @Router /menu/getMenuList [post]
+export const addBaseMenu = (data) => {
+  return service({
+    url: '/menu/addBaseMenu',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 获取基础路由列表
+// @Produce  application/json
+// @Param 可以什么都不填 调一下即可
+// @Router /menu/getBaseMenuTree [post]
+export const getBaseMenuTree = () => {
+  return service({
+    url: '/menu/getBaseMenuTree',
+    method: 'post'
+  })
+}
+
+// @Summary 添加用户menu关联关系
+// @Produce  application/json
+// @Param menus Object authorityId string
+// @Router /menu/getMenuList [post]
+export const addMenuAuthority = (data) => {
+  return service({
+    url: '/menu/addMenuAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 获取用户menu关联关系
+// @Produce  application/json
+// @Param authorityId string
+// @Router /menu/getMenuAuthority [post]
+export const getMenuAuthority = (data) => {
+  return service({
+    url: '/menu/getMenuAuthority',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 删除menu
+// @Produce  application/json
+// @Param ID float64
+// @Router /menu/deleteBaseMenu [post]
+export const deleteBaseMenu = (data) => {
+  return service({
+    url: '/menu/deleteBaseMenu',
+    method: 'post',
+    data
+  })
+}
+
+// @Summary 修改menu列表
+// @Produce  application/json
+// @Param menu Object
+// @Router /menu/updateBaseMenu [post]
+export const updateBaseMenu = (data) => {
+  return service({
+    url: '/menu/updateBaseMenu',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags menu
+// @Summary 根据id获取菜单
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.GetById true "根据id获取菜单"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /menu/getBaseMenuById [post]
+export const getBaseMenuById = (data) => {
+  return service({
+    url: '/menu/getBaseMenuById',
+    method: 'post',
+    data
+  })
+}

+ 80 - 0
src/api/sysDictionary.js

@@ -0,0 +1,80 @@
+import service from '@/utils/request'
+// @Tags SysDictionary
+// @Summary 创建SysDictionary
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionary true "创建SysDictionary"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysDictionary/createSysDictionary [post]
+export const createSysDictionary = (data) => {
+  return service({
+    url: '/sysDictionary/createSysDictionary',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysDictionary
+// @Summary 删除SysDictionary
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionary true "删除SysDictionary"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysDictionary/deleteSysDictionary [delete]
+export const deleteSysDictionary = (data) => {
+  return service({
+    url: '/sysDictionary/deleteSysDictionary',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysDictionary
+// @Summary 更新SysDictionary
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionary true "更新SysDictionary"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /sysDictionary/updateSysDictionary [put]
+export const updateSysDictionary = (data) => {
+  return service({
+    url: '/sysDictionary/updateSysDictionary',
+    method: 'put',
+    data
+  })
+}
+
+// @Tags SysDictionary
+// @Summary 用id查询SysDictionary
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionary true "用id查询SysDictionary"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /sysDictionary/findSysDictionary [get]
+export const findSysDictionary = (params) => {
+  return service({
+    url: '/sysDictionary/findSysDictionary',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysDictionary
+// @Summary 分页获取SysDictionary列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.PageInfo true "分页获取SysDictionary列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysDictionary/getSysDictionaryList [get]
+export const getSysDictionaryList = (params) => {
+  return service({
+    url: '/sysDictionary/getSysDictionaryList',
+    method: 'get',
+    params
+  })
+}

+ 80 - 0
src/api/sysDictionaryDetail.js

@@ -0,0 +1,80 @@
+import service from '@/utils/request'
+// @Tags SysDictionaryDetail
+// @Summary 创建SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionaryDetail true "创建SysDictionaryDetail"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysDictionaryDetail/createSysDictionaryDetail [post]
+export const createSysDictionaryDetail = (data) => {
+  return service({
+    url: '/sysDictionaryDetail/createSysDictionaryDetail',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysDictionaryDetail
+// @Summary 删除SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionaryDetail true "删除SysDictionaryDetail"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysDictionaryDetail/deleteSysDictionaryDetail [delete]
+export const deleteSysDictionaryDetail = (data) => {
+  return service({
+    url: '/sysDictionaryDetail/deleteSysDictionaryDetail',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysDictionaryDetail
+// @Summary 更新SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionaryDetail true "更新SysDictionaryDetail"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /sysDictionaryDetail/updateSysDictionaryDetail [put]
+export const updateSysDictionaryDetail = (data) => {
+  return service({
+    url: '/sysDictionaryDetail/updateSysDictionaryDetail',
+    method: 'put',
+    data
+  })
+}
+
+// @Tags SysDictionaryDetail
+// @Summary 用id查询SysDictionaryDetail
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysDictionaryDetail true "用id查询SysDictionaryDetail"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /sysDictionaryDetail/findSysDictionaryDetail [get]
+export const findSysDictionaryDetail = (params) => {
+  return service({
+    url: '/sysDictionaryDetail/findSysDictionaryDetail',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysDictionaryDetail
+// @Summary 分页获取SysDictionaryDetail列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.PageInfo true "分页获取SysDictionaryDetail列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysDictionaryDetail/getSysDictionaryDetailList [get]
+export const getSysDictionaryDetailList = (params) => {
+  return service({
+    url: '/sysDictionaryDetail/getSysDictionaryDetailList',
+    method: 'get',
+    params
+  })
+}

+ 48 - 0
src/api/sysOperationRecord.js

@@ -0,0 +1,48 @@
+import service from '@/utils/request'
+// @Tags SysOperationRecord
+// @Summary 删除SysOperationRecord
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysOperationRecord true "删除SysOperationRecord"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysOperationRecord/deleteSysOperationRecord [delete]
+export const deleteSysOperationRecord = (data) => {
+  return service({
+    url: '/sysOperationRecord/deleteSysOperationRecord',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysOperationRecord
+// @Summary 删除SysOperationRecord
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "删除SysOperationRecord"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysOperationRecord/deleteSysOperationRecord [delete]
+export const deleteSysOperationRecordByIds = (data) => {
+  return service({
+    url: '/sysOperationRecord/deleteSysOperationRecordByIds',
+    method: 'delete',
+    data
+  })
+}
+
+// @Tags SysOperationRecord
+// @Summary 分页获取SysOperationRecord列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.PageInfo true "分页获取SysOperationRecord列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysOperationRecord/getSysOperationRecordList [get]
+export const getSysOperationRecordList = (params) => {
+  return service({
+    url: '/sysOperationRecord/getSysOperationRecordList',
+    method: 'get',
+    params
+  })
+}

+ 111 - 0
src/api/sysParams.js

@@ -0,0 +1,111 @@
+import service from '@/utils/request'
+// @Tags SysParams
+// @Summary 创建参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysParams true "创建参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"创建成功"}"
+// @Router /sysParams/createSysParams [post]
+export const createSysParams = (data) => {
+  return service({
+    url: '/sysParams/createSysParams',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags SysParams
+// @Summary 删除参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysParams true "删除参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysParams/deleteSysParams [delete]
+export const deleteSysParams = (params) => {
+  return service({
+    url: '/sysParams/deleteSysParams',
+    method: 'delete',
+    params
+  })
+}
+
+// @Tags SysParams
+// @Summary 批量删除参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.IdsReq true "批量删除参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"删除成功"}"
+// @Router /sysParams/deleteSysParams [delete]
+export const deleteSysParamsByIds = (params) => {
+  return service({
+    url: '/sysParams/deleteSysParamsByIds',
+    method: 'delete',
+    params
+  })
+}
+
+// @Tags SysParams
+// @Summary 更新参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysParams true "更新参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"更新成功"}"
+// @Router /sysParams/updateSysParams [put]
+export const updateSysParams = (data) => {
+  return service({
+    url: '/sysParams/updateSysParams',
+    method: 'put',
+    data
+  })
+}
+
+// @Tags SysParams
+// @Summary 用id查询参数
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query model.SysParams true "用id查询参数"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"查询成功"}"
+// @Router /sysParams/findSysParams [get]
+export const findSysParams = (params) => {
+  return service({
+    url: '/sysParams/findSysParams',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysParams
+// @Summary 分页获取参数列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data query request.PageInfo true "分页获取参数列表"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /sysParams/getSysParamsList [get]
+export const getSysParamsList = (params) => {
+  return service({
+    url: '/sysParams/getSysParamsList',
+    method: 'get',
+    params
+  })
+}
+
+// @Tags SysParams
+// @Summary 不需要鉴权的参数接口
+// @accept application/json
+// @Produce application/json
+// @Param data query systemReq.SysParamsSearch true "分页获取参数列表"
+// @Success 200 {object} response.Response{data=object,msg=string} "获取成功"
+// @Router /sysParams/getSysParam [get]
+export const getSysParam = (params) => {
+  return service({
+    url: '/sysParams/getSysParam',
+    method: 'get',
+    params
+  })
+}

+ 55 - 0
src/api/system.js

@@ -0,0 +1,55 @@
+import service from '@/utils/request'
+// @Tags systrm
+// @Summary 获取配置文件内容
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
+// @Router /system/getSystemConfig [post]
+export const getSystemConfig = () => {
+  return service({
+    url: '/system/getSystemConfig',
+    method: 'post'
+  })
+}
+
+// @Tags system
+// @Summary 设置配置文件内容
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Param data body sysModel.System true
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
+// @Router /system/setSystemConfig [post]
+export const setSystemConfig = (data) => {
+  return service({
+    url: '/system/setSystemConfig',
+    method: 'post',
+    data
+  })
+}
+
+// @Tags system
+// @Summary 获取服务器运行状态
+// @Security ApiKeyAuth
+// @Produce  application/json
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"返回成功"}"
+// @Router /system/getServerInfo [post]
+export const getSystemState = () => {
+  return service({
+    url: '/system/getServerInfo',
+    method: 'post',
+    donNotShowLoading: true
+  })
+}
+
+/**
+ * 重启服务
+ * @param data
+ * @returns {*}
+ */
+export const reloadSystem = (data) => {
+  return service({
+    url: '/system/reloadSystem',
+    method: 'post',
+    data
+  })
+}

+ 181 - 0
src/api/user.js

@@ -0,0 +1,181 @@
+import service from '@/utils/request'
+// @Summary 用户登录
+// @Produce  application/json
+// @Param data body {username:"string",password:"string"}
+// @Router /base/login [post]
+export const login = (data) => {
+  return service({
+    url: '/base/login',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Summary 获取验证码
+// @Produce  application/json
+// @Param data body {username:"string",password:"string"}
+// @Router /base/captcha [post]
+export const captcha = () => {
+  return service({
+    url: '/base/captcha',
+    method: 'post'
+  })
+}
+
+// @Summary 用户注册
+// @Produce  application/json
+// @Param data body {username:"string",password:"string"}
+// @Router /base/resige [post]
+export const register = (data) => {
+  return service({
+    url: '/user/admin_register',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Summary 修改密码
+// @Produce  application/json
+// @Param data body {username:"string",password:"string",newPassword:"string"}
+// @Router /user/changePassword [post]
+export const changePassword = (data) => {
+  return service({
+    url: '/user/changePassword',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Tags User
+// @Summary 分页获取用户列表
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body modelInterface.PageInfo true "分页获取用户列表"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /user/getUserList [post]
+export const getUserList = (data) => {
+  return service({
+    url: '/user/getUserList',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Tags User
+// @Summary 设置用户权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.SetUserAuth true "设置用户权限"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/setUserAuthority [post]
+export const setUserAuthority = (data) => {
+  return service({
+    url: '/user/setUserAuthority',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Tags SysUser
+// @Summary 删除用户
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body request.SetUserAuth true "删除用户"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/deleteUser [delete]
+export const deleteUser = (data) => {
+  return service({
+    url: '/user/deleteUser',
+    method: 'delete',
+    data: data
+  })
+}
+
+// @Tags SysUser
+// @Summary 设置用户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysUser true "设置用户信息"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/setUserInfo [put]
+export const setUserInfo = (data) => {
+  return service({
+    url: '/user/setUserInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// @Tags SysUser
+// @Summary 设置用户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysUser true "设置用户信息"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/setSelfInfo [put]
+export const setSelfInfo = (data) => {
+  return service({
+    url: '/user/setSelfInfo',
+    method: 'put',
+    data: data
+  })
+}
+
+// @Tags SysUser
+// @Summary 设置自身界面配置
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body model.SysUser true "设置自身界面配置"
+// @Success 200 {string} string "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/setSelfSetting [put]
+export const setSelfSetting = (data) => {
+  return service({
+    url: '/user/setSelfSetting',
+    method: 'put',
+    data: data
+  })
+}
+
+// @Tags User
+// @Summary 设置用户权限
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Param data body api.setUserAuthorities true "设置用户权限"
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"修改成功"}"
+// @Router /user/setUserAuthorities [post]
+export const setUserAuthorities = (data) => {
+  return service({
+    url: '/user/setUserAuthorities',
+    method: 'post',
+    data: data
+  })
+}
+
+// @Tags User
+// @Summary 获取用户信息
+// @Security ApiKeyAuth
+// @accept application/json
+// @Produce application/json
+// @Success 200 {string} json "{"success":true,"data":{},"msg":"获取成功"}"
+// @Router /user/getUserInfo [get]
+export const getUserInfo = () => {
+  return service({
+    url: '/user/getUserInfo',
+    method: 'get'
+  })
+}
+
+export const resetPassword = (data) => {
+  return service({
+    url: '/user/resetPassword',
+    method: 'post',
+    data: data
+  })
+}

BIN
src/assets/404.png


Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 0 - 0
src/assets/background.svg


BIN
src/assets/banner.jpg


BIN
src/assets/banner2.jpg


BIN
src/assets/dashboard.png


BIN
src/assets/docs.png


BIN
src/assets/flipped-aurora.png


BIN
src/assets/github.png


+ 1 - 0
src/assets/icons/ai-gva.svg

@@ -0,0 +1 @@
+<svg t="1718869075082" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="13564" width="200" height="200"><path d="M274.344554 173.673875c16.429399 0 30.950568 8.00526 39.956484 20.338945a34.906655 34.906655 0 0 1 14.660796 15.754537l1.117013 2.815803 210.138065 597.927736c6.795162 19.268474-5.329083 40.817516-27.041022 48.147913-20.641469 6.95806-42.493035-1.419537-50.451753-18.803052l-1.117013-2.815803-54.477653-154.939008H134.997185L80.566074 837.039954c-6.771891 19.268474-29.856826 28.949253-51.568765 21.618855-20.641469-6.981331-32.602816-26.761769-27.925325-45.239025l0.861031-2.908888L212.047809 212.58316c4.025901-11.426112 13.799764-19.477914 25.598214-22.619512 9.029188-10.006575 22.107548-16.289773 36.67526-16.289773z m386.416675 169.460176c21.828295 0 39.723774 10.890876 41.469106 24.713912l0.116356 2.210755v461.652153c0 14.893506-18.616883 26.947938-41.585462 26.947938-21.805024 0-39.700503-10.890876-41.422565-24.737183l-0.162897-2.210755V370.081989c0-14.870235 18.616883-26.924667 41.585462-26.924667z m-389.697901-48.171184L163.620643 600.628812h214.88537l-107.442685-305.665945z m602.163076-206.181978a12.566396 12.566396 0 0 1 8.144887 8.051802l32.509731 99.041817 101.694723 36.07021a12.566396 12.566396 0 0 1-0.791218 23.922695l-99.53051 27.995137-30.857483 97.552467a12.566396 12.566396 0 0 1-23.899423 0.116355l-32.509732-99.018546-98.669479-31.322905a12.566396 12.566396 0 0 1-0.186169-23.876152l97.505924-32.812256 30.834212-97.529195a12.566396 12.566396 0 0 1 15.754537-8.191429zM649.544557 0.513593c2.676177 0.884302 4.770576 3.025243 5.608336 5.724692l18.523798 59.294772 60.970292 20.66474a8.796477 8.796477 0 0 1-0.325796 16.755194l-60.73758 18.058377-19.780438 59.550754a8.796477 8.796477 0 0 1-16.731924-0.162898l-18.523798-59.271501-59.062061-17.825665a8.796477 8.796477 0 0 1-0.395609-16.708653l59.574025-20.943993 19.780438-59.574025a8.796477 8.796477 0 0 1 11.100317-5.561794z" p-id="13565"></path></svg>

+ 1 - 0
src/assets/icons/customer-gva.svg

@@ -0,0 +1 @@
+<svg t="1702221805491" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="5932"><path d="M876.48 981.312H147.52a104.832 104.832 0 0 1-104.832-104.832V469.76a46.08 46.08 0 0 1 46.912-46.912 46.08 46.08 0 0 1 46.912 46.912v406.72c0 6.272 4.736 11.008 10.944 11.008h729.088a10.688 10.688 0 0 0 10.88-11.008V469.76a46.08 46.08 0 0 1 46.976-46.912 46.08 46.08 0 0 1 46.912 46.912v406.72c0 57.92-46.912 104.832-104.832 104.832z"  p-id="5933"></path><path d="M934.4 516.672a49.664 49.664 0 0 1-31.296-12.48L512 152.192l-391.104 352a46.784 46.784 0 0 1-65.728-3.2 48.64 48.64 0 0 1 3.2-67.2l422.4-378.624a47.616 47.616 0 0 1 62.528 0l422.4 378.624a46.784 46.784 0 0 1 3.136 65.664 46.08 46.08 0 0 1-34.432 17.28z" p-id="5934"></path><path d="M627.776 981.312H396.16a46.08 46.08 0 0 1-46.912-46.912V558.912A46.08 46.08 0 0 1 396.16 512h229.952a46.08 46.08 0 0 1 46.976 46.912V934.4a44.8 44.8 0 0 1-45.44 46.912z m-184.64-93.824h136.128v-281.6H443.136v281.6z" p-id="5935"></path></svg>

BIN
src/assets/kefu.png


BIN
src/assets/login_background.jpg


+ 33 - 0
src/assets/login_background.svg

@@ -0,0 +1,33 @@
+<svg xmlns="http://www.w3.org/2000/svg" version="1.1" baseProfile="full" width="100%" height="100%" viewBox="0 0 1400 800">
+
+  <rect x="1300" y="400" rx="40" ry="40" width="150" height="150" stroke="rgb(129, 201, 149)" fill="rgb(129, 201, 149)">
+    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 1450 550" to="360 1450 550" repeatCount="indefinite"/>
+  </rect>
+
+  <path d="M 100 350 A 150 150 0 1 1 400 350 Q400 370 380 370 L 250 370 L 120 370 Q100 370 100 350" fill="#a2b3ff">
+    <animateMotion path="M 800 -200 L 800 -300 L 800 -200" dur="20s" begin="0s" repeatCount="indefinite"/>
+    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 210 530 ; -30 210 530 ; 0 210 530" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
+  </path>
+
+  <circle cx="150" cy="150" r="180" stroke="#85FFBD" fill="#85FFBD">
+    <animateMotion path="M 0 0 L 40 20 Z" dur="5s" repeatCount="indefinite"/>
+  </circle>
+
+  <!-- 三角形 -->
+  <path d="M 165 580 L 270 580 Q275 578 270 570 L 223 483 Q220 480 217 483 L 165 570 Q160 578 165 580"  fill="#a2b3ff">
+    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="0 210 530" to="360 210 530" repeatCount="indefinite"/>
+  </path>
+
+<!--  <circle cx="1200" cy="600" r="30" stroke="rgb(241, 243, 244)" fill="rgb(241, 243, 244)">-->
+<!--    <animateMotion path="M 0 0 L -20 40 Z" dur="9s" repeatCount="indefinite"/>-->
+<!--  </circle>-->
+
+  <path d="M 100 350 A 40 40 0 1 1 180 350 L 180 430 A 40 40 0 1 1 100 430 Z" fill="#3054EB">
+    <animateMotion path="M 140 390 L 180 360 L 140 390" dur="20s" begin="0s" repeatCount="indefinite"/>
+    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="30s" type="rotate" values="0 140 390; -60 140 390; 0 140 390" keyTimes="0 ; 0.5 ; 1" repeatCount="indefinite"/>
+  </path>
+
+  <rect x="400" y="600" rx="40" ry="40" width="100" height="100" stroke="rgb(129, 201, 149)" fill="#3054EB">
+    <animateTransform attributeType="XML" attributeName="transform" begin="0s" dur="35s" type="rotate" from="-30 550 750" to="330 550 750" repeatCount="indefinite"/>
+  </rect>
+</svg>

Файлын зөрүү хэтэрхий том тул дарагдсан байна
+ 10 - 0
src/assets/login_left.svg


BIN
src/assets/login_right_banner.jpg


BIN
src/assets/logo.jpg


BIN
src/assets/logo.png


BIN
src/assets/logo_login.png


BIN
src/assets/nav_logo.png


BIN
src/assets/noBody.png


BIN
src/assets/notFound.png


BIN
src/assets/qm.png


BIN
src/assets/video.png


+ 67 - 0
src/components/arrayCtrl/arrayCtrl.vue

@@ -0,0 +1,67 @@
+<template>
+  <div class="flex gap-2">
+    <el-tag
+      v-for="tag in modelValue"
+      :key="tag"
+      :closable="editable"
+      :disable-transitions="false"
+      @close="handleClose(tag)"
+    >
+      {{ tag }}
+    </el-tag>
+    <template v-if="editable">
+      <el-input
+        v-if="inputVisible"
+        ref="InputRef"
+        v-model="inputValue"
+        class="w-20"
+        size="small"
+        @keyup.enter="handleInputConfirm"
+        @blur="handleInputConfirm"
+      />
+      <el-button v-else class="button-new-tag" size="small" @click="showInput">
+        + 新增
+      </el-button>
+    </template>
+  </div>
+</template>
+
+<script setup>
+  defineOptions({
+    name: 'ArrayCtrl'
+  })
+
+  import { nextTick, ref } from 'vue'
+
+  const inputValue = ref('')
+  const inputVisible = ref(false)
+  const InputRef = ref(null)
+
+  const modelValue = defineModel()
+
+  defineProps({
+    editable: {
+      type: Boolean,
+      default: () => false
+    }
+  })
+
+  const handleClose = (tag) => {
+    modelValue.value.splice(modelValue.value.indexOf(tag), 1)
+  }
+
+  const showInput = () => {
+    inputVisible.value = true
+    nextTick(() => {
+      InputRef.value?.input?.focus()
+    })
+  }
+
+  const handleInputConfirm = () => {
+    if (inputValue.value) {
+      modelValue.value.push(inputValue.value)
+    }
+    inputVisible.value = false
+    inputValue.value = ''
+  }
+</script>

+ 44 - 0
src/components/bottomInfo/bottomInfo.vue

@@ -0,0 +1,44 @@
+<!--
+此文件受版权保护,未经授权禁止修改!如果您尚未获得授权,请通过微信(shouzi_1994)联系我们以购买授权。在未授权状态下,只需保留此代码,不会影响任何正常使用。
+     未经授权的商用使用可能会被我们的资产搜索引擎爬取,并可能导致后续索赔。索赔金额将不低于高级授权费的十倍。请您遵守版权法律法规,尊重知识产权。
+ -->
+<template>
+  <div
+    class="flex flex-col md:flex-row gap-2 items-center text-sm text-slate-700 dark:text-slate-500 justify-center py-2"
+  >
+    <div class="text-center">
+      <span class="mr-1">Powered by</span>
+      <span>
+        <a
+          class="font-bold text-active"
+          href="https://github.com/flipped-aurora/gin-vue-admin"
+          >Gin-Vue-Admin</a
+        >
+      </span>
+    </div>
+    <slot />
+    <div class="text-center">
+      <span class="mr-1">Copyright</span>
+      <span>
+        <a
+          class="font-bold text-active"
+          href="https://github.com/flipped-aurora"
+          >flipped-aurora团队</a
+        >
+      </span>
+    </div>
+  </div>
+</template>
+
+<script setup>
+  defineOptions({
+    name: 'BottomInfo'
+  })
+
+  console.log(
+    `%c powered by %c flipped-aurorae %c`,
+    'background:#0081ff; padding: 1px; border-radius: 3px 0 0 3px; color: #fff',
+    'background:#354855; padding: 1px 5px; border-radius: 0 3px 3px 0; color: #fff; font-weight: bold;',
+    'background:transparent'
+  )
+</script>

+ 54 - 0
src/components/charts/index.vue

@@ -0,0 +1,54 @@
+<!--
+   本组件参考 arco-pro 的实现
+    https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/components/chart/index.vue
+    @auther: bypanghu<[email protected]>
+    @date: 2024/5/8
+!-->
+
+<template>
+  <VCharts
+    v-if="renderChart"
+    :option="options"
+    :autoresize="autoResize"
+    :style="{ width, height }"
+  />
+</template>
+
+<script setup>
+  import { ref, nextTick } from 'vue'
+  import VCharts from 'vue-echarts'
+  import { useWindowResize } from '@/hooks/use-windows-resize'
+
+  defineProps({
+    options: {
+      type: Object,
+      default() {
+        return {}
+      }
+    },
+    autoResize: {
+      type: Boolean,
+      default: true
+    },
+    width: {
+      type: String,
+      default: '100%'
+    },
+    height: {
+      type: String,
+      default: '100%'
+    }
+  })
+  const renderChart = ref(false)
+  nextTick(() => {
+    renderChart.value = true
+  })
+  useWindowResize(() => {
+    renderChart.value = false
+    nextTick(() => {
+      renderChart.value = true
+    })
+  })
+</script>
+
+<style scoped lang="less"></style>

+ 196 - 0
src/components/commandMenu/index.vue

@@ -0,0 +1,196 @@
+<template>
+  <el-dialog
+    v-model="dialogVisible"
+    width="30%"
+    class="overlay"
+    :show-close="false"
+  >
+    <template #header>
+      <input
+        v-model="searchInput"
+        class="quick-input"
+        placeholder="请输入你需要快捷到达的功能"
+      />
+    </template>
+
+    <div v-for="(option, index) in options" :key="index">
+      <div v-if="option.children.length" class="quick-title">
+        {{ option.label }}
+      </div>
+      <div
+        v-for="(item, key) in option.children"
+        :key="index + '-' + key"
+        class="quick-item"
+        @click="item.func"
+      >
+        {{ item.label }}
+      </div>
+    </div>
+
+    <template #footer>
+      <span class="dialog-footer">
+        <el-button @click="close">关闭</el-button>
+      </span>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+  import { reactive, ref, watch } from 'vue'
+  import { useRouter } from 'vue-router'
+  import { useRouterStore } from '@/pinia/modules/router'
+  import { useAppStore, useUserStore } from '@/pinia'
+  defineOptions({
+    name: 'CommandMenu'
+  })
+  const appStore = useAppStore()
+  const userStore = useUserStore()
+
+  const router = useRouter()
+  const route = useRouter()
+  const routerStore = useRouterStore()
+  const dialogVisible = ref(false)
+  const searchInput = ref('')
+  const options = reactive([])
+  const deepMenus = (menus) => {
+    const arr = []
+    menus?.forEach((menu) => {
+      if (!menu?.children) return
+      if (menu.children && menu.children.length > 0) {
+        arr.push(...deepMenus(menu.children))
+      } else {
+        if (
+          menu.meta.title &&
+          menu.meta.title.indexOf(searchInput.value) > -1
+        ) {
+          arr.push({
+            label: menu.meta.title,
+            func: () => changeRouter(menu)
+          })
+        }
+      }
+    })
+    return arr
+  }
+
+  const addQuickMenu = () => {
+    const option = {
+      label: '跳转',
+      children: []
+    }
+    const menus = deepMenus(routerStore.asyncRouters[0]?.children || [])
+    option.children.push(...menus)
+    options.push(option)
+  }
+
+  const addQuickOption = () => {
+    const option = {
+      label: '操作',
+      children: []
+    }
+    const quickArr = [
+      {
+        label: '亮色主题',
+        func: () => changeMode(false)
+      },
+      {
+        label: '暗色主题',
+        func: () => changeMode(true)
+      },
+      {
+        label: '退出登录',
+        func: () => userStore.LoginOut()
+      }
+    ]
+    option.children.push(
+      ...quickArr.filter((item) => item.label.indexOf(searchInput.value) > -1)
+    )
+    options.push(option)
+  }
+
+  addQuickMenu()
+  addQuickOption()
+
+  const open = () => {
+    dialogVisible.value = true
+  }
+
+  const changeRouter = (e) => {
+    const index = e.name
+    const query = {}
+    const params = {}
+    routerStore.routeMap[index]?.parameters &&
+      routerStore.routeMap[index]?.parameters.forEach((item) => {
+        if (item.type === 'query') {
+          query[item.key] = item.value
+        } else {
+          params[item.key] = item.value
+        }
+      })
+    if (index === route.name) return
+    if (e.name.indexOf('http://') > -1 || e.name.indexOf('https://') > -1) {
+      window.open(e.name)
+    } else {
+      router.push({ name: index, query, params })
+    }
+    dialogVisible.value = false
+  }
+
+  const changeMode = (darkMode) => {
+    appStore.toggleTheme(darkMode)
+  }
+
+  const close = () => {
+    dialogVisible.value = false
+  }
+
+  defineExpose({ open })
+
+  watch(searchInput, () => {
+    options.length = 0
+    addQuickMenu()
+    addQuickOption()
+  })
+</script>
+
+<style lang="scss">
+  .overlay {
+    border-radius: 4px;
+    .el-dialog__header {
+      padding: 0 !important;
+      margin-right: 0 !important;
+    }
+    .el-dialog__body {
+      padding: 12px !important;
+      height: 50vh;
+      overflow: auto !important;
+    }
+    .quick-title {
+      margin-top: 8px;
+      font-size: 12px;
+      font-weight: 600;
+      color: #666;
+    }
+    .quick-input {
+      @apply bg-gray-50 dark:bg-gray-800;
+      color: #666;
+      border-radius: 4px 4px 0 0;
+      border: none;
+      padding: 12px 16px;
+      box-sizing: border-box;
+      width: 100%;
+      font-size: 16px;
+      border-bottom: 1px solid #ddd;
+    }
+    .quick-item {
+      font-size: 14px;
+      padding: 8px;
+      margin: 4px 0;
+      &:hover {
+        @apply bg-gray-200 dark:bg-slate-500;
+        cursor: pointer;
+        border-radius: 4px;
+      }
+    }
+  }
+</style>

+ 90 - 0
src/components/customPic/index.vue

@@ -0,0 +1,90 @@
+<template>
+  <span class="headerAvatar">
+    <template v-if="picType === 'avatar'">
+      <el-avatar v-if="userStore.userInfo.headerImg" :size="30" :src="avatar" />
+      <el-avatar v-else :size="30" :src="noAvatar" />
+    </template>
+    <template v-if="picType === 'img'">
+      <img v-if="userStore.userInfo.headerImg" :src="avatar" class="avatar" />
+      <img v-else :src="noAvatar" class="avatar" />
+    </template>
+    <template v-if="picType === 'file'">
+      <el-image
+        :src="file"
+        class="file"
+        :preview-src-list="previewSrcList"
+        :preview-teleported="true"
+      />
+    </template>
+  </span>
+</template>
+
+<script setup>
+  import noAvatarPng from '@/assets/noBody.png'
+  import { useUserStore } from '@/pinia/modules/user'
+  import { computed, ref } from 'vue'
+
+  defineOptions({
+    name: 'CustomPic'
+  })
+
+  const props = defineProps({
+    picType: {
+      type: String,
+      required: false,
+      default: 'avatar'
+    },
+    picSrc: {
+      type: String,
+      required: false,
+      default: ''
+    },
+    preview: {
+      type: Boolean,
+      default: false
+    }
+  })
+
+  const path = ref(import.meta.env.VITE_BASE_API + '/')
+  const noAvatar = ref(noAvatarPng)
+
+  const userStore = useUserStore()
+
+  const avatar = computed(() => {
+    if (props.picSrc === '') {
+      if (
+        userStore.userInfo.headerImg !== '' &&
+        userStore.userInfo.headerImg.slice(0, 4) === 'http'
+      ) {
+        return userStore.userInfo.headerImg
+      }
+      return path.value + userStore.userInfo.headerImg
+    } else {
+      if (props.picSrc !== '' && props.picSrc.slice(0, 4) === 'http') {
+        return props.picSrc
+      }
+      return path.value + props.picSrc
+    }
+  })
+  const file = computed(() => {
+    if (props.picSrc && props.picSrc.slice(0, 4) !== 'http') {
+      return path.value + props.picSrc
+    }
+    return props.picSrc
+  })
+  const previewSrcList = computed(() => (props.preview ? [file.value] : []))
+</script>
+
+<style scoped>
+  .headerAvatar {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    margin-right: 8px;
+  }
+  .file {
+    width: 80px;
+    height: 80px;
+    position: relative;
+  }
+</style>

+ 75 - 0
src/components/exportExcel/exportExcel.vue

@@ -0,0 +1,75 @@
+<template>
+  <el-button type="primary" icon="download" @click="exportExcelFunc"
+    >导出</el-button
+  >
+</template>
+
+<script setup>
+
+import { exportExcel } from '@/api/exportTemplate'
+
+  const props = defineProps({
+    templateId: {
+      type: String,
+      required: true
+    },
+    condition: {
+      type: Object,
+      default: () => ({})
+    },
+    limit: {
+      type: Number,
+      default: 0
+    },
+    offset: {
+      type: Number,
+      default: 0
+    },
+    order: {
+      type: String,
+      default: ''
+    }
+  })
+
+  import { ElMessage } from 'element-plus'
+
+  const exportExcelFunc = async () => {
+    if (props.templateId === '') {
+      ElMessage.error('组件未设置模板ID')
+      return
+    }
+    let baseUrl = import.meta.env.VITE_BASE_API
+    if (baseUrl === "/"){
+      baseUrl = ""
+    }
+    const paramsCopy = JSON.parse(JSON.stringify(props.condition))
+    if (props.limit) {
+      paramsCopy.limit = props.limit
+    }
+    if (props.offset) {
+      paramsCopy.offset = props.offset
+    }
+    if (props.order) {
+      paramsCopy.order = props.order
+    }
+    const params = Object.entries(paramsCopy)
+      .map(
+        ([key, value]) =>
+          `${encodeURIComponent(key)}=${encodeURIComponent(value)}`
+      )
+      .join('&')
+
+    const res = await exportExcel({
+      templateID: props.templateId,
+      params
+    })
+
+    if(res.code === 0){
+      ElMessage.success('创建导出任务成功,开始下载')
+      const url = `${baseUrl}${res.data}`
+      window.open(url, '_blank')
+    }
+
+
+  }
+</script>

+ 40 - 0
src/components/exportExcel/exportTemplate.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-button type="primary" icon="download" @click="exportTemplateFunc"
+    >下载模板</el-button
+  >
+</template>
+
+<script setup>
+  import { ElMessage } from 'element-plus'
+  import {exportTemplate} from "@/api/exportTemplate";
+
+  const props = defineProps({
+    templateId: {
+      type: String,
+      required: true
+    }
+  })
+
+
+  const exportTemplateFunc = async () => {
+    if (props.templateId === '') {
+      ElMessage.error('组件未设置模板ID')
+      return
+    }
+    let baseUrl = import.meta.env.VITE_BASE_API
+    if (baseUrl === "/"){
+      baseUrl = ""
+    }
+
+    const res = await exportTemplate({
+      templateID: props.templateId
+    })
+
+    if(res.code === 0){
+      ElMessage.success('创建导出任务成功,开始下载')
+      const url = `${baseUrl}${res.data}`
+      window.open(url, '_blank')
+    }
+
+  }
+</script>

+ 45 - 0
src/components/exportExcel/importExcel.vue

@@ -0,0 +1,45 @@
+<template>
+  <el-upload
+    :action="url"
+    :show-file-list="false"
+    :on-success="handleSuccess"
+    :multiple="false"
+    :headers="{'x-token': token}"
+  >
+    <el-button type="primary" icon="upload" class="ml-3"> 导入 </el-button>
+  </el-upload>
+</template>
+
+<script setup>
+  import { ElMessage } from 'element-plus'
+  import { useUserStore } from "@/pinia";
+
+  let baseUrl = import.meta.env.VITE_BASE_API
+  if (baseUrl === "/"){
+    baseUrl = ""
+  }
+
+  const props = defineProps({
+    templateId: {
+      type: String,
+      required: true
+    }
+  })
+
+  const userStore = useUserStore()
+
+  const token = userStore.token
+
+  const emit = defineEmits(['on-success'])
+
+  const url = `${baseUrl}/sysExportTemplate/importExcel?templateID=${props.templateId}`
+
+  const handleSuccess = (res) => {
+    if (res.code === 0) {
+      ElMessage.success('导入成功')
+      emit('on-success')
+    } else {
+      ElMessage.error(res.msg)
+    }
+  }
+</script>

+ 31 - 0
src/components/office/docx.vue

@@ -0,0 +1,31 @@
+<template>
+  <vue-office-docx :src="docx" @rendered="rendered" />
+</template>
+<script>
+  export default {
+    name: 'Docx'
+  }
+</script>
+<script setup>
+  import { ref, watch } from 'vue'
+
+  // 引入VueOfficeDocx组件
+  import VueOfficeDocx from '@vue-office/docx'
+  // 引入相关样式
+  import '@vue-office/docx/lib/index.css'
+
+  const model = defineModel({
+    type: String
+  })
+
+  const docx = ref(null)
+  watch(
+    () => model,
+    (value) => {
+      docx.value = value
+    },
+    { immediate: true }
+  )
+  const rendered = () => {}
+</script>
+<style lang="scss" scoped></style>

+ 36 - 0
src/components/office/excel.vue

@@ -0,0 +1,36 @@
+<template>
+  <VueOfficeExcel
+    :src="excel"
+    @rendered="renderedHandler"
+    @error="errorHandler"
+    style="height: 100vh; width: 100vh"
+  />
+</template>
+<script>
+  export default {
+    name: 'Excel'
+  }
+</script>
+<script setup>
+  //引入VueOfficeExcel组件
+  import VueOfficeExcel from '@vue-office/excel'
+  //引入相关样式
+  import '@vue-office/excel/lib/index.css'
+  import { ref, watch } from 'vue'
+
+  const props = defineProps({
+    modelValue: {
+      type: String,
+      default: () => ''
+    }
+  })
+  const excel = ref('')
+  watch(
+    () => props.modelValue,
+    (val) => (excel.value = val),
+    { immediate: true }
+  )
+  const renderedHandler = () => {}
+  const errorHandler = () => {}
+</script>
+<style></style>

+ 49 - 0
src/components/office/index.vue

@@ -0,0 +1,49 @@
+<template>
+  <div class="border border-solid border-gray-100 h-full w-full">
+    <el-row>
+      <div v-if="ext === 'docx'">
+        <Docx v-model="fullFileURL" />
+      </div>
+      <div v-else-if="ext === 'pdf'">
+        <Pdf v-model="fullFileURL" />
+      </div>
+      <div v-else-if="ext === 'xlsx'">
+        <Excel v-model="fullFileURL" />
+      </div>
+      <div v-else-if="ext === 'image'">
+        <el-image :src="fullFileURL" lazy />
+      </div>
+    </el-row>
+  </div>
+</template>
+<script>
+  export default {
+    name: 'Office'
+  }
+</script>
+<script setup>
+  import { ref, watch, computed } from 'vue'
+  import Docx from '@/components/office/docx.vue'
+  import Pdf from '@/components/office/pdf.vue'
+  import Excel from '@/components/office/excel.vue'
+
+  const path = ref(import.meta.env.VITE_BASE_API)
+
+  const model = defineModel({ type: String })
+
+  const fileUrl = ref('')
+  const ext = ref('')
+  watch(
+    () => model,
+    (val) => {
+      fileUrl.value = val
+      const fileExt = val.split('.')[1] || ''
+      const image = ['png', 'jpg', 'jpeg', 'gif']
+      ext.value = image.includes(fileExt?.toLowerCase()) ? 'image' : fileExt
+    },
+    { immediate: true }
+  )
+  const fullFileURL = computed(() => {
+    return path.value + '/' + fileUrl.value
+  })
+</script>

+ 39 - 0
src/components/office/pdf.vue

@@ -0,0 +1,39 @@
+<template>
+  <vue-office-pdf
+    :src="pdf"
+    @rendered="renderedHandler"
+    @error="errorHandler"
+  />
+</template>
+<script>
+  export default {
+    name: 'Pdf'
+  }
+</script>
+<script setup>
+  import { ref, watch } from 'vue'
+
+  //引入VueOfficeDocx组件
+  import VueOfficePdf from '@vue-office/pdf'
+  //引入相关样式
+  import '@vue-office/docx/lib/index.css'
+  console.log('pdf===>')
+  const props = defineProps({
+    modelValue: {
+      type: String,
+      default: () => ''
+    }
+  })
+  const pdf = ref(null)
+  watch(
+    () => props.modelValue,
+    (val) => (pdf.value = val),
+    { immediate: true }
+  )
+  const renderedHandler = () => {
+    console.log('pdf 加载成功')
+  }
+  const errorHandler = () => {
+    console.log('pdf 错误')
+  }
+</script>

+ 86 - 0
src/components/richtext/rich-edit.vue

@@ -0,0 +1,86 @@
+<template>
+  <div class="border border-solid border-gray-100 h-full">
+    <Toolbar
+      :editor="editorRef"
+      :default-config="toolbarConfig"
+      mode="default"
+    />
+    <Editor
+      v-model="valueHtml"
+      class="overflow-y-hidden mt-0.5"
+      style="height: 18rem"
+      :default-config="editorConfig"
+      mode="default"
+      @onCreated="handleCreated"
+      @onChange="change"
+    />
+  </div>
+</template>
+
+<script setup>
+  import '@wangeditor/editor/dist/css/style.css' // 引入 css
+
+  const basePath = import.meta.env.VITE_BASE_API
+
+  import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
+  import { Editor, Toolbar } from '@wangeditor/editor-for-vue'
+
+  import { ElMessage } from 'element-plus'
+  import { getUrl } from '@/utils/image'
+
+  const emits = defineEmits(['change', 'update:modelValue'])
+
+  const change = (editor) => {
+    emits('change', editor)
+    emits('update:modelValue', valueHtml.value)
+  }
+
+  const props = defineProps({
+    modelValue: {
+      type: String,
+      default: ''
+    }
+  })
+
+  const editorRef = shallowRef()
+  const valueHtml = ref('')
+
+  const toolbarConfig = {}
+  const editorConfig = {
+    placeholder: '请输入内容...',
+    MENU_CONF: {}
+  }
+  editorConfig.MENU_CONF['uploadImage'] = {
+    fieldName: 'file',
+    server: basePath + '/fileUploadAndDownload/upload?noSave=1',
+    customInsert(res, insertFn) {
+      if (res.code === 0) {
+        const urlPath = getUrl(res.data.file.url)
+        insertFn(urlPath, res.data.file.name)
+        return
+      }
+      ElMessage.error(res.msg)
+    }
+  }
+
+  // 组件销毁时,也及时销毁编辑器
+  onBeforeUnmount(() => {
+    const editor = editorRef.value
+    if (editor == null) return
+    editor.destroy()
+  })
+
+  const handleCreated = (editor) => {
+    editorRef.value = editor
+    valueHtml.value = props.modelValue
+  }
+
+  watch(
+    () => props.modelValue,
+    () => {
+      valueHtml.value = props.modelValue
+    }
+  )
+</script>
+
+<style scoped lang="scss"></style>

+ 58 - 0
src/components/richtext/rich-view.vue

@@ -0,0 +1,58 @@
+<template>
+  <div class="border border-solid border-gray-100 h-full">
+    <Editor
+      v-model="valueHtml"
+      class="overflow-y-hidden mt-0.5"
+      :default-config="editorConfig"
+      mode="default"
+      @onCreated="handleCreated"
+      @onChange="change"
+    />
+  </div>
+</template>
+<script setup>
+  import '@wangeditor/editor/dist/css/style.css' // 引入 css
+
+  import { onBeforeUnmount, ref, shallowRef, watch } from 'vue'
+  import { Editor } from '@wangeditor/editor-for-vue'
+
+  const emits = defineEmits(['change', 'update:modelValue'])
+  const editorConfig = ref({
+    readOnly: true
+  })
+  const change = (editor) => {
+    emits('change', editor)
+    emits('update:modelValue', valueHtml.value)
+  }
+
+  const props = defineProps({
+    modelValue: {
+      type: String,
+      default: ''
+    }
+  })
+
+  const editorRef = shallowRef()
+  const valueHtml = ref('')
+
+  // 组件销毁时,也及时销毁编辑器
+  onBeforeUnmount(() => {
+    const editor = editorRef.value
+    if (editor == null) return
+    editor.destroy()
+  })
+
+  const handleCreated = (editor) => {
+    editorRef.value = editor
+    valueHtml.value = props.modelValue
+  }
+
+  watch(
+    () => props.modelValue,
+    () => {
+      valueHtml.value = props.modelValue
+    }
+  )
+</script>
+
+<style scoped lang="scss"></style>

+ 87 - 0
src/components/selectFile/selectFile.vue

@@ -0,0 +1,87 @@
+<template>
+  <div>
+    <el-upload
+      v-model:file-list="fileList"
+      multiple
+      :action="`${getBaseUrl()}/fileUploadAndDownload/upload?noSave=1`"
+      :on-error="uploadError"
+      :on-success="uploadSuccess"
+      :on-remove="uploadRemove"
+      :show-file-list="true"
+      :limit="limit"
+      :accept="accept"
+      class="upload-btn"
+      :headers="{'x-token': token}"
+    >
+      <el-button type="primary"> 上传文件 </el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue'
+  import { ElMessage } from 'element-plus'
+  import { getBaseUrl } from '@/utils/format'
+  import { useUserStore } from "@/pinia";
+
+  defineOptions({
+    name: 'UploadCommon'
+  })
+
+  defineProps({
+    limit: {
+      type: Number,
+      default: 3
+    },
+    accept: {
+      type: String,
+      default: ''
+    }
+  })
+
+  const userStore = useUserStore()
+
+  const token = userStore.token
+
+  const fullscreenLoading = ref(false)
+
+  const model = defineModel({ type: Array })
+
+  const fileList = ref(model.value)
+
+  const emits = defineEmits(['on-success', 'on-error'])
+
+  const uploadSuccess = (res) => {
+    const { data, code } = res
+    if (code !== 0) {
+      ElMessage({
+        type: 'error',
+        message: '上传失败' + res.msg
+      })
+      fileList.value.pop()
+      return
+    }
+    model.value.push({
+      name: data.file.name,
+      url: data.file.url
+    })
+    emits('on-success', res)
+  }
+
+  const uploadRemove = (file) => {
+    const index = model.value.indexOf(file)
+    if (index > -1) {
+      model.value.splice(index, 1)
+      fileList.value = model.value
+    }
+  }
+
+  const uploadError = (err) => {
+    ElMessage({
+      type: 'error',
+      message: '上传失败'
+    })
+    fullscreenLoading.value = false
+    emits('on-error', err)
+  }
+</script>

+ 86 - 0
src/components/selectImage/selectComponent.vue

@@ -0,0 +1,86 @@
+<template>
+  <div
+      class="w-40 h-40 relative rounded border border-dashed border-gray-300 cursor-pointer group"
+      :class="rounded ? 'rounded-full' : ''"
+  >
+    <div class="w-full h-full overflow-hidden" :class="rounded ? 'rounded-full' : ''">
+      <el-icon
+          v-if="isVideoExt(model || '')"
+          :size="32"
+          class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]"
+      >
+        <VideoPlay />
+      </el-icon>
+      <video
+          v-if="isVideoExt(model || '')"
+          class="w-full h-full object-cover"
+          muted
+          preload="metadata"
+      >
+        <source :src="getUrl(model) + '#t=1'" />
+      </video>
+
+      <el-image
+          v-if="model && !isVideoExt(model)"
+          class="w-full h-full"
+          :src="imgUrl"
+          :preview-src-list="srcList"
+          fit="cover"
+      />
+      <div
+          v-else
+          class="text-gray-600 group-hover:bg-gray-200 group-hover:opacity-60 w-full h-full flex justify-center items-center"
+          @click="chooseItem"
+      >
+        <el-icon>
+          <plus />
+        </el-icon>
+        上传
+      </div>
+    </div>
+    <!-- 删除按钮在外层容器中 -->
+    <div
+        v-if="model"
+        class="right-0 top-0 hidden text-gray-400 group-hover:flex justify-center items-center absolute z-10"
+        @click="deleteItem"
+    >
+      <el-icon :size="24">
+        <CircleCloseFilled />
+      </el-icon>
+    </div>
+  </div>
+</template>
+<script setup>
+  import { getUrl, isVideoExt } from '@/utils/image'
+  import { CircleCloseFilled, Plus } from '@element-plus/icons-vue'
+  import { computed } from 'vue'
+
+  const props = defineProps({
+    model: {
+      default: '',
+      type: String
+    },
+    rounded: {
+      default: false,
+      type: Boolean
+    }
+  })
+
+  const emits = defineEmits(['chooseItem', 'deleteItem'])
+
+  const chooseItem = () => {
+    emits('chooseItem')
+  }
+
+  const deleteItem = () => {
+    emits('deleteItem')
+  }
+
+  const imgUrl = computed(() => {
+    return getUrl(props.model)
+  })
+
+  const srcList = computed(() => {
+    return imgUrl.value ? [imgUrl.value] : []
+  })
+</script>

+ 453 - 0
src/components/selectImage/selectImage.vue

@@ -0,0 +1,453 @@
+<template>
+  <div>
+    <selectComponent :rounded="rounded" v-if="!props.multiple" :model="model" @chooseItem="openChooseImg" @deleteItem="openChooseImg" />
+    <div v-else class="w-full gap-4 flex flex-wrap">
+      <selectComponent :rounded="rounded" v-for="(item, index) in model" :key="index" :model="item" @chooseItem="openChooseImg"
+                       @deleteItem="deleteImg(index)"
+      />
+      <selectComponent :rounded="rounded" v-if="model?.length < props.maxUpdateCount || props.maxUpdateCount === 0"
+                       @chooseItem="openChooseImg" @deleteItem="openChooseImg"
+      />
+    </div>
+
+    <el-drawer v-model="drawer" title="媒体库 | 点击“文件名”可以编辑,选择的类别即是上传的类别" :size="880">
+      <div class="flex">
+        <div class="w-64" style="border-right: solid 1px var(--el-border-color);">
+          <el-scrollbar style="height: calc(100vh - 110px)">
+            <el-tree
+                :data="categories"
+                node-key="id"
+                :props="defaultProps"
+                @node-click="handleNodeClick"
+                default-expand-all
+            >
+              <template #default="{ node, data }">
+                <div class="w-36" :class="search.classId === data.ID ? 'text-blue-500 font-bold' : ''">{{ data.name }}
+                </div>
+                <el-dropdown>
+                  <el-icon class="ml-3 text-right" v-if="data.ID > 0"><MoreFilled /></el-icon>
+                  <el-icon class="ml-3 text-right mt-1" v-else><Plus /></el-icon>
+                  <template #dropdown>
+                    <el-dropdown-menu>
+                      <el-dropdown-item @click="addCategoryFun(data)">添加分类</el-dropdown-item>
+                      <el-dropdown-item @click="editCategory(data)" v-if="data.ID > 0">编辑分类</el-dropdown-item>
+                      <el-dropdown-item @click="deleteCategoryFun(data.ID)" v-if="data.ID > 0">删除分类</el-dropdown-item>
+                    </el-dropdown-menu>
+                  </template>
+                </el-dropdown>
+              </template>
+            </el-tree>
+          </el-scrollbar>
+        </div>
+        <div class="ml-4 w-[605px]">
+          <div class="gva-btn-list gap-2">
+            <el-input v-model.trim="search.keyword" class="w-96" placeholder="请输入文件名或备注" clearable />
+            <el-button type="primary" icon="search" @click="onSubmit"></el-button>
+          </div>
+          <div class="gva-btn-list gap-2">
+            <el-button @click="useSelectedImages" type="danger" :disabled="selectedImages.length === 0" :icon="ArrowLeftBold">选定</el-button>
+            <upload-common :image-common="imageCommon" :classId="search.classId" @on-success="onSuccess" />
+            <cropper-image :classId="search.classId" @on-success="onSuccess" />
+            <QRCodeUpload :classId="search.classId" @on-success="onSuccess" />
+            <upload-image :image-url="imageUrl" :file-size="2048" :max-w-h="1080" :classId="search.classId" @on-success="onSuccess" />
+          </div>
+          <div class="flex flex-wrap gap-4">
+            <div v-for="(item,key) in picList" :key="key" class="w-40">
+              <div class="w-40 h-40 border rounded overflow-hidden border-dashed border-gray-300 cursor-pointer relative group">
+                <el-image :key="key" :src="getUrl(item.url)" fit="cover" class="w-full h-full relative" @click="toggleImageSelection(item)" :class="{ selected: isSelected(item) }">
+                  <template #error>
+                    <el-icon v-if="isVideoExt(item.url || '')" :size="32" class="absolute top-[calc(50%-16px)] left-[calc(50%-16px)]">
+                      <VideoPlay />
+                    </el-icon>
+                    <video v-if="isVideoExt(item.url || '')"
+                           class="w-full h-full object-cover"
+                           muted
+                           preload="metadata"
+                           @click="toggleImageSelection(item)"
+                           :class="{ selected: isSelected(item) }"
+                    >
+                      <source :src="getUrl(item.url) + '#t=1'">
+                      您的浏览器不支持视频播放
+                    </video>
+                    <div v-else class="w-full h-full object-cover flex items-center justify-center">
+                      <el-icon :size="32">
+                        <icon-picture />
+                      </el-icon>
+                    </div>
+                  </template>
+                </el-image>
+                <div class="absolute -right-1 top-1 w-8 h-8 group-hover:inline-block hidden" @click="deleteCheck(item)">
+                  <el-icon :size="18">
+                    <CloseBold />
+                  </el-icon>
+                </div>
+              </div>
+              <div class="overflow-hidden text-nowrap overflow-ellipsis text-center w-full cursor-pointer" @click="editFileNameFunc(item)">
+                {{ item.name }}
+              </div>
+            </div>
+          </div>
+          <el-pagination
+              :current-page="page"
+              :page-size="pageSize"
+              :total="total"
+              class="justify-center"
+              layout="total, prev, pager, next, jumper"
+              @current-change="handleCurrentChange"
+              @size-change="handleSizeChange"
+          />
+        </div>
+      </div>
+    </el-drawer>
+
+
+    <!-- 添加分类弹窗 -->
+    <el-dialog v-model="categoryDialogVisible" @close="closeAddCategoryDialog" width="520"
+               :title="(categoryFormData.ID === 0 ? '添加' : '编辑') + '分类'"
+               draggable
+    >
+      <el-form ref="categoryForm" :rules="rules" :model="categoryFormData" label-width="80px">
+        <el-form-item label="上级分类">
+          <el-tree-select
+              v-model="categoryFormData.pid"
+              :data="categories"
+              check-strictly
+              :props="defaultProps"
+              :render-after-expand="false"
+              style="width: 240px"
+          />
+        </el-form-item>
+        <el-form-item label="分类名称" prop="name">
+          <el-input v-model.trim="categoryFormData.name" placeholder="分类名称"></el-input>
+        </el-form-item>
+      </el-form>
+      <template #footer>
+        <el-button @click="closeAddCategoryDialog">取消</el-button>
+        <el-button type="primary" @click="confirmAddCategory">确定</el-button>
+      </template>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+import { getUrl, isVideoExt } from '@/utils/image'
+import { ref } from 'vue'
+import { getFileList, editFileName, deleteFile } from '@/api/fileUploadAndDownload'
+import UploadImage from '@/components/upload/image.vue'
+import UploadCommon from '@/components/upload/common.vue'
+import WarningBar from '@/components/warningBar/warningBar.vue'
+import { ElMessage, ElMessageBox } from 'element-plus'
+import {
+  ArrowLeftBold,
+  CloseBold,
+  MoreFilled,
+  Picture as IconPicture,
+  Plus,
+  VideoPlay
+} from '@element-plus/icons-vue'
+import selectComponent from '@/components/selectImage/selectComponent.vue'
+import { addCategory, deleteCategory, getCategoryList } from '@/api/attachmentCategory'
+import CropperImage from "@/components/upload/cropper.vue";
+import QRCodeUpload from "@/components/upload/QR-code.vue";
+
+const imageUrl = ref('')
+const imageCommon = ref('')
+
+const search = ref({
+  keyword: null,
+  classId: 0
+})
+const page = ref(1)
+const total = ref(0)
+const pageSize = ref(20)
+
+const model = defineModel({ type: [String, Array] })
+
+const props = defineProps({
+  multiple: {
+    type: Boolean,
+    default: false
+  },
+  fileType: {
+    type: String,
+    default: ''
+  },
+  maxUpdateCount: {
+    type: Number,
+    default: 0
+  },
+  rounded: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const deleteImg = (index) => {
+  model.value.splice(index, 1)
+}
+
+const handleSizeChange = (val) => {
+  pageSize.value = val
+  getImageList()
+}
+
+const handleCurrentChange = (val) => {
+  page.value = val
+  getImageList()
+}
+
+const onSubmit = () => {
+  search.value.classId = 0
+  page.value = 1
+  getImageList()
+}
+
+const editFileNameFunc = async(row) => {
+  ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    inputPattern: /\S/,
+    inputErrorMessage: '不能为空',
+    inputValue: row.name
+  }).then(async({ value }) => {
+    row.name = value
+    const res = await editFileName(row)
+    if (res.code === 0) {
+      ElMessage({
+        type: 'success',
+        message: '编辑成功!'
+      })
+      await getImageList()
+    }
+  }).catch(() => {
+    ElMessage({
+      type: 'info',
+      message: '取消修改'
+    })
+  })
+}
+
+const drawer = ref(false)
+const picList = ref([])
+
+const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp', 'svg']
+const videoTypeList = ['mp4', 'avi', 'rmvb', 'rm', 'asf', 'divx', 'mpg', 'mpeg', 'mpe', 'wmv', 'mkv', 'vob']
+
+const listObj = {
+  image: imageTypeList,
+  video: videoTypeList
+}
+
+const chooseImg = (url) => {
+  if (props.fileType) {
+    const typeSuccess = listObj[props.fileType].some(item => {
+      if (url?.toLowerCase().includes(item)) {
+        return true
+      }
+    })
+    if (!typeSuccess) {
+      ElMessage({
+        type: 'error',
+        message: '当前类型不支持使用'
+      })
+      return
+    }
+  }
+  //if (props.multiple) {
+  //  model.value.push(url)
+  //} else {
+  model.value = url
+  //}
+  drawer.value = false
+}
+
+const openChooseImg = async() => {
+  if (model.value && !props.multiple) {
+    model.value = ''
+    return
+  }
+  await getImageList()
+  await fetchCategories()
+  drawer.value = true
+}
+
+const getImageList = async() => {
+  const res = await getFileList({ page: page.value, pageSize: pageSize.value, ...search.value })
+  if (res.code === 0) {
+    picList.value = res.data.list
+    total.value = res.data.total
+    page.value = res.data.page
+    pageSize.value = res.data.pageSize
+  }
+}
+
+const deleteCheck = (item) => {
+  ElMessageBox.confirm('是否删除该文件', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(async() => {
+    const res = await deleteFile(item)
+    if (res.code === 0) {
+      ElMessage({
+        type: 'success',
+        message: '删除成功!'
+      })
+      await getImageList()
+    }
+  }).catch(() => {
+    ElMessage({
+      type: 'info',
+      message: '已取消删除'
+    })
+  })
+}
+
+const defaultProps = {
+  children: 'children',
+  label: 'name',
+  value: 'ID'
+}
+
+const categories = ref([])
+const fetchCategories = async() => {
+  const res = await getCategoryList()
+  let data = {
+    name: '全部分类',
+    ID: 0,
+    pid: 0,
+    children:[]
+  }
+  if (res.code === 0) {
+    categories.value = res.data || []
+    categories.value.unshift(data)
+  }
+}
+
+const handleNodeClick = (node) => {
+  search.value.keyword = null
+  search.value.classId = node.ID
+  page.value = 1
+  getImageList()
+}
+
+const onSuccess = () => {
+  search.value.keyword = null
+  page.value = 1
+  getImageList()
+}
+
+const categoryDialogVisible = ref(false)
+const categoryFormData = ref({
+  ID: 0,
+  pid: 0,
+  name: ''
+})
+
+const categoryForm = ref(null)
+const rules = ref({
+  name: [
+    { required: true, message: '请输入分类名称', trigger: 'blur' },
+    { max: 20, message: '最多20位字符', trigger: 'blur' }
+  ]
+})
+
+const addCategoryFun = (category) => {
+  categoryDialogVisible.value = true
+  categoryFormData.value.ID = 0
+  categoryFormData.value.pid = category.ID
+}
+
+const editCategory = (category) => {
+  categoryFormData.value = {
+    ID: category.ID,
+    pid: category.pid,
+    name: category.name
+  }
+  categoryDialogVisible.value = true
+}
+
+const deleteCategoryFun = async(id) => {
+  const res = await deleteCategory({ id: id })
+  if (res.code === 0) {
+    ElMessage.success({ type: 'success', message: '删除成功' })
+    await fetchCategories()
+  }
+}
+
+const confirmAddCategory = async() => {
+  categoryForm.value.validate(async valid => {
+    if (valid) {
+      const res = await addCategory(categoryFormData.value)
+      if (res.code === 0) {
+        ElMessage({ type: 'success', message: '操作成功' })
+        await fetchCategories()
+        closeAddCategoryDialog()
+      }
+    }
+  })
+}
+
+const closeAddCategoryDialog = () => {
+  categoryDialogVisible.value = false
+  categoryFormData.value = {
+    ID: 0,
+    pid: 0,
+    name: ''
+  }
+}
+
+const selectedImages = ref([])
+
+const toggleImageSelection = (item) => {
+  if (props.multiple === false) {
+    chooseImg(item.url)
+    return
+  }
+  const index = selectedImages.value.findIndex(img => img.ID === item.ID)
+  if (index > -1) {
+    selectedImages.value.splice(index, 1)
+  } else {
+    selectedImages.value.push(item)
+  }
+}
+
+const isSelected = (item) => {
+  return selectedImages.value.some(img => img.ID === item.ID)
+}
+
+const useSelectedImages = () => {
+  selectedImages.value.forEach((item) => {
+    model.value.push(item.url)
+  })
+  drawer.value = false
+  selectedImages.value = []
+}
+
+</script>
+<style scoped>
+.selected {
+  border: 3px solid #409eff;
+}
+
+.selected:before {
+  content: "";
+  position: absolute;
+  left: 0;
+  top: 0;
+  border: 10px solid #409eff;
+}
+
+.selected:after {
+  content: "";
+  width: 9px;
+  height: 14px;
+  position: absolute;
+  left: 6px;
+  top: 0;
+  border: 3px solid #fff;
+  border-top-color: transparent;
+  border-left-color: transparent;
+  transform: rotate(45deg);
+}
+</style>

+ 32 - 0
src/components/svgIcon/svgIcon.vue

@@ -0,0 +1,32 @@
+<template>
+  <svg :class="svgClass" v-bind="$attrs" :color="color">
+    <use :xlink:href="'#' + name" rel="external nofollow" />
+  </svg>
+</template>
+<script setup>
+  import { computed } from 'vue'
+  const props = defineProps({
+    name: {
+      type: String,
+      required: true
+    },
+    color: {
+      type: String,
+      default: 'currentColor'
+    }
+  })
+
+  const svgClass = computed(() => {
+    if (props.name) {
+      return `svg-icon ${props.name}`
+    }
+    return 'svg-icon'
+  })
+</script>
+<style scoped>
+  .svg-icon {
+    @apply w-4 h-4;
+    fill: currentColor;
+    vertical-align: middle;
+  }
+</style>

+ 65 - 0
src/components/upload/QR-code.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+    <el-button type="primary" icon="iphone" @click="createQrCode"> 扫码上传</el-button>
+  </div>
+
+  <el-dialog v-model="dialogVisible" title="扫码上传" width="320px" :show-close="false" append-to-body :close-on-click-modal="false"
+             draggable
+  >
+    <div class="m-2">
+      <vue-qr :logoSrc="logoSrc"
+              :size="291"
+              :margin="0"
+              :autoColor="true"
+              :dotScale="1"
+              :text="codeUrl"
+              colorDark="green"
+              colorLight="white"
+              ref="qrcode"
+      />
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="onFinished">完成上传</el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import logoSrc from '@/assets/logo.png'
+import vueQr from 'vue-qr/src/packages/vue-qr.vue'
+import { ref } from 'vue'
+import { useUserStore } from '@/pinia/modules/user'
+
+defineOptions({
+  name: 'QRCodeUpload'
+})
+
+const emit = defineEmits(['on-success'])
+
+const props = defineProps({
+  classId: {
+    type: Number,
+    default: 0
+  }
+})
+
+const dialogVisible = ref(false)
+const userStore = useUserStore()
+const codeUrl = ref('')
+
+const createQrCode = () => {
+  const local = window.location
+  codeUrl.value = local.protocol + '//' + local.host + '/#/scanUpload?id=' + props.classId + '&token=' + userStore.token + '&t=' + Date.now()
+  dialogVisible.value = true
+  console.log(codeUrl.value)
+}
+
+const onFinished = () => {
+  dialogVisible.value = false
+  codeUrl.value = ''
+  emit('on-success', '')
+}
+</script>

+ 90 - 0
src/components/upload/common.vue

@@ -0,0 +1,90 @@
+<template>
+  <div>
+    <el-upload
+      :action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
+      :before-upload="checkFile"
+      :on-error="uploadError"
+      :on-success="uploadSuccess"
+      :show-file-list="false"
+      :data="{'classId': props.classId}"
+      :headers="{'x-token': token}"
+      multiple
+      class="upload-btn"
+    >
+      <el-button type="primary" :icon="Upload">普通上传</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+  import { ref } from 'vue'
+  import { ElMessage } from 'element-plus'
+  import { isVideoMime, isImageMime } from '@/utils/image'
+  import { getBaseUrl } from '@/utils/format'
+  import { Upload } from "@element-plus/icons-vue";
+  import { useUserStore } from "@/pinia";
+
+  defineOptions({
+    name: 'UploadCommon'
+  })
+
+  const userStore = useUserStore()
+
+  const token = userStore.token
+
+  const props = defineProps({
+    classId: {
+      type: Number,
+      default: 0
+    }
+  })
+
+  const emit = defineEmits(['on-success'])
+
+  const fullscreenLoading = ref(false)
+
+  const checkFile = (file) => {
+    fullscreenLoading.value = true
+    const isLt500K = file.size / 1024 / 1024 < 0.5 // 500K, @todo 应支持在项目中设置
+    const isLt5M = file.size / 1024 / 1024 < 5 // 5MB, @todo 应支持项目中设置
+    const isVideo = isVideoMime(file.type)
+    const isImage = isImageMime(file.type)
+    let pass = true
+    if (!isVideo && !isImage) {
+      ElMessage.error(
+        '上传图片只能是 jpg,png,svg,webp 格式, 上传视频只能是 mp4,webm 格式!'
+      )
+      fullscreenLoading.value = false
+      pass = false
+    }
+    if (!isLt5M && isVideo) {
+      ElMessage.error('上传视频大小不能超过 5MB')
+      fullscreenLoading.value = false
+      pass = false
+    }
+    if (!isLt500K && isImage) {
+      ElMessage.error('未压缩的上传图片大小不能超过 500KB,请使用压缩上传')
+      fullscreenLoading.value = false
+      pass = false
+    }
+
+    console.log('upload file check result: ', pass)
+
+    return pass
+  }
+
+  const uploadSuccess = (res) => {
+    const { data } = res
+    if (data.file) {
+      emit('on-success', data.file.url)
+    }
+  }
+
+  const uploadError = () => {
+    ElMessage({
+      type: 'error',
+      message: '上传失败'
+    })
+    fullscreenLoading.value = false
+  }
+</script>

+ 237 - 0
src/components/upload/cropper.vue

@@ -0,0 +1,237 @@
+<template>
+  <el-upload
+      ref="uploadRef"
+      :action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
+      accept="image/*"
+      :show-file-list="false"
+      :auto-upload="false"
+      :data="{'classId': props.classId}"
+      :on-success="handleImageSuccess"
+      :on-change="handleFileChange"
+      :headers="{'x-token': token}"
+  >
+    <el-button type="primary" icon="crop"> 裁剪上传</el-button>
+  </el-upload>
+
+  <el-dialog v-model="dialogVisible" title="图片裁剪" width="1200px" append-to-body @close="dialogVisible = false" :close-on-click-modal="false" draggable>
+    <div class="flex gap-[30px] h-[600px]">
+      <!-- 左侧编辑区 -->
+      <div class="flex flex-col flex-1">
+        <div class="flex-1 bg-[#f8f8f8] rounded-lg overflow-hidden">
+          <VueCropper
+              ref="cropperRef"
+              :img="imgSrc"
+              outputType="jpeg"
+              :autoCrop="true"
+              :autoCropWidth="cropWidth"
+              :autoCropHeight="cropHeight"
+              :fixedBox="false"
+              :fixed="fixedRatio"
+              :fixedNumber="fixedNumber"
+              :centerBox="true"
+              :canMoveBox="true"
+              :full="false"
+              :maxImgSize="1200"
+              :original="true"
+              @realTime="handleRealTime"
+          ></VueCropper>
+        </div>
+
+        <!-- 工具栏 -->
+        <div class="mt-[20px] flex items-center p-[10px] bg-white rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
+          <el-button-group>
+            <el-tooltip content="向左旋转">
+              <el-button @click="rotate(-90)" :icon="RefreshLeft" />
+            </el-tooltip>
+            <el-tooltip content="向右旋转">
+              <el-button @click="rotate(90)" :icon="RefreshRight" />
+            </el-tooltip>
+            <el-button :icon="Plus" @click="changeScale(1)"></el-button>
+            <el-button :icon="Minus" @click="changeScale(-1)"></el-button>
+          </el-button-group>
+
+
+          <el-select v-model="currentRatio" placeholder="选择比例" class="w-32 ml-4" @change="onCurrentRatio">
+            <el-option v-for="(item, index) in ratioOptions" :key="index" :label="item.label" :value="index" />
+          </el-select>
+        </div>
+      </div>
+
+      <!-- 右侧预览区 -->
+      <div class="w-[340px]">
+        <div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]">
+          <div class="mb-[15px] text-gray-600">裁剪预览</div>
+          <div class="bg-white p-5 rounded-lg shadow-[0_2px_12px_rgba(0,0,0,0.1)]"
+               :style="{'width': previews.w + 'px', 'height': previews.h + 'px'}"
+          >
+            <div class="w-full h-full relative overflow-hidden">
+              <img :src="previews.url" :style="previews.img" alt="" class="max-w-none absolute transition-all duration-300 ease-in-out image-render-pixelated origin-[0_0]" />
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+    <template #footer>
+      <div class="dialog-footer">
+        <el-button @click="dialogVisible = false">取 消</el-button>
+        <el-button type="primary" @click="handleUpload" :loading="uploading"> {{ uploading ? '上传中...' : '上 传' }}
+        </el-button>
+      </div>
+    </template>
+  </el-dialog>
+</template>
+
+<script setup>
+import { ref, getCurrentInstance } from 'vue'
+import { ElMessage } from 'element-plus'
+import { RefreshLeft, RefreshRight, Plus, Minus } from '@element-plus/icons-vue'
+import 'vue-cropper/dist/index.css'
+import { VueCropper } from 'vue-cropper'
+import { getBaseUrl } from '@/utils/format'
+import { useUserStore } from "@/pinia";
+
+defineOptions({
+  name: 'CropperImage'
+})
+
+const emit = defineEmits(['on-success'])
+
+const props = defineProps({
+  classId: {
+    type: Number,
+    default: 0
+  }
+})
+
+const uploadRef = ref(null)
+// 响应式数据
+const dialogVisible = ref(false)
+const imgSrc = ref('')
+const cropperRef = ref(null)
+const { proxy } = getCurrentInstance()
+const previews = ref({})
+const uploading = ref(false)
+
+// 缩放控制
+const changeScale = (value) => {
+  proxy.$refs.cropperRef.changeScale(value)
+}
+
+// 比例预设
+const ratioOptions = ref([
+  { label: '1:1', value: [1, 1] },
+  { label: '16:9', value: [16, 9] },
+  { label: '9:16', value: [9, 16] },
+  { label: '4:3', value: [4, 3] },
+  { label: '自由比例', value: [] }
+])
+
+const fixedNumber = ref([1, 1])
+const cropWidth = ref(300)
+const cropHeight = ref(300)
+
+const fixedRatio = ref(false)
+const currentRatio = ref(4)
+const onCurrentRatio = () => {
+  fixedNumber.value = ratioOptions.value[currentRatio.value].value
+  switch (currentRatio.value) {
+    case 0:
+      cropWidth.value = 300
+      cropHeight.value = 300
+      fixedRatio.value = true
+      break
+    case 1:
+      cropWidth.value = 300
+      cropHeight.value = 300 * 9 / 16
+      fixedRatio.value = true
+      break
+    case 2:
+      cropWidth.value = 300 * 9 / 16
+      cropHeight.value = 300
+      fixedRatio.value = true
+      break
+    case 3:
+      cropWidth.value = 300
+      cropHeight.value = 300 * 3 / 4
+      fixedRatio.value = true
+      break
+    default:
+      cropWidth.value = 300
+      cropHeight.value = 300
+      fixedRatio.value = false
+  }
+}
+
+// 文件处理
+const handleFileChange = (file) => {
+  const isImage = file.raw.type.includes('image')
+  if (!isImage) {
+    ElMessage.error('请选择图片文件')
+    return
+  }
+
+  if (file.raw.size / 1024 / 1024 > 8) {
+    ElMessage.error('文件大小不能超过8MB!')
+    return false
+  }
+
+  const reader = new FileReader()
+  reader.onload = (e) => {
+    imgSrc.value = e.target.result
+    dialogVisible.value = true
+  }
+  reader.readAsDataURL(file.raw)
+}
+
+// 旋转控制
+const rotate = (degree) => {
+  if (degree === -90) {
+    proxy.$refs.cropperRef.rotateLeft()
+  } else {
+    proxy.$refs.cropperRef.rotateRight()
+  }
+}
+
+// 实时预览
+const handleRealTime = (data) => {
+  previews.value = data
+  //console.log(data)
+}
+
+// 上传处理
+const handleUpload = () => {
+  uploading.value = true
+  proxy.$refs.cropperRef.getCropBlob((blob) => {
+    try {
+      const file = new File([blob], `${Date.now()}.jpg`, { type: 'image/jpeg' })
+      uploadRef.value.clearFiles()
+      uploadRef.value.handleStart(file)
+      uploadRef.value.submit()
+
+    } catch (error) {
+      uploading.value = false
+      ElMessage.error('上传失败: ' + error.message)
+    }
+  })
+}
+
+const handleImageSuccess = (res) => {
+  const { data } = res
+  if (data) {
+    setTimeout(() => {
+      uploading.value = false
+      dialogVisible.value = false
+      previews.value = {}
+      ElMessage.success('上传成功')
+      emit('on-success', data.url)
+    }, 1000)
+  }
+}
+
+</script>
+
+<style scoped>
+:deep(.vue-cropper) {
+  background: transparent;
+}
+</style>

+ 102 - 0
src/components/upload/image.vue

@@ -0,0 +1,102 @@
+<template>
+  <div>
+    <el-upload
+      :action="`${getBaseUrl()}/fileUploadAndDownload/upload`"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      :before-upload="beforeImageUpload"
+      :multiple="false"
+      :data="{'classId': props.classId}"
+      :headers="{'x-token': token}"
+    >
+      <el-button type="primary" :icon="Upload">压缩上传</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+  import ImageCompress from '@/utils/image'
+  import { ElMessage } from 'element-plus'
+  import { getBaseUrl } from '@/utils/format'
+  import { Upload } from "@element-plus/icons-vue";
+  import { useUserStore } from "@/pinia";
+
+  defineOptions({
+    name: 'UploadImage'
+  })
+
+  const emit = defineEmits(['on-success'])
+  const props = defineProps({
+    imageUrl: {
+      type: String,
+      default: ''
+    },
+    fileSize: {
+      type: Number,
+      default: 2048 // 2M 超出后执行压缩
+    },
+    maxWH: {
+      type: Number,
+      default: 1920 // 图片长宽上限
+    },
+    classId: {
+      type: Number,
+      default: 0
+    }
+  })
+
+  const userStore = useUserStore()
+
+  const token = userStore.token
+
+  const beforeImageUpload = (file) => {
+    const isJPG = file.type?.toLowerCase() === 'image/jpeg'
+    const isPng = file.type?.toLowerCase() === 'image/png'
+    if (!isJPG && !isPng) {
+      ElMessage.error('上传头像图片只能是 jpg或png 格式!')
+      return false
+    }
+
+    const isRightSize = file.size / 1024 < props.fileSize
+    if (!isRightSize) {
+      // 压缩
+      const compress = new ImageCompress(file, props.fileSize, props.maxWH)
+      return compress.compress()
+    }
+    return isRightSize
+  }
+
+  const handleImageSuccess = (res) => {
+    const { data } = res
+    if (data.file) {
+      emit('on-success', data.file.url)
+    }
+  }
+</script>
+
+<style lang="scss" scoped>
+  .image-uploader {
+    border: 1px dashed #d9d9d9;
+    width: 180px;
+    border-radius: 6px;
+    cursor: pointer;
+    position: relative;
+    overflow: hidden;
+  }
+  .image-uploader {
+    border-color: #409eff;
+  }
+  .image-uploader-icon {
+    font-size: 28px;
+    color: #8c939d;
+    width: 178px;
+    height: 178px;
+    line-height: 178px;
+    text-align: center;
+  }
+  .image {
+    width: 178px;
+    height: 178px;
+    display: block;
+  }
+</style>

+ 33 - 0
src/components/warningBar/warningBar.vue

@@ -0,0 +1,33 @@
+<template>
+  <div
+    class="px-1.5 py-2 flex items-center rounded-sm mt-2 bg-amber-50 gap-2 mb-3 text-amber-500 dark:bg-amber-700 dark:text-gray-200"
+    :class="href && 'cursor-pointer'"
+    @click="open"
+  >
+    <el-icon class="text-xl">
+      <warning-filled />
+    </el-icon>
+    <span>
+      {{ title }}
+    </span>
+  </div>
+</template>
+<script setup>
+  import { WarningFilled } from '@element-plus/icons-vue'
+  const prop = defineProps({
+    title: {
+      type: String,
+      default: ''
+    },
+    href: {
+      type: String,
+      default: ''
+    }
+  })
+
+  const open = () => {
+    if (prop.href) {
+      window.open(prop.href)
+    }
+  }
+</script>

+ 53 - 0
src/core/config.js

@@ -0,0 +1,53 @@
+/**
+ * 网站配置文件
+ */
+const greenText = (text) => `\x1b[32m${text}\x1b[0m`
+
+const config = {
+  appName: 'Gin-Vue-Admin',
+  appLogo: 'logo.png',
+  showViteLogo: true,
+  logs: []
+}
+
+export const viteLogo = (env) => {
+  if (config.showViteLogo) {
+    console.log(
+      greenText(
+        `> 欢迎使用Gin-Vue-Admin,开源地址:https://github.com/flipped-aurora/gin-vue-admin`
+      )
+    )
+    console.log(greenText(`> 当前版本:v2.8.0`))
+    console.log(greenText(`> 加群方式:微信:shouzi_1994 QQ群:470239250`))
+    console.log(
+      greenText(`> 项目地址:https://github.com/flipped-aurora/gin-vue-admin`)
+    )
+    console.log(greenText(`> 插件市场:https://plugin.gin-vue-admin.com`))
+    console.log(
+      greenText(`> GVA讨论社区:https://support.qq.com/products/371961`)
+    )
+    console.log(
+      greenText(
+        `> 默认自动化文档地址:http://127.0.0.1:${env.VITE_SERVER_PORT}/swagger/index.html`
+      )
+    )
+    console.log(
+      greenText(`> 默认前端文件运行地址:http://127.0.0.1:${env.VITE_CLI_PORT}`)
+    )
+    console.log(
+      greenText(
+        `--------------------------------------版权声明--------------------------------------`
+      )
+    )
+    console.log(greenText(`** 版权所有方:flipped-aurora开源团队 **`))
+    console.log(greenText(`** 版权持有公司:北京翻转极光科技有限责任公司 **`))
+    console.log(
+      greenText(
+        `** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html **`
+      )
+    )
+    console.log('\n')
+  }
+}
+
+export default config

+ 27 - 0
src/core/gin-vue-admin.js

@@ -0,0 +1,27 @@
+/*
+ * gin-vue-admin web框架组
+ *
+ * */
+// 加载网站配置文件夹
+import { register } from './global'
+
+export default {
+  install: (app) => {
+    register(app)
+    console.log(`
+       欢迎使用 Gin-Vue-Admin
+       当前版本:v2.8.0
+       加群方式:微信:shouzi_1994 QQ群:622360840
+       项目地址:https://github.com/flipped-aurora/gin-vue-admin
+       插件市场:https://plugin.gin-vue-admin.com
+       GVA讨论社区:https://support.qq.com/products/371961
+       默认自动化文档地址:http://127.0.0.1:${import.meta.env.VITE_SERVER_PORT}/swagger/index.html
+       默认前端文件运行地址:http://127.0.0.1:${import.meta.env.VITE_CLI_PORT}
+       如果项目让您获得了收益,希望您能请团队喝杯可乐:https://www.gin-vue-admin.com/coffee/index.html
+       --------------------------------------版权声明--------------------------------------
+       ** 版权所有方:flipped-aurora开源团队 **
+       ** 版权持有公司:北京翻转极光科技有限责任公司 **
+       ** 剔除授权标识需购买商用授权:https://gin-vue-admin.com/empower/index.html **
+    `)
+  }
+}

+ 59 - 0
src/core/global.js

@@ -0,0 +1,59 @@
+import config from './config'
+import { h } from 'vue'
+
+// 统一导入el-icon图标
+import * as ElIconModules from '@element-plus/icons-vue'
+import svgIcon from '@/components/svgIcon/svgIcon.vue'
+// 导入转换图标名称的函数
+
+const createIconComponent = (name) => ({
+  name: 'SvgIcon',
+  render() {
+    return h(svgIcon, {
+      name: name
+    })
+  }
+})
+
+const registerIcons = async (app) => {
+  const iconModules = import.meta.glob('@/assets/icons/**/*.svg') // 系统目录 svg 图标
+  const pluginIconModules = import.meta.glob(
+    '@/plugin/**/assets/icons/**/*.svg'
+  ) // 插件目录 svg 图标
+  const mergedIconModules = Object.assign({}, iconModules, pluginIconModules) // 合并所有 svg 图标
+  for (const path in mergedIconModules) {
+    let pluginName = ''
+    if (path.startsWith('/src/plugin/')) {
+      pluginName = `${path.split('/')[3]}-`
+    }
+    const iconName = path
+      .split('/')
+      .pop()
+      .replace(/\.svg$/, '')
+    // 如果iconName带空格则不加入到图标库中并且提示名称不合法
+    if (iconName.indexOf(' ') !== -1) {
+      console.error(`icon ${iconName}.svg includes whitespace in ${path}`)
+      continue
+    }
+    const key = `${pluginName}${iconName}`
+    // 开发模式下列出所有 svg 图标,方便开发者直接查找复制使用
+    import.meta.env.MODE == 'development' &&
+      console.log(`svg-icon-component: <${key} />`)
+    const iconComponent = createIconComponent(key)
+    config.logs.push({
+      key: key,
+      label: key
+    })
+    app.component(key, iconComponent)
+  }
+}
+
+export const register = (app) => {
+  // 统一注册el-icon图标
+  for (const iconName in ElIconModules) {
+    app.component(iconName, ElIconModules[iconName])
+  }
+  app.component('SvgIcon', svgIcon)
+  registerIcons(app)
+  app.config.globalProperties.$GIN_VUE_ADMIN = config
+}

+ 40 - 0
src/directive/auth.js

@@ -0,0 +1,40 @@
+// 权限按钮展示指令
+import { useUserStore } from '@/pinia/modules/user'
+export default {
+  install: (app) => {
+    const userStore = useUserStore()
+    app.directive('auth', {
+      // 当被绑定的元素插入到 DOM 中时……
+      mounted: function (el, binding) {
+        const userInfo = userStore.userInfo
+        let type = ''
+        switch (Object.prototype.toString.call(binding.value)) {
+          case '[object Array]':
+            type = 'Array'
+            break
+          case '[object String]':
+            type = 'String'
+            break
+          case '[object Number]':
+            type = 'Number'
+            break
+          default:
+            type = ''
+            break
+        }
+        if (type === '') {
+          el.parentNode.removeChild(el)
+          return
+        }
+        const waitUse = binding.value.toString().split(',')
+        let flag = waitUse.some((item) => Number(item) === userInfo.authorityId)
+        if (binding.modifiers.not) {
+          flag = !flag
+        }
+        if (!flag) {
+          el.parentNode.removeChild(el)
+        }
+      }
+    })
+  }
+}

+ 18 - 0
src/hooks/charts.js

@@ -0,0 +1,18 @@
+// 本组件参考 arco-pro 的实现
+// https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/hooks/chart-option.ts
+
+import { computed } from 'vue'
+import { useAppStore } from '@/pinia'
+
+export default function useChartOption(sourceOption) {
+  const appStore = useAppStore()
+  const isDark = computed(() => {
+    return appStore.isDark
+  })
+  const chartOption = computed(() => {
+    return sourceOption(isDark.value)
+  })
+  return {
+    chartOption
+  }
+}

+ 35 - 0
src/hooks/responsive.js

@@ -0,0 +1,35 @@
+// 本组件参考 arco-pro 的实现
+// https://github.com/arco-design/arco-design-pro-vue/blob/main/arco-design-pro-vite/src/hooks/responsive.ts
+
+import { onMounted, onBeforeMount, onBeforeUnmount } from 'vue'
+import { useDebounceFn } from '@vueuse/core'
+import { useAppStore } from '@/pinia'
+import { addEventListen, removeEventListen } from '@/utils/event'
+
+const WIDTH = 992
+
+function queryDevice() {
+  const rect = document.body.getBoundingClientRect()
+  return rect.width - 1 < WIDTH
+}
+
+export default function useResponsive(immediate) {
+  const appStore = useAppStore()
+  function resizeHandler() {
+    if (!document.hidden) {
+      const isMobile = queryDevice()
+      appStore.toggleDevice(isMobile ? 'mobile' : 'desktop')
+      // appStore.toggleDevice(isMobile);
+    }
+  }
+  const debounceFn = useDebounceFn(resizeHandler, 100)
+  onMounted(() => {
+    if (immediate) debounceFn()
+  })
+  onBeforeMount(() => {
+    addEventListen(window, 'resize', debounceFn)
+  })
+  onBeforeUnmount(() => {
+    removeEventListen(window, 'resize', debounceFn)
+  })
+}

+ 23 - 0
src/hooks/use-windows-resize.js

@@ -0,0 +1,23 @@
+// 监听 window 的 resize 事件,返回当前窗口的宽高
+import { shallowRef } from 'vue'
+import { tryOnMounted, useEventListener } from '@vueuse/core'
+
+const width = shallowRef(0)
+const height = shallowRef(0)
+
+export const useWindowResize = (cb) => {
+  const onResize = () => {
+    width.value = window.innerWidth
+    height.value = window.innerHeight
+    if (cb && typeof cb === 'function') {
+      cb(width.value, height.value)
+    }
+  }
+
+  tryOnMounted(onResize)
+  useEventListener('resize', onResize, { passive: true })
+  return {
+    width,
+    height
+  }
+}

+ 21 - 0
src/main.js

@@ -0,0 +1,21 @@
+import './style/element_visiable.scss'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+import { createApp } from 'vue'
+import ElementPlus from 'element-plus'
+
+import 'element-plus/dist/index.css'
+// 引入gin-vue-admin前端初始化相关内容
+import './core/gin-vue-admin'
+// 引入封装的router
+import router from '@/router/index'
+import '@/permission'
+import run from '@/core/gin-vue-admin.js'
+import auth from '@/directive/auth'
+import { store } from '@/pinia'
+import App from './App.vue'
+
+const app = createApp(App)
+app.config.productionTip = false
+
+app.use(run).use(ElementPlus).use(store).use(auth).use(router).mount('#app')
+export default app

+ 70 - 0
src/pathInfo.json

@@ -0,0 +1,70 @@
+{
+  "/src/view/about/index.vue": "About",
+  "/src/view/dashboard/components/banner.vue": "Banner",
+  "/src/view/dashboard/components/card.vue": "Card",
+  "/src/view/dashboard/components/charts-content-numbers.vue": "ChartsContentNumbers",
+  "/src/view/dashboard/components/charts-people-numbers.vue": "ChartsPeopleNumbers",
+  "/src/view/dashboard/components/charts.vue": "Charts",
+  "/src/view/dashboard/components/notice.vue": "Notice",
+  "/src/view/dashboard/components/pluginTable.vue": "PluginTable",
+  "/src/view/dashboard/components/quickLinks.vue": "QuickLinks",
+  "/src/view/dashboard/components/table.vue": "Table",
+  "/src/view/dashboard/components/wiki.vue": "Wiki",
+  "/src/view/dashboard/index.vue": "Dashboard",
+  "/src/view/error/index.vue": "Error",
+  "/src/view/error/reload.vue": "Reload",
+  "/src/view/example/breakpoint/breakpoint.vue": "BreakPoint",
+  "/src/view/example/customer/customer.vue": "Customer",
+  "/src/view/example/index.vue": "Example",
+  "/src/view/example/upload/scanUpload.vue": "scanUpload",
+  "/src/view/example/upload/upload.vue": "Upload",
+  "/src/view/init/index.vue": "Init",
+  "/src/view/layout/aside/asideComponent/asyncSubmenu.vue": "AsyncSubmenu",
+  "/src/view/layout/aside/asideComponent/index.vue": "AsideComponent",
+  "/src/view/layout/aside/asideComponent/menuItem.vue": "MenuItem",
+  "/src/view/layout/aside/combinationMode.vue": "GvaAside",
+  "/src/view/layout/aside/headMode.vue": "GvaAside",
+  "/src/view/layout/aside/index.vue": "Index",
+  "/src/view/layout/aside/normalMode.vue": "GvaAside",
+  "/src/view/layout/header/index.vue": "Index",
+  "/src/view/layout/header/tools.vue": "Tools",
+  "/src/view/layout/iframe.vue": "GvaLayoutIframe",
+  "/src/view/layout/index.vue": "GvaLayout",
+  "/src/view/layout/screenfull/index.vue": "Screenfull",
+  "/src/view/layout/search/search.vue": "BtnBox",
+  "/src/view/layout/setting/index.vue": "GvaSetting",
+  "/src/view/layout/setting/title.vue": "layoutSettingTitle",
+  "/src/view/layout/tabs/index.vue": "HistoryComponent",
+  "/src/view/login/index.vue": "Login",
+  "/src/view/person/person.vue": "Person",
+  "/src/view/routerHolder.vue": "RouterHolder",
+  "/src/view/superAdmin/api/api.vue": "Api",
+  "/src/view/superAdmin/authority/authority.vue": "Authority",
+  "/src/view/superAdmin/authority/components/apis.vue": "Apis",
+  "/src/view/superAdmin/authority/components/datas.vue": "Datas",
+  "/src/view/superAdmin/authority/components/menus.vue": "Menus",
+  "/src/view/superAdmin/dictionary/sysDictionary.vue": "SysDictionary",
+  "/src/view/superAdmin/dictionary/sysDictionaryDetail.vue": "SysDictionaryDetail",
+  "/src/view/superAdmin/index.vue": "SuperAdmin",
+  "/src/view/superAdmin/menu/components/components-cascader.vue": "ComponentsCascader",
+  "/src/view/superAdmin/menu/icon.vue": "Icon",
+  "/src/view/superAdmin/menu/menu.vue": "Menus",
+  "/src/view/superAdmin/operation/sysOperationRecord.vue": "SysOperationRecord",
+  "/src/view/superAdmin/params/sysParams.vue": "SysParams",
+  "/src/view/superAdmin/user/user.vue": "User",
+  "/src/view/system/state.vue": "State",
+  "/src/view/systemTools/autoCode/component/fieldDialog.vue": "FieldDialog",
+  "/src/view/systemTools/autoCode/component/previewCodeDialog.vue": "PreviewCodeDialog",
+  "/src/view/systemTools/autoCode/index.vue": "AutoCode",
+  "/src/view/systemTools/autoCodeAdmin/index.vue": "AutoCodeAdmin",
+  "/src/view/systemTools/autoPkg/autoPkg.vue": "AutoPkg",
+  "/src/view/systemTools/exportTemplate/exportTemplate.vue": "ExportTemplate",
+  "/src/view/systemTools/formCreate/index.vue": "FormGenerator",
+  "/src/view/systemTools/index.vue": "System",
+  "/src/view/systemTools/installPlugin/index.vue": "Index",
+  "/src/view/systemTools/pubPlug/pubPlug.vue": "PubPlug",
+  "/src/view/systemTools/system/system.vue": "Config",
+  "/src/plugin/announcement/form/info.vue": "InfoForm",
+  "/src/plugin/announcement/view/info.vue": "Info",
+  "/src/plugin/email/view/index.vue": "Email"
+}

+ 147 - 0
src/permission.js

@@ -0,0 +1,147 @@
+import { useUserStore } from '@/pinia/modules/user'
+import { useRouterStore } from '@/pinia/modules/router'
+import getPageTitle from '@/utils/page'
+import router from '@/router'
+import Nprogress from 'nprogress'
+import 'nprogress/nprogress.css'
+
+// 配置 NProgress
+Nprogress.configure({
+  showSpinner: false,
+  ease: 'ease',
+  speed: 500
+})
+
+// 白名单路由
+const WHITE_LIST = ['Login', 'Init']
+
+// 处理路由加载
+const setupRouter = async (userStore) => {
+  try {
+    const routerStore = useRouterStore()
+    await Promise.all([routerStore.SetAsyncRouter(), userStore.GetUserInfo()])
+
+    routerStore.asyncRouters.forEach((route) => router.addRoute(route))
+    return true
+  } catch (error) {
+    console.error('Setup router failed:', error)
+    return false
+  }
+}
+
+// 移除加载动画
+const removeLoading = () => {
+  const element = document.getElementById('gva-loading-box')
+  element?.remove()
+}
+
+// 处理组件缓存
+const handleKeepAlive = async (to) => {
+  if (!to.matched.some((item) => item.meta.keepAlive)) return
+
+  if (to.matched?.length > 2) {
+    for (let i = 1; i < to.matched.length; i++) {
+      const element = to.matched[i - 1]
+
+      if (element.name === 'layout') {
+        to.matched.splice(i, 1)
+        await handleKeepAlive(to)
+        continue
+      }
+
+      if (typeof element.components.default === 'function') {
+        await element.components.default()
+        await handleKeepAlive(to)
+      }
+    }
+  }
+}
+
+// 处理路由重定向
+const handleRedirect = (to, userStore) => {
+  if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
+    return { ...to, replace: true }
+  }
+  return { path: '/layout/404' }
+}
+
+// 路由守卫
+router.beforeEach(async (to, from) => {
+  const userStore = useUserStore()
+  const routerStore = useRouterStore()
+  const token = userStore.token
+
+  Nprogress.start()
+
+  // 处理元数据和缓存
+  to.meta.matched = [...to.matched]
+  await handleKeepAlive(to)
+
+  // 设置页面标题
+  document.title = getPageTitle(to.meta.title, to)
+
+  if (to.meta.client) {
+    return true
+  }
+
+  // 白名单路由处理
+  if (WHITE_LIST.includes(to.name)) {
+    if (token) {
+      if(!routerStore.asyncRouterFlag){
+        await setupRouter(userStore)
+      }
+      if(userStore.userInfo.authority.defaultRouter){
+        return { name: userStore.userInfo.authority.defaultRouter }
+      }
+    }
+    return  true
+  }
+
+  // 需要登录的路由处理
+  if (token) {
+    // 处理需要跳转到首页的情况
+    if (sessionStorage.getItem('needToHome') === 'true') {
+      sessionStorage.removeItem('needToHome')
+      return { path: '/' }
+    }
+
+    // 处理异步路由
+    if (!routerStore.asyncRouterFlag && !WHITE_LIST.includes(from.name)) {
+      const setupSuccess = await setupRouter(userStore)
+
+      if (setupSuccess && userStore.token) {
+        return handleRedirect(to, userStore)
+      }
+
+      return {
+        name: 'Login',
+        query: { redirect: to.href }
+      }
+    }
+
+    return to.matched.length ? true : { path: '/layout/404' }
+  }
+
+  // 未登录跳转登录页
+  return {
+    name: 'Login',
+    query: {
+      redirect: document.location.hash
+    }
+  }
+})
+
+// 路由加载完成
+router.afterEach(() => {
+  document.querySelector('.main-cont.main-right')?.scrollTo(0, 0)
+  Nprogress.done()
+})
+
+// 路由错误处理
+router.onError((error) => {
+  console.error('Router error:', error)
+  Nprogress.remove()
+})
+
+// 移除初始加载动画
+removeLoading()

+ 8 - 0
src/pinia/index.js

@@ -0,0 +1,8 @@
+import { createPinia } from 'pinia'
+import { useAppStore } from '@/pinia/modules/app'
+import { useUserStore } from '@/pinia/modules/user'
+import { useDictionaryStore } from '@/pinia/modules/dictionary'
+
+const store = createPinia()
+
+export { store, useAppStore, useUserStore, useDictionaryStore }

+ 133 - 0
src/pinia/modules/app.js

@@ -0,0 +1,133 @@
+import { defineStore } from 'pinia'
+import { ref, watchEffect, reactive } from 'vue'
+import { setBodyPrimaryColor } from '@/utils/format'
+import { useDark, usePreferredDark } from '@vueuse/core'
+
+export const useAppStore = defineStore('app', () => {
+  const device = ref('')
+  const drawerSize = ref('')
+  const operateMinWith = ref('240')
+  const config = reactive({
+    weakness: false,
+    grey: false,
+    primaryColor: '#3b82f6',
+    showTabs: true,
+    darkMode: 'auto',
+    layout_side_width: 256,
+    layout_side_collapsed_width: 80,
+    layout_side_item_height: 48,
+    show_watermark: true,
+    side_mode: 'normal',
+    // 页面过渡动画配置
+    transition_type: 'slide'
+  })
+
+  const isDark = useDark({
+    selector: 'html',
+    attribute: 'class',
+    valueDark: 'dark',
+    valueLight: 'light'
+  })
+
+  const preferredDark = usePreferredDark()
+
+  const toggleTheme = (darkMode) => {
+    isDark.value = darkMode
+  }
+
+  const toggleWeakness = (e) => {
+    config.weakness = e
+  }
+
+  const toggleGrey = (e) => {
+    config.grey = e
+  }
+
+  const togglePrimaryColor = (e) => {
+    config.primaryColor = e
+  }
+
+  const toggleTabs = (e) => {
+    config.showTabs = e
+  }
+
+  const toggleDevice = (e) => {
+    if (e === 'mobile') {
+      drawerSize.value = '100%'
+      operateMinWith.value = '80'
+    } else {
+      drawerSize.value = '800'
+      operateMinWith.value = '240'
+    }
+    device.value = e
+  }
+
+  const toggleDarkMode = (e) => {
+    config.darkMode = e
+  }
+
+  // 监听系统主题变化
+  watchEffect(() => {
+    if (config.darkMode === 'auto') {
+      isDark.value = preferredDark.value
+      return
+    }
+    isDark.value = config.darkMode === 'dark'
+  })
+
+  const toggleConfigSideWidth = (e) => {
+    config.layout_side_width = e
+  }
+
+  const toggleConfigSideCollapsedWidth = (e) => {
+    config.layout_side_collapsed_width = e
+  }
+
+  const toggleConfigSideItemHeight = (e) => {
+    config.layout_side_item_height = e
+  }
+
+  const toggleConfigWatermark = (e) => {
+    config.show_watermark = e
+  }
+
+  const toggleSideMode = (e) => {
+    config.side_mode = e
+  }
+
+  const toggleTransition = (e) => {
+    config.transition_type = e
+  }
+
+  // 监听色弱模式和灰色模式
+  watchEffect(() => {
+    document.documentElement.classList.toggle('html-weakenss', config.weakness)
+    document.documentElement.classList.toggle('html-grey', config.grey)
+  })
+
+  // 监听主题色
+  watchEffect(() => {
+    setBodyPrimaryColor(config.primaryColor, isDark.value ? 'dark' : 'light')
+  })
+
+  return {
+    isDark,
+    device,
+    drawerSize,
+    operateMinWith,
+    config,
+    toggleTheme,
+    toggleDevice,
+    toggleWeakness,
+    toggleGrey,
+    togglePrimaryColor,
+    toggleTabs,
+    toggleDarkMode,
+    toggleConfigSideWidth,
+    toggleConfigSideCollapsedWidth,
+    toggleConfigSideItemHeight,
+    toggleConfigWatermark,
+    toggleSideMode,
+    toggleTransition
+  }
+})

+ 41 - 0
src/pinia/modules/dictionary.js

@@ -0,0 +1,41 @@
+import { findSysDictionary } from '@/api/sysDictionary'
+
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useDictionaryStore = defineStore('dictionary', () => {
+  const dictionaryMap = ref({})
+
+  const setDictionaryMap = (dictionaryRes) => {
+    dictionaryMap.value = { ...dictionaryMap.value, ...dictionaryRes }
+  }
+
+  const getDictionary = async (type) => {
+    if (dictionaryMap.value[type] && dictionaryMap.value[type].length) {
+      return dictionaryMap.value[type]
+    } else {
+      const res = await findSysDictionary({ type })
+      if (res.code === 0) {
+        const dictionaryRes = {}
+        const dict = []
+        res.data.resysDictionary.sysDictionaryDetails &&
+          res.data.resysDictionary.sysDictionaryDetails.forEach((item) => {
+            dict.push({
+              label: item.label,
+              value: item.value,
+              extend: item.extend
+            })
+          })
+        dictionaryRes[res.data.resysDictionary.type] = dict
+        setDictionaryMap(dictionaryRes)
+        return dictionaryMap.value[type]
+      }
+    }
+  }
+
+  return {
+    dictionaryMap,
+    setDictionaryMap,
+    getDictionary
+  }
+})

+ 31 - 0
src/pinia/modules/params.js

@@ -0,0 +1,31 @@
+import { getSysParam } from '@/api/sysParams'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+export const useParamsStore = defineStore('params', () => {
+    const paramsMap = ref({})
+
+    const setParamsMap = (paramsRes) => {
+        paramsMap.value = { ...paramsMap.value, ...paramsRes }
+    }
+
+    const getParams = async(key) => {
+        if (paramsMap.value[key] && paramsMap.value[key].length) {
+            return paramsMap.value[key]
+        } else {
+            const res = await getSysParam({ key })
+            if (res.code === 0) {
+                const paramsRes = {}
+                paramsRes[key] = res.data.value
+                setParamsMap(paramsRes)
+                return paramsMap.value[key]
+            }
+        }
+    }
+
+    return {
+        paramsMap,
+        setParamsMap,
+        getParams
+    }
+})

+ 144 - 0
src/pinia/modules/router.js

@@ -0,0 +1,144 @@
+import { asyncRouterHandle } from '@/utils/asyncRouter'
+import { emitter } from '@/utils/bus.js'
+import { asyncMenu } from '@/api/menu'
+import { defineStore } from 'pinia'
+import { ref, watchEffect } from 'vue'
+import pathInfo from '@/pathInfo.json'
+
+const notLayoutRouterArr = []
+const keepAliveRoutersArr = []
+const nameMap = {}
+
+const formatRouter = (routes, routeMap, parent) => {
+  routes &&
+    routes.forEach((item) => {
+      item.parent = parent
+      item.meta.btns = item.btns
+      item.meta.hidden = item.hidden
+      if (item.meta.defaultMenu === true) {
+        if (!parent) {
+          item = { ...item, path: `/${item.path}` }
+          notLayoutRouterArr.push(item)
+        }
+      }
+      routeMap[item.name] = item
+      if (item.children && item.children.length > 0) {
+        formatRouter(item.children, routeMap, item)
+      }
+    })
+}
+
+const KeepAliveFilter = (routes) => {
+  routes &&
+    routes.forEach((item) => {
+      // 子菜单中有 keep-alive 的,父菜单也必须 keep-alive,否则无效。这里将子菜单中有 keep-alive 的父菜单也加入。
+      if (
+        (item.children && item.children.some((ch) => ch.meta.keepAlive)) ||
+        item.meta.keepAlive
+      ) {
+        const path = item.meta.path
+        keepAliveRoutersArr.push(pathInfo[path])
+        nameMap[item.name] = pathInfo[path]
+      }
+      if (item.children && item.children.length > 0) {
+        KeepAliveFilter(item.children)
+      }
+    })
+}
+
+export const useRouterStore = defineStore('router', () => {
+  const keepAliveRouters = ref([])
+  const asyncRouterFlag = ref(0)
+  const setKeepAliveRouters = (history) => {
+    const keepArrTemp = []
+    history.forEach((item) => {
+      if (nameMap[item.name]) {
+        keepArrTemp.push(nameMap[item.name])
+      }
+    })
+    keepAliveRouters.value = Array.from(new Set(keepArrTemp))
+  }
+  emitter.on('setKeepAlive', setKeepAliveRouters)
+
+  const asyncRouters = ref([])
+
+  const topMenu = ref([])
+
+  const leftMenu = ref([])
+
+  const menuMap = {}
+
+  const topActive = ref('')
+
+  const setLeftMenu = (name) => {
+    sessionStorage.setItem('topActive', name)
+    topActive.value = name
+    leftMenu.value = []
+    if (menuMap[name]?.children) {
+      leftMenu.value = menuMap[name].children
+    }
+    return menuMap[name]?.children
+  }
+
+  watchEffect(() => {
+    let topActive = sessionStorage.getItem('topActive')
+    asyncRouters.value[0]?.children.forEach((item) => {
+      if (item.hidden) return
+      menuMap[item.name] = item
+      topMenu.value.push({ ...item, children: [] })
+    })
+
+    setLeftMenu(topActive)
+  })
+
+  const routeMap = {}
+  // 从后台获取动态路由
+  const SetAsyncRouter = async () => {
+    asyncRouterFlag.value++
+    const baseRouter = [
+      {
+        path: '/layout',
+        name: 'layout',
+        component: 'view/layout/index.vue',
+        meta: {
+          title: '底层layout'
+        },
+        children: []
+      }
+    ]
+    const asyncRouterRes = await asyncMenu()
+    const asyncRouter = asyncRouterRes.data.menus
+    asyncRouter &&
+      asyncRouter.push({
+        path: 'reload',
+        name: 'Reload',
+        hidden: true,
+        meta: {
+          title: '',
+          closeTab: true
+        },
+        component: 'view/error/reload.vue'
+      })
+    formatRouter(asyncRouter, routeMap)
+    baseRouter[0].children = asyncRouter
+    if (notLayoutRouterArr.length !== 0) {
+      baseRouter.push(...notLayoutRouterArr)
+    }
+    asyncRouterHandle(baseRouter)
+    KeepAliveFilter(asyncRouter)
+    asyncRouters.value = baseRouter
+    return true
+  }
+
+  return {
+    topActive,
+    setLeftMenu,
+    topMenu,
+    leftMenu,
+    asyncRouters,
+    keepAliveRouters,
+    asyncRouterFlag,
+    SetAsyncRouter,
+    routeMap
+  }
+})

Энэ ялгаанд хэт олон файл өөрчлөгдсөн тул зарим файлыг харуулаагүй болно