Просмотр исходного кода

feat(frontend): migrate web/ to frontend/ with Wails-compatible vite config

lq 1 месяц назад
Родитель
Сommit
4585c82f2d
100 измененных файлов с 5291 добавлено и 0 удалено
  1. 2 0
      .gitignore
  2. 26 0
      frontend/.docker-compose/nginx/conf.d/my.conf
  3. 32 0
      frontend/.docker-compose/nginx/conf.d/nginx.conf
  4. 1 0
      frontend/.dockerignore
  5. 11 0
      frontend/.env.development
  6. 7 0
      frontend/.env.production
  7. 4 0
      frontend/.eslintignore
  8. 256 0
      frontend/.eslintrc.js
  9. 3 0
      frontend/.gitignore
  10. 15 0
      frontend/Dockerfile
  11. 101 0
      frontend/README.md
  12. 8 0
      frontend/babel.config.js
  13. BIN
      frontend/favicon.ico
  14. 14 0
      frontend/index.html
  15. 10 0
      frontend/jsconfig.json
  16. 37 0
      frontend/limit.js
  17. 22 0
      frontend/openDocument.js
  18. 65 0
      frontend/package.json
  19. 6 0
      frontend/postcss.config.js
  20. 33 0
      frontend/src/App.vue
  21. 146 0
      frontend/src/api/api.js
  22. 85 0
      frontend/src/api/authority.js
  23. 27 0
      frontend/src/api/authorityBtn.js
  24. 43 0
      frontend/src/api/breakpoint.js
  25. 52 0
      frontend/src/api/carInfo.js
  26. 51 0
      frontend/src/api/carOwner.js
  27. 32 0
      frontend/src/api/casbin.js
  28. 52 0
      frontend/src/api/channel.js
  29. 60 0
      frontend/src/api/device.js
  30. 14 0
      frontend/src/api/email.js
  31. 26 0
      frontend/src/api/enter.js
  32. 44 0
      frontend/src/api/fileUploadAndDownload.js
  33. 17 0
      frontend/src/api/github.js
  34. 51 0
      frontend/src/api/guardBooth.js
  35. 14 0
      frontend/src/api/jwt.js
  36. 113 0
      frontend/src/api/menu.js
  37. 43 0
      frontend/src/api/pricing.js
  38. 52 0
      frontend/src/api/pullOver.js
  39. 44 0
      frontend/src/api/shortlist.js
  40. 80 0
      frontend/src/api/sysDictionary.js
  41. 80 0
      frontend/src/api/sysDictionaryDetail.js
  42. 48 0
      frontend/src/api/sysOperationRecord.js
  43. 174 0
      frontend/src/api/user.js
  44. 171 0
      frontend/src/api/vehicle.js
  45. 1 0
      frontend/src/assets/background.svg
  46. BIN
      frontend/src/assets/dashboard.png
  47. BIN
      frontend/src/assets/docs.png
  48. BIN
      frontend/src/assets/flipped-aurora.png
  49. BIN
      frontend/src/assets/github.png
  50. 1 0
      frontend/src/assets/icons/customer-gva.svg
  51. BIN
      frontend/src/assets/kefu.png
  52. BIN
      frontend/src/assets/lc_logo.jpg
  53. BIN
      frontend/src/assets/login_background.jpg
  54. 33 0
      frontend/src/assets/login_background.svg
  55. 123 0
      frontend/src/assets/login_left.svg
  56. BIN
      frontend/src/assets/login_right_banner.jpg
  57. BIN
      frontend/src/assets/logo.jpg
  58. BIN
      frontend/src/assets/logo.png
  59. BIN
      frontend/src/assets/logo1.png
  60. BIN
      frontend/src/assets/logo_login.png
  61. BIN
      frontend/src/assets/nav_logo.png
  62. BIN
      frontend/src/assets/noBody.png
  63. BIN
      frontend/src/assets/notFound.png
  64. BIN
      frontend/src/assets/qm.png
  65. BIN
      frontend/src/assets/sign.png
  66. BIN
      frontend/src/assets/video.png
  67. 196 0
      frontend/src/components/commandMenu/index.vue
  68. 104 0
      frontend/src/components/customPic/index.vue
  69. 58 0
      frontend/src/components/exportExcel/exportExcel.vue
  70. 28 0
      frontend/src/components/exportExcel/exportTemplate.vue
  71. 40 0
      frontend/src/components/exportExcel/importExcel.vue
  72. 35 0
      frontend/src/components/office/docx.vue
  73. 33 0
      frontend/src/components/office/excel.vue
  74. 53 0
      frontend/src/components/office/index.vue
  75. 36 0
      frontend/src/components/office/pdf.vue
  76. 90 0
      frontend/src/components/richtext/rich-edit.vue
  77. 62 0
      frontend/src/components/richtext/rich-view.vue
  78. 85 0
      frontend/src/components/selectFile/selectFile.vue
  79. 466 0
      frontend/src/components/selectImage/selectImage.vue
  80. 39 0
      frontend/src/components/svgIcon/svgIcon.vue
  81. 75 0
      frontend/src/components/upload/common.vue
  82. 97 0
      frontend/src/components/upload/image.vue
  83. 33 0
      frontend/src/components/warningBar/warningBar.vue
  84. 18 0
      frontend/src/core/config.js
  85. 12 0
      frontend/src/core/gin-vue-admin.js
  86. 45 0
      frontend/src/core/global.js
  87. 41 0
      frontend/src/directive/auth.js
  88. 23 0
      frontend/src/hooks/use-windows-resize.js
  89. 195 0
      frontend/src/lang/en.js
  90. 33 0
      frontend/src/lang/index.js
  91. 195 0
      frontend/src/lang/zh-CN.js
  92. 195 0
      frontend/src/lang/zh-HK.js
  93. 195 0
      frontend/src/lang/zh-TW.js
  94. 45 0
      frontend/src/main.js
  95. 122 0
      frontend/src/permission.js
  96. 7 0
      frontend/src/pinia/index.js
  97. 40 0
      frontend/src/pinia/modules/dictionary.js
  98. 103 0
      frontend/src/pinia/modules/router.js
  99. 162 0
      frontend/src/pinia/modules/user.js
  100. 0 0
      frontend/src/plugin/email/api/email.js

+ 2 - 0
.gitignore

@@ -1,6 +1,8 @@
 .idea/
 /web/node_modules
 /web/dist
+/frontend/node_modules
+/frontend/dist
 
 .DS_Store
 

+ 26 - 0
frontend/.docker-compose/nginx/conf.d/my.conf

@@ -0,0 +1,26 @@
+server {
+    listen       8080;
+    server_name localhost;
+
+    #charset koi8-r;
+    #access_log  logs/host.access.log  main;
+
+    location / {
+        root /usr/share/nginx/html;
+        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api {
+        proxy_set_header Host $http_host;
+        proxy_set_header  X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        rewrite ^/api/(.*)$ /$1 break;  #重写
+        proxy_pass http://177.7.0.12:8888; # 设置代理服务器的协议和地址
+     }
+
+    location /api/swagger/index.html {
+        proxy_pass http://127.0.0.1:8888/swagger/index.html;
+     }
+ }

+ 32 - 0
frontend/.docker-compose/nginx/conf.d/nginx.conf

@@ -0,0 +1,32 @@
+server {
+    listen  80;
+    server_name localhost;
+
+    #charset koi8-r;
+    #access_log  logs/host.access.log  main;
+
+    location / {
+        root /usr/share/nginx/html/dist;
+        add_header Cache-Control 'no-store, no-cache, must-revalidate, proxy-revalidate, max-age=0';
+        try_files $uri $uri/ /index.html;
+    }
+
+    location /api {
+        proxy_set_header Host $http_host;
+        proxy_set_header  X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        rewrite ^/api/(.*)$ /$1 break;  #重写
+        proxy_pass http://127.0.0.1:8888; # 设置代理服务器的协议和地址
+     }
+    location  /form-generator {
+        proxy_set_header Host $http_host;
+        proxy_set_header X-Real-IP $remote_addr;
+        proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
+        proxy_set_header X-Forwarded-Proto $scheme;
+        proxy_pass http://127.0.0.1:8888;
+    }
+    location /api/swagger/index.html {
+        proxy_pass http://127.0.0.1:8888/swagger/index.html;
+     }
+ }

+ 1 - 0
frontend/.dockerignore

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

+ 11 - 0
frontend/.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
frontend/.env.production

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

+ 4 - 0
frontend/.eslintignore

@@ -0,0 +1,4 @@
+build/*.js
+src/assets
+public
+dist

+ 256 - 0
frontend/.eslintrc.js

@@ -0,0 +1,256 @@
+module.exports = {
+  root: true,
+  parserOptions: {
+    parser: '@babel/eslint-parser',
+    sourceType: 'module'
+  },
+  env: {
+    browser: true,
+    node: true,
+    es6: true
+  },
+  extends: ['plugin:vue/recommended', 'eslint:recommended'],
+  rules: {
+    'vue/no-v-model-argument': 0,
+    'vue/max-attributes-per-line': 2,
+    'vue/singleline-html-element-content-newline': 'off',
+    'vue/multiline-html-element-content-newline': 'off',
+    'vue/multi-word-component-names': 'off',
+    'vue/no-v-html': 'off',
+    'accessor-pairs': 2,
+    'arrow-spacing': [
+      2,
+      {
+        before: true,
+        after: true
+      }
+    ],
+    'block-spacing': [2, 'always'],
+    'brace-style': [
+      2,
+      '1tbs',
+      {
+        allowSingleLine: true
+      }
+    ],
+    camelcase: [
+      0,
+      {
+        properties: 'always'
+      }
+    ],
+    'comma-dangle': [2, 'only-multiline'],
+    'comma-spacing': [
+      2,
+      {
+        before: false,
+        after: true
+      }
+    ],
+    'comma-style': [2, 'last'],
+    'constructor-super': 2,
+    curly: [2, 'multi-line'],
+    'dot-location': [2, 'property'],
+    'eol-last': 2,
+    eqeqeq: ['error', 'always', { null: 'ignore' }],
+    'generator-star-spacing': [
+      2,
+      {
+        before: true,
+        after: true
+      }
+    ],
+    'handle-callback-err': [2, '^(err|error)$'],
+    indent: [
+      2,
+      2,
+      {
+        SwitchCase: 1
+      }
+    ],
+    'jsx-quotes': [2, 'prefer-single'],
+    'key-spacing': [
+      2,
+      {
+        beforeColon: false,
+        afterColon: true
+      }
+    ],
+    'keyword-spacing': [
+      2,
+      {
+        before: true,
+        after: true
+      }
+    ],
+    'new-cap': [
+      2,
+      {
+        newIsCap: true,
+        capIsNew: false
+      }
+    ],
+    'new-parens': 2,
+    'no-array-constructor': 2,
+    'no-caller': 2,
+    'no-console': 'off',
+    'no-class-assign': 2,
+    'no-cond-assign': 2,
+    'no-const-assign': 2,
+    'no-control-regex': 0,
+    'no-delete-var': 2,
+    'no-dupe-args': 2,
+    'no-dupe-class-members': 2,
+    'no-dupe-keys': 2,
+    'no-duplicate-case': 2,
+    'no-empty-character-class': 2,
+    'no-empty-pattern': 2,
+    'no-eval': 2,
+    'no-ex-assign': 2,
+    'no-extend-native': 2,
+    'no-extra-bind': 2,
+    'no-extra-boolean-cast': 2,
+    'no-extra-parens': [2, 'functions'],
+    'no-fallthrough': 2,
+    'no-floating-decimal': 2,
+    'no-func-assign': 2,
+    'no-implied-eval': 2,
+    'no-inner-declarations': [2, 'functions'],
+    'no-invalid-regexp': 2,
+    'no-irregular-whitespace': 2,
+    'no-iterator': 2,
+    'no-label-var': 2,
+    'no-labels': [
+      2,
+      {
+        allowLoop: false,
+        allowSwitch: false
+      }
+    ],
+    'no-lone-blocks': 2,
+    'no-mixed-spaces-and-tabs': 2,
+    'no-multi-spaces': 2,
+    'no-multi-str': 2,
+    'no-multiple-empty-lines': [
+      2,
+      {
+        max: 1
+      }
+    ],
+    'no-native-reassign': 2,
+    'no-negated-in-lhs': 2,
+    'no-new-object': 2,
+    'no-new-require': 2,
+    'no-new-symbol': 2,
+    'no-new-wrappers': 2,
+    'no-obj-calls': 2,
+    'no-octal': 2,
+    'no-octal-escape': 2,
+    'no-path-concat': 2,
+    'no-proto': 2,
+    'no-redeclare': 2,
+    'no-regex-spaces': 2,
+    'no-return-assign': [2, 'except-parens'],
+    'no-self-assign': 2,
+    'no-self-compare': 2,
+    'no-sequences': 2,
+    'no-shadow-restricted-names': 2,
+    'no-spaced-func': 2,
+    'no-sparse-arrays': 2,
+    'no-this-before-super': 2,
+    'no-throw-literal': 2,
+    'no-trailing-spaces': 2,
+    'no-undef': 'off',
+    'no-undef-init': 2,
+    'no-unexpected-multiline': 2,
+    'no-unmodified-loop-condition': 2,
+    'no-unneeded-ternary': [
+      2,
+      {
+        defaultAssignment: false
+      }
+    ],
+    'no-unreachable': 2,
+    'no-unsafe-finally': 2,
+    'no-unused-vars': [
+      2,
+      {
+        vars: 'all',
+        args: 'none'
+      }
+    ],
+    'no-useless-call': 2,
+    'no-useless-computed-key': 2,
+    'no-useless-constructor': 2,
+    'no-useless-escape': 0,
+    'no-whitespace-before-property': 2,
+    'no-with': 2,
+    'one-var': [
+      2,
+      {
+        initialized: 'never'
+      }
+    ],
+    'operator-linebreak': [
+      2,
+      'after',
+      {
+        overrides: {
+          '?': 'before',
+          ':': 'before'
+        }
+      }
+    ],
+    'padded-blocks': [2, 'never'],
+    quotes: [
+      2,
+      'single',
+      {
+        avoidEscape: true,
+        allowTemplateLiterals: true
+      }
+    ],
+    semi: [2, 'never'],
+    'semi-spacing': [
+      2,
+      {
+        before: false,
+        after: true
+      }
+    ],
+    'space-before-blocks': [2, 'always'],
+    'space-before-function-paren': [2, 'never'],
+    'space-in-parens': [2, 'never'],
+    'space-infix-ops': 2,
+    'space-unary-ops': [
+      2,
+      {
+        words: true,
+        nonwords: false
+      }
+    ],
+    'spaced-comment': [
+      2,
+      'always',
+      {
+        markers: ['global', 'globals', 'eslint', 'eslint-disable', '*package', '!', ',']
+      }
+    ],
+    'template-curly-spacing': [2, 'never'],
+    'use-isnan': 2,
+    'valid-typeof': 2,
+    'wrap-iife': [2, 'any'],
+    'yield-star-spacing': [2, 'both'],
+    yoda: [2, 'never'],
+    'prefer-const': 2,
+    'no-debugger': process.env.NODE_ENV === 'production' ? 2 : 0,
+    'object-curly-spacing': [
+      2,
+      'always',
+      {
+        objectsInObjects: false
+      }
+    ],
+    'array-bracket-spacing': [2, 'never']
+  }
+}

+ 3 - 0
frontend/.gitignore

@@ -0,0 +1,3 @@
+node_modules/*
+package-lock.json
+yarn.lock

+ 15 - 0
frontend/Dockerfile

@@ -0,0 +1,15 @@
+FROM node:16
+
+WORKDIR /gva_web/
+COPY . .
+
+RUN yarn && yarn build
+
+FROM nginx:alpine
+LABEL MAINTAINER="SliverHorn@sliver_horn@qq.com"
+
+COPY .docker-compose/nginx/conf.d/my.conf /etc/nginx/conf.d/my.conf
+COPY --from=0 /gva_web/dist /usr/share/nginx/html
+RUN cat /etc/nginx/nginx.conf
+RUN cat /etc/nginx/conf.d/my.conf
+RUN ls -al /usr/share/nginx/html

+ 101 - 0
frontend/README.md

@@ -0,0 +1,101 @@
+# gin-vue-admin 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
+
+```

+ 8 - 0
frontend/babel.config.js

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

BIN
frontend/favicon.ico


+ 14 - 0
frontend/index.html

@@ -0,0 +1,14 @@
+<!DOCTYPE html>
+<html lang="zh-cn">
+<head>
+    <meta charset="utf-8">
+    <meta name="viewport" content="width=device-width,initial-scale=1.0">
+    <meta content="Gin,Vue,Admin,Smart Parking,停车管理" name="keywords" />
+    <link rel="icon" href="favicon.ico">
+    <title>智慧停车管理系统</title>
+</head>
+<body>
+    <div id="app"></div>
+    <script type="module" src="./src/main.js"></script>
+</body>
+</html>

+ 10 - 0
frontend/jsconfig.json

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

+ 37 - 0
frontend/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', '文件修改成功')
+        }
+      })
+    }
+  })
+}

+ 22 - 0
frontend/openDocument.js

@@ -0,0 +1,22 @@
+/*
+    若作为代码出售的产品(任何涉及代码交付第三方作为后续开发)必须保留此脚本
+                         或标注原作者信息
+                          否则将依法维权
+*/
+
+var child_process = require('child_process')
+
+var url = 'https://www.gin-vue-admin.com'
+var cmd = ''
+console.log(process.platform)
+switch (process.platform) {
+  case 'win32':
+    cmd = 'start'
+    child_process.exec(cmd + ' ' + url)
+    break
+
+  case 'darwin':
+    cmd = 'open'
+    child_process.exec(cmd + ' ' + url)
+    break
+}

+ 65 - 0
frontend/package.json

@@ -0,0 +1,65 @@
+{
+    "name": "gin-vue-admin",
+    "version": "2.6.3",
+    "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.1.0",
+        "@vue-office/docx": "^1.3.0",
+        "@vue-office/excel": "^1.4.5",
+        "@vue-office/pdf": "^1.5.3",
+        "@vueuse/core": "^10.7.2",
+        "@wangeditor/editor": "^5.1.23",
+        "@wangeditor/editor-for-vue": "^5.1.12",
+        "axios": "^1.4.0",
+        "core-js": "^3.31.1",
+        "echarts": "5.4.3",
+        "element-plus": "^2.3.8",
+        "highlight.js": "^11.8.0",
+        "js-cookie": "^3.0.5",
+        "jsencrypt": "^3.3.2",
+        "marked": "4.3.0",
+        "mitt": "^3.0.1",
+        "nprogress": "^0.2.0",
+        "path": "^0.12.7",
+        "pinia": "^2.1.4",
+        "qs": "^6.11.2",
+        "screenfull": "^6.0.2",
+        "sortablejs": "^1.15.2",
+        "spark-md5": "^3.0.2",
+        "tailwindcss": "^3.3.3",
+        "vue": "^3.4.21",
+        "vue-i18n": "^9.14.4",
+        "vue-router": "^4.3.2",
+        "vuedraggable": "^4.1.0"
+    },
+    "devDependencies": {
+        "@babel/eslint-parser": "^7.22.9",
+        "@vitejs/plugin-legacy": "^4.1.0",
+        "@vitejs/plugin-vue": "^4.2.3",
+        "@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.3.4",
+        "babel-plugin-import": "^1.13.6",
+        "chalk": "^4.1.2",
+        "dotenv": "^16.3.1",
+        "eslint": "^8.49.0",
+        "eslint-plugin-vue": "^9.15.1",
+        "sass": "^1.54.0",
+        "terser": "^5.19.1",
+        "unplugin-auto-import": "^0.16.6",
+        "unplugin-vue-components": "^0.25.1",
+        "vite": "^4.4.6",
+        "vite-plugin-banner": "^0.7.0",
+        "vite-plugin-importer": "^0.2.5"
+    }
+}

+ 6 - 0
frontend/postcss.config.js

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

+ 33 - 0
frontend/src/App.vue

@@ -0,0 +1,33 @@
+<template>
+  <div id="app">
+    <el-config-provider :locale="zhCn">
+      <router-view />
+    </el-config-provider>
+  </div>
+</template>
+
+<script setup>
+import zhCn from 'element-plus/dist/locale/zh-cn.mjs'
+// element 2.3.8之前使用下面的语句
+// import zhCn from 'element-plus/lib/locale/lang/zh-cn'
+
+defineOptions({
+  name: 'App'
+})
+
+</script>
+<style lang="scss">
+@tailwind base;
+@tailwind components;
+@tailwind utilities;
+// 引入初始化样式
+#app {
+  background: #eee;
+  height: 100vh;
+  overflow: hidden;
+  font-weight: 400 !important;
+}
+.el-button{
+  font-weight: 400 !important;
+}
+</style>

+ 146 - 0
frontend/src/api/api.js

@@ -0,0 +1,146 @@
+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'
+  })
+}

+ 85 - 0
frontend/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
+  })
+}

+ 27 - 0
frontend/src/api/authorityBtn.js

@@ -0,0 +1,27 @@
+
+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
+  })
+}
+

+ 43 - 0
frontend/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
+  })
+}

+ 52 - 0
frontend/src/api/carInfo.js

@@ -0,0 +1,52 @@
+import service from '@/utils/request'
+// 新增车辆信息
+export const addCarInfo = (data) => {
+    return service({
+        url: '/vehicle/create',
+        method: 'post',
+        data
+    })
+}
+
+// 编辑车辆信息
+export const editCarInfo = (data) => {
+    return service({
+        url: '/vehicle/update',
+        method: 'put',
+        data
+    })
+}
+
+// 根据ID获取车辆信息
+export const carInfoId = (id) => {
+    return service({
+        url: '/vehicle/get?id=' + id,
+        method: 'get'
+    })
+}
+
+// 根据车牌号获取车辆信息
+export const carInfoPhone = (num) => {
+    return service({
+        url: '/vehicle/get-by-plate?plate_number=' + num,
+        method: 'get'
+    })
+}
+
+// 获取车辆列表
+export const obtainCarInfoList = (params) => {
+    return service({
+        url: '/vehicle/list',
+        method: 'get',
+        params
+    })
+}
+
+// 删除车辆
+export const removeCarInfo = (id) => {
+    return service({
+        url: '/vehicle/delete?id=' + id,
+        method: 'delete',
+    })
+}
+

+ 51 - 0
frontend/src/api/carOwner.js

@@ -0,0 +1,51 @@
+import service from '@/utils/request'
+// 新增车主
+export const addCarOwner = (data) => {
+  return service({
+    url: '/owner/create',
+    method: 'post',
+    data
+  })
+}
+
+// 编辑车主
+export const editCarOwner = (data) => {
+  return service({
+    url: '/owner/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据id获取车主信息
+export const carOwnerInfoId = (id) => {
+  return service({
+    url: '/owner/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 根据手机号获取车主信息
+export const carOwnerInfoPhone = (phone) => {
+  return service({
+    url: '/owner/get-by-phone?phone=' + phone,
+    method: 'get'
+  })
+}
+
+// 获取车主列表
+export const obtainCarOwnerList = (params) => {
+  return service({
+    url: '/owner/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除车主
+export const removeCarOwner = (id) => {
+  return service({
+    url: '/owner/delete?id=' + id,
+    method: 'delete',
+  })
+}

+ 32 - 0
frontend/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
+  })
+}

+ 52 - 0
frontend/src/api/channel.js

@@ -0,0 +1,52 @@
+import service from '@/utils/request'
+
+// 创建通道
+export const addChannel = (data) => {
+  return service({
+    url: '/parking/channel/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新通道
+export const editChannel = (data) => {
+  return service({
+    url: '/parking/channel/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据ID获取通道
+export const obtainChannelId = (id) => {
+  return service({
+    url: '/parking/channel/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 根据编码获取通道
+export const obtainChannelCode = (code) => {
+  return service({
+    url: '/parking/channel/get-by-code?code=' + code,
+    method: 'get'
+  })
+}
+
+// 获取通道列表
+export const obtainChannelList = (params) => {
+  return service({
+    url: '/parking/channel/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除通道
+export const deleteChannel = (id) => {
+  return service({
+    url: '/parking/channel/delete?id=' + id,
+    method: 'delete'
+  })
+}

+ 60 - 0
frontend/src/api/device.js

@@ -0,0 +1,60 @@
+import service from '@/utils/request'
+
+// 创建设备
+export const addDevice = (data) => {
+  return service({
+    url: '/parking/device/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新设备
+export const editDevice = (data) => {
+  return service({
+    url: '/parking/device/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据ID获取设备
+export const obtainDeviceId = (id) => {
+  return service({
+    url: '/parking/device/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 根据编码获取设备
+export const obtainDeviceCode = (code) => {
+  return service({
+    url: '/parking/device/get-by-code?code=' + code,
+    method: 'get'
+  })
+}
+
+// 获取设备列表
+export const obtainDeviceList = (params) => {
+  return service({
+    url: '/parking/device/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除设备
+export const deleteDevice = (id) => {
+  return service({
+    url: '/parking/device/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 连接设备
+export const joinDevice = (id) => {
+  return service({
+    url: '/parking/device/connectedDevice?id=' + id,
+    method: 'get'
+  })
+}

+ 14 - 0
frontend/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
+  })
+}

+ 26 - 0
frontend/src/api/enter.js

@@ -0,0 +1,26 @@
+import service from '@/utils/request'
+
+// 根据ID获取车辆记录
+export const obtainEnterId = () => {
+    return service({
+        url: '/vehicle/record/get',
+        method: 'GET'
+    })
+}
+
+// 根据车牌号获取车辆记录列表
+export const obtainEnterNumber = (num) => {
+    return service({
+        url: '/vehicle/record/list-by-plate?plate_number=' + num,
+        method: 'GET'
+    })
+}
+
+// 获取所有车辆记录列表
+export const obtainEnterList = (params) => {
+    return service({
+        url: '/vehicle/record/list',
+        method: 'GET',
+        params
+    })
+}

+ 44 - 0
frontend/src/api/fileUploadAndDownload.js

@@ -0,0 +1,44 @@
+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
+  })
+}

+ 17 - 0
frontend/src/api/github.js

@@ -0,0 +1,17 @@
+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'
+  })
+}

+ 51 - 0
frontend/src/api/guardBooth.js

@@ -0,0 +1,51 @@
+import service from '@/utils/request'
+
+// 创建岗亭
+export const addGuardBooth = (data) => {
+    return service({
+        url: '/parking/booth/create',
+        method: 'post',
+        data
+    })
+}
+
+// 更新岗亭
+export const editGuardBooth = (data) => {
+    return service({
+        url: '/parking/booth/update',
+        method: 'put',
+        data
+    })
+}
+
+// 根据ID获取岗亭
+export const obtainGuardBoothId = (id) => {
+    return service({
+        url: '/parking/booth/get?id=' + id,
+        method: 'get'
+    })
+}
+// 根据编码获取岗亭
+export const obtainGuardBoothCode = (code) => {
+    return service({
+        url: '/parking/booth/get-by-code?code=' + code,
+        method: 'get',
+    })
+}
+
+// 获取岗亭列表
+export const obtainGuardBoothList = (params) => {
+    return service({
+        url: '/parking/booth/list',
+        method: 'get',
+        params
+    })
+}
+
+// 删除岗亭
+export const deleteGuardBooth = (id) => {
+    return service({
+        url: '/parking/booth/delete?id=' + id,
+        method: 'delete'
+    })
+}

+ 14 - 0
frontend/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
frontend/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
+  })
+}

+ 43 - 0
frontend/src/api/pricing.js

@@ -0,0 +1,43 @@
+import service from '@/utils/request'
+// 创建收费配置
+export const addPricing = (data) => {
+  return service({
+    url: '/vehicle/fee-config/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新收费配置
+export const editPricing = (data) => {
+  return service({
+    url: '/vehicle/fee-config/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据ID获取收费配置
+export const pricingId = (id) => {
+  return service({
+    url: '/vehicle/fee-config/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获取收费配置列表
+export const obtainPricingList = (params) => {
+  return service({
+    url: '/vehicle/fee-config/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除收费配置
+export const removePricing = (id) => {
+  return service({
+    url: '/vehicle/fee-config/delete?id=' + id,
+    method: 'delete',
+  })
+}

+ 52 - 0
frontend/src/api/pullOver.js

@@ -0,0 +1,52 @@
+import service from '@/utils/request'
+
+// 创建停车区域
+export const addPullOver = (data) => {
+    return service({
+        url: '/parking/lot/create',
+        method: 'post',
+        data
+    })
+}
+
+// 更新停车区域
+export const editPullOver = (data) => {
+    return service({
+        url: '/parking/lot/update',
+        method: 'put',
+        data
+    })
+}
+
+// 根据ID获取停车区域
+export const obtainPullOverId = (id) => {
+    return service({
+        url: '/parking/lot/get?id=' + id,
+        method: 'get'
+    })
+}
+
+// 根据编码获取停车区域
+export const obtainPullOverCode = (code) => {
+    return service({
+        url: '/parking/lot/get-by-code?code=' + code,
+        method: 'get'
+    })
+}
+
+// 获取停车区域列表
+export const obtainPullOverList = (params) => {
+    return service({
+        url: '/parking/lot/list',
+        method: 'get',
+        params
+    })
+}
+
+// 删除停车区域
+export const removePullOver = (id) => {
+    return service({
+        url: '/parking/lot/delete?id=' + id,
+        method: 'delete',
+    })
+}

+ 44 - 0
frontend/src/api/shortlist.js

@@ -0,0 +1,44 @@
+import service from '@/utils/request'
+
+// 新增黑白名单
+export const addShortlist = (data) => {
+  return service({
+    url: '/shortlist/createShortlist',
+    method: 'post',
+    data
+  })
+}
+
+// 修改黑白名单
+export const updateShortlist = (data) => {
+  return service({
+    url: '/shortlist/updateShortlist',
+    method: 'put',
+    data
+  })
+}
+
+// 查询所有黑白名单
+export const queryAllShortlists = () => {
+  return service({
+    url: '/shortlist/queryAllShortlists',
+    method: 'get'
+  })
+}
+
+// 查询黑白名单
+export const queryShortlistList = (data) => {
+  return service({
+    url: '/shortlist/queryShortlistList',
+    method: 'post',
+    data
+  })
+}
+
+// 删除黑白名单
+export const deleteShortlist = (id) => {
+  return service({
+    url: '/shortlist/deleteShortlist?id=' + id,
+    method: 'delete'
+  })
+}

+ 80 - 0
frontend/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
frontend/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
frontend/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
+  })
+}

+ 174 - 0
frontend/src/api/user.js

@@ -0,0 +1,174 @@
+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 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
+  })
+}
+
+//返回公钥
+
+export const routerPublicKey = () => {
+  return service({
+    url: '/base/routerPublicKey',
+    method: 'get'
+  })
+}

+ 171 - 0
frontend/src/api/vehicle.js

@@ -0,0 +1,171 @@
+import service from '@/utils/request'
+
+// 新增车主
+export const addVehicle = (data) => {
+  return service({
+    url: '/vehicle/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新车辆信息
+export const editVehicle = (data) => {
+  return service({
+    url: '/vehicle/update',
+    method: 'put',
+    data
+  })
+}
+
+export const queryAllVehicles = () => {
+  return service({
+    url: '/vehicle/all',
+    method: 'get'
+  })
+}
+
+// 根据ID获取车辆信息
+export const obtainVehicleId = (id) => {
+  return service({
+    url: '/vehicle/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 根据车牌号获取车辆信息
+export const obtainVehicleLicense = (number) => {
+  return service({
+    url: '/vehicle/get-by-plate?plate_number=' + number,
+    method: 'get'
+  })
+}
+
+// 获取车辆列表
+export const obtainVehicleList = (params) => {
+  return service({
+    url: '/vehicle/list',
+    method: 'get',
+    params
+  })
+}
+
+// 删除车辆
+export const removeVehicleList = (params) => {
+  return service({
+    url: '/vehicle/delete',
+    method: 'delete',
+    params
+  })
+}
+
+// 车辆入场
+export const vehicleEntry = (data) => {
+  return service({
+    url: '/vehicle/entry',
+    method: 'post',
+    data
+  })
+}
+
+// 车辆出场
+export const vehicleAppear = (data) => {
+  return service({
+    url: '/vehicle/exit',
+    method: 'post',
+    data
+  })
+}
+
+// 创建收费配置
+export const createCharge = (data) => {
+  return service({
+    url: '/vehicle/fee-config/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新收费配置
+export const editCharge = (data) => {
+  return service({
+    url: '/vehicle/fee-config/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据ID获取收费配置
+export const obtainChargeId = (id) => {
+  return service({
+    url: '/vehicle/fee-config/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获取收费配置列表
+export const obtainChargeList = (id) => {
+  return service({
+    url: '/vehicle/fee-config/list',
+    method: 'get'
+  })
+}
+
+export const queryVehicleRecordList = (data) => {
+  return service({
+    url: '/vehicle/record/list',
+    method: 'post',
+    data
+  })
+}
+
+// 创建车辆类型
+export const createVehicleType = (data) => {
+  return service({
+    url: '/vehicle/type/create',
+    method: 'post',
+    data
+  })
+}
+
+// 更新车辆类型
+export const editVehicleType = (data) => {
+  return service({
+    url: '/vehicle/type/update',
+    method: 'put',
+    data
+  })
+}
+
+// 根据ID获取车辆类型
+export const obtainVehicleTypeId = (id) => {
+  return service({
+    url: '/vehicle/type/get?id=' + id,
+    method: 'get'
+  })
+}
+
+// 获取车辆类型列表
+export const obtainVehicleTypeList = (data) => {
+  return service({
+    url: '/vehicle/type/list',
+    method: 'post',
+    data
+  })
+}
+
+// 删除车辆类型
+export const removeVehicleType = (id) => {
+  return service({
+    url: '/vehicle/type/delete?id=' + id,
+    method: 'delete'
+  })
+}
+
+// 获取所有车辆类型(下拉选择)
+export const getAllVehicleTypes = () => {
+  return service({
+    url: '/vehicle/type/all',
+    method: 'get'
+  })
+}

Разница между файлами не показана из-за своего большого размера
+ 1 - 0
frontend/src/assets/background.svg


BIN
frontend/src/assets/dashboard.png


BIN
frontend/src/assets/docs.png


BIN
frontend/src/assets/flipped-aurora.png


BIN
frontend/src/assets/github.png


Разница между файлами не показана из-за своего большого размера
+ 1 - 0
frontend/src/assets/icons/customer-gva.svg


BIN
frontend/src/assets/kefu.png


BIN
frontend/src/assets/lc_logo.jpg


BIN
frontend/src/assets/login_background.jpg


+ 33 - 0
frontend/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>

Разница между файлами не показана из-за своего большого размера
+ 123 - 0
frontend/src/assets/login_left.svg


BIN
frontend/src/assets/login_right_banner.jpg


BIN
frontend/src/assets/logo.jpg


BIN
frontend/src/assets/logo.png


BIN
frontend/src/assets/logo1.png


BIN
frontend/src/assets/logo_login.png


BIN
frontend/src/assets/nav_logo.png


BIN
frontend/src/assets/noBody.png


BIN
frontend/src/assets/notFound.png


BIN
frontend/src/assets/qm.png


BIN
frontend/src/assets/sign.png


BIN
frontend/src/assets/video.png


+ 196 - 0
frontend/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, useRoute } from 'vue-router'
+import { useRouterStore } from '@/pinia/modules/router'
+import { useUserStore } from '@/pinia/modules/user'
+
+defineOptions({
+  name: 'CommandMenu',
+})
+
+const router = useRouter()
+const route = useRouter()
+const userStore = useUserStore()
+const routerStore = useRouterStore()
+const dialogVisible = ref(false)
+const searchInput = ref('')
+const options = reactive([])
+const deepMenus = (menus) => {
+  const arr = []
+  menus.forEach(menu => {
+    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('light')
+    }, {
+      label: '暗色主题',
+      func: () => changeMode('dark')
+    }, {
+      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 = (e) => {
+  if (e === null) {
+    userStore.changeSideMode('dark')
+    return
+  }
+  userStore.changeSideMode(e)
+}
+
+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{
+    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{
+      cursor: pointer;
+      background: #eee;
+      border-radius: 4px;
+    }
+  }
+}
+
+</style>

+ 104 - 0
frontend/src/components/customPic/index.vue

@@ -0,0 +1,104 @@
+<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>

+ 58 - 0
frontend/src/components/exportExcel/exportExcel.vue

@@ -0,0 +1,58 @@
+<template>
+  <el-button
+    type="primary"
+    icon="download"
+    @click="exportExcelFunc"
+  >导出</el-button>
+</template>
+
+<script setup>
+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
+  }
+  const baseUrl = import.meta.env.VITE_BASE_API
+  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 url = `${baseUrl}/sysExportTemplate/exportExcel?templateID=${props.templateId}${params ? '&' + params : ''}`
+
+  window.open(url, '_blank')
+}
+</script>

+ 28 - 0
frontend/src/components/exportExcel/exportTemplate.vue

@@ -0,0 +1,28 @@
+<template>
+  <el-button
+    type="primary"
+    icon="download"
+    @click="exportTemplate"
+  >下载模板</el-button>
+</template>
+
+<script setup>
+const props = defineProps({
+  templateId: {
+    type: String,
+    required: true
+  }
+})
+
+import { ElMessage } from 'element-plus'
+
+const exportTemplate = async() => {
+  if (props.templateId === '') {
+    ElMessage.error('组件未设置模板ID')
+    return
+  }
+  const baseUrl = import.meta.env.VITE_BASE_API
+  const url = `${baseUrl}/sysExportTemplate/exportTemplate?templateID=${props.templateId}`
+  window.open(url, '_blank')
+}
+</script>

+ 40 - 0
frontend/src/components/exportExcel/importExcel.vue

@@ -0,0 +1,40 @@
+<template>
+  <el-upload
+    :action="url"
+    :show-file-list="false"
+    :on-success="handleSuccess"
+    :multiple="false"
+  >
+    <el-button
+      type="primary"
+      icon="upload"
+    >导入</el-button>
+  </el-upload>
+
+</template>
+
+<script setup>
+import { ElMessage } from 'element-plus'
+
+const baseUrl = import.meta.env.VITE_BASE_API
+
+const props = defineProps({
+  templateId: {
+    type: String,
+    required: true
+  }
+})
+
+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>

+ 35 - 0
frontend/src/components/office/docx.vue

@@ -0,0 +1,35 @@
+<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>

+ 33 - 0
frontend/src/components/office/excel.vue

@@ -0,0 +1,33 @@
+<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>

+ 53 - 0
frontend/src/components/office/index.vue

@@ -0,0 +1,53 @@
+<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) ? 'image' : fileExt
+  },
+  { immediate: true }
+)
+const fullFileURL = computed(() => {
+  return path.value + '/' + fileUrl.value
+})
+</script>

+ 36 - 0
frontend/src/components/office/pdf.vue

@@ -0,0 +1,36 @@
+<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>

+ 90 - 0
frontend/src/components/richtext/rich-edit.vue

@@ -0,0 +1,90 @@
+<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 { useUserStore } from '@/pinia/modules/user'
+import { ElMessage } from 'element-plus'
+import { getUrl } from '@/utils/image'
+
+const userStore = useUserStore()
+
+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>

+ 62 - 0
frontend/src/components/richtext/rich-view.vue

@@ -0,0 +1,62 @@
+<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'
+
+import { useUserStore } from '@/pinia/modules/user'
+
+const userStore = useUserStore()
+
+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>

+ 85 - 0
frontend/src/components/selectFile/selectFile.vue

@@ -0,0 +1,85 @@
+<template>
+  <div>
+    <el-upload
+      multiple
+      :action="`${path}/fileUploadAndDownload/upload?noSave=1`"
+      :on-error="uploadError"
+      :on-success="uploadSuccess"
+      :show-file-list="true"
+      :file-list="fileList"
+      :limit="limit"
+      :accept="accept"
+      class="upload-btn"
+    >
+      <el-button type="primary">上传文件</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+
+import { ref, watch } from 'vue'
+import { ElMessage } from 'element-plus'
+import { useUserStore } from '@/pinia/modules/user'
+
+defineOptions({
+  name: 'UploadCommon',
+})
+
+const props = defineProps({
+  modelValue: {
+    type: Array,
+    default: () => []
+  },
+  limit: {
+    type: Number,
+    default: 3
+  },
+  accept: {
+    type: String,
+    default: ''
+  },
+})
+
+const path = ref(import.meta.env.VITE_BASE_API)
+
+const userStore = useUserStore()
+const fullscreenLoading = ref(false)
+
+const fileList = ref(props.modelValue)
+
+const emits = defineEmits(['update:modelValue'])
+
+watch(fileList.value, (val) => {
+  console.log(val)
+  emits('update:modelValue', val)
+})
+
+watch(
+  () => props.modelValue,
+  value => {
+    fileList.value = value
+  },
+  { immediate: true }
+)
+const uploadSuccess = (res) => {
+  const { data } = res
+  if (data.file) {
+    fileList.value.push({
+      name: data.file.name,
+      url: data.file.url
+    })
+    fullscreenLoading.value = false
+  }
+}
+
+const uploadError = () => {
+  ElMessage({
+    type: 'error',
+    message: '上传失败'
+  })
+  fullscreenLoading.value = false
+}
+
+</script>
+

+ 466 - 0
frontend/src/components/selectImage/selectImage.vue

@@ -0,0 +1,466 @@
+<template>
+  <div>
+    <div
+      v-if="!multiple"
+      class="update-image"
+      :style="{
+        'background-image': `url(${getUrl(model)})`,
+        'position': 'relative',
+      }"
+    >
+      <el-icon
+        v-if="isVideoExt(model || '')"
+        :size="32"
+        class="video video-icon"
+        style=""
+      >
+        <VideoPlay />
+      </el-icon>
+      <video
+        v-if="isVideoExt(model || '')"
+        class="avatar video-avatar video"
+        muted
+        preload="metadata"
+        style=""
+        @click="openChooseImg"
+      >
+        <source :src="getUrl(model) + '#t=1'">
+      </video>
+      <span
+        v-if="model"
+        class="update"
+        style="position: absolute;"
+        @click="openChooseImg"
+      >
+        <el-icon>
+          <delete />
+        </el-icon>
+        删除</span>
+      <span
+        v-else
+        class="update text-gray-600"
+        @click="openChooseImg"
+      >
+        <el-icon>
+          <plus />
+        </el-icon>
+        上传</span>
+    </div>
+    <div
+      v-else
+      class="multiple-img"
+    >
+      <div
+        v-for="(item, index) in multipleValue"
+        :key="index"
+        class="update-image"
+        :style="{
+          'background-image': `url(${getUrl(item)})`,
+          'position': 'relative',
+        }"
+      >
+        <el-icon
+          v-if="isVideoExt(item || '')"
+          :size="32"
+          class="video video-icon"
+        >
+          <VideoPlay />
+        </el-icon>
+        <video
+          v-if="isVideoExt(item || '')"
+          class="avatar video-avatar video"
+          muted
+          preload="metadata"
+          @click="deleteImg(index)"
+        >
+          <source :src="getUrl(item) + '#t=1'">
+        </video>
+        <span
+          class="update"
+          style="position: absolute;"
+          @click="deleteImg(index)"
+        >
+          <el-icon>
+            <delete />
+          </el-icon>
+          删除</span>
+      </div>
+      <div
+        v-if="!maxUpdateCount || maxUpdateCount>multipleValue.length"
+        class="add-image"
+      >
+        <span
+          class="update  text-gray-600"
+          @click="openChooseImg"
+        >
+          <el-icon>
+            <Plus />
+          </el-icon>
+          上传</span>
+      </div>
+    </div>
+    <el-drawer
+      v-model="drawer"
+      title="媒体库"
+      size="650px"
+    >
+      <warning-bar
+        title="点击“文件名/备注”可以编辑文件名或者备注内容。"
+      />
+      <div class="gva-btn-list">
+        <upload-common
+          :image-common="imageCommon"
+          @on-success="getImageList"
+        />
+        <upload-image
+          :image-url="imageUrl"
+          :file-size="512"
+          :max-w-h="1080"
+          @on-success="getImageList"
+        />
+        <el-input
+          v-model="search.keyword"
+          class="keyword"
+          placeholder="请输入文件名或备注"
+        />
+        <el-button
+          type="primary"
+          icon="search"
+          @click="getImageList"
+        >查询
+        </el-button>
+      </div>
+      <div class="media">
+        <div
+          v-for="(item,key) in picList"
+          :key="key"
+          class="media-box"
+        >
+          <div class="header-img-box-list">
+            <el-image
+              :key="key"
+              :src="getUrl(item.url)"
+              fit="cover"
+              style="width: 100%;height: 100%;"
+              @click="chooseImg(item.url)"
+            >
+              <template #error>
+                <el-icon
+                  v-if="isVideoExt(item.url || '')"
+                  :size="32"
+                  class="video video-icon"
+                >
+                  <VideoPlay />
+                </el-icon>
+                <video
+                  v-if="isVideoExt(item.url || '')"
+                  class="avatar video-avatar video"
+                  muted
+                  preload="metadata"
+                  @click="chooseImg(item.url)"
+                >
+                  <source :src="getUrl(item.url) + '#t=1'">
+                  您的浏览器不支持视频播放
+                </video>
+                <div
+                  v-else
+                  class="header-img-box-list"
+                >
+                  <el-icon class="lost-image">
+                    <icon-picture />
+                  </el-icon>
+                </div>
+              </template>
+            </el-image>
+          </div>
+          <div
+            class="img-title"
+            @click="editFileNameFunc(item)"
+          >{{ item.name }}
+          </div>
+        </div>
+      </div>
+      <el-pagination
+        :current-page="page"
+        :page-size="pageSize"
+        :total="total"
+        :style="{'justify-content':'center'}"
+        layout="total, prev, pager, next, jumper"
+        @current-change="handleCurrentChange"
+        @size-change="handleSizeChange"
+      />
+    </el-drawer>
+  </div>
+</template>
+
+<script setup>
+
+import { getUrl, isVideoExt } from '@/utils/image'
+import { onMounted, ref } from 'vue'
+import { getFileList, editFileName } 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 { Delete, Plus, Picture as IconPicture } from '@element-plus/icons-vue'
+
+const imageUrl = ref('')
+const imageCommon = ref('')
+
+const search = ref({})
+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
+  }
+})
+const multipleValue = ref([])
+
+onMounted(() => {
+  if (props.multiple) {
+    multipleValue.value = model.value
+  }
+})
+const deleteImg = (index) => {
+  multipleValue.value.splice(index, 1)
+  model.value = multipleValue.value
+}
+
+// 分页
+const handleSizeChange = (val) => {
+  pageSize.value = val
+  getImageList()
+}
+
+const handleCurrentChange = (val) => {
+  page.value = val
+  getImageList()
+}
+const editFileNameFunc = async(row) => {
+  ElMessageBox.prompt('请输入文件名或者备注', '编辑', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    inputPattern: /\S/,
+    inputErrorMessage: '不能为空',
+    inputValue: row.name
+  }).then(async({ value }) => {
+    row.name = value
+    // console.log(row)
+    const res = await editFileName(row)
+    if (res.code === 0) {
+      ElMessage({
+        type: 'success',
+        message: '编辑成功!',
+      })
+      getImageList()
+    }
+  }).catch(() => {
+    ElMessage({
+      type: 'info',
+      message: '取消修改'
+    })
+  })
+}
+
+const drawer = ref(false)
+const picList = ref([])
+
+const imageTypeList = ['png', 'jpg', 'jpeg', 'gif', 'bmp', 'webp']
+const videoTyteList = ['mp4', 'avi', 'rmvb', 'rm', 'asf', 'divx', 'mpg', 'mpeg', 'mpe', 'wmv', 'mkv', 'vob']
+
+const listObj = {
+  image: imageTypeList,
+  video: videoTyteList
+}
+
+const chooseImg = (url) => {
+  console.log(url)
+  if (props.fileType) {
+    const typeSuccess = listObj[props.fileType].some(item => {
+      if (url.includes(item)) {
+        return true
+      }
+    })
+    if (!typeSuccess) {
+      ElMessage({
+        type: 'error',
+        message: '当前类型不支持使用'
+      })
+      return
+    }
+  }
+  if (props.multiple) {
+    multipleValue.value.push(url)
+    model.value = multipleValue.value
+  } else {
+    model.value = url
+  }
+  drawer.value = false
+}
+const openChooseImg = async() => {
+  if (model.value && !props.multiple) {
+    model.value = ''
+    return
+  }
+  await getImageList()
+  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
+  }
+}
+
+</script>
+
+<style scoped lang="scss">
+
+.multiple-img {
+  display: flex;
+  gap: 8px;
+  width: 100%;
+  flex-wrap: wrap;
+}
+
+.add-image {
+  width: 120px;
+  height: 120px;
+  line-height: 120px;
+  display: flex;
+  justify-content: center;
+  border-radius: 20px;
+  border: 1px dashed #ccc;
+  background-size: cover;
+  cursor: pointer;
+}
+
+.update-image {
+  cursor: pointer;
+  width: 120px;
+  height: 120px;
+  line-height: 120px;
+  display: flex;
+  justify-content: center;
+  border-radius: 20px;
+  border: 1px dashed #ccc;
+  background-repeat: no-repeat;
+  background-size: cover;
+  position: relative;
+
+  &:hover {
+    color: #fff;
+    background: linear-gradient(
+            to bottom,
+            rgba(255, 255, 255, 0.15) 0%,
+            rgba(0, 0, 0, 0.15) 100%
+    ),
+    radial-gradient(
+            at top center,
+            rgba(255, 255, 255, 0.4) 0%,
+            rgba(0, 0, 0, 0.4) 120%
+    ) #989898;
+    background-blend-mode: multiply, multiply;
+    background-size: cover;
+
+    .update {
+      color: #fff;
+    }
+
+    .video {
+      opacity: 0.2;
+    }
+  }
+
+  .video-icon {
+    position: absolute;
+    left: calc(50% - 16px);
+    top: calc(50% - 16px);
+  }
+
+  video {
+    object-fit: cover;
+    max-width: 100%;
+    border-radius: 20px;
+  }
+
+  .update {
+    height: 120px;
+    width: 120px;
+    text-align: center;
+    color: transparent;
+    position: absolute;
+  }
+}
+
+.media {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 12px;
+  .media-box {
+    width: 120px;
+    .img-title {
+      white-space: nowrap;
+      overflow: hidden;
+      text-overflow: ellipsis;
+      line-height: 36px;
+      text-align: center;
+      cursor: pointer;
+    }
+
+    .header-img-box-list {
+      width: 120px;
+      height: 120px;
+      border: 1px dashed #ccc;
+      border-radius: 8px;
+      text-align: center;
+      line-height: 120px;
+      cursor: pointer;
+      overflow: hidden;
+
+      .el-image__inner {
+        max-width: 120px;
+        max-height: 120px;
+        vertical-align: middle;
+        width: unset;
+        height: unset;
+      }
+
+      .el-image {
+        position: relative;
+      }
+
+      .video-icon {
+        position: absolute;
+        left: calc(50% - 16px);
+        top: calc(50% - 16px);
+      }
+
+      video {
+        object-fit: cover;
+        max-width: 100%;
+        min-height: 100%;
+        border-radius: 8px;
+      }
+    }
+  }
+}
+</style>

+ 39 - 0
frontend/src/components/svgIcon/svgIcon.vue

@@ -0,0 +1,39 @@
+<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>

+ 75 - 0
frontend/src/components/upload/common.vue

@@ -0,0 +1,75 @@
+<template>
+  <div>
+    <el-upload
+      :action="`${path}/fileUploadAndDownload/upload`"
+      :before-upload="checkFile"
+      :on-error="uploadError"
+      :on-success="uploadSuccess"
+      :show-file-list="false"
+      class="upload-btn"
+    >
+      <el-button type="primary">普通上传</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { isVideoMime, isImageMime } from '@/utils/image'
+
+defineOptions({
+  name: 'UploadCommon',
+})
+
+const emit = defineEmits(['on-success'])
+const path = ref(import.meta.env.VITE_BASE_API)
+
+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>
+

+ 97 - 0
frontend/src/components/upload/image.vue

@@ -0,0 +1,97 @@
+
+<template>
+  <div>
+    <el-upload
+      :action="`${path}/fileUploadAndDownload/upload`"
+      :show-file-list="false"
+      :on-success="handleImageSuccess"
+      :before-upload="beforeImageUpload"
+      :multiple="false"
+    >
+      <el-button type="primary">压缩上传</el-button>
+    </el-upload>
+  </div>
+</template>
+
+<script setup>
+import ImageCompress from '@/utils/image'
+import { ref } from 'vue'
+import { ElMessage } from 'element-plus'
+import { useUserStore } from '@/pinia/modules/user'
+
+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 // 图片长宽上限
+  }
+})
+
+const path = ref(import.meta.env.VITE_BASE_API)
+
+const userStore = useUserStore()
+
+const beforeImageUpload = (file) => {
+  const isJPG = file.type === 'image/jpeg'
+  const isPng = file.type === '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
frontend/src/components/warningBar/warningBar.vue

@@ -0,0 +1,33 @@
+<template>
+  <div
+    class="px-1.5 py-2 flex items-center bg-amber-50 rounded gap-2 mb-3 text-amber-500"
+    :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>

+ 18 - 0
frontend/src/core/config.js

@@ -0,0 +1,18 @@
+/**
+ * 网站配置文件
+ */
+
+const config = {
+  appName: '智慧道闸',
+  appLogo: '@/assets/logo1.png',
+  showViteLogo: true,
+  logs: [],
+}
+
+export const viteLogo = (env) => {
+  if (config.showViteLogo) {
+    const chalk = require('chalk')
+  }
+}
+
+export default config

+ 12 - 0
frontend/src/core/gin-vue-admin.js

@@ -0,0 +1,12 @@
+/*
+ * gin-vue-admin web框架组
+ *
+ * */
+// 加载网站配置文件夹
+import { register } from './global'
+
+export default {
+  install: (app) => {
+    register(app)
+  }
+}

+ 45 - 0
frontend/src/core/global.js

@@ -0,0 +1,45 @@
+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')
+  for (const path in iconModules) {
+    const iconName = path.split('/').pop().replace(/\.svg$/, '')
+    // 如果iconName带空格则不加入到图标库中并且提示名称不合法
+    console.log(iconName)
+    if (iconName.indexOf(' ') !== -1) {
+      console.error(`icon ${iconName}.svg includes whitespace`)
+      continue
+    }
+    const iconComponent = createIconComponent(iconName)
+    config.logs.push({
+      'key': iconName,
+      'label': iconName,
+    })
+    app.component(iconName, 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
+}

+ 41 - 0
frontend/src/directive/auth.js

@@ -0,0 +1,41 @@
+// 权限按钮展示指令
+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)
+        }
+      }
+    })
+  }
+}
+

+ 23 - 0
frontend/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,
+  }
+}

+ 195 - 0
frontend/src/lang/en.js

@@ -0,0 +1,195 @@
+export default {
+  park: 'Parking Area',
+  booth: 'Guard Booth',
+  access: 'Passage',
+  device: 'Device',
+  modify: 'Edit',
+  remove: 'Delete',
+  new: 'Add',
+  reload: 'Refresh',
+  coding: 'Code',
+  Name: 'Name',
+  IpAddress: 'IP Address',
+  Remarks: 'Remarks',
+  Action: 'Operation',
+  Direction: 'In/Out Direction',
+  Allow: 'Allow temporary vehicles entry and exit',
+  Type: 'Category',
+  Port: 'Port',
+  Enable: 'Enable',
+  Connect: 'Connect',
+  ConnectSuccess: 'Connect Success',
+  GuardAdd: 'Add Guard Booth',
+  GuardEdit: 'Edit Guard Booth',
+  BoothCode: 'Booth Code',
+  BoothName: 'Booth Name',
+  Passage_Booth: 'Passage Booth',
+  Passage_Code: 'Channel Code',
+  Passage_Name: 'Channel Name',
+  Entry_Exit: 'Entry Exit Direction',
+  advance: 'advance',
+  out: 'out',
+  yes: 'yes',
+  no: 'no',
+  Cancel: 'Cancel',
+  Confirm: 'Confirm',
+  Device_Channel: 'Device Channel',
+  Device_Name: 'Device Name',
+  Device_Code: 'Device Code',
+  Connection_Mode: 'Connection Mode',
+  Area_Code: 'Area Code',
+  Surname: 'Surname',
+  Mobile_Phone: 'Mobile Phone',
+  Email: 'Email',
+  Expire_Time: 'Expire Time',
+  roster: 'Black & White List',
+  Search: 'Search',
+  LicensePlateNo: 'License Plate No',
+  RFID_Tag: 'RFID Tag',
+  List_Type: 'List Type',
+  Reset: 'Reset',
+  WhiteList: 'White List',
+  BlackList: 'Black List',
+  SelectVehicle: 'Select Vehicle',
+  CarOwner: 'Car Owner',
+  AddCarOwner: 'Add Car Owner',
+  ElectronicTag: 'Electronic Tag',
+  VehicleType: 'Vehicle Type',
+  VehicleBrand: 'Vehicle Brand',
+  VehicleColor: 'Vehicle Color',
+  ManageVehicleType: 'Manage Vehicle Type',
+  AddNewType: 'Add New Type',
+  BindOwner: 'Bind Owner',
+  InitialFee: 'Initial Fee',
+  UnitFee: 'Unit Fee',
+  IsVipFree: 'VIP Free',
+  DailyMaxFee: 'Daily Max Fee',
+  VipDiscount: 'VIP Discount',
+  FreeDuration: 'Free Duration (Min)',
+  StartFee: 'Start Fee',
+  UnitTime: 'Unit Time (Min)',
+  AddChargeConfig: 'Add Charge Config',
+  EditChargeConfig: 'Edit Charge Config',
+  EntryTime: 'Entry Time',
+  ExitTime: 'Exit Time',
+  StayDuration: 'Stay Duration',
+  Fee: 'Fee',
+  PayStatus: 'Payment Status',
+  ExitBooth: 'Exit Booth',
+  ExitPassage: 'Exit Passage',
+  ParkManage: 'Parking Lot Manage',
+  ParkInfoManage: 'Parking Info Manage',
+  OwnerManage: 'Owner Manage',
+  BlackWhiteList: 'Black White List',
+  VehicleInfo: 'Vehicle Info',
+  ChargeConfig: 'Charge Config',
+  ReportInfoQuery: 'Report Info Query',
+  InOutRecord: 'In Out Record',
+  AbnormalReport: 'Abnormal Report',
+  StatisticsQuery: 'Statistics Query',
+  OnlineVehicleStats: 'Online Vehicle Statistics',
+  deleteBoothTip: 'Are you sure you want to delete this booth?',
+  deletePassageTip: 'Are you sure you want to delete this access?',
+  deleteDeviceTip: 'Are you sure you want to delete this device?',
+  delOwnerTip: 'Are you sure to delete this car owner information?',
+  tip: 'Prompt',
+  deleteSuccess: 'Deleted successfully',
+  deleteFailed: 'Deletion failed',
+  cancelDelete: 'Delete canceled',
+  editSuccess: 'Edit successfully',
+  editFailed: 'Edit failed',
+  isVip: 'Become VIP',
+  noVip: 'Not VIP',
+  carPlateInput: 'Please enter license plate number',
+  carTypeSelect: 'Please select vehicle type',
+  ownerSelect: 'Please select owner',
+  manageCarType: 'Manage Vehicle Types',
+  typeName: 'Type Name',
+  confirmDeleteCar: 'Are you sure you want to delete this vehicle?',
+  rfidLabel: 'RFID Tag (For UHF)',
+  placeholderPorsche: 'e.g. Porsche',
+  placeholderBlack: 'e.g. Black',
+  tempCarOptional: 'Optional for temporary vehicles',
+  parkTip: 'e.g. Monthly Rental Car',
+  licensePlate: 'License Plate',
+  view: 'View',
+  vehicleNumber: 'Vehicle Identification Number',
+  ownerName: 'Owner Name',
+  blackWhiteList: 'Blacklist & Whitelist Management',
+  expireTime: 'Expiration Time',
+  pleaseSelectVehicle: 'Please select a vehicle',
+  permanentValidity: 'No selection means permanent valid',
+  saveFail: 'Save failed',
+  addSuccess: 'Added successfully',
+  confirmDelete: 'Confirm delete?',
+  parking: 'ParkingManage', // 车场管理
+  parkingInfo: 'ParkingInfoManage', // 车场信息管理
+  carOwner: 'OwnerManage', // 车主管理
+  carInfo: 'VehicleInfo', // 车辆信息
+  pricing: 'FeeConfig', // 收费配置
+  report: 'ReportQuery', // 报表查询
+  enter: 'AccessRecord', // 进出记录
+  sheet: 'ExceptionReport', // 异常报表
+  statistics: 'StatQuery', // 统计查询
+  vehicle: 'VehicleStat', // 场内车辆统计
+  dashboard: 'Dashboard', // 数据看板
+  superAdmin: 'SuperAdmin', // 超级管理员
+  authority: 'RoleManage', // 角色管理
+  menu: 'MenuManage', // 菜单管理
+  api: 'APIManage', // 接口管理
+  user: 'UserManage', // 用户管理
+  dictionary: 'DictManage', // 字典管理
+  operation: 'OperHistory', // 操作日志
+  person: 'PersonalInformation',
+  areaName: 'Area Name',
+  areaCapacity: 'Area Capacity',
+  createArea: 'Create Area',
+  editArea: 'Edit Area',
+  addPassage: 'Add Passage',
+  editPassage: 'Edit Passage',
+  addDevice: 'Add Device',
+  editDevice: 'Edit Device',
+  smartBarrier: 'SmartBarrier',
+  language: 'Language',
+  currRole: 'CurrentRole',
+  menuCmd: 'MenuCommand',
+  personalInfo: 'PersonalInfo',
+  logout: 'Logout',
+  barrierSystem: 'Smart Barrier System',
+  placeholderUser: 'Please enter username',
+  placeholderPwd: 'Please enter password',
+  placeholderCode: 'Please enter verification code',
+  login: 'Login',
+  selectParkArea: 'Please select parking area',
+  pleaseSelect: 'Please select',
+  serialName: 'Serial Port Name',
+  baudRate: 'Baud Rate',
+  inputPhone: 'Please enter phone number',
+  inputOwnerName: 'Please enter owner name',
+  editOwner: 'Edit Owner',
+  selectPavilion: 'Please select booth',
+  fillCompleteInfo: 'Please fill in all info except remarks',
+  allow: 'Allow',
+  disallow: 'Disallow',
+  enable: 'Enable',
+  disable: 'Disable',
+  currentDate: 'Current Date',
+  addFail: 'Add Failed',
+  vipFree: 'VIP Free',
+  vipNotFree: 'VIP Not Free',
+  noLabel: 'No Label',
+  permanentValid: 'Permanent Valid',
+  addCar: 'Add Vehicle',
+  editCar: 'Edit Vehicle',
+  unknown: 'Unknown',
+  tempCar: 'Temporary Vehicle',
+  getListFail: 'Failed to get list',
+  inputTypeName: 'Please enter type name',
+  notOut: 'Not Exit',
+  unpaid: 'Unpaid',
+  paid: 'Paid',
+  minute: 'Minute',
+  entryImg: 'Entry Image',
+  exitImg: 'Exit Image',
+  recordDetail: 'Access Record Details'
+}

+ 33 - 0
frontend/src/lang/index.js

@@ -0,0 +1,33 @@
+import { reactive, computed } from 'vue'
+import zhCN from './zh-CN'
+import zhHK from './zh-HK'
+import zhTW from './zh-TW'
+import en from './en'
+
+// 语言包
+const messages = {
+  'zh-CN': zhCN,
+  'zh-HK': zhHK,
+  'zh-TW': zhTW,
+  'en': en
+}
+
+// 当前语言(持久化到本地)
+const locale = reactive({
+  lang: localStorage.getItem('lang') || 'zh-CN'
+})
+
+// 切换语言
+const setLang = (lang) => {
+  locale.lang = lang
+  localStorage.setItem('lang', lang)
+}
+
+// 当前使用的翻译文本
+const t = computed(() => messages[locale.lang])
+
+export {
+  locale,
+  t,
+  setLang
+}

+ 195 - 0
frontend/src/lang/zh-CN.js

@@ -0,0 +1,195 @@
+export default {
+  park: '停车区域',
+  booth: '岗亭',
+  access: '通道',
+  device: '设备',
+  modify: '编辑',
+  remove: '删除',
+  new: '新增',
+  reload: '刷新',
+  coding: '编码',
+  Name: '名称',
+  IpAddress: 'ip地址',
+  Remarks: '备注',
+  Action: '操作',
+  Direction: '进出方向',
+  advance: '进',
+  out: '出',
+  yes: '是',
+  no: '否',
+  Allow: '是否允许临时车进出',
+  Type: '类别',
+  Port: '端口',
+  Enable: '是否启用',
+  Connect: '连接',
+  ConnectSuccess: '连接成功',
+  GuardAdd: '岗亭新增',
+  GuardEdit: '岗亭编辑',
+  BoothCode: '岗亭编码',
+  BoothName: '岗亭名称',
+  Passage_Booth: '通道岗亭',
+  Passage_Code: '通道编码',
+  Passage_Name: '通道名称',
+  Entry_Exit: '进出方向',
+  Cancel: '取消',
+  Confirm: '确认',
+  Device_Channel: '设备通道',
+  Device_Name: '设备名称',
+  Device_Code: '设备编码',
+  Connection_Mode: '连接方式',
+  Area_Code: '区域编码',
+  Surname: '姓氏',
+  Mobile_Phone: '手机',
+  Email: '邮箱',
+  Expire_Time: '过期时间',
+  roster: '黑白名单',
+  Search: '搜索',
+  LicensePlateNo: '车牌号码',
+  licensePlate: '车牌',
+  RFID_Tag: 'RFID标签',
+  List_Type: '名单类型',
+  Reset: '重置',
+  WhiteList: '白名单',
+  BlackList: '黑名单',
+  SelectVehicle: '选择车辆',
+  CarOwner: '车主',
+  AddCarOwner: '新增车主',
+  ElectronicTag: '电子标签',
+  VehicleType: '车辆类型',
+  VehicleBrand: '车辆品牌',
+  VehicleColor: '车辆颜色',
+  ManageVehicleType: '管理车辆类型',
+  AddNewType: '新增类型',
+  BindOwner: '绑定车主',
+  InitialFee: '初始费用',
+  UnitFee: '单位费用',
+  IsVipFree: '是否为VIP免费',
+  DailyMaxFee: '每日最大费用',
+  VipDiscount: 'vip折扣',
+  FreeDuration: '免费时长(分钟)',
+  StartFee: '起步费用',
+  UnitTime: '单位时间(分钟)',
+  AddChargeConfig: '新增收费配置',
+  EditChargeConfig: '编辑收费配置',
+  EntryTime: '入场时间',
+  ExitTime: '出场时间',
+  StayDuration: '停留时长',
+  Fee: '费用',
+  PayStatus: '支付状态',
+  ExitBooth: '出场岗亭',
+  ExitPassage: '出场通道',
+  ParkManage: '停车场管理',
+  ParkInfoManage: '停车信息管理',
+  OwnerManage: '车主管理',
+  BlackWhiteList: '黑白名单',
+  VehicleInfo: '车辆信息',
+  ChargeConfig: '收费配置',
+  ReportInfoQuery: '报表信息查询',
+  InOutRecord: '进出记录',
+  AbnormalReport: '异常报表',
+  StatisticsQuery: '统计信息查询',
+  OnlineVehicleStats: '在线车辆统计',
+  deleteBoothTip: '确定要删除该岗亭吗?',
+  deletePassageTip: '确定删除该通道吗?',
+  deleteDeviceTip: '确定要删除该设备吗?',
+  delOwnerTip: '确定要删除该车主信息吗',
+  tip: '提示',
+  deleteSuccess: '删除成功',
+  deleteFailed: '删除失败',
+  cancelDelete: '取消删除',
+  editSuccess: '编辑成功',
+  editFailed: '编辑失败',
+  isVip: '已成为VIP',
+  noVip: '未成为VIP',
+  carPlateInput: '请输入车牌号',
+  carTypeSelect: '请选择车辆类型',
+  ownerSelect: '请选择车主',
+  manageCarType: '管理车辆类型',
+  typeName: '类型名称',
+  confirmDeleteCar: '确定删除该车辆吗',
+  rfidLabel: 'RFID标签(UHF用)',
+  placeholderPorsche: '如:保时捷',
+  placeholderBlack: '如:黑色',
+  tempCarOptional: '临时车可不选',
+  parkTip: '如:月租车',
+  view: '查看',
+  vehicleNumber: '车辆识别号码',
+  ownerName: '车主名称',
+  blackWhiteList: '黑白名单管理',
+  expireTime: '到期时间',
+  pleaseSelectVehicle: '请选择车辆',
+  permanentValidity: '不选则永久有效',
+  saveFail: '保存失败',
+  addSuccess: '新增成功',
+  confirmDelete: '确定删除?',
+  parking: '停车场管理',
+  parkingInfo: '停车信息管理',
+  carOwner: '车主管理',
+  carInfo: '车辆信息',
+  pricing: '收费配置',
+  report: '报表信息查询',
+  enter: '进出记录',
+  sheet: '异常报表',
+  statistics: '统计信息查询',
+  vehicle: '在场车辆统计',
+  dashboard: '仪表盘',
+  superAdmin: '超级管理员',
+  authority: '角色管理',
+  menu: '菜单管理',
+  api: 'API管理',
+  user: '用户管理',
+  dictionary: '字典管理',
+  operation: '操作历史',
+  person: '个人信息',
+  areaName: '区域名称',
+  areaCapacity: '区域容量',
+  createArea: '创建区域',
+  editArea: '编辑区域',
+  addPassage: '通道新增',
+  editPassage: '通道编辑',
+  addDevice: '新增设备',
+  editDevice: '编辑设备',
+  smartBarrier: '智慧道闸',
+  language: '语言',
+  currRole: '当前角色',
+  menuCmd: '菜单指令',
+  personalInfo: '个人信息',
+  logout: '登出',
+  barrierSystem: '智慧道闸系统',
+  placeholderUser: '请输入用户名',
+  placeholderPwd: '请输入密码',
+  placeholderCode: '请输入验证码',
+  login: '登录',
+  selectParkArea: '请选择停车区域',
+  pleaseSelect: '请选择',
+  serialName: '串口名称',
+  baudRate: '串口波特率',
+  inputPhone: '请输入手机号',
+  inputOwnerName: '请输入车主名称',
+  editOwner: '编辑车主',
+  selectPavilion: '请选择岗亭',
+  fillCompleteInfo: '请将除备注以外的信息填写完整',
+  allow: '允许',
+  disallow: '不允许',
+  enable: '启用',
+  disable: '不启用',
+  currentDate: '当前日期',
+  addFail: '新增失败',
+  vipFree: 'VIP免费',
+  vipNotFree: 'VIP不免费',
+  noLabel: '无标签',
+  permanentValid: '永久有效',
+  addCar: '新增车辆',
+  editCar: '编辑车辆',
+  unknown: '未知',
+  tempCar: '临时车辆',
+  getListFail: '获取列表失败',
+  inputTypeName: '请输入类型名称',
+  notOut: '未出场',
+  unpaid: '未支付',
+  paid: '已支付',
+  minute: '分钟',
+  entryImg: '入场图片',
+  exitImg: '出场图片',
+  recordDetail: '进出记录详情'
+}

+ 195 - 0
frontend/src/lang/zh-HK.js

@@ -0,0 +1,195 @@
+export default {
+  park: '停車區域',
+  booth: '崗亭',
+  access: '通道',
+  device: '設備',
+  modify: '編輯',
+  remove: '刪除',
+  new: '新增',
+  reload: '重新整理',
+  coding: '編碼',
+  Name: '名稱',
+  IpAddress: 'IP位址',
+  Remarks: '備註',
+  Action: '操作',
+  Direction: '進出方向',
+  advance: '進',
+  out: '出',
+  yes: '是',
+  no: '否',
+  Allow: '是否容許臨時車輛進出',
+  Type: '類別',
+  Port: '埠口',
+  Enable: '是否啟用',
+  Connect: '連接',
+  ConnectSuccess: '連接成功',
+  GuardAdd: '崗亭新增',
+  GuardEdit: '編輯崗亭',
+  BoothCode: '崗亭編碼',
+  BoothName: '崗亭名稱',
+  Passage_Booth: '通道崗亭',
+  Passage_Code: '通道編碼',
+  Passage_Name: '通道名稱',
+  Entry_Exit: '進出方向',
+  Cancel: '取消',
+  Confirm: '確認',
+  Device_Channel: '裝置通道',
+  Device_Name: '裝置名稱',
+  Device_Code: '裝置編碼',
+  Connection_Mode: '連接方式',
+  Area_Code: '區域編碼',
+  Surname: '姓氏',
+  Mobile_Phone: '手提電話',
+  Email: '電郵',
+  Expire_Time: '過期時間',
+  roster: '黑白名單',
+  Search: '搜尋',
+  LicensePlateNo: '車牌號碼',
+  RFID_Tag: 'RFID標籤',
+  List_Type: '名單類型',
+  Reset: '重設',
+  WhiteList: '白名單',
+  BlackList: '黑名單',
+  SelectVehicle: '選擇車輛',
+  CarOwner: '車主',
+  AddCarOwner: '新增車主',
+  ElectronicTag: '電子標籤',
+  VehicleType: '車輛類型',
+  VehicleBrand: '車輛品牌',
+  VehicleColor: '車輛顏色',
+  ManageVehicleType: '管理車輛類型',
+  AddNewType: '新增類型',
+  BindOwner: '綁定車主',
+  InitialFee: '初始費用',
+  UnitFee: '單位費用',
+  IsVipFree: '是否為VIP免費',
+  DailyMaxFee: '每日最大費用',
+  VipDiscount: 'VIP折扣',
+  FreeDuration: '免費時長(分鐘)',
+  StartFee: '起步費用',
+  UnitTime: '單位時間(分鐘)',
+  AddChargeConfig: '新增收費配置',
+  EditChargeConfig: '編輯收費配置',
+  EntryTime: '入場時間',
+  ExitTime: '出場時間',
+  StayDuration: '停留時長',
+  Fee: '費用',
+  PayStatus: '支付狀態',
+  ExitBooth: '出場崗亭',
+  ExitPassage: '出場通道',
+  ParkManage: '停車場管理',
+  ParkInfoManage: '停車資訊管理',
+  OwnerManage: '車主管理',
+  BlackWhiteList: '黑白名單',
+  VehicleInfo: '車輛資訊',
+  ChargeConfig: '收費配置',
+  ReportInfoQuery: '報表資訊查詢',
+  InOutRecord: '進出紀錄',
+  AbnormalReport: '異常報表',
+  StatisticsQuery: '統計資訊查詢',
+  OnlineVehicleStats: '線上車輛統計',
+  deleteBoothTip: '確定要刪除此崗亭嗎?',
+  deletePassageTip: '確定刪除此通道嗎?',
+  deleteDeviceTip: '確定要刪除此設備嗎?',
+  delOwnerTip: '確定要刪除該車主資訊嗎',
+  tip: '提示',
+  deleteSuccess: '刪除成功',
+  deleteFailed: '刪除失敗',
+  cancelDelete: '取消刪除',
+  editSuccess: '編輯成功',
+  editFailed: '编辑失敗',
+  isVip: '已成為VIP',
+  noVip: '未成為VIP',
+  carPlateInput: '請輸入車牌號碼',
+  carTypeSelect: '請選擇車輛類型',
+  ownerSelect: '請選擇車主',
+  manageCarType: '管理車輛類型',
+  typeName: '類型名稱',
+  confirmDeleteCar: '確定刪除該車輛嗎',
+  rfidLabel: 'RFID標籤(UHF用)',
+  placeholderPorsche: '如:保時捷',
+  placeholderBlack: '如:黑色',
+  tempCarOptional: '臨時車可唔選擇',
+  parkTip: '如:月租車',
+  licensePlate: '車牌',
+  view: '查看',
+  vehicleNumber: '車輛識別號碼',
+  ownerName: '車主名稱',
+  blackWhiteList: '黑白名單管理',
+  expireTime: '到期時間',
+  pleaseSelectVehicle: '請選擇車輛',
+  permanentValidity: '唔揀就永久有效',
+  saveFail: '儲存失敗',
+  addSuccess: '新增成功',
+  confirmDelete: '確定刪除?',
+  parking: '停車場管理',
+  parkingInfo: '停車資訊管理',
+  carOwner: '車主管理',
+  carInfo: '車輛資訊',
+  pricing: '收費設定',
+  report: '報表資訊查詢',
+  enter: '出入記錄',
+  sheet: '異常報表',
+  statistics: '統計資訊查詢',
+  vehicle: '在場車輛統計',
+  dashboard: '儀表板',
+  superAdmin: '超級管理員',
+  authority: '角色管理',
+  menu: '選單管理',
+  api: 'API管理',
+  user: '用戶管理',
+  dictionary: '字典管理',
+  operation: '操作記錄',
+  person: '個人資訊',
+  areaName: '區域名稱',
+  areaCapacity: '區域容量',
+  createArea: '建立區域',
+  editArea: '編輯區域',
+  addPassage: '通道新增',
+  editPassage: '通道編輯',
+  addDevice: '新增設備',
+  editDevice: '編輯設備',
+  smartBarrier: '智慧閘機',
+  language: '語言',
+  currRole: '當前角色',
+  menuCmd: '選單指令',
+  personalInfo: '個人資訊',
+  logout: '登出',
+  barrierSystem: '智慧閘機系統',
+  placeholderUser: '請輸入用戶名',
+  placeholderPwd: '請輸入密碼',
+  placeholderCode: '請輸入驗證碼',
+  login: '登入',
+  selectParkArea: '請選擇停車區域',
+  pleaseSelect: '請選擇',
+  serialName: '串口名稱',
+  baudRate: '串口波特率',
+  inputPhone: '請輸入手機號',
+  inputOwnerName: '請輸入車主名稱',
+  editOwner: '編輯車主',
+  selectPavilion: '請選擇崗亭',
+  fillCompleteInfo: '請填妥備註以外所有資料',
+  allow: '允許',
+  disallow: '不允許',
+  enable: '啟用',
+  disable: '不啟用',
+  currentDate: '當前日期',
+  addFail: '新增失敗',
+  vipFree: 'VIP免費',
+  vipNotFree: 'VIP不免費',
+  noLabel: '無標籤',
+  permanentValid: '永久有效',
+  addCar: '新增車輛',
+  editCar: '編輯車輛',
+  unknown: '未知',
+  tempCar: '臨時車輛',
+  getListFail: '獲取列表失敗',
+  inputTypeName: '請輸入類型名稱',
+  notOut: '未出場',
+  unpaid: '未支付',
+  paid: '已支付',
+  minute: '分鐘',
+  entryImg: '入場圖片',
+  exitImg: '出場圖片',
+  recordDetail: '進出記錄詳情'
+}

+ 195 - 0
frontend/src/lang/zh-TW.js

@@ -0,0 +1,195 @@
+export default {
+  park: '停車區域',
+  booth: '崗亭',
+  access: '通道',
+  device: '裝置',
+  modify: '編輯',
+  remove: '刪除',
+  new: '新增',
+  reload: '重新整理',
+  coding: '編碼',
+  Name: '名稱',
+  IpAddress: 'IP位址',
+  Remarks: '備註',
+  Action: '操作',
+  Direction: '進出方向',
+  Allow: '是否允許臨時車輛進出',
+  Type: '類別',
+  Port: '連接埠',
+  Enable: '是否啟用',
+  Connect: '連線',
+  ConnectSuccess: '連線成功',
+  GuardAdd: '崗亭新增',
+  GuardEdit: '編輯崗亭',
+  BoothCode: '崗亭編號',
+  BoothName: '崗亭名稱',
+  Passage_Booth: '通道崗亭',
+  Passage_Code: '通道代碼',
+  Passage_Name: '通道名稱',
+  Entry_Exit: '進出方向',
+  advance: '進',
+  out: '出',
+  yes: '是',
+  no: '否',
+  Cancel: '取消',
+  Confirm: '確認',
+  Device_Channel: '裝置通道',
+  Device_Name: '裝置名稱',
+  Device_Code: '裝置編號',
+  Connection_Mode: '連線方式',
+  Area_Code: '區域代碼',
+  Surname: '姓氏',
+  Mobile_Phone: '行動電話',
+  Email: '電子信箱',
+  Expire_Time: '到期時間',
+  Black_White: '黑白名單',
+  Search: '搜尋',
+  LicensePlateNo: '車牌號碼',
+  RFID_Tag: 'RFID標籤',
+  List_Type: '名單類型',
+  Reset: '重設',
+  WhiteList: '白名單',
+  BlackList: '黑名單',
+  SelectVehicle: '選擇車輛',
+  CarOwner: '車主',
+  AddCarOwner: '新增車主',
+  ElectronicTag: '電子標籤',
+  VehicleType: '車輛類型',
+  VehicleBrand: '車輛品牌',
+  VehicleColor: '車輛顏色',
+  ManageVehicleType: '管理車輛類型',
+  AddNewType: '新增類型',
+  BindOwner: '綁定車主',
+  InitialFee: '初始費用',
+  UnitFee: '單位費用',
+  IsVipFree: '是否為VIP免費',
+  DailyMaxFee: '每日最大費用',
+  VipDiscount: 'VIP折扣',
+  FreeDuration: '免費時長(分鐘)',
+  StartFee: '起步費用',
+  UnitTime: '單位時間(分鐘)',
+  AddChargeConfig: '新增收費設定',
+  EditChargeConfig: '編輯收費設定',
+  EntryTime: '入場時間',
+  ExitTime: '出場時間',
+  StayDuration: '停留時長',
+  Fee: '費用',
+  PayStatus: '付款狀態',
+  ExitBooth: '出場崗亭',
+  ExitPassage: '出場通道',
+  ParkManage: '停車場管理',
+  ParkInfoManage: '停車資訊管理',
+  OwnerManage: '車主管理',
+  roster: '黑白名單',
+  VehicleInfo: '車輛資訊',
+  ChargeConfig: '收費設定',
+  ReportInfoQuery: '報表資訊查詢',
+  InOutRecord: '進出紀錄',
+  AbnormalReport: '異常報表',
+  StatisticsQuery: '統計資訊查詢',
+  OnlineVehicleStats: '線上車輛統計',
+  deleteBoothTip: '確定要刪除此崗亭嗎?',
+  deletePassageTip: '確定刪除此通道嗎?',
+  deleteDeviceTip: '確定要刪除此設備嗎?',
+  delOwnerTip: '確定要刪除該車主資訊嗎',
+  tip: '提示',
+  deleteSuccess: '刪除成功',
+  deleteFailed: '刪除失敗',
+  cancelDelete: '取消刪除',
+  editSuccess: '編輯成功',
+  editFailed: '編輯失敗',
+  isVip: '已成為VIP',
+  noVip: '未成為VIP',
+  carPlateInput: '請輸入車牌號碼',
+  carTypeSelect: '請選擇車輛類別',
+  ownerSelect: '請選擇車主',
+  manageCarType: '管理車輛類別',
+  typeName: '類別名稱',
+  confirmDeleteCar: '確定刪除該車輛嗎',
+  rfidLabel: 'RFID標籤(UHF用)',
+  placeholderPorsche: '如:保時捷',
+  placeholderBlack: '如:黑色',
+  tempCarOptional: '臨時車可不選',
+  parkTip: '如:月租車',
+  licensePlate: '車牌',
+  view: '檢視',
+  vehicleNumber: '車輛識別號碼',
+  ownerName: '車主名稱',
+  blackWhiteList: '黑白名單管理',
+  expireTime: '到期時間',
+  pleaseSelectVehicle: '請選擇車輛',
+  permanentValidity: '未選取則永久有效',
+  saveFail: '儲存失敗',
+  addSuccess: '新增成功',
+  confirmDelete: '確定刪除?',
+  parking: '停車場管理',
+  parkingInfo: '停車資訊管理',
+  carOwner: '車主管理',
+  carInfo: '車輛資訊',
+  pricing: '收費設定',
+  report: '報表查詢',
+  enter: '進出紀錄',
+  sheet: '異常報表',
+  statistics: '統計查詢',
+  vehicle: '場內車輛統計',
+  dashboard: '儀表板',
+  superAdmin: '超級管理員',
+  authority: '角色管理',
+  menu: '選單管理',
+  api: 'API管理',
+  user: '使用者管理',
+  dictionary: '字典管理',
+  operation: '操作紀錄',
+  person: '個人資訊',
+  areaName: '區域名稱',
+  areaCapacity: '區域容量',
+  createArea: '建立區域',
+  editArea: '編輯區域',
+  addPassage: '通道新增',
+  editPassage: '通道編輯',
+  addDevice: '新增設備',
+  editDevice: '編輯設備',
+  smartBarrier: '智慧閘門',
+  language: '語言',
+  currRole: '目前角色',
+  menuCmd: '選單指令',
+  personalInfo: '個人資訊',
+  logout: '登出',
+  barrierSystem: '智慧閘門系統',
+  placeholderUser: '請輸入使用者名稱',
+  placeholderPwd: '請輸入密碼',
+  placeholderCode: '請輸入驗證碼',
+  login: '登入',
+  selectParkArea: '請選擇停車區域',
+  pleaseSelect: '請選擇',
+  serialName: '串列埠名稱',
+  baudRate: '串列埠鮑率',
+  inputPhone: '請輸入手機號',
+  inputOwnerName: '請輸入車主名稱',
+  editOwner: '編輯車主',
+  selectPavilion: '請選擇崗亭',
+  fillCompleteInfo: '請填寫備註以外之完整資訊',
+  allow: '允許',
+  disallow: '不允許',
+  enable: '啟用',
+  disable: '不啟用',
+  currentDate: '目前日期',
+  addFail: '新增失敗',
+  vipFree: 'VIP免費',
+  vipNotFree: 'VIP不免費',
+  noLabel: '無標籤',
+  permanentValid: '永久有效',
+  addCar: '新增車輛',
+  editCar: '編輯車輛',
+  unknown: '未知',
+  tempCar: '臨時車輛',
+  getListFail: '獲取列表失敗',
+  inputTypeName: '請輸入類型名稱',
+  notOut: '未出場',
+  unpaid: '未付款',
+  paid: '已付款',
+  minute: '分鐘',
+  entryImg: '入場圖片',
+  exitImg: '出場圖片',
+  recordDetail: '進出記錄詳情'
+}

+ 45 - 0
frontend/src/main.js

@@ -0,0 +1,45 @@
+import 'element-plus/es/components/message/style/css'
+import 'element-plus/es/components/loading/style/css'
+import 'element-plus/es/components/notification/style/css'
+import 'element-plus/es/components/message-box/style/css'
+import './style/element_visiable.scss'
+import { createApp } from 'vue'
+// 引入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'
+import { initDom } from './utils/positionToCode'
+import { t, setLang, locale } from './lang'
+// import en from 'element-plus/es/locale/lang/en'
+initDom()
+/**
+ * @description 导入加载进度条,防止首屏加载时间过长,用户等待
+ *
+ * */
+import Nprogress from 'nprogress'
+import 'nprogress/nprogress.css'
+Nprogress.configure({ showSpinner: false, ease: 'ease', speed: 500 })
+Nprogress.start()
+
+/**
+ * 无需在这块结束,会在路由中间件中结束此块内容
+ * */
+
+const app = createApp(App)
+app.config.productionTip = false
+app.config.globalProperties.$t = t
+app.config.globalProperties.$setLang = setLang
+app.config.globalProperties.$locale = locale
+app
+  .use(run)
+  .use(store)
+  .use(auth)
+  .use(router)
+  .mount('#app')
+
+export default app

+ 122 - 0
frontend/src/permission.js

@@ -0,0 +1,122 @@
+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'
+
+const whiteList = ['Login', 'Init']
+
+const getRouter = async(userStore) => {
+  const routerStore = useRouterStore()
+  await routerStore.SetAsyncRouter()
+  await userStore.GetUserInfo()
+  const asyncRouters = routerStore.asyncRouters
+  asyncRouters.forEach(asyncRouter => {
+    router.addRoute(asyncRouter)
+  })
+}
+
+async function handleKeepAlive(to) {
+  if (to.matched.some(item => item.meta.keepAlive)) {
+    if (to.matched && 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)
+        }
+        // 如果没有按需加载完成则等待加载
+        if (typeof element.components.default === 'function') {
+          await element.components.default()
+          await handleKeepAlive(to)
+        }
+      }
+    }
+  }
+}
+
+router.beforeEach(async(to, from) => {
+  const routerStore = useRouterStore()
+  Nprogress.start()
+  const userStore = useUserStore()
+  to.meta.matched = [...to.matched]
+  handleKeepAlive(to)
+  const token = userStore.token
+  // 在白名单中的判断情况
+  document.title = getPageTitle(to.meta.title, to)
+  if(to.meta.client) {
+    return true
+  }
+  if (whiteList.indexOf(to.name) > -1) {
+    if (token) {
+      if (!routerStore.asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
+        await getRouter(userStore)
+      }
+      // token 可以解析但是却是不存在的用户 id 或角色 id 会导致无限调用
+      if (userStore.userInfo?.authority?.defaultRouter != null) {
+        if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
+          return { name: userStore.userInfo.authority.defaultRouter }
+        } else {
+          return { path: '/layout/404' }
+        }
+      } else {
+        // 强制退出账号
+        userStore.ClearStorage()
+        return {
+          name: 'Login',
+          query: {
+            redirect: document.location.hash
+          }
+        }
+      }
+    } else {
+      return true
+    }
+  } else {
+    // 不在白名单中并且已经登录的时候
+    if (token) {
+      // 添加flag防止多次获取动态路由和栈溢出
+      if (!routerStore.asyncRouterFlag && whiteList.indexOf(from.name) < 0) {
+        await getRouter(userStore)
+        if (userStore.token) {
+          if (router.hasRoute(userStore.userInfo.authority.defaultRouter)) {
+            return { ...to, replace: true }
+          } else {
+            return { path: '/layout/404' }
+          }
+        } else {
+          return {
+            name: 'Login',
+            query: { redirect: to.href }
+          }
+        }
+      } else {
+        if (to.matched.length) {
+          return true
+        } else {
+          return { path: '/layout/404' }
+        }
+      }
+    }
+    // 不在白名单中并且未登录的时候
+    if (!token) {
+      return {
+        name: 'Login',
+        query: {
+          redirect: document.location.hash
+        }
+      }
+    }
+  }
+})
+
+router.afterEach(() => {
+  // 路由加载完成后关闭进度条
+  document.getElementsByClassName('main-cont main-right')[0]?.scrollTo(0, 0)
+  Nprogress.done()
+})
+
+router.onError(() => {
+  // 路由发生错误后销毁进度条
+  Nprogress.remove()
+})

+ 7 - 0
frontend/src/pinia/index.js

@@ -0,0 +1,7 @@
+import { createPinia } from 'pinia'
+
+const store = createPinia()
+
+export {
+  store
+}

+ 40 - 0
frontend/src/pinia/modules/dictionary.js

@@ -0,0 +1,40 @@
+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
+  }
+})

+ 103 - 0
frontend/src/pinia/modules/router.js

@@ -0,0 +1,103 @@
+import { asyncRouterHandle } from '@/utils/asyncRouter'
+import { emitter } from '@/utils/bus.js'
+import { asyncMenu } from '@/api/menu'
+import { defineStore } from 'pinia'
+import { ref } from 'vue'
+
+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)) {
+      item.component && item.component().then(val => {
+        keepAliveRoutersArr.push(val.default.name)
+        nameMap[item.name] = val.default.name
+      })
+    }
+    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 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 {
+    asyncRouters,
+    keepAliveRouters,
+    asyncRouterFlag,
+    SetAsyncRouter,
+    routeMap
+  }
+})
+

+ 162 - 0
frontend/src/pinia/modules/user.js

@@ -0,0 +1,162 @@
+import { login, getUserInfo, setSelfInfo } from '@/api/user'
+import { jsonInBlacklist } from '@/api/jwt'
+import router from '@/router/index'
+import { ElLoading, ElMessage } from 'element-plus'
+import { defineStore } from 'pinia'
+import { ref, computed, watch } from 'vue'
+import { useRouterStore } from './router'
+import cookie from 'js-cookie'
+
+export const useUserStore = defineStore('user', () => {
+  const loadingInstance = ref(null)
+
+  const userInfo = ref({
+    uuid: '',
+    nickName: '',
+    headerImg: '',
+    authority: {},
+    sideMode: 'dark',
+    activeColor: 'var(--el-color-primary)',
+    baseColor: '#fff'
+  })
+  const token = ref(window.localStorage.getItem('token') || cookie.get('x-token') || '')
+  const setUserInfo = (val) => {
+    userInfo.value = val
+  }
+
+  const setToken = (val) => {
+    token.value = val
+  }
+
+  const NeedInit = () => {
+    token.value = ''
+    window.localStorage.removeItem('token')
+    router.push({ name: 'Init', replace: true })
+  }
+
+  const ResetUserInfo = (value = {}) => {
+    userInfo.value = {
+      ...userInfo.value,
+      ...value
+    }
+  }
+  /* 获取用户信息*/
+  const GetUserInfo = async() => {
+    const res = await getUserInfo()
+    if (res.code === 0) {
+      setUserInfo(res.data.userInfo)
+    }
+    return res
+  }
+  /* 登录*/
+  const LoginIn = async(loginInfo) => {
+    loadingInstance.value = ElLoading.service({
+      fullscreen: true,
+      text: '登录中,请稍候...',
+    })
+    try {
+      const res = await login(loginInfo)
+      if (res.code === 0) {
+        setUserInfo(res.data.user)
+        setToken(res.data.token)
+        const routerStore = useRouterStore()
+        await routerStore.SetAsyncRouter()
+        const asyncRouters = routerStore.asyncRouters
+        asyncRouters.forEach(asyncRouter => {
+          router.addRoute(asyncRouter)
+        })
+
+        if (!router.hasRoute(userInfo.value.authority.defaultRouter)) {
+          ElMessage.error('请联系管理员进行授权')
+        } else {
+          await router.replace({ name: userInfo.value.authority.defaultRouter })
+        }
+
+        loadingInstance.value.close()
+
+        const isWin = ref(/windows/i.test(navigator.userAgent))
+        if (isWin.value) {
+          window.localStorage.setItem('osType', 'WIN')
+        } else {
+          window.localStorage.setItem('osType', 'MAC')
+        }
+        return true
+      }
+    } catch (e) {
+      loadingInstance.value.close()
+    }
+    loadingInstance.value.close()
+  }
+  /* 登出*/
+  const LoginOut = async() => {
+    const res = await jsonInBlacklist()
+    if (res.code === 0) {
+      await ClearStorage()
+      router.push({ name: 'Login', replace: true })
+      window.location.reload()
+    }
+  }
+  /* 清理数据 */
+  const ClearStorage = async() => {
+    token.value = ''
+    sessionStorage.clear()
+    window.localStorage.removeItem('token')
+    cookie.remove('x-token')
+  }
+  /* 设置侧边栏模式*/
+  const changeSideMode = async(data) => {
+    const res = await setSelfInfo({ sideMode: data })
+    if (res.code === 0) {
+      userInfo.value.sideMode = data
+      ElMessage({
+        type: 'success',
+        message: '设置成功'
+      })
+    }
+  }
+
+  const mode = computed(() => userInfo.value.sideMode)
+  const sideMode = computed(() => {
+    if (userInfo.value.sideMode === 'dark') {
+      return '#191a23'
+    } else if (userInfo.value.sideMode === 'light') {
+      return '#fff'
+    } else {
+      return userInfo.value.sideMode
+    }
+  })
+  const baseColor = computed(() => {
+    if (userInfo.value.sideMode === 'dark') {
+      return '#fff'
+    } else if (userInfo.value.sideMode === 'light') {
+      return '#191a23'
+    } else {
+      return userInfo.value.baseColor
+    }
+  })
+  const activeColor = computed(() => {
+    return 'var(--el-color-primary)'
+  })
+
+  watch(() => token.value, () => {
+    window.localStorage.setItem('token', token.value)
+  })
+
+  return {
+    userInfo,
+    token,
+    NeedInit,
+    ResetUserInfo,
+    GetUserInfo,
+    LoginIn,
+    LoginOut,
+    changeSideMode,
+    mode,
+    sideMode,
+    setToken,
+    baseColor,
+    activeColor,
+    loadingInstance,
+    ClearStorage
+  }
+})

+ 0 - 0
frontend/src/plugin/email/api/email.js


Некоторые файлы не были показаны из-за большого количества измененных файлов