jiangbiao il y a 2 mois
Parent
commit
61c62a47c4
100 fichiers modifiés avec 4804 ajouts et 5764 suppressions
  1. 1 1
      canteen-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java
  2. 0 22
      canteen-ui/.editorconfig
  3. 3 6
      canteen-ui/.env.development
  4. 6 3
      canteen-ui/.env.production
  5. 7 8
      canteen-ui/.env.staging
  6. 0 10
      canteen-ui/.eslintignore
  7. 0 199
      canteen-ui/.eslintrc.js
  8. 20 0
      canteen-ui/LICENSE
  9. 93 16
      canteen-ui/README.md
  10. 0 13
      canteen-ui/babel.config.js
  11. 1 1
      canteen-ui/bin/build.bat
  12. 1 1
      canteen-ui/bin/package.bat
  13. 2 2
      canteen-ui/bin/run-web.bat
  14. 0 35
      canteen-ui/build/index.js
  15. 0 0
      canteen-ui/html/ie.html
  16. 28 21
      canteen-ui/index.html
  17. 30 72
      canteen-ui/package.json
  18. 0 2
      canteen-ui/public/robots.txt
  19. 0 0
      canteen-ui/public/styles/theme-chalk/index.css
  20. 10 23
      canteen-ui/src/App.vue
  21. 1 1
      canteen-ui/src/api/system/dict/type.js
  22. 0 9
      canteen-ui/src/assets/icons/index.js
  23. 1 0
      canteen-ui/src/assets/icons/svg/moon.svg
  24. 1 0
      canteen-ui/src/assets/icons/svg/sunny.svg
  25. 0 22
      canteen-ui/src/assets/icons/svgo.yml
  26. BIN
      canteen-ui/src/assets/images/profile.jpg
  27. BIN
      canteen-ui/src/assets/logo/logo.png
  28. 1 1
      canteen-ui/src/assets/styles/btn.scss
  29. 4 0
      canteen-ui/src/assets/styles/element-ui.scss
  30. 0 31
      canteen-ui/src/assets/styles/element-variables.scss
  31. 3 1
      canteen-ui/src/assets/styles/index.scss
  32. 59 56
      canteen-ui/src/assets/styles/ruoyi.scss
  33. 30 21
      canteen-ui/src/assets/styles/sidebar.scss
  34. 221 0
      canteen-ui/src/assets/styles/variables.module.scss
  35. 0 54
      canteen-ui/src/assets/styles/variables.scss
  36. 72 77
      canteen-ui/src/components/Breadcrumb/index.vue
  37. 164 151
      canteen-ui/src/components/Crontab/day.vue
  38. 125 112
      canteen-ui/src/components/Crontab/hour.vue
  39. 269 390
      canteen-ui/src/components/Crontab/index.vue
  40. 119 109
      canteen-ui/src/components/Crontab/min.vue
  41. 133 106
      canteen-ui/src/components/Crontab/month.vue
  42. 527 546
      canteen-ui/src/components/Crontab/result.vue
  43. 120 109
      canteen-ui/src/components/Crontab/second.vue
  44. 186 191
      canteen-ui/src/components/Crontab/week.vue
  45. 139 121
      canteen-ui/src/components/Crontab/year.vue
  46. 0 49
      canteen-ui/src/components/DictData/index.js
  47. 54 61
      canteen-ui/src/components/DictTag/index.vue
  48. 148 171
      canteen-ui/src/components/Editor/index.vue
  49. 155 164
      canteen-ui/src/components/FileUpload/index.vue
  50. 11 13
      canteen-ui/src/components/Hamburger/index.vue
  51. 122 131
      canteen-ui/src/components/HeaderSearch/index.vue
  52. 42 35
      canteen-ui/src/components/IconSelect/index.vue
  53. 8 11
      canteen-ui/src/components/IconSelect/requireIcons.js
  54. 55 53
      canteen-ui/src/components/ImagePreview/index.vue
  55. 164 179
      canteen-ui/src/components/ImageUpload/index.vue
  56. 74 83
      canteen-ui/src/components/Pagination/index.vue
  57. 0 142
      canteen-ui/src/components/PanThumb/index.vue
  58. 0 106
      canteen-ui/src/components/RightPanel/index.vue
  59. 100 95
      canteen-ui/src/components/RightToolbar/index.vue
  60. 5 13
      canteen-ui/src/components/RuoYi/Doc/index.vue
  61. 6 14
      canteen-ui/src/components/RuoYi/Git/index.vue
  62. 7 42
      canteen-ui/src/components/Screenfull/index.vue
  63. 38 49
      canteen-ui/src/components/SizeSelect/index.vue
  64. 27 35
      canteen-ui/src/components/SvgIcon/index.vue
  65. 10 0
      canteen-ui/src/components/SvgIcon/svgicon.js
  66. 0 170
      canteen-ui/src/components/ThemePicker/index.vue
  67. 157 134
      canteen-ui/src/components/TopNav/index.vue
  68. 25 30
      canteen-ui/src/components/iFrame/index.vue
  69. 66 0
      canteen-ui/src/directive/common/copyText.js
  70. 0 64
      canteen-ui/src/directive/dialog/drag.js
  71. 0 34
      canteen-ui/src/directive/dialog/dragHeight.js
  72. 0 30
      canteen-ui/src/directive/dialog/dragWidth.js
  73. 6 20
      canteen-ui/src/directive/index.js
  74. 0 54
      canteen-ui/src/directive/module/clipboard.js
  75. 4 4
      canteen-ui/src/directive/permission/hasPermi.js
  76. 5 5
      canteen-ui/src/directive/permission/hasRole.js
  77. 24 32
      canteen-ui/src/layout/components/AppMain.vue
  78. 17 25
      canteen-ui/src/layout/components/IframeToggle/index.vue
  79. 11 34
      canteen-ui/src/layout/components/InnerLink/index.vue
  80. 103 88
      canteen-ui/src/layout/components/Navbar.vue
  81. 175 231
      canteen-ui/src/layout/components/Settings/index.vue
  82. 0 25
      canteen-ui/src/layout/components/Sidebar/FixiOSBug.js
  83. 0 33
      canteen-ui/src/layout/components/Sidebar/Item.vue
  84. 29 32
      canteen-ui/src/layout/components/Sidebar/Link.vue
  85. 37 31
      canteen-ui/src/layout/components/Sidebar/Logo.vue
  86. 69 68
      canteen-ui/src/layout/components/Sidebar/SidebarItem.vue
  87. 100 53
      canteen-ui/src/layout/components/Sidebar/index.vue
  88. 86 73
      canteen-ui/src/layout/components/TagsView/ScrollPane.vue
  89. 255 218
      canteen-ui/src/layout/components/TagsView/index.vue
  90. 0 1
      canteen-ui/src/layout/components/index.js
  91. 95 92
      canteen-ui/src/layout/index.vue
  92. 0 45
      canteen-ui/src/layout/mixin/ResizeHandler.js
  93. 50 54
      canteen-ui/src/main.js
  94. 19 13
      canteen-ui/src/permission.js
  95. 3 3
      canteen-ui/src/plugins/auth.js
  96. 7 7
      canteen-ui/src/plugins/download.js
  97. 11 13
      canteen-ui/src/plugins/index.js
  98. 16 17
      canteen-ui/src/plugins/modal.js
  99. 15 17
      canteen-ui/src/plugins/tab.js
  100. 16 25
      canteen-ui/src/router/index.js

+ 1 - 1
canteen-generator/src/main/java/com/ruoyi/generator/util/VelocityUtils.java

@@ -129,7 +129,7 @@ public class VelocityUtils
      */
     public static List<String> getTemplateList(String tplCategory, String tplWebType)
     {
-        String useWebType = "vm/vue";
+        String useWebType = "vm/vue/v3";
         if ("element-plus".equals(tplWebType))
         {
             useWebType = "vm/vue/v3";

+ 0 - 22
canteen-ui/.editorconfig

@@ -1,22 +0,0 @@
-# 告诉EditorConfig插件,这是根文件,不用继续往上查找
-root = true
-
-# 匹配全部文件
-[*]
-# 设置字符集
-charset = utf-8
-# 缩进风格,可选space、tab
-indent_style = space
-# 缩进的空格数
-indent_size = 2
-# 结尾换行符,可选lf、cr、crlf
-end_of_line = lf
-# 在文件结尾插入新行
-insert_final_newline = true
-# 删除一行中的前后空格
-trim_trailing_whitespace = true
-
-# 匹配md结尾的文件
-[*.md]
-insert_final_newline = false
-trim_trailing_whitespace = false

+ 3 - 6
canteen-ui/.env.development

@@ -1,11 +1,8 @@
 # 页面标题
-VUE_APP_TITLE = 食堂管理系统
+VITE_APP_TITLE = 就餐管理系统
 
 # 开发环境配置
-ENV = 'development'
+VITE_APP_ENV = 'development'
 
 # 若依管理系统/开发环境
-VUE_APP_BASE_API = '/dev-api'
-
-# 路由懒加载
-VUE_CLI_BABEL_TRANSPILE_MODULES = true
+VITE_APP_BASE_API = '/dev-api'

+ 6 - 3
canteen-ui/.env.production

@@ -1,8 +1,11 @@
 # 页面标题
-VUE_APP_TITLE = 食堂管理系统
+VITE_APP_TITLE = 就餐管理系统
 
 # 生产环境配置
-ENV = 'production'
+VITE_APP_ENV = 'production'
 
 # 若依管理系统/生产环境
-VUE_APP_BASE_API = '/prod-api'
+VITE_APP_BASE_API = '/prod-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip

+ 7 - 8
canteen-ui/.env.staging

@@ -1,12 +1,11 @@
 # 页面标题
-VUE_APP_TITLE = 食堂管理系统
+VITE_APP_TITLE = 就餐管理系统
 
-BABEL_ENV = production
+# 生产环境配置
+VITE_APP_ENV = 'staging'
 
-NODE_ENV = production
+# 若依管理系统/生产环境
+VITE_APP_BASE_API = '/stage-api'
 
-# 测试环境配置
-ENV = 'staging'
-
-# 若依管理系统/测试环境
-VUE_APP_BASE_API = '/stage-api'
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip

+ 0 - 10
canteen-ui/.eslintignore

@@ -1,10 +0,0 @@
-# 忽略build目录下类型为js的文件的语法检查
-build/*.js
-# 忽略src/assets目录下文件的语法检查
-src/assets
-# 忽略public目录下文件的语法检查
-public
-# 忽略当前目录下为js的文件的语法检查
-*.js
-# 忽略当前目录下为vue的文件的语法检查
-*.vue

+ 0 - 199
canteen-ui/.eslintrc.js

@@ -1,199 +0,0 @@
-// ESlint 检查配置
-module.exports = {
-  root: true,
-  parserOptions: {
-    parser: 'babel-eslint',
-    sourceType: 'module'
-  },
-  env: {
-    browser: true,
-    node: true,
-    es6: true,
-  },
-  extends: ['plugin:vue/recommended', 'eslint:recommended'],
-
-  // add your custom rules here
-  //it is base on https://github.com/vuejs/eslint-config-vue
-  rules: {
-    "vue/max-attributes-per-line": [2, {
-      "singleline": 10,
-      "multiline": {
-        "max": 1,
-        "allowFirstLine": false
-      }
-    }],
-    "vue/singleline-html-element-content-newline": "off",
-    "vue/multiline-html-element-content-newline":"off",
-    "vue/name-property-casing": ["error", "PascalCase"],
-    "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, 'never'],
-    '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': 2,
-    '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']
-  }
-}

+ 20 - 0
canteen-ui/LICENSE

@@ -0,0 +1,20 @@
+The MIT License (MIT)
+
+Copyright (c) 2018 RuoYi
+
+Permission is hereby granted, free of charge, to any person obtaining a copy of
+this software and associated documentation files (the "Software"), to deal in
+the Software without restriction, including without limitation the rights to
+use, copy, modify, merge, publish, distribute, sublicense, and/or sell copies of
+the Software, and to permit persons to whom the Software is furnished to do so,
+subject to the following conditions:
+
+The above copyright notice and this permission notice shall be included in all
+copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
+IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, FITNESS
+FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR
+COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER
+IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN
+CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.

Fichier diff supprimé car celui-ci est trop grand
+ 93 - 16
canteen-ui/README.md


+ 0 - 13
canteen-ui/babel.config.js

@@ -1,13 +0,0 @@
-module.exports = {
-  presets: [
-    // https://github.com/vuejs/vue-cli/tree/master/packages/@vue/babel-preset-app
-    '@vue/cli-plugin-babel/preset'
-  ],
-  'env': {
-    'development': {
-      // babel-plugin-dynamic-import-node plugin only does one thing by converting all import() to require().
-      // This plugin can significantly increase the speed of hot updates, when you have a large number of pages.
-      'plugins': ['dynamic-import-node']
-    }
-  }
-}

+ 1 - 1
canteen-ui/bin/build.bat

@@ -7,6 +7,6 @@ echo.
 cd %~dp0
 
 cd ..
-npm run build:prod
+yarn build:prod
 
 pause

+ 1 - 1
canteen-ui/bin/package.bat

@@ -7,6 +7,6 @@ echo.
 cd %~dp0
 
 cd ..
-npm install --registry=https://registry.npmmirror.com
+yarn --registry=https://registry.npmmirror.com
 
 pause

+ 2 - 2
canteen-ui/bin/run-web.bat

@@ -1,12 +1,12 @@
 @echo off
 echo.
-echo [信息] 使用 Vue CLI 命令运行 Web 工程。
+echo [信息] 使用 Vite 命令运行 Web 工程。
 echo.
 
 %~d0
 cd %~dp0
 
 cd ..
-npm run dev
+yarn dev
 
 pause

+ 0 - 35
canteen-ui/build/index.js

@@ -1,35 +0,0 @@
-const { run } = require('runjs')
-const chalk = require('chalk')
-const config = require('../vue.config.js')
-const rawArgv = process.argv.slice(2)
-const args = rawArgv.join(' ')
-
-if (process.env.npm_config_preview || rawArgv.includes('--preview')) {
-  const report = rawArgv.includes('--report')
-
-  run(`vue-cli-service build ${args}`)
-
-  const port = 9526
-  const publicPath = config.publicPath
-
-  var connect = require('connect')
-  var serveStatic = require('serve-static')
-  const app = connect()
-
-  app.use(
-    publicPath,
-    serveStatic('./dist', {
-      index: ['index.html', '/']
-    })
-  )
-
-  app.listen(port, function () {
-    console.log(chalk.green(`> Preview at  http://localhost:${port}${publicPath}`))
-    if (report) {
-      console.log(chalk.green(`> Report at  http://localhost:${port}${publicPath}report.html`))
-    }
-
-  })
-} else {
-  run(`vue-cli-service build ${args}`)
-}

+ 0 - 0
canteen-ui/public/html/ie.html → canteen-ui/html/ie.html


+ 28 - 21
canteen-ui/public/index.html → canteen-ui/index.html

@@ -1,14 +1,15 @@
 <!DOCTYPE html>
 <html>
-  <head>
-    <meta charset="utf-8">
-    <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
-    <meta name="renderer" content="webkit">
-    <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
-    <link rel="icon" href="<%= BASE_URL %>favicon.ico">
-    <title><%= webpackConfig.name %></title>
-    <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
-	  <style>
+
+<head>
+  <meta charset="utf-8">
+  <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
+  <meta name="renderer" content="webkit">
+  <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no">
+  <link rel="icon" href="/favicon.ico">
+  <title>就餐管理系统</title>
+  <!--[if lt IE 11]><script>window.location.href='/html/ie.html';</script><![endif]-->
+  <style>
     html,
     body,
     #app {
@@ -16,6 +17,7 @@
       margin: 0px;
       padding: 0px;
     }
+
     .chromeframe {
       margin: 0.2em 0;
       background: #ccc;
@@ -92,6 +94,7 @@
         -ms-transform: rotate(0deg);
         transform: rotate(0deg);
       }
+
       100% {
         -webkit-transform: rotate(360deg);
         -ms-transform: rotate(360deg);
@@ -105,6 +108,7 @@
         -ms-transform: rotate(0deg);
         transform: rotate(0deg);
       }
+
       100% {
         -webkit-transform: rotate(360deg);
         -ms-transform: rotate(360deg);
@@ -194,15 +198,18 @@
       opacity: 0.5;
     }
   </style>
-  </head>
-  <body>
-    <div id="app">
-	    <div id="loader-wrapper">
-		    <div id="loader"></div>
-		    <div class="loader-section section-left"></div>
-		    <div class="loader-section section-right"></div>
-		    <div class="load_title">正在加载系统资源,请耐心等待</div>
-        </div>
-	</div>
-  </body>
-</html>
+</head>
+
+<body>
+  <div id="app">
+    <div id="loader-wrapper">
+      <div id="loader"></div>
+      <div class="loader-section section-left"></div>
+      <div class="loader-section section-right"></div>
+      <div class="load_title">正在加载系统资源,请耐心等待</div>
+    </div>
+  </div>
+  <script type="module" src="/src/main.js"></script>
+</body>
+
+</html>

+ 30 - 72
canteen-ui/package.json

@@ -1,90 +1,48 @@
 {
   "name": "ruoyi",
   "version": "3.8.9",
-  "description": "食堂管理系统",
+  "description": "若依管理系统",
   "author": "若依",
   "license": "MIT",
+  "type": "module",
   "scripts": {
-    "dev": "vue-cli-service serve",
-    "build:prod": "vue-cli-service build",
-    "build:stage": "vue-cli-service build --mode staging",
-    "preview": "node build/index.js --preview",
-    "lint": "eslint --ext .js,.vue src"
+    "dev": "vite",
+    "build:prod": "vite build",
+    "build:stage": "vite build --mode staging",
+    "preview": "vite preview"
   },
-  "husky": {
-    "hooks": {
-      "pre-commit": "lint-staged"
-    }
-  },
-  "lint-staged": {
-    "src/**/*.{js,vue}": [
-      "eslint --fix",
-      "git add"
-    ]
-  },
-  "keywords": [
-    "vue",
-    "admin",
-    "dashboard",
-    "element-ui",
-    "boilerplate",
-    "admin-template",
-    "management-system"
-  ],
   "repository": {
     "type": "git",
     "url": "https://gitee.com/y_project/RuoYi-Vue.git"
   },
   "dependencies": {
-    "@riophae/vue-treeselect": "0.4.0",
+    "@element-plus/icons-vue": "2.3.1",
+    "@vueup/vue-quill": "1.2.0",
+    "@vueuse/core": "10.11.0",
     "axios": "0.28.1",
-    "clipboard": "2.0.8",
-    "core-js": "3.37.1",
-    "echarts": "5.4.0",
-    "element-ui": "2.15.14",
+    "clipboard": "2.0.11",
+    "echarts": "5.5.1",
+    "element-plus": "2.7.6",
     "file-saver": "2.0.5",
-    "fuse.js": "6.4.3",
-    "highlight.js": "9.18.5",
-    "js-beautify": "1.13.0",
-    "js-cookie": "3.0.1",
-    "jsencrypt": "3.0.0-rc.1",
+    "fuse.js": "6.6.2",
+    "js-beautify": "1.14.11",
+    "js-cookie": "3.0.5",
+    "jsencrypt": "3.3.2",
     "nprogress": "0.2.0",
-    "quill": "2.0.2",
-    "screenfull": "5.0.2",
-    "sortablejs": "1.10.2",
-    "splitpanes": "2.4.1",
-    "vue": "2.6.12",
-    "vue-count-to": "1.0.13",
-    "vue-cropper": "0.5.5",
-    "vue-meta": "2.4.0",
-    "vue-router": "3.4.9",
-    "vuedraggable": "2.24.3",
-    "vuex": "3.6.0"
+    "pinia": "2.1.7",
+    "splitpanes": "3.1.5",
+    "vue": "3.4.31",
+    "vue-cropper": "1.1.1",
+    "vue-router": "4.4.0",
+    "vuedraggable": "4.1.0"
   },
   "devDependencies": {
-    "@vue/cli-plugin-babel": "4.4.6",
-    "@vue/cli-plugin-eslint": "4.4.6",
-    "@vue/cli-service": "4.4.6",
-    "babel-eslint": "10.1.0",
-    "babel-plugin-dynamic-import-node": "2.3.3",
-    "chalk": "4.1.0",
-    "compression-webpack-plugin": "6.1.2",
-    "connect": "3.6.6",
-    "eslint": "7.15.0",
-    "eslint-plugin-vue": "7.2.0",
-    "lint-staged": "10.5.3",
-    "sass": "1.32.13",
-    "sass-loader": "10.1.1",
-    "script-ext-html-webpack-plugin": "2.1.5",
-    "svg-sprite-loader": "5.1.1",
-    "vue-template-compiler": "2.6.12"
-  },
-  "engines": {
-    "node": ">=8.9",
-    "npm": ">= 3.0.0"
-  },
-  "browserslist": [
-    "> 1%",
-    "last 2 versions"
-  ]
+    "@vitejs/plugin-vue": "5.0.5",
+    "sass": "1.77.5",
+    "unplugin-auto-import": "0.17.6",
+    "unplugin-vue-setup-extend-plus": "1.0.1",
+    "vite": "5.3.2",
+    "vite-plugin-compression": "0.5.1",
+    "vite-plugin-svg-icons": "2.0.1"
+  }
 }

+ 0 - 2
canteen-ui/public/robots.txt

@@ -1,2 +0,0 @@
-User-agent: *
-Disallow: /

Fichier diff supprimé car celui-ci est trop grand
+ 0 - 0
canteen-ui/public/styles/theme-chalk/index.css


+ 10 - 23
canteen-ui/src/App.vue

@@ -1,28 +1,15 @@
 <template>
-  <div id="app">
-    <router-view />
-    <theme-picker />
-  </div>
+  <router-view />
 </template>
 
-<script>
-import ThemePicker from "@/components/ThemePicker";
+<script setup>
+import useSettingsStore from '@/store/modules/settings'
+import { handleThemeStyle } from '@/utils/theme'
 
-export default {
-  name: "App",
-  components: { ThemePicker },
-  metaInfo() {
-    return {
-      title: this.$store.state.settings.dynamicTitle && this.$store.state.settings.title,
-      titleTemplate: title => {
-        return title ? `${title} - ${process.env.VUE_APP_TITLE}` : process.env.VUE_APP_TITLE
-      }
-    }
-  }
-};
+onMounted(() => {
+  nextTick(() => {
+    // 初始化主题样式
+    handleThemeStyle(useSettingsStore().theme)
+  })
+})
 </script>
-<style scoped>
-#app .theme-picker {
-  display: none;
-}
-</style>

+ 1 - 1
canteen-ui/src/api/system/dict/type.js

@@ -57,4 +57,4 @@ export function optionselect() {
     url: '/system/dict/type/optionselect',
     method: 'get'
   })
-}
+}

+ 0 - 9
canteen-ui/src/assets/icons/index.js

@@ -1,9 +0,0 @@
-import Vue from 'vue'
-import SvgIcon from '@/components/SvgIcon'// svg component
-
-// register globally
-Vue.component('svg-icon', SvgIcon)
-
-const req = require.context('./svg', false, /\.svg$/)
-const requireAll = requireContext => requireContext.keys().map(requireContext)
-requireAll(req)

+ 1 - 0
canteen-ui/src/assets/icons/svg/moon.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303018722" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="1447" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M368.832 67.2c51.328-16.384 89.216 34.112 75.712 76.416a346.816 346.816 0 0 0 435.84 435.84c42.304-13.44 92.8 24.384 76.48 75.712A467.968 467.968 0 1 1 368.832 67.2z m-35.776 122.688a368.832 368.832 0 1 0 501.056 501.056 445.952 445.952 0 0 1-501.056-501.056z" p-id="1448"></path></svg>

+ 1 - 0
canteen-ui/src/assets/icons/svg/sunny.svg

@@ -0,0 +1 @@
+<?xml version="1.0" standalone="no"?><!DOCTYPE svg PUBLIC "-//W3C//DTD SVG 1.1//EN" "http://www.w3.org/Graphics/SVG/1.1/DTD/svg11.dtd"><svg t="1733303115132" class="icon" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" p-id="12397" xmlns:xlink="http://www.w3.org/1999/xlink" width="200" height="200"><path d="M512 890.432c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 0 1-66.816 0v-66.752c0-18.432 14.976-33.408 33.408-33.408z m-267.52-110.848a33.408 33.408 0 0 1 0 47.232l-47.296 47.232a33.408 33.408 0 0 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0z m582.336 0l47.232 47.232a33.408 33.408 0 0 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 1 1 47.232-47.232zM512 200.32a311.68 311.68 0 1 1 0 623.296 311.68 311.68 0 0 1 0-623.36z m0 66.752a244.864 244.864 0 1 0 0 489.728 244.864 244.864 0 0 0 0-489.728zM100.16 478.592a33.408 33.408 0 1 1 0 66.816H33.408a33.408 33.408 0 0 1 0-66.816h66.752z m890.432 0a33.408 33.408 0 0 1 0 66.816h-66.752a33.408 33.408 0 1 1 0-66.816h66.752zM197.184 149.952l47.232 47.232a33.408 33.408 0 1 1-47.232 47.232l-47.232-47.232a33.408 33.408 0 0 1 47.232-47.232z m676.864 0a33.408 33.408 0 0 1 0 47.232l-47.232 47.232a33.408 33.408 0 1 1-47.232-47.232l47.232-47.232a33.408 33.408 0 0 1 47.232 0zM512 0c18.432 0 33.408 14.976 33.408 33.408v66.752a33.408 33.408 0 1 1-66.816 0V33.408C478.592 14.976 493.568 0 512 0z" p-id="12398"></path></svg>

+ 0 - 22
canteen-ui/src/assets/icons/svgo.yml

@@ -1,22 +0,0 @@
-# replace default config
-
-# multipass: true
-# full: true
-
-plugins:
-
-  # - name
-  #
-  # or:
-  # - name: false
-  # - name: true
-  #
-  # or:
-  # - name:
-  #     param1: 1
-  #     param2: 2
-
-- removeAttrs:
-    attrs:
-      - 'fill'
-      - 'fill-rule'

BIN
canteen-ui/src/assets/images/profile.jpg


BIN
canteen-ui/src/assets/logo/logo.png


+ 1 - 1
canteen-ui/src/assets/styles/btn.scss

@@ -1,4 +1,4 @@
-@import './variables.scss';
+@import './variables.module.scss';
 
 @mixin colorBtn($color) {
   background: $color;

+ 4 - 0
canteen-ui/src/assets/styles/element-ui.scss

@@ -89,4 +89,8 @@
   > .el-submenu__title
   .el-submenu__icon-arrow {
   display: none;
+}
+
+.el-dropdown .el-dropdown-link{
+  color: var(--el-color-primary) !important;
 }

+ 0 - 31
canteen-ui/src/assets/styles/element-variables.scss

@@ -1,31 +0,0 @@
-/**
-* I think element-ui's default theme color is too light for long-term use.
-* So I modified the default color and you can modify it to your liking.
-**/
-
-/* theme color */
-$--color-primary: #1890ff;
-$--color-success: #13ce66;
-$--color-warning: #ffba00;
-$--color-danger: #ff4949;
-// $--color-info: #1E1E1E;
-
-$--button-font-weight: 400;
-
-// $--color-text-regular: #1f2d3d;
-
-$--border-color-light: #dfe4ed;
-$--border-color-lighter: #e6ebf5;
-
-$--table-border: 1px solid #dfe6ec;
-
-/* icon font path, required */
-$--font-path: '~element-ui/lib/theme-chalk/fonts';
-
-@import "~element-ui/packages/theme-chalk/src/index";
-
-// the :export directive is the magic sauce for webpack
-// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
-:export {
-  theme: $--color-primary;
-}

+ 3 - 1
canteen-ui/src/assets/styles/index.scss

@@ -1,12 +1,14 @@
-@import './variables.scss';
+@import './variables.module.scss';
 @import './mixin.scss';
 @import './transition.scss';
 @import './element-ui.scss';
 @import './sidebar.scss';
 @import './btn.scss';
+@import './ruoyi.scss';
 
 body {
   height: 100%;
+  margin: 0;
   -moz-osx-font-smoothing: grayscale;
   -webkit-font-smoothing: antialiased;
   text-rendering: optimizeLegibility;

+ 59 - 56
canteen-ui/src/assets/styles/ruoyi.scss

@@ -1,69 +1,56 @@
 /**
-* 通用css样式布局处理
-* Copyright (c) 2019 ruoyi
-*/
+ * 通用css样式布局处理
+ * Copyright (c) 2019 ruoyi
+ */
 
 /** 基础通用 **/
 .pt5 {
   padding-top: 5px;
 }
-
 .pr5 {
   padding-right: 5px;
 }
-
 .pb5 {
   padding-bottom: 5px;
 }
-
 .mt5 {
   margin-top: 5px;
 }
-
 .mr5 {
   margin-right: 5px;
 }
-
 .mb5 {
   margin-bottom: 5px;
 }
-
 .mb8 {
   margin-bottom: 8px;
 }
-
 .ml5 {
   margin-left: 5px;
 }
-
 .mt10 {
   margin-top: 10px;
 }
-
 .mr10 {
   margin-right: 10px;
 }
-
 .mb10 {
   margin-bottom: 10px;
 }
 .ml10 {
-	margin-left: 10px;
+  margin-left: 10px;
 }
-
 .mt20 {
   margin-top: 20px;
 }
-
 .mr20 {
   margin-right: 20px;
 }
-
 .mb20 {
   margin-bottom: 20px;
 }
 .ml20 {
-	margin-left: 20px;
+  margin-left: 20px;
 }
 
 .h1, .h2, .h3, .h4, .h5, .h6, h1, h2, h3, h4, h5, h6 {
@@ -73,15 +60,14 @@
   color: inherit;
 }
 
-.el-message-box__status + .el-message-box__message{
-  word-break: break-word;
+.el-form .el-form-item__label {
+  font-weight: 700;
 }
-
 .el-dialog:not(.is-fullscreen) {
   margin-top: 6vh !important;
 }
 
-.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body {
+.el-dialog.scrollbar .el-dialog__body {
   overflow: auto;
   overflow-x: hidden;
   max-height: 70vh;
@@ -92,13 +78,12 @@
   .el-table__header-wrapper, .el-table__fixed-header-wrapper {
     th {
       word-break: break-word;
-      background-color: #f8f8f9;
+      background-color: #f8f8f9 !important;
       color: #515a6e;
-      height: 40px;
+      height: 40px !important;
       font-size: 13px;
     }
   }
-
   .el-table__body-wrapper {
     .el-button [class*="el-icon-"] + span {
       margin-left: 1px;
@@ -108,11 +93,11 @@
 
 /** 表单布局 **/
 .form-header {
-  font-size: 15px;
-  color: #6379bb;
-  border-bottom: 1px solid #ddd;
-  margin: 8px 10px 25px 10px;
-  padding-bottom: 5px
+  font-size:15px;
+  color:#6379bb;
+  border-bottom:1px solid #ddd;
+  margin:8px 10px 25px 10px;
+  padding-bottom:5px
 }
 
 /** 表格布局 **/
@@ -120,35 +105,54 @@
   display: flex;
   justify-content: flex-end;
   margin-top: 20px;
+  background-color: transparent !important;
 }
 
-/* tree border */
-.tree-border {
-  margin-top: 5px;
-  border: 1px solid #e5e6e7;
-  background: #FFFFFF none;
-  border-radius: 4px;
+/* 弹窗中的分页器 */
+.el-dialog .pagination-container {
+  position: static !important;
+  margin: 10px 0 0 0;
+  padding: 0 !important;
+  
+  .el-pagination {
+    position: static;
+  }
 }
 
+/* 移动端适配 */
 @media (max-width: 768px) {
-  .pagination-container .el-pagination > .el-pagination__jump {
-    display: none !important;
-  }
-  .pagination-container .el-pagination > .el-pagination__sizes {
-    display: none !important;
+  .pagination-container {
+    .el-pagination {
+      > .el-pagination__jump {
+        display: none !important;
+      }
+      > .el-pagination__sizes {
+        display: none !important;
+      }
+    }
   }
 }
 
-.el-table .fixed-width .el-button--mini {
+/* tree border */
+.tree-border {
+  margin-top: 5px;
+  border: 1px solid var(--el-border-color-light, #e5e6e7);
+  background: var(--el-bg-color, #FFFFFF) none;
+  border-radius:4px;
+  width: 100%;
+}
+
+.el-table .fixed-width .el-button--small {
   padding-left: 0;
   padding-right: 0;
   width: inherit;
 }
 
 /** 表格更多操作下拉样式 */
-.el-table .el-dropdown-link,.el-table .el-dropdown-selfdefine {
-	cursor: pointer;
-	margin-left: 5px;
+.el-table .el-dropdown-link {
+  cursor: pointer;
+  color: #409EFF;
+  margin-left: 10px;
 }
 
 .el-table .el-dropdown, .el-icon-arrow-down {
@@ -185,12 +189,12 @@
 }
 
 .el-card__header {
-  padding: 14px 15px 7px;
+  padding: 14px 15px 7px !important;
   min-height: 40px;
 }
 
 .el-card__body {
-  padding: 15px 20px 20px 20px;
+  padding: 15px 20px 20px 20px !important;
 }
 
 .card-box {
@@ -258,10 +262,9 @@
 }
 
 .avatar-upload-preview {
-  position: relative;
+  position: absolute;
   top: 50%;
-  left: 50%;
-  transform: translate(-50%, -50%);
+  transform: translate(50%, -50%);
   width: 200px;
   height: 200px;
   border-radius: 50%;
@@ -270,18 +273,18 @@
 }
 
 /* 拖拽列样式 */
-.sortable-ghost {
+.sortable-ghost{
   opacity: .8;
-  color: #fff !important;
-  background: #42b983 !important;
+  color: #fff!important;
+  background: #42b983!important;
 }
 
+/* 表格右侧工具栏样式 */
 .top-right-btn {
-  position: relative;
-  float: right;
+  margin-left: auto;
 }
 
 /* 分割面板样式 */
 .splitpanes.default-theme .splitpanes__pane {
-  background-color: #fff!important;
+  background-color: var(--splitpanes-default-bg) !important;
 }

+ 30 - 21
canteen-ui/src/assets/styles/sidebar.scss

@@ -1,7 +1,7 @@
 #app {
 
   .main-container {
-    height: 100%;
+    min-height: 100%;
     transition: margin-left .28s;
     margin-left: $base-sidebar-width;
     position: relative;
@@ -12,10 +12,8 @@
   }
 
   .sidebar-container {
-    -webkit-transition: width .28s;
     transition: width 0.28s;
     width: $base-sidebar-width !important;
-    background-color: $base-menu-background;
     height: 100%;
     position: fixed;
     font-size: 0px;
@@ -70,26 +68,30 @@
       width: 100% !important;
     }
 
-    .el-menu-item, .el-submenu__title {
+    .el-menu-item, .menu-title {
       overflow: hidden !important;
       text-overflow: ellipsis !important;
       white-space: nowrap !important;
     }
 
+    .el-menu-item .el-menu-tooltip__trigger {
+      display: inline-block !important;
+    }
+
     // menu hover
-    .submenu-title-noDropdown,
-    .el-submenu__title {
+    .sub-menu-title-noDropdown,
+    .el-sub-menu__title {
       &:hover {
         background-color: rgba(0, 0, 0, 0.06) !important;
       }
     }
 
-    & .theme-dark .is-active > .el-submenu__title {
+    & .theme-dark .is-active > .el-sub-menu__title {
       color: $base-menu-color-active !important;
     }
 
-    & .nest-menu .el-submenu>.el-submenu__title,
-    & .el-submenu .el-menu-item {
+    & .nest-menu .el-sub-menu>.el-sub-menu__title,
+    & .el-sub-menu .el-menu-item {
       min-width: $base-sidebar-width !important;
 
       &:hover {
@@ -97,9 +99,9 @@
       }
     }
 
-    & .theme-dark .nest-menu .el-submenu>.el-submenu__title,
-    & .theme-dark .el-submenu .el-menu-item {
-      background-color: $base-sub-menu-background !important;
+    & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
+    & .theme-dark .el-sub-menu .el-menu-item {
+      background-color: $base-sub-menu-background;
 
       &:hover {
         background-color: $base-sub-menu-hover !important;
@@ -116,7 +118,7 @@
       margin-left: 54px;
     }
 
-    .submenu-title-noDropdown {
+    .sub-menu-title-noDropdown {
       padding: 0 !important;
       position: relative;
 
@@ -129,10 +131,10 @@
       }
     }
 
-    .el-submenu {
+    .el-sub-menu {
       overflow: hidden;
 
-      &>.el-submenu__title {
+      &>.el-sub-menu__title {
         padding: 0 !important;
 
         .svg-icon {
@@ -143,8 +145,8 @@
     }
 
     .el-menu--collapse {
-      .el-submenu {
-        &>.el-submenu__title {
+      .el-sub-menu {
+        &>.el-sub-menu__title {
           &>span {
             height: 0;
             width: 0;
@@ -152,12 +154,19 @@
             visibility: hidden;
             display: inline-block;
           }
+          &>i {
+            height: 0;
+            width: 0;
+            overflow: hidden;
+            visibility: hidden;
+            display: inline-block;
+          }
         }
       }
     }
   }
 
-  .el-menu--collapse .el-menu .el-submenu {
+  .el-menu--collapse .el-menu .el-sub-menu {
     min-width: $base-sidebar-width !important;
   }
 
@@ -198,15 +207,15 @@
     }
   }
 
-  .nest-menu .el-submenu>.el-submenu__title,
+  .nest-menu .el-sub-menu>.el-sub-menu__title,
   .el-menu-item {
     &:hover {
-      // you can use $subMenuHover
+      // you can use $sub-menuHover
       background-color: rgba(0, 0, 0, 0.06) !important;
     }
   }
 
-  // the scroll bar appears when the subMenu is too long
+  // the scroll bar appears when the sub-menu is too long
   >.el-menu--popup {
     max-height: 100vh;
     overflow-y: auto;

+ 221 - 0
canteen-ui/src/assets/styles/variables.module.scss

@@ -0,0 +1,221 @@
+// base color
+$blue: #324157;
+$light-blue: #333c46;
+$red: #C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow: #FEC171;
+$panGreen: #30B08F;
+
+// 默认主题变量
+$menuText: #bfcbd9;
+$menuActiveText: #409eff;
+$menuBg: #304156;
+$menuHover: #263445;
+
+// 浅色主题theme-light
+$menuLightBg: #ffffff;
+$menuLightHover: #f0f1f5;
+$menuLightText: #303133;
+$menuLightActiveText: #409EFF;
+
+// 基础变量
+$base-sidebar-width: 200px;
+$sideBarWidth: 200px;
+
+// 菜单暗色变量
+$base-menu-color: #bfcbd9;
+$base-menu-color-active: #f4f4f5;
+$base-menu-background: #304156;
+$base-sub-menu-background: #1f2d3d;
+$base-sub-menu-hover: #001528;
+
+// 组件变量
+$--color-primary: #409EFF;
+$--color-success: #67C23A;
+$--color-warning: #E6A23C;
+$--color-danger: #F56C6C;
+$--color-info: #909399;
+
+:export {
+  menuText: $menuText;
+  menuActiveText: $menuActiveText;
+  menuBg: $menuBg;
+  menuHover: $menuHover;
+  menuLightBg: $menuLightBg;
+  menuLightHover: $menuLightHover;
+  menuLightText: $menuLightText;
+  menuLightActiveText: $menuLightActiveText;
+  sideBarWidth: $sideBarWidth;
+  // 导出基础颜色
+  blue: $blue;
+  lightBlue: $light-blue;
+  red: $red;
+  pink: $pink;
+  green: $green;
+  tiffany: $tiffany;
+  yellow: $yellow;
+  panGreen: $panGreen;
+  // 导出组件颜色
+  colorPrimary: $--color-primary;
+  colorSuccess: $--color-success;
+  colorWarning: $--color-warning;
+  colorDanger: $--color-danger;
+  colorInfo: $--color-info;
+}
+
+// CSS变量定义
+:root {
+  /* 亮色模式变量 */
+  --sidebar-bg: #{$menuBg};
+  --sidebar-text: #{$menuText};
+  --menu-hover: #{$menuHover};
+  
+  --navbar-bg: #ffffff;
+  --navbar-text: #303133;
+  
+  /* splitpanes default-theme 变量 */
+  --splitpanes-default-bg: #ffffff;
+
+}
+
+// 暗黑模式变量
+html.dark {
+  /* 默认通用 */
+  --el-bg-color: #141414;
+  --el-bg-color-overlay: #1d1e1f;
+  --el-text-color-primary: #ffffff;
+  --el-text-color-regular: #d0d0d0;
+  --el-border-color: #434343;
+  --el-border-color-light: #434343;
+
+  /* 侧边栏 */
+  --sidebar-bg: #141414;
+  --sidebar-text: #ffffff;
+  --menu-hover: #2d2d2d;
+  --menu-active-text: #{$menuActiveText};
+
+  /* 顶部导航栏 */
+  --navbar-bg: #141414;
+  --navbar-text: #ffffff;
+  --navbar-hover: #141414;
+
+  /* 标签栏 */
+  --tags-bg: #141414;
+  --tags-item-bg: #1d1e1f;
+  --tags-item-border: #303030;
+  --tags-item-text: #d0d0d0;
+  --tags-item-hover: #2d2d2d;
+  --tags-close-hover: #64666a;
+
+  /* splitpanes 组件暗黑模式变量 */
+  --splitpanes-bg: #141414;
+  --splitpanes-border: #303030;
+  --splitpanes-splitter-bg: #1d1e1f;
+  --splitpanes-splitter-hover-bg: #2d2d2d;
+
+  /* blockquote 暗黑模式变量 */
+  --blockquote-bg: #1d1e1f;
+  --blockquote-border: #303030;
+  --blockquote-text: #d0d0d0;
+  
+  /* Cron 时间表达式 模式变量 */
+  --cron-border: #303030;
+
+  /* splitpanes default-theme 暗黑模式变量 */
+  --splitpanes-default-bg: #141414;
+
+  /* 侧边栏菜单覆盖 */
+   .sidebar-container {
+    .el-menu-item, .menu-title {
+      color: var(--el-text-color-regular);
+    }
+    & .theme-dark .nest-menu .el-sub-menu>.el-sub-menu__title,
+    & .theme-dark .el-sub-menu .el-menu-item {
+      background-color: var(--el-bg-color) !important;
+    }
+  }
+
+  /* 顶部栏栏菜单覆盖 */
+  .el-menu--horizontal {
+    .el-menu-item {
+      &:not(.is-disabled) {
+        &:hover,
+        &:focus {
+          background-color: var(--navbar-hover) !important;
+        }
+      }
+    }
+  }
+
+  /* 分割窗格覆盖 */
+  .splitpanes {
+    background-color: var(--splitpanes-bg);
+
+    .splitpanes__pane {
+      background-color: var(--splitpanes-bg);
+      border-color: var(--splitpanes-border);
+    }
+
+    .splitpanes__splitter {
+      background-color: var(--splitpanes-splitter-bg);
+      border-color: var(--splitpanes-border);
+
+      &:hover {
+        background-color: var(--splitpanes-splitter-hover-bg);
+      }
+
+      &:before,
+      &:after {
+        background-color: var(--splitpanes-border);
+      }
+    }
+  }
+
+  /* 表格样式覆盖 */
+  .el-table {
+    --el-table-header-bg-color: var(--el-bg-color-overlay) !important;
+    --el-table-header-text-color: var(--el-text-color-regular) !important;
+    --el-table-border-color: var(--el-border-color-light) !important;
+    --el-table-row-hover-bg-color: var(--el-bg-color-overlay) !important;
+
+    .el-table__header-wrapper, .el-table__fixed-header-wrapper {
+      th {
+        background-color: var(--el-bg-color-overlay, #f8f8f9) !important;
+        color: var(--el-text-color-regular, #515a6e);
+      }
+    }
+  }
+
+  /* 树组件高亮样式覆盖 */
+  .el-tree {
+    .el-tree-node.is-current > .el-tree-node__content {
+      background-color: var(--el-bg-color-overlay) !important;
+      color: var(--el-color-primary);
+    }
+
+    .el-tree-node__content:hover {
+      background-color: var(--el-bg-color-overlay);
+    }
+  }
+  
+  /* 下拉菜单样式覆盖 */
+  .el-dropdown-menu__item:not(.is-disabled):focus, .el-dropdown-menu__item:not(.is-disabled):hover{
+    background-color: var(--navbar-hover) !important;
+  }
+
+  /* blockquote样式覆盖 */
+  blockquote {
+    background-color: var(--blockquote-bg) !important;
+    border-left-color: var(--blockquote-border) !important;
+    color: var(--blockquote-text) !important;
+  }
+  
+  /* 时间表达式标题样式覆盖 */
+  .popup-result .title {
+    background: var(--cron-border);
+  }
+
+}
+

+ 0 - 54
canteen-ui/src/assets/styles/variables.scss

@@ -1,54 +0,0 @@
-// base color
-$blue:#324157;
-$light-blue:#3A71A8;
-$red:#C03639;
-$pink: #E65D6E;
-$green: #30B08F;
-$tiffany: #4AB7BD;
-$yellow:#FEC171;
-$panGreen: #30B08F;
-
-// 默认菜单主题风格
-$base-menu-color:#bfcbd9;
-$base-menu-color-active:#f4f4f5;
-$base-menu-background:#304156;
-$base-logo-title-color: #ffffff;
-
-$base-menu-light-color:rgba(0,0,0,.70);
-$base-menu-light-background:#ffffff;
-$base-logo-light-title-color: #001529;
-
-$base-sub-menu-background:#1f2d3d;
-$base-sub-menu-hover:#001528;
-
-// 自定义暗色菜单风格
-/**
-$base-menu-color:hsla(0,0%,100%,.65);
-$base-menu-color-active:#fff;
-$base-menu-background:#001529;
-$base-logo-title-color: #ffffff;
-
-$base-menu-light-color:rgba(0,0,0,.70);
-$base-menu-light-background:#ffffff;
-$base-logo-light-title-color: #001529;
-
-$base-sub-menu-background:#000c17;
-$base-sub-menu-hover:#001528;
-*/
-
-$base-sidebar-width: 200px;
-
-// the :export directive is the magic sauce for webpack
-// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
-:export {
-  menuColor: $base-menu-color;
-  menuLightColor: $base-menu-light-color;
-  menuColorActive: $base-menu-color-active;
-  menuBackground: $base-menu-background;
-  menuLightBackground: $base-menu-light-background;
-  subMenuBackground: $base-sub-menu-background;
-  subMenuHover: $base-sub-menu-hover;
-  sideBarWidth: $base-sidebar-width;
-  logoTitleColor: $base-logo-title-color;
-  logoLightTitleColor: $base-logo-light-title-color
-}

+ 72 - 77
canteen-ui/src/components/Breadcrumb/index.vue

@@ -9,95 +9,90 @@
   </el-breadcrumb>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      levelList: null
-    }
-  },
-  watch: {
-    $route(route) {
-      // if you go to the redirect page, do not update the breadcrumbs
-      if (route.path.startsWith('/redirect/')) {
-        return
-      }
-      this.getBreadcrumb()
-    }
-  },
-  created() {
-    this.getBreadcrumb()
-  },
-  methods: {
-    getBreadcrumb() {
-      // only show routes with meta.title
-      let matched = []
-      const router = this.$route
-      const pathNum = this.findPathNum(router.path)
-      // multi-level menu
-      if (pathNum > 2) {
-        const reg = /\/\w+/gi
-        const pathList = router.path.match(reg).map((item, index) => {
-          if (index !== 0) item = item.slice(1)
-          return item
-        })
-        this.getMatched(pathList, this.$store.getters.defaultRoutes, matched)
-      } else {
-        matched = router.matched.filter(item => item.meta && item.meta.title)
-      }
-      // 判断是否为首页
-      if (!this.isDashboard(matched[0])) {
-        matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
-      }
-      this.levelList = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
-    },
-    findPathNum(str, char = "/") {
-      let index = str.indexOf(char)
-      let num = 0
-      while (index !== -1) {
-        num++
-        index = str.indexOf(char, index + 1)
-      }
-      return num
-    },
-    getMatched(pathList, routeList, matched) {
-      let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
-      if (data) {
-        matched.push(data)
-        if (data.children && pathList.length) {
-          pathList.shift()
-          this.getMatched(pathList, data.children, matched)
-        }
-      }
-    },
-    isDashboard(route) {
-      const name = route && route.name
-      if (!name) {
-        return false
-      }
-      return name.trim() === 'Index'
-    },
-    handleLink(item) {
-      const { redirect, path } = item
-      if (redirect) {
-        this.$router.push(redirect)
-        return
-      }
-      this.$router.push(path)
+<script setup>
+import usePermissionStore from '@/store/modules/permission'
+
+const route = useRoute()
+const router = useRouter()
+const permissionStore = usePermissionStore()
+const levelList = ref([])
+
+function getBreadcrumb() {
+  // only show routes with meta.title
+  let matched = []
+  const pathNum = findPathNum(route.path)
+  // multi-level menu
+  if (pathNum > 2) {
+    const reg = /\/\w+/gi
+    const pathList = route.path.match(reg).map((item, index) => {
+      if (index !== 0) item = item.slice(1)
+      return item
+    })
+    getMatched(pathList, permissionStore.defaultRoutes, matched)
+  } else {
+    matched = route.matched.filter((item) => item.meta && item.meta.title)
+  }
+  // 判断是否为首页
+  if (!isDashboard(matched[0])) {
+    matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched)
+  }
+  levelList.value = matched.filter(item => item.meta && item.meta.title && item.meta.breadcrumb !== false)
+}
+function findPathNum(str, char = "/") {
+  let index = str.indexOf(char)
+  let num = 0
+  while (index !== -1) {
+    num++
+    index = str.indexOf(char, index + 1)
+  }
+  return num
+}
+function getMatched(pathList, routeList, matched) {
+  let data = routeList.find(item => item.path == pathList[0] || (item.name += '').toLowerCase() == pathList[0])
+  if (data) {
+    matched.push(data)
+    if (data.children && pathList.length) {
+      pathList.shift()
+      getMatched(pathList, data.children, matched)
     }
   }
 }
+function isDashboard(route) {
+  const name = route && route.name
+  if (!name) {
+    return false
+  }
+  return name.trim() === 'Index'
+}
+function handleLink(item) {
+  const { redirect, path } = item
+  if (redirect) {
+    router.push(redirect)
+    return
+  }
+  router.push(path)
+}
+
+watchEffect(() => {
+  // if you go to the redirect page, do not update the breadcrumbs
+  if (route.path.startsWith('/redirect/')) {
+    return
+  }
+  getBreadcrumb()
+})
+getBreadcrumb()
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
 .app-breadcrumb.el-breadcrumb {
   display: inline-block;
   font-size: 14px;
   line-height: 50px;
   margin-left: 8px;
+
   .no-redirect {
     color: #97a8be;
     cursor: text;
   }
 }
-</style>
+</style>

+ 164 - 151
canteen-ui/src/components/Crontab/day.vue

@@ -1,161 +1,174 @@
 <template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				日,允许的通配符[, - * ? / L W]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                日,允许的通配符[, - * ? / L W]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				不指定
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                不指定
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				周期从
-				<el-input-number v-model='cycle01' :min="1" :max="30" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="31" /> 日
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                周期从
+                <el-input-number v-model='cycle01' :min="1" :max="30" /> -
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="31" /> 日
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				
-				<el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
-				<el-input-number v-model='average02' :min="1" :max="31 - average01 || 1" /> 日执行一次
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                
+                <el-input-number v-model='average01' :min="1" :max="30" /> 号开始,每
+                <el-input-number v-model='average02' :min="1" :max="31 - average01" /> 日执行一次
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="5">
-				每月
-				<el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="5">
+                每月
+                <el-input-number v-model='workday' :min="1" :max="31" /> 号最近的那个工作日
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="6">
-				本月最后一天
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="6">
+                本月最后一天
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="7">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 31" :key="item" :value="item">{{item}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="7">
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
+                    <el-option v-for="item in 31" :key="item" :label="item" :value="item" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
-
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			workday: 1,
-			cycle01: 1,
-			cycle02: 2,
-			average01: 1,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-day',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			('day rachange');
-			if (this.radioValue !== 2 && this.cron.week !== '?') {
-				this.$emit('update', 'week', '?', 'day')
-			}
-
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'day', '*');
-					break;
-				case 2:
-					this.$emit('update', 'day', '?');
-					break;
-				case 3:
-					this.$emit('update', 'day', this.cycleTotal);
-					break;
-				case 4:
-					this.$emit('update', 'day', this.averageTotal);
-					break;
-				case 5:
-					this.$emit('update', 'day', this.workday + 'W');
-					break;
-				case 6:
-					this.$emit('update', 'day', 'L');
-					break;
-				case 7:
-					this.$emit('update', 'day', this.checkboxString);
-					break;
-			}
-			('day rachange end');
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'day', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'day', this.averageTotal);
-			}
-		},
-		// 最近工作日值变化时
-		workdayChange() {
-			if (this.radioValue == '5') {
-				this.$emit('update', 'day', this.workdayCheck + 'W');
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '7') {
-				this.$emit('update', 'day', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'workdayCheck': 'workdayChange',
-		'checkboxString': 'checkboxChange',
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 1, 30)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 31, 31)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 1, 30)
-			const average02 = this.checkNum(this.average02, 1, 31 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算工作日格式
-		workdayCheck: function () {
-			const workday = this.checkNum(this.workday, 1, 31)
-			return workday;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: "",
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const workday = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([1])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 1, 30)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 31)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 1, 30)
+    average02.value = props.check(average02.value, 1, 31 - average01.value)
+    return average01.value + '/' + average02.value
+})
+const workdayTotal = computed(() => {
+    workday.value = props.check(workday.value, 1, 31)
+    return workday.value + 'W'
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.day, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, workdayTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === "*") {
+        radioValue.value = 1
+    } else if (value === "?") {
+        radioValue.value = 2
+    } else if (value.indexOf("-") > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else if (value.indexOf("/") > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[0])
+        average02.value = Number(indexArr[1])
+        radioValue.value = 4
+    } else if (value.indexOf("W") > -1) {
+        const indexArr = value.split("W")
+        workday.value = Number(indexArr[0])
+        radioValue.value = 5
+    } else if (value === "L") {
+        radioValue.value = 6
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 7
+    }
+}
+// 单选按钮值变化时
+function onRadioChange() {
+    if (radioValue.value === 2 && props.cron.week === '?') {
+        emit('update', 'week', '*', 'day')
+    }
+    if (radioValue.value !== 2 && props.cron.week !== '?') {
+        emit('update', 'week', '?', 'day')
+    }
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'day', '*', 'day')
+            break
+        case 2:
+            emit('update', 'day', '?', 'day')
+            break
+        case 3:
+            emit('update', 'day', cycleTotal.value, 'day')
+            break
+        case 4:
+            emit('update', 'day', averageTotal.value, 'day')
+            break
+        case 5:
+            emit('update', 'day', workdayTotal.value, 'day')
+            break
+        case 6:
+            emit('update', 'day', 'L', 'day')
+            break
+        case 7:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'day', checkboxString.value, 'day')
+            break
+    }
 }
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 18.8rem;
+}
+</style>

+ 125 - 112
canteen-ui/src/components/Crontab/hour.vue

@@ -1,120 +1,133 @@
 <template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				小时,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                小时,允许的通配符[, - * /]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				周期从
-				<el-input-number v-model='cycle01' :min="0" :max="22" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="23" /> 小
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                周期从
+                <el-input-number v-model='cycle01' :min="0" :max="22" /> -
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="23" /> 
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				
-				<el-input-number v-model='average01' :min="0" :max="22" /> 时开始,每
-				<el-input-number v-model='average02' :min="1" :max="23 - average01 || 0" /> 小时执行一次
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                
+                <el-input-number v-model='average01' :min="0" :max="22" /> 时开始,每
+                <el-input-number v-model='average02' :min="1" :max="23 - average01" /> 小时执行一次
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 24" :key="item" :value="item-1">{{item-1}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
+                    <el-option v-for="item in 24" :key="item" :label="item - 1" :value="item - 1" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			cycle01: 0,
-			cycle02: 1,
-			average01: 0,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-hour',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			if (this.cron.min === '*') {
-			    this.$emit('update', 'min', '0', 'hour');
-			}
-			if (this.cron.second === '*') {
-			    this.$emit('update', 'second', '0', 'hour');
-			}
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'hour', '*')
-					break;
-				case 2:
-					this.$emit('update', 'hour', this.cycleTotal);
-					break;
-				case 3:
-					this.$emit('update', 'hour', this.averageTotal);
-					break;
-				case 4:
-					this.$emit('update', 'hour', this.checkboxString);
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '2') {
-				this.$emit('update', 'hour', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'hour', this.averageTotal);
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'hour', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange'
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 0, 22)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 23)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 0, 22)
-			const average02 = this.checkNum(this.average02, 1, 23 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: "",
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 0, 22)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 23)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 0, 22)
+    average02.value = props.check(average02.value, 1, 23 - average01.value)
+    return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.hour, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (props.cron.min === '*') {
+        emit('update', 'min', '0', 'hour');
+    }
+    if (props.cron.second === '*') {
+        emit('update', 'second', '0', 'hour');
+    }
+    if (value === '*') {
+        radioValue.value = 1
+    } else if (value.indexOf('-') > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 2
+    } else if (value.indexOf('/') > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[0])
+        average02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 4
+    }
+}
+function onRadioChange() {
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'hour', '*', 'hour')
+            break
+        case 2:
+            emit('update', 'hour', cycleTotal.value, 'hour')
+            break
+        case 3:
+            emit('update', 'hour', averageTotal.value, 'hour')
+            break
+        case 4:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'hour', checkboxString.value, 'hour')
+            break
+    }
 }
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 18.8rem;
+}
+</style>

+ 269 - 390
canteen-ui/src/components/Crontab/index.vue

@@ -1,320 +1,233 @@
 <template>
-  <div>
-    <el-tabs type="border-card">
-      <el-tab-pane label="秒" v-if="shouldHide('second')">
-        <CrontabSecond
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronsecond"
-        />
-      </el-tab-pane>
+    <div>
+        <el-tabs type="border-card">
+            <el-tab-pane label="秒" v-if="shouldHide('second')">
+                <CrontabSecond
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronsecond"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="分钟" v-if="shouldHide('min')">
-        <CrontabMin
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronmin"
-        />
-      </el-tab-pane>
+            <el-tab-pane label="分钟" v-if="shouldHide('min')">
+                <CrontabMin
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronmin"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="小时" v-if="shouldHide('hour')">
-        <CrontabHour
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronhour"
-        />
-      </el-tab-pane>
+            <el-tab-pane label="小时" v-if="shouldHide('hour')">
+                <CrontabHour
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronhour"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="日" v-if="shouldHide('day')">
-        <CrontabDay
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronday"
-        />
-      </el-tab-pane>
+            <el-tab-pane label="日" v-if="shouldHide('day')">
+                <CrontabDay
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronday"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="月" v-if="shouldHide('month')">
-        <CrontabMonth
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronmonth"
-        />
-      </el-tab-pane>
+            <el-tab-pane label="月" v-if="shouldHide('month')">
+                <CrontabMonth
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronmonth"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="周" v-if="shouldHide('week')">
-        <CrontabWeek
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronweek"
-        />
-      </el-tab-pane>
+            <el-tab-pane label="周" v-if="shouldHide('week')">
+                <CrontabWeek
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronweek"
+                />
+            </el-tab-pane>
 
-      <el-tab-pane label="年" v-if="shouldHide('year')">
-        <CrontabYear
-          @update="updateCrontabValue"
-          :check="checkNumber"
-          :cron="crontabValueObj"
-          ref="cronyear"
-        />
-      </el-tab-pane>
-    </el-tabs>
+            <el-tab-pane label="年" v-if="shouldHide('year')">
+                <CrontabYear
+                    @update="updateCrontabValue"
+                    :check="checkNumber"
+                    :cron="crontabValueObj"
+                    ref="cronyear"
+                />
+            </el-tab-pane>
+        </el-tabs>
 
-    <div class="popup-main">
-      <div class="popup-result">
-        <p class="title">时间表达式</p>
-        <table>
-          <thead>
-            <th v-for="item of tabTitles" width="40" :key="item">{{item}}</th>
-            <th>Cron 表达式</th>
-          </thead>
-          <tbody>
-            <td>
-              <span>{{crontabValueObj.second}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.min}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.hour}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.day}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.month}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.week}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueObj.year}}</span>
-            </td>
-            <td>
-              <span>{{crontabValueString}}</span>
-            </td>
-          </tbody>
-        </table>
-      </div>
-      <CrontabResult :ex="crontabValueString"></CrontabResult>
+        <div class="popup-main">
+            <div class="popup-result">
+                <p class="title">时间表达式</p>
+                <table>
+                    <thead>
+                        <th v-for="item of tabTitles" :key="item">{{item}}</th>
+                        <th>Cron 表达式</th>
+                    </thead>
+                    <tbody>
+                        <td>
+                            <span v-if="crontabValueObj.second.length < 10">{{crontabValueObj.second}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.second" placement="top"><span>{{crontabValueObj.second}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.min.length < 10">{{crontabValueObj.min}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.min" placement="top"><span>{{crontabValueObj.min}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.hour.length < 10">{{crontabValueObj.hour}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.hour" placement="top"><span>{{crontabValueObj.hour}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.day.length < 10">{{crontabValueObj.day}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.day" placement="top"><span>{{crontabValueObj.day}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.month.length < 10">{{crontabValueObj.month}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.month" placement="top"><span>{{crontabValueObj.month}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.week.length < 10">{{crontabValueObj.week}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.week" placement="top"><span>{{crontabValueObj.week}}</span></el-tooltip>
+                        </td>
+                        <td>
+                            <span v-if="crontabValueObj.year.length < 10">{{crontabValueObj.year}}</span>
+                            <el-tooltip v-else :content="crontabValueObj.year" placement="top"><span>{{crontabValueObj.year}}</span></el-tooltip>
+                        </td>
+                        <td class="result">
+                            <span v-if="crontabValueString.length < 90">{{crontabValueString}}</span>
+                            <el-tooltip v-else :content="crontabValueString" placement="top"><span>{{crontabValueString}}</span></el-tooltip>
+                        </td>
+                    </tbody>
+                </table>
+            </div>
+            <CrontabResult :ex="crontabValueString"></CrontabResult>
 
-      <div class="pop_btn">
-        <el-button size="small" type="primary" @click="submitFill">确定</el-button>
-        <el-button size="small" type="warning" @click="clearCron">重置</el-button>
-        <el-button size="small" @click="hidePopup">取消</el-button>
-      </div>
+            <div class="pop_btn">
+                <el-button type="primary" @click="submitFill">确定</el-button>
+                <el-button type="warning" @click="clearCron">重置</el-button>
+                <el-button @click="hidePopup">取消</el-button>
+            </div>
+        </div>
     </div>
-  </div>
 </template>
 
-<script>
-import CrontabSecond from "./second.vue";
-import CrontabMin from "./min.vue";
-import CrontabHour from "./hour.vue";
-import CrontabDay from "./day.vue";
-import CrontabMonth from "./month.vue";
-import CrontabWeek from "./week.vue";
-import CrontabYear from "./year.vue";
-import CrontabResult from "./result.vue";
-
-export default {
-  data() {
-    return {
-      tabTitles: ["秒", "分钟", "小时", "日", "月", "周", "年"],
-      tabActive: 0,
-      myindex: 0,
-      crontabValueObj: {
-        second: "*",
-        min: "*",
-        hour: "*",
-        day: "*",
-        month: "*",
-        week: "?",
-        year: "",
-      },
-    };
-  },
-  name: "vcrontab",
-  props: ["expression", "hideComponent"],
-  methods: {
-    shouldHide(key) {
-      if (this.hideComponent && this.hideComponent.includes(key)) return false;
-      return true;
+<script setup>
+import CrontabSecond from "./second.vue"
+import CrontabMin from "./min.vue"
+import CrontabHour from "./hour.vue"
+import CrontabDay from "./day.vue"
+import CrontabMonth from "./month.vue"
+import CrontabWeek from "./week.vue"
+import CrontabYear from "./year.vue"
+import CrontabResult from "./result.vue"
+const { proxy } = getCurrentInstance()
+const emit = defineEmits(['hide', 'fill'])
+const props = defineProps({
+    hideComponent: {
+        type: Array,
+        default: () => [],
     },
-    resolveExp() {
-      // 反解析 表达式
-      if (this.expression) {
-        let arr = this.expression.split(" ");
+    expression: {
+        type: String,
+        default: ""
+    }
+})
+const tabTitles = ref(["秒", "分钟", "小时", "日", "月", "周", "年"])
+const tabActive = ref(0)
+const hideComponent = ref([])
+const expression = ref('')
+const crontabValueObj = ref({
+    second: "*",
+    min: "*",
+    hour: "*",
+    day: "*",
+    month: "*",
+    week: "?",
+    year: "",
+})
+const crontabValueString = computed(() => {
+    const obj = crontabValueObj.value
+    return obj.second
+        + " "
+        + obj.min
+        + " "
+        + obj.hour
+        + " "
+        + obj.day
+        + " "
+        + obj.month
+        + " "
+        + obj.week
+        + (obj.year === "" ? "" : " " + obj.year)
+})
+watch(expression, () => resolveExp())
+function shouldHide(key) {
+    return !(hideComponent.value && hideComponent.value.includes(key))
+}
+function resolveExp() {
+    // 反解析 表达式
+    if (expression.value) {
+        const arr = expression.value.split(/\s+/)
         if (arr.length >= 6) {
-          //6 位以上是合法表达式
-          let obj = {
-            second: arr[0],
-            min: arr[1],
-            hour: arr[2],
-            day: arr[3],
-            month: arr[4],
-            week: arr[5],
-            year: arr[6] ? arr[6] : "",
-          };
-          this.crontabValueObj = {
-            ...obj,
-          };
-          for (let i in obj) {
-            if (obj[i]) this.changeRadio(i, obj[i]);
-          }
+            //6 位以上是合法表达式
+            let obj = {
+                second: arr[0],
+                min: arr[1],
+                hour: arr[2],
+                day: arr[3],
+                month: arr[4],
+                week: arr[5],
+                year: arr[6] ? arr[6] : ""
+            }
+            crontabValueObj.value = {
+                ...obj,
+            }
         }
-      } else {
+    } else {
         // 没有传入的表达式 则还原
-        this.clearCron();
-      }
-    },
-    // tab切换值
-    tabCheck(index) {
-      this.tabActive = index;
-    },
-    // 由子组件触发,更改表达式组成的字段值
-    updateCrontabValue(name, value, from) {
-      "updateCrontabValue", name, value, from;
-      this.crontabValueObj[name] = value;
-      if (from && from !== name) {
-        console.log(`来自组件 ${from} 改变了 ${name} ${value}`);
-        this.changeRadio(name, value);
-      }
-    },
-    // 赋值到组件
-    changeRadio(name, value) {
-      let arr = ["second", "min", "hour", "month"],
-        refName = "cron" + name,
-        insValue;
-
-      if (!this.$refs[refName]) return;
-
-      if (arr.includes(name)) {
-        if (value === "*") {
-          insValue = 1;
-        } else if (value.indexOf("-") > -1) {
-          let indexArr = value.split("-");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].cycle01 = 0)
-            : (this.$refs[refName].cycle01 = indexArr[0]);
-          this.$refs[refName].cycle02 = indexArr[1];
-          insValue = 2;
-        } else if (value.indexOf("/") > -1) {
-          let indexArr = value.split("/");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].average01 = 0)
-            : (this.$refs[refName].average01 = indexArr[0]);
-          this.$refs[refName].average02 = indexArr[1];
-          insValue = 3;
-        } else {
-          insValue = 4;
-          this.$refs[refName].checkboxList = value.split(",");
-        }
-      } else if (name == "day") {
-        if (value === "*") {
-          insValue = 1;
-        } else if (value == "?") {
-          insValue = 2;
-        } else if (value.indexOf("-") > -1) {
-          let indexArr = value.split("-");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].cycle01 = 0)
-            : (this.$refs[refName].cycle01 = indexArr[0]);
-          this.$refs[refName].cycle02 = indexArr[1];
-          insValue = 3;
-        } else if (value.indexOf("/") > -1) {
-          let indexArr = value.split("/");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].average01 = 0)
-            : (this.$refs[refName].average01 = indexArr[0]);
-          this.$refs[refName].average02 = indexArr[1];
-          insValue = 4;
-        } else if (value.indexOf("W") > -1) {
-          let indexArr = value.split("W");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].workday = 0)
-            : (this.$refs[refName].workday = indexArr[0]);
-          insValue = 5;
-        } else if (value === "L") {
-          insValue = 6;
-        } else {
-          this.$refs[refName].checkboxList = value.split(",");
-          insValue = 7;
-        }
-      } else if (name == "week") {
-        if (value === "*") {
-          insValue = 1;
-        } else if (value == "?") {
-          insValue = 2;
-        } else if (value.indexOf("-") > -1) {
-          let indexArr = value.split("-");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].cycle01 = 0)
-            : (this.$refs[refName].cycle01 = indexArr[0]);
-          this.$refs[refName].cycle02 = indexArr[1];
-          insValue = 3;
-        } else if (value.indexOf("#") > -1) {
-          let indexArr = value.split("#");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].average01 = 1)
-            : (this.$refs[refName].average01 = indexArr[0]);
-          this.$refs[refName].average02 = indexArr[1];
-          insValue = 4;
-        } else if (value.indexOf("L") > -1) {
-          let indexArr = value.split("L");
-          isNaN(indexArr[0])
-            ? (this.$refs[refName].weekday = 1)
-            : (this.$refs[refName].weekday = indexArr[0]);
-          insValue = 5;
-        } else {
-          this.$refs[refName].checkboxList = value.split(",");
-          insValue = 6;
-        }
-      } else if (name == "year") {
-        if (value == "") {
-          insValue = 1;
-        } else if (value == "*") {
-          insValue = 2;
-        } else if (value.indexOf("-") > -1) {
-          insValue = 3;
-        } else if (value.indexOf("/") > -1) {
-          insValue = 4;
-        } else {
-          this.$refs[refName].checkboxList = value.split(",");
-          insValue = 5;
-        }
-      }
-      this.$refs[refName].radioValue = insValue;
-    },
-    // 表单选项的子组件校验数字格式(通过-props传递)
-    checkNumber(value, minLimit, maxLimit) {
-      // 检查必须为整数
-      value = Math.floor(value);
-      if (value < minLimit) {
-        value = minLimit;
-      } else if (value > maxLimit) {
-        value = maxLimit;
-      }
-      return value;
-    },
-    // 隐藏弹窗
-    hidePopup() {
-      this.$emit("hide");
-    },
-    // 填充表达式
-    submitFill() {
-      this.$emit("fill", this.crontabValueString);
-      this.hidePopup();
-    },
-    clearCron() {
-      // 还原选择项
-      ("准备还原");
-      this.crontabValueObj = {
+        clearCron()
+    }
+}
+// tab切换值
+function tabCheck(index) {
+    tabActive.value = index
+}
+// 由子组件触发,更改表达式组成的字段值
+function updateCrontabValue(name, value, from) {
+    crontabValueObj.value[name] = value
+}
+// 表单选项的子组件校验数字格式(通过-props传递)
+function checkNumber(value, minLimit, maxLimit) {
+    // 检查必须为整数
+    value = Math.floor(value)
+    if (value < minLimit) {
+        value = minLimit
+    } else if (value > maxLimit) {
+        value = maxLimit
+    }
+    return value
+}
+// 隐藏弹窗
+function hidePopup() {
+    emit("hide")
+}
+// 填充表达式
+function submitFill() {
+    emit("fill", crontabValueString.value)
+    hidePopup()
+}
+function clearCron() {
+    // 还原选择项
+    crontabValueObj.value = {
         second: "*",
         min: "*",
         hour: "*",
@@ -322,109 +235,75 @@ export default {
         month: "*",
         week: "?",
         year: "",
-      };
-      for (let j in this.crontabValueObj) {
-        this.changeRadio(j, this.crontabValueObj[j]);
-      }
-    },
-  },
-  computed: {
-    crontabValueString: function() {
-      let obj = this.crontabValueObj;
-      let str =
-        obj.second +
-        " " +
-        obj.min +
-        " " +
-        obj.hour +
-        " " +
-        obj.day +
-        " " +
-        obj.month +
-        " " +
-        obj.week +
-        (obj.year == "" ? "" : " " + obj.year);
-      return str;
-    },
-  },
-  components: {
-    CrontabSecond,
-    CrontabMin,
-    CrontabHour,
-    CrontabDay,
-    CrontabMonth,
-    CrontabWeek,
-    CrontabYear,
-    CrontabResult,
-  },
-  watch: {
-    expression: "resolveExp",
-    hideComponent(value) {
-      // 隐藏部分组件
-    },
-  },
-  mounted: function() {
-    this.resolveExp();
-  },
-};
+    }
+}
+onMounted(() => {
+    expression.value = props.expression
+    hideComponent.value = props.hideComponent
+})
 </script>
-<style scoped>
+
+<style lang="scss" scoped>
 .pop_btn {
-  text-align: center;
-  margin-top: 20px;
+    text-align: center;
+    margin-top: 20px;
 }
 .popup-main {
-  position: relative;
-  margin: 10px auto;
-  background: #fff;
-  border-radius: 5px;
-  font-size: 12px;
-  overflow: hidden;
+    position: relative;
+    margin: 10px auto;
+    border-radius: 5px;
+    font-size: 12px;
+    overflow: hidden;
 }
 .popup-title {
-  overflow: hidden;
-  line-height: 34px;
-  padding-top: 6px;
-  background: #f2f2f2;
+    overflow: hidden;
+    line-height: 34px;
+    padding-top: 6px;
+    background: #f2f2f2;
 }
 .popup-result {
-  box-sizing: border-box;
-  line-height: 24px;
-  margin: 25px auto;
-  padding: 15px 10px 10px;
-  border: 1px solid #ccc;
-  position: relative;
+    box-sizing: border-box;
+    line-height: 24px;
+    margin: 25px auto;
+    padding: 15px 10px 10px;
+    border: 1px solid #ccc;
+    position: relative;
 }
 .popup-result .title {
-  position: absolute;
-  top: -28px;
-  left: 50%;
-  width: 140px;
-  font-size: 14px;
-  margin-left: -70px;
-  text-align: center;
-  line-height: 30px;
-  background: #fff;
+    position: absolute;
+    top: -28px;
+    left: 50%;
+    width: 140px;
+    font-size: 14px;
+    margin-left: -70px;
+    text-align: center;
+    line-height: 30px;
+    background: #fff;
 }
 .popup-result table {
-  text-align: center;
-  width: 100%;
-  margin: 0 auto;
+    text-align: center;
+    width: 100%;
+    margin: 0 auto;
+}
+.popup-result table td:not(.result) {
+    width: 3.5rem;
+    min-width: 3.5rem;
+    max-width: 3.5rem;
 }
 .popup-result table span {
-  display: block;
-  width: 100%;
-  font-family: arial;
-  line-height: 30px;
-  height: 30px;
-  white-space: nowrap;
-  overflow: hidden;
-  border: 1px solid #e8e8e8;
+    display: block;
+    width: 100%;
+    font-family: arial;
+    line-height: 30px;
+    height: 30px;
+    white-space: nowrap;
+    overflow: hidden;
+    border: 1px solid #e8e8e8;
 }
 .popup-result-scroll {
-  font-size: 12px;
-  line-height: 24px;
-  height: 10em;
-  overflow-y: auto;
+    font-size: 12px;
+    line-height: 24px;
+    height: 10em;
+    overflow-y: auto;
 }
-</style>
+</style>

+ 119 - 109
canteen-ui/src/components/Crontab/min.vue

@@ -1,116 +1,126 @@
 <template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				分钟,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                分钟,允许的通配符[, - * /]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				周期从
-				<el-input-number v-model='cycle01' :min="0" :max="58" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 分钟
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                周期从
+                <el-input-number v-model='cycle01' :min="0" :max="58" /> -
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 分钟
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				从
-				<el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始,每
-				<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 分钟执行一次
-			</el-radio>
-		</el-form-item>
-
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                从
+                <el-input-number v-model='average01' :min="0" :max="58" /> 分钟开始, 每
+                <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 分钟执行一次
+            </el-radio>
+        </el-form-item>
 
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
+                    <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: "",
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 0, 58)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 0, 58)
+    average02.value = props.check(average02.value, 1, 59 - average01.value)
+    return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.min, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === '*') {
+        radioValue.value = 1
+    } else if (value.indexOf('-') > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 2
+    } else if (value.indexOf('/') > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[0])
+        average02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 4
+    }
+}
+function onRadioChange() {
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'min', '*', 'min')
+            break
+        case 2:
+            emit('update', 'min', cycleTotal.value, 'min')
+            break
+        case 3:
+            emit('update', 'min', averageTotal.value, 'min')
+            break
+        case 4:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'min', checkboxString.value, 'min')
+            break
+    }
+}
+</script>
 
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			cycle01: 1,
-			cycle02: 2,
-			average01: 0,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-min',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'min', '*', 'min');
-					break;
-				case 2:
-					this.$emit('update', 'min', this.cycleTotal, 'min');
-					break;
-				case 3:
-					this.$emit('update', 'min', this.averageTotal, 'min');
-					break;
-				case 4:
-					this.$emit('update', 'min', this.checkboxString, 'min');
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '2') {
-				this.$emit('update', 'min', this.cycleTotal, 'min');
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'min', this.averageTotal, 'min');
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'min', this.checkboxString, 'min');
-			}
-		},
-
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange',
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 0, 58)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 0, 58)
-			const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 19.8rem;
 }
-</script>
+</style>

+ 133 - 106
canteen-ui/src/components/Crontab/month.vue

@@ -1,114 +1,141 @@
 <template>
-	<el-form size='small'>
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				月,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                月,允许的通配符[, - * /]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				周期从
-				<el-input-number v-model='cycle01' :min="1" :max="11" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 2" :max="12" /> 月
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                周期从
+                <el-input-number v-model='cycle01' :min="1" :max="11" /> -
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="12" /> 月
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				
-				<el-input-number v-model='average01' :min="1" :max="11" /> 月开始,每
-				<el-input-number v-model='average02' :min="1" :max="12 - average01 || 0" /> 月月执行一次
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                
+                <el-input-number v-model='average01' :min="1" :max="11" /> 月开始,每
+                <el-input-number v-model='average02' :min="1" :max="12 - average01" /> 月月执行一次
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 12" :key="item" :value="item">{{item}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
+                    <el-option v-for="item in monthList" :key="item.key" :label="item.value" :value="item.key" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			cycle01: 1,
-			cycle02: 2,
-			average01: 1,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.check
-		}
-	},
-	name: 'crontab-month',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'month', '*');
-					break;
-				case 2:
-					this.$emit('update', 'month', this.cycleTotal);
-					break;
-				case 3:
-					this.$emit('update', 'month', this.averageTotal);
-					break;
-				case 4:
-					this.$emit('update', 'month', this.checkboxString);
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '2') {
-				this.$emit('update', 'month', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'month', this.averageTotal);
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'month', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange'
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 1, 11)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 2, 12)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 1, 11)
-			const average02 = this.checkNum(this.average02, 1, 12 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: "",
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(1)
+const cycle01 = ref(1)
+const cycle02 = ref(2)
+const average01 = ref(1)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([1])
+const monthList = ref([
+    {key: 1, value: '一月'},
+    {key: 2, value: '二月'},
+    {key: 3, value: '三月'},
+    {key: 4, value: '四月'},
+    {key: 5, value: '五月'},
+    {key: 6, value: '六月'},
+    {key: 7, value: '七月'},
+    {key: 8, value: '八月'},
+    {key: 9, value: '九月'},
+    {key: 10, value: '十月'},
+    {key: 11, value: '十一月'},
+    {key: 12, value: '十二月'}
+])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 1, 11)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 12)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 1, 11)
+    average02.value = props.check(average02.value, 1, 12 - average01.value)
+    return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.month, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === '*') {
+        radioValue.value = 1
+    } else if (value.indexOf('-') > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 2
+    } else if (value.indexOf('/') > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[0])
+        average02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 4
+    }
+}
+function onRadioChange() {
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'month', '*', 'month')
+            break
+        case 2:
+            emit('update', 'month', cycleTotal.value, 'month')
+            break
+        case 3:
+            emit('update', 'month', averageTotal.value, 'month')
+            break
+        case 4:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'month', checkboxString.value, 'month')
+            break
+    }
 }
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 18.8rem;
+}
+</style>

+ 527 - 546
canteen-ui/src/components/Crontab/result.vue

@@ -10,550 +10,531 @@
 	</div>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			dayRule: '',
-			dayRuleSup: '',
-			dateArr: [],
-			resultList: [],
-			isShow: false
-		}
-	},
-	name: 'crontab-result',
-	methods: {
-		// 表达式值变化时,开始去计算结果
-		expressionChange() {
-
-			// 计算开始-隐藏结果
-			this.isShow = false;
-			// 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
-			let ruleArr = this.$options.propsData.ex.split(' ');
-			// 用于记录进入循环的次数
-			let nums = 0;
-			// 用于暂时存符号时间规则结果的数组
-			let resultArr = [];
-			// 获取当前时间精确至[年、月、日、时、分、秒]
-			let nTime = new Date();
-			let nYear = nTime.getFullYear();
-			let nMonth = nTime.getMonth() + 1;
-			let nDay = nTime.getDate();
-			let nHour = nTime.getHours();
-			let nMin = nTime.getMinutes();
-			let nSecond = nTime.getSeconds();
-			// 根据规则获取到近100年可能年数组、月数组等等
-			this.getSecondArr(ruleArr[0]);
-			this.getMinArr(ruleArr[1]);
-			this.getHourArr(ruleArr[2]);
-			this.getDayArr(ruleArr[3]);
-			this.getMonthArr(ruleArr[4]);
-			this.getWeekArr(ruleArr[5]);
-			this.getYearArr(ruleArr[6], nYear);
-			// 将获取到的数组赋值-方便使用
-			let sDate = this.dateArr[0];
-			let mDate = this.dateArr[1];
-			let hDate = this.dateArr[2];
-			let DDate = this.dateArr[3];
-			let MDate = this.dateArr[4];
-			let YDate = this.dateArr[5];
-			// 获取当前时间在数组中的索引
-			let sIdx = this.getIndex(sDate, nSecond);
-			let mIdx = this.getIndex(mDate, nMin);
-			let hIdx = this.getIndex(hDate, nHour);
-			let DIdx = this.getIndex(DDate, nDay);
-			let MIdx = this.getIndex(MDate, nMonth);
-			let YIdx = this.getIndex(YDate, nYear);
-			// 重置月日时分秒的函数(后面用的比较多)
-			const resetSecond = function () {
-				sIdx = 0;
-				nSecond = sDate[sIdx]
-			}
-			const resetMin = function () {
-				mIdx = 0;
-				nMin = mDate[mIdx]
-				resetSecond();
-			}
-			const resetHour = function () {
-				hIdx = 0;
-				nHour = hDate[hIdx]
-				resetMin();
-			}
-			const resetDay = function () {
-				DIdx = 0;
-				nDay = DDate[DIdx]
-				resetHour();
-			}
-			const resetMonth = function () {
-				MIdx = 0;
-				nMonth = MDate[MIdx]
-				resetDay();
-			}
-			// 如果当前年份不为数组中当前值
-			if (nYear !== YDate[YIdx]) {
-				resetMonth();
-			}
-			// 如果当前月份不为数组中当前值
-			if (nMonth !== MDate[MIdx]) {
-				resetDay();
-			}
-			// 如果当前“日”不为数组中当前值
-			if (nDay !== DDate[DIdx]) {
-				resetHour();
-			}
-			// 如果当前“时”不为数组中当前值
-			if (nHour !== hDate[hIdx]) {
-				resetMin();
-			}
-			// 如果当前“分”不为数组中当前值
-			if (nMin !== mDate[mIdx]) {
-				resetSecond();
-			}
-
-			// 循环年份数组
-			goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
-				let YY = YDate[Yi];
-				// 如果到达最大值时
-				if (nMonth > MDate[MDate.length - 1]) {
-					resetMonth();
-					continue;
-				}
-				// 循环月份数组
-				goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
-					// 赋值、方便后面运算
-					let MM = MDate[Mi];
-					MM = MM < 10 ? '0' + MM : MM;
-					// 如果到达最大值时
-					if (nDay > DDate[DDate.length - 1]) {
-						resetDay();
-						if (Mi == MDate.length - 1) {
-							resetMonth();
-							continue goYear;
-						}
-						continue;
-					}
-					// 循环日期数组
-					goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
-						// 赋值、方便后面运算
-						let DD = DDate[Di];
-						let thisDD = DD < 10 ? '0' + DD : DD;
-
-						// 如果到达最大值时
-						if (nHour > hDate[hDate.length - 1]) {
-							resetHour();
-							if (Di == DDate.length - 1) {
-								resetDay();
-								if (Mi == MDate.length - 1) {
-									resetMonth();
-									continue goYear;
-								}
-								continue goMonth;
-							}
-							continue;
-						}
-
-						// 判断日期的合法性,不合法的话也是跳出当前循环
-						if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && this.dayRule !== 'workDay' && this.dayRule !== 'lastWeek' && this.dayRule !== 'lastDay') {
-							resetDay();
-							continue goMonth;
-						}
-						// 如果日期规则中有值时
-						if (this.dayRule == 'lastDay') {
-							// 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
-
-							if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-								while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-									DD--;
-
-									thisDD = DD < 10 ? '0' + DD : DD;
-								}
-							}
-						} else if (this.dayRule == 'workDay') {
-							// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
-							if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-								while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-									DD--;
-									thisDD = DD < 10 ? '0' + DD : DD;
-								}
-							}
-							// 获取达到条件的日期是星期X
-							let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
-							// 当星期日时
-							if (thisWeek == 1) {
-								// 先找下一个日,并判断是否为月底
-								DD++;
-								thisDD = DD < 10 ? '0' + DD : DD;
-								// 判断下一日已经不是合法日期
-								if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-									DD -= 3;
-								}
-							} else if (thisWeek == 7) {
-								// 当星期6时只需判断不是1号就可进行操作
-								if (this.dayRuleSup !== 1) {
-									DD--;
-								} else {
-									DD += 2;
-								}
-							}
-						} else if (this.dayRule == 'weekDay') {
-							// 如果指定了是星期几
-							// 获取当前日期是属于星期几
-							let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
-							// 校验当前星期是否在星期池(dayRuleSup)中
-							if (this.dayRuleSup.indexOf(thisWeek) < 0) {
-								// 如果到达最大值时
-								if (Di == DDate.length - 1) {
-									resetDay();
-									if (Mi == MDate.length - 1) {
-										resetMonth();
-										continue goYear;
-									}
-									continue goMonth;
-								}
-								continue;
-							}
-						} else if (this.dayRule == 'assWeek') {
-							// 如果指定了是第几周的星期几
-							// 获取每月1号是属于星期几
-							let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
-							if (this.dayRuleSup[1] >= thisWeek) {
-								DD = (this.dayRuleSup[0] - 1) * 7 + this.dayRuleSup[1] - thisWeek + 1;
-							} else {
-								DD = this.dayRuleSup[0] * 7 + this.dayRuleSup[1] - thisWeek + 1;
-							}
-						} else if (this.dayRule == 'lastWeek') {
-							// 如果指定了每月最后一个星期几
-							// 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
-							if (this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-								while (DD > 0 && this.checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
-									DD--;
-									thisDD = DD < 10 ? '0' + DD : DD;
-								}
-							}
-							// 获取月末最后一天是星期几
-							let thisWeek = this.formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
-							// 找到要求中最近的那个星期几
-							if (this.dayRuleSup < thisWeek) {
-								DD -= thisWeek - this.dayRuleSup;
-							} else if (this.dayRuleSup > thisWeek) {
-								DD -= 7 - (this.dayRuleSup - thisWeek)
-							}
-						}
-						// 判断时间值是否小于10置换成“05”这种格式
-						DD = DD < 10 ? '0' + DD : DD;
-
-						// 循环“时”数组
-						goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
-							let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
-
-							// 如果到达最大值时
-							if (nMin > mDate[mDate.length - 1]) {
-								resetMin();
-								if (hi == hDate.length - 1) {
-									resetHour();
-									if (Di == DDate.length - 1) {
-										resetDay();
-										if (Mi == MDate.length - 1) {
-											resetMonth();
-											continue goYear;
-										}
-										continue goMonth;
-									}
-									continue goDay;
-								}
-								continue;
-							}
-							// 循环"分"数组
-							goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
-								let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi];
-
-								// 如果到达最大值时
-								if (nSecond > sDate[sDate.length - 1]) {
-									resetSecond();
-									if (mi == mDate.length - 1) {
-										resetMin();
-										if (hi == hDate.length - 1) {
-											resetHour();
-											if (Di == DDate.length - 1) {
-												resetDay();
-												if (Mi == MDate.length - 1) {
-													resetMonth();
-													continue goYear;
-												}
-												continue goMonth;
-											}
-											continue goDay;
-										}
-										continue goHour;
-									}
-									continue;
-								}
-								// 循环"秒"数组
-								goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
-									let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si];
-									// 添加当前时间(时间合法性在日期循环时已经判断)
-									if (MM !== '00' && DD !== '00') {
-										resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
-										nums++;
-									}
-									// 如果条数满了就退出循环
-									if (nums == 5) break goYear;
-									// 如果到达最大值时
-									if (si == sDate.length - 1) {
-										resetSecond();
-										if (mi == mDate.length - 1) {
-											resetMin();
-											if (hi == hDate.length - 1) {
-												resetHour();
-												if (Di == DDate.length - 1) {
-													resetDay();
-													if (Mi == MDate.length - 1) {
-														resetMonth();
-														continue goYear;
-													}
-													continue goMonth;
-												}
-												continue goDay;
-											}
-											continue goHour;
-										}
-										continue goMin;
-									}
-								} //goSecond
-							} //goMin
-						}//goHour
-					}//goDay
-				}//goMonth
-			}
-			// 判断100年内的结果条数
-			if (resultArr.length == 0) {
-				this.resultList = ['没有达到条件的结果!'];
-			} else {
-				this.resultList = resultArr;
-				if (resultArr.length !== 5) {
-					this.resultList.push('最近100年内只有上面' + resultArr.length + '条结果!')
-				}
-			}
-			// 计算完成-显示结果
-			this.isShow = true;
-
-
-		},
-		// 用于计算某位数字在数组中的索引
-		getIndex(arr, value) {
-			if (value <= arr[0] || value > arr[arr.length - 1]) {
-				return 0;
-			} else {
-				for (let i = 0; i < arr.length - 1; i++) {
-					if (value > arr[i] && value <= arr[i + 1]) {
-						return i + 1;
-					}
-				}
-			}
-		},
-		// 获取"年"数组
-		getYearArr(rule, year) {
-			this.dateArr[5] = this.getOrderArr(year, year + 100);
-			if (rule !== undefined) {
-				if (rule.indexOf('-') >= 0) {
-					this.dateArr[5] = this.getCycleArr(rule, year + 100, false)
-				} else if (rule.indexOf('/') >= 0) {
-					this.dateArr[5] = this.getAverageArr(rule, year + 100)
-				} else if (rule !== '*') {
-					this.dateArr[5] = this.getAssignArr(rule)
-				}
-			}
-		},
-		// 获取"月"数组
-		getMonthArr(rule) {
-			this.dateArr[4] = this.getOrderArr(1, 12);
-			if (rule.indexOf('-') >= 0) {
-				this.dateArr[4] = this.getCycleArr(rule, 12, false)
-			} else if (rule.indexOf('/') >= 0) {
-				this.dateArr[4] = this.getAverageArr(rule, 12)
-			} else if (rule !== '*') {
-				this.dateArr[4] = this.getAssignArr(rule)
-			}
-		},
-		// 获取"日"数组-主要为日期规则
-		getWeekArr(rule) {
-			// 只有当日期规则的两个值均为“”时则表达日期是有选项的
-			if (this.dayRule == '' && this.dayRuleSup == '') {
-				if (rule.indexOf('-') >= 0) {
-					this.dayRule = 'weekDay';
-					this.dayRuleSup = this.getCycleArr(rule, 7, false)
-				} else if (rule.indexOf('#') >= 0) {
-					this.dayRule = 'assWeek';
-					let matchRule = rule.match(/[0-9]{1}/g);
-					this.dayRuleSup = [Number(matchRule[1]), Number(matchRule[0])];
-					this.dateArr[3] = [1];
-					if (this.dayRuleSup[1] == 7) {
-						this.dayRuleSup[1] = 0;
-					}
-				} else if (rule.indexOf('L') >= 0) {
-					this.dayRule = 'lastWeek';
-					this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
-					this.dateArr[3] = [31];
-					if (this.dayRuleSup == 7) {
-						this.dayRuleSup = 0;
-					}
-				} else if (rule !== '*' && rule !== '?') {
-					this.dayRule = 'weekDay';
-					this.dayRuleSup = this.getAssignArr(rule)
-				}
-			}
-		},
-		// 获取"日"数组-少量为日期规则
-		getDayArr(rule) {
-			this.dateArr[3] = this.getOrderArr(1, 31);
-			this.dayRule = '';
-			this.dayRuleSup = '';
-			if (rule.indexOf('-') >= 0) {
-				this.dateArr[3] = this.getCycleArr(rule, 31, false)
-				this.dayRuleSup = 'null';
-			} else if (rule.indexOf('/') >= 0) {
-				this.dateArr[3] = this.getAverageArr(rule, 31)
-				this.dayRuleSup = 'null';
-			} else if (rule.indexOf('W') >= 0) {
-				this.dayRule = 'workDay';
-				this.dayRuleSup = Number(rule.match(/[0-9]{1,2}/g)[0]);
-				this.dateArr[3] = [this.dayRuleSup];
-			} else if (rule.indexOf('L') >= 0) {
-				this.dayRule = 'lastDay';
-				this.dayRuleSup = 'null';
-				this.dateArr[3] = [31];
-			} else if (rule !== '*' && rule !== '?') {
-				this.dateArr[3] = this.getAssignArr(rule)
-				this.dayRuleSup = 'null';
-			} else if (rule == '*') {
-				this.dayRuleSup = 'null';
-			}
-		},
-		// 获取"时"数组
-		getHourArr(rule) {
-			this.dateArr[2] = this.getOrderArr(0, 23);
-			if (rule.indexOf('-') >= 0) {
-				this.dateArr[2] = this.getCycleArr(rule, 24, true)
-			} else if (rule.indexOf('/') >= 0) {
-				this.dateArr[2] = this.getAverageArr(rule, 23)
-			} else if (rule !== '*') {
-				this.dateArr[2] = this.getAssignArr(rule)
-			}
-		},
-		// 获取"分"数组
-		getMinArr(rule) {
-			this.dateArr[1] = this.getOrderArr(0, 59);
-			if (rule.indexOf('-') >= 0) {
-				this.dateArr[1] = this.getCycleArr(rule, 60, true)
-			} else if (rule.indexOf('/') >= 0) {
-				this.dateArr[1] = this.getAverageArr(rule, 59)
-			} else if (rule !== '*') {
-				this.dateArr[1] = this.getAssignArr(rule)
-			}
-		},
-		// 获取"秒"数组
-		getSecondArr(rule) {
-			this.dateArr[0] = this.getOrderArr(0, 59);
-			if (rule.indexOf('-') >= 0) {
-				this.dateArr[0] = this.getCycleArr(rule, 60, true)
-			} else if (rule.indexOf('/') >= 0) {
-				this.dateArr[0] = this.getAverageArr(rule, 59)
-			} else if (rule !== '*') {
-				this.dateArr[0] = this.getAssignArr(rule)
-			}
-		},
-		// 根据传进来的min-max返回一个顺序的数组
-		getOrderArr(min, max) {
-			let arr = [];
-			for (let i = min; i <= max; i++) {
-				arr.push(i);
-			}
-			return arr;
-		},
-		// 根据规则中指定的零散值返回一个数组
-		getAssignArr(rule) {
-			let arr = [];
-			let assiginArr = rule.split(',');
-			for (let i = 0; i < assiginArr.length; i++) {
-				arr[i] = Number(assiginArr[i])
-			}
-			arr.sort(this.compare)
-			return arr;
-		},
-		// 根据一定算术规则计算返回一个数组
-		getAverageArr(rule, limit) {
-			let arr = [];
-			let agArr = rule.split('/');
-			let min = Number(agArr[0]);
-			let step = Number(agArr[1]);
-			while (min <= limit) {
-				arr.push(min);
-				min += step;
-			}
-			return arr;
-		},
-		// 根据规则返回一个具有周期性的数组
-		getCycleArr(rule, limit, status) {
-			// status--表示是否从0开始(则从1开始)
-			let arr = [];
-			let cycleArr = rule.split('-');
-			let min = Number(cycleArr[0]);
-			let max = Number(cycleArr[1]);
-			if (min > max) {
-				max += limit;
-			}
-			for (let i = min; i <= max; i++) {
-				let add = 0;
-				if (status == false && i % limit == 0) {
-					add = limit;
-				}
-				arr.push(Math.round(i % limit + add))
-			}
-			arr.sort(this.compare)
-			return arr;
-		},
-		// 比较数字大小(用于Array.sort)
-		compare(value1, value2) {
-			if (value2 - value1 > 0) {
-				return -1;
-			} else {
-				return 1;
-			}
-		},
-		// 格式化日期格式如:2017-9-19 18:04:33
-		formatDate(value, type) {
-			// 计算日期相关值
-			let time = typeof value == 'number' ? new Date(value) : value;
-			let Y = time.getFullYear();
-			let M = time.getMonth() + 1;
-			let D = time.getDate();
-			let h = time.getHours();
-			let m = time.getMinutes();
-			let s = time.getSeconds();
-			let week = time.getDay();
-			// 如果传递了type的话
-			if (type == undefined) {
-				return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
-			} else if (type == 'week') {
-				// 在quartz中 1为星期日
-				return week + 1;
-			}
-		},
-		// 检查日期是否存在
-		checkDate(value) {
-			let time = new Date(value);
-			let format = this.formatDate(time)
-			return value === format;
-		}
-	},
-	watch: {
-		'ex': 'expressionChange'
-	},
-	props: ['ex'],
-	mounted: function () {
-		// 初始化 获取一次结果
-		this.expressionChange();
-	}
+<script setup>
+const props = defineProps({
+    ex: {
+        type: String,
+        default: ''
+    }
+})
+const dayRule = ref('')
+const dayRuleSup = ref('')
+const dateArr = ref([])
+const resultList = ref([])
+const isShow = ref(false)
+watch(() => props.ex, () => expressionChange())
+// 表达式值变化时,开始去计算结果
+function expressionChange() {
+    // 计算开始-隐藏结果
+    isShow.value = false;
+    // 获取规则数组[0秒、1分、2时、3日、4月、5星期、6年]
+    let ruleArr = props.ex.split(' ');
+    // 用于记录进入循环的次数
+    let nums = 0;
+    // 用于暂时存符号时间规则结果的数组
+    let resultArr = [];
+    // 获取当前时间精确至[年、月、日、时、分、秒]
+    let nTime = new Date();
+    let nYear = nTime.getFullYear();
+    let nMonth = nTime.getMonth() + 1;
+    let nDay = nTime.getDate();
+    let nHour = nTime.getHours();
+    let nMin = nTime.getMinutes();
+    let nSecond = nTime.getSeconds();
+    // 根据规则获取到近100年可能年数组、月数组等等
+    getSecondArr(ruleArr[0]);
+    getMinArr(ruleArr[1]);
+    getHourArr(ruleArr[2]);
+    getDayArr(ruleArr[3]);
+    getMonthArr(ruleArr[4]);
+    getWeekArr(ruleArr[5]);
+    getYearArr(ruleArr[6], nYear);
+    // 将获取到的数组赋值-方便使用
+    let sDate = dateArr.value[0];
+    let mDate = dateArr.value[1];
+    let hDate = dateArr.value[2];
+    let DDate = dateArr.value[3];
+    let MDate = dateArr.value[4];
+    let YDate = dateArr.value[5];
+    // 获取当前时间在数组中的索引
+    let sIdx = getIndex(sDate, nSecond);
+    let mIdx = getIndex(mDate, nMin);
+    let hIdx = getIndex(hDate, nHour);
+    let DIdx = getIndex(DDate, nDay);
+    let MIdx = getIndex(MDate, nMonth);
+    let YIdx = getIndex(YDate, nYear);
+    // 重置月日时分秒的函数(后面用的比较多)
+    const resetSecond = function () {
+        sIdx = 0;
+        nSecond = sDate[sIdx]
+    }
+    const resetMin = function () {
+        mIdx = 0;
+        nMin = mDate[mIdx]
+        resetSecond();
+    }
+    const resetHour = function () {
+        hIdx = 0;
+        nHour = hDate[hIdx]
+        resetMin();
+    }
+    const resetDay = function () {
+        DIdx = 0;
+        nDay = DDate[DIdx]
+        resetHour();
+    }
+    const resetMonth = function () {
+        MIdx = 0;
+        nMonth = MDate[MIdx]
+        resetDay();
+    }
+    // 如果当前年份不为数组中当前值
+    if (nYear !== YDate[YIdx]) {
+        resetMonth();
+    }
+    // 如果当前月份不为数组中当前值
+    if (nMonth !== MDate[MIdx]) {
+        resetDay();
+    }
+    // 如果当前“日”不为数组中当前值
+    if (nDay !== DDate[DIdx]) {
+        resetHour();
+    }
+    // 如果当前“时”不为数组中当前值
+    if (nHour !== hDate[hIdx]) {
+        resetMin();
+    }
+    // 如果当前“分”不为数组中当前值
+    if (nMin !== mDate[mIdx]) {
+        resetSecond();
+    }
+    // 循环年份数组
+    goYear: for (let Yi = YIdx; Yi < YDate.length; Yi++) {
+        let YY = YDate[Yi];
+        // 如果到达最大值时
+        if (nMonth > MDate[MDate.length - 1]) {
+            resetMonth();
+            continue;
+        }
+        // 循环月份数组
+        goMonth: for (let Mi = MIdx; Mi < MDate.length; Mi++) {
+            // 赋值、方便后面运算
+            let MM = MDate[Mi];
+            MM = MM < 10 ? '0' + MM : MM;
+            // 如果到达最大值时
+            if (nDay > DDate[DDate.length - 1]) {
+                resetDay();
+                if (Mi === MDate.length - 1) {
+                    resetMonth();
+                    continue goYear;
+                }
+                continue;
+            }
+            // 循环日期数组
+            goDay: for (let Di = DIdx; Di < DDate.length; Di++) {
+                // 赋值、方便后面运算
+                let DD = DDate[Di];
+                let thisDD = DD < 10 ? '0' + DD : DD;
+                // 如果到达最大值时
+                if (nHour > hDate[hDate.length - 1]) {
+                    resetHour();
+                    if (Di === DDate.length - 1) {
+                        resetDay();
+                        if (Mi === MDate.length - 1) {
+                            resetMonth();
+                            continue goYear;
+                        }
+                        continue goMonth;
+                    }
+                    continue;
+                }
+                // 判断日期的合法性,不合法的话也是跳出当前循环
+                if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true && dayRule.value !== 'workDay' && dayRule.value !== 'lastWeek' && dayRule.value !== 'lastDay') {
+                    resetDay();
+                    continue goMonth;
+                }
+                // 如果日期规则中有值时
+                if (dayRule.value === 'lastDay') {
+                    // 如果不是合法日期则需要将前将日期调到合法日期即月末最后一天
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                            DD--;
+                            thisDD = DD < 10 ? '0' + DD : DD;
+                        }
+                    }
+                } else if (dayRule.value === 'workDay') {
+                    // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                            DD--;
+                            thisDD = DD < 10 ? '0' + DD : DD;
+                        }
+                    }
+                    // 获取达到条件的日期是星期X
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
+                    // 当星期日时
+                    if (thisWeek === 1) {
+                        // 先找下一个日,并判断是否为月底
+                        DD++;
+                        thisDD = DD < 10 ? '0' + DD : DD;
+                        // 判断下一日已经不是合法日期
+                        if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                            DD -= 3;
+                        }
+                    } else if (thisWeek === 7) {
+                        // 当星期6时只需判断不是1号就可进行操作
+                        if (dayRuleSup.value !== 1) {
+                            DD--;
+                        } else {
+                            DD += 2;
+                        }
+                    }
+                } else if (dayRule.value === 'weekDay') {
+                    // 如果指定了是星期几
+                    // 获取当前日期是属于星期几
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
+                    // 校验当前星期是否在星期池(dayRuleSup)中
+                    if (dayRuleSup.value.indexOf(thisWeek) < 0) {
+                        // 如果到达最大值时
+                        if (Di === DDate.length - 1) {
+                            resetDay();
+                            if (Mi === MDate.length - 1) {
+                                resetMonth();
+                                continue goYear;
+                            }
+                            continue goMonth;
+                        }
+                        continue;
+                    }
+                } else if (dayRule.value === 'assWeek') {
+                    // 如果指定了是第几周的星期几
+                    // 获取每月1号是属于星期几
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + DD + ' 00:00:00'), 'week');
+                    if (dayRuleSup.value[1] >= thisWeek) {
+                        DD = (dayRuleSup.value[0] - 1) * 7 + dayRuleSup.value[1] - thisWeek + 1;
+                    } else {
+                        DD = dayRuleSup.value[0] * 7 + dayRuleSup.value[1] - thisWeek + 1;
+                    }
+                } else if (dayRule.value === 'lastWeek') {
+                    // 如果指定了每月最后一个星期几
+                    // 校验并调整如果是2月30号这种日期传进来时需调整至正常月底
+                    if (checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                        while (DD > 0 && checkDate(YY + '-' + MM + '-' + thisDD + ' 00:00:00') !== true) {
+                            DD--;
+                            thisDD = DD < 10 ? '0' + DD : DD;
+                        }
+                    }
+                    // 获取月末最后一天是星期几
+                    let thisWeek = formatDate(new Date(YY + '-' + MM + '-' + thisDD + ' 00:00:00'), 'week');
+                    // 找到要求中最近的那个星期几
+                    if (dayRuleSup.value < thisWeek) {
+                        DD -= thisWeek - dayRuleSup.value;
+                    } else if (dayRuleSup.value > thisWeek) {
+                        DD -= 7 - (dayRuleSup.value - thisWeek)
+                    }
+                }
+                // 判断时间值是否小于10置换成“05”这种格式
+                DD = DD < 10 ? '0' + DD : DD;
+                // 循环“时”数组
+                goHour: for (let hi = hIdx; hi < hDate.length; hi++) {
+                    let hh = hDate[hi] < 10 ? '0' + hDate[hi] : hDate[hi]
+                    // 如果到达最大值时
+                    if (nMin > mDate[mDate.length - 1]) {
+                        resetMin();
+                        if (hi === hDate.length - 1) {
+                            resetHour();
+                            if (Di === DDate.length - 1) {
+                                resetDay();
+                                if (Mi === MDate.length - 1) {
+                                    resetMonth();
+                                    continue goYear;
+                                }
+                                continue goMonth;
+                            }
+                            continue goDay;
+                        }
+                        continue;
+                    }
+                    // 循环"分"数组
+                    goMin: for (let mi = mIdx; mi < mDate.length; mi++) {
+                        let mm = mDate[mi] < 10 ? '0' + mDate[mi] : mDate[mi];
+                        // 如果到达最大值时
+                        if (nSecond > sDate[sDate.length - 1]) {
+                            resetSecond();
+                            if (mi === mDate.length - 1) {
+                                resetMin();
+                                if (hi === hDate.length - 1) {
+                                    resetHour();
+                                    if (Di === DDate.length - 1) {
+                                        resetDay();
+                                        if (Mi === MDate.length - 1) {
+                                            resetMonth();
+                                            continue goYear;
+                                        }
+                                        continue goMonth;
+                                    }
+                                    continue goDay;
+                                }
+                                continue goHour;
+                            }
+                            continue;
+                        }
+                        // 循环"秒"数组
+                        goSecond: for (let si = sIdx; si <= sDate.length - 1; si++) {
+                            let ss = sDate[si] < 10 ? '0' + sDate[si] : sDate[si];
+                            // 添加当前时间(时间合法性在日期循环时已经判断)
+                            if (MM !== '00' && DD !== '00') {
+                                resultArr.push(YY + '-' + MM + '-' + DD + ' ' + hh + ':' + mm + ':' + ss)
+                                nums++;
+                            }
+                            // 如果条数满了就退出循环
+                            if (nums === 5) break goYear;
+                            // 如果到达最大值时
+                            if (si === sDate.length - 1) {
+                                resetSecond();
+                                if (mi === mDate.length - 1) {
+                                    resetMin();
+                                    if (hi === hDate.length - 1) {
+                                        resetHour();
+                                        if (Di === DDate.length - 1) {
+                                            resetDay();
+                                            if (Mi === MDate.length - 1) {
+                                                resetMonth();
+                                                continue goYear;
+                                            }
+                                            continue goMonth;
+                                        }
+                                        continue goDay;
+                                    }
+                                    continue goHour;
+                                }
+                                continue goMin;
+                            }
+                        } //goSecond
+                    } //goMin
+                }//goHour
+            }//goDay
+        }//goMonth
+    }
+    // 判断100年内的结果条数
+    if (resultArr.length === 0) {
+        resultList.value = ['没有达到条件的结果!'];
+    } else {
+        resultList.value = resultArr;
+        if (resultArr.length !== 5) {
+            resultList.value.push('最近100年内只有上面' + resultArr.length + '条结果!')
+        }
+    }
+    // 计算完成-显示结果
+    isShow.value = true;
 }
-
-</script>
+// 用于计算某位数字在数组中的索引
+function getIndex(arr, value) {
+    if (value <= arr[0] || value > arr[arr.length - 1]) {
+        return 0;
+    } else {
+        for (let i = 0; i < arr.length - 1; i++) {
+            if (value > arr[i] && value <= arr[i + 1]) {
+                return i + 1;
+            }
+        }
+    }
+}
+// 获取"年"数组
+function getYearArr(rule, year) {
+    dateArr.value[5] = getOrderArr(year, year + 100);
+    if (rule !== undefined) {
+        if (rule.indexOf('-') >= 0) {
+            dateArr.value[5] = getCycleArr(rule, year + 100, false)
+        } else if (rule.indexOf('/') >= 0) {
+            dateArr.value[5] = getAverageArr(rule, year + 100)
+        } else if (rule !== '*') {
+            dateArr.value[5] = getAssignArr(rule)
+        }
+    }
+}
+// 获取"月"数组
+function getMonthArr(rule) {
+    dateArr.value[4] = getOrderArr(1, 12);
+    if (rule.indexOf('-') >= 0) {
+        dateArr.value[4] = getCycleArr(rule, 12, false)
+    } else if (rule.indexOf('/') >= 0) {
+        dateArr.value[4] = getAverageArr(rule, 12)
+    } else if (rule !== '*') {
+        dateArr.value[4] = getAssignArr(rule)
+    }
+}
+// 获取"日"数组-主要为日期规则
+function getWeekArr(rule) {
+    // 只有当日期规则的两个值均为“”时则表达日期是有选项的
+    if (dayRule.value === '' && dayRuleSup.value === '') {
+        if (rule.indexOf('-') >= 0) {
+            dayRule.value = 'weekDay';
+            dayRuleSup.value = getCycleArr(rule, 7, false)
+        } else if (rule.indexOf('#') >= 0) {
+            dayRule.value = 'assWeek';
+            let matchRule = rule.match(/[0-9]{1}/g);
+            dayRuleSup.value = [Number(matchRule[1]), Number(matchRule[0])];
+            dateArr.value[3] = [1];
+            if (dayRuleSup.value[1] === 7) {
+                dayRuleSup.value[1] = 0;
+            }
+        } else if (rule.indexOf('L') >= 0) {
+            dayRule.value = 'lastWeek';
+            dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
+            dateArr.value[3] = [31];
+            if (dayRuleSup.value === 7) {
+                dayRuleSup.value = 0;
+            }
+        } else if (rule !== '*' && rule !== '?') {
+            dayRule.value = 'weekDay';
+            dayRuleSup.value = getAssignArr(rule)
+        }
+    }
+}
+// 获取"日"数组-少量为日期规则
+function getDayArr(rule) {
+    dateArr.value[3] = getOrderArr(1, 31);
+    dayRule.value = '';
+    dayRuleSup.value = '';
+    if (rule.indexOf('-') >= 0) {
+        dateArr.value[3] = getCycleArr(rule, 31, false)
+        dayRuleSup.value = 'null';
+    } else if (rule.indexOf('/') >= 0) {
+        dateArr.value[3] = getAverageArr(rule, 31)
+        dayRuleSup.value = 'null';
+    } else if (rule.indexOf('W') >= 0) {
+        dayRule.value = 'workDay';
+        dayRuleSup.value = Number(rule.match(/[0-9]{1,2}/g)[0]);
+        dateArr.value[3] = [dayRuleSup.value];
+    } else if (rule.indexOf('L') >= 0) {
+        dayRule.value = 'lastDay';
+        dayRuleSup.value = 'null';
+        dateArr.value[3] = [31];
+    } else if (rule !== '*' && rule !== '?') {
+        dateArr.value[3] = getAssignArr(rule)
+        dayRuleSup.value = 'null';
+    } else if (rule === '*') {
+        dayRuleSup.value = 'null';
+    }
+}
+// 获取"时"数组
+function getHourArr(rule) {
+    dateArr.value[2] = getOrderArr(0, 23);
+    if (rule.indexOf('-') >= 0) {
+        dateArr.value[2] = getCycleArr(rule, 24, true)
+    } else if (rule.indexOf('/') >= 0) {
+        dateArr.value[2] = getAverageArr(rule, 23)
+    } else if (rule !== '*') {
+        dateArr.value[2] = getAssignArr(rule)
+    }
+}
+// 获取"分"数组
+function getMinArr(rule) {
+    dateArr.value[1] = getOrderArr(0, 59);
+    if (rule.indexOf('-') >= 0) {
+        dateArr.value[1] = getCycleArr(rule, 60, true)
+    } else if (rule.indexOf('/') >= 0) {
+        dateArr.value[1] = getAverageArr(rule, 59)
+    } else if (rule !== '*') {
+        dateArr.value[1] = getAssignArr(rule)
+    }
+}
+// 获取"秒"数组
+function getSecondArr(rule) {
+    dateArr.value[0] = getOrderArr(0, 59);
+    if (rule.indexOf('-') >= 0) {
+        dateArr.value[0] = getCycleArr(rule, 60, true)
+    } else if (rule.indexOf('/') >= 0) {
+        dateArr.value[0] = getAverageArr(rule, 59)
+    } else if (rule !== '*') {
+        dateArr.value[0] = getAssignArr(rule)
+    }
+}
+// 根据传进来的min-max返回一个顺序的数组
+function getOrderArr(min, max) {
+    let arr = [];
+    for (let i = min; i <= max; i++) {
+        arr.push(i);
+    }
+    return arr;
+}
+// 根据规则中指定的零散值返回一个数组
+function getAssignArr(rule) {
+    let arr = [];
+    let assiginArr = rule.split(',');
+    for (let i = 0; i < assiginArr.length; i++) {
+        arr[i] = Number(assiginArr[i])
+    }
+    arr.sort(compare)
+    return arr;
+}
+// 根据一定算术规则计算返回一个数组
+function getAverageArr(rule, limit) {
+    let arr = [];
+    let agArr = rule.split('/');
+    let min = Number(agArr[0]);
+    let step = Number(agArr[1]);
+    while (min <= limit) {
+        arr.push(min);
+        min += step;
+    }
+    return arr;
+}
+// 根据规则返回一个具有周期性的数组
+function getCycleArr(rule, limit, status) {
+    // status--表示是否从0开始(则从1开始)
+    let arr = [];
+    let cycleArr = rule.split('-');
+    let min = Number(cycleArr[0]);
+    let max = Number(cycleArr[1]);
+    if (min > max) {
+        max += limit;
+    }
+    for (let i = min; i <= max; i++) {
+        let add = 0;
+        if (status === false && i % limit === 0) {
+            add = limit;
+        }
+        arr.push(Math.round(i % limit + add))
+    }
+    arr.sort(compare)
+    return arr;
+}
+// 比较数字大小(用于Array.sort)
+function compare(value1, value2) {
+    if (value2 - value1 > 0) {
+        return -1;
+    } else {
+        return 1;
+    }
+}
+// 格式化日期格式如:2017-9-19 18:04:33
+function formatDate(value, type) {
+    // 计算日期相关值
+    let time = typeof value == 'number' ? new Date(value) : value;
+    let Y = time.getFullYear();
+    let M = time.getMonth() + 1;
+    let D = time.getDate();
+    let h = time.getHours();
+    let m = time.getMinutes();
+    let s = time.getSeconds();
+    let week = time.getDay();
+    // 如果传递了type的话
+    if (type === undefined) {
+        return Y + '-' + (M < 10 ? '0' + M : M) + '-' + (D < 10 ? '0' + D : D) + ' ' + (h < 10 ? '0' + h : h) + ':' + (m < 10 ? '0' + m : m) + ':' + (s < 10 ? '0' + s : s);
+    } else if (type === 'week') {
+        // 在quartz中 1为星期日
+        return week + 1;
+    }
+}
+// 检查日期是否存在
+function checkDate(value) {
+    let time = new Date(value);
+    let format = formatDate(time)
+    return value === format;
+}
+onMounted(() => {
+    expressionChange()
+})
+</script>

+ 120 - 109
canteen-ui/src/components/Crontab/second.vue

@@ -1,117 +1,128 @@
 <template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				秒,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                秒,允许的通配符[, - * /]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				周期从
-				<el-input-number v-model='cycle01' :min="0" :max="58" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : 1" :max="59" /> 秒
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                周期从
+                <el-input-number v-model='cycle01' :min="0" :max="58" /> -
+                <el-input-number v-model='cycle02' :min="cycle01 + 1" :max="59" /> 秒
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				
-				<el-input-number v-model='average01' :min="0" :max="58" /> 秒开始,每
-				<el-input-number v-model='average02' :min="1" :max="59 - average01 || 0" /> 秒执行一次
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                
+                <el-input-number v-model='average01' :min="0" :max="58" /> 秒开始,每
+                <el-input-number v-model='average02' :min="1" :max="59 - average01" /> 秒执行一次
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="10">
+                    <el-option v-for="item in 60" :key="item" :label="item - 1" :value="item - 1" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 1,
-			cycle01: 1,
-			cycle02: 2,
-			average01: 0,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-second',
-	props: ['check', 'radioParent'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'second', '*', 'second');
-					break;
-				case 2:
-					this.$emit('update', 'second', this.cycleTotal);
-					break;
-				case 3:
-					this.$emit('update', 'second', this.averageTotal);
-					break;
-				case 4:
-					this.$emit('update', 'second', this.checkboxString);
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '2') {
-				this.$emit('update', 'second', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'second', this.averageTotal);
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'second', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange',
-		radioParent() {
-			this.radioValue = this.radioParent
-		}
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, 0, 58)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : 1, 59)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, 0, 58)
-			const average02 = this.checkNum(this.average02, 1, 59 - average01 || 0)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: "",
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(1)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([0])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 0, 58)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 59)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 0, 58)
+    average02.value = props.check(average02.value, 1, 59 - average01.value)
+    return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.second, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === '*') {
+        radioValue.value = 1
+    } else if (value.indexOf('-') > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 2
+    } else if (value.indexOf('/') > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[0])
+        average02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 4
+    }
+}
+// 单选按钮值变化时
+function onRadioChange() {
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'second', '*', 'second')
+            break
+        case 2:
+            emit('update', 'second', cycleTotal.value, 'second')
+            break
+        case 3:
+            emit('update', 'second', averageTotal.value, 'second')
+            break
+        case 4:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'second', checkboxString.value, 'second')
+            break
+    }
 }
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 18.8rem;
+}
+</style>

+ 186 - 191
canteen-ui/src/components/Crontab/week.vue

@@ -1,202 +1,197 @@
 <template>
-	<el-form size='small'>
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="1">
-				周,允许的通配符[, - * ? / L #]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="1">
+                周,允许的通配符[, - * ? / L #]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="2">
-				不指定
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="2">
+                不指定
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="3">
-				周期从星期
-				<el-select clearable v-model="cycle01">
-					<el-option
-						v-for="(item,index) of weekList"
-						:key="index"
-						:label="item.value"
-						:value="item.key"
-						:disabled="item.key === 1"
-					>{{item.value}}</el-option>
-				</el-select>
-				-
-				<el-select clearable v-model="cycle02">
-					<el-option
-						v-for="(item,index) of weekList"
-						:key="index"
-						:label="item.value"
-						:value="item.key"
-						:disabled="item.key < cycle01 && item.key !== 1"
-					>{{item.value}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="3">
+                周期从
+                <el-select clearable v-model="cycle01">
+                    <el-option
+                        v-for="(item,index) of weekList"
+                        :key="index"
+                        :label="item.value"
+                        :value="item.key"
+                        :disabled="item.key === 7"
+                    >{{item.value}}</el-option>
+                </el-select>
+                -
+                <el-select clearable v-model="cycle02">
+                    <el-option
+                        v-for="(item,index) of weekList"
+                        :key="index"
+                        :label="item.value"
+                        :value="item.key"
+                        :disabled="item.key <= cycle01"
+                    >{{item.value}}</el-option>
+                </el-select>
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="4">
-				
-				<el-input-number v-model='average01' :min="1" :max="4" /> 周的星期
-				<el-select clearable v-model="average02">
-					<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="4">
+                
+                <el-input-number v-model='average01' :min="1" :max="4" /> 周的
+                <el-select clearable v-model="average02">
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="5">
-				本月最后一个星期
-				<el-select clearable v-model="weekday">
-					<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="item.key">{{item.value}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="5">
+                本月最后一个
+                <el-select clearable v-model="weekday">
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio v-model='radioValue' :label="6">
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
-					<el-option v-for="(item,index) of weekList" :key="index" :label="item.value" :value="String(item.key)">{{item.value}}</el-option>
-				</el-select>
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio v-model='radioValue' :value="6">
+                指定
+                <el-select class="multiselect" clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="6">
+                    <el-option v-for="item in weekList" :key="item.key" :label="item.value" :value="item.key" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
 
-	</el-form>
+    </el-form>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			radioValue: 2,
-			weekday: 2,
-			cycle01: 2,
-			cycle02: 3,
-			average01: 1,
-			average02: 2,
-			checkboxList: [],
-			weekList: [
-				{
-					key: 2,
-					value: '星期一'
-				},
-				{
-					key: 3,
-					value: '星期二'
-				},
-				{
-					key: 4,
-					value: '星期三'
-				},
-				{
-					key: 5,
-					value: '星期四'
-				},
-				{
-					key: 6,
-					value: '星期五'
-				},
-				{
-					key: 7,
-					value: '星期六'
-				},
-				{
-					key: 1,
-					value: '星期日'
-				}
-			],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-week',
-	props: ['check', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			if (this.radioValue !== 2 && this.cron.day !== '?') {
-				this.$emit('update', 'day', '?', 'week');
-			}
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'week', '*');
-					break;
-				case 2:
-					this.$emit('update', 'week', '?');
-					break;
-				case 3:
-					this.$emit('update', 'week', this.cycleTotal);
-					break;
-				case 4:
-					this.$emit('update', 'week', this.averageTotal);
-					break;
-				case 5:
-					this.$emit('update', 'week', this.weekdayCheck + 'L');
-					break;
-				case 6:
-					this.$emit('update', 'week', this.checkboxString);
-					break;
-			}
-		},
-
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'week', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'week', this.averageTotal);
-			}
-		},
-		// 最近工作日值变化时
-		weekdayChange() {
-			if (this.radioValue == '5') {
-				this.$emit('update', 'week', this.weekday + 'L');
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '6') {
-				this.$emit('update', 'week', this.checkboxString);
-			}
-		},
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'weekdayCheck': 'weekdayChange',
-		'checkboxString': 'checkboxChange',
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			this.cycle01 = this.checkNum(this.cycle01, 1, 7)
-			this.cycle02 = this.checkNum(this.cycle02, 1, 7)
-			return this.cycle01 + '-' + this.cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			this.average01 = this.checkNum(this.average01, 1, 4)
-			this.average02 = this.checkNum(this.average02, 1, 7)
-			return this.average02 + '#' + this.average01;
-		},
-		// 最近的工作日(格式)
-		weekdayCheck: function () {
-			this.weekday = this.checkNum(this.weekday, 1, 7)
-			return this.weekday;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str == '' ? '*' : str;
-		}
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: ""
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const radioValue = ref(2)
+const cycle01 = ref(2)
+const cycle02 = ref(3)
+const average01 = ref(1)
+const average02 = ref(2)
+const weekday = ref(2)
+const checkboxList = ref([])
+const checkCopy = ref([2])
+const weekList = ref([
+    {key: 1, value: '星期日'},
+    {key: 2, value: '星期一'},
+    {key: 3, value: '星期二'},
+    {key: 4, value: '星期三'},
+    {key: 5, value: '星期四'},
+    {key: 6, value: '星期五'},
+    {key: 7, value: '星期六'}
+])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, 1, 6)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, 7)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, 1, 4)
+    average02.value = props.check(average02.value, 1, 7)
+    return average02.value + '#' + average01.value
+})
+const weekdayTotal = computed(() => {
+    weekday.value = props.check(weekday.value, 1, 7)
+    return weekday.value + 'L'
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.week, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, weekdayTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === "*") {
+        radioValue.value = 1
+    } else if (value === "?") {
+        radioValue.value = 2
+    } else if (value.indexOf("-") > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else if (value.indexOf("#") > -1) {
+        const indexArr = value.split('#')
+        average01.value = Number(indexArr[1])
+        average02.value = Number(indexArr[0])
+        radioValue.value = 4
+    } else if (value.indexOf("L") > -1) {
+        const indexArr = value.split("L")
+        weekday.value = Number(indexArr[0])
+        radioValue.value = 5
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 6
+    }
+}
+function onRadioChange() {
+    if (radioValue.value === 2 && props.cron.day === '?') {
+        emit('update', 'day', '*', 'week')
+    }
+    if (radioValue.value !== 2 && props.cron.day !== '?') {
+        emit('update', 'day', '?', 'week')
+    }
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'week', '*', 'week')
+            break
+        case 2:
+            emit('update', 'week', '?', 'week')
+            break
+        case 3:
+            emit('update', 'week', cycleTotal.value, 'week')
+            break
+        case 4:
+            emit('update', 'week', averageTotal.value, 'week')
+            break
+        case 5:
+            emit('update', 'week', weekdayTotal.value, 'week')
+            break
+        case 6:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'week', checkboxString.value, 'week')
+            break
+    }
 }
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.5rem;
+}
+.el-select, .el-select--small {
+    width: 8rem;
+}
+.el-select.multiselect, .el-select--small.multiselect {
+    width: 17.8rem;
+}
+</style>

+ 139 - 121
canteen-ui/src/components/Crontab/year.vue

@@ -1,131 +1,149 @@
 <template>
-	<el-form size="small">
-		<el-form-item>
-			<el-radio :label="1" v-model='radioValue'>
-				不填,允许的通配符[, - * /]
-			</el-radio>
-		</el-form-item>
+    <el-form>
+        <el-form-item>
+            <el-radio :value="1" v-model='radioValue'>
+                不填,允许的通配符[, - * /]
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio :label="2" v-model='radioValue'>
-				每年
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio :value="2" v-model='radioValue'>
+                每年
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio :label="3" v-model='radioValue'>
-				周期从
-				<el-input-number v-model='cycle01' :min='fullYear' :max="2098" /> -
-				<el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099" />
-			</el-radio>
-		</el-form-item>
+        <el-form-item>
+            <el-radio :value="3" v-model='radioValue'>
+                周期从
+                <el-input-number v-model='cycle01' :min='fullYear' :max="2098"/> -
+                <el-input-number v-model='cycle02' :min="cycle01 ? cycle01 + 1 : fullYear + 1" :max="2099"/>
+            </el-radio>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio :label="4" v-model='radioValue'>
-				
-				<el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始,每
-				<el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear" /> 年执行一次
-			</el-radio>
+        <el-form-item>
+            <el-radio :value="4" v-model='radioValue'>
+                
+                <el-input-number v-model='average01' :min='fullYear' :max="2098"/> 年开始,每
+                <el-input-number v-model='average02' :min="1" :max="2099 - average01 || fullYear"/> 年执行一次
+            </el-radio>
 
-		</el-form-item>
+        </el-form-item>
 
-		<el-form-item>
-			<el-radio :label="5" v-model='radioValue'>
-				指定
-				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple>
-					<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
-				</el-select>
-			</el-radio>
-		</el-form-item>
-	</el-form>
+        <el-form-item>
+            <el-radio :value="5" v-model='radioValue'>
+                指定
+                <el-select clearable v-model="checkboxList" placeholder="可多选" multiple :multiple-limit="8">
+                    <el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
+                </el-select>
+            </el-radio>
+        </el-form-item>
+    </el-form>
 </template>
 
-<script>
-export default {
-	data() {
-		return {
-			fullYear: 0,
-			radioValue: 1,
-			cycle01: 0,
-			cycle02: 0,
-			average01: 0,
-			average02: 1,
-			checkboxList: [],
-			checkNum: this.$options.propsData.check
-		}
-	},
-	name: 'crontab-year',
-	props: ['check', 'month', 'cron'],
-	methods: {
-		// 单选按钮值变化时
-		radioChange() {
-			switch (this.radioValue) {
-				case 1:
-					this.$emit('update', 'year', '');
-					break;
-				case 2:
-					this.$emit('update', 'year', '*');
-					break;
-				case 3:
-					this.$emit('update', 'year', this.cycleTotal);
-					break;
-				case 4:
-					this.$emit('update', 'year', this.averageTotal);
-					break;
-				case 5:
-					this.$emit('update', 'year', this.checkboxString);
-					break;
-			}
-		},
-		// 周期两个值变化时
-		cycleChange() {
-			if (this.radioValue == '3') {
-				this.$emit('update', 'year', this.cycleTotal);
-			}
-		},
-		// 平均两个值变化时
-		averageChange() {
-			if (this.radioValue == '4') {
-				this.$emit('update', 'year', this.averageTotal);
-			}
-		},
-		// checkbox值变化时
-		checkboxChange() {
-			if (this.radioValue == '5') {
-				this.$emit('update', 'year', this.checkboxString);
-			}
-		}
-	},
-	watch: {
-		'radioValue': 'radioChange',
-		'cycleTotal': 'cycleChange',
-		'averageTotal': 'averageChange',
-		'checkboxString': 'checkboxChange'
-	},
-	computed: {
-		// 计算两个周期值
-		cycleTotal: function () {
-			const cycle01 = this.checkNum(this.cycle01, this.fullYear, 2098)
-			const cycle02 = this.checkNum(this.cycle02, cycle01 ? cycle01 + 1 : this.fullYear + 1, 2099)
-			return cycle01 + '-' + cycle02;
-		},
-		// 计算平均用到的值
-		averageTotal: function () {
-			const average01 = this.checkNum(this.average01, this.fullYear, 2098)
-			const average02 = this.checkNum(this.average02, 1, 2099 - average01 || this.fullYear)
-			return average01 + '/' + average02;
-		},
-		// 计算勾选的checkbox值合集
-		checkboxString: function () {
-			let str = this.checkboxList.join();
-			return str;
-		}
-	},
-	mounted: function () {
-		// 仅获取当前年份
-		this.fullYear = Number(new Date().getFullYear());
-		this.cycle01 = this.fullYear
-		this.average01 = this.fullYear
-	}
+<script setup>
+const emit = defineEmits(['update'])
+const props = defineProps({
+    cron: {
+        type: Object,
+        default: {
+            second: "*",
+            min: "*",
+            hour: "*",
+            day: "*",
+            month: "*",
+            week: "?",
+            year: ""
+        }
+    },
+    check: {
+        type: Function,
+        default: () => {
+        }
+    }
+})
+const fullYear = ref(0)
+const maxFullYear = ref(0)
+const radioValue = ref(1)
+const cycle01 = ref(0)
+const cycle02 = ref(0)
+const average01 = ref(0)
+const average02 = ref(1)
+const checkboxList = ref([])
+const checkCopy = ref([])
+const cycleTotal = computed(() => {
+    cycle01.value = props.check(cycle01.value, fullYear.value, maxFullYear.value - 1)
+    cycle02.value = props.check(cycle02.value, cycle01.value + 1, maxFullYear.value)
+    return cycle01.value + '-' + cycle02.value
+})
+const averageTotal = computed(() => {
+    average01.value = props.check(average01.value, fullYear.value, maxFullYear.value - 1)
+    average02.value = props.check(average02.value, 1, 10)
+    return average01.value + '/' + average02.value
+})
+const checkboxString = computed(() => {
+    return checkboxList.value.join(',')
+})
+watch(() => props.cron.year, value => changeRadioValue(value))
+watch([radioValue, cycleTotal, averageTotal, checkboxString], () => onRadioChange())
+function changeRadioValue(value) {
+    if (value === '') {
+        radioValue.value = 1
+    } else if (value === "*") {
+        radioValue.value = 2
+    } else if (value.indexOf("-") > -1) {
+        const indexArr = value.split('-')
+        cycle01.value = Number(indexArr[0])
+        cycle02.value = Number(indexArr[1])
+        radioValue.value = 3
+    } else if (value.indexOf("/") > -1) {
+        const indexArr = value.split('/')
+        average01.value = Number(indexArr[1])
+        average02.value = Number(indexArr[0])
+        radioValue.value = 4
+    } else {
+        checkboxList.value = [...new Set(value.split(',').map(item => Number(item)))]
+        radioValue.value = 5
+    }
 }
+function onRadioChange() {
+    switch (radioValue.value) {
+        case 1:
+            emit('update', 'year', '', 'year')
+            break
+        case 2:
+            emit('update', 'year', '*', 'year')
+            break
+        case 3:
+            emit('update', 'year', cycleTotal.value, 'year')
+            break
+        case 4:
+            emit('update', 'year', averageTotal.value, 'year')
+            break
+        case 5:
+            if (checkboxList.value.length === 0) {
+                checkboxList.value.push(checkCopy.value[0])
+            } else {
+                checkCopy.value = checkboxList.value
+            }
+            emit('update', 'year', checkboxString.value, 'year')
+            break
+    }
+}
+onMounted(() => {
+    fullYear.value = Number(new Date().getFullYear())
+    maxFullYear.value = fullYear.value + 10
+    cycle01.value = fullYear.value
+    cycle02.value = cycle01.value + 1
+    average01.value = fullYear.value
+    checkCopy.value = [fullYear.value]
+})
 </script>
+
+<style lang="scss" scoped>
+.el-input-number--small, .el-select, .el-select--small {
+    margin: 0 0.2rem;
+}
+.el-select, .el-select--small {
+    width: 18.8rem;
+}
+</style>

+ 0 - 49
canteen-ui/src/components/DictData/index.js

@@ -1,49 +0,0 @@
-import Vue from 'vue'
-import store from '@/store'
-import DataDict from '@/utils/dict'
-import { getDicts as getDicts } from '@/api/system/dict/data'
-
-function searchDictByKey(dict, key) {
-  if (key == null && key == "") {
-    return null
-  }
-  try {
-    for (let i = 0; i < dict.length; i++) {
-      if (dict[i].key == key) {
-        return dict[i].value
-      }
-    }
-  } catch (e) {
-    return null
-  }
-}
-
-function install() {
-  Vue.use(DataDict, {
-    metas: {
-      '*': {
-        labelField: 'dictLabel',
-        valueField: 'dictValue',
-        request(dictMeta) {
-          const storeDict = searchDictByKey(store.getters.dict, dictMeta.type)
-          if (storeDict) {
-            return new Promise(resolve => { resolve(storeDict) })
-          } else {
-            return new Promise((resolve, reject) => {
-              getDicts(dictMeta.type).then(res => {
-                store.dispatch('dict/setDict', { key: dictMeta.type, value: res.data })
-                resolve(res.data)
-              }).catch(error => {
-                reject(error)
-              })
-            })
-          }
-        },
-      },
-    },
-  })
-}
-
-export default {
-  install,
-}

+ 54 - 61
canteen-ui/src/components/DictTag/index.vue

@@ -3,22 +3,19 @@
     <template v-for="(item, index) in options">
       <template v-if="values.includes(item.value)">
         <span
-          v-if="(item.raw.listClass == 'default' || item.raw.listClass == '') && (item.raw.cssClass == '' || item.raw.cssClass == null)"
+          v-if="(item.elTagType == 'default' || item.elTagType == '') && (item.elTagClass == '' || item.elTagClass == null)"
           :key="item.value"
           :index="index"
-          :class="item.raw.cssClass"
-          >{{ item.label + ' ' }}</span
-        >
+          :class="item.elTagClass"
+        >{{ item.label + " " }}</span>
         <el-tag
           v-else
           :disable-transitions="true"
-          :key="item.value"
+          :key="item.value + ''"
           :index="index"
-          :type="item.raw.listClass == 'primary' ? '' : item.raw.listClass"
-          :class="item.raw.cssClass"
-        >
-          {{ item.label + ' ' }}
-        </el-tag>
+          :type="item.elTagType"
+          :class="item.elTagClass"
+        >{{ item.label + " " }}</el-tag>
       </template>
     </template>
     <template v-if="unmatch && showValue">
@@ -27,61 +24,57 @@
   </div>
 </template>
 
-<script>
-export default {
-  name: "DictTag",
-  props: {
-    options: {
-      type: Array,
-      default: null,
-    },
-    value: [Number, String, Array],
-    // 当未找到匹配的数据时,显示value
-    showValue: {
-      type: Boolean,
-      default: true,
-    },
-    separator: {
-      type: String,
-      default: ","
-    }
-  },
-  data() {
-    return {
-      unmatchArray: [], // 记录未匹配的项
-    }
-  },
-  computed: {
-    values() {
-      if (this.value === null || typeof this.value === 'undefined' || this.value === '') return []
-      return Array.isArray(this.value) ? this.value.map(item => '' + item) : String(this.value).split(this.separator)
-    },
-    unmatch() {
-      this.unmatchArray = []
-      // 没有value不显示
-      if (this.value === null || typeof this.value === 'undefined' || this.value === '' || this.options.length === 0) return false
-      // 传入值为数组
-      let unmatch = false // 添加一个标志来判断是否有未匹配项
-      this.values.forEach(item => {
-        if (!this.options.some(v => v.value === item)) {
-          this.unmatchArray.push(item)
-          unmatch = true // 如果有未匹配项,将标志设置为true
-        }
-      })
-      return unmatch // 返回标志的值
-    },
+<script setup>
+// 记录未匹配的项
+const unmatchArray = ref([]);
 
+const props = defineProps({
+  // 数据
+  options: {
+    type: Array,
+    default: null,
+  },
+  // 当前的值
+  value: [Number, String, Array],
+  // 当未找到匹配的数据时,显示value
+  showValue: {
+    type: Boolean,
+    default: true,
   },
-  filters: {
-    handleArray(array) {
-      if (array.length === 0) return '';
-      return array.reduce((pre, cur) => {
-        return pre + ' ' + cur;
-      })
-    },
+  separator: {
+    type: String,
+    default: ",",
   }
-};
+});
+
+const values = computed(() => {
+  if (props.value === null || typeof props.value === 'undefined' || props.value === '') return [];
+  return Array.isArray(props.value) ? props.value.map(item => '' + item) : String(props.value).split(props.separator);
+});
+
+const unmatch = computed(() => {
+  unmatchArray.value = [];
+  // 没有value不显示
+  if (props.value === null || typeof props.value === 'undefined' || props.value === '' || !Array.isArray(props.options) || props.options.length === 0) return false
+  // 传入值为数组
+  let unmatch = false // 添加一个标志来判断是否有未匹配项
+  values.value.forEach(item => {
+    if (!props.options.some(v => v.value === item)) {
+      unmatchArray.value.push(item)
+      unmatch = true // 如果有未匹配项,将标志设置为true
+    }
+  })
+  return unmatch // 返回标志的值
+});
+
+function handleArray(array) {
+  if (array.length === 0) return "";
+  return array.reduce((pre, cur) => {
+    return pre + " " + cur;
+  });
+}
 </script>
+
 <style scoped>
 .el-tag + .el-tag {
   margin-left: 10px;

+ 148 - 171
canteen-ui/src/components/Editor/index.vue

@@ -8,195 +8,172 @@
       name="file"
       :show-file-list="false"
       :headers="headers"
-      style="display: none"
-      ref="upload"
-      v-if="this.type == 'url'"
+      class="editor-img-uploader"
+      v-if="type == 'url'"
     >
+      <i ref="uploadRef" class="editor-img-uploader"></i>
     </el-upload>
-    <div class="editor" ref="editor" :style="styles"></div>
+  </div>
+  <div class="editor">
+    <quill-editor
+      ref="quillEditorRef"
+      v-model:content="content"
+      contentType="html"
+      @textChange="(e) => $emit('update:modelValue', content)"
+      :options="options"
+      :style="styles"
+    />
   </div>
 </template>
 
-<script>
-import Quill from "quill";
-import "quill/dist/quill.core.css";
-import "quill/dist/quill.snow.css";
-import "quill/dist/quill.bubble.css";
+<script setup>
+import { QuillEditor } from "@vueup/vue-quill";
+import "@vueup/vue-quill/dist/vue-quill.snow.css";
 import { getToken } from "@/utils/auth";
 
-export default {
-  name: "Editor",
-  props: {
-    /* 编辑器的内容 */
-    value: {
-      type: String,
-      default: "",
-    },
-    /* 高度 */
-    height: {
-      type: Number,
-      default: null,
-    },
-    /* 最小高度 */
-    minHeight: {
-      type: Number,
-      default: null,
-    },
-    /* 只读 */
-    readOnly: {
-      type: Boolean,
-      default: false,
-    },
-    /* 上传文件大小限制(MB) */
-    fileSize: {
-      type: Number,
-      default: 5,
-    },
-    /* 类型(base64格式、url格式) */
-    type: {
-      type: String,
-      default: "url",
-    }
+const { proxy } = getCurrentInstance();
+
+const quillEditorRef = ref();
+const uploadUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
+const headers = ref({
+  Authorization: "Bearer " + getToken()
+});
+
+const props = defineProps({
+  /* 编辑器的内容 */
+  modelValue: {
+    type: String,
   },
-  data() {
-    return {
-      uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
-      headers: {
-        Authorization: "Bearer " + getToken()
-      },
-      Quill: null,
-      currentValue: "",
-      options: {
-        theme: "snow",
-        bounds: document.body,
-        debug: "warn",
-        modules: {
-          // 工具栏配置
-          toolbar: [
-            ["bold", "italic", "underline", "strike"],       // 加粗 斜体 下划线 删除线
-            ["blockquote", "code-block"],                    // 引用  代码块
-            [{ list: "ordered" }, { list: "bullet" }],       // 有序、无序列表
-            [{ indent: "-1" }, { indent: "+1" }],            // 缩进
-            [{ size: ["small", false, "large", "huge"] }],   // 字体大小
-            [{ header: [1, 2, 3, 4, 5, 6, false] }],         // 标题
-            [{ color: [] }, { background: [] }],             // 字体颜色、字体背景颜色
-            [{ align: [] }],                                 // 对齐方式
-            ["clean"],                                       // 清除文本格式
-            ["link", "image", "video"]                       // 链接、图片、视频
-          ],
-        },
-        placeholder: "请输入内容",
-        readOnly: this.readOnly,
-      },
-    };
+  /* 高度 */
+  height: {
+    type: Number,
+    default: null,
   },
-  computed: {
-    styles() {
-      let style = {};
-      if (this.minHeight) {
-        style.minHeight = `${this.minHeight}px`;
-      }
-      if (this.height) {
-        style.height = `${this.height}px`;
-      }
-      return style;
-    },
+  /* 最小高度 */
+  minHeight: {
+    type: Number,
+    default: null,
   },
-  watch: {
-    value: {
-      handler(val) {
-        if (val !== this.currentValue) {
-          this.currentValue = val === null ? "" : val;
-          if (this.Quill) {
-            this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue);
-          }
-        }
-      },
-      immediate: true,
-    },
+  /* 只读 */
+  readOnly: {
+    type: Boolean,
+    default: false,
   },
-  mounted() {
-    this.init();
+  /* 上传文件大小限制(MB) */
+  fileSize: {
+    type: Number,
+    default: 5,
   },
-  beforeDestroy() {
-    this.Quill = null;
+  /* 类型(base64格式、url格式) */
+  type: {
+    type: String,
+    default: "url",
+  }
+});
+
+const options = ref({
+  theme: "snow",
+  bounds: document.body,
+  debug: "warn",
+  modules: {
+    // 工具栏配置
+    toolbar: [
+      ["bold", "italic", "underline", "strike"],      // 加粗 斜体 下划线 删除线
+      ["blockquote", "code-block"],                   // 引用  代码块
+      [{ list: "ordered" }, { list: "bullet" }],      // 有序、无序列表
+      [{ indent: "-1" }, { indent: "+1" }],           // 缩进
+      [{ size: ["small", false, "large", "huge"] }],  // 字体大小
+      [{ header: [1, 2, 3, 4, 5, 6, false] }],        // 标题
+      [{ color: [] }, { background: [] }],            // 字体颜色、字体背景颜色
+      [{ align: [] }],                                // 对齐方式
+      ["clean"],                                      // 清除文本格式
+      ["link", "image", "video"]                      // 链接、图片、视频
+    ],
   },
-  methods: {
-    init() {
-      const editor = this.$refs.editor;
-      this.Quill = new Quill(editor, this.options);
-      // 如果设置了上传地址则自定义图片上传事件
-      if (this.type == 'url') {
-        let toolbar = this.Quill.getModule("toolbar");
-        toolbar.addHandler("image", (value) => {
-          if (value) {
-            this.$refs.upload.$children[0].$refs.input.click();
-          } else {
-            this.quill.format("image", false);
-          }
-        });
-      }
-      this.Quill.clipboard.dangerouslyPasteHTML(this.currentValue);
-      this.Quill.on("text-change", (delta, oldDelta, source) => {
-        const html = this.$refs.editor.children[0].innerHTML;
-        const text = this.Quill.getText();
-        const quill = this.Quill;
-        this.currentValue = html;
-        this.$emit("input", html);
-        this.$emit("on-change", { html, text, quill });
-      });
-      this.Quill.on("text-change", (delta, oldDelta, source) => {
-        this.$emit("on-text-change", delta, oldDelta, source);
-      });
-      this.Quill.on("selection-change", (range, oldRange, source) => {
-        this.$emit("on-selection-change", range, oldRange, source);
-      });
-      this.Quill.on("editor-change", (eventName, ...args) => {
-        this.$emit("on-editor-change", eventName, ...args);
-      });
-    },
-    // 上传前校检格式和大小
-    handleBeforeUpload(file) {
-      const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
-      const isJPG = type.includes(file.type);
-      // 检验文件格式
-      if (!isJPG) {
-        this.$message.error(`图片格式错误!`);
-        return false;
-      }
-      // 校检文件大小
-      if (this.fileSize) {
-        const isLt = file.size / 1024 / 1024 < this.fileSize;
-        if (!isLt) {
-          this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
-          return false;
-        }
-      }
-      return true;
-    },
-    handleUploadSuccess(res, file) {
-      // 如果上传成功
-      if (res.code == 200) {
-        // 获取富文本组件实例
-        let quill = this.Quill;
-        // 获取光标所在位置
-        let length = quill.getSelection().index;
-        // 插入图片  res.url为服务器返回的图片地址
-        quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
-        // 调整光标到最后
-        quill.setSelection(length + 1);
+  placeholder: "请输入内容",
+  readOnly: props.readOnly
+});
+
+const styles = computed(() => {
+  let style = {};
+  if (props.minHeight) {
+    style.minHeight = `${props.minHeight}px`;
+  }
+  if (props.height) {
+    style.height = `${props.height}px`;
+  }
+  return style;
+});
+
+const content = ref("");
+watch(() => props.modelValue, (v) => {
+  if (v !== content.value) {
+    content.value = v == undefined ? "<p></p>" : v;
+  }
+}, { immediate: true });
+
+// 如果设置了上传地址则自定义图片上传事件
+onMounted(() => {
+  if (props.type == 'url') {
+    let quill = quillEditorRef.value.getQuill();
+    let toolbar = quill.getModule("toolbar");
+    toolbar.addHandler("image", (value) => {
+      if (value) {
+        proxy.$refs.uploadRef.click();
       } else {
-        this.$message.error("图片插入失败");
+        quill.format("image", false);
       }
-    },
-    handleUploadError() {
-      this.$message.error("图片插入失败");
-    },
-  },
-};
+    });
+  }
+});
+
+// 上传前校检格式和大小
+function handleBeforeUpload(file) {
+  const type = ["image/jpeg", "image/jpg", "image/png", "image/svg"];
+  const isJPG = type.includes(file.type);
+  //检验文件格式
+  if (!isJPG) {
+    proxy.$modal.msgError(`图片格式错误!`);
+    return false;
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  return true;
+}
+
+// 上传成功处理
+function handleUploadSuccess(res, file) {
+  // 如果上传成功
+  if (res.code == 200) {
+    // 获取富文本实例
+    let quill = toRaw(quillEditorRef.value).getQuill();
+    // 获取光标位置
+    let length = quill.selection.savedRange.index;
+    // 插入图片,res.url为服务器返回的图片链接地址
+    quill.insertEmbed(length, "image", import.meta.env.VITE_APP_BASE_API + res.fileName);
+    // 调整光标到最后
+    quill.setSelection(length + 1);
+  } else {
+    proxy.$modal.msgError("图片插入失败");
+  }
+}
+
+// 上传失败处理
+function handleUploadError() {
+  proxy.$modal.msgError("图片插入失败");
+}
 </script>
 
 <style>
+.editor-img-uploader {
+  display: none;
+}
 .editor, .ql-toolbar {
   white-space: pre-wrap !important;
   line-height: normal !important;

+ 155 - 164
canteen-ui/src/components/FileUpload/index.vue

@@ -16,19 +16,18 @@
       v-if="!disabled"
     >
       <!-- 上传按钮 -->
-      <el-button size="mini" type="primary">选取文件</el-button>
-      <!-- 上传提示 -->
-      <div class="el-upload__tip" slot="tip" v-if="showTip">
-        请上传
-        <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
-        <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
-        的文件
-      </div>
+      <el-button type="primary">选取文件</el-button>
     </el-upload>
-
+    <!-- 上传提示 -->
+    <div class="el-upload__tip" v-if="showTip && !disabled">
+      请上传
+      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
+      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
+      的文件
+    </div>
     <!-- 文件列表 -->
     <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
-      <li :key="file.url" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
         <el-link :href="`${baseUrl}${file.url}`" :underline="false" target="_blank">
           <span class="el-icon-document"> {{ getFileName(file.name) }} </span>
         </el-link>
@@ -40,169 +39,161 @@
   </div>
 </template>
 
-<script>
+<script setup>
 import { getToken } from "@/utils/auth";
 
-export default {
-  name: "FileUpload",
-  props: {
-    // 值
-    value: [String, Object, Array],
-    // 数量限制
-    limit: {
-      type: Number,
-      default: 5
-    },
-    // 大小限制(MB)
-    fileSize: {
-      type: Number,
-      default: 5
-    },
-    // 文件类型, 例如['png', 'jpg', 'jpeg']
-    fileType: {
-      type: Array,
-      default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
-    },
-    // 是否显示提示
-    isShowTip: {
-      type: Boolean,
-      default: true
-    },
-    // 禁用组件(仅查看文件)
-    disabled: {
-      type: Boolean,
-      default: false
-    }
+const props = defineProps({
+  modelValue: [String, Object, Array],
+  // 数量限制
+  limit: {
+    type: Number,
+    default: 5
   },
-  data() {
-    return {
-      number: 0,
-      uploadList: [],
-      baseUrl: process.env.VUE_APP_BASE_API,
-      uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传文件服务器地址
-      headers: {
-        Authorization: "Bearer " + getToken(),
-      },
-      fileList: [],
-    };
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 5
   },
-  watch: {
-    value: {
-      handler(val) {
-        if (val) {
-          let temp = 1;
-          // 首先将值转为数组
-          const list = Array.isArray(val) ? val : this.value.split(',');
-          // 然后将数组转为对象数组
-          this.fileList = list.map(item => {
-            if (typeof item === "string") {
-              item = { name: item, url: item };
-            }
-            item.uid = item.uid || new Date().getTime() + temp++;
-            return item;
-          });
-        } else {
-          this.fileList = [];
-          return [];
-        }
-      },
-      deep: true,
-      immediate: true
-    }
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    default: () => ["doc", "docx", "xls", "xlsx", "ppt", "pptx", "txt", "pdf"]
   },
-  computed: {
-    // 是否显示提示
-    showTip() {
-      return this.isShowTip && (this.fileType || this.fileSize);
-    },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true
   },
-  methods: {
-    // 上传前校检格式和大小
-    handleBeforeUpload(file) {
-      // 校检文件类型
-      if (this.fileType) {
-        const fileName = file.name.split('.');
-        const fileExt = fileName[fileName.length - 1];
-        const isTypeOk = this.fileType.indexOf(fileExt) >= 0;
-        if (!isTypeOk) {
-          this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}格式文件!`);
-          return false;
-        }
-      }
-      // 校检文件名是否包含特殊字符
-      if (file.name.includes(',')) {
-        this.$modal.msgError('文件名不正确,不能包含英文逗号!');
-        return false;
-      }
-      // 校检文件大小
-      if (this.fileSize) {
-        const isLt = file.size / 1024 / 1024 < this.fileSize;
-        if (!isLt) {
-          this.$modal.msgError(`上传文件大小不能超过 ${this.fileSize} MB!`);
-          return false;
-        }
-      }
-      this.$modal.loading("正在上传文件,请稍候...");
-      this.number++;
-      return true;
-    },
-    // 文件个数超出
-    handleExceed() {
-      this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
-    },
-    // 上传失败
-    handleUploadError(err) {
-      this.$modal.msgError("上传文件失败,请重试");
-      this.$modal.closeLoading();
-    },
-    // 上传成功回调
-    handleUploadSuccess(res, file) {
-      if (res.code === 200) {
-        this.uploadList.push({ name: res.fileName, url: res.fileName });
-        this.uploadedSuccessfully();
-      } else {
-        this.number--;
-        this.$modal.closeLoading();
-        this.$modal.msgError(res.msg);
-        this.$refs.fileUpload.handleRemove(file);
-        this.uploadedSuccessfully();
-      }
-    },
-    // 删除文件
-    handleDelete(index) {
-      this.fileList.splice(index, 1);
-      this.$emit("input", this.listToString(this.fileList));
-    },
-    // 上传结束处理
-    uploadedSuccessfully() {
-      if (this.number > 0 && this.uploadList.length === this.number) {
-        this.fileList = this.fileList.concat(this.uploadList);
-        this.uploadList = [];
-        this.number = 0;
-        this.$emit("input", this.listToString(this.fileList));
-        this.$modal.closeLoading();
-      }
-    },
-    // 获取文件名称
-    getFileName(name) {
-      // 如果是url那么取最后的名字 如果不是直接返回
-      if (name.lastIndexOf("/") > -1) {
-        return name.slice(name.lastIndexOf("/") + 1);
-      } else {
-        return name;
-      }
-    },
-    // 对象转成指定字符串分隔
-    listToString(list, separator) {
-      let strs = "";
-      separator = separator || ",";
-      for (let i in list) {
-        strs += list[i].url + separator;
+  // 禁用组件(仅查看文件)
+  disabled: {
+    type: Boolean,
+    default: false
+  }
+});
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits();
+const number = ref(0);
+const uploadList = ref([]);
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传文件服务器地址
+const headers = ref({ Authorization: "Bearer " + getToken() });
+const fileList = ref([]);
+const showTip = computed(
+  () => props.isShowTip && (props.fileType || props.fileSize)
+);
+
+watch(() => props.modelValue, val => {
+  if (val) {
+    let temp = 1;
+    // 首先将值转为数组
+    const list = Array.isArray(val) ? val : props.modelValue.split(',');
+    // 然后将数组转为对象数组
+    fileList.value = list.map(item => {
+      if (typeof item === "string") {
+        item = { name: item, url: item };
       }
-      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+      item.uid = item.uid || new Date().getTime() + temp++;
+      return item;
+    });
+  } else {
+    fileList.value = [];
+    return [];
+  }
+},{ deep: true, immediate: true });
+
+// 上传前校检格式和大小
+function handleBeforeUpload(file) {
+  // 校检文件类型
+  if (props.fileType.length) {
+    const fileName = file.name.split('.');
+    const fileExt = fileName[fileName.length - 1];
+    const isTypeOk = props.fileType.indexOf(fileExt) >= 0;
+    if (!isTypeOk) {
+      proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}格式文件!`);
+      return false;
     }
   }
-};
+  // 校检文件名是否包含特殊字符
+  if (file.name.includes(',')) {
+    proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
+    return false;
+  }
+  // 校检文件大小
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy.$modal.msgError(`上传文件大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  proxy.$modal.loading("正在上传文件,请稍候...");
+  number.value++;
+  return true;
+}
+
+// 文件个数超出
+function handleExceed() {
+  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+}
+
+// 上传失败
+function handleUploadError(err) {
+  proxy.$modal.msgError("上传文件失败");
+}
+
+// 上传成功回调
+function handleUploadSuccess(res, file) {
+  if (res.code === 200) {
+    uploadList.value.push({ name: res.fileName, url: res.fileName });
+    uploadedSuccessfully();
+  } else {
+    number.value--;
+    proxy.$modal.closeLoading();
+    proxy.$modal.msgError(res.msg);
+    proxy.$refs.fileUpload.handleRemove(file);
+    uploadedSuccessfully();
+  }
+}
+
+// 删除文件
+function handleDelete(index) {
+  fileList.value.splice(index, 1);
+  emit("update:modelValue", listToString(fileList.value));
+}
+
+// 上传结束处理
+function uploadedSuccessfully() {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit("update:modelValue", listToString(fileList.value));
+    proxy.$modal.closeLoading();
+  }
+}
+
+// 获取文件名称
+function getFileName(name) {
+  // 如果是url那么取最后的名字 如果不是直接返回
+  if (name.lastIndexOf("/") > -1) {
+    return name.slice(name.lastIndexOf("/") + 1);
+  } else {
+    return name;
+  }
+}
+
+// 对象转成指定字符串分隔
+function listToString(list, separator) {
+  let strs = "";
+  separator = separator || ",";
+  for (let i in list) {
+    if (list[i].url) {
+      strs += list[i].url + separator;
+    }
+  }
+  return strs != '' ? strs.substr(0, strs.length - 1) : '';
+}
 </script>
 
 <style scoped lang="scss">

+ 11 - 13
canteen-ui/src/components/Hamburger/index.vue

@@ -7,26 +7,24 @@
       xmlns="http://www.w3.org/2000/svg"
       width="64"
       height="64"
+      fill="currentColor"
     >
       <path d="M408 442h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8zm-8 204c0 4.4 3.6 8 8 8h480c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8H408c-4.4 0-8 3.6-8 8v56zm504-486H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zm0 632H120c-4.4 0-8 3.6-8 8v56c0 4.4 3.6 8 8 8h784c4.4 0 8-3.6 8-8v-56c0-4.4-3.6-8-8-8zM142.4 642.1L298.7 519a8.84 8.84 0 0 0 0-13.9L142.4 381.9c-5.8-4.6-14.4-.5-14.4 6.9v246.3a8.9 8.9 0 0 0 14.4 7z" />
     </svg>
   </div>
 </template>
 
-<script>
-export default {
-  name: 'Hamburger',
-  props: {
-    isActive: {
-      type: Boolean,
-      default: false
-    }
-  },
-  methods: {
-    toggleClick() {
-      this.$emit('toggleClick')
-    }
+<script setup>
+defineProps({
+  isActive: {
+    type: Boolean,
+    default: false
   }
+})
+
+const emit = defineEmits()
+const toggleClick = () => {
+  emit('toggleClick');
 }
 </script>
 

+ 122 - 131
canteen-ui/src/components/HeaderSearch/index.vue

@@ -1,8 +1,8 @@
 <template>
-  <div :class="{'show':show}" class="header-search">
+  <div :class="{ 'show': show }" class="header-search">
     <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
     <el-select
-      ref="headerSearchSelect"
+      ref="headerSearchSelectRef"
       v-model="search"
       :remote-method="querySearch"
       filterable
@@ -17,145 +17,136 @@
   </div>
 </template>
 
-<script>
-// fuse is a lightweight fuzzy-search module
-// make search results more in line with expectations
-import Fuse from 'fuse.js/dist/fuse.min.js'
-import path from 'path'
+<script setup>
+import Fuse from 'fuse.js'
+import { getNormalPath } from '@/utils/ruoyi'
 import { isHttp } from '@/utils/validate'
+import usePermissionStore from '@/store/modules/permission'
 
-export default {
-  name: 'HeaderSearch',
-  data() {
-    return {
-      search: '',
-      options: [],
-      searchPool: [],
-      show: false,
-      fuse: undefined
+const search = ref('');
+const options = ref([]);
+const searchPool = ref([]);
+const show = ref(false);
+const fuse = ref(undefined);
+const headerSearchSelectRef = ref(null);
+const router = useRouter();
+const routes = computed(() => usePermissionStore().defaultRoutes);
+
+function click() {
+  show.value = !show.value
+  if (show.value) {
+    headerSearchSelectRef.value && headerSearchSelectRef.value.focus()
+  }
+};
+function close() {
+  headerSearchSelectRef.value && headerSearchSelectRef.value.blur()
+  options.value = []
+  show.value = false
+}
+function change(val) {
+  const path = val.path;
+  const query = val.query;
+  if (isHttp(path)) {
+    // http(s):// 路径新窗口打开
+    const pindex = path.indexOf("http");
+    window.open(path.substr(pindex, path.length), "_blank");
+  } else {
+    if (query) {
+      router.push({ path: path, query: JSON.parse(query) });
+    } else {
+      router.push(path)
     }
-  },
-  computed: {
-    routes() {
-      return this.$store.getters.defaultRoutes
+  }
+
+  search.value = ''
+  options.value = []
+  nextTick(() => {
+    show.value = false
+  })
+}
+function initFuse(list) {
+  fuse.value = new Fuse(list, {
+    shouldSort: true,
+    threshold: 0.4,
+    location: 0,
+    distance: 100,
+    minMatchCharLength: 1,
+    keys: [{
+      name: 'title',
+      weight: 0.7
+    }, {
+      name: 'path',
+      weight: 0.3
+    }]
+  })
+}
+// Filter out the routes that can be displayed in the sidebar
+// And generate the internationalized title
+function generateRoutes(routes, basePath = '', prefixTitle = []) {
+  let res = []
+
+  for (const r of routes) {
+    // skip hidden router
+    if (r.hidden) { continue }
+    const p = r.path.length > 0 && r.path[0] === '/' ? r.path : '/' + r.path;
+    const data = {
+      path: !isHttp(r.path) ? getNormalPath(basePath + p) : r.path,
+      title: [...prefixTitle]
     }
-  },
-  watch: {
-    routes() {
-      this.searchPool = this.generateRoutes(this.routes)
-    },
-    searchPool(list) {
-      this.initFuse(list)
-    },
-    show(value) {
-      if (value) {
-        document.body.addEventListener('click', this.close)
-      } else {
-        document.body.removeEventListener('click', this.close)
+
+    if (r.meta && r.meta.title) {
+      data.title = [...data.title, r.meta.title]
+
+      if (r.redirect !== 'noRedirect') {
+        // only push the routes with title
+        // special case: need to exclude parent router without redirect
+        res.push(data)
       }
     }
-  },
-  mounted() {
-    this.searchPool = this.generateRoutes(this.routes)
-  },
-  methods: {
-    click() {
-      this.show = !this.show
-      if (this.show) {
-        this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.focus()
-      }
-    },
-    close() {
-      this.$refs.headerSearchSelect && this.$refs.headerSearchSelect.blur()
-      this.options = []
-      this.show = false
-    },
-    change(val) {
-      const path = val.path;
-      const query = val.query;
-      if(isHttp(val.path)) {
-        // http(s):// 路径新窗口打开
-        const pindex = path.indexOf("http");
-        window.open(path.substr(pindex, path.length), "_blank");
-      } else {
-        if (query) {
-          this.$router.push({ path: path, query: JSON.parse(query) });
-        } else {
-          this.$router.push(path)
-        }
-      }
-      this.search = ''
-      this.options = []
-      this.$nextTick(() => {
-        this.show = false
-      })
-    },
-    initFuse(list) {
-      this.fuse = new Fuse(list, {
-        shouldSort: true,
-        threshold: 0.4,
-        location: 0,
-        distance: 100,
-        minMatchCharLength: 1,
-        keys: [{
-          name: 'title',
-          weight: 0.7
-        }, {
-          name: 'path',
-          weight: 0.3
-        }]
-      })
-    },
-    // Filter out the routes that can be displayed in the sidebar
-    // And generate the internationalized title
-    generateRoutes(routes, basePath = '/', prefixTitle = []) {
-      let res = []
-
-      for (const router of routes) {
-        // skip hidden router
-        if (router.hidden) { continue }
-
-        const data = {
-          path: !isHttp(router.path) ? path.resolve(basePath, router.path) : router.path,
-          title: [...prefixTitle]
-        }
-
-        if (router.meta && router.meta.title) {
-          data.title = [...data.title, router.meta.title]
-
-          if (router.redirect !== 'noRedirect') {
-            // only push the routes with title
-            // special case: need to exclude parent router without redirect
-            res.push(data)
-          }
-        }
-
-        if (router.query) {
-          data.query = router.query
-        }
-
-        // recursive child routes
-        if (router.children) {
-          const tempRoutes = this.generateRoutes(router.children, data.path, data.title)
-          if (tempRoutes.length >= 1) {
-            res = [...res, ...tempRoutes]
-          }
-        }
-      }
-      return res
-    },
-    querySearch(query) {
-      if (query !== '') {
-        this.options = this.fuse.search(query)
-      } else {
-        this.options = []
+    if (r.query) {
+      data.query = r.query
+    }
+
+    // recursive child routes
+    if (r.children) {
+      const tempRoutes = generateRoutes(r.children, data.path, data.title)
+      if (tempRoutes.length >= 1) {
+        res = [...res, ...tempRoutes]
       }
     }
   }
+  return res
+}
+function querySearch(query) {
+  if (query !== '') {
+    options.value = fuse.value.search(query)
+  } else {
+    options.value = []
+  }
 }
+
+onMounted(() => {
+  searchPool.value = generateRoutes(routes.value);
+})
+
+watchEffect(() => {
+  searchPool.value = generateRoutes(routes.value)
+})
+
+watch(show, (value) => {
+  if (value) {
+    document.body.addEventListener('click', close)
+  } else {
+    document.body.removeEventListener('click', close)
+  }
+})
+
+watch(searchPool, (list) => {
+  initFuse(list)
+})
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
 .header-search {
   font-size: 0 !important;
 
@@ -175,7 +166,7 @@ export default {
     display: inline-block;
     vertical-align: middle;
 
-    ::v-deep .el-input__inner {
+    :deep(.el-input__inner) {
       border-radius: 0;
       border: 0;
       padding-left: 0;
@@ -193,4 +184,4 @@ export default {
     }
   }
 }
-</style>
+</style>

+ 42 - 35
canteen-ui/src/components/IconSelect/index.vue

@@ -1,8 +1,14 @@
-<!-- @author zhengjie -->
 <template>
   <div class="icon-body">
-    <el-input v-model="name" class="icon-search" clearable placeholder="请输入图标名称" @clear="filterIcons" @input="filterIcons">
-      <i slot="suffix" class="el-icon-search el-input__icon" />
+    <el-input
+      v-model="iconName"
+      class="icon-search"
+      clearable
+      placeholder="请输入图标名称"
+      @clear="filterIcons"
+      @input="filterIcons"
+    >
+      <template #suffix><i class="el-icon-search el-input__icon" /></template>
     </el-input>
     <div class="icon-list">
       <div class="list-container">
@@ -17,42 +23,43 @@
   </div>
 </template>
 
-<script>
+<script setup>
 import icons from './requireIcons'
-export default {
-  name: 'IconSelect',
-  props: {
-    activeIcon: {
-      type: String
-    }
-  },
-  data() {
-    return {
-      name: '',
-      iconList: icons
-    }
-  },
-  methods: {
-    filterIcons() {
-      this.iconList = icons
-      if (this.name) {
-        this.iconList = this.iconList.filter(item => item.includes(this.name))
-      }
-    },
-    selectedIcon(name) {
-      this.$emit('selected', name)
-      document.body.click()
-    },
-    reset() {
-      this.name = ''
-      this.iconList = icons
-    }
+
+const props = defineProps({
+  activeIcon: {
+    type: String
   }
+});
+
+const iconName = ref('');
+const iconList = ref(icons);
+const emit = defineEmits(['selected']);
+
+function filterIcons() {
+  iconList.value = icons
+  if (iconName.value) {
+    iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
+  }
+}
+
+function selectedIcon(name) {
+  emit('selected', name)
+  document.body.click()
 }
+
+function reset() {
+  iconName.value = ''
+  iconList.value = icons
+}
+
+defineExpose({
+  reset
+})
 </script>
 
-<style rel="stylesheet/scss" lang="scss" scoped>
-  .icon-body {
+<style lang='scss' scoped>
+   .icon-body {
     width: 100%;
     padding: 10px;
     .icon-search {
@@ -101,4 +108,4 @@ export default {
       }
     }
   }
-</style>
+</style>

+ 8 - 11
canteen-ui/src/components/IconSelect/requireIcons.js

@@ -1,11 +1,8 @@
-
-const req = require.context('../../assets/icons/svg', false, /\.svg$/)
-const requireAll = requireContext => requireContext.keys()
-
-const re = /\.\/(.*)\.svg/
-
-const icons = requireAll(req).map(i => {
-  return i.match(re)[1]
-})
-
-export default icons
+let icons = []
+const modules = import.meta.glob('./../../assets/icons/svg/*.svg');
+for (const path in modules) {
+  const p = path.split('assets/icons/svg/')[1].split('.svg')[0];
+  icons.push(p);
+}
+
+export default icons

+ 55 - 53
canteen-ui/src/components/ImagePreview/index.vue

@@ -4,65 +4,67 @@
     fit="cover"
     :style="`width:${realWidth};height:${realHeight};`"
     :preview-src-list="realSrcList"
+    preview-teleported
   >
-    <div slot="error" class="image-slot">
-      <i class="el-icon-picture-outline"></i>
-    </div>
+    <template #error>
+      <div class="image-slot">
+        <el-icon><picture-filled /></el-icon>
+      </div>
+    </template>
   </el-image>
 </template>
 
-<script>
+<script setup>
 import { isExternal } from "@/utils/validate";
 
-export default {
-  name: "ImagePreview",
-  props: {
-    src: {
-      type: String,
-      default: ""
-    },
-    width: {
-      type: [Number, String],
-      default: ""
-    },
-    height: {
-      type: [Number, String],
-      default: ""
-    }
+const props = defineProps({
+  src: {
+    type: String,
+    default: ""
   },
-  computed: {
-    realSrc() {
-      if (!this.src) {
-        return;
-      }
-      let real_src = this.src.split(",")[0];
-      if (isExternal(real_src)) {
-        return real_src;
-      }
-      return process.env.VUE_APP_BASE_API + real_src;
-    },
-    realSrcList() {
-      if (!this.src) {
-        return;
-      }
-      let real_src_list = this.src.split(",");
-      let srcList = [];
-      real_src_list.forEach(item => {
-        if (isExternal(item)) {
-          return srcList.push(item);
-        }
-        return srcList.push(process.env.VUE_APP_BASE_API + item);
-      });
-      return srcList;
-    },
-    realWidth() {
-      return typeof this.width == "string" ? this.width : `${this.width}px`;
-    },
-    realHeight() {
-      return typeof this.height == "string" ? this.height : `${this.height}px`;
-    }
+  width: {
+    type: [Number, String],
+    default: ""
   },
-};
+  height: {
+    type: [Number, String],
+    default: ""
+  }
+});
+
+const realSrc = computed(() => {
+  if (!props.src) {
+    return;
+  }
+  let real_src = props.src.split(",")[0];
+  if (isExternal(real_src)) {
+    return real_src;
+  }
+  return import.meta.env.VITE_APP_BASE_API + real_src;
+});
+
+const realSrcList = computed(() => {
+  if (!props.src) {
+    return;
+  }
+  let real_src_list = props.src.split(",");
+  let srcList = [];
+  real_src_list.forEach(item => {
+    if (isExternal(item)) {
+      return srcList.push(item);
+    }
+    return srcList.push(import.meta.env.VITE_APP_BASE_API + item);
+  });
+  return srcList;
+});
+
+const realWidth = computed(() =>
+  typeof props.width == "string" ? props.width : `${props.width}px`
+);
+
+const realHeight = computed(() =>
+  typeof props.height == "string" ? props.height : `${props.height}px`
+);
 </script>
 
 <style lang="scss" scoped>
@@ -70,14 +72,14 @@ export default {
   border-radius: 5px;
   background-color: #ebeef5;
   box-shadow: 0 0 5px 1px #ccc;
-  ::v-deep .el-image__inner {
+  :deep(.el-image__inner) {
     transition: all 0.3s;
     cursor: pointer;
     &:hover {
       transform: scale(1.2);
     }
   }
-  ::v-deep .image-slot {
+  :deep(.image-slot) {
     display: flex;
     justify-content: center;
     align-items: center;

+ 164 - 179
canteen-ui/src/components/ImageUpload/index.vue

@@ -10,28 +10,31 @@
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
       ref="imageUpload"
-      :on-remove="handleDelete"
+      :before-remove="handleDelete"
       :show-file-list="true"
       :headers="headers"
       :file-list="fileList"
       :on-preview="handlePictureCardPreview"
-      :class="{hide: this.fileList.length >= this.limit}"
+      :class="{ hide: fileList.length >= limit }"
     >
-      <i class="el-icon-plus"></i>
+      <el-icon class="avatar-uploader-icon"><plus /></el-icon>
     </el-upload>
-
     <!-- 上传提示 -->
-    <div class="el-upload__tip" slot="tip" v-if="showTip">
+    <div class="el-upload__tip" v-if="showTip">
       请上传
-      <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
-      <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
+      <template v-if="fileSize">
+        大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b>
+      </template>
+      <template v-if="fileType">
+        格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b>
+      </template>
       的文件
     </div>
 
     <el-dialog
-      :visible.sync="dialogVisible"
+      v-model="dialogVisible"
       title="预览"
-      width="800"
+      width="800px"
       append-to-body
     >
       <img
@@ -42,190 +45,172 @@
   </div>
 </template>
 
-<script>
+<script setup>
 import { getToken } from "@/utils/auth";
 import { isExternal } from "@/utils/validate";
 
-export default {
-  props: {
-    value: [String, Object, Array],
-    // 图片数量限制
-    limit: {
-      type: Number,
-      default: 5,
-    },
-    // 大小限制(MB)
-    fileSize: {
-       type: Number,
-      default: 5,
-    },
-    // 文件类型, 例如['png', 'jpg', 'jpeg']
-    fileType: {
-      type: Array,
-      default: () => ["png", "jpg", "jpeg"],
-    },
-    // 是否显示提示
-    isShowTip: {
-      type: Boolean,
-      default: true
-    }
+const props = defineProps({
+  modelValue: [String, Object, Array],
+  // 图片数量限制
+  limit: {
+    type: Number,
+    default: 5,
   },
-  data() {
-    return {
-      number: 0,
-      uploadList: [],
-      dialogImageUrl: "",
-      dialogVisible: false,
-      hideUpload: false,
-      baseUrl: process.env.VUE_APP_BASE_API,
-      uploadImgUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
-      headers: {
-        Authorization: "Bearer " + getToken(),
-      },
-      fileList: []
-    };
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 5,
   },
-  watch: {
-    value: {
-      handler(val) {
-        if (val) {
-          // 首先将值转为数组
-          const list = Array.isArray(val) ? val : this.value.split(',');
-          // 然后将数组转为对象数组
-          this.fileList = list.map(item => {
-            if (typeof item === "string") {
-              if (item.indexOf(this.baseUrl) === -1 && !isExternal(item)) {
-                  item = { name: this.baseUrl + item, url: this.baseUrl + item };
-              } else {
-                  item = { name: item, url: item };
-              }
-            }
-            return item;
-          });
-        } else {
-          this.fileList = [];
-          return [];
-        }
-      },
-      deep: true,
-      immediate: true
-    }
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    default: () => ["png", "jpg", "jpeg"],
   },
-  computed: {
-    // 是否显示提示
-    showTip() {
-      return this.isShowTip && (this.fileType || this.fileSize);
-    },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true
   },
-  methods: {
-    // 上传前loading加载
-    handleBeforeUpload(file) {
-      let isImg = false;
-      if (this.fileType.length) {
-        let fileExtension = "";
-        if (file.name.lastIndexOf(".") > -1) {
-          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
-        }
-        isImg = this.fileType.some(type => {
-          if (file.type.indexOf(type) > -1) return true;
-          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
-          return false;
-        });
-      } else {
-        isImg = file.type.indexOf("image") > -1;
-      }
+});
 
-      if (!isImg) {
-        this.$modal.msgError(`文件格式不正确,请上传${this.fileType.join("/")}图片格式文件!`);
-        return false;
-      }
-      if (file.name.includes(',')) {
-        this.$modal.msgError('文件名不正确,不能包含英文逗号!');
-        return false;
-      }
-      if (this.fileSize) {
-        const isLt = file.size / 1024 / 1024 < this.fileSize;
-        if (!isLt) {
-          this.$modal.msgError(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
-          return false;
-        }
-      }
-      this.$modal.loading("正在上传图片,请稍候...");
-      this.number++;
-    },
-    // 文件个数超出
-    handleExceed() {
-      this.$modal.msgError(`上传文件数量不能超过 ${this.limit} 个!`);
-    },
-    // 上传成功回调
-    handleUploadSuccess(res, file) {
-      if (res.code === 200) {
-        this.uploadList.push({ name: res.fileName, url: res.fileName });
-        this.uploadedSuccessfully();
-      } else {
-        this.number--;
-        this.$modal.closeLoading();
-        this.$modal.msgError(res.msg);
-        this.$refs.imageUpload.handleRemove(file);
-        this.uploadedSuccessfully();
-      }
-    },
-    // 删除图片
-    handleDelete(file) {
-      const findex = this.fileList.map(f => f.name).indexOf(file.name);
-      if (findex > -1) {
-        this.fileList.splice(findex, 1);
-        this.$emit("input", this.listToString(this.fileList));
-      }
-    },
-    // 上传失败
-    handleUploadError() {
-      this.$modal.msgError("上传图片失败,请重试");
-      this.$modal.closeLoading();
-    },
-    // 上传结束处理
-    uploadedSuccessfully() {
-      if (this.number > 0 && this.uploadList.length === this.number) {
-        this.fileList = this.fileList.concat(this.uploadList);
-        this.uploadList = [];
-        this.number = 0;
-        this.$emit("input", this.listToString(this.fileList));
-        this.$modal.closeLoading();
-      }
-    },
-    // 预览
-    handlePictureCardPreview(file) {
-      this.dialogImageUrl = file.url;
-      this.dialogVisible = true;
-    },
-    // 对象转成指定字符串分隔
-    listToString(list, separator) {
-      let strs = "";
-      separator = separator || ",";
-      for (let i in list) {
-        if (list[i].url) {
-          strs += list[i].url.replace(this.baseUrl, "") + separator;
+const { proxy } = getCurrentInstance();
+const emit = defineEmits();
+const number = ref(0);
+const uploadList = ref([]);
+const dialogImageUrl = ref("");
+const dialogVisible = ref(false);
+const baseUrl = import.meta.env.VITE_APP_BASE_API;
+const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/common/upload"); // 上传的图片服务器地址
+const headers = ref({ Authorization: "Bearer " + getToken() });
+const fileList = ref([]);
+const showTip = computed(
+  () => props.isShowTip && (props.fileType || props.fileSize)
+);
+
+watch(() => props.modelValue, val => {
+  if (val) {
+    // 首先将值转为数组
+    const list = Array.isArray(val) ? val : props.modelValue.split(",");
+    // 然后将数组转为对象数组
+    fileList.value = list.map(item => {
+      if (typeof item === "string") {
+        if (item.indexOf(baseUrl) === -1 && !isExternal(item)) {
+          item = { name: baseUrl + item, url: baseUrl + item };
+        } else {
+          item = { name: item, url: item };
         }
       }
-      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+      return item;
+    });
+  } else {
+    fileList.value = [];
+    return [];
+  }
+},{ deep: true, immediate: true });
+
+// 上传前loading加载
+function handleBeforeUpload(file) {
+  let isImg = false;
+  if (props.fileType.length) {
+    let fileExtension = "";
+    if (file.name.lastIndexOf(".") > -1) {
+      fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
     }
+    isImg = props.fileType.some(type => {
+      if (file.type.indexOf(type) > -1) return true;
+      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+      return false;
+    });
+  } else {
+    isImg = file.type.indexOf("image") > -1;
+  }
+  if (!isImg) {
+    proxy.$modal.msgError(`文件格式不正确,请上传${props.fileType.join("/")}图片格式文件!`);
+    return false;
+  }
+  if (file.name.includes(',')) {
+    proxy.$modal.msgError('文件名不正确,不能包含英文逗号!');
+    return false;
+  }
+  if (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  proxy.$modal.loading("正在上传图片,请稍候...");
+  number.value++;
+}
+
+// 文件个数超出
+function handleExceed() {
+  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+}
+
+// 上传成功回调
+function handleUploadSuccess(res, file) {
+  if (res.code === 200) {
+    uploadList.value.push({ name: res.fileName, url: res.fileName });
+    uploadedSuccessfully();
+  } else {
+    number.value--;
+    proxy.$modal.closeLoading();
+    proxy.$modal.msgError(res.msg);
+    proxy.$refs.imageUpload.handleRemove(file);
+    uploadedSuccessfully();
+  }
+}
+
+// 删除图片
+function handleDelete(file) {
+  const findex = fileList.value.map(f => f.name).indexOf(file.name);
+  if (findex > -1 && uploadList.value.length === number.value) {
+    fileList.value.splice(findex, 1);
+    emit("update:modelValue", listToString(fileList.value));
+    return false;
   }
-};
-</script>
-<style scoped lang="scss">
-// .el-upload--picture-card 控制加号部分
-::v-deep.hide .el-upload--picture-card {
-    display: none;
 }
-// 去掉动画效果
-::v-deep .el-list-enter-active,
-::v-deep .el-list-leave-active {
-    transition: all 0s;
+
+// 上传结束处理
+function uploadedSuccessfully() {
+  if (number.value > 0 && uploadList.value.length === number.value) {
+    fileList.value = fileList.value.filter(f => f.url !== undefined).concat(uploadList.value);
+    uploadList.value = [];
+    number.value = 0;
+    emit("update:modelValue", listToString(fileList.value));
+    proxy.$modal.closeLoading();
+  }
 }
 
-::v-deep .el-list-enter, .el-list-leave-active {
-  opacity: 0;
-  transform: translateY(0);
+// 上传失败
+function handleUploadError() {
+  proxy.$modal.msgError("上传图片失败");
+  proxy.$modal.closeLoading();
 }
-</style>
 
+// 预览
+function handlePictureCardPreview(file) {
+  dialogImageUrl.value = file.url;
+  dialogVisible.value = true;
+}
+
+// 对象转成指定字符串分隔
+function listToString(list, separator) {
+  let strs = "";
+  separator = separator || ",";
+  for (let i in list) {
+    if (undefined !== list[i].url && list[i].url.indexOf("blob:") !== 0) {
+      strs += list[i].url.replace(baseUrl, "") + separator;
+    }
+  }
+  return strs != "" ? strs.substr(0, strs.length - 1) : "";
+}
+</script>
+
+<style scoped lang="scss">
+// .el-upload--picture-card 控制加号部分
+:deep(.hide .el-upload--picture-card) {
+    display: none;
+}
+</style>

+ 74 - 83
canteen-ui/src/components/Pagination/index.vue

@@ -1,106 +1,97 @@
 <template>
-  <div :class="{'hidden':hidden}" class="pagination-container">
+  <div :class="{ 'hidden': hidden }" class="pagination-container">
     <el-pagination
       :background="background"
-      :current-page.sync="currentPage"
-      :page-size.sync="pageSize"
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
       :layout="layout"
       :page-sizes="pageSizes"
       :pager-count="pagerCount"
       :total="total"
-      v-bind="$attrs"
       @size-change="handleSizeChange"
       @current-change="handleCurrentChange"
     />
   </div>
 </template>
 
-<script>
+<script setup>
 import { scrollTo } from '@/utils/scroll-to'
 
-export default {
-  name: 'Pagination',
-  props: {
-    total: {
-      required: true,
-      type: Number
-    },
-    page: {
-      type: Number,
-      default: 1
-    },
-    limit: {
-      type: Number,
-      default: 20
-    },
-    pageSizes: {
-      type: Array,
-      default() {
-        return [10, 20, 30, 50]
-      }
-    },
-    // 移动端页码按钮的数量端默认值5
-    pagerCount: {
-      type: Number,
-      default: document.body.clientWidth < 992 ? 5 : 7
-    },
-    layout: {
-      type: String,
-      default: 'total, sizes, prev, pager, next, jumper'
-    },
-    background: {
-      type: Boolean,
-      default: true
-    },
-    autoScroll: {
-      type: Boolean,
-      default: true
-    },
-    hidden: {
-      type: Boolean,
-      default: false
-    }
+const props = defineProps({
+  total: {
+    required: true,
+    type: Number
   },
-  data() {
-    return {
-    };
+  page: {
+    type: Number,
+    default: 1
   },
-  computed: {
-    currentPage: {
-      get() {
-        return this.page
-      },
-      set(val) {
-        this.$emit('update:page', val)
-      }
-    },
-    pageSize: {
-      get() {
-        return this.limit
-      },
-      set(val) {
-        this.$emit('update:limit', val)
-      }
-    }
+  limit: {
+    type: Number,
+    default: 20
   },
-  methods: {
-    handleSizeChange(val) {
-      if (this.currentPage * val > this.total) {
-        this.currentPage = 1
-      }
-      this.$emit('pagination', { page: this.currentPage, limit: val })
-      if (this.autoScroll) {
-        scrollTo(0, 800)
-      }
-    },
-    handleCurrentChange(val) {
-      this.$emit('pagination', { page: val, limit: this.pageSize })
-      if (this.autoScroll) {
-        scrollTo(0, 800)
-      }
+  pageSizes: {
+    type: Array,
+    default() {
+      return [10, 20, 30, 50]
     }
+  },
+  // 移动端页码按钮的数量端默认值5
+  pagerCount: {
+    type: Number,
+    default: document.body.clientWidth < 992 ? 5 : 7
+  },
+  layout: {
+    type: String,
+    default: 'total, sizes, prev, pager, next, jumper'
+  },
+  background: {
+    type: Boolean,
+    default: true
+  },
+  autoScroll: {
+    type: Boolean,
+    default: true
+  },
+  hidden: {
+    type: Boolean,
+    default: false
+  }
+})
+
+const emit = defineEmits();
+const currentPage = computed({
+  get() {
+    return props.page
+  },
+  set(val) {
+    emit('update:page', val)
+  }
+})
+const pageSize = computed({
+  get() {
+    return props.limit
+  },
+  set(val){
+    emit('update:limit', val)
+  }
+})
+function handleSizeChange(val) {
+  if (currentPage.value * val > props.total) {
+    currentPage.value = 1
+  }
+  emit('pagination', { page: currentPage.value, limit: val })
+  if (props.autoScroll) {
+    scrollTo(0, 800)
+  }
+}
+function handleCurrentChange(val) {
+  emit('pagination', { page: val, limit: pageSize.value })
+  if (props.autoScroll) {
+    scrollTo(0, 800)
   }
 }
+
 </script>
 
 <style scoped>
@@ -110,4 +101,4 @@ export default {
 .pagination-container.hidden {
   display: none;
 }
-</style>
+</style>

+ 0 - 142
canteen-ui/src/components/PanThumb/index.vue

@@ -1,142 +0,0 @@
-<template>
-  <div :style="{zIndex:zIndex,height:height,width:width}" class="pan-item">
-    <div class="pan-info">
-      <div class="pan-info-roles-container">
-        <slot />
-      </div>
-    </div>
-    <!-- eslint-disable-next-line -->
-    <div :style="{backgroundImage: `url(${image})`}" class="pan-thumb"></div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'PanThumb',
-  props: {
-    image: {
-      type: String,
-      required: true
-    },
-    zIndex: {
-      type: Number,
-      default: 1
-    },
-    width: {
-      type: String,
-      default: '150px'
-    },
-    height: {
-      type: String,
-      default: '150px'
-    }
-  }
-}
-</script>
-
-<style scoped>
-.pan-item {
-  width: 200px;
-  height: 200px;
-  border-radius: 50%;
-  display: inline-block;
-  position: relative;
-  cursor: default;
-  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.2);
-}
-
-.pan-info-roles-container {
-  padding: 20px;
-  text-align: center;
-}
-
-.pan-thumb {
-  width: 100%;
-  height: 100%;
-  background-position: center center;
-  background-size: cover;
-  border-radius: 50%;
-  overflow: hidden;
-  position: absolute;
-  transform-origin: 95% 40%;
-  transition: all 0.3s ease-in-out;
-}
-
-/* .pan-thumb:after {
-  content: '';
-  width: 8px;
-  height: 8px;
-  position: absolute;
-  border-radius: 50%;
-  top: 40%;
-  left: 95%;
-  margin: -4px 0 0 -4px;
-  background: radial-gradient(ellipse at center, rgba(14, 14, 14, 1) 0%, rgba(125, 126, 125, 1) 100%);
-  box-shadow: 0 0 1px rgba(255, 255, 255, 0.9);
-} */
-
-.pan-info {
-  position: absolute;
-  width: inherit;
-  height: inherit;
-  border-radius: 50%;
-  overflow: hidden;
-  box-shadow: inset 0 0 0 5px rgba(0, 0, 0, 0.05);
-}
-
-.pan-info h3 {
-  color: #fff;
-  text-transform: uppercase;
-  position: relative;
-  letter-spacing: 2px;
-  font-size: 18px;
-  margin: 0 60px;
-  padding: 22px 0 0 0;
-  height: 85px;
-  font-family: 'Open Sans', Arial, sans-serif;
-  text-shadow: 0 0 1px #fff, 0 1px 2px rgba(0, 0, 0, 0.3);
-}
-
-.pan-info p {
-  color: #fff;
-  padding: 10px 5px;
-  font-style: italic;
-  margin: 0 30px;
-  font-size: 12px;
-  border-top: 1px solid rgba(255, 255, 255, 0.5);
-}
-
-.pan-info p a {
-  display: block;
-  color: #333;
-  width: 80px;
-  height: 80px;
-  background: rgba(255, 255, 255, 0.3);
-  border-radius: 50%;
-  color: #fff;
-  font-style: normal;
-  font-weight: 700;
-  text-transform: uppercase;
-  font-size: 9px;
-  letter-spacing: 1px;
-  padding-top: 24px;
-  margin: 7px auto 0;
-  font-family: 'Open Sans', Arial, sans-serif;
-  opacity: 0;
-  transition: transform 0.3s ease-in-out 0.2s, opacity 0.3s ease-in-out 0.2s, background 0.2s linear 0s;
-  transform: translateX(60px) rotate(90deg);
-}
-
-.pan-info p a:hover {
-  background: rgba(255, 255, 255, 0.5);
-}
-
-.pan-item:hover .pan-thumb {
-  transform: rotate(-110deg);
-}
-
-.pan-item:hover .pan-info p a {
-  opacity: 1;
-  transform: translateX(0px) rotate(0deg);
-}
-</style>

+ 0 - 106
canteen-ui/src/components/RightPanel/index.vue

@@ -1,106 +0,0 @@
-<template>
-  <div ref="rightPanel" class="rightPanel-container">
-    <div class="rightPanel-background" />
-    <div class="rightPanel">
-      <div class="rightPanel-items">
-        <slot />
-      </div>
-    </div>
-  </div>
-</template>
-
-<script>
-export default {
-  name: 'RightPanel',
-  props: {
-    clickNotClose: {
-      default: false,
-      type: Boolean
-    }
-  },
-  computed: {
-    show: {
-      get() {
-        return this.$store.state.settings.showSettings
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'showSettings',
-          value: val
-        })
-      }
-    }
-  },
-  watch: {
-    show(value) {
-      if (value && !this.clickNotClose) {
-        this.addEventClick()
-      }
-    }
-  },
-  mounted() {
-    this.addEventClick()
-  },
-  beforeDestroy() {
-    const elx = this.$refs.rightPanel
-    elx.remove()
-  },
-  methods: {
-    addEventClick() {
-      window.addEventListener('click', this.closeSidebar)
-    },
-    closeSidebar(evt) {
-      const parent = evt.target.closest('.el-drawer__body')
-      if (!parent) {
-        this.show = false
-        window.removeEventListener('click', this.closeSidebar)
-      }
-    }
-  }
-}
-</script>
-
-<style lang="scss" scoped>
-.rightPanel-background {
-  position: fixed;
-  top: 0;
-  left: 0;
-  opacity: 0;
-  transition: opacity .3s cubic-bezier(.7, .3, .1, 1);
-  background: rgba(0, 0, 0, .2);
-  z-index: -1;
-}
-
-.rightPanel {
-  width: 100%;
-  max-width: 260px;
-  height: 100vh;
-  position: fixed;
-  top: 0;
-  right: 0;
-  box-shadow: 0px 0px 15px 0px rgba(0, 0, 0, .05);
-  transition: all .25s cubic-bezier(.7, .3, .1, 1);
-  transform: translate(100%);
-  background: #fff;
-  z-index: 40000;
-}
-
-.handle-button {
-  width: 48px;
-  height: 48px;
-  position: absolute;
-  left: -48px;
-  text-align: center;
-  font-size: 24px;
-  border-radius: 6px 0 0 6px !important;
-  z-index: 0;
-  pointer-events: auto;
-  cursor: pointer;
-  color: #fff;
-  line-height: 48px;
-  i {
-    font-size: 24px;
-    line-height: 48px;
-  }
-}
-</style>

+ 100 - 95
canteen-ui/src/components/RightToolbar/index.vue

@@ -2,26 +2,28 @@
   <div class="top-right-btn" :style="style">
     <el-row>
       <el-tooltip class="item" effect="dark" :content="showSearch ? '隐藏搜索' : '显示搜索'" placement="top" v-if="search">
-        <el-button size="mini" circle icon="el-icon-search" @click="toggleSearch()" />
+        <el-button circle icon="Search" @click="toggleSearch()" />
       </el-tooltip>
       <el-tooltip class="item" effect="dark" content="刷新" placement="top">
-        <el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
+        <el-button circle icon="Refresh" @click="refresh()" />
       </el-tooltip>
       <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
-        <el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
+        <el-button circle icon="Menu" @click="showColumn()" v-if="showColumnsType == 'transfer'"/>
         <el-dropdown trigger="click" :hide-on-click="false" style="padding-left: 12px" v-if="showColumnsType == 'checkbox'">
-          <el-button size="mini" circle icon="el-icon-menu" />
-          <el-dropdown-menu slot="dropdown">
-            <template v-for="item in columns">
-              <el-dropdown-item :key="item.key">
-                <el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
-              </el-dropdown-item>
-            </template>
-          </el-dropdown-menu>
+          <el-button circle icon="Menu" />
+          <template #dropdown>
+            <el-dropdown-menu>
+              <template v-for="item in columns" :key="item.key">
+                <el-dropdown-item>
+                  <el-checkbox :checked="item.visible" @change="checkboxChange($event, item.label)" :label="item.label" />
+                </el-dropdown-item>
+              </template>
+            </el-dropdown-menu>
+          </template>
         </el-dropdown>
       </el-tooltip>
     </el-row>
-    <el-dialog :title="title" :visible.sync="open" append-to-body>
+    <el-dialog :title="title" v-model="open" append-to-body>
       <el-transfer
         :titles="['显示', '隐藏']"
         v-model="value"
@@ -31,99 +33,102 @@
     </el-dialog>
   </div>
 </template>
-<script>
-export default {
-  name: "RightToolbar",
-  data() {
-    return {
-      // 显隐数据
-      value: [],
-      // 弹出层标题
-      title: "显示/隐藏",
-      // 是否显示弹出层
-      open: false,
-    };
+
+<script setup>
+const props = defineProps({
+  /* 是否显示检索条件 */
+  showSearch: {
+    type: Boolean,
+    default: true,
   },
-  props: {
-    /* 是否显示检索条件 */
-    showSearch: {
-      type: Boolean,
-      default: true,
-    },
-    /* 显隐列信息 */
-    columns: {
-      type: Array,
-    },
-    /* 是否显示检索图标 */
-    search: {
-      type: Boolean,
-      default: true,
-    },
-    /* 显隐列类型(transfer穿梭框、checkbox复选框) */
-    showColumnsType: {
-      type: String,
-      default: "checkbox",
-    },
-    /* 右外边距 */
-    gutter: {
-      type: Number,
-      default: 10,
-    },
+  /* 显隐列信息 */
+  columns: {
+    type: Array,
   },
-  computed: {
-    style() {
-      const ret = {};
-      if (this.gutter) {
-        ret.marginRight = `${this.gutter / 2}px`;
-      }
-      return ret;
-    }
+  /* 是否显示检索图标 */
+  search: {
+    type: Boolean,
+    default: true,
   },
-  created() {
-    if (this.showColumnsType == 'transfer') {
-      // 显隐列初始默认隐藏列
-      for (let item in this.columns) {
-        if (this.columns[item].visible === false) {
-          this.value.push(parseInt(item));
-        }
-      }
-    }
+  /* 显隐列类型(transfer穿梭框、checkbox复选框) */
+  showColumnsType: {
+    type: String,
+    default: "checkbox",
   },
-  methods: {
-    // 搜索
-    toggleSearch() {
-      this.$emit("update:showSearch", !this.showSearch);
-    },
-    // 刷新
-    refresh() {
-      this.$emit("queryTable");
-    },
-    // 右侧列表元素变化
-    dataChange(data) {
-      for (let item in this.columns) {
-        const key = this.columns[item].key;
-        this.columns[item].visible = !data.includes(key);
-      }
-    },
-    // 打开显隐列dialog
-    showColumn() {
-      this.open = true;
-    },
-    // 勾选
-    checkboxChange(event, label) {
-      this.columns.filter(item => item.label == label)[0].visible = event;
-    }
+  /* 右外边距 */
+  gutter: {
+    type: Number,
+    default: 10,
   },
-};
+})
+
+const emits = defineEmits(['update:showSearch', 'queryTable']);
+
+// 显隐数据
+const value = ref([]);
+// 弹出层标题
+const title = ref("显示/隐藏");
+// 是否显示弹出层
+const open = ref(false);
+
+const style = computed(() => {
+  const ret = {};
+  if (props.gutter) {
+    ret.marginRight = `${props.gutter / 2}px`;
+  }
+  return ret;
+});
+
+// 搜索
+function toggleSearch() {
+  emits("update:showSearch", !props.showSearch);
+}
+
+// 刷新
+function refresh() {
+  emits("queryTable");
+}
+
+// 右侧列表元素变化
+function dataChange(data) {
+  for (let item in props.columns) {
+    const key = props.columns[item].key;
+    props.columns[item].visible = !data.includes(key);
+  }
+}
+
+// 打开显隐列dialog
+function showColumn() {
+  open.value = true;
+}
+
+if (props.showColumnsType == 'transfer') {
+  // 显隐列初始默认隐藏列
+  for (let item in props.columns) {
+    if (props.columns[item].visible === false) {
+      value.value.push(parseInt(item));
+    }
+  }
+}
+
+// 勾选
+function checkboxChange(event, label) {
+  props.columns.filter(item => item.label == label)[0].visible = event;
+}
+
 </script>
-<style lang="scss" scoped>
-::v-deep .el-transfer__button {
+
+<style lang='scss' scoped>
+:deep(.el-transfer__button) {
   border-radius: 50%;
-  padding: 12px;
   display: block;
   margin-left: 0px;
 }
-::v-deep .el-transfer__button:first-child {
+:deep(.el-transfer__button:first-child) {
   margin-bottom: 10px;
 }
+:deep(.el-dropdown-menu__item) {
+  line-height: 30px;
+  padding: 0 17px;
+}
 </style>

+ 5 - 13
canteen-ui/src/components/RuoYi/Doc/index.vue

@@ -4,18 +4,10 @@
   </div>
 </template>
 
-<script>
-export default {
-  name: 'RuoYiDoc',
-  data() {
-    return {
-      url: 'http://doc.ruoyi.vip/ruoyi-vue'
-    }
-  },
-  methods: {
-    goto() {
-      window.open(this.url)
-    }
-  }
+<script setup>
+const url = ref('http://doc.ruoyi.vip/ruoyi-vue');
+
+function goto() {
+  window.open(url.value)
 }
 </script>

+ 6 - 14
canteen-ui/src/components/RuoYi/Git/index.vue

@@ -4,18 +4,10 @@
   </div>
 </template>
 
-<script>
-export default {
-  name: 'RuoYiGit',
-  data() {
-    return {
-      url: 'https://gitee.com/y_project/RuoYi-Vue'
-    }
-  },
-  methods: {
-    goto() {
-      window.open(this.url)
-    }
-  }
+<script setup>
+const url = ref('https://gitee.com/y_project/RuoYi-Vue');
+
+function goto() {
+  window.open(url.value)
 }
-</script>
+</script>

+ 7 - 42
canteen-ui/src/components/Screenfull/index.vue

@@ -1,57 +1,22 @@
 <template>
   <div>
-    <svg-icon :icon-class="isFullscreen?'exit-fullscreen':'fullscreen'" @click="click" />
+    <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
   </div>
 </template>
 
-<script>
-import screenfull from 'screenfull'
+<script setup>
+import { useFullscreen } from '@vueuse/core'
 
-export default {
-  name: 'Screenfull',
-  data() {
-    return {
-      isFullscreen: false
-    }
-  },
-  mounted() {
-    this.init()
-  },
-  beforeDestroy() {
-    this.destroy()
-  },
-  methods: {
-    click() {
-      if (!screenfull.isEnabled) {
-        this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
-        return false
-      }
-      screenfull.toggle()
-    },
-    change() {
-      this.isFullscreen = screenfull.isFullscreen
-    },
-    init() {
-      if (screenfull.isEnabled) {
-        screenfull.on('change', this.change)
-      }
-    },
-    destroy() {
-      if (screenfull.isEnabled) {
-        screenfull.off('change', this.change)
-      }
-    }
-  }
-}
+const { isFullscreen, enter, exit, toggle } = useFullscreen();
 </script>
 
-<style scoped>
+<style lang='scss' scoped>
 .screenfull-svg {
   display: inline-block;
   cursor: pointer;
-  fill: #5a5e66;;
+  fill: #5a5e66;
   width: 20px;
   height: 20px;
   vertical-align: 10px;
 }
-</style>
+</style>

+ 38 - 49
canteen-ui/src/components/SizeSelect/index.vue

@@ -1,56 +1,45 @@
 <template>
-  <el-dropdown trigger="click" @command="handleSetSize">
-    <div>
-      <svg-icon class-name="size-icon" icon-class="size" />
-    </div>
-    <el-dropdown-menu slot="dropdown">
-      <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size===item.value" :command="item.value">
-        {{ item.label }}
-      </el-dropdown-item>
-    </el-dropdown-menu>
-  </el-dropdown>
+  <div>
+    <el-dropdown trigger="click" @command="handleSetSize">
+      <div class="size-icon--style">
+        <svg-icon class-name="size-icon" icon-class="size" />
+      </div>
+      <template #dropdown>
+        <el-dropdown-menu>
+          <el-dropdown-item v-for="item of sizeOptions" :key="item.value" :disabled="size === item.value" :command="item.value">
+            {{ item.label }}
+          </el-dropdown-item>
+        </el-dropdown-menu>
+      </template>
+    </el-dropdown>
+  </div>
 </template>
 
-<script>
-export default {
-  data() {
-    return {
-      sizeOptions: [
-        { label: 'Default', value: 'default' },
-        { label: 'Medium', value: 'medium' },
-        { label: 'Small', value: 'small' },
-        { label: 'Mini', value: 'mini' }
-      ]
-    }
-  },
-  computed: {
-    size() {
-      return this.$store.getters.size
-    }
-  },
-  methods: {
-    handleSetSize(size) {
-      this.$ELEMENT.size = size
-      this.$store.dispatch('app/setSize', size)
-      this.refreshView()
-      this.$message({
-        message: 'Switch Size Success',
-        type: 'success'
-      })
-    },
-    refreshView() {
-      // In order to make the cached page re-rendered
-      this.$store.dispatch('tagsView/delAllCachedViews', this.$route)
+<script setup>
+import useAppStore from "@/store/modules/app";
 
-      const { fullPath } = this.$route
-
-      this.$nextTick(() => {
-        this.$router.replace({
-          path: '/redirect' + fullPath
-        })
-      })
-    }
-  }
+const appStore = useAppStore();
+const size = computed(() => appStore.size);
+const route = useRoute();
+const router = useRouter();
+const { proxy } = getCurrentInstance();
+const sizeOptions = ref([
+  { label: "较大", value: "large" },
+  { label: "默认", value: "default" },
+  { label: "稍小", value: "small" },
+]);
 
+function handleSetSize(size) {
+  proxy.$modal.loading("正在设置布局大小,请稍候...");
+  appStore.setSize(size);
+  setTimeout("window.location.reload()", 1000);
 }
 </script>
+
+<style lang='scss' scoped>
+.size-icon--style {
+  font-size: 18px;
+  line-height: 50px;
+  padding-right: 7px;
+}
+</style>

+ 27 - 35
canteen-ui/src/components/SvgIcon/index.vue

@@ -1,15 +1,11 @@
 <template>
-  <div v-if="isExternal" :style="styleExternalIcon" class="svg-external-icon svg-icon" v-on="$listeners" />
-  <svg v-else :class="svgClass" aria-hidden="true" v-on="$listeners">
-    <use :xlink:href="iconName" />
+  <svg :class="svgClass" aria-hidden="true">
+    <use :xlink:href="iconName" :fill="color" />
   </svg>
 </template>
 
 <script>
-import { isExternal } from '@/utils/validate'
-
-export default {
-  name: 'SvgIcon',
+export default defineComponent({
   props: {
     iconClass: {
       type: String,
@@ -18,44 +14,40 @@ export default {
     className: {
       type: String,
       default: ''
-    }
-  },
-  computed: {
-    isExternal() {
-      return isExternal(this.iconClass)
     },
-    iconName() {
-      return `#icon-${this.iconClass}`
+    color: {
+      type: String,
+      default: ''
     },
-    svgClass() {
-      if (this.className) {
-        return 'svg-icon ' + this.className
-      } else {
+  },
+  setup(props) {
+    return {
+      iconName: computed(() => `#icon-${props.iconClass}`),
+      svgClass: computed(() => {
+        if (props.className) {
+          return `svg-icon ${props.className}`
+        }
         return 'svg-icon'
-      }
-    },
-    styleExternalIcon() {
-      return {
-        mask: `url(${this.iconClass}) no-repeat 50% 50%`,
-        '-webkit-mask': `url(${this.iconClass}) no-repeat 50% 50%`
-      }
+      })
     }
   }
-}
+})
 </script>
 
-<style scoped>
+<style scope lang="scss">
+.sub-el-icon,
+.nav-icon {
+  display: inline-block;
+  font-size: 15px;
+  margin-right: 12px;
+  position: relative;
+}
+
 .svg-icon {
   width: 1em;
   height: 1em;
-  vertical-align: -0.15em;
+  position: relative;
   fill: currentColor;
-  overflow: hidden;
-}
-
-.svg-external-icon {
-  background-color: currentColor;
-  mask-size: cover!important;
-  display: inline-block;
+  vertical-align: -2px;
 }
 </style>

+ 10 - 0
canteen-ui/src/components/SvgIcon/svgicon.js

@@ -0,0 +1,10 @@
+import * as components from '@element-plus/icons-vue'
+
+export default {
+    install: (app) => {
+        for (const key in components) {
+            const componentConfig = components[key];
+            app.component(componentConfig.name, componentConfig);
+        }
+    },
+};

+ 0 - 170
canteen-ui/src/components/ThemePicker/index.vue

@@ -1,170 +0,0 @@
-<template>
-  <el-color-picker
-    v-model="theme"
-    :predefine="['#409EFF', '#1890ff', '#304156','#212121','#11a983', '#13c2c2', '#6959CD', '#f5222d', ]"
-    class="theme-picker"
-    popper-class="theme-picker-dropdown"
-  />
-</template>
-
-<script>
-const ORIGINAL_THEME = '#409EFF' // default color
-
-export default {
-  data() {
-    return {
-      chalk: '', // content of theme-chalk css
-      theme: ''
-    }
-  },
-  computed: {
-    defaultTheme() {
-      return this.$store.state.settings.theme
-    }
-  },
-  watch: {
-    defaultTheme: {
-      handler: function(val, oldVal) {
-        this.theme = val
-      },
-      immediate: true
-    },
-    async theme(val) {
-      await this.setTheme(val)
-    }
-  },
-  created() {
-    if(this.defaultTheme !== ORIGINAL_THEME) {
-      this.setTheme(this.defaultTheme)
-    }
-  },
-  methods: {
-    async setTheme(val) {
-      const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
-      if (typeof val !== 'string') return
-      const themeCluster = this.getThemeCluster(val.replace('#', ''))
-      const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
-
-      const getHandler = (variable, id) => {
-        return () => {
-          const originalCluster = this.getThemeCluster(ORIGINAL_THEME.replace('#', ''))
-          const newStyle = this.updateStyle(this[variable], originalCluster, themeCluster)
-
-          let styleTag = document.getElementById(id)
-          if (!styleTag) {
-            styleTag = document.createElement('style')
-            styleTag.setAttribute('id', id)
-            document.head.appendChild(styleTag)
-          }
-          styleTag.innerText = newStyle
-        }
-      }
-
-      if (!this.chalk) {
-        const url = `/styles/theme-chalk/index.css`
-        await this.getCSSString(url, 'chalk')
-      }
-
-      const chalkHandler = getHandler('chalk', 'chalk-style')
-      chalkHandler()
-
-      const styles = [].slice.call(document.querySelectorAll('style'))
-        .filter(style => {
-          const text = style.innerText
-          return new RegExp(oldVal, 'i').test(text) && !/Chalk Variables/.test(text)
-        })
-      styles.forEach(style => {
-        const { innerText } = style
-        if (typeof innerText !== 'string') return
-        style.innerText = this.updateStyle(innerText, originalCluster, themeCluster)
-      })
-
-      this.$emit('change', val)
-    },
-
-    updateStyle(style, oldCluster, newCluster) {
-      let newStyle = style
-      oldCluster.forEach((color, index) => {
-        newStyle = newStyle.replace(new RegExp(color, 'ig'), newCluster[index])
-      })
-      return newStyle
-    },
-
-    getCSSString(url, variable) {
-      return new Promise(resolve => {
-        const xhr = new XMLHttpRequest()
-        xhr.onreadystatechange = () => {
-          if (xhr.readyState === 4 && xhr.status === 200) {
-            this[variable] = xhr.responseText.replace(/@font-face{[^}]+}/, '')
-            resolve()
-          }
-        }
-        xhr.open('GET', url)
-        xhr.send()
-      })
-    },
-
-    getThemeCluster(theme) {
-      const tintColor = (color, tint) => {
-        let red = parseInt(color.slice(0, 2), 16)
-        let green = parseInt(color.slice(2, 4), 16)
-        let blue = parseInt(color.slice(4, 6), 16)
-
-        if (tint === 0) { // when primary color is in its rgb space
-          return [red, green, blue].join(',')
-        } else {
-          red += Math.round(tint * (255 - red))
-          green += Math.round(tint * (255 - green))
-          blue += Math.round(tint * (255 - blue))
-
-          red = red.toString(16)
-          green = green.toString(16)
-          blue = blue.toString(16)
-
-          return `#${red}${green}${blue}`
-        }
-      }
-
-      const shadeColor = (color, shade) => {
-        let red = parseInt(color.slice(0, 2), 16)
-        let green = parseInt(color.slice(2, 4), 16)
-        let blue = parseInt(color.slice(4, 6), 16)
-
-        red = Math.round((1 - shade) * red)
-        green = Math.round((1 - shade) * green)
-        blue = Math.round((1 - shade) * blue)
-
-        red = red.toString(16)
-        green = green.toString(16)
-        blue = blue.toString(16)
-
-        return `#${red}${green}${blue}`
-      }
-
-      const clusters = [theme]
-      for (let i = 0; i <= 9; i++) {
-        clusters.push(tintColor(theme, Number((i / 10).toFixed(2))))
-      }
-      clusters.push(shadeColor(theme, 0.1))
-      return clusters
-    }
-  }
-}
-</script>
-
-<style>
-.theme-message,
-.theme-picker-dropdown {
-  z-index: 99999 !important;
-}
-
-.theme-picker .el-color-picker__trigger {
-  height: 26px !important;
-  width: 26px !important;
-  padding: 2px;
-}
-
-.theme-picker-dropdown .el-color-dropdown__link-btn {
-  display: none;
-}
-</style>

+ 157 - 134
canteen-ui/src/components/TopNav/index.vue

@@ -3,6 +3,7 @@
     :default-active="activeMenu"
     mode="horizontal"
     @select="handleSelect"
+    :ellipsis="false"
   >
     <template v-for="(item, index) in topMenus">
       <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber">
@@ -14,156 +15,158 @@
     </template>
 
     <!-- 顶部菜单超出数量折叠 -->
-    <el-submenu :style="{'--theme': theme}" index="more" :key="visibleNumber" v-if="topMenus.length > visibleNumber">
-      <template slot="title">更多菜单</template>
+    <el-sub-menu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
+      <template #title>更多菜单</template>
       <template v-for="(item, index) in topMenus">
         <el-menu-item
           :index="item.path"
           :key="index"
           v-if="index >= visibleNumber">
-          <svg-icon
-            v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
-            :icon-class="item.meta.icon"/>
-          {{ item.meta.title }}
+        <svg-icon
+          v-if="item.meta && item.meta.icon && item.meta.icon !== '#'"
+          :icon-class="item.meta.icon"/>
+        {{ item.meta.title }}
         </el-menu-item>
       </template>
-    </el-submenu>
+    </el-sub-menu>
   </el-menu>
 </template>
 
-<script>
-import { constantRoutes } from "@/router";
-import { isHttp } from "@/utils/validate";
+<script setup>
+import { constantRoutes } from "@/router"
+import { isHttp } from '@/utils/validate'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
 
+// 顶部栏初始数
+const visibleNumber = ref(null);
+// 当前激活菜单的 index
+const currentIndex = ref(null);
 // 隐藏侧边栏路由
 const hideList = ['/index', '/user/profile'];
 
-export default {
-  data() {
-    return {
-      // 顶部栏初始数
-      visibleNumber: 5,
-      // 当前激活菜单的 index
-      currentIndex: undefined
-    };
-  },
-  computed: {
-    theme() {
-      return this.$store.state.settings.theme;
-    },
-    // 顶部显示菜单
-    topMenus() {
-      let topMenus = [];
-      this.routers.map((menu) => {
-        if (menu.hidden !== true) {
-          // 兼容顶部栏一级菜单内部跳转
-          if (menu.path === "/") {
-            topMenus.push(menu.children[0]);
-          } else {
-            topMenus.push(menu);
-          }
-        }
-      });
-      return topMenus;
-    },
-    // 所有的路由信息
-    routers() {
-      return this.$store.state.permission.topbarRouters;
-    },
-    // 设置子路由
-    childrenMenus() {
-      var childrenMenus = [];
-      this.routers.map((router) => {
-        for (var item in router.children) {
-          if (router.children[item].parentPath === undefined) {
-            if(router.path === "/") {
-              router.children[item].path = "/" + router.children[item].path;
-            } else {
-              if(!isHttp(router.children[item].path)) {
-                router.children[item].path = router.path + "/" + router.children[item].path;
-              }
-            }
-            router.children[item].parentPath = router.path;
-          }
-          childrenMenus.push(router.children[item]);
-        }
-      });
-      return constantRoutes.concat(childrenMenus);
-    },
-    // 默认激活的菜单
-    activeMenu() {
-      const path = this.$route.path;
-      let activePath = path;
-      if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
-        const tmpPath = path.substring(1, path.length);
-        if (!this.$route.meta.link) {
-          activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
-          this.$store.dispatch('app/toggleSideBarHide', false);
-        }
-      } else if(!this.$route.children) {
-        activePath = path;
-        this.$store.dispatch('app/toggleSideBarHide', true);
-      }
-      this.activeRoutes(activePath);
-      return activePath;
-    },
-  },
-  beforeMount() {
-    window.addEventListener('resize', this.setVisibleNumber)
-  },
-  beforeDestroy() {
-    window.removeEventListener('resize', this.setVisibleNumber)
-  },
-  mounted() {
-    this.setVisibleNumber();
-  },
-  methods: {
-    // 根据宽度计算设置显示栏数
-    setVisibleNumber() {
-      const width = document.body.getBoundingClientRect().width / 3;
-      this.visibleNumber = parseInt(width / 85);
-    },
-    // 菜单选择事件
-    handleSelect(key, keyPath) {
-      this.currentIndex = key;
-      const route = this.routers.find(item => item.path === key);
-      if (isHttp(key)) {
-        // http(s):// 路径新窗口打开
-        window.open(key, "_blank");
-      } else if (!route || !route.children) {
-        // 没有子路由路径内部打开
-        const routeMenu = this.childrenMenus.find(item => item.path === key);
-        if (routeMenu && routeMenu.query) {
-          let query = JSON.parse(routeMenu.query);
-          this.$router.push({ path: key, query: query });
-        } else {
-          this.$router.push({ path: key });
-        }
-        this.$store.dispatch('app/toggleSideBarHide', true);
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+const route = useRoute();
+const router = useRouter();
+
+// 主题颜色
+const theme = computed(() => settingsStore.theme);
+// 所有的路由信息
+const routers = computed(() => permissionStore.topbarRouters);
+
+// 顶部显示菜单
+const topMenus = computed(() => {
+  let topMenus = [];
+  routers.value.map((menu) => {
+    if (menu.hidden !== true) {
+      // 兼容顶部栏一级菜单内部跳转
+      if (menu.path === "/") {
+          topMenus.push(menu.children[0]);
       } else {
-        // 显示左侧联动菜单
-        this.activeRoutes(key);
-        this.$store.dispatch('app/toggleSideBarHide', false);
+          topMenus.push(menu);
       }
-    },
-    // 当前激活的路由
-    activeRoutes(key) {
-      var routes = [];
-      if (this.childrenMenus && this.childrenMenus.length > 0) {
-        this.childrenMenus.map((item) => {
-          if (key == item.parentPath || (key == "index" && "" == item.path)) {
-            routes.push(item);
+    }
+  })
+  return topMenus;
+})
+
+// 设置子路由
+const childrenMenus = computed(() => {
+  let childrenMenus = [];
+  routers.value.map((router) => {
+    for (let item in router.children) {
+      if (router.children[item].parentPath === undefined) {
+        if(router.path === "/") {
+          router.children[item].path = "/" + router.children[item].path;
+        } else {
+          if(!isHttp(router.children[item].path)) {
+            router.children[item].path = router.path + "/" + router.children[item].path;
           }
-        });
-      }
-      if(routes.length > 0) {
-        this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
-      } else {
-        this.$store.dispatch('app/toggleSideBarHide', true);
+        }
+        router.children[item].parentPath = router.path;
       }
+      childrenMenus.push(router.children[item]);
+    }
+  })
+  return constantRoutes.concat(childrenMenus);
+})
+
+// 默认激活的菜单
+const activeMenu = computed(() => {
+  const path = route.path;
+  let activePath = path;
+  if (path !== undefined && path.lastIndexOf("/") > 0 && hideList.indexOf(path) === -1) {
+    const tmpPath = path.substring(1, path.length);
+    if (!route.meta.link) {
+      activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
+      appStore.toggleSideBarHide(false);
+    }
+  } else if(!route.children) {
+    activePath = path;
+    appStore.toggleSideBarHide(true);
+  }
+  activeRoutes(activePath);
+  return activePath;
+})
+
+function setVisibleNumber() {
+  const width = document.body.getBoundingClientRect().width / 3;
+  visibleNumber.value = parseInt(width / 85);
+}
+
+function handleSelect(key, keyPath) {
+  currentIndex.value = key;
+  const route = routers.value.find(item => item.path === key);
+  if (isHttp(key)) {
+    // http(s):// 路径新窗口打开
+    window.open(key, "_blank");
+  } else if (!route || !route.children) {
+    // 没有子路由路径内部打开
+    const routeMenu = childrenMenus.value.find(item => item.path === key);
+    if (routeMenu && routeMenu.query) {
+      let query = JSON.parse(routeMenu.query);
+      router.push({ path: key, query: query });
+    } else {
+      router.push({ path: key });
     }
-  },
-};
+    appStore.toggleSideBarHide(true);
+  } else {
+    // 显示左侧联动菜单
+    activeRoutes(key);
+    appStore.toggleSideBarHide(false);
+  }
+}
+
+function activeRoutes(key) {
+  let routes = [];
+  if (childrenMenus.value && childrenMenus.value.length > 0) {
+    childrenMenus.value.map((item) => {
+      if (key == item.parentPath || (key == "index" && "" == item.path)) {
+        routes.push(item);
+      }
+    });
+  }
+  if(routes.length > 0) {
+    permissionStore.setSidebarRouters(routes);
+  } else {
+    appStore.toggleSideBarHide(true);
+  }
+  return routes;
+}
+
+onMounted(() => {
+  window.addEventListener('resize', setVisibleNumber)
+})
+onBeforeUnmount(() => {
+  window.removeEventListener('resize', setVisibleNumber)
+})
+
+onMounted(() => {
+  setVisibleNumber()
+})
 </script>
 
 <style lang="scss">
@@ -176,13 +179,13 @@ export default {
   margin: 0 10px !important;
 }
 
-.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
+.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-sub-menu.is-active .el-submenu__title {
   border-bottom: 2px solid #{'var(--theme)'} !important;
   color: #303133;
 }
 
-/* submenu item */
-.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
+/* sub-menu item */
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
   float: left;
   height: 50px !important;
   line-height: 50px !important;
@@ -190,4 +193,24 @@ export default {
   padding: 0 5px !important;
   margin: 0 10px !important;
 }
+
+/* 背景色隐藏 */
+.topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):focus, .topmenu-container.el-menu--horizontal>.el-menu-item:not(.is-disabled):hover, .topmenu-container.el-menu--horizontal>.el-submenu .el-submenu__title:hover {
+  background-color: #ffffff;
+}
+
+/* 图标右间距 */
+.topmenu-container .svg-icon {
+  margin-right: 4px;
+}
+
+/* topmenu more arrow */
+.topmenu-container .el-sub-menu .el-sub-menu__icon-arrow {
+  position: static;
+  vertical-align: middle;
+  margin-left: 8px;
+  margin-top: 0px;
+}
+
+
 </style>

+ 25 - 30
canteen-ui/src/components/iFrame/index.vue

@@ -1,36 +1,31 @@
 <template>
   <div v-loading="loading" :style="'height:' + height">
-    <iframe
-      :src="src"
-      frameborder="no"
-      style="width: 100%; height: 100%"
-      scrolling="auto"
-    />
+    <iframe 
+      :src="url" 
+      frameborder="no" 
+      style="width: 100%; height: 100%" 
+      scrolling="auto" />
   </div>
 </template>
-<script>
-export default {
-  props: {
-    src: {
-      type: String,
-      required: true
-    },
-  },
-  data() {
-    return {
-      height: document.documentElement.clientHeight - 94.5 + "px;",
-      loading: true,
-      url: this.src
-    };
-  },
-  mounted: function () {
-    setTimeout(() => {
-      this.loading = false;
-    }, 300);
-    const that = this;
-    window.onresize = function temp() {
-      that.height = document.documentElement.clientHeight - 94.5 + "px;";
-    };
+
+<script setup>
+const props = defineProps({
+  src: {
+    type: String,
+    required: true
   }
-};
+})
+
+const height = ref(document.documentElement.clientHeight - 94.5 + "px;")
+const loading = ref(true)
+const url = computed(() => props.src)
+
+onMounted(() => {
+  setTimeout(() => {
+    loading.value = false;
+  }, 300);
+  window.onresize = function temp() {
+    height.value = document.documentElement.clientHeight - 94.5 + "px;";
+  };
+})
 </script>

+ 66 - 0
canteen-ui/src/directive/common/copyText.js

@@ -0,0 +1,66 @@
+/**
+* v-copyText 复制文本内容
+* Copyright (c) 2022 ruoyi
+*/
+
+export default {
+  beforeMount(el, { value, arg }) {
+    if (arg === "callback") {
+      el.$copyCallback = value;
+    } else {
+      el.$copyValue = value;
+      const handler = () => {
+        copyTextToClipboard(el.$copyValue);
+        if (el.$copyCallback) {
+          el.$copyCallback(el.$copyValue);
+        }
+      };
+      el.addEventListener("click", handler);
+      el.$destroyCopy = () => el.removeEventListener("click", handler);
+    }
+  }
+}
+
+function copyTextToClipboard(input, { target = document.body } = {}) {
+  const element = document.createElement('textarea');
+  const previouslyFocusedElement = document.activeElement;
+
+  element.value = input;
+
+  // Prevent keyboard from showing on mobile
+  element.setAttribute('readonly', '');
+
+  element.style.contain = 'strict';
+  element.style.position = 'absolute';
+  element.style.left = '-9999px';
+  element.style.fontSize = '12pt'; // Prevent zooming on iOS
+
+  const selection = document.getSelection();
+  const originalRange = selection.rangeCount > 0 && selection.getRangeAt(0);
+
+  target.append(element);
+  element.select();
+
+  // Explicit selection workaround for iOS
+  element.selectionStart = 0;
+  element.selectionEnd = input.length;
+
+  let isSuccess = false;
+  try {
+    isSuccess = document.execCommand('copy');
+  } catch { }
+
+  element.remove();
+
+  if (originalRange) {
+    selection.removeAllRanges();
+    selection.addRange(originalRange);
+  }
+
+  // Get the focus back on the previously focused element, if any
+  if (previouslyFocusedElement) {
+    previouslyFocusedElement.focus();
+  }
+
+  return isSuccess;
+}

+ 0 - 64
canteen-ui/src/directive/dialog/drag.js

@@ -1,64 +0,0 @@
-/**
-* v-dialogDrag 弹窗拖拽
-* Copyright (c) 2019 ruoyi
-*/
-
-export default {
-  bind(el, binding, vnode, oldVnode) {
-    const value = binding.value
-    if (value == false) return
-    // 获取拖拽内容头部
-    const dialogHeaderEl = el.querySelector('.el-dialog__header');
-    const dragDom = el.querySelector('.el-dialog');
-    dialogHeaderEl.style.cursor = 'move';
-    // 获取原有属性 ie dom元素.currentStyle 火狐谷歌 window.getComputedStyle(dom元素, null);
-    const sty = dragDom.currentStyle || window.getComputedStyle(dragDom, null);
-    dragDom.style.position = 'absolute';
-    dragDom.style.marginTop = 0;
-    let width = dragDom.style.width;
-    if (width.includes('%')) {
-      width = +document.body.clientWidth * (+width.replace(/\%/g, '') / 100);
-    } else {
-      width = +width.replace(/\px/g, '');
-    }
-    dragDom.style.left = `${(document.body.clientWidth - width) / 2}px`;
-    // 鼠标按下事件
-    dialogHeaderEl.onmousedown = (e) => {
-      // 鼠标按下,计算当前元素距离可视区的距离 (鼠标点击位置距离可视窗口的距离)
-      const disX = e.clientX - dialogHeaderEl.offsetLeft;
-      const disY = e.clientY - dialogHeaderEl.offsetTop;
-
-      // 获取到的值带px 正则匹配替换
-      let styL, styT;
-
-      // 注意在ie中 第一次获取到的值为组件自带50% 移动之后赋值为px
-      if (sty.left.includes('%')) {
-        styL = +document.body.clientWidth * (+sty.left.replace(/\%/g, '') / 100);
-        styT = +document.body.clientHeight * (+sty.top.replace(/\%/g, '') / 100);
-      } else {
-        styL = +sty.left.replace(/\px/g, '');
-        styT = +sty.top.replace(/\px/g, '');
-      };
-
-      // 鼠标拖拽事件
-      document.onmousemove = function (e) {
-        // 通过事件委托,计算移动的距离 (开始拖拽至结束拖拽的距离)
-        const l = e.clientX - disX;
-        const t = e.clientY - disY;
-
-        let finallyL = l + styL
-        let finallyT = t + styT
-
-        // 移动当前元素
-        dragDom.style.left = `${finallyL}px`;
-        dragDom.style.top = `${finallyT}px`;
-
-      };
-
-      document.onmouseup = function (e) {
-        document.onmousemove = null;
-        document.onmouseup = null;
-      };
-    }
-  }
-};

+ 0 - 34
canteen-ui/src/directive/dialog/dragHeight.js

@@ -1,34 +0,0 @@
-/**
- * v-dialogDragWidth 可拖动弹窗高度(右下角)
- * Copyright (c) 2019 ruoyi
- */
-
-export default {
-  bind(el) {
-    const dragDom = el.querySelector('.el-dialog');
-    const lineEl = document.createElement('div');
-    lineEl.style = 'width: 6px; background: inherit; height: 10px; position: absolute; right: 0; bottom: 0; margin: auto; z-index: 1; cursor: nwse-resize;';
-    lineEl.addEventListener('mousedown',
-      function(e) {
-        // 鼠标按下,计算当前元素距离可视区的距离
-        const disX = e.clientX - el.offsetLeft;
-        const disY = e.clientY - el.offsetTop;
-        // 当前宽度 高度
-        const curWidth = dragDom.offsetWidth;
-        const curHeight = dragDom.offsetHeight;
-        document.onmousemove = function(e) {
-          e.preventDefault(); // 移动时禁用默认事件
-          // 通过事件委托,计算移动的距离
-          const xl = e.clientX - disX;
-          const yl = e.clientY - disY
-          dragDom.style.width = `${curWidth + xl}px`;
-          dragDom.style.height = `${curHeight + yl}px`;
-        };
-        document.onmouseup = function(e) {
-          document.onmousemove = null;
-          document.onmouseup = null;
-        };
-      }, false);
-    dragDom.appendChild(lineEl);
-  }
-}

+ 0 - 30
canteen-ui/src/directive/dialog/dragWidth.js

@@ -1,30 +0,0 @@
-/**
- * v-dialogDragWidth 可拖动弹窗宽度(右侧边)
- * Copyright (c) 2019 ruoyi
- */
-
-export default {
-  bind(el) {
-    const dragDom = el.querySelector('.el-dialog');
-    const lineEl = document.createElement('div');
-    lineEl.style = 'width: 5px; background: inherit; height: 80%; position: absolute; right: 0; top: 0; bottom: 0; margin: auto; z-index: 1; cursor: w-resize;';
-    lineEl.addEventListener('mousedown',
-      function (e) {
-        // 鼠标按下,计算当前元素距离可视区的距离
-        const disX = e.clientX - el.offsetLeft;
-        // 当前宽度
-        const curWidth = dragDom.offsetWidth;
-        document.onmousemove = function (e) {
-          e.preventDefault(); // 移动时禁用默认事件
-          // 通过事件委托,计算移动的距离
-          const l = e.clientX - disX;
-          dragDom.style.width = `${curWidth + l}px`;
-        };
-        document.onmouseup = function (e) {
-          document.onmousemove = null;
-          document.onmouseup = null;
-        };
-      }, false);
-    dragDom.appendChild(lineEl);
-  }
-}

+ 6 - 20
canteen-ui/src/directive/index.js

@@ -1,23 +1,9 @@
 import hasRole from './permission/hasRole'
 import hasPermi from './permission/hasPermi'
-import dialogDrag from './dialog/drag'
-import dialogDragWidth from './dialog/dragWidth'
-import dialogDragHeight from './dialog/dragHeight'
-import clipboard from './module/clipboard'
+import copyText from './common/copyText'
 
-const install = function(Vue) {
-  Vue.directive('hasRole', hasRole)
-  Vue.directive('hasPermi', hasPermi)
-  Vue.directive('clipboard', clipboard)
-  Vue.directive('dialogDrag', dialogDrag)
-  Vue.directive('dialogDragWidth', dialogDragWidth)
-  Vue.directive('dialogDragHeight', dialogDragHeight)
-}
-
-if (window.Vue) {
-  window['hasRole'] = hasRole
-  window['hasPermi'] = hasPermi
-  Vue.use(install); // eslint-disable-line
-}
-
-export default install
+export default function directive(app){
+  app.directive('hasRole', hasRole)
+  app.directive('hasPermi', hasPermi)
+  app.directive('copyText', copyText)
+}

+ 0 - 54
canteen-ui/src/directive/module/clipboard.js

@@ -1,54 +0,0 @@
-/**
-* v-clipboard 文字复制剪贴
-* Copyright (c) 2021 ruoyi
-*/
-
-import Clipboard from 'clipboard'
-export default {
-  bind(el, binding, vnode) {
-    switch (binding.arg) {
-      case 'success':
-        el._vClipBoard_success = binding.value;
-        break;
-      case 'error':
-        el._vClipBoard_error = binding.value;
-        break;
-      default: {
-        const clipboard = new Clipboard(el, {
-          text: () => binding.value,
-          action: () => binding.arg === 'cut' ? 'cut' : 'copy'
-        });
-        clipboard.on('success', e => {
-          const callback = el._vClipBoard_success;
-          callback && callback(e);
-        });
-        clipboard.on('error', e => {
-          const callback = el._vClipBoard_error;
-          callback && callback(e);
-        });
-        el._vClipBoard = clipboard;
-      }
-    }
-  },
-  update(el, binding) {
-    if (binding.arg === 'success') {
-      el._vClipBoard_success = binding.value;
-    } else if (binding.arg === 'error') {
-      el._vClipBoard_error = binding.value;
-    } else {
-      el._vClipBoard.text = function () { return binding.value; };
-      el._vClipBoard.action = () => binding.arg === 'cut' ? 'cut' : 'copy';
-    }
-  },
-  unbind(el, binding) {
-    if (!el._vClipboard) return
-    if (binding.arg === 'success') {
-      delete el._vClipBoard_success;
-    } else if (binding.arg === 'error') {
-      delete el._vClipBoard_error;
-    } else {
-      el._vClipBoard.destroy();
-      delete el._vClipBoard;
-    }
-  }
-}

+ 4 - 4
canteen-ui/src/directive/permission/hasPermi.js

@@ -2,14 +2,14 @@
  * v-hasPermi 操作权限处理
  * Copyright (c) 2019 ruoyi
  */
-
-import store from '@/store'
+ 
+import useUserStore from '@/store/modules/user'
 
 export default {
-  inserted(el, binding, vnode) {
+  mounted(el, binding, vnode) {
     const { value } = binding
     const all_permission = "*:*:*";
-    const permissions = store.getters && store.getters.permissions
+    const permissions = useUserStore().permissions
 
     if (value && value instanceof Array && value.length > 0) {
       const permissionFlag = value

+ 5 - 5
canteen-ui/src/directive/permission/hasRole.js

@@ -2,14 +2,14 @@
  * v-hasRole 角色权限处理
  * Copyright (c) 2019 ruoyi
  */
-
-import store from '@/store'
+ 
+import useUserStore from '@/store/modules/user'
 
 export default {
-  inserted(el, binding, vnode) {
+  mounted(el, binding, vnode) {
     const { value } = binding
     const super_admin = "admin";
-    const roles = store.getters && store.getters.roles
+    const roles = useUserStore().roles
 
     if (value && value instanceof Array && value.length > 0) {
       const roleFlag = value
@@ -22,7 +22,7 @@ export default {
         el.parentNode && el.parentNode.removeChild(el)
       }
     } else {
-      throw new Error(`请设置角色权限标签值"`)
+      throw new Error(`请设置角色权限标签值`)
     }
   }
 }

+ 24 - 32
canteen-ui/src/layout/components/AppMain.vue

@@ -1,43 +1,34 @@
 <template>
   <section class="app-main">
-    <transition name="fade-transform" mode="out-in">
-      <keep-alive :include="cachedViews">
-        <router-view v-if="!$route.meta.link" :key="key" />
-      </keep-alive>
-    </transition>
+    <router-view v-slot="{ Component, route }">
+      <transition name="fade-transform" mode="out-in">
+        <keep-alive :include="tagsViewStore.cachedViews">
+          <component v-if="!route.meta.link" :is="Component" :key="route.path"/>
+        </keep-alive>
+      </transition>
+    </router-view>
     <iframe-toggle />
   </section>
 </template>
 
-<script>
+<script setup>
 import iframeToggle from "./IframeToggle/index"
+import useTagsViewStore from '@/store/modules/tagsView'
 
-export default {
-  name: 'AppMain',
-  components: { iframeToggle },
-  computed: {
-    cachedViews() {
-      return this.$store.state.tagsView.cachedViews
-    },
-    key() {
-      return this.$route.path
-    }
-  },
-  watch: {
-    $route() {
-      this.addIframe()
-    }
-  },
-  mounted() {
-    this.addIframe()
-  },
-  methods: {
-    addIframe() {
-      const {name} = this.$route
-      if (name && this.$route.meta.link) {
-        this.$store.dispatch('tagsView/addIframeView', this.$route)
-      }
-    }
+const route = useRoute()
+const tagsViewStore = useTagsViewStore()
+
+onMounted(() => {
+  addIframe()
+})
+
+watch((route) => {
+  addIframe()
+})
+
+function addIframe() {
+  if (route.meta.link) {
+    useTagsViewStore().addIframeView(route)
   }
 }
 </script>
@@ -89,3 +80,4 @@ export default {
   border-radius: 3px;
 }
 </style>
+

+ 17 - 25
canteen-ui/src/layout/components/IframeToggle/index.vue

@@ -1,33 +1,25 @@
 <template>
-  <transition-group name="fade-transform" mode="out-in">
-    <inner-link
-      v-for="(item, index) in iframeViews"
-      :key="item.path"
-      :iframeId="'iframe' + index"
-      v-show="$route.path === item.path"
-      :src="iframeUrl(item.meta.link, item.query)"
-    ></inner-link>
-  </transition-group>
+  <inner-link
+    v-for="(item, index) in tagsViewStore.iframeViews"
+    :key="item.path"
+    :iframeId="'iframe' + index"
+    v-show="route.path === item.path"
+    :src="iframeUrl(item.meta.link, item.query)"
+  ></inner-link>
 </template>
 
-<script>
+<script setup>
 import InnerLink from "../InnerLink/index";
+import useTagsViewStore from "@/store/modules/tagsView";
 
-export default {
-  components: { InnerLink },
-  computed: {
-    iframeViews() {
-      return this.$store.state.tagsView.iframeViews;
-    }
-  },
-  methods: {
-    iframeUrl(url, query) {
-      if (Object.keys(query).length > 0) {
-        let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
-        return url + "?" + params;
-      }
-      return url;
-    }
+const route = useRoute();
+const tagsViewStore = useTagsViewStore();
+
+function iframeUrl(url, query) {
+  if (Object.keys(query).length > 0) {
+    let params = Object.keys(query).map((key) => key + "=" + query[key]).join("&");
+    return url + "?" + params;
   }
+  return url;
 }
 </script>

+ 11 - 34
canteen-ui/src/layout/components/InnerLink/index.vue

@@ -1,5 +1,5 @@
 <template>
-  <div :style="'height:' + height" v-loading="loading" element-loading-text="正在加载页面,请稍候!">
+  <div :style="'height:' + height">
     <iframe
       :id="iframeId"
       style="width: 100%; height: 100%"
@@ -9,39 +9,16 @@
   </div>
 </template>
 
-<script>
-export default {
-  props: {
-    src: {
-      type: String,
-      default: "/"
-    },
-    iframeId: {
-      type: String
-    }
+<script setup>
+const props = defineProps({
+  src: {
+    type: String,
+    default: "/"
   },
-  data() {
-    return {
-      loading: false,
-      height: document.documentElement.clientHeight - 94.5 + "px;"
-    };
-  },
-  mounted() {
-    var _this = this;
-    const iframeId = ("#" + this.iframeId).replace(/\//g, "\\/");
-    const iframe = document.querySelector(iframeId);
-    // iframe页面loading控制
-    if (iframe.attachEvent) {
-      this.loading = true;
-      iframe.attachEvent("onload", function () {
-        _this.loading = false;
-      });
-    } else {
-      this.loading = true;
-      iframe.onload = function () {
-        _this.loading = false;
-      };
-    }
+  iframeId: {
+    type: String
   }
-};
+});
+
+const height = ref(document.documentElement.clientHeight - 94.5 + "px");
 </script>

+ 103 - 88
canteen-ui/src/layout/components/Navbar.vue

@@ -1,13 +1,12 @@
 <template>
   <div class="navbar">
-    <hamburger id="hamburger-container" :is-active="sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
-
-    <breadcrumb v-if="!topNav" id="breadcrumb-container" class="breadcrumb-container" />
-    <top-nav v-if="topNav" id="topmenu-container" class="topmenu-container" />
+    <hamburger id="hamburger-container" :is-active="appStore.sidebar.opened" class="hamburger-container" @toggleClick="toggleSideBar" />
+    <breadcrumb v-if="!settingsStore.topNav" id="breadcrumb-container" class="breadcrumb-container" />
+    <top-nav v-if="settingsStore.topNav" id="topmenu-container" class="topmenu-container" />
 
     <div class="right-menu">
-      <template v-if="device!=='mobile'">
-        <search id="header-search" class="right-menu-item" />
+      <template v-if="appStore.device !== 'mobile'">
+        <header-search id="header-search" class="right-menu-item" />
 
         <el-tooltip content="源码地址" effect="dark" placement="bottom">
           <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
@@ -19,115 +18,117 @@
 
         <screenfull id="screenfull" class="right-menu-item hover-effect" />
 
+        <el-tooltip content="主题模式" effect="dark" placement="bottom">
+          <div class="right-menu-item hover-effect theme-switch-wrapper" @click="toggleTheme">
+            <svg-icon v-if="settingsStore.isDark" icon-class="sunny" />
+            <svg-icon v-if="!settingsStore.isDark" icon-class="moon" />
+          </div>
+        </el-tooltip>
+
         <el-tooltip content="布局大小" effect="dark" placement="bottom">
           <size-select id="size-select" class="right-menu-item hover-effect" />
         </el-tooltip>
-
       </template>
-
-      <el-dropdown class="avatar-container right-menu-item hover-effect" trigger="click">
-        <div class="avatar-wrapper">
-          <img :src="avatar" class="user-avatar">
-          <i class="el-icon-caret-bottom" />
-        </div>
-        <el-dropdown-menu slot="dropdown">
-          <router-link to="/user/profile">
-            <el-dropdown-item>个人中心</el-dropdown-item>
-          </router-link>
-          <el-dropdown-item @click.native="setting = true">
-            <span>布局设置</span>
-          </el-dropdown-item>
-          <el-dropdown-item divided @click.native="logout">
-            <span>退出登录</span>
-          </el-dropdown-item>
-        </el-dropdown-menu>
-      </el-dropdown>
+      <div class="avatar-container">
+        <el-dropdown @command="handleCommand" class="right-menu-item hover-effect" trigger="click">
+          <div class="avatar-wrapper">
+            <img :src="userStore.avatar" class="user-avatar" />
+            <el-icon><caret-bottom /></el-icon>
+          </div>
+          <template #dropdown>
+            <el-dropdown-menu>
+              <router-link to="/user/profile">
+                <el-dropdown-item>个人中心</el-dropdown-item>
+              </router-link>
+              <el-dropdown-item command="setLayout" v-if="settingsStore.showSettings">
+                <span>布局设置</span>
+              </el-dropdown-item>
+              <el-dropdown-item divided command="logout">
+                <span>退出登录</span>
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
     </div>
   </div>
 </template>
 
-<script>
-import { mapGetters } from 'vuex'
+<script setup>
+import { ElMessageBox } from 'element-plus'
 import Breadcrumb from '@/components/Breadcrumb'
 import TopNav from '@/components/TopNav'
 import Hamburger from '@/components/Hamburger'
 import Screenfull from '@/components/Screenfull'
 import SizeSelect from '@/components/SizeSelect'
-import Search from '@/components/HeaderSearch'
+import HeaderSearch from '@/components/HeaderSearch'
 import RuoYiGit from '@/components/RuoYi/Git'
 import RuoYiDoc from '@/components/RuoYi/Doc'
+import useAppStore from '@/store/modules/app'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
 
-export default {
-  components: {
-    Breadcrumb,
-    TopNav,
-    Hamburger,
-    Screenfull,
-    SizeSelect,
-    Search,
-    RuoYiGit,
-    RuoYiDoc
-  },
-  computed: {
-    ...mapGetters([
-      'sidebar',
-      'avatar',
-      'device'
-    ]),
-    setting: {
-      get() {
-        return this.$store.state.settings.showSettings
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'showSettings',
-          value: val
-        })
-      }
-    },
-    topNav: {
-      get() {
-        return this.$store.state.settings.topNav
-      }
-    }
-  },
-  methods: {
-    toggleSideBar() {
-      this.$store.dispatch('app/toggleSideBar')
-    },
-    logout() {
-      this.$confirm('确定注销并退出系统吗?', '提示', {
-        confirmButtonText: '确定',
-        cancelButtonText: '取消',
-        type: 'warning'
-      }).then(() => {
-        this.$store.dispatch('LogOut').then(() => {
-          location.href = '/index'
-        })
-      }).catch(() => {})
-    }
+const appStore = useAppStore()
+const userStore = useUserStore()
+const settingsStore = useSettingsStore()
+
+function toggleSideBar() {
+  appStore.toggleSideBar()
+}
+
+function handleCommand(command) {
+  switch (command) {
+    case "setLayout":
+      setLayout();
+      break;
+    case "logout":
+      logout();
+      break;
+    default:
+      break;
   }
 }
+
+function logout() {
+  ElMessageBox.confirm('确定注销并退出系统吗?', '提示', {
+    confirmButtonText: '确定',
+    cancelButtonText: '取消',
+    type: 'warning'
+  }).then(() => {
+    userStore.logOut().then(() => {
+      location.href = '/index';
+    })
+  }).catch(() => { });
+}
+
+const emits = defineEmits(['setLayout'])
+function setLayout() {
+  emits('setLayout');
+}
+
+function toggleTheme() {
+  settingsStore.toggleTheme()
+}
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
 .navbar {
   height: 50px;
   overflow: hidden;
   position: relative;
-  background: #fff;
-  box-shadow: 0 1px 4px rgba(0,21,41,.08);
+  background: var(--navbar-bg);
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
 
   .hamburger-container {
     line-height: 46px;
     height: 100%;
     float: left;
     cursor: pointer;
-    transition: background .3s;
-    -webkit-tap-highlight-color:transparent;
+    transition: background 0.3s;
+    -webkit-tap-highlight-color: transparent;
 
     &:hover {
-      background: rgba(0, 0, 0, .025)
+      background: rgba(0, 0, 0, 0.025);
     }
   }
 
@@ -149,6 +150,7 @@ export default {
     float: right;
     height: 100%;
     line-height: 50px;
+    display: flex;
 
     &:focus {
       outline: none;
@@ -159,21 +161,34 @@ export default {
       padding: 0 8px;
       height: 100%;
       font-size: 18px;
-      color: #5a5e66;
+      color: var(--navbar-text);
       vertical-align: text-bottom;
 
       &.hover-effect {
         cursor: pointer;
-        transition: background .3s;
+        transition: background 0.3s;
 
         &:hover {
-          background: rgba(0, 0, 0, .025)
+          background: rgba(0, 0, 0, 0.025);
+        }
+      }
+
+      &.theme-switch-wrapper {
+        display: flex;
+        align-items: center;
+
+        svg {
+          transition: transform 0.3s;
+          
+          &:hover {
+            transform: scale(1.15);
+          }
         }
       }
     }
 
     .avatar-container {
-      margin-right: 30px;
+      margin-right: 40px;
 
       .avatar-wrapper {
         margin-top: 5px;
@@ -186,7 +201,7 @@ export default {
           border-radius: 10px;
         }
 
-        .el-icon-caret-bottom {
+        i {
           cursor: pointer;
           position: absolute;
           right: -20px;

+ 175 - 231
canteen-ui/src/layout/components/Settings/index.vue

@@ -1,260 +1,204 @@
 <template>
-  <el-drawer size="280px" :visible="visible" :with-header="false" :append-to-body="true" :show-close="false">
-    <div class="drawer-container">
-      <div>
-        <div class="setting-drawer-content">
-          <div class="setting-drawer-title">
-            <h3 class="drawer-title">主题风格设置</h3>
-          </div>
-          <div class="setting-drawer-block-checbox">
-            <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
-              <img src="@/assets/images/dark.svg" alt="dark">
-              <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
-                <i aria-label="图标: check" class="anticon anticon-check">
-                  <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
-                    <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
-                  </svg>
-                </i>
-              </div>
-            </div>
-            <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
-              <img src="@/assets/images/light.svg" alt="light">
-              <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
-                <i aria-label="图标: check" class="anticon anticon-check">
-                  <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class="">
-                    <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z"/>
-                  </svg>
-                </i>
-              </div>
-            </div>
-          </div>
-
-          <div class="drawer-item">
-            <span>主题颜色</span>
-            <theme-picker style="float: right;height: 26px;margin: -3px 8px 0 0;" @change="themeChange" />
-          </div>
+  <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
+    <div class="setting-drawer-title">
+      <h3 class="drawer-title">主题风格设置</h3>
+    </div>
+    <div class="setting-drawer-block-checbox">
+      <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-dark')">
+        <img src="@/assets/images/dark.svg" alt="dark" />
+        <div v-if="sideTheme === 'theme-dark'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
+          <i aria-label="图标: check" class="anticon anticon-check">
+            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+              <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
+            </svg>
+          </i>
         </div>
-
-        <el-divider/>
-
-        <h3 class="drawer-title">系统布局配置</h3>
-
-        <div class="drawer-item">
-          <span>开启 TopNav</span>
-          <el-switch v-model="topNav" class="drawer-switch" />
+      </div>
+      <div class="setting-drawer-block-checbox-item" @click="handleTheme('theme-light')">
+        <img src="@/assets/images/light.svg" alt="light" />
+        <div v-if="sideTheme === 'theme-light'" class="setting-drawer-block-checbox-selectIcon" style="display: block;">
+          <i aria-label="图标: check" class="anticon anticon-check">
+            <svg viewBox="64 64 896 896" data-icon="check" width="1em" height="1em" :fill="theme" aria-hidden="true" focusable="false" class>
+              <path d="M912 190h-69.9c-9.8 0-19.1 4.5-25.1 12.2L404.7 724.5 207 474a32 32 0 0 0-25.1-12.2H112c-6.7 0-10.4 7.7-6.3 12.9l273.9 347c12.8 16.2 37.4 16.2 50.3 0l488.4-618.9c4.1-5.1.4-12.8-6.3-12.8z" />
+            </svg>
+          </i>
         </div>
+      </div>
+    </div>
+    <div class="drawer-item">
+      <span>主题颜色</span>
+      <span class="comp-style">
+        <el-color-picker v-model="theme" :predefine="predefineColors" @change="themeChange"/>
+      </span>
+    </div>
+    <el-divider />
 
-        <div class="drawer-item">
-          <span>开启 Tags-Views</span>
-          <el-switch v-model="tagsView" class="drawer-switch" />
-        </div>
+    <h3 class="drawer-title">系统布局配置</h3>
 
-        <div class="drawer-item">
-          <span>固定 Header</span>
-          <el-switch v-model="fixedHeader" class="drawer-switch" />
-        </div>
+    <div class="drawer-item">
+      <span>开启 TopNav</span>
+      <span class="comp-style">
+        <el-switch v-model="settingsStore.topNav" @change="topNavChange" class="drawer-switch" />
+      </span>
+    </div>
 
-        <div class="drawer-item">
-          <span>显示 Logo</span>
-          <el-switch v-model="sidebarLogo" class="drawer-switch" />
-        </div>
+    <div class="drawer-item">
+      <span>开启 Tags-Views</span>
+      <span class="comp-style">
+        <el-switch v-model="settingsStore.tagsView" class="drawer-switch" />
+      </span>
+    </div>
 
-        <div class="drawer-item">
-          <span>动态标题</span>
-          <el-switch v-model="dynamicTitle" class="drawer-switch" />
-        </div>
+    <div class="drawer-item">
+      <span>固定 Header</span>
+      <span class="comp-style">
+        <el-switch v-model="settingsStore.fixedHeader" class="drawer-switch" />
+      </span>
+    </div>
 
-        <el-divider/>
+    <div class="drawer-item">
+      <span>显示 Logo</span>
+      <span class="comp-style">
+        <el-switch v-model="settingsStore.sidebarLogo" class="drawer-switch" />
+      </span>
+    </div>
 
-        <el-button size="small" type="primary" plain icon="el-icon-document-add" @click="saveSetting">保存配置</el-button>
-        <el-button size="small" plain icon="el-icon-refresh" @click="resetSetting">重置配置</el-button>
-      </div>
+    <div class="drawer-item">
+      <span>动态标题</span>
+      <span class="comp-style">
+        <el-switch v-model="settingsStore.dynamicTitle" class="drawer-switch" />
+      </span>
     </div>
+
+    <el-divider />
+
+    <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
+    <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
   </el-drawer>
-</template>
 
-<script>
-import ThemePicker from '@/components/ThemePicker'
+</template>
 
-export default {
-  components: { ThemePicker },
-  data() {
-    return {
-      theme: this.$store.state.settings.theme,
-      sideTheme: this.$store.state.settings.sideTheme
-    };
-  },
-  computed: {
-    visible: {
-      get() {
-        return this.$store.state.settings.showSettings
-      }
-    },
-    fixedHeader: {
-      get() {
-        return this.$store.state.settings.fixedHeader
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'fixedHeader',
-          value: val
-        })
-      }
-    },
-    topNav: {
-      get() {
-        return this.$store.state.settings.topNav
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'topNav',
-          value: val
-        })
-        if (!val) {
-          this.$store.dispatch('app/toggleSideBarHide', false);
-          this.$store.commit("SET_SIDEBAR_ROUTERS", this.$store.state.permission.defaultRoutes);
-        }
-      }
-    },
-    tagsView: {
-      get() {
-        return this.$store.state.settings.tagsView
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'tagsView',
-          value: val
-        })
-      }
-    },
-    sidebarLogo: {
-      get() {
-        return this.$store.state.settings.sidebarLogo
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'sidebarLogo',
-          value: val
-        })
-      }
-    },
-    dynamicTitle: {
-      get() {
-        return this.$store.state.settings.dynamicTitle
-      },
-      set(val) {
-        this.$store.dispatch('settings/changeSetting', {
-          key: 'dynamicTitle',
-          value: val
-        })
-      }
-    },
-  },
-  methods: {
-    themeChange(val) {
-      this.$store.dispatch('settings/changeSetting', {
-        key: 'theme',
-        value: val
-      })
-      this.theme = val;
-    },
-    handleTheme(val) {
-      this.$store.dispatch('settings/changeSetting', {
-        key: 'sideTheme',
-        value: val
-      })
-      this.sideTheme = val;
-    },
-    saveSetting() {
-      this.$modal.loading("正在保存到本地,请稍候...");
-      this.$cache.local.set(
-        "layout-setting",
-        `{
-            "topNav":${this.topNav},
-            "tagsView":${this.tagsView},
-            "fixedHeader":${this.fixedHeader},
-            "sidebarLogo":${this.sidebarLogo},
-            "dynamicTitle":${this.dynamicTitle},
-            "sideTheme":"${this.sideTheme}",
-            "theme":"${this.theme}"
-          }`
-      );
-      setTimeout(this.$modal.closeLoading(), 1000)
-    },
-    resetSetting() {
-      this.$modal.loading("正在清除设置缓存并刷新,请稍候...");
-      this.$cache.local.remove("layout-setting")
-      setTimeout("window.location.reload()", 1000)
-    }
+<script setup>
+import variables from '@/assets/styles/variables.module.scss'
+import axios from 'axios'
+import { ElLoading, ElMessage } from 'element-plus'
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+import { handleThemeStyle } from '@/utils/theme'
+
+const { proxy } = getCurrentInstance();
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+const showSettings = ref(false);
+const theme = ref(settingsStore.theme);
+const sideTheme = ref(settingsStore.sideTheme);
+const storeSettings = computed(() => settingsStore);
+const predefineColors = ref(["#409EFF", "#ff4500", "#ff8c00", "#ffd700", "#90ee90", "#00ced1", "#1e90ff", "#c71585"]);
+
+/** 是否需要topnav */
+function topNavChange(val) {
+  if (!val) {
+    appStore.toggleSideBarHide(false);
+    permissionStore.setSidebarRouters(permissionStore.defaultRoutes);
   }
 }
-</script>
 
-<style lang="scss" scoped>
-  .setting-drawer-content {
-    .setting-drawer-title {
-      margin-bottom: 12px;
-      color: rgba(0, 0, 0, .85);
-      font-size: 14px;
-      line-height: 22px;
-      font-weight: bold;
-    }
+function themeChange(val) {
+  settingsStore.theme = val;
+  handleThemeStyle(val);
+}
 
-    .setting-drawer-block-checbox {
-      display: flex;
-      justify-content: flex-start;
-      align-items: center;
-      margin-top: 10px;
-      margin-bottom: 20px;
+function handleTheme(val) {
+  settingsStore.sideTheme = val;
+  sideTheme.value = val;
+}
 
-      .setting-drawer-block-checbox-item {
-        position: relative;
-        margin-right: 16px;
-        border-radius: 2px;
-        cursor: pointer;
+function saveSetting() {
+  proxy.$modal.loading("正在保存到本地,请稍候...");
+  let layoutSetting = {
+    "topNav": storeSettings.value.topNav,
+    "tagsView": storeSettings.value.tagsView,
+    "fixedHeader": storeSettings.value.fixedHeader,
+    "sidebarLogo": storeSettings.value.sidebarLogo,
+    "dynamicTitle": storeSettings.value.dynamicTitle,
+    "sideTheme": storeSettings.value.sideTheme,
+    "theme": storeSettings.value.theme
+  };
+  localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
+  setTimeout(proxy.$modal.closeLoading(), 1000)
+}
 
-        img {
-          width: 48px;
-          height: 48px;
-        }
+function resetSetting() {
+  proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
+  localStorage.removeItem("layout-setting")
+  setTimeout("window.location.reload()", 1000)
+}
 
-        .setting-drawer-block-checbox-selectIcon {
-          position: absolute;
-          top: 0;
-          right: 0;
-          width: 100%;
-          height: 100%;
-          padding-top: 15px;
-          padding-left: 24px;
-          color: #1890ff;
-          font-weight: 700;
-          font-size: 14px;
-        }
-      }
-    }
-  }
+function openSetting() {
+  showSettings.value = true;
+}
 
-  .drawer-container {
-    padding: 20px;
+defineExpose({
+  openSetting,
+})
+</script>
+
+<style lang='scss' scoped>
+.setting-drawer-title {
+  margin-bottom: 12px;
+  color: var(--el-text-color-primary, rgba(0, 0, 0, 0.85));
+  line-height: 22px;
+  font-weight: bold;
+
+  .drawer-title {
     font-size: 14px;
-    line-height: 1.5;
-    word-wrap: break-word;
+  }
+}
 
-    .drawer-title {
-      margin-bottom: 12px;
-      color: rgba(0, 0, 0, .85);
-      font-size: 14px;
-      line-height: 22px;
+.setting-drawer-block-checbox {
+  display: flex;
+  justify-content: flex-start;
+  align-items: center;
+  margin-top: 10px;
+  margin-bottom: 20px;
+
+  .setting-drawer-block-checbox-item {
+    position: relative;
+    margin-right: 16px;
+    border-radius: 2px;
+    cursor: pointer;
+
+    img {
+      width: 48px;
+      height: 48px;
     }
 
-    .drawer-item {
-      color: rgba(0, 0, 0, .65);
+    .setting-drawer-block-checbox-selectIcon {
+      position: absolute;
+      top: 0;
+      right: 0;
+      width: 100%;
+      height: 100%;
+      padding-top: 15px;
+      padding-left: 24px;
+      color: #1890ff;
+      font-weight: 700;
       font-size: 14px;
-      padding: 12px 0;
     }
+  }
+}
 
-    .drawer-switch {
-      float: right
-    }
+.drawer-item {
+  color: var(--el-text-color-regular, rgba(0, 0, 0, 0.65));
+  padding: 12px 0;
+  font-size: 14px;
+
+  .comp-style {
+    float: right;
+    margin: -3px 8px 0px 0px;
   }
-</style>
+}
+</style>

+ 0 - 25
canteen-ui/src/layout/components/Sidebar/FixiOSBug.js

@@ -1,25 +0,0 @@
-export default {
-  computed: {
-    device() {
-      return this.$store.state.app.device
-    }
-  },
-  mounted() {
-    // In order to fix the click on menu on the ios device will trigger the mouseleave bug
-    this.fixBugIniOS()
-  },
-  methods: {
-    fixBugIniOS() {
-      const $subMenu = this.$refs.subMenu
-      if ($subMenu) {
-        const handleMouseleave = $subMenu.handleMouseleave
-        $subMenu.handleMouseleave = (e) => {
-          if (this.device === 'mobile') {
-            return
-          }
-          handleMouseleave(e)
-        }
-      }
-    }
-  }
-}

+ 0 - 33
canteen-ui/src/layout/components/Sidebar/Item.vue

@@ -1,33 +0,0 @@
-<script>
-export default {
-  name: 'MenuItem',
-  functional: true,
-  props: {
-    icon: {
-      type: String,
-      default: ''
-    },
-    title: {
-      type: String,
-      default: ''
-    }
-  },
-  render(h, context) {
-    const { icon, title } = context.props
-    const vnodes = []
-
-    if (icon) {
-      vnodes.push(<svg-icon icon-class={icon}/>)
-    }
-
-    if (title) {
-      if (title.length > 5) {
-        vnodes.push(<span slot='title' title={(title)}>{(title)}</span>)
-      } else {
-        vnodes.push(<span slot='title'>{(title)}</span>)
-      }
-    }
-    return vnodes
-  }
-}
-</script>

+ 29 - 32
canteen-ui/src/layout/components/Sidebar/Link.vue

@@ -1,43 +1,40 @@
 <template>
-  <component :is="type" v-bind="linkProps(to)">
+  <component :is="type" v-bind="linkProps()">
     <slot />
   </component>
 </template>
 
-<script>
+<script setup>
 import { isExternal } from '@/utils/validate'
 
-export default {
-  props: {
-    to: {
-      type: [String, Object],
-      required: true
-    }
-  },
-  computed: {
-    isExternal() {
-      return isExternal(this.to)
-    },
-    type() {
-      if (this.isExternal) {
-        return 'a'
-      }
-      return 'router-link'
-    }
-  },
-  methods: {
-    linkProps(to) {
-      if (this.isExternal) {
-        return {
-          href: to,
-          target: '_blank',
-          rel: 'noopener'
-        }
-      }
-      return {
-        to: to
-      }
+const props = defineProps({
+  to: {
+    type: [String, Object],
+    required: true
+  }
+})
+
+const isExt = computed(() => {
+  return isExternal(props.to)
+})
+
+const type = computed(() => {
+  if (isExt.value) {
+    return 'a'
+  }
+  return 'router-link'
+})
+
+function linkProps() {
+  if (isExt.value) {
+    return {
+      href: props.to,
+      target: '_blank',
+      rel: 'noopener'
     }
   }
+  return {
+    to: props.to
+  }
 }
 </script>

+ 37 - 31
canteen-ui/src/layout/components/Sidebar/Logo.vue

@@ -1,48 +1,54 @@
 <template>
-  <div class="sidebar-logo-container" :class="{'collapse':collapse}" :style="{ backgroundColor: sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
+  <div class="sidebar-logo-container" :class="{ 'collapse': collapse }">
     <transition name="sidebarLogoFade">
       <router-link v-if="collapse" key="collapse" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 v-else class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
+        <h1 v-else class="sidebar-title">{{ title }}</h1>
       </router-link>
       <router-link v-else key="expand" class="sidebar-logo-link" to="/">
         <img v-if="logo" :src="logo" class="sidebar-logo" />
-        <h1 class="sidebar-title" :style="{ color: sideTheme === 'theme-dark' ? variables.logoTitleColor : variables.logoLightTitleColor }">{{ title }} </h1>
+        <h1 class="sidebar-title">{{ title }}</h1>
       </router-link>
     </transition>
   </div>
 </template>
 
-<script>
-import logoImg from '@/assets/logo/logo.png'
-import variables from '@/assets/styles/variables.scss'
+<script setup>
+import logo from '@/assets/logo/logo.png'
+import useSettingsStore from '@/store/modules/settings'
+import variables from '@/assets/styles/variables.module.scss'
 
-export default {
-  name: 'SidebarLogo',
-  props: {
-    collapse: {
-      type: Boolean,
-      required: true
-    }
-  },
-  computed: {
-    variables() {
-      return variables;
-    },
-    sideTheme() {
-      return this.$store.state.settings.sideTheme
-    }
-  },
-  data() {
-    return {
-      title: process.env.VUE_APP_TITLE,
-      logo: logoImg
-    }
+defineProps({
+  collapse: {
+    type: Boolean,
+    required: true
   }
-}
+})
+
+const title = import.meta.env.VITE_APP_TITLE;
+const settingsStore = useSettingsStore();
+const sideTheme = computed(() => settingsStore.sideTheme);
+
+// 获取Logo背景色
+const getLogoBackground = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-bg)';
+  }
+  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg;
+});
+
+// 获取Logo文字颜色
+const getLogoTextColor = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-text)';
+  }
+  return sideTheme.value === 'theme-dark' ? '#fff' : variables.menuLightText;
+});
 </script>
 
 <style lang="scss" scoped>
+@import '@/assets/styles/variables.module.scss';
+
 .sidebarLogoFade-enter-active {
   transition: opacity 1.5s;
 }
@@ -57,7 +63,7 @@ export default {
   width: 100%;
   height: 50px;
   line-height: 50px;
-  background: #2b2f3a;
+  background: v-bind(getLogoBackground);
   text-align: center;
   overflow: hidden;
 
@@ -75,7 +81,7 @@ export default {
     & .sidebar-title {
       display: inline-block;
       margin: 0;
-      color: #fff;
+      color: v-bind(getLogoTextColor);
       font-weight: 600;
       line-height: 50px;
       font-size: 14px;
@@ -90,4 +96,4 @@ export default {
     }
   }
 }
-</style>
+</style>

+ 69 - 68
canteen-ui/src/layout/components/Sidebar/SidebarItem.vue

@@ -1,17 +1,20 @@
 <template>
   <div v-if="!item.hidden">
-    <template v-if="hasOneShowingChild(item.children,item) && (!onlyOneChild.children||onlyOneChild.noShowingChildren)&&!item.alwaysShow">
+    <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow">
       <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)">
-        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{'submenu-title-noDropdown':!isNest}">
-          <item :icon="onlyOneChild.meta.icon||(item.meta&&item.meta.icon)" :title="onlyOneChild.meta.title" />
+        <el-menu-item :index="resolvePath(onlyOneChild.path)" :class="{ 'submenu-title-noDropdown': !isNest }">
+          <svg-icon :icon-class="onlyOneChild.meta.icon || (item.meta && item.meta.icon)"/>
+          <template #title><span class="menu-title" :title="hasTitle(onlyOneChild.meta.title)">{{ onlyOneChild.meta.title }}</span></template>
         </el-menu-item>
       </app-link>
     </template>
 
-    <el-submenu v-else ref="subMenu" :index="resolvePath(item.path)" popper-append-to-body>
-      <template slot="title">
-        <item v-if="item.meta" :icon="item.meta && item.meta.icon" :title="item.meta.title" />
+    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" teleported>
+      <template v-if="item.meta" #title>
+        <svg-icon :icon-class="item.meta && item.meta.icon" />
+        <span class="menu-title" :title="hasTitle(item.meta.title)">{{ item.meta.title }}</span>
       </template>
+
       <sidebar-item
         v-for="(child, index) in item.children"
         :key="child.path + index"
@@ -20,80 +23,78 @@
         :base-path="resolvePath(child.path)"
         class="nest-menu"
       />
-    </el-submenu>
+    </el-sub-menu>
   </div>
 </template>
 
-<script>
-import path from 'path'
+<script setup>
 import { isExternal } from '@/utils/validate'
-import Item from './Item'
 import AppLink from './Link'
-import FixiOSBug from './FixiOSBug'
+import { getNormalPath } from '@/utils/ruoyi'
 
-export default {
-  name: 'SidebarItem',
-  components: { Item, AppLink },
-  mixins: [FixiOSBug],
-  props: {
-    // route object
-    item: {
-      type: Object,
-      required: true
-    },
-    isNest: {
-      type: Boolean,
-      default: false
-    },
-    basePath: {
-      type: String,
-      default: ''
-    }
+const props = defineProps({
+  // route object
+  item: {
+    type: Object,
+    required: true
   },
-  data() {
-    this.onlyOneChild = null
-    return {}
+  isNest: {
+    type: Boolean,
+    default: false
   },
-  methods: {
-    hasOneShowingChild(children = [], parent) {
-      if (!children) {
-        children = [];
-      }
-      const showingChildren = children.filter(item => {
-        if (item.hidden) {
-          return false
-        }
-        // Temp set(will be used if only has one showing child)
-        this.onlyOneChild = item
-        return true
-      })
-
-      // When there is only one child router, the child router is displayed by default
-      if (showingChildren.length === 1) {
-        return true
-      }
+  basePath: {
+    type: String,
+    default: ''
+  }
+})
 
-      // Show parent if there are no child router to display
-      if (showingChildren.length === 0) {
-        this.onlyOneChild = { ... parent, path: '', noShowingChildren: true }
-        return true
-      }
+const onlyOneChild = ref({});
 
+function hasOneShowingChild(children = [], parent) {
+  if (!children) {
+    children = [];
+  }
+  const showingChildren = children.filter(item => {
+    if (item.hidden) {
       return false
-    },
-    resolvePath(routePath, routeQuery) {
-      if (isExternal(routePath)) {
-        return routePath
-      }
-      if (isExternal(this.basePath)) {
-        return this.basePath
-      }
-      if (routeQuery) {
-        let query = JSON.parse(routeQuery);
-        return { path: path.resolve(this.basePath, routePath), query: query }
-      }
-      return path.resolve(this.basePath, routePath)
     }
+    onlyOneChild.value = item
+    return true
+  })
+
+  // When there is only one child router, the child router is displayed by default
+  if (showingChildren.length === 1) {
+    return true
+  }
+
+  // Show parent if there are no child router to display
+  if (showingChildren.length === 0) {
+    onlyOneChild.value = { ...parent, path: '', noShowingChildren: true }
+    return true
+  }
+
+  return false
+};
+
+function resolvePath(routePath, routeQuery) {
+  if (isExternal(routePath)) {
+    return routePath
+  }
+  if (isExternal(props.basePath)) {
+    return props.basePath
+  }
+  if (routeQuery) {
+    let query = JSON.parse(routeQuery);
+    return { path: getNormalPath(props.basePath + '/' + routePath), query: query }
+  }
+  return getNormalPath(props.basePath + '/' + routePath)
+}
+
+function hasTitle(title){
+  if (title.length > 5) {
+    return title;
+  } else {
+    return "";
   }
 }
 </script>

+ 100 - 53
canteen-ui/src/layout/components/Sidebar/index.vue

@@ -1,57 +1,104 @@
 <template>
-    <div :class="{'has-logo':showLogo}" :style="{ backgroundColor: settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground }">
-        <logo v-if="showLogo" :collapse="isCollapse" />
-        <el-scrollbar :class="settings.sideTheme" wrap-class="scrollbar-wrapper">
-            <el-menu
-                :default-active="activeMenu"
-                :collapse="isCollapse"
-                :background-color="settings.sideTheme === 'theme-dark' ? variables.menuBackground : variables.menuLightBackground"
-                :text-color="settings.sideTheme === 'theme-dark' ? variables.menuColor : variables.menuLightColor"
-                :unique-opened="true"
-                :active-text-color="settings.theme"
-                :collapse-transition="false"
-                mode="vertical"
-            >
-                <sidebar-item
-                    v-for="(route, index) in sidebarRouters"
-                    :key="route.path  + index"
-                    :item="route"
-                    :base-path="route.path"
-                />
-            </el-menu>
-        </el-scrollbar>
-    </div>
+  <div :class="{ 'has-logo': showLogo }" class="sidebar-container">
+    <logo v-if="showLogo" :collapse="isCollapse" />
+    <el-scrollbar wrap-class="scrollbar-wrapper">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :background-color="getMenuBackground"
+        :text-color="getMenuTextColor"
+        :unique-opened="true"
+        :active-text-color="theme"
+        :collapse-transition="false"
+        mode="vertical"
+        :class="sideTheme"
+      >
+        <sidebar-item
+          v-for="(route, index) in sidebarRouters"
+          :key="route.path + index"
+          :item="route"
+          :base-path="route.path"
+        />
+      </el-menu>
+    </el-scrollbar>
+  </div>
 </template>
 
-<script>
-import { mapGetters, mapState } from "vuex";
-import Logo from "./Logo";
-import SidebarItem from "./SidebarItem";
-import variables from "@/assets/styles/variables.scss";
-
-export default {
-    components: { SidebarItem, Logo },
-    computed: {
-        ...mapState(["settings"]),
-        ...mapGetters(["sidebarRouters", "sidebar"]),
-        activeMenu() {
-            const route = this.$route;
-            const { meta, path } = route;
-            // if set path, the sidebar will highlight the path you set
-            if (meta.activeMenu) {
-                return meta.activeMenu;
-            }
-            return path;
-        },
-        showLogo() {
-            return this.$store.state.settings.sidebarLogo;
-        },
-        variables() {
-            return variables;
-        },
-        isCollapse() {
-            return !this.sidebar.opened;
-        }
-    }
-};
+<script setup>
+import Logo from './Logo'
+import SidebarItem from './SidebarItem'
+import variables from '@/assets/styles/variables.module.scss'
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+const route = useRoute();
+const appStore = useAppStore()
+const settingsStore = useSettingsStore()
+const permissionStore = usePermissionStore()
+
+const sidebarRouters = computed(() => permissionStore.sidebarRouters);
+const showLogo = computed(() => settingsStore.sidebarLogo);
+const sideTheme = computed(() => settingsStore.sideTheme);
+const theme = computed(() => settingsStore.theme);
+const isCollapse = computed(() => !appStore.sidebar.opened);
+
+// 获取菜单背景色
+const getMenuBackground = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-bg)';
+  }
+  return sideTheme.value === 'theme-dark' ? variables.menuBg : variables.menuLightBg;
+});
+
+// 获取菜单文字颜色
+const getMenuTextColor = computed(() => {
+  if (settingsStore.isDark) {
+    return 'var(--sidebar-text)';
+  }
+  return sideTheme.value === 'theme-dark' ? variables.menuText : variables.menuLightText;
+});
+
+const activeMenu = computed(() => {
+  const { meta, path } = route;
+  if (meta.activeMenu) {
+    return meta.activeMenu;
+  }
+  return path;
+});
 </script>
+
+<style lang="scss" scoped>
+.sidebar-container {
+  background-color: v-bind(getMenuBackground);
+  
+  .scrollbar-wrapper {
+    background-color: v-bind(getMenuBackground);
+  }
+
+  .el-menu {
+    border: none;
+    height: 100%;
+    width: 100% !important;
+    
+    .el-menu-item, .el-sub-menu__title {
+      &:hover {
+        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
+      }
+    }
+
+    .el-menu-item {
+      color: v-bind(getMenuTextColor);
+      
+      &.is-active {
+        color: var(--menu-active-text, #409eff);
+        background-color: var(--menu-hover, rgba(0, 0, 0, 0.06)) !important;
+      }
+    }
+
+    .el-sub-menu__title {
+      color: v-bind(getMenuTextColor);
+    }
+  }
+}
+</style>

+ 86 - 73
canteen-ui/src/layout/components/TagsView/ScrollPane.vue

@@ -1,94 +1,107 @@
 <template>
-  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.native.prevent="handleScroll">
+  <el-scrollbar
+    ref="scrollContainer"
+    :vertical="false"
+    class="scroll-container"
+    @wheel.prevent="handleScroll"
+  >
     <slot />
   </el-scrollbar>
 </template>
 
-<script>
-const tagAndTagSpacing = 4 // tagAndTagSpacing
+<script setup>
+import useTagsViewStore from '@/store/modules/tagsView'
 
-export default {
-  name: 'ScrollPane',
-  data() {
-    return {
-      left: 0
-    }
-  },
-  computed: {
-    scrollWrapper() {
-      return this.$refs.scrollContainer.$refs.wrap
-    }
-  },
-  mounted() {
-    this.scrollWrapper.addEventListener('scroll', this.emitScroll, true)
-  },
-  beforeDestroy() {
-    this.scrollWrapper.removeEventListener('scroll', this.emitScroll)
-  },
-  methods: {
-    handleScroll(e) {
-      const eventDelta = e.wheelDelta || -e.deltaY * 40
-      const $scrollWrapper = this.scrollWrapper
-      $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
-    },
-    emitScroll() {
-      this.$emit('scroll')
-    },
-    moveToTarget(currentTag) {
-      const $container = this.$refs.scrollContainer.$el
-      const $containerWidth = $container.offsetWidth
-      const $scrollWrapper = this.scrollWrapper
-      const tagList = this.$parent.$refs.tag
-
-      let firstTag = null
-      let lastTag = null
-
-      // find first tag and last tag
-      if (tagList.length > 0) {
-        firstTag = tagList[0]
-        lastTag = tagList[tagList.length - 1]
-      }
+const tagAndTagSpacing = ref(4);
+const { proxy } = getCurrentInstance();
+
+const scrollWrapper = computed(() => proxy.$refs.scrollContainer.$refs.wrapRef);
+
+onMounted(() => {
+  scrollWrapper.value.addEventListener('scroll', emitScroll, true)
+})
+
+onBeforeUnmount(() => {
+  scrollWrapper.value.removeEventListener('scroll', emitScroll)
+})
+
+function handleScroll(e) {
+  const eventDelta = e.wheelDelta || -e.deltaY * 40
+  const $scrollWrapper = scrollWrapper.value;
+  $scrollWrapper.scrollLeft = $scrollWrapper.scrollLeft + eventDelta / 4
+}
+
+const emits = defineEmits()
+const emitScroll = () => {
+  emits('scroll')
+}
+
+const tagsViewStore = useTagsViewStore()
+const visitedViews = computed(() => tagsViewStore.visitedViews);
+
+function moveToTarget(currentTag) {
+  const $container = proxy.$refs.scrollContainer.$el
+  const $containerWidth = $container.offsetWidth
+  const $scrollWrapper = scrollWrapper.value;
 
-      if (firstTag === currentTag) {
-        $scrollWrapper.scrollLeft = 0
-      } else if (lastTag === currentTag) {
-        $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
-      } else {
-        // find preTag and nextTag
-        const currentIndex = tagList.findIndex(item => item === currentTag)
-        const prevTag = tagList[currentIndex - 1]
-        const nextTag = tagList[currentIndex + 1]
-
-        // the tag's offsetLeft after of nextTag
-        const afterNextTagOffsetLeft = nextTag.$el.offsetLeft + nextTag.$el.offsetWidth + tagAndTagSpacing
-
-        // the tag's offsetLeft before of prevTag
-        const beforePrevTagOffsetLeft = prevTag.$el.offsetLeft - tagAndTagSpacing
-
-        if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
-          $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
-        } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
-          $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+  let firstTag = null
+  let lastTag = null
+
+  // find first tag and last tag
+  if (visitedViews.value.length > 0) {
+    firstTag = visitedViews.value[0]
+    lastTag = visitedViews.value[visitedViews.value.length - 1]
+  }
+
+  if (firstTag === currentTag) {
+    $scrollWrapper.scrollLeft = 0
+  } else if (lastTag === currentTag) {
+    $scrollWrapper.scrollLeft = $scrollWrapper.scrollWidth - $containerWidth
+  } else {
+    const tagListDom = document.getElementsByClassName('tags-view-item');
+    const currentIndex = visitedViews.value.findIndex(item => item === currentTag)
+    let prevTag = null
+    let nextTag = null
+    for (const k in tagListDom) {
+      if (k !== 'length' && Object.hasOwnProperty.call(tagListDom, k)) {
+        if (tagListDom[k].dataset.path === visitedViews.value[currentIndex - 1].path) {
+          prevTag = tagListDom[k];
+        }
+        if (tagListDom[k].dataset.path === visitedViews.value[currentIndex + 1].path) {
+          nextTag = tagListDom[k];
         }
       }
     }
+
+    // the tag's offsetLeft after of nextTag
+    const afterNextTagOffsetLeft = nextTag.offsetLeft + nextTag.offsetWidth + tagAndTagSpacing.value
+
+    // the tag's offsetLeft before of prevTag
+    const beforePrevTagOffsetLeft = prevTag.offsetLeft - tagAndTagSpacing.value
+    if (afterNextTagOffsetLeft > $scrollWrapper.scrollLeft + $containerWidth) {
+      $scrollWrapper.scrollLeft = afterNextTagOffsetLeft - $containerWidth
+    } else if (beforePrevTagOffsetLeft < $scrollWrapper.scrollLeft) {
+      $scrollWrapper.scrollLeft = beforePrevTagOffsetLeft
+    }
   }
 }
+
+defineExpose({
+  moveToTarget,
+})
 </script>
 
-<style lang="scss" scoped>
+<style lang='scss' scoped>
 .scroll-container {
   white-space: nowrap;
   position: relative;
   overflow: hidden;
   width: 100%;
-  ::v-deep {
-    .el-scrollbar__bar {
-      bottom: 0px;
-    }
-    .el-scrollbar__wrap {
-      height: 49px;
-    }
+  :deep(.el-scrollbar__bar) {
+    bottom: 0px;
+  }
+  :deep(.el-scrollbar__wrap) {
+    height: 39px;
   }
 }
-</style>
+</style>

+ 255 - 218
canteen-ui/src/layout/components/TagsView/index.vue

@@ -1,235 +1,259 @@
 <template>
   <div id="tags-view-container" class="tags-view-container">
-    <scroll-pane ref="scrollPane" class="tags-view-wrapper" @scroll="handleScroll">
+    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
       <router-link
         v-for="tag in visitedViews"
-        ref="tag"
         :key="tag.path"
-        :class="isActive(tag)?'active':''"
+        :data-path="tag.path"
+        :class="isActive(tag) ? 'active' : ''"
         :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
-        tag="span"
         class="tags-view-item"
         :style="activeStyle(tag)"
-        @click.middle.native="!isAffix(tag)?closeSelectedTag(tag):''"
-        @contextmenu.prevent.native="openMenu(tag,$event)"
+        @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
+        @contextmenu.prevent="openMenu(tag, $event)"
       >
         {{ tag.title }}
-        <span v-if="!isAffix(tag)" class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" />
+        <span v-if="!isAffix(tag)" @click.prevent.stop="closeSelectedTag(tag)">
+          <close class="el-icon-close" style="width: 1em; height: 1em;vertical-align: middle;" />
+        </span>
       </router-link>
     </scroll-pane>
-    <ul v-show="visible" :style="{left:left+'px',top:top+'px'}" class="contextmenu">
-      <li @click="refreshSelectedTag(selectedTag)"><i class="el-icon-refresh-right"></i> 刷新页面</li>
-      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)"><i class="el-icon-close"></i> 关闭当前</li>
-      <li @click="closeOthersTags"><i class="el-icon-circle-close"></i> 关闭其他</li>
-      <li v-if="!isFirstView()" @click="closeLeftTags"><i class="el-icon-back"></i> 关闭左侧</li>
-      <li v-if="!isLastView()" @click="closeRightTags"><i class="el-icon-right"></i> 关闭右侧</li>
-      <li @click="closeAllTags(selectedTag)"><i class="el-icon-circle-close"></i> 全部关闭</li>
+    <ul v-show="visible" :style="{ left: left + 'px', top: top + 'px' }" class="contextmenu">
+      <li @click="refreshSelectedTag(selectedTag)">
+        <refresh-right style="width: 1em; height: 1em;" /> 刷新页面
+      </li>
+      <li v-if="!isAffix(selectedTag)" @click="closeSelectedTag(selectedTag)">
+        <close style="width: 1em; height: 1em;" /> 关闭当前
+      </li>
+      <li @click="closeOthersTags">
+        <circle-close style="width: 1em; height: 1em;" /> 关闭其他
+      </li>
+      <li v-if="!isFirstView()" @click="closeLeftTags">
+        <back style="width: 1em; height: 1em;" /> 关闭左侧
+      </li>
+      <li v-if="!isLastView()" @click="closeRightTags">
+        <right style="width: 1em; height: 1em;" /> 关闭右侧
+      </li>
+      <li @click="closeAllTags(selectedTag)">
+        <circle-close style="width: 1em; height: 1em;" /> 全部关闭
+      </li>
     </ul>
   </div>
 </template>
 
-<script>
+<script setup>
 import ScrollPane from './ScrollPane'
-import path from 'path'
-
-export default {
-  components: { ScrollPane },
-  data() {
-    return {
-      visible: false,
-      top: 0,
-      left: 0,
-      selectedTag: {},
-      affixTags: []
-    }
-  },
-  computed: {
-    visitedViews() {
-      return this.$store.state.tagsView.visitedViews
-    },
-    routes() {
-      return this.$store.state.permission.routes
-    },
-    theme() {
-      return this.$store.state.settings.theme;
+import { getNormalPath } from '@/utils/ruoyi'
+import useTagsViewStore from '@/store/modules/tagsView'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+
+const visible = ref(false);
+const top = ref(0);
+const left = ref(0);
+const selectedTag = ref({});
+const affixTags = ref([]);
+const scrollPaneRef = ref(null);
+
+const { proxy } = getCurrentInstance();
+const route = useRoute();
+const router = useRouter();
+
+const visitedViews = computed(() => useTagsViewStore().visitedViews);
+const routes = computed(() => usePermissionStore().routes);
+const theme = computed(() => useSettingsStore().theme);
+
+watch(route, () => {
+  addTags()
+  moveToCurrentTag()
+})
+
+watch(visible, (value) => {
+  if (value) {
+    document.body.addEventListener('click', closeMenu)
+  } else {
+    document.body.removeEventListener('click', closeMenu)
+  }
+})
+
+onMounted(() => {
+  initTags()
+  addTags()
+})
+
+function isActive(r) {
+  return r.path === route.path
+}
+
+function activeStyle(tag) {
+  if (!isActive(tag)) return {};
+  return {
+    "background-color": theme.value,
+    "border-color": theme.value
+  };
+}
+
+function isAffix(tag) {
+  return tag.meta && tag.meta.affix
+}
+
+function isFirstView() {
+  try {
+    return selectedTag.value.fullPath === '/index' || selectedTag.value.fullPath === visitedViews.value[1].fullPath
+  } catch (err) {
+    return false
+  }
+}
+
+function isLastView() {
+  try {
+    return selectedTag.value.fullPath === visitedViews.value[visitedViews.value.length - 1].fullPath
+  } catch (err) {
+    return false
+  }
+}
+
+function filterAffixTags(routes, basePath = '') {
+  let tags = []
+  routes.forEach(route => {
+    if (route.meta && route.meta.affix) {
+      const tagPath = getNormalPath(basePath + '/' + route.path)
+      tags.push({
+        fullPath: tagPath,
+        path: tagPath,
+        name: route.name,
+        meta: { ...route.meta }
+      })
     }
-  },
-  watch: {
-    $route() {
-      this.addTags()
-      this.moveToCurrentTag()
-    },
-    visible(value) {
-      if (value) {
-        document.body.addEventListener('click', this.closeMenu)
-      } else {
-        document.body.removeEventListener('click', this.closeMenu)
+    if (route.children) {
+      const tempTags = filterAffixTags(route.children, route.path)
+      if (tempTags.length >= 1) {
+        tags = [...tags, ...tempTags]
       }
     }
-  },
-  mounted() {
-    this.initTags()
-    this.addTags()
-  },
-  methods: {
-    isActive(route) {
-      return route.path === this.$route.path
-    },
-    activeStyle(tag) {
-      if (!this.isActive(tag)) return {};
-      return {
-        "background-color": this.theme,
-        "border-color": this.theme
-      };
-    },
-    isAffix(tag) {
-      return tag.meta && tag.meta.affix
-    },
-    isFirstView() {
-      try {
-        return this.selectedTag.fullPath === '/index' || this.selectedTag.fullPath === this.visitedViews[1].fullPath
-      } catch (err) {
-        return false
-      }
-    },
-    isLastView() {
-      try {
-        return this.selectedTag.fullPath === this.visitedViews[this.visitedViews.length - 1].fullPath
-      } catch (err) {
-        return false
-      }
-    },
-    filterAffixTags(routes, basePath = '/') {
-      let tags = []
-      routes.forEach(route => {
-        if (route.meta && route.meta.affix) {
-          const tagPath = path.resolve(basePath, route.path)
-          tags.push({
-            fullPath: tagPath,
-            path: tagPath,
-            name: route.name,
-            meta: { ...route.meta }
-          })
-        }
-        if (route.children) {
-          const tempTags = this.filterAffixTags(route.children, route.path)
-          if (tempTags.length >= 1) {
-            tags = [...tags, ...tempTags]
-          }
-        }
-      })
-      return tags
-    },
-    initTags() {
-      const affixTags = this.affixTags = this.filterAffixTags(this.routes)
-      for (const tag of affixTags) {
-        // Must have tag name
-        if (tag.name) {
-          this.$store.dispatch('tagsView/addVisitedView', tag)
-        }
-      }
-    },
-    addTags() {
-      const { name } = this.$route
-      if (name) {
-        this.$store.dispatch('tagsView/addView', this.$route)
-      }
-    },
-    moveToCurrentTag() {
-      const tags = this.$refs.tag
-      this.$nextTick(() => {
-        for (const tag of tags) {
-          if (tag.to.path === this.$route.path) {
-            this.$refs.scrollPane.moveToTarget(tag)
-            // when query is different then update
-            if (tag.to.fullPath !== this.$route.fullPath) {
-              this.$store.dispatch('tagsView/updateVisitedView', this.$route)
-            }
-            break
-          }
-        }
-      })
-    },
-    refreshSelectedTag(view) {
-      this.$tab.refreshPage(view);
-      if (this.$route.meta.link) {
-        this.$store.dispatch('tagsView/delIframeView', this.$route)
-      }
-    },
-    closeSelectedTag(view) {
-      this.$tab.closePage(view).then(({ visitedViews }) => {
-        if (this.isActive(view)) {
-          this.toLastView(visitedViews, view)
-        }
-      })
-    },
-    closeRightTags() {
-      this.$tab.closeRightPage(this.selectedTag).then(visitedViews => {
-        if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
-          this.toLastView(visitedViews)
-        }
-      })
-    },
-    closeLeftTags() {
-      this.$tab.closeLeftPage(this.selectedTag).then(visitedViews => {
-        if (!visitedViews.find(i => i.fullPath === this.$route.fullPath)) {
-          this.toLastView(visitedViews)
-        }
-      })
-    },
-    closeOthersTags() {
-      this.$router.push(this.selectedTag.fullPath).catch(()=>{});
-      this.$tab.closeOtherPage(this.selectedTag).then(() => {
-        this.moveToCurrentTag()
-      })
-    },
-    closeAllTags(view) {
-      this.$tab.closeAllPage().then(({ visitedViews }) => {
-        if (this.affixTags.some(tag => tag.path === this.$route.path)) {
-          return
-        }
-        this.toLastView(visitedViews, view)
-      })
-    },
-    toLastView(visitedViews, view) {
-      const latestView = visitedViews.slice(-1)[0]
-      if (latestView) {
-        this.$router.push(latestView.fullPath)
-      } else {
-        // now the default is to redirect to the home page if there is no tags-view,
-        // you can adjust it according to your needs.
-        if (view.name === 'Dashboard') {
-          // to reload home page
-          this.$router.replace({ path: '/redirect' + view.fullPath })
-        } else {
-          this.$router.push('/')
+  })
+  return tags
+}
+
+function initTags() {
+  const res = filterAffixTags(routes.value);
+  affixTags.value = res;
+  for (const tag of res) {
+    // Must have tag name
+    if (tag.name) {
+       useTagsViewStore().addVisitedView(tag)
+    }
+  }
+}
+
+function addTags() {
+  const { name } = route
+  if (name) {
+    useTagsViewStore().addView(route)
+  }
+}
+
+function moveToCurrentTag() {
+  nextTick(() => {
+    for (const r of visitedViews.value) {
+      if (r.path === route.path) {
+        scrollPaneRef.value.moveToTarget(r);
+        // when query is different then update
+        if (r.fullPath !== route.fullPath) {
+          useTagsViewStore().updateVisitedView(route)
         }
       }
-    },
-    openMenu(tag, e) {
-      const menuMinWidth = 105
-      const offsetLeft = this.$el.getBoundingClientRect().left // container margin left
-      const offsetWidth = this.$el.offsetWidth // container width
-      const maxLeft = offsetWidth - menuMinWidth // left boundary
-      const left = e.clientX - offsetLeft + 15 // 15: margin right
-
-      if (left > maxLeft) {
-        this.left = maxLeft
-      } else {
-        this.left = left
-      }
+    }
+  })
+}
+
+function refreshSelectedTag(view) {
+  proxy.$tab.refreshPage(view);
+  if (route.meta.link) {
+    useTagsViewStore().delIframeView(route);
+  }
+}
 
-      this.top = e.clientY
-      this.visible = true
-      this.selectedTag = tag
-    },
-    closeMenu() {
-      this.visible = false
-    },
-    handleScroll() {
-      this.closeMenu()
+function closeSelectedTag(view) {
+  proxy.$tab.closePage(view).then(({ visitedViews }) => {
+    if (isActive(view)) {
+      toLastView(visitedViews, view)
     }
+  })
+}
+
+function closeRightTags() {
+  proxy.$tab.closeRightPage(selectedTag.value).then(visitedViews => {
+    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+      toLastView(visitedViews)
+    }
+  })
+}
+
+function closeLeftTags() {
+  proxy.$tab.closeLeftPage(selectedTag.value).then(visitedViews => {
+    if (!visitedViews.find(i => i.fullPath === route.fullPath)) {
+      toLastView(visitedViews)
+    }
+  })
+}
+
+function closeOthersTags() {
+  router.push(selectedTag.value).catch(() => { });
+  proxy.$tab.closeOtherPage(selectedTag.value).then(() => {
+    moveToCurrentTag()
+  })
+}
+
+function closeAllTags(view) {
+  proxy.$tab.closeAllPage().then(({ visitedViews }) => {
+    if (affixTags.value.some(tag => tag.path === route.path)) {
+      return
+    }
+    toLastView(visitedViews, view)
+  })
+}
+
+function toLastView(visitedViews, view) {
+  const latestView = visitedViews.slice(-1)[0]
+  if (latestView) {
+    router.push(latestView.fullPath)
+  } else {
+    // now the default is to redirect to the home page if there is no tags-view,
+    // you can adjust it according to your needs.
+    if (view.name === 'Dashboard') {
+      // to reload home page
+      router.replace({ path: '/redirect' + view.fullPath })
+    } else {
+      router.push('/')
+    }
+  }
+}
+
+function openMenu(tag, e) {
+  const menuMinWidth = 105
+  const offsetLeft = proxy.$el.getBoundingClientRect().left // container margin left
+  const offsetWidth = proxy.$el.offsetWidth // container width
+  const maxLeft = offsetWidth - menuMinWidth // left boundary
+  const l = e.clientX - offsetLeft + 15 // 15: margin right
+
+  if (l > maxLeft) {
+    left.value = maxLeft
+  } else {
+    left.value = l
   }
+
+  top.value = e.clientY
+  visible.value = true
+  selectedTag.value = tag
+}
+
+function closeMenu() {
+  visible.value = false
+}
+
+function handleScroll() {
+  closeMenu()
 }
 </script>
 
@@ -237,9 +261,10 @@ export default {
 .tags-view-container {
   height: 34px;
   width: 100%;
-  background: #fff;
-  border-bottom: 1px solid #d8dce5;
+  background: var(--tags-bg, #fff);
+  border-bottom: 1px solid var(--tags-item-border, #d8dce5);
   box-shadow: 0 1px 3px 0 rgba(0, 0, 0, .12), 0 0 3px 0 rgba(0, 0, 0, .04);
+
   .tags-view-wrapper {
     .tags-view-item {
       display: inline-block;
@@ -247,23 +272,27 @@ export default {
       cursor: pointer;
       height: 26px;
       line-height: 26px;
-      border: 1px solid #d8dce5;
-      color: #495060;
-      background: #fff;
+      border: 1px solid var(--tags-item-border, #d8dce5);
+      color: var(--tags-item-text, #495060);
+      background: var(--tags-item-bg, #fff);
       padding: 0 8px;
       font-size: 12px;
       margin-left: 5px;
       margin-top: 4px;
+
       &:first-of-type {
         margin-left: 15px;
       }
+
       &:last-of-type {
         margin-right: 15px;
       }
+
       &.active {
         background-color: #42b983;
         color: #fff;
         border-color: #42b983;
+
         &::before {
           content: '';
           background: #fff;
@@ -272,14 +301,15 @@ export default {
           height: 8px;
           border-radius: 50%;
           position: relative;
-          margin-right: 2px;
+          margin-right: 5px;
         }
       }
     }
   }
+
   .contextmenu {
     margin: 0;
-    background: #fff;
+    background: var(--el-bg-color-overlay, #fff);
     z-index: 3000;
     position: absolute;
     list-style-type: none;
@@ -287,14 +317,17 @@ export default {
     border-radius: 4px;
     font-size: 12px;
     font-weight: 400;
-    color: #333;
+    color: var(--tags-item-text, #333);
     box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, .3);
+    border: 1px solid var(--el-border-color-light, #e4e7ed);
+
     li {
       margin: 0;
       padding: 7px 16px;
       cursor: pointer;
+
       &:hover {
-        background: #eee;
+        background: var(--tags-item-hover, #eee);
       }
     }
   }
@@ -313,16 +346,20 @@ export default {
       text-align: center;
       transition: all .3s cubic-bezier(.645, .045, .355, 1);
       transform-origin: 100% 50%;
+
       &:before {
         transform: scale(.6);
         display: inline-block;
         vertical-align: -3px;
       }
+
       &:hover {
-        background-color: #b4bccc;
+        background-color: var(--tags-close-hover, #b4bccc);
         color: #fff;
+        width: 12px !important;
+        height: 12px !important;
       }
     }
   }
 }
-</style>
+</style>

+ 0 - 1
canteen-ui/src/layout/components/index.js

@@ -1,5 +1,4 @@
 export { default as AppMain } from './AppMain'
 export { default as Navbar } from './Navbar'
 export { default as Settings } from './Settings'
-export { default as Sidebar } from './Sidebar/index.vue'
 export { default as TagsView } from './TagsView/index.vue'

+ 95 - 92
canteen-ui/src/layout/index.vue

@@ -1,111 +1,114 @@
 <template>
-  <div :class="classObj" class="app-wrapper" :style="{'--current-color': theme}">
-    <div v-if="device==='mobile'&&sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
-    <sidebar v-if="!sidebar.hide" class="sidebar-container"/>
-    <div :class="{hasTagsView:needTagsView,sidebarHide:sidebar.hide}" class="main-container">
-      <div :class="{'fixed-header':fixedHeader}">
-        <navbar/>
-        <tags-view v-if="needTagsView"/>
+  <div :class="classObj" class="app-wrapper" :style="{ '--current-color': theme }">
+    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside"/>
+    <sidebar v-if="!sidebar.hide" class="sidebar-container" />
+    <div :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }" class="main-container">
+      <div :class="{ 'fixed-header': fixedHeader }">
+        <navbar @setLayout="setLayout" />
+        <tags-view v-if="needTagsView" />
       </div>
-      <app-main/>
-      <right-panel>
-        <settings/>
-      </right-panel>
+      <app-main />
+      <settings ref="settingRef" />
     </div>
   </div>
 </template>
 
-<script>
-import RightPanel from '@/components/RightPanel'
-import { AppMain, Navbar, Settings, Sidebar, TagsView } from './components'
-import ResizeMixin from './mixin/ResizeHandler'
-import { mapState } from 'vuex'
-import variables from '@/assets/styles/variables.scss'
-
-export default {
-  name: 'Layout',
-  components: {
-    AppMain,
-    Navbar,
-    RightPanel,
-    Settings,
-    Sidebar,
-    TagsView
-  },
-  mixins: [ResizeMixin],
-  computed: {
-    ...mapState({
-      theme: state => state.settings.theme,
-      sideTheme: state => state.settings.sideTheme,
-      sidebar: state => state.app.sidebar,
-      device: state => state.app.device,
-      needTagsView: state => state.settings.tagsView,
-      fixedHeader: state => state.settings.fixedHeader
-    }),
-    classObj() {
-      return {
-        hideSidebar: !this.sidebar.opened,
-        openSidebar: this.sidebar.opened,
-        withoutAnimation: this.sidebar.withoutAnimation,
-        mobile: this.device === 'mobile'
-      }
-    },
-    variables() {
-      return variables;
-    }
-  },
-  methods: {
-    handleClickOutside() {
-      this.$store.dispatch('app/closeSideBar', { withoutAnimation: false })
-    }
+<script setup>
+import { useWindowSize } from '@vueuse/core'
+import Sidebar from './components/Sidebar/index.vue'
+import { AppMain, Navbar, Settings, TagsView } from './components'
+import defaultSettings from '@/settings'
+
+import useAppStore from '@/store/modules/app'
+import useSettingsStore from '@/store/modules/settings'
+
+const settingsStore = useSettingsStore()
+const theme = computed(() => settingsStore.theme);
+const sideTheme = computed(() => settingsStore.sideTheme);
+const sidebar = computed(() => useAppStore().sidebar);
+const device = computed(() => useAppStore().device);
+const needTagsView = computed(() => settingsStore.tagsView);
+const fixedHeader = computed(() => settingsStore.fixedHeader);
+
+const classObj = computed(() => ({
+  hideSidebar: !sidebar.value.opened,
+  openSidebar: sidebar.value.opened,
+  withoutAnimation: sidebar.value.withoutAnimation,
+  mobile: device.value === 'mobile'
+}))
+
+const { width, height } = useWindowSize();
+const WIDTH = 992; // refer to Bootstrap's responsive design
+
+watch(() => device.value, () => {
+  if (device.value === 'mobile' && sidebar.value.opened) {
+    useAppStore().closeSideBar({ withoutAnimation: false })
+  }
+})
+
+watchEffect(() => {
+  if (width.value - 1 < WIDTH) {
+    useAppStore().toggleDevice('mobile')
+    useAppStore().closeSideBar({ withoutAnimation: true })
+  } else {
+    useAppStore().toggleDevice('desktop')
   }
+})
+
+function handleClickOutside() {
+  useAppStore().closeSideBar({ withoutAnimation: false })
+}
+
+const settingRef = ref(null);
+function setLayout() {
+  settingRef.value.openSetting();
 }
 </script>
 
 <style lang="scss" scoped>
-  @import "~@/assets/styles/mixin.scss";
-  @import "~@/assets/styles/variables.scss";
-
-  .app-wrapper {
-    @include clearfix;
-    position: relative;
-    height: 100%;
-    width: 100%;
-
-    &.mobile.openSidebar {
-      position: fixed;
-      top: 0;
-    }
-  }
+  @import "@/assets/styles/mixin.scss";
+  @import "@/assets/styles/variables.module.scss";
 
-  .drawer-bg {
-    background: #000;
-    opacity: 0.3;
-    width: 100%;
-    top: 0;
-    height: 100%;
-    position: absolute;
-    z-index: 999;
-  }
+.app-wrapper {
+  @include clearfix;
+  position: relative;
+  height: 100%;
+  width: 100%;
 
-  .fixed-header {
+  &.mobile.openSidebar {
     position: fixed;
     top: 0;
-    right: 0;
-    z-index: 9;
-    width: calc(100% - #{$base-sidebar-width});
-    transition: width 0.28s;
   }
+}
 
-  .hideSidebar .fixed-header {
-    width: calc(100% - 54px);
-  }
+.drawer-bg {
+  background: #000;
+  opacity: 0.3;
+  width: 100%;
+  top: 0;
+  height: 100%;
+  position: absolute;
+  z-index: 999;
+}
 
-  .sidebarHide .fixed-header {
-    width: 100%;
-  }
+.fixed-header {
+  position: fixed;
+  top: 0;
+  right: 0;
+  z-index: 9;
+  width: calc(100% - #{$base-sidebar-width});
+  transition: width 0.28s;
+}
 
-  .mobile .fixed-header {
-    width: 100%;
-  }
-</style>
+.hideSidebar .fixed-header {
+  width: calc(100% - 54px);
+}
+
+.sidebarHide .fixed-header {
+  width: 100%;
+}
+
+.mobile .fixed-header {
+  width: 100%;
+}
+</style>

+ 0 - 45
canteen-ui/src/layout/mixin/ResizeHandler.js

@@ -1,45 +0,0 @@
-import store from '@/store'
-
-const { body } = document
-const WIDTH = 992 // refer to Bootstrap's responsive design
-
-export default {
-  watch: {
-    $route(route) {
-      if (this.device === 'mobile' && this.sidebar.opened) {
-        store.dispatch('app/closeSideBar', { withoutAnimation: false })
-      }
-    }
-  },
-  beforeMount() {
-    window.addEventListener('resize', this.$_resizeHandler)
-  },
-  beforeDestroy() {
-    window.removeEventListener('resize', this.$_resizeHandler)
-  },
-  mounted() {
-    const isMobile = this.$_isMobile()
-    if (isMobile) {
-      store.dispatch('app/toggleDevice', 'mobile')
-      store.dispatch('app/closeSideBar', { withoutAnimation: true })
-    }
-  },
-  methods: {
-    // use $_ for mixins properties
-    // https://vuejs.org/v2/style-guide/index.html#Private-property-names-essential
-    $_isMobile() {
-      const rect = body.getBoundingClientRect()
-      return rect.width - 1 < WIDTH
-    },
-    $_resizeHandler() {
-      if (!document.hidden) {
-        const isMobile = this.$_isMobile()
-        store.dispatch('app/toggleDevice', isMobile ? 'mobile' : 'desktop')
-
-        if (isMobile) {
-          store.dispatch('app/closeSideBar', { withoutAnimation: true })
-        }
-      }
-    }
-  }
-}

+ 50 - 54
canteen-ui/src/main.js

@@ -1,28 +1,37 @@
-import Vue from 'vue'
+import { createApp } from 'vue'
 
 import Cookies from 'js-cookie'
 
-import Element from 'element-ui'
-import './assets/styles/element-variables.scss'
+import ElementPlus from 'element-plus'
+import 'element-plus/dist/index.css'
+import 'element-plus/theme-chalk/dark/css-vars.css'
+import locale from 'element-plus/es/locale/lang/zh-cn'
 
 import '@/assets/styles/index.scss' // global css
-import '@/assets/styles/ruoyi.scss' // ruoyi css
+
 import App from './App'
 import store from './store'
 import router from './router'
 import directive from './directive' // directive
+
+// 注册指令
 import plugins from './plugins' // plugins
 import { download } from '@/utils/request'
 
-import './assets/icons' // icon
+// svg图标
+import 'virtual:svg-icons-register'
+import SvgIcon from '@/components/SvgIcon'
+import elementIcons from '@/components/SvgIcon/svgicon'
+
 import './permission' // permission control
-import { getDicts } from "@/api/system/dict/data";
-import { getConfigKey } from "@/api/system/config";
-import { parseTime, resetForm, addDateRange, selectDictLabel, selectDictLabels, handleTree } from "@/utils/ruoyi";
+
+import { useDict } from '@/utils/dict'
+import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
+
 // 分页组件
-import Pagination from "@/components/Pagination";
+import Pagination from '@/components/Pagination'
 // 自定义表格工具组件
-import RightToolbar from "@/components/RightToolbar"
+import RightToolbar from '@/components/RightToolbar'
 // 富文本组件
 import Editor from "@/components/Editor"
 // 文件上传组件
@@ -33,54 +42,41 @@ import ImageUpload from "@/components/ImageUpload"
 import ImagePreview from "@/components/ImagePreview"
 // 字典标签组件
 import DictTag from '@/components/DictTag'
-// 头部标签组件
-import VueMeta from 'vue-meta'
-// 字典数据组件
-import DictData from '@/components/DictData'
+
+const app = createApp(App)
 
 // 全局方法挂载
-Vue.prototype.getDicts = getDicts
-Vue.prototype.getConfigKey = getConfigKey
-Vue.prototype.parseTime = parseTime
-Vue.prototype.resetForm = resetForm
-Vue.prototype.addDateRange = addDateRange
-Vue.prototype.selectDictLabel = selectDictLabel
-Vue.prototype.selectDictLabels = selectDictLabels
-Vue.prototype.download = download
-Vue.prototype.handleTree = handleTree
+app.config.globalProperties.useDict = useDict
+app.config.globalProperties.download = download
+app.config.globalProperties.parseTime = parseTime
+app.config.globalProperties.resetForm = resetForm
+app.config.globalProperties.handleTree = handleTree
+app.config.globalProperties.addDateRange = addDateRange
+app.config.globalProperties.selectDictLabel = selectDictLabel
+app.config.globalProperties.selectDictLabels = selectDictLabels
 
 // 全局组件挂载
-Vue.component('DictTag', DictTag)
-Vue.component('Pagination', Pagination)
-Vue.component('RightToolbar', RightToolbar)
-Vue.component('Editor', Editor)
-Vue.component('FileUpload', FileUpload)
-Vue.component('ImageUpload', ImageUpload)
-Vue.component('ImagePreview', ImagePreview)
-
-Vue.use(directive)
-Vue.use(plugins)
-Vue.use(VueMeta)
-DictData.install()
-
-/**
- * If you don't want to use mock-server
- * you want to use MockJs for mock api
- * you can execute: mockXHR()
- *
- * Currently MockJs will be used in the production environment,
- * please remove it before going online! ! !
- */
-
-Vue.use(Element, {
-  size: Cookies.get('size') || 'medium' // set element-ui default size
-})
+app.component('DictTag', DictTag)
+app.component('Pagination', Pagination)
+app.component('FileUpload', FileUpload)
+app.component('ImageUpload', ImageUpload)
+app.component('ImagePreview', ImagePreview)
+app.component('RightToolbar', RightToolbar)
+app.component('Editor', Editor)
 
-Vue.config.productionTip = false
+app.use(router)
+app.use(store)
+app.use(plugins)
+app.use(elementIcons)
+app.component('svg-icon', SvgIcon)
 
-new Vue({
-  el: '#app',
-  router,
-  store,
-  render: h => h(App)
+directive(app)
+
+// 使用element-plus 并且设置全局的大小
+app.use(ElementPlus, {
+  locale: locale,
+  // 支持 large、default、small
+  size: Cookies.get('size') || 'default'
 })
+
+app.mount('#app')

+ 19 - 13
canteen-ui/src/permission.js

@@ -1,11 +1,13 @@
 import router from './router'
-import store from './store'
-import { Message } from 'element-ui'
+import { ElMessage } from 'element-plus'
 import NProgress from 'nprogress'
 import 'nprogress/nprogress.css'
 import { getToken } from '@/utils/auth'
-import { isPathMatch } from '@/utils/validate'
+import { isHttp, isPathMatch } from '@/utils/validate'
 import { isRelogin } from '@/utils/request'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
 
 NProgress.configure({ showSpinner: false })
 
@@ -18,7 +20,7 @@ const isWhiteList = (path) => {
 router.beforeEach((to, from, next) => {
   NProgress.start()
   if (getToken()) {
-    to.meta.title && store.dispatch('settings/setTitle', to.meta.title)
+    to.meta.title && useSettingsStore().setTitle(to.meta.title)
     /* has token*/
     if (to.path === '/login') {
       next({ path: '/' })
@@ -26,22 +28,26 @@ router.beforeEach((to, from, next) => {
     } else if (isWhiteList(to.path)) {
       next()
     } else {
-      if (store.getters.roles.length === 0) {
+      if (useUserStore().roles.length === 0) {
         isRelogin.show = true
         // 判断当前用户是否已拉取完user_info信息
-        store.dispatch('GetInfo').then(() => {
+        useUserStore().getInfo().then(() => {
           isRelogin.show = false
-          store.dispatch('GenerateRoutes').then(accessRoutes => {
+          usePermissionStore().generateRoutes().then(accessRoutes => {
             // 根据roles权限生成可访问的路由表
-            router.addRoutes(accessRoutes) // 动态添加可访问路由表
+            accessRoutes.forEach(route => {
+              if (!isHttp(route.path)) {
+                router.addRoute(route) // 动态添加可访问路由表
+              }
+            })
             next({ ...to, replace: true }) // hack方法 确保addRoutes已完成
           })
         }).catch(err => {
-            store.dispatch('LogOut').then(() => {
-              Message.error(err)
-              next({ path: '/' })
-            })
+          useUserStore().logOut().then(() => {
+            ElMessage.error(err)
+            next({ path: '/' })
           })
+        })
       } else {
         next()
       }
@@ -52,7 +58,7 @@ router.beforeEach((to, from, next) => {
       // 在免登录白名单,直接进入
       next()
     } else {
-      next(`/login?redirect=${encodeURIComponent(to.fullPath)}`) // 否则全部重定向到登录页
+      next(`/login?redirect=${to.fullPath}`) // 否则全部重定向到登录页
       NProgress.done()
     }
   }

+ 3 - 3
canteen-ui/src/plugins/auth.js

@@ -1,8 +1,8 @@
-import store from '@/store'
+import useUserStore from '@/store/modules/user'
 
 function authPermission(permission) {
   const all_permission = "*:*:*";
-  const permissions = store.getters && store.getters.permissions
+  const permissions = useUserStore().permissions
   if (permission && permission.length > 0) {
     return permissions.some(v => {
       return all_permission === v || v === permission
@@ -14,7 +14,7 @@ function authPermission(permission) {
 
 function authRole(role) {
   const super_admin = "admin";
-  const roles = store.getters && store.getters.roles
+  const roles = useUserStore().roles
   if (role && role.length > 0) {
     return roles.some(v => {
       return super_admin === v || v === role

+ 7 - 7
canteen-ui/src/plugins/download.js

@@ -1,11 +1,11 @@
-import axios from 'axios'
-import {Loading, Message} from 'element-ui'
+import axios from 'axios'
+import { ElLoading, ElMessage } from 'element-plus'
 import { saveAs } from 'file-saver'
 import { getToken } from '@/utils/auth'
 import errorCode from '@/utils/errorCode'
-import { blobValidate } from "@/utils/ruoyi";
+import { blobValidate } from '@/utils/ruoyi'
 
-const baseURL = process.env.VUE_APP_BASE_API
+const baseURL = import.meta.env.VITE_APP_BASE_API
 let downloadLoadingInstance;
 
 export default {
@@ -45,7 +45,7 @@ export default {
   },
   zip(url, name) {
     var url = baseURL + url
-    downloadLoadingInstance = Loading.service({ text: "正在下载数据,请稍候", spinner: "el-icon-loading", background: "rgba(0, 0, 0, 0.7)", })
+    downloadLoadingInstance = ElLoading.service({ text: "正在下载数据,请稍候", background: "rgba(0, 0, 0, 0.7)", })
     axios({
       method: 'get',
       url: url,
@@ -62,7 +62,7 @@ export default {
       downloadLoadingInstance.close();
     }).catch((r) => {
       console.error(r)
-      Message.error('下载文件出现错误,请联系管理员!')
+      ElMessage.error('下载文件出现错误,请联系管理员!')
       downloadLoadingInstance.close();
     })
   },
@@ -73,7 +73,7 @@ export default {
     const resText = await data.text();
     const rspObj = JSON.parse(resText);
     const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
-    Message.error(errMsg);
+    ElMessage.error(errMsg);
   }
 }
 

+ 11 - 13
canteen-ui/src/plugins/index.js

@@ -4,17 +4,15 @@ import cache from './cache'
 import modal from './modal'
 import download from './download'
 
-export default {
-  install(Vue) {
-    // 页签操作
-    Vue.prototype.$tab = tab
-    // 认证对象
-    Vue.prototype.$auth = auth
-    // 缓存对象
-    Vue.prototype.$cache = cache
-    // 模态框对象
-    Vue.prototype.$modal = modal
-    // 下载文件
-    Vue.prototype.$download = download
-  }
+export default function installPlugins(app){
+  // 页签操作
+  app.config.globalProperties.$tab = tab
+  // 认证对象
+  app.config.globalProperties.$auth = auth
+  // 缓存对象
+  app.config.globalProperties.$cache = cache
+  // 模态框对象
+  app.config.globalProperties.$modal = modal
+  // 下载文件
+  app.config.globalProperties.$download = download
 }

+ 16 - 17
canteen-ui/src/plugins/modal.js

@@ -1,59 +1,59 @@
-import { Message, MessageBox, Notification, Loading } from 'element-ui'
+import { ElMessage, ElMessageBox, ElNotification, ElLoading } from 'element-plus'
 
 let loadingInstance;
 
 export default {
   // 消息提示
   msg(content) {
-    Message.info(content)
+    ElMessage.info(content)
   },
   // 错误消息
   msgError(content) {
-    Message.error(content)
+    ElMessage.error(content)
   },
   // 成功消息
   msgSuccess(content) {
-    Message.success(content)
+    ElMessage.success(content)
   },
   // 警告消息
   msgWarning(content) {
-    Message.warning(content)
+    ElMessage.warning(content)
   },
   // 弹出提示
   alert(content) {
-    MessageBox.alert(content, "系统提示")
+    ElMessageBox.alert(content, "系统提示")
   },
   // 错误提示
   alertError(content) {
-    MessageBox.alert(content, "系统提示", { type: 'error' })
+    ElMessageBox.alert(content, "系统提示", { type: 'error' })
   },
   // 成功提示
   alertSuccess(content) {
-    MessageBox.alert(content, "系统提示", { type: 'success' })
+    ElMessageBox.alert(content, "系统提示", { type: 'success' })
   },
   // 警告提示
   alertWarning(content) {
-    MessageBox.alert(content, "系统提示", { type: 'warning' })
+    ElMessageBox.alert(content, "系统提示", { type: 'warning' })
   },
   // 通知提示
   notify(content) {
-    Notification.info(content)
+    ElNotification.info(content)
   },
   // 错误通知
   notifyError(content) {
-    Notification.error(content);
+    ElNotification.error(content);
   },
   // 成功通知
   notifySuccess(content) {
-    Notification.success(content)
+    ElNotification.success(content)
   },
   // 警告通知
   notifyWarning(content) {
-    Notification.warning(content)
+    ElNotification.warning(content)
   },
   // 确认窗体
   confirm(content) {
-    return MessageBox.confirm(content, "系统提示", {
+    return ElMessageBox.confirm(content, "系统提示", {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
       type: "warning",
@@ -61,7 +61,7 @@ export default {
   },
   // 提交内容
   prompt(content) {
-    return MessageBox.prompt(content, "系统提示", {
+    return ElMessageBox.prompt(content, "系统提示", {
       confirmButtonText: '确定',
       cancelButtonText: '取消',
       type: "warning",
@@ -69,10 +69,9 @@ export default {
   },
   // 打开遮罩层
   loading(content) {
-    loadingInstance = Loading.service({
+    loadingInstance = ElLoading.service({
       lock: true,
       text: content,
-      spinner: "el-icon-loading",
       background: "rgba(0, 0, 0, 0.7)",
     })
   },

+ 15 - 17
canteen-ui/src/plugins/tab.js

@@ -1,10 +1,10 @@
-import store from '@/store'
-import router from '@/router';
+import useTagsViewStore from '@/store/modules/tagsView'
+import router from '@/router'
 
 export default {
   // 刷新当前tab页签
   refreshPage(obj) {
-    const { path, query, matched } = router.currentRoute;
+    const { path, query, matched } = router.currentRoute.value;
     if (obj === undefined) {
       matched.forEach((m) => {
         if (m.components && m.components.default && m.components.default.name) {
@@ -14,7 +14,7 @@ export default {
         }
       });
     }
-    return store.dispatch('tagsView/delCachedView', obj).then(() => {
+    return useTagsViewStore().delCachedView(obj).then(() => {
       const { path, query } = obj
       router.replace({
         path: '/redirect' + path,
@@ -24,7 +24,7 @@ export default {
   },
   // 关闭当前tab页签,打开新页签
   closeOpenPage(obj) {
-    store.dispatch("tagsView/delView", router.currentRoute);
+    useTagsViewStore().delView(router.currentRoute.value);
     if (obj !== undefined) {
       return router.push(obj);
     }
@@ -32,7 +32,7 @@ export default {
   // 关闭指定tab页签
   closePage(obj) {
     if (obj === undefined) {
-      return store.dispatch('tagsView/delView', router.currentRoute).then(({ visitedViews }) => {
+      return useTagsViewStore().delView(router.currentRoute.value).then(({ visitedViews }) => {
         const latestView = visitedViews.slice(-1)[0]
         if (latestView) {
           return router.push(latestView.fullPath)
@@ -40,32 +40,30 @@ export default {
         return router.push('/');
       });
     }
-    return store.dispatch('tagsView/delView', obj);
+    return useTagsViewStore().delView(obj);
   },
   // 关闭所有tab页签
   closeAllPage() {
-    return store.dispatch('tagsView/delAllViews');
+    return useTagsViewStore().delAllViews();
   },
   // 关闭左侧tab页签
   closeLeftPage(obj) {
-    return store.dispatch('tagsView/delLeftTags', obj || router.currentRoute);
+    return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
   },
   // 关闭右侧tab页签
   closeRightPage(obj) {
-    return store.dispatch('tagsView/delRightTags', obj || router.currentRoute);
+    return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
   },
   // 关闭其他tab页签
   closeOtherPage(obj) {
-    return store.dispatch('tagsView/delOthersViews', obj || router.currentRoute);
+    return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
   },
-  // 添加tab页签
-  openPage(title, url, params) {
-    const obj = { path: url, meta: { title: title } }
-    store.dispatch('tagsView/addView', obj);
-    return router.push({ path: url, query: params });
+  // 打开tab页签
+  openPage(url) {
+    return router.push(url);
   },
   // 修改tab页签
   updatePage(obj) {
-    return store.dispatch('tagsView/updateVisitedView', obj);
+    return useTagsViewStore().updateVisitedView(obj);
   }
 }

+ 16 - 25
canteen-ui/src/router/index.js

@@ -1,8 +1,4 @@
-import Vue from 'vue'
-import Router from 'vue-router'
-
-Vue.use(Router)
-
+import { createWebHistory, createRouter } from 'vue-router'
 /* Layout */
 import Layout from '@/layout'
 
@@ -37,7 +33,7 @@ export const constantRoutes = [
     children: [
       {
         path: '/redirect/:path(.*)',
-        component: () => import('@/views/redirect')
+        component: () => import('@/views/redirect/index.vue')
       }
     ]
   },
@@ -52,7 +48,7 @@ export const constantRoutes = [
     hidden: true
   },
   {
-    path: '/404',
+    path: "/:pathMatch(.*)*",
     component: () => import('@/views/error/404'),
     hidden: true
   },
@@ -64,10 +60,10 @@ export const constantRoutes = [
   {
     path: '',
     component: Layout,
-    redirect: 'index',
+    redirect: '/index',
     children: [
       {
-        path: 'index',
+        path: '/index',
         component: () => import('@/views/index'),
         name: 'Index',
         meta: { title: '首页', icon: 'dashboard', affix: true }
@@ -164,20 +160,15 @@ export const dynamicRoutes = [
   }
 ]
 
-// 防止连续点击多次路由报错
-let routerPush = Router.prototype.push;
-let routerReplace = Router.prototype.replace;
-// push
-Router.prototype.push = function push(location) {
-  return routerPush.call(this, location).catch(err => err)
-}
-// replace
-Router.prototype.replace = function push(location) {
-  return routerReplace.call(this, location).catch(err => err)
-}
+const router = createRouter({
+  history: createWebHistory(),
+  routes: constantRoutes,
+  scrollBehavior(to, from, savedPosition) {
+    if (savedPosition) {
+      return savedPosition
+    }
+    return { top: 0 }
+  },
+});
 
-export default new Router({
-  mode: 'history', // 去掉url中的#
-  scrollBehavior: () => ({ y: 0 }),
-  routes: constantRoutes
-})
+export default router;

Certains fichiers n'ont pas été affichés car il y a eu trop de fichiers modifiés dans ce diff