13127578837 пре 1 година
комит
bf194a9756
100 измењених фајлова са 9041 додато и 0 уклоњено
  1. 9 0
      .env.development
  2. 9 0
      .env.offline
  3. 13 0
      .env.production
  4. 15 0
      .env.staging
  5. 25 0
      .gitignore
  6. 1 0
      .npmrc
  7. 3 0
      README.md
  8. 12 0
      bin/build.bat
  9. 12 0
      bin/package.bat
  10. 12 0
      bin/run-web.bat
  11. 324 0
      index.html
  12. 76 0
      package.json
  13. BIN
      public/error.png
  14. BIN
      public/loading.gif
  15. 20 0
      src/App.vue
  16. 12 0
      src/api/common.js
  17. 26 0
      src/api/index.js
  18. 9 0
      src/api/menu.js
  19. BIN
      src/assets/401_images/401.gif
  20. BIN
      src/assets/404_images/404.png
  21. BIN
      src/assets/404_images/404_cloud.png
  22. BIN
      src/assets/images/bg.png
  23. BIN
      src/assets/images/dkq.png
  24. BIN
      src/assets/images/gzh.png
  25. BIN
      src/assets/images/heyan.png
  26. BIN
      src/assets/images/logo.png
  27. BIN
      src/assets/images/middle-bg.jpg
  28. BIN
      src/assets/images/return.png
  29. BIN
      src/assets/images/sfz.png
  30. BIN
      src/assets/images/tianbao.png
  31. BIN
      src/assets/images/yuyue.png
  32. 393 0
      src/assets/js/dataFormate.js
  33. 43 0
      src/assets/js/flexible.js
  34. 99 0
      src/assets/styles/btn.scss
  35. 132 0
      src/assets/styles/element-ui.scss
  36. 66 0
      src/assets/styles/handle.scss
  37. 1290 0
      src/assets/styles/index.scss
  38. 66 0
      src/assets/styles/mixin.scss
  39. 247 0
      src/assets/styles/ruoyi.scss
  40. 608 0
      src/assets/styles/sidebar.scss
  41. 141 0
      src/assets/styles/themes.scss
  42. 170 0
      src/assets/styles/transition.scss
  43. 79 0
      src/assets/styles/variables.module.scss
  44. 84 0
      src/components/Breadcrumb/index.vue
  45. 49 0
      src/components/DictTag/index.vue
  46. 193 0
      src/components/FileUpload/index.vue
  47. 181 0
      src/components/HeaderSearch/index.vue
  48. 103 0
      src/components/IconSelect/index.vue
  49. 25 0
      src/components/IconSelect/requireIcons.js
  50. 78 0
      src/components/ImagePreview/index.vue
  51. 208 0
      src/components/ImageUpload/index.vue
  52. 112 0
      src/components/Pagination/index.vue
  53. 3 0
      src/components/ParentView/index.vue
  54. 100 0
      src/components/RightToolbar/index.vue
  55. 22 0
      src/components/Screenfull/index.vue
  56. 54 0
      src/components/SizeSelect/index.vue
  57. 53 0
      src/components/SvgIcon/index.vue
  58. 10 0
      src/components/SvgIcon/svgicon.js
  59. 62 0
      src/components/TopNav/Link.vue
  60. 105 0
      src/components/TopNav/index.vue
  61. 199 0
      src/components/TopNav/topNavItem.vue
  62. 154 0
      src/components/TreeSelect/index.vue
  63. 31 0
      src/components/iFrame/index.vue
  64. 90 0
      src/components/layout/index.vue
  65. 69 0
      src/components/officialAccount/index.vue
  66. 66 0
      src/directive/common/copyText.js
  67. 9 0
      src/directive/index.js
  68. 28 0
      src/directive/permission/hasPermi.js
  69. 28 0
      src/directive/permission/hasRole.js
  70. 32 0
      src/layout/components/AppMain.vue
  71. 30 0
      src/layout/components/InnerLink/index.vue
  72. 300 0
      src/layout/components/Navbar.vue
  73. 323 0
      src/layout/components/Settings/index.vue
  74. 62 0
      src/layout/components/Sidebar/Link.vue
  75. 123 0
      src/layout/components/Sidebar/Logo.vue
  76. 118 0
      src/layout/components/Sidebar/SidebarItem.vue
  77. 58 0
      src/layout/components/Sidebar/index.vue
  78. 93 0
      src/layout/components/TagsView/ScrollPane.vue
  79. 361 0
      src/layout/components/TagsView/index.vue
  80. 4 0
      src/layout/components/index.js
  81. 124 0
      src/layout/index.vue
  82. 100 0
      src/main.js
  83. 18 0
      src/permission.js
  84. 60 0
      src/plugins/auth.js
  85. 77 0
      src/plugins/cache.js
  86. 97 0
      src/plugins/common.js
  87. 38 0
      src/plugins/download.js
  88. 82 0
      src/plugins/img.js
  89. 25 0
      src/plugins/index.js
  90. 82 0
      src/plugins/modal.js
  91. 65 0
      src/plugins/tab.js
  92. 84 0
      src/router/index.js
  93. 45 0
      src/settings.js
  94. 13 0
      src/store/index.js
  95. 44 0
      src/store/modules/app.js
  96. 187 0
      src/store/modules/common.js
  97. 15 0
      src/store/modules/index.js
  98. 139 0
      src/store/modules/permission.js
  99. 43 0
      src/store/modules/settings.js
  100. 171 0
      src/store/modules/tagsView.js

+ 9 - 0
.env.development

@@ -0,0 +1,9 @@
+# 页面标题
+VITE_APP_TITLE = "太仓访客预约系统"
+
+# 开发环境配置
+VITE_APP_ENV = 'development'
+# # 管理系统/开发环境
+VITE_APP_BASE_API = 'http://172.16.120.165:801/dev-api'
+# # #websocket请求地址
+VITE_APP_WEBSOCKET_API = 'ws://172.16.120.49:9891/websocket/'

+ 9 - 0
.env.offline

@@ -0,0 +1,9 @@
+# 页面标题
+VITE_APP_TITLE = 
+
+# 开发环境配置
+VITE_APP_ENV = 'offline'
+# 管理系统/开发环境
+VITE_APP_BASE_API = '/offline-api'
+#websocket请求地址
+VITE_APP_WEBSOCKET_API = 'ws://172.16.120.49:9891/websocket/'

+ 13 - 0
.env.production

@@ -0,0 +1,13 @@
+# 页面标题
+VITE_APP_TITLE = "太仓访客预约系统"
+
+# 生产环境配置
+VITE_APP_ENV = 'production'
+
+# 管理系统/生产环境
+ VITE_APP_BASE_API = 'https://gateWay.usky.cn/prod-api'
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+#websocket请求地址
+VITE_APP_WEBSOCKET_API = 'ws://gateway.usky.cn/wss'

+ 15 - 0
.env.staging

@@ -0,0 +1,15 @@
+# 页面标题
+VITE_APP_TITLE = 
+
+# 生产环境配置
+VITE_APP_ENV = 'staging'
+
+# 管理系统/生产环境
+VITE_APP_BASE_API = '/stage-api'
+
+# 是否在打包时开启压缩,支持 gzip 和 brotli
+VITE_BUILD_COMPRESS = gzip
+
+#websocket请求地址
+VITE_APP_WEBSOCKET_API = 'ws://172.16.120.49:9891/websocket/'
+

+ 25 - 0
.gitignore

@@ -0,0 +1,25 @@
+.DS_Store
+.history/
+node_modules/
+dist/
+npm-debug.log*
+yarn-debug.log*
+yarn-error.log*
+**/*.log
+*.zip
+
+tests/**/coverage/
+tests/e2e/reports
+selenium-debug.log
+
+# Editor directories and files
+.idea
+.vscode
+*.suo
+*.ntvs*
+*.njsproj
+*.sln
+*.local
+
+package-lock.json
+yarn.lock

+ 1 - 0
.npmrc

@@ -0,0 +1 @@
+registry=https://registry.npmmirror.com/

+ 3 - 0
README.md

@@ -0,0 +1,3 @@
+# 太仓访客预约系统
+
+## 项目介绍

+ 12 - 0
bin/build.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 打包Web工程,生成dist文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn build:prod
+
+pause

+ 12 - 0
bin/package.bat

@@ -0,0 +1,12 @@
+@echo off
+echo.
+echo [信息] 安装Web工程,生成node_modules文件。
+echo.
+
+%~d0
+cd %~dp0
+
+cd ..
+yarn --registry=https://registry.npmmirror.com
+
+pause

+ 12 - 0
bin/run-web.bat

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

+ 324 - 0
index.html

@@ -0,0 +1,324 @@
+<!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">
+    <title>预约引导系统</title>
+    <style>
+        html,
+        body,
+        #app {
+            height: 100%;
+            margin: 0px;
+            padding: 0px;
+        }
+        
+        .chromeframe {
+            margin: 0.2em 0;
+            background: #ccc;
+            color: #000;
+            padding: 0.2em 0;
+        }
+        
+        #loader-wrapper {
+            position: fixed;
+            top: 0;
+            left: 0;
+            width: 100%;
+            height: 100%;
+            z-index: 999999;
+        }
+        
+        #loader {
+            display: block;
+            position: relative;
+            left: 50%;
+            top: 50%;
+            width: 150px;
+            height: 150px;
+            margin: -75px 0 0 -75px;
+            border-radius: 50%;
+            border: 3px solid transparent;
+            border-top-color: #409eff;
+            -webkit-animation: spin 2s linear infinite;
+            -ms-animation: spin 2s linear infinite;
+            -moz-animation: spin 2s linear infinite;
+            -o-animation: spin 2s linear infinite;
+            animation: spin 2s linear infinite;
+            z-index: 1001;
+        }
+        
+        #loader:before {
+            content: "";
+            position: absolute;
+            top: 5px;
+            left: 5px;
+            right: 5px;
+            bottom: 5px;
+            border-radius: 50%;
+            border: 3px solid transparent;
+            border-top-color: #409eff;
+            -webkit-animation: spin 3s linear infinite;
+            -moz-animation: spin 3s linear infinite;
+            -o-animation: spin 3s linear infinite;
+            -ms-animation: spin 3s linear infinite;
+            animation: spin 3s linear infinite;
+        }
+        
+        #loader:after {
+            content: "";
+            position: absolute;
+            top: 15px;
+            left: 15px;
+            right: 15px;
+            bottom: 15px;
+            border-radius: 50%;
+            border: 3px solid transparent;
+            border-top-color: #409eff;
+            -moz-animation: spin 1.5s linear infinite;
+            -o-animation: spin 1.5s linear infinite;
+            -ms-animation: spin 1.5s linear infinite;
+            -webkit-animation: spin 1.5s linear infinite;
+            animation: spin 1.5s linear infinite;
+        }
+        
+        @-webkit-keyframes spin {
+            0% {
+                -webkit-transform: rotate(0deg);
+                -ms-transform: rotate(0deg);
+                transform: rotate(0deg);
+            }
+            100% {
+                -webkit-transform: rotate(360deg);
+                -ms-transform: rotate(360deg);
+                transform: rotate(360deg);
+            }
+        }
+        
+        @keyframes spin {
+            0% {
+                -webkit-transform: rotate(0deg);
+                -ms-transform: rotate(0deg);
+                transform: rotate(0deg);
+            }
+            100% {
+                -webkit-transform: rotate(360deg);
+                -ms-transform: rotate(360deg);
+                transform: rotate(360deg);
+            }
+        }
+        
+        #loader-wrapper .loader-section {
+            position: fixed;
+            top: 0;
+            width: 51%;
+            height: 100%;
+            background: #f7f7f7;
+            z-index: 1000;
+            -webkit-transform: translateX(0);
+            -ms-transform: translateX(0);
+            transform: translateX(0);
+        }
+        
+        #loader-wrapper .loader-section.section-left {
+            left: 0;
+        }
+        
+        #loader-wrapper .loader-section.section-right {
+            right: 0;
+        }
+        
+        .loaded #loader-wrapper .loader-section.section-left {
+            -webkit-transform: translateX(-100%);
+            -ms-transform: translateX(-100%);
+            transform: translateX(-100%);
+            -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+            transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        }
+        
+        .loaded #loader-wrapper .loader-section.section-right {
+            -webkit-transform: translateX(100%);
+            -ms-transform: translateX(100%);
+            transform: translateX(100%);
+            -webkit-transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+            transition: all 0.7s 0.3s cubic-bezier(0.645, 0.045, 0.355, 1.000);
+        }
+        
+        .loaded #loader {
+            opacity: 0;
+            -webkit-transition: all 0.3s ease-out;
+            transition: all 0.3s ease-out;
+        }
+        
+        .loaded #loader-wrapper {
+            visibility: hidden;
+            -webkit-transform: translateY(-100%);
+            -ms-transform: translateY(-100%);
+            transform: translateY(-100%);
+            -webkit-transition: all 0.3s 1s ease-out;
+            transition: all 0.3s 1s ease-out;
+        }
+        
+        .no-js #loader-wrapper {
+            display: none;
+        }
+        
+        .no-js h1 {
+            color: #222222;
+        }
+        
+        #loader-wrapper .load_title {
+            font-family: 'Open Sans';
+            color: #000;
+            font-size: 19px;
+            width: 100%;
+            text-align: center;
+            z-index: 9999999999999;
+            position: absolute;
+            top: 60%;
+            opacity: 1;
+            line-height: 30px;
+        }
+        
+        #loader-wrapper .load_title span {
+            font-weight: normal;
+            font-style: italic;
+            font-size: 13px;
+            color: #000;
+            opacity: 0.5;
+        }
+    </style>
+    <style>
+        #loading {
+            background-color: #f7f7f7;
+            height: 100%;
+            width: 100%;
+            position: fixed;
+            z-index: 1;
+            margin-top: 0px;
+            top: 0px;
+            left: 0px;
+        }
+        
+        #loading-center {
+            width: 100%;
+            height: 100%;
+            position: relative;
+        }
+        
+        #loading-center-absolute {
+            position: absolute;
+            left: 50%;
+            top: 50%;
+            height: 200px;
+            width: 200px;
+            margin-top: -100px;
+            margin-left: -100px;
+            -ms-transform: rotate(-135deg);
+            -webkit-transform: rotate(-135deg);
+            transform: rotate(-135deg);
+        }
+        
+        .object {
+            -moz-border-radius: 50% 50% 50% 50%;
+            -webkit-border-radius: 50% 50% 50% 50%;
+            border-radius: 50% 50% 50% 50%;
+            position: absolute;
+            border-top: 5px solid #409eff;
+            border-bottom: 5px solid transparent;
+            border-left: 5px solid #409eff;
+            border-right: 5px solid transparent;
+            -webkit-animation: animate 2s infinite;
+            animation: animate 2s infinite;
+        }
+        
+        #object_one {
+            left: 75px;
+            top: 75px;
+            width: 50px;
+            height: 50px;
+        }
+        
+        #object_two {
+            left: 65px;
+            top: 65px;
+            width: 70px;
+            height: 70px;
+            -webkit-animation-delay: 0.2s;
+            animation-delay: 0.2s;
+        }
+        
+        #object_three {
+            left: 55px;
+            top: 55px;
+            width: 90px;
+            height: 90px;
+            -webkit-animation-delay: 0.4s;
+            animation-delay: 0.4s;
+        }
+        
+        #object_four {
+            left: 45px;
+            top: 45px;
+            width: 110px;
+            height: 110px;
+            -webkit-animation-delay: 0.6s;
+            animation-delay: 0.6s;
+        }
+        
+        @-webkit-keyframes animate {
+            50% {
+                -ms-transform: rotate(360deg) scale(0.8);
+                -webkit-transform: rotate(360deg) scale(0.8);
+                transform: rotate(360deg) scale(0.8);
+            }
+        }
+        
+        @keyframes animate {
+            50% {
+                -ms-transform: rotate(360deg) scale(0.8);
+                -webkit-transform: rotate(360deg) scale(0.8);
+                transform: rotate(360deg) scale(0.8);
+            }
+        }
+        /* 高德地图底部左侧logo信息等 */
+        
+        .amap-logo,
+        .amap-copyright {
+            display: none !important;
+        }
+    </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 id="loading">
+            <div id="loading-center">
+                <div id="loading-center-absolute">
+                    <div class="object" id="object_four"></div>
+                    <div class="object" id="object_three"></div>
+                    <div class="object" id="object_two"></div>
+                    <div class="object" id="object_one"></div>
+                </div>
+                <div id="loader-wrapper">
+                    <div class="load_title">正在加载系统资源,请耐心等待</div>
+                </div>
+            </div>
+        </div>
+    </div>
+    <script type="module" src="/src/main.js"></script>
+    <script>
+    </script>
+</body>
+
+</html>

+ 76 - 0
package.json

@@ -0,0 +1,76 @@
+{
+    "name": "visitor",
+    "version": "1.0.0",
+    "description": "访客预约系统",
+    "author": "",
+    "private": true,
+    "license": "MIT",
+    "scripts": {
+        "dev": "vite",
+        "build:prod": "vite build",
+        "build:stage": "vite build --mode staging",
+        "build:offline": "vite build --mode offline",
+        "preview": "vite preview",
+        "clear": "rimraf node_modules&&npm install"
+    },
+    "repository": {
+        "type": "git",
+        "url": "https://gitee.com/y_project/RuoYi-Cloud.git"
+    },
+    "dependencies": {
+        "@iconfu/svg-inject": "^1.2.3",
+        "@rollup/plugin-alias": "^5.0.0",
+        "@vueuse/core": "8.5.0",
+        "@wangeditor/editor": "^5.1.15",
+        "@wangeditor/editor-for-vue": "^5.1.12",
+        "axios": "^1.2.2",
+        "clipboard": "^2.0.11",
+        "dayjs": "^1.11.10",
+        "echarts": "^5.4.1",
+        "echarts-liquidfill": "^3.1.0",
+        "element-plus": "2.4.1",
+        "file-saver": "2.0.5",
+        "fuse.js": "6.5.3",
+        "highcharts": "^10.3.3",
+        "hls.js": "^1.4.12",
+        "js-beautify": "^1.14.4",
+        "js-cookie": "3.0.1",
+        "js-md5": "^0.7.3",
+        "jsencrypt": "3.2.1",
+        "lodash-es": "^4.17.21",
+        "mockjs": "^1.1.0",
+        "nprogress": "0.2.0",
+        "pinia": "2.0.14",
+        "pinia-plugin-persistedstate": "^3.1.0",
+        "qrcodejs2-fix": "^0.0.1",
+        "sockjs-client": "^1.6.1",
+        "speak-tts": "^2.0.8",
+        "stompjs": "^2.3.3",
+        "swiper": "^10.3.0",
+        "video.js": "^8.0.4",
+        "videojs-contrib-hls": "^5.15.0",
+        "vue": "3.2.31",
+        "vue-amap": "^0.5.10",
+        "vue-cropper": "1.0.3",
+        "vue-demi": "^0.13.11",
+        "vue-router": "4.0.14",
+        "vue-ueditor-wrap": "^3.0.8",
+        "vue3-lazy": "^1.0.0-alpha.1",
+        "vue3-seamless-scroll": "^2.0.1",
+        "vue3-video-play": "^1.3.2",
+        "vuedraggable": "^2.24.3"
+    },
+    "devDependencies": {
+        "@vitejs/plugin-vue": "^4.0.0",
+        "lib-flexible": "^0.3.2",
+        "sass": "^1.57.1",
+        "terser": "^5.24.0",
+        "unplugin-auto-import": "^0.17.5",
+        "uuid": "^9.0.0",
+        "vite": "^4.5.3",
+        "vite-plugin-compression": "0.5.1",
+        "vite-plugin-resolve-externals": "^0.2.2",
+        "vite-plugin-svg-icons": "2.0.1",
+        "vite-plugin-vue-setup-extend": "0.4.0"
+    }
+}


BIN
public/loading.gif


+ 20 - 0
src/App.vue

@@ -0,0 +1,20 @@
+<template>
+  <router-view />
+</template>
+<script setup>
+(function(){
+    function w() {
+    var r = document.documentElement;
+    var a = r.getBoundingClientRect().width;//获取当前设备的宽度
+        if (a > 1920 ){//720不固定,根据设计稿的宽度定
+            a = 1920;
+        } 
+        var rem = a / 192;
+        r.style.fontSize = rem + "px"
+    }
+    w();
+    window.addEventListener("resize", function() {//监听横竖屏切换
+        w()
+    }, false);
+})();
+</script>

+ 12 - 0
src/api/common.js

@@ -0,0 +1,12 @@
+import request from "@/utils/request";
+
+// 上传文件
+export function upload(file) {
+    let fd = new FormData()
+    fd.append('file', file)
+    return request({
+        url: "/service-file/upload",
+        method: "post",
+        data: fd,
+    });
+}

+ 26 - 0
src/api/index.js

@@ -0,0 +1,26 @@
+import request from "@/utils/request";
+
+// 组织查询(富士)
+export function getOrganization(params) {
+    return request({
+        url: "http://192.168.10.26:50014/api/E8/organization/get-page-list",
+        method: "post",
+        params,
+    });
+}
+// 人员查询(富士)
+export function getCustomer(params) {
+    return request({
+        url: "http://192.168.10.26:50014/api/E8/customer/get-page-list",
+        method: "post",
+        params,
+    });
+}
+// 新增访客(富士)
+export function addVisitor(params) {
+    return request({
+        url: "http://192.168.10.26:50014/api/E8Door/visitor-registration",
+        method: "post",
+        params,
+    });
+}

+ 9 - 0
src/api/menu.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+// 获取路由
+export const getRouters = () => {
+  return request({
+    url: '/system/menu/getRouters',
+    method: 'get'
+  })
+}

BIN
src/assets/401_images/401.gif


BIN
src/assets/404_images/404.png


BIN
src/assets/404_images/404_cloud.png


BIN
src/assets/images/bg.png


BIN
src/assets/images/dkq.png


BIN
src/assets/images/gzh.png


BIN
src/assets/images/heyan.png


BIN
src/assets/images/logo.png


BIN
src/assets/images/middle-bg.jpg


BIN
src/assets/images/return.png


BIN
src/assets/images/sfz.png


BIN
src/assets/images/tianbao.png


BIN
src/assets/images/yuyue.png


+ 393 - 0
src/assets/js/dataFormate.js

@@ -0,0 +1,393 @@
+/**
+ * 获取本周、本季度、本月、上月的开始日期、结束日期
+ */
+var now = new Date(); //当前日期
+var nowDayOfWeek = now.getDay(); //今天本周的第几天
+var nowDay = now.getDate(); //当前日
+var nowMonth = now.getMonth(); //当前月
+var nowYear = now.getYear(); //当前年
+nowYear += (nowYear < 2000) ? 1900 : 0; //
+var lastMonthDate = new Date(); //上月日期
+lastMonthDate.setDate(1);
+lastMonthDate.setMonth(lastMonthDate.getMonth() - 1);
+// var lastYear = lastMonthDate.getYear();
+var lastMonth = lastMonthDate.getMonth();
+// 获取本周几
+function getWeekDate() {
+    let weeks = new Array(
+        "周日",
+        "周一",
+        "周二",
+        "周三",
+        "周四",
+        "周五",
+        "周六"
+    );
+    var week = weeks[nowDayOfWeek];
+    return week;
+}
+//格式化日期:yyyy-MM-dd
+function formatDate(date) {
+    var myyear = date.getFullYear();
+    var mymonth = date.getMonth() + 1;
+    var myweekday = date.getDate();
+    if (mymonth < 10) {
+        mymonth = "0" + mymonth;
+    }
+    if (myweekday < 10) {
+        myweekday = "0" + myweekday;
+    }
+    return (myyear + "-" + mymonth + "-" + myweekday);
+}
+//获得某月的天数
+function getMonthDays(myMonth) {
+    var monthStartDate = new Date(nowYear, myMonth, 1);
+    var monthEndDate = new Date(nowYear, myMonth + 1, 1);
+    var days = (monthEndDate - monthStartDate) / (1000 * 60 * 60 * 24);
+    return days;
+}
+//获取季度
+function getQuarter() {
+    var today = new Date(); //获取当前时间
+    var month = today.getMonth() + 1; //getMonth返回0-11
+    if (month >= 1 && month <= 3) {
+        return "1";
+    } else if (month >= 4 && month <= 6) {
+        return "2";
+    } else if (month >= 7 && month <= 9) {
+        return "3";
+    } else {
+        return "4";
+    }
+}
+//获得本季度的开始月份
+function getQuarterStartMonth() {
+    var quarterStartMonth = 0;
+    if (nowMonth < 3) {
+        quarterStartMonth = 0;
+    }
+    if (2 < nowMonth && nowMonth < 6) {
+        quarterStartMonth = 3;
+    }
+    if (5 < nowMonth && nowMonth < 9) {
+        quarterStartMonth = 6;
+    }
+    if (nowMonth > 8) {
+        quarterStartMonth = 9;
+    }
+    return quarterStartMonth;
+}
+//获得本周的开始日期
+function getWeekStartDate() {
+    var weekStartDate = new Date(nowYear, nowMonth, nowDay - nowDayOfWeek + 1);
+    return formatDate(weekStartDate) + ' 00:00:00';
+}
+//获得本周的结束日期
+function getWeekEndDate() {
+    var weekEndDate = new Date(nowYear, nowMonth, nowDay + (7 - nowDayOfWeek));
+    return formatDate(weekEndDate) + ' 23:59:59';
+}
+//获得上周的开始日期
+function getLastWeekStartDate() {
+    var weekStartDate = new Date(nowYear, nowMonth, nowDay - nowDayOfWeek - 7);
+    return formatDate(weekStartDate) + ' 00:00:00';
+}
+//获得上周的结束日期
+function getLastWeekEndDate() {
+    var weekEndDate = new Date(nowYear, nowMonth, nowDay - nowDayOfWeek - 1);
+    return formatDate(weekEndDate) + ' 23:59:59';
+}
+//获得本月的开始日期
+function getMonthStartDate() {
+    var monthStartDate = new Date(nowYear, nowMonth, 1);
+    return formatDate(monthStartDate) + ' 00:00:00';
+}
+//获得本月的结束日期
+function getMonthEndDate() {
+    var monthEndDate = new Date(nowYear, nowMonth, getMonthDays(nowMonth));
+    return formatDate(monthEndDate) + ' 23:59:59';
+}
+//获得上月开始时间
+function getLastMonthStartDate() {
+    var lastMonthStartDate = new Date(nowYear, lastMonth, 1);
+    return formatDate(lastMonthStartDate) + ' 00:00:00';
+}
+//获得上月结束时间
+function getLastMonthEndDate() {
+    var lastMonthEndDate = new Date(nowYear, lastMonth, getMonthDays(lastMonth));
+    return formatDate(lastMonthEndDate) + ' 23:59:59';
+}
+//获得本季度的开始日期
+function getQuarterStartDate() {
+    var quarterStartDate = new Date(nowYear, getQuarterStartMonth(), 1);
+    return formatDate(quarterStartDate) + ' 00:00:00';
+}
+//或的本季度的结束日期
+function getQuarterEndDate() {
+    var quarterEndMonth = getQuarterStartMonth() + 2;
+    var quarterStartDate = new Date(nowYear, quarterEndMonth,
+        getMonthDays(quarterEndMonth));
+    return formatDate(quarterStartDate) + ' 23:59:59';
+}
+//获得前半年开始时间
+function getHalfYearStartDate() {
+    var firstHalfYearStartDate = new Date(nowYear, 1 - 1, 1);
+    return formatDate(firstHalfYearStartDate) + ' 00:00:00';
+}
+//获前半年结束时间
+function getHalfYearEndDate() {
+    var firstHalfYearEndDate = new Date(nowYear, 6 - 1, 30);
+    return formatDate(firstHalfYearEndDate) + ' 23:59:59';
+}
+//获得前今年开始时间
+function getThisYearStartDate() {
+    var firstThisYearStartDate = new Date(nowYear, 1 - 1, 1);
+    return formatDate(firstThisYearStartDate) + ' 00:00:00';
+}
+//获前今年结束时间
+function getThisYearEndDate() {
+    var firstThisYearEndDate = new Date(nowYear, 12 - 1, 31);
+    return formatDate(firstThisYearEndDate) + ' 23:59:59';
+}
+//获当前起前一个月时间
+function getThisDateBeforMonth() {
+
+    var lastMonthToday = new Date(
+        new Date().getTime() - 30 * 24 * 60 * 60 * 1000
+    );
+    var lastMonthYear = lastMonthToday.getFullYear();
+    let month = lastMonthToday.getMonth() + 1
+    var lastMonth = month < 10 ? '0' + month : month;
+    var lastMonthDay =
+        lastMonthToday.getDate < 10 ?
+        "0" + lastMonthToday.getDate :
+        lastMonthToday.getDate();
+    var lastMonthKsrq = lastMonthYear + "-" + lastMonth + "-" + lastMonthDay + " 00:00:00";
+    return lastMonthKsrq
+}
+//获当前起后一个月时间
+function getThisDateNextMonth() {
+    var nextMonthToday = new Date(
+        new Date().getTime() + 30 * 24 * 60 * 60 * 1000
+    );
+    var nextMonthYear = nextMonthToday.getFullYear();
+    let month = nextMonthToday.getMonth() + 1
+    var nextMonth = month < 10 ? '0' + month : month;
+    var nextMonthDay =
+        nextMonthToday.getDate < 10 ?
+        "0" + nextMonthToday.getDate :
+        nextMonthToday.getDate();
+    var nextMonthJsrq = nextMonthYear + "-" + nextMonth + "-" + nextMonthDay;
+    return nextMonthJsrq
+}
+
+// 中国标准时间转年月日时分秒
+function timestampToTime(timestamp) {
+    // var chinaStandard=Mon Jul 19 2021 11:11:55 GMT+0800 (中国标准时间);
+    var date = new Date(timestamp);
+    var y = date.getFullYear();
+    var m = date.getMonth() + 1;
+    m = m < 10 ? ('0' + m) : m;
+    var d = date.getDate();
+    d = d < 10 ? ('0' + d) : d;
+    var h = date.getHours();
+    h = h < 10 ? ('0' + h) : h;
+    var minute = date.getMinutes();
+    minute = minute < 10 ? ('0' + minute) : minute;
+    var second = date.getSeconds();
+    second = second < 10 ? ('0' + second) : second;
+    let time = y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
+    return time
+}
+
+//获取当前日期yy-mm-dd
+//date 为时间对象
+function getNYRSFM() {
+    var year = now.getFullYear() + '年'
+    var month = now.getMonth() + 1 + '月'
+    var day = now.getDate() + '日'
+    var sfb = now.getHours() + ':' + now.getMinutes() + ':' + now.getSeconds()
+    return year + month + day + " " + sfb;
+}
+
+function getNYRSFM2() {
+    var y = now.getFullYear();
+    var m = now.getMonth() + 1;
+    m = m < 10 ? '0' + m : m;
+    var d = now.getDate();
+    d = d < 10 ? '0' + d : d;
+    var h = now.getHours();
+    h = h < 10 ? '0' + h : h;
+    var minute = now.getMinutes();
+    minute = minute < 10 ? '0' + minute : minute;
+    var second = now.getSeconds();
+    second = second < 10 ? '0' + second : second;
+    let time = y + '-' + m + '-' + d + ' ' + h + ':' + minute + ':' + second;
+    return time
+}
+/**
+ * 获得相对当前周AddWeekCount个周的起止日期
+ * AddWeekCount为0代表当前周  为-1代表上一个周  为1代表下一个周以此类推
+ * **/
+function getWeekStartAndEnd(AddWeekCount) {
+    //起止日期数组
+    var startStop = new Array();
+    //一天的毫秒数
+    var millisecond = 1000 * 60 * 60 * 24;
+    //获取当前时间
+    var currentDate = new Date();
+    //相对于当前日期AddWeekCount个周的日期
+    currentDate = new Date(currentDate.getTime() + (millisecond * 7 * AddWeekCount));
+    //返回date是一周中的某一天
+    var week = currentDate.getDay();
+    //返回date是一个月中的某一天
+    // var month = currentDate.getDate();
+    //减去的天数
+    var minusDay = week != 0 ? week - 1 : 6;
+    //获得当前周的第一天
+    var currentWeekFirstDay = new Date(currentDate.getTime() - (millisecond * minusDay));
+    //获得当前周的最后一天
+    var currentWeekLastDay = new Date(currentWeekFirstDay.getTime() + (millisecond * 6));
+    //添加至数组
+    startStop.push(getDateStr3(currentWeekFirstDay));
+    startStop.push(getDateStr3(currentWeekLastDay));
+
+    return startStop;
+}
+/**
+ * 获得相对当月AddMonthCount个月的起止日期
+ * AddMonthCount为0 代表当月 为-1代表上一个月 为1代表下一个月 以此类推
+ * ***/
+function getMonthStartAndEnd(AddMonthCount) {
+    //起止日期数组
+    var startStop = new Array();
+    //获取当前时间
+    var currentDate = new Date();
+    var month = currentDate.getMonth() + AddMonthCount;
+    if (month < 0) {
+        var n = parseInt((-month) / 12);
+        month += n * 12;
+        currentDate.setFullYear(currentDate.getFullYear() - n);
+    }
+    currentDate = new Date(currentDate.setMonth(month));
+    //获得当前月份0-11
+    var currentMonth = currentDate.getMonth();
+    //获得当前年份4位年
+    var currentYear = currentDate.getFullYear();
+    //获得上一个月的第一天
+    var currentMonthFirstDay = new Date(currentYear, currentMonth, 1);
+    //获得上一月的最后一天
+    var currentMonthLastDay = new Date(currentYear, currentMonth + 1, 0);
+    //添加至数组
+    startStop.push(getDateStr3(currentMonthFirstDay));
+    startStop.push(getDateStr3(currentMonthLastDay));
+    //返回
+    return startStop;
+}
+
+/**
+ * 获得当前年月日
+ * ***/
+function YearMonthDate() {
+    var date = new Date();
+    var mon = date.getMonth() + 1;
+    var day = date.getDate();
+    var currDate = date.getFullYear() + "-" + (mon < 10 ? "0" + mon : mon) + "-" + (day < 10 ? "0" + day : day) + " 23:59:59";
+    return currDate
+}
+
+/**
+年月日时分秒转时间戳:
+***/
+function getTimeFormat(timeS) {
+    let time = (new Date(timeS).getTime()) / 1000; //除1000 是变成秒级的时间戳 不除就是毫秒级
+    return time;
+}
+
+/**
+ * 获取当前时间戳
+ */
+function getTimestamp() {
+    let time = Math.round(new Date());
+    return time;
+}
+// 中国标准时间format yyyy-mm-dd
+function format(time) {
+    let ymd = ''
+    let mouth = (time.getMonth() + 1) >= 10 ? (time.getMonth() + 1) : ('0' + (time.getMonth() + 1))
+    let day = time.getDate() >= 10 ? time.getDate() : ('0' + time.getDate())
+    ymd += time.getFullYear() + '-' // 获取年份。
+    ymd += mouth + '-' // 获取月份。
+    ymd += day // 获取日。
+    return ymd // 返回日期。
+}
+
+/**
+ * 根据时间范围创建时间
+ * @param {*} start 
+ * @param {*} end 
+ * @param {*} type 1day 2hour
+ * @returns 
+ */
+function getRangeDate(start, end, type) {
+    let dateArr = []
+    let startArr = start.split('-')
+    let endArr = end.split('-')
+    let db = new Date()
+    db.setUTCFullYear(startArr[0], startArr[1] - 1, startArr[2])
+    let de = new Date()
+    de.setUTCFullYear(endArr[0], endArr[1] - 1, endArr[2])
+    let unixDb = db.getTime()
+    let unixDe = de.getTime()
+    let stamp
+    const oneDay = 24 * 60 * 60 * 1000;
+    for (stamp = unixDb; stamp <= unixDe;) {
+        if (type == 1) {
+            dateArr.push(`${format(new Date(parseInt(stamp)))}${h}`)
+        }
+        if (type == 2) {
+            for (let i = 0; i < 24; i++) {
+                let h = 0;
+                if (i < 10) {
+                    h = ` 0${i}`
+                } else {
+                    h = ` ${i}`
+                }
+                dateArr.push(`${format(new Date(parseInt(stamp)))}${h}`)
+            }
+        }
+        stamp = stamp + oneDay
+    }
+    return dateArr
+}
+
+
+export {
+    getWeekStartAndEnd,
+    getMonthStartAndEnd,
+    getHalfYearStartDate,
+    getHalfYearEndDate,
+    getThisYearStartDate,
+    getThisYearEndDate,
+    getWeekStartDate,
+    getWeekEndDate,
+    getLastWeekStartDate,
+    getLastWeekEndDate,
+    getMonthStartDate,
+    getMonthEndDate,
+    getLastMonthStartDate,
+    getLastMonthEndDate,
+    getQuarterStartDate,
+    getQuarterEndDate,
+    timestampToTime,
+    getThisDateBeforMonth,
+    getThisDateNextMonth,
+    YearMonthDate,
+    getNYRSFM,
+    getWeekDate,
+    getTimeFormat,
+    getNYRSFM2,
+    getTimestamp,
+    getRangeDate,
+    getQuarter
+}

+ 43 - 0
src/assets/js/flexible.js

@@ -0,0 +1,43 @@
+(function flexible(window, document) {
+    var docEl = document.documentElement;
+    var dpr = window.devicePixelRatio || 1;
+
+    // adjust body font size
+    function setBodyFontSize() {
+        if (document.body) {
+            document.body.style.fontSize = 12 * dpr + "px";
+        } else {
+            document.addEventListener("DOMContentLoaded", setBodyFontSize);
+        }
+    }
+    setBodyFontSize();
+
+    // set 1rem = viewWidth / 10
+    function setRemUnit() {
+        var rem = docEl.clientWidth / 192;    //192等份
+        docEl.style.fontSize = rem + "px";
+    }
+
+    setRemUnit();
+
+    // reset rem unit on page resize
+    window.addEventListener("resize", setRemUnit);
+    window.addEventListener("pageshow", function(e) {
+        if (e.persisted) {
+            setRemUnit();
+        }
+    });
+
+    // detect 0.5px supports
+    if (dpr >= 2) {
+        var fakeBody = document.createElement("body");
+        var testElement = document.createElement("div");
+        testElement.style.border = ".5px solid transparent";
+        fakeBody.appendChild(testElement);
+        docEl.appendChild(fakeBody);
+        if (testElement.offsetHeight === 1) {
+            docEl.classList.add("hairlines");
+        }
+        docEl.removeChild(fakeBody);
+    }
+})(window, document);

+ 99 - 0
src/assets/styles/btn.scss

@@ -0,0 +1,99 @@
+@import './variables.module.scss';
+
+@mixin colorBtn($color) {
+  background: $color;
+
+  &:hover {
+    color: $color;
+
+    &:before,
+    &:after {
+      background: $color;
+    }
+  }
+}
+
+.blue-btn {
+  @include colorBtn($blue)
+}
+
+.light-blue-btn {
+  @include colorBtn($light-blue)
+}
+
+.red-btn {
+  @include colorBtn($red)
+}
+
+.pink-btn {
+  @include colorBtn($pink)
+}
+
+.green-btn {
+  @include colorBtn($green)
+}
+
+.tiffany-btn {
+  @include colorBtn($tiffany)
+}
+
+.yellow-btn {
+  @include colorBtn($yellow)
+}
+
+.pan-btn {
+  font-size: 14px;
+  color: #fff;
+  padding: 14px 36px;
+  border-radius: 8px;
+  border: none;
+  outline: none;
+  transition: 600ms ease all;
+  position: relative;
+  display: inline-block;
+
+  &:hover {
+    background: #fff;
+
+    &:before,
+    &:after {
+      width: 100%;
+      transition: 600ms ease all;
+    }
+  }
+
+  &:before,
+  &:after {
+    content: '';
+    position: absolute;
+    top: 0;
+    right: 0;
+    height: 2px;
+    width: 0;
+    transition: 400ms ease all;
+  }
+
+  &::after {
+    right: inherit;
+    top: inherit;
+    left: 0;
+    bottom: 0;
+  }
+}
+
+.custom-button {
+  display: inline-block;
+  line-height: 1;
+  white-space: nowrap;
+  cursor: pointer;
+  background: #fff;
+  color: #fff;
+  -webkit-appearance: none;
+  text-align: center;
+  box-sizing: border-box;
+  outline: 0;
+  margin: 0;
+  padding: 10px 15px;
+  font-size: 14px;
+  border-radius: 4px;
+}

+ 132 - 0
src/assets/styles/element-ui.scss

@@ -0,0 +1,132 @@
+// cover some element-ui styles
+
+.el-breadcrumb__inner,
+.el-breadcrumb__inner a {
+    font-weight: 400 !important;
+}
+
+.el-upload {
+    input[type="file"] {
+        display: none !important;
+    }
+}
+
+.el-upload__input {
+    display: none;
+}
+
+.cell {
+    .el-tag {
+        margin-right: 0px;
+    }
+}
+
+.small-padding {
+    .cell {
+        padding-left: 5px;
+        padding-right: 5px;
+    }
+}
+
+.fixed-width {
+    .el-button--mini {
+        padding: 7px 10px;
+        width: 60px;
+    }
+}
+
+.status-col {
+    .cell {
+        padding: 0 10px;
+        text-align: center;
+
+        .el-tag {
+            margin-right: 0px;
+        }
+    }
+}
+
+// to fixed https://github.com/ElemeFE/element/issues/2461
+.el-dialog {
+    transform: none;
+    left: 0;
+    position: relative;
+    margin: 0 auto;
+}
+
+// refine element ui upload
+.upload-container {
+    .el-upload {
+        width: 100%;
+
+        .el-upload-dragger {
+            width: 100%;
+            height: 200px;
+        }
+    }
+}
+
+// dropdown
+.el-dropdown-menu {
+    a {
+        display: block
+    }
+}
+
+// fix date-picker ui bug in filter-item
+.el-range-editor.el-input__inner {
+    display: inline-flex !important;
+}
+
+// to fix el-date-picker css style
+.el-range-separator {
+    box-sizing: content-box;
+}
+
+.el-menu--collapse>div>.el-submenu>.el-submenu__title .el-submenu__icon-arrow {
+    display: none;
+}
+
+.el-dropdown .el-dropdown-link {
+    color: var(--el-color-primary) !important;
+}
+
+.form-inline {
+    display: flex;
+    flex-wrap: wrap;
+
+    .el-form-item {
+        flex: 0 0 50%;
+
+        .el-form-item__content {
+            align-items: flex-start;
+        }
+    }
+
+    .el-form-pack-6 {
+        flex: 0 0 25%;
+        height: min-content;
+    }
+
+    .el-form-pack-8 {
+        flex: 0 0 33.33333333%;
+        height: min-content;
+    }
+
+    .el-form-pack-12 {
+        flex: 0 0 50%;
+        height: min-content;
+    }
+
+    .el-form-pack-24 {
+        flex: 0 0 100%;
+        height: min-content;
+    }
+
+    .el-form-pack-12,
+    .el-form-pack-24 {
+        .el-form-item {
+            width: 100%;
+        }
+    }
+}

+ 66 - 0
src/assets/styles/handle.scss

@@ -0,0 +1,66 @@
+@import "./themes.scss";
+
+//切换主题时 获取不同的主题色值
+@mixin themeify {
+
+    @each $theme-name,
+    $theme-map in $themes {
+        //!global 把局部变量强升为全局变量
+        $theme-map: $theme-map  !global;
+
+        //判断html的data-theme的属性值  #{}是sass的插值表达式
+        //& sass嵌套里的父容器标识   @content是混合器插槽,像vue的slot
+        [data-theme="#{$theme-name}"] & {
+            @content;
+        }
+    }
+}
+
+
+//从主题色map中取出对应颜色
+@function themed($key) {
+    @return map-get($theme-map, $key);
+}
+
+//获取背景颜色
+@mixin background_color($color) {
+    @include themeify {
+        background-color: themed($color) !important;
+    }
+}
+
+//获取字体颜色
+@mixin font_color($color) {
+    @include themeify {
+        color: themed($color) !important;
+    }
+}
+
+//获取icon颜色
+@mixin fill_color($fill) {
+    @include themeify {
+        fill: themed($fill) !important;
+    }
+}
+
+//获取border
+@mixin border($border) {
+    @include themeify {
+        border: themed($border);
+    }
+}
+
+//获取border_color
+@mixin border_color($color) {
+    @include themeify {
+        border-color: themed($color);
+    }
+}
+
+
+//获取box-shadow
+@mixin box_shadow($color) {
+    @include themeify {
+        box-shadow: themed($color);
+    }
+}

+ 1290 - 0
src/assets/styles/index.scss

@@ -0,0 +1,1290 @@
+@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%;
+    width:100%;
+    max-width: 3840px;
+    margin: 0;
+    -moz-osx-font-smoothing: grayscale;
+    -webkit-font-smoothing: antialiased;
+    text-rendering: optimizeLegibility;
+    font-family: 'Microsoft YaHei', '微软雅黑',Helvetica Neue, Helvetica, PingFang SC, Hiragino Sans GB, Microsoft YaHei, Arial, sans-serif;
+}
+
+label {
+    font-weight: 700;
+}
+
+html {
+    height: 100%;
+    box-sizing: border-box;
+}
+
+#app {
+    height: 100%;
+}
+
+*,
+*:before,
+*:after {
+    box-sizing: inherit;
+}
+
+.no-padding {
+    padding: 0px !important;
+}
+
+.padding-content {
+    padding: 4px 0;
+}
+
+a:focus,
+a:active {
+    outline: none;
+}
+
+a,
+a:focus,
+a:hover {
+    cursor: pointer;
+    color: inherit;
+    text-decoration: none;
+}
+
+div:focus {
+    outline: none;
+}
+
+.fr {
+    float: right;
+}
+
+.fl {
+    float: left;
+}
+
+.pr-5 {
+    padding-right: 5px;
+}
+
+.pl-5 {
+    padding-left: 5px;
+}
+
+.block {
+    display: block;
+}
+
+.pointer {
+    cursor: pointer;
+}
+
+.inlineBlock {
+    display: block;
+}
+
+.clearfix {
+    &:after {
+        visibility: hidden;
+        display: block;
+        font-size: 0;
+        content: " ";
+        clear: both;
+        height: 0;
+    }
+}
+
+aside {
+    background: #eef1f6;
+    padding: 8px 24px;
+    margin-bottom: 20px;
+    border-radius: 2px;
+    display: block;
+    line-height: 32px;
+    font-size: 16px;
+    font-family: 'Microsoft YaHei', '微软雅黑',-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, Oxygen, Ubuntu, Cantarell, "Fira Sans", "Droid Sans", "Helvetica Neue", sans-serif;
+    color: #2c3e50;
+    -webkit-font-smoothing: antialiased;
+    -moz-osx-font-smoothing: grayscale;
+
+    a {
+        color: #337ab7;
+        cursor: pointer;
+
+        &:hover {
+            color: rgb(32, 160, 255);
+        }
+    }
+}
+
+//main-container全局样式
+.components-container {
+    margin: 30px 50px;
+    position: relative;
+}
+
+.text-center {
+    text-align: center
+}
+
+.sub-navbar {
+    height: 50px;
+    line-height: 50px;
+    position: relative;
+    width: 100%;
+    text-align: right;
+    padding-right: 20px;
+    transition: 600ms ease position;
+    background: linear-gradient(90deg, rgba(32, 182, 249, 1) 0%, rgba(32, 182, 249, 1) 0%, rgba(33, 120, 241, 1) 100%, rgba(33, 120, 241, 1) 100%);
+
+    .subtitle {
+        font-size: 20px;
+        color: #fff;
+    }
+
+    &.draft {
+        background: #d0d0d0;
+    }
+
+    &.deleted {
+        background: #d0d0d0;
+    }
+}
+
+.link-type,
+.link-type:focus {
+    color: #337ab7;
+    cursor: pointer;
+
+    &:hover {
+        color: rgb(32, 160, 255);
+    }
+}
+
+.filter-container {
+    padding-bottom: 10px;
+
+    .filter-item {
+        display: inline-block;
+        vertical-align: middle;
+        margin-bottom: 10px;
+    }
+}
+
+//refine vue-multiselect plugin
+.multiselect {
+    line-height: 16px;
+}
+
+.multiselect--active {
+    z-index: 1000 !important;
+}
+
+.inlineForm .el-form-item {
+    margin-right: 20px !important;
+    margin-bottom: 12px;
+}
+
+.el-form--inline .el-form-item__label {
+    padding-right: 8px
+}
+
+.operate-area .el-button {
+    margin-bottom: 12px;
+    height: 32px;
+    font-size: 14px;
+}
+
+.operate-area .el-row {
+    margin-left: 0 !important;
+    margin-right: 0 !important
+}
+
+.search-area .el-button,
+.operate-area .el-button {
+    padding: 8px 7px !important;
+}
+
+.search-area.el-form .el-form-item__label{
+    font-weight:normal;
+    color:#333;
+
+}
+
+
+.top-right-btn .el-button {
+    padding: 8px !important;
+}
+
+.el-form-item .el-button+.el-button {
+    margin-left: 12px !important;
+}
+
+//弹框底部button
+.el-dialog__footer,
+.dialog-footer {
+    .el-button {
+        width: 66px !important;
+    }
+}
+
+//上传图片button
+.el-upload--picture {
+    .el-button {
+        width: 66px !important;
+    }
+}
+
+// 表格状态按钮高度
+.cell {
+    .el-button.is-link {
+        height: 20px
+    }
+
+    .el-switch {
+        height: auto;
+
+        .el-switch__core {
+            height: 18px;
+            min-width: 34px;
+
+            .el-switch__action {
+                width: 14px;
+                height: 14px;
+                left: 2px
+            }
+        }
+    }
+}
+
+.cell .el-switch.is-checked .el-switch__core .el-switch__action {
+    left: calc(100% - 16px) !important;
+}
+
+// 滚动条自定义
+::-webkit-scrollbar {
+    /* 滚动条整体样式 */
+    width: 5px;
+    /*高宽分别对应横竖滚动条的尺寸*/
+    height: 5px;
+}
+
+::-webkit-scrollbar-thumb {
+    /* 滚动条里面小方块 */
+    border-radius: 10px;
+    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0.2);
+    background-color: #9e9e9e;
+}
+
+::-webkit-scrollbar-track {
+    /* 滚动条里面轨道 */
+    -webkit-box-shadow: inset 0 0 5px rgba(0, 0, 0, 0);
+    border-radius: 10px;
+}
+
+.el-form--inline .el-form-item {
+    // margin-bottom: 18px;
+}
+
+.el-form--inline .el-input {
+    --el-input-width: 100% !important;
+}
+
+.statistics {
+    // width: 32%;
+    float: right;
+    text-align: right;
+    vertical-align: middle;
+
+    >div {
+        // width: 25%;
+        display: inline-block;
+        margin-top: 6px;
+        margin-right: 10px;
+
+        >span:nth-child(2) {
+            font-size: 16px;
+            font-weight: 700;
+        }
+    }
+}
+
+// 综合云图改版 start
+.unitEquipInner {}
+
+.unitEquipInner .el-row {
+    margin-top: 2rem;
+    height: calc(100% - 4rem);
+
+    // display:flex;
+    .connectNumberItem {
+        height: 100%;
+        display: flex;
+        align-items: center;
+        font-size: 1.4rem;
+
+        .bigNum {
+            font-size: 2.4rem;
+            font-weight: bold
+        }
+
+        img {
+            height: 7.7rem;
+        }
+    }
+}
+
+.onOffBox .content {
+    margin-top: 1.8rem;
+    background: url('@/assets/images/business/iot/onOffBg.png');
+    background-size: 100% 100%;
+    background-repeat: no-repeat;
+    position: relative;
+    height: 11.4rem;
+    color: #fff;
+
+    .numStastic {
+        font-size: 1.4rem;
+
+        div {
+            margin: 0px 0 .5rem;
+            padding: 0.5rem .5rem 0.5rem 1rem;
+        }
+    }
+
+    span:first-child {
+        min-width: 7rem;
+        display: inline-block
+    }
+
+    span:last-child {
+        font-size: 1.8rem;
+        color: #2AD7F0;
+        font-weight: 500;
+    }
+}
+
+.timeSelectSec {
+    position: absolute;
+    right: 0;
+
+    .el-radio-button--small .el-radio-button__inner {
+        padding: .7rem 1.1rem;
+        font-size: 1.2rem;
+    }
+
+    .el-radio-button:first-child .el-radio-button__inner,
+    .el-radio-button:last-child .el-radio-button__inner {
+        border-radius: 0px;
+    }
+
+    .el-radio-button:first-child .el-radio-button__inner {
+        border-left: 1px solid#00B7CD;
+    }
+
+    .el-radio-button__inner {
+        background: transparent;
+        color: #fff;
+        border: 1px solid #00B7CD
+    }
+
+    .el-radio-button__inner:hover {
+        color: #00B7CD
+    }
+
+    .el-radio-button__original-radio:checked+.el-radio-button__inner {
+        background-color: rgba(0, 0, 0, 0);
+        border-color: #00B7CD;
+        box-shadow: inset 0 0 10px 1px #00B7CD !important;
+    }
+}
+
+.DeviceSelectSec {
+    .el-select {
+        width: 12rem;
+        margin-bottom: 1rem;
+        display: block;
+    }
+
+    .el-input .el-input__wrapper {
+        border: 1px solid #00B7CD !important;
+        height: 2.8rem
+    }
+
+    //去除默认样式
+    .el-input .el-input__wrapper:hover {
+        box-shadow: 0 0 0 0px #00B7CD inset !important;
+    }
+
+    .el-input .el-input__wrapper:active {
+        box-shadow: 0 0 0 0px #00B7CD inset !important;
+    }
+
+    .el-input .el-input__wrapper.is-focus {
+        box-shadow: none !important;
+    }
+
+    .el-select .el-input.is-focus .el-input__wrapper {
+        border-color: none !important;
+        box-shadow: none !important;
+    }
+
+    .input-search2 {
+        .el-input__suffix {}
+    }
+}
+
+.equipDangerData {
+    .equipDangerOne {
+        background: linear-gradient(to right, rgba(61, 153, 221, .06), rgba(61, 153, 221, .1), rgba(61, 153, 221, .2));
+        margin: 1rem 1rem 0;
+        border-radius: 0 0 2rem 0;
+        padding: 0px 1rem;
+        width: 19rem;
+        line-height: 3.9rem;
+
+        span {
+            display: inline-block;
+        }
+
+        span:first-child {
+            width: 10rem;
+            font-size: 1.4rem;
+        }
+
+        span:last-child {
+            margin-left: 1rem;
+            font-size: 2rem;
+            color: #00B7CD;
+            font-weight: bold
+        }
+    }
+}
+
+.newIot2.facilityTypeContainer {
+    width: 40rem;
+    margin: 0 auto;
+    position: fixed;
+    bottom: 2rem;
+    z-index: 1;
+    text-align: center;
+    left: calc(50% - 20rem);
+
+    .swiper-container {
+        width: 100%;
+    }
+
+    .swiper-slide {
+        text-align: center;
+        margin: 10px 0;
+        display: -webkit-box;
+        display: -ms-flexbox;
+        display: -webkit-flex;
+        display: flex;
+        -webkit-box-pack: center;
+        -ms-flex-pack: center;
+        -webkit-justify-content: center;
+        justify-content: center;
+        -webkit-box-align: center;
+        -ms-flex-align: center;
+        -webkit-align-items: center;
+        align-items: center;
+        background-repeat: no-repeat;
+        background-position: center center;
+        background-size: cover;
+    }
+
+    .facilityItem {
+        display: inline-block;
+        color: #fff;
+        text-align: center;
+        width: 8rem;
+        height: 8rem;
+        background-image: url(@/assets/images/business/iot/facilityBg2.png);
+        background-size: 100% 100%;
+        display: flex;
+        justify-content: center;
+        align-items: center;
+        cursor: pointer;
+
+        span {
+            width: 20px;
+            height: 20px
+        }
+
+        >div {
+            div:first-child {
+                font-size: 1.8rem
+            }
+
+            div:last-child {
+                font-size: 1.4rem
+            }
+        }
+    }
+
+    .facilityItem:hover,
+    .swiper-slide.active .facilityItem {
+        background-image: url(@/assets/images/business/iot/facilityBg2Active.png);
+    }
+
+    .swiper-button-next,
+    .swiper-rtl .swiper-button-prev {
+        right: var(--swiper-navigation-sides-offset, -31px);
+        left: auto;
+
+        img {
+            width: 5rem
+        }
+    }
+
+    .swiper-button-prev,
+    .swiper-rtl .swiper-button-next {
+        left: var(--swiper-navigation-sides-offset, -33px);
+        right: auto;
+
+        img {
+            width: 5rem
+        }
+    }
+
+    .swiper-button-next:after,
+    .swiper-rtl .swiper-button-prev:after,
+    .swiper-button-prev:after,
+    .swiper-rtl .swiper-button-next:after {
+        content: ''
+    }
+}
+
+.alarmInfoContent {
+    display: flex;
+
+    .contentOne {
+        margin-right: 2rem;
+        text-align: center;
+        background: url(@/assets/images/business/ITONew3/alarmInfoBg1.png);
+        background-size: 100% 100%;
+        width: calc(100%/3 - 4rem/3);
+        display: inline-block;
+        height: calc(100% - 1.8rem);
+        padding: 1rem 0;
+        font-size: 1.4rem;
+
+        .num {
+            font-size: 2.4rem;
+            color: #2AD7F0;
+        }
+
+        .title {
+            margin-top: 1.6rem;
+            margin-bottom: 2rem;
+            font-size: 1.4rem;
+        }
+    }
+
+    .contentOne:last-child {
+        // height:calc(100%/2 - 3.5rem/2);
+        margin-right: 0;
+        padding: 0;
+        background: none;
+
+        .num {
+            color: #53EA8C
+        }
+
+        .short {
+            padding: 1rem 0;
+            background: url(@/assets/images/business/ITONew3/alarmInfoBg1.png);
+            background-size: 100% 100%;
+
+            .title {
+                margin-bottom: .5rem
+            }
+        }
+
+        .short:first-child {
+            margin-bottom: 2rem;
+        }
+    }
+}
+
+//   综合云图改版end
+
+/* 外边距、内边距全局样式
+------------------------------- */
+
+@for $i from 0 through 100 {
+
+    // margin
+    .m#{$i} {
+        margin: #{$i}px !important;
+    }
+
+    .mt#{$i} {
+        margin-top: #{$i}px !important;
+    }
+
+    .mr#{$i} {
+        margin-right: #{$i}px !important;
+    }
+
+    .mb#{$i} {
+        margin-bottom: #{$i}px !important;
+    }
+
+    .ml#{$i} {
+        margin-left: #{$i}px !important;
+    }
+
+    .mlr#{$i} {
+        margin-left: #{$i}px !important;
+        margin-right: #{$i}px !important;
+    }
+
+    .mtb#{$i} {
+        margin-top: #{$i}px !important;
+        margin-bottom: #{$i}px !important;
+    }
+
+    // padding
+    .p#{$i} {
+        padding: #{$i}px !important;
+    }
+
+    .pt#{$i} {
+        padding-top: #{$i}px !important;
+    }
+
+    .pr#{$i} {
+        padding-right: #{$i}px !important;
+    }
+
+    .pb#{$i} {
+        padding-bottom: #{$i}px !important;
+    }
+
+    .pl#{$i} {
+        padding-left: #{$i}px !important;
+    }
+
+    .plr#{$i} {
+        padding-left: #{$i}px !important;
+        padding-right: #{$i}px !important;
+    }
+
+    .ptb#{$i} {
+        padding-top: #{$i}px !important;
+        padding-bottom: #{$i}px !important;
+    }
+}
+
+
+/* 文本
+------------------------------- */
+
+@for $i from 10 through 500 {
+    .font#{$i} {
+        font-size: #{$i}px !important;
+    }
+}
+
+// 电视墙样式 start
+#tvWallWraper {
+    min-width: 1024px;
+    background: #021132;
+    width: 100%;
+    height: 100%;
+    color: #fff;
+    overflow-y: scroll;
+    text-align: center;
+    font-size: 1.2rem;
+
+    header {
+        height: 5.4rem;
+        line-height: 5.4rem;
+        width: 100%;
+        font-size: 2.8rem;
+        text-align: center;
+        background: url(@/assets/images/business/deviceManage/tvWall/headerBg.png);
+        background-size: 100% 100%;
+        background-repeat: no-repeat;
+        position: relative
+    }
+
+    .statisticBox {
+        text-align: center;
+        margin-top: 2rem;
+        font-size: 1.6rem;
+
+        // margin-bottom:2rem;
+        >div {
+            display: inline-block;
+            width: 10.9rem;
+            height: 4rem;
+            line-height: 4rem;
+            text-align: center;
+            margin-right: 2rem;
+            background-size: 100% 100%;
+            background-image: url(@/assets/images/business/deviceManage/tvWall/warning.png)
+        }
+
+        div:nth-child(2) {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/default.png)
+        }
+
+        div:last-child {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/offline.png)
+        }
+    }
+
+    .main-inner {
+        box-sizing: border-box;
+        overflow: hidden;
+        margin: 0 4.5rem 2rem;
+        height: 90rem;
+
+        .blockOne {
+            position: relative;
+            // width:29.8rem;
+            width: calc(100%/6 - 1.5rem*5/6);
+            height: 28rem;
+            background-size: 100% 100%;
+            background-repeat: no-repeat;
+            margin-top: 2rem;
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            // display:block;
+            float: left;
+            margin-right: 1.5rem;
+
+            .title {
+                width: 100%;
+                height: 2.6rem;
+                line-height: 2.6rem;
+                position: absolute;
+                top: 0;
+                text-align: center;
+                background: url(@/assets/images/business/deviceManage/tvWall/titleBg.png);
+                background-size: 100% 100%;
+            }
+
+            .warningInfo {
+                position: absolute;
+                top: 3.5rem;
+                width: 99%;
+                height: 2rem;
+                line-height: 2rem;
+                background: #FF3939;
+                text-align: center;
+                font-size: 1.4rem;
+            }
+
+            .mySwiper {
+                padding-bottom: 3rem;
+
+                .swiper-pagination-bullet-active {
+                    background: #fff !important;
+                }
+
+                .swiper-pagination-bullet {
+                    background: #7d9ef3;
+                    width: .5rem;
+                    height: .5rem;
+                }
+
+                .swiper-slide {
+                    text-align: center;
+                    display: flex;
+                    justify-content: center;
+                    align-items: center;
+                    display: block;
+
+                    .staticsOne {
+                        margin: 0 auto;
+                        display: flex;
+                        justify-content: space-between;
+                        width: 55%;
+                        height: 2.8rem;
+                        line-height: 2.8rem;
+                        background: #0D204F;
+                        margin-bottom: 1rem;
+                        font-size: 1.4rem;
+                        padding: 0 2rem;
+                    }
+                }
+
+                .swiper-button-prev:after,
+                .swiper-rtl .swiper-button-next:after,
+                .swiper-button-next:after,
+                .swiper-rtl .swiper-button-prev:after {
+                    content: ''
+                }
+
+                .swiper-button-prev,
+                .swiper-button-next {
+                    top: 37%
+                }
+            }
+
+            .checkMore {
+                width: 11.2rem;
+                height: 2.8rem;
+                line-height: 2.8rem;
+                text-align: center;
+                background: #2E50AA;
+                bottom: 3.5rem;
+                position: absolute;
+                left: 50%;
+                margin-left: -5.6rem;
+                border-radius: 2px;
+                cursor: pointer;
+            }
+
+            .title2 {
+                width: 11.2rem;
+                height: 2.8rem;
+                line-height: 2.8rem;
+                text-align: center;
+                bottom: 3rem;
+                position: absolute;
+                left: 50%;
+                margin-left: -5.6rem;
+                color: #fff;
+            }
+
+            .tpChartBox {
+                width: 12rem;
+                height: calc(100% - 10rem);
+                background: #053275;
+                box-shadow: rgb(131, 161, 242) 0px 0px 14px -3px inset
+            }
+
+            .gaugeChartBox {
+                width: 100%;
+                height: calc(100% - 10rem);
+            }
+
+            .pieChartBox {
+                width: 40%;
+                height: 100%;
+                display: inline-block;
+                margin: 0 1rem;
+                height: calc(100% - 10rem);
+            }
+
+            .batteryImg {
+                width: 3.2rem;
+                position: absolute;
+                top: 3.8rem;
+                right: 1.2rem;
+                background-image: url(@/assets/images/business/deviceManage/tvWall/battery.png);
+                background-size: 100% 100%;
+                padding-right: 0.3rem;
+            }
+
+            .switchControl {
+                width: 15rem;
+                height: 15rem;
+                text-align: center;
+                line-height: 15rem;
+                font-size: 2.6rem;
+                color: #0390FF;
+                background-image: url(@/assets/images/business/deviceManage/tvWall/switchBg.png);
+                background-size: 100% 100%;
+            }
+
+            .footerText {
+                position: absolute;
+                bottom: 1rem;
+                width: 100%;
+                left: 0;
+                text-align: center;
+                color: #C9C9C9
+            }
+        }
+
+        .warningBg {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/warningBg.png);
+        }
+
+        .faultBg {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/faultBg.png);
+        }
+
+        .offlineBg {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/offlineBg.png);
+        }
+
+        .normalBg {
+            background-image: url(@/assets/images/business/deviceManage/tvWall/blockOneBg.png);
+        }
+
+        block:nth-child(6n+6) .blockOne {
+            margin-right: 0;
+        }
+
+        // .blockOne:first-child{
+        //     background-image:url(@/assets/images/business/deviceManage/tvWall/warningBg.png);
+        // }
+    }
+
+    footer {
+        text-align: center;
+
+        .footerInner::-webkit-scrollbar {
+            height: 6px;
+            cursor: pointer;
+        }
+
+        .footerInner::-webkit-scrollbar-thumb {
+            background: rgb(113, 178, 248);
+            border-radius: 0;
+        }
+
+        .footerInner::-webkit-scrollbar-track {
+            background: rgba(255, 255, 255, .8);
+            border-radius: 0;
+        }
+
+        .footerInner {
+            width: 65rem;
+            margin: 0 auto;
+            white-space: nowrap;
+            // overflow-x: auto;
+            position: relative;
+
+            div.swiper-slide {
+                display: inline-block;
+                width: 13rem !important;
+                // padding:0 2rem;
+                height: 4rem;
+                line-height: 4rem;
+                font-size: 1.6rem;
+                text-align: center;
+                background-size: 100% 100%;
+                background-image: url(@/assets/images/business/deviceManage/tvWall/footerBg.png);
+                cursor: pointer;
+
+                span {
+                    width: 7.95rem;
+                    display: inline-block;
+                    white-space: nowrap;
+                    /* 不换行 */
+                    overflow: hidden;
+                    /* 超出部分隐藏 */
+                }
+            }
+
+            .swiper-slide.active,
+            .swiper-slide:hover {
+                background-image: url(@/assets/images/business/deviceManage/tvWall/footerBgActive.png);
+            }
+
+            .swiper-button-next,
+            .swiper-rtl .swiper-button-prev {
+                width: 1.6rem;
+                height: 4rem;
+                background-size: 100% 100%;
+                background-image: url(@/assets/images/business/deviceManage/tvWall/arrowRight.png);
+                right: var(--swiper-navigation-sides-offset, -4rem);
+                left: auto;
+            }
+
+            .swiper-button-prev,
+            .swiper-rtl .swiper-button-next {
+                width: 1.6rem;
+                height: 4rem;
+                background-size: 100% 100%;
+                background-image: url(@/assets/images/business/deviceManage/tvWall/arrowLeft.png);
+                left: var(--swiper-navigation-sides-offset, -4rem);
+                right: auto;
+            }
+
+            .swiper-button-next:hover,
+            .swiper-rtl .swiper-button-prev:hover {
+                background-image: url(@/assets/images/business/deviceManage/tvWall/arrowRightActive.png);
+            }
+
+            .swiper-button-prev:hover,
+            .swiper-rtl .swiper-button-next:hover {
+                background-image: url(@/assets/images/business/deviceManage/tvWall/arrowLeftActive.png);
+            }
+
+            .swiper-button-prev:after,
+            .swiper-rtl .swiper-button-next:after,
+            .swiper-button-next:after,
+            .swiper-rtl .swiper-button-prev:after {
+                content: ''
+            }
+        }
+    }
+
+    .posLeft,
+    .posRight {
+        position: fixed;
+        top: 50%;
+        font-size: 2.8rem;
+        cursor: pointer;
+    }
+
+    .posLeft {
+        left: 1.5rem;
+    }
+
+    .posRight {
+        right: 1.5rem;
+    }
+
+    .el-pagination {
+        position: absolute;
+        top: 45%;
+
+        // width:calc( 100% - 24px);
+        .el-icon {
+            display: none
+        }
+
+        .btn-prev {
+            position: absolute;
+            left: .7rem;
+            background: url(@/assets/images/business/deviceManage/tvWall/leftArrowBig.png);
+        }
+
+        .btn-next {
+            position: absolute;
+            right: .7rem;
+            background: url(@/assets/images/business/deviceManage/tvWall/rightArrowBig.png);
+        }
+
+        .btn-prev,
+        .btn-next {
+            background-size: 100% 100%;
+            width: 2.8rem;
+        }
+    }
+
+    .fullScreen {
+        position: fixed;
+        bottom: 2rem;
+        right: 2rem;
+        font-size: 1.2rem;
+        cursor: pointer;
+
+        img {
+            width: 2rem
+        }
+
+        img:hover {
+            opacity: .7
+        }
+    }
+}
+
+.el-select-dropdown__item.selected{
+    font-weight:normal!important
+}
+
+.dialogLoadText{
+    padding:50px;text-align:center;
+    
+}
+
+
+// ui规范
+input::placeholder{
+    color:#999 !important;
+    font-family: 'Microsoft YaHei', '微软雅黑';
+}
+.el-input__suffix svg{
+    width:12px !important;
+    height:12px !important;
+}
+.el-button--default{
+    border-radius: 4px !important;
+    // border:1px solid #e5e5e5 !important;
+    // background-color: #fff !important;
+}
+.cell{
+    button{
+        border:none !important;
+    }
+}
+// .operate-area >button{
+//     margin-left: 20px !important;
+// }
+// .operate-area >button:nth-child(1){
+//     margin-left: 0px !important;
+// }
+.el-table__cell .cell{
+    font-size: 13px !important;
+}
+.tableWrap{
+    overflow-y: scroll;
+    padding-bottom:2px;
+}
+.pagination{
+    margin-right:6px;
+    // bottom:36px;
+}
+.el-button--default span{
+    font-size: 13px !important;
+}
+
+// 项目管理 start
+.el-table .el-table__cell{
+    // vertical-align: top!important;
+}
+.porjectLine span{
+    display:inline-block;
+    vertical-align: top;
+}
+.porjectLine span:first-child{
+    width:40px;
+}
+.timeTitle{
+    color:#333;
+    margin-top:10px
+}
+.projectItem{
+    border-bottom:1px solid rgba(212,212,212,.5);
+    padding:5px 0
+}
+ .tooltip-width{
+    width:400px 
+}
+.porjectLine span:last-child{
+    width:calc(100% - 0px);
+    display: -webkit-box;
+    -webkit-box-orient: vertical;
+    -webkit-line-clamp: 5;
+    overflow: hidden;
+    text-overflow: ellipsis;
+}
+.projectItem .porjectLine:first-child{
+    font-size:11px
+
+}
+.projectItem .porjectLine:last-child{
+    font-size:10px;
+ 
+}
+
+
+
+.projectItem:last-child{
+    border-bottom:none;
+}
+
+.addArea{
+    position:relative;width: 95%; margin-left: 5%; border: 1px solid #d9d9d9; border-radius: 5px; padding: 10px; margin-bottom: 10px
+}
+
+
+.reportRecordBox{
+    font-size:14px;
+
+        .el-card__body {
+          padding: 15px;
+        }
+      
+}
+.reportRecordBox{
+    .searchList{
+        margin-top:10px;
+        height: 18vh;
+        overflow: scroll;
+        div{
+            border-bottom: 1px solid #D9D9D9;
+            line-height:36px;
+            font-size:14px;
+            margin:0 2px;
+            color:#333;
+            cursor:pointer;
+        
+        }
+        div.active,div:hover{
+            color:#0C83FA;
+            
+    
+        }
+        
+    
+    } 
+    .reportListBox{
+        margin-top:10px;
+        height: calc(100vh - 208px);
+        overflow: scroll;
+    }
+    .reportListOne{
+        display:flex;
+        align-items: center;
+        padding:10px;
+        border-bottom:1px solid #D9D9D9;
+        cursor:pointer;
+        
+        img{
+            
+            width:46px;
+        }
+    }
+    .reportListOne:hover,.reportListOne.active{
+        background: #E8F1FE;
+    }
+
+    //名字图标
+    .roundPer{
+        border-radius:50%;
+        background:#48A4FF;
+        color:#fff;
+        width:46px;
+        height:46px;
+        line-height:46px;
+        text-align:center;
+        margin-right:10px;
+    }
+    
+} 
+
+.reportDetailBox{
+    height:calc(100vh - 150px);
+    overflow:scroll;
+ .reportDetailOne{
+    // padding:20px 0 20px;
+    // border-bottom:1px solid #D9D9D9;
+    .copyIcon{
+        float:right;
+        margin-right:10px;
+        cursor:pointer;
+    }
+    >div{
+        display:inline-block;
+        vertical-align: top;
+    }
+    .time{
+        font-size:12px;
+        color: #666666;
+        margin:5px 0;
+    }
+    .projectOne {
+        margin-top:30px;
+        .title{
+            // font-weight:bold;
+            margin:5px 0;
+        }
+        .content{
+            color:rgba(3,3,3,.8);
+            line-height:24px;   
+        }
+    }
+    
+ }
+
+}
+// 项目管理 end

+ 66 - 0
src/assets/styles/mixin.scss

@@ -0,0 +1,66 @@
+@mixin clearfix {
+  &:after {
+    content: "";
+    display: table;
+    clear: both;
+  }
+}
+
+@mixin scrollBar {
+  &::-webkit-scrollbar-track-piece {
+    background: #d3dce6;
+  }
+
+  &::-webkit-scrollbar {
+    width: 6px;
+  }
+
+  &::-webkit-scrollbar-thumb {
+    background: #99a9bf;
+    border-radius: 20px;
+  }
+}
+
+@mixin relative {
+  position: relative;
+  width: 100%;
+  height: 100%;
+}
+
+@mixin pct($pct) {
+  width: #{$pct};
+  position: relative;
+  margin: 0 auto;
+}
+
+@mixin triangle($width, $height, $color, $direction) {
+  $width: $width/2;
+  $color-border-style: $height solid $color;
+  $transparent-border-style: $width solid transparent;
+  height: 0;
+  width: 0;
+
+  @if $direction==up {
+    border-bottom: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+
+  @else if $direction==right {
+    border-left: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+
+  @else if $direction==down {
+    border-top: $color-border-style;
+    border-left: $transparent-border-style;
+    border-right: $transparent-border-style;
+  }
+
+  @else if $direction==left {
+    border-right: $color-border-style;
+    border-top: $transparent-border-style;
+    border-bottom: $transparent-border-style;
+  }
+}

+ 247 - 0
src/assets/styles/ruoyi.scss

@@ -0,0 +1,247 @@
+/**
+ * 通用css样式布局处理
+ * Copyright (c) 2019 ruoyi
+ */
+/** 基础通用 **/
+
+.boxSizing {
+    box-sizing: border-box;
+    overflow: hidden;
+}
+
+.h1,
+.h2,
+.h3,
+.h4,
+.h5,
+.h6,
+h1,
+h2,
+h3,
+h4,
+h5,
+h6 {
+    font-family: inherit;
+    font-weight: 500;
+    line-height: 1.1;
+    color: inherit;
+}
+
+.el-dialog:not(.is-fullscreen) {
+    //  margin-top: 20vh !important;
+    // display: flex;
+    // flex-direction: column;
+    // margin: 0 !important;
+    // position: absolute;
+    // top: 50%;
+    // left: 50%;
+    // transform: translate(-50%, -50%);
+    // overflow-y:auto
+}
+
+.el-dialog.scrollbar .el-dialog__body {
+    overflow: auto;
+    overflow-x: hidden;
+    max-height: 70vh;
+    padding: 10px 20px 0;
+}
+
+.el-table {
+    .el-table__cell {
+        padding: 0 !important;
+        height: 40px;
+    }
+
+    .el-table__header-wrapper,
+    .el-table__fixed-header-wrapper {
+        th {
+            word-break: break-word;
+            background-color:  #f5f7fa !important;
+            border-top: 1px solid #e5e5e5;
+            border-bottom: 1px solid #e5e5e5;
+            box-sizing: border-box;
+            color: #515a6e;
+            height: 40px !important;
+            font-size: 14px;
+        }
+    }
+    
+
+    .el-table__body-wrapper {
+        .el-button [class*="el-icon-"]+span {
+            margin-left: 1px;
+        }
+    }
+}
+#app .app-container .el-table tr  {
+        background-color: #fff;
+        color:#333,
+
+    
+}
+/** 表单布局 **/
+
+.form-header {
+    font-size: 15px;
+    color: #6379bb;
+    border-bottom: 1px solid #ddd;
+    margin: 8px 10px 25px 10px;
+    padding-bottom: 5px;
+}
+
+/** 表格布局 **/
+
+
+/* tree border */
+
+@media (max-width: 768px) {
+    .pagination-container .el-pagination>.el-pagination__jump {
+        display: none !important;
+    }
+
+    .pagination-container .el-pagination>.el-pagination__sizes {
+        display: none !important;
+    }
+}
+
+.el-table .fixed-width .el-button--small {
+    padding-left: 0;
+    padding-right: 0;
+    width: inherit;
+}
+
+/** 表格更多操作下拉样式 */
+
+.el-table .el-dropdown-link {
+    cursor: pointer;
+    color: #409eff;
+    margin-left: 10px;
+}
+
+.el-table .el-dropdown,
+.el-icon-arrow-down {
+    font-size: 12px;
+}
+
+.el-tree-node__content>.el-checkbox {
+    margin-right: 8px;
+}
+
+.list-group-striped>.list-group-item {
+    border-left: 0;
+    border-right: 0;
+    border-radius: 0;
+    padding-left: 0;
+    padding-right: 0;
+}
+
+.list-group {
+    padding-left: 0px;
+    list-style: none;
+}
+
+.list-group-item {
+    border-bottom: 1px solid #e7eaec;
+    border-top: 1px solid #e7eaec;
+    margin-bottom: -1px;
+    padding: 11px 0px;
+    font-size: 13px;
+}
+
+.pull-right {
+    float: right !important;
+}
+
+.card-box {
+    padding-right: 15px;
+    padding-left: 15px;
+    margin-bottom: 10px;
+}
+
+/* button color */
+
+.el-button--cyan.is-active,
+.el-button--cyan:active {
+    background: #20b2aa;
+    border-color: #20b2aa;
+    color: #ffffff;
+}
+
+.el-button--cyan:focus,
+.el-button--cyan:hover {
+    background: #48d1cc;
+    border-color: #48d1cc;
+    color: #ffffff;
+}
+
+.el-button--cyan {
+    background-color: #20b2aa;
+    border-color: #20b2aa;
+    color: #ffffff;
+}
+
+/* text color */
+
+.text-navy {
+    color: #1ab394;
+}
+
+.text-primary {
+    color: inherit;
+}
+
+.text-success {
+    color: #1c84c6;
+}
+
+.text-info {
+    color: #23c6c8;
+}
+
+.text-warning {
+    color: #f8ac59;
+}
+
+.text-danger {
+    color: #ed5565;
+}
+
+.text-muted {
+    color: #888888;
+}
+
+/* image */
+
+.img-circle {
+    border-radius: 50%;
+}
+
+.img-lg {
+    width: 120px;
+    height: 120px;
+}
+
+.avatar-upload-preview {
+    position: absolute;
+    top: 50%;
+    transform: translate(50%, -50%);
+    width: 200px;
+    height: 200px;
+    border-radius: 50%;
+    box-shadow: 0 0 4px #ccc;
+    overflow: hidden;
+}
+
+/* 拖拽列样式 */
+
+.sortable-ghost {
+    opacity: 0.8;
+    color: #fff !important;
+    background: #42b983 !important;
+}
+
+/* 表格右侧工具栏样式 */
+
+.top-right-btn {
+    margin-left: auto;
+}

+ 608 - 0
src/assets/styles/sidebar.scss

@@ -0,0 +1,608 @@
+@import './handle.scss';
+//布局样式 开始
+#app {
+    @include fill_color('fontColor');
+    @include font_color('fontColor');
+    font-family: 'Microsoft YaHei', '微软雅黑', -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, "Apple Color Emoji", "Segoe UI Emoji", Segoe UI Symbol, "Noto Color Emoji";
+    font-weight: 400;
+    //左侧菜单栏样式 开始
+    .sidebar-container {
+        @include background_color("leftBackColor");
+        // menu hover
+        .sub-menu-title-noDropdown,
+        .el-sub-menu__title {
+            &:hover {
+                @include background_color("leftMenuBackColorHover");
+            }
+        }
+        & .sidebar-menu-container .is-active>.el-sub-menu__title {
+            @include font_color('leftFontActiveColor');
+        }
+        & .nest-menu .el-sub-menu>.el-sub-menu__title,
+        & .el-sub-menu .el-menu-item {
+            min-width: $base-sidebar-width !important;
+            height: 48px;
+            line-height: 48px;
+            &:hover {
+                @include background_color("leftMenuBackColorHover");
+            }
+        }
+        & .sidebar-menu-container .nest-menu .el-sub-menu>.el-sub-menu__title,
+        & .sidebar-menu-container .el-sub-menu .el-menu-item {
+            @include background_color("leftBackColor");
+            &:hover {
+                @include background_color("leftMenuBackColorHover");
+            }
+        }
+    }
+    // menu hover
+    .sidebar-menu-container {
+        .el-menu {
+            @include background_color("leftBackColor");
+            div {
+                @include font_color('fontColor');
+            }
+            .el-menu-item {
+                @include font_color('fontColor');
+            }
+            .el-menu-item.is-active {
+                @include font_color('leftFontActiveColor');
+                @include background_color("leftMenuBackColorHover");
+                .imgSvg {
+                    filter: drop-shadow(#409eff -30px -18px 0px);
+                }
+            }
+            .el-menu-item:hover {
+                @include background_color("leftMenuBackColorHover");
+                @include font_color('leftFontActiveColor');
+            }
+        }
+    }
+    .sidebar-logo-container {
+        @include background_color("leftBackColor");
+        .sidebar-icon-link {
+            #hamburger-container {
+                font-size: '14px';
+                @include fill_color('iconColor');
+            }
+            .icon-title {
+                @include font_color('titleColor');
+                display: 'inline-block';
+                font-size: 14px;
+            }
+        }
+        .sidebar-logo-link {
+            .sidebar-title {
+                @include font_color('titleColor');
+            }
+        }
+    }
+    //左侧菜单栏样式 结束
+    //顶部导航栏样式 开始
+    .navbar {
+        @include background_color("topBackColor");
+        .sidebar-logo-link {
+            .sidebar-title {
+                @include font_color('titleColor');
+            }
+        }
+        #hamburger-container {
+            @include fill_color('iconColor');
+        }
+    }
+    //顶部导航栏样式 结束
+    //中间内容样式 开始
+    .app-main {
+        @include background_color("containerBackColor1");
+        height: 100%;
+        width: 100%;
+        padding: 12px;
+        position: relative;
+        overflow: hidden;
+        display: flex;
+        .app-themes {
+            @include background_color("containerBackColor2");
+        }
+        .app-container {
+            height: 100%;
+            width: 100%;
+            &-count {
+                margin: 0 0px 12px 0px;
+                border: 1.5px rgba(141, 141, 141, 0.13) solid;
+                box-shadow: inset 0px 0px 10px 0px rgba(141, 141, 141, 0.02);
+                .el-col {
+                    display: flex;
+                    justify-content: center;
+                    border: 1px solid rgba(141, 141, 141, 0.08);
+                    .img {
+                        width: 50px;
+                        margin: auto 20px auto 0;
+                        display: inline-block;
+                    }
+                    .content {
+                        margin: auto 0 auto 0;
+                        display: inline-block;
+                        p:nth-child(1) {
+                            font-weight: 700;
+                            font-size: 14px;
+                            margin: 0 0 10px 0;
+                        }
+                        p:nth-child(2),
+                        p:nth-child(3) {
+                            font-size: 16px;
+                            margin: 10px 0 0 0;
+                        }
+                    }
+                }
+            }
+        }
+    }
+    //中间内容样式 结束
+}
+
+//布局样式 开始
+//组件功能样式 开始
+#app {
+    //面包屑样式 开始
+    .app-themes,
+    .app-container {
+        .el-breadcrumb__inner,
+        .el-breadcrumb__inner a,
+        .el-breadcrumb__inner.is-link {
+            @include font_color('fontColor');
+        }
+        .el-breadcrumb__inner a:hover {
+            color: #409EFF !important;
+        }
+        .el-breadcrumb__separator {
+            @include font_color('fontColor');
+        }
+    }
+    //面包屑样式 结束
+    //可关闭小标签页 开始
+    .main-container {
+        #tags-view-container.tags-view-container {
+            @include background_color("containerBackColor2");
+        }
+        .tags-view-container .tags-view-wrapper .tags-view-item.active {
+            @include background_color('closeBackColor');
+            border-color: rgba(12, 131, 250, .15) !important;
+            @include font_color('closeBackColorBefore');
+        }
+        .tags-view-container .tags-view-wrapper .tags-view-item.active::before {
+            @include background_color('closeBackColorBefore');
+        }
+    }
+    //可关闭小标签页 结束
+    //icon图标样式 开始
+    //icon图标样式 结束
+    //table样式 开始
+    .app-themes,
+    .app-container {
+        .el-table {
+            font-size: 13px;
+            @include background_color("tableBackColor");
+            @include font_color('tableFontColor');
+            @include fill_color("fontColor");
+            .el-button>span {
+                font-size: 12px;
+            }
+            tr {
+                @include background_color("tableBackColor");
+            }
+            td,
+            th.is-leaf {
+                @include border_color("tableBorderBackColoe");
+                @include background_color("tableBackColor");
+            }
+            .el-table__header-wrapper th,
+            .el-table__fixed-header-wrapper th {
+                @include font_color('tableHeaderFontColor');
+                @include background_color("tableHeaderBackColor");
+            }
+            .el-table__expand-icon>.el-icon {
+                @include font_color('tableIconColor');
+            }
+        }
+        .el-table--enable-row-hover .el-table__body tr:hover>td {
+            @include background_color("tableBackColorHover");
+        }
+        .el-table--border .el-table__inner-wrapper::after,
+        .el-table--border::after,
+        .el-table--border::before,
+        .el-table__inner-wrapper::before,
+        .el-table__border-left-patch {
+            @include background_color("tableBorderBackColoe");
+        }
+    }
+    //table样式 结束
+}
+
+body {
+    //card样式 开始
+    .el-card {
+        @include background_color("containerBackColor2");
+        @include border_color("containerBorderColor");
+        @include font_color('fontColor');
+        .el-card__header {
+            padding: 15px 20px;
+            @include background_color("containerHeaderBackColor");
+            @include border_color("containerBorderColor");
+            @include font_color('fontColor');
+        }
+        .el-card__body {
+            .card-header {
+                border-bottom: 1px solid #e4e7ed;
+                @include background_color("containerHeaderBackColor");
+                @include border_color("containerBorderColor");
+            }
+        }
+    }
+    //card样式 结束
+    //tabs样式 开始
+    .el-tabs {
+        @include font_color('fontColor');
+        .el-tabs__item {
+            @include font_color('tabsFontColor');
+            &:hover {
+                color: #409eff !important;
+            }
+            &.is-active {
+                color: #409eff !important;
+            }
+        }
+        .el-tabs__nav-wrap::after {
+            background-color: #e4e7ed;
+            height:1px;
+        }
+    }
+    .el-tabs--border-card {
+        @include border("tabsBorder");
+        @include background_color("tabsContentBackColor");
+        >.el-tabs__header {
+            border: 0px;
+            @include background_color("tabsHeaderBackColor");
+            .el-tabs__item {
+                @include font_color('tabsFontColor');
+            }
+            .el-tabs__item.is-active {
+                color: #409eff !important;
+                @include background_color("tabsBackColorActive");
+                @include border("tabsBorder");
+            }
+        }
+        >.el-tabs__content {}
+    }
+    //tabs样式 结束
+    //tree树样式 开始
+    .el-tree {
+        @include background_color("treeBackColor");
+        @include font_color('treeFontColor');
+        .el-tree-node.is-current>.el-tree-node__content {
+            @include background_color("treeBackColorActive");
+        }
+        .el-tree-node__content {
+            &:hover {
+                @include background_color("treeBackColorHover");
+            }
+        }
+        .el-tree-node__expand-icon {
+            @include font_color('treeIconFontColor');
+        }
+        .el-tree-node__expand-icon.is-leaf {
+            color: transparent !important;
+        }
+        .el-tree-node__label {
+            overflow: hidden;
+            white-space: nowrap;
+            text-overflow: ellipsis;
+        }
+        &.tree-border {
+            margin-top: 5px;
+            border-radius: 4px;
+            width: 100%;
+            @include border("treeBorder");
+        }
+    }
+    .scroll_cd {
+        margin-top: 5px;
+        border-radius: 4px;
+        width: 100%;
+        overflow: auto;
+        max-height: 500px !important;
+        @include border("treeBorder");
+        .el-tree {
+            &.tree-border {
+                border: 0px !important;
+            }
+        }
+    }
+    //tree树样式 结束
+    //input样式 开始
+    .el-input {
+        .el-input__wrapper {
+            @include font_color('inputFontColor');
+            @include box_shadow("inputBoxShadow");
+            @include background_color("inputBackColor");
+            transition: none;
+            &:hover {
+                @include box_shadow("inputBoxShadow1")
+            }
+            &:active {
+                @include box_shadow("inputBoxShadow1")
+            }
+            &.is-focus {
+                @include box_shadow("inputBoxShadow1")
+            }
+            .el-input__inner {
+                @include font_color('inputFontColor');
+            }
+        }
+        .el-input__wrapper .el-input.is-disabled {
+            @include background_color("inputDisabledBack");
+            .el-input__wrapper,
+            .el-input__inner {
+                cursor: not-allowed;
+            }
+        }
+    }
+    //input样式 结束
+    //日期~时间选择器样式 开始
+    .el-date-editor {
+        &.el-input__wrapper {
+            @include font_color('dateTimeFontColor');
+            @include box_shadow("dateTimeBoxShadow");
+            @include background_color("dateTimeBackColor");
+        }
+        transition: none;
+        &:hover {
+            @include box_shadow("dateTimeBoxShadow")
+        }
+        &:active {
+            @include box_shadow("dateTimeBoxShadow")
+        }
+        &:focus {
+            @include box_shadow("dateTimeBoxShadow")
+        }
+        &.is-active {
+            @include box_shadow("dateTimeBoxShadow1");
+        }
+        .el-range-input {
+            @include font_color('dateTimeFontColor');
+        }
+        .el-range-separator {
+            @include font_color('dateTimeFontColor');
+        }
+    }
+    //日期~时间选择器样式 结束
+    //分页选择器样式 开始
+    .el-pagination {
+        padding: 0;
+        font-size: 14px;
+        .el-pagination__total,
+        .el-pagination__sizes,
+        .el-input__inner,
+        .el-pagination__jump {
+            @include font_color('pagingFontColor');
+        }
+    }
+    .el-pagination__editor {
+        margin-right: 8px
+    }
+    //分页选择器样式 结束
+    //form表单样式 开始
+    .el-form {
+        .el-form-item__label {
+            // @include font_color('formFontColor');
+            font-weight: 700;
+        }
+    }
+    //from表单样式 结束
+    //dialog弹窗样式 开始
+    .el-dialog {
+        @include background_color("dialogBackColor");
+        .el-dialog__header {
+            height: 50px;
+            line-height: 50px;
+            padding: 0 20px;
+            @include background_color("dialogHeaderBackColor");
+            margin-right: 0;
+        }
+        .el-dialog__body {
+            @include font_color('dialogTitleFontColor');
+        }
+        .el-dialog__title {
+            font-size: 16px;
+            font-weight: 600;
+            @include font_color('dialogTitleFontColor');
+        }
+        .el-dialog__headerbtn {
+            width: 50px;
+            height: 50px;
+            top: 3px;
+        }
+        .el-dialog__headerbtn .el-dialog__close {
+            @include font_color('dialogIconFontColor');
+        }
+        .el-dialog__footer {
+            padding: 30px 20px 12px 20px;
+            text-align: center;
+            margin: 0 auto;
+            flex: 0 0 100%;
+            .el-button+.el-button {
+                margin-left: 70px;
+            }
+        }
+    }
+    //dialog弹窗样式 结束
+}
+
+//组件功能样式 结束
+#app {
+    .main-container {
+        transition: margin-left .28s;
+        margin-left: $base-sidebar-width;
+        position: relative;
+    }
+    .sidebarHide {
+        margin-left: 0 !important;
+    }
+    .sidebar-container {
+        -webkit-transition: width .28s;
+        transition: width 0.28s;
+        width: $base-sidebar-width !important;
+        height: 100%;
+        position: fixed;
+        font-size: 0px;
+        top: 50px;
+        // top: 0;
+        bottom: 0;
+        left: 0;
+        z-index: 1001;
+        overflow: hidden;
+        -webkit-box-shadow: 2px 0 6px rgba(0, 21, 41, .35);
+        box-shadow: 2px 3px 6px rgba(0, 21, 41, .35);
+        // reset element-ui css
+        .horizontal-collapse-transition {
+            transition: 0s width ease-in-out, 0s padding-left ease-in-out, 0s padding-right ease-in-out;
+        }
+        .el-scrollbar__wrap {
+            // height: calc(100vh - 50px) !important;
+        }
+        .scrollbar-wrapper {
+            overflow-x: hidden !important;
+        }
+        .el-scrollbar__bar.is-vertical {
+            right: 0px;
+        }
+        .el-scrollbar {
+            height: calc(100vh - 107px);
+            overflow-y: auto;
+        }
+        .el-scrollbar.left-menu-type {
+            height: calc(100% - 48px);
+        }
+        .is-horizontal {
+            display: none;
+        }
+        a {
+            display: inline-block;
+            width: 100%;
+            overflow: hidden;
+        }
+        .svg-icon {
+            margin-right: 16px;
+        }
+        .el-menu {
+            border: none;
+            height: 100%;
+            width: 100% !important;
+        }
+        .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;
+        }
+    }
+    .hideSidebar {
+        .sidebar-container {
+            width: 54px !important;
+        }
+        .main-container {
+            margin-left: 54px;
+        }
+        .sub-menu-title-noDropdown {
+            padding: 0 !important;
+            position: relative;
+            .el-tooltip {
+                padding: 0 !important;
+                .svg-icon {
+                    margin-left: 20px;
+                }
+            }
+        }
+        .el-menu--collapse {
+            .el-sub-menu {
+                &>.el-sub-menu__title {
+                    &>span {
+                        height: 0;
+                        width: 0;
+                        overflow: hidden;
+                        visibility: hidden;
+                        display: inline-block;
+                    }
+                    &>i {
+                        height: 0;
+                        width: 0;
+                        overflow: hidden;
+                        visibility: hidden;
+                        display: inline-block;
+                    }
+                }
+            }
+        }
+    }
+    .el-menu--collapse .el-menu .el-sub-menu {
+        min-width: $base-sidebar-width !important;
+    }
+    // mobile responsive
+    .mobile {
+        .main-container {
+            margin-left: 0px;
+        }
+        .sidebar-container {
+            transition: transform .28s;
+            width: $base-sidebar-width !important;
+        }
+        &.hideSidebar {
+            .sidebar-container {
+                pointer-events: none;
+                transition-duration: 0.3s;
+                transform: translate3d(-$base-sidebar-width, 0, 0);
+            }
+        }
+    }
+    .withoutAnimation {
+        .main-container,
+        .sidebar-container {
+            transition: none;
+        }
+    }
+}
+
+// when menu collapsed
+.el-menu--vertical {
+    &>.el-menu {
+        .svg-icon {
+            margin-right: 16px;
+        }
+    }
+    .nest-menu .el-sub-menu>.el-sub-menu__title,
+    .el-menu-item {
+        &:hover {
+            // you can use $sub-menuHover
+            background-color: rgba(0, 0, 0, 0.06) !important;
+        }
+    }
+    // the scroll bar appears when the sub-menu is too long
+    >.el-menu--popup {
+        // max-height: 100vh;
+        // overflow-y: auto;
+        // &::-webkit-scrollbar-track-piece {
+        //     background: #d3dce6;
+        // }
+        // &::-webkit-scrollbar {
+        //     width: 6px;
+        // }
+        // &::-webkit-scrollbar-thumb {
+        //     background: #99a9bf;
+        //     border-radius: 20px;
+        // }
+    }
+}
+
+

+ 141 - 0
src/assets/styles/themes.scss

@@ -0,0 +1,141 @@
+@import "./variables.module.scss";
+$themes: (
+    theme-dark: ( //深色主题theme-dark
+        fontColor: #ffffff, //默认 全局字体色
+        titleColor: #ffffff, //默认 全局标题字体颜色
+        iconColor: #ffffff, //默认 全局icon图标颜色
+
+        leftBackColor: #1f1f1f, //主题 左侧背景色
+        leftFontActiveColor: #409eff, //主题 左侧菜单选中字体颜色
+        leftMenuBackColorHover: rgb(0 0 0 / 50%), //顶部菜单栏 鼠标放入背景颜色
+
+        topBackColor: #1f1f1f, //主题 头部背景色
+        topMenuBackColorHover: rgb(0 0 0 / 50%), //主题 顶部菜单栏 鼠标放入背景颜色
+
+        containerBackColor1: #333333, //主题 主体内容背景色1
+        containerBackColor2: #1f1f1f, //主题 主体内容背景色2
+        containerBorder: 1px solid #333333, //主题 边框
+        containerBorderColor:#333333, //主题 边框颜色
+        containerHeaderBackColor:rgb(0 0 0 / 25%), //主题 头部背景颜色
+        containerModularBackColor: rgb(0 0 0 / 25%), //主题 内容div背景颜色
+
+        closeBackColor: #409eff, //可关闭小标签页 标签背景颜色
+        closeBackColorBefore: #ffffff, //可关闭小标签页 选中背景颜色
+
+        tabsContentBackColor: rgb(31 31 31 / 20%), //tabs切换 内容背景颜色
+        tabsHeaderBackColor: rgb(0 0 0 / 25%), //tabs切换 头部背景颜色
+        tabsFontColor: #ffffff, //tabs切换 字体颜色
+        tabsBorder: 1px solid #333333, //tabs切换 边框
+        tabsBackColorActive: #1f1f1f, //tabs切换 选中背景色
+
+        inputBackColor: #1f1f1f, //input框 背景颜色
+        inputFontColor: #cfcfcf, //input框 字体颜色
+        inputBoxShadow: 0 0 0 1px #333333 inset, //input框 默认阴影色
+        inputBoxShadow1: 0 0 0 1px #409eff inset, //input框 鼠标放入阴影色-选中阴影色-
+        inputDisabledBack: rgb(0 0 0 / 20%), //input框 无法选中背景色
+        inputBorderActive: 1px #409eff solid, //input框 选中边框色
+
+        tableBackColor: #1f1f1f, //table表格 背景颜色
+        tableFontColor: #ffffff, //table表格 字体颜色
+        tableHeaderBackColor: rgb(0 0 0 / 25%), //table表格 头部背景颜色
+        tableHeaderFontColor: #9a9a9a, //table表格 头部字体颜色
+        tableBorderBackColoe: #333333, //table表格 边框+边框背景色
+        tableBackColorHover: rgb(0 0 0 / 25%), //table表格 鼠标放入背景色
+        tableIconColor: #969696, //table表格 icon字体颜色
+
+        treeBorder: 1px solid #333333, //tree树 边框
+        treeBackColor: rgb(0 0 0 / 0%), //tree树 背景颜色
+        treeFontColor: #ffffff, //tree树 字体颜色
+        treeIconFontColor: #969696, //tree树 icon字体颜色
+        treeBackColorHover: rgb(0 0 0 / 25%), //tree树 鼠标放入字体颜色
+        treeBackColorActive: rgb(0 0 0 / 25%), //tree树 选中字体颜色
+
+        dateTimeFontColor: #cfcfcf, //时间~日期选择器 字体颜色
+        dateTimeBackColor: #1f1f1f, //时间~日期选择器 背景颜色
+        dateTimeBoxShadow: 0 0 0 1px #333333 inset, //时间~日期选择器 默认阴影色
+        dateTimeBoxShadow1: 0 0 0 1px #409eff inset, //时间~日期选择器 鼠标放入阴影色-选中阴影色-
+
+        formFontColor: #e0e0e0, //form表单 字体颜色
+
+        pagingFontColor: #e0e0e0, //分页选择器 字体颜色
+
+        popupBackColor: rgb(0 0 0 / 30%), //全局查岗响应 背景颜色
+        popupBoxShadow: (0px 12px 32px 4px rgba(100, 100, 100, 0.13), 0px 8px 20px rgba(100, 100, 100, 0.13)), //全局查岗响应 阴影颜色
+        popupBorder: 1px rgb(0 0 0 / 100%) solid, //全局查岗响应 边框
+
+        dialogBackColor: #1f1f1f, //dialog弹窗 背景颜色
+        dialogHeaderBackColor: rgb(0 0 0 / 25%), //dialog弹窗 头部背景颜色
+        dialogTitleFontColor: #ffffff, //dialog弹窗 title字体颜色
+        dialogIconFontColor: #ffffff, //dialog弹窗 icon颜色
+
+    ),
+    theme-light: ( //浅色主题theme-light
+        fontColor: #000000, //默认 全局字体色
+        titleColor: #000000, //默认 全局标题字体颜色
+        iconColor: #000000, //默认 全局icon图标颜色
+
+        leftBackColor: #ffffff, //主题 左侧背景色
+        leftFontActiveColor: #409eff, //主题 左侧菜单选中字体颜色
+        leftMenuBackColorHover: rgb(0 0 0 / 10%), //顶部菜单栏 鼠标放入背景颜色
+
+        topBackColor: #ffffff, //主题 头部背景色
+        topMenuBackColorHover: rgb(0 0 0 / 10%), //主题 顶部菜单栏 鼠标放入背景颜色
+
+        containerBackColor1: #f0f2f5, //主题 主体内容背景色1
+        containerBackColor2: #ffffff, //主题 主体内容背景色2
+        containerBorder: 1px solid #e0e0e0, //主题 边框
+        containerBorderColor: #e0e0e0, //主题 边框颜色
+        containerHeaderBackColor:#ffffff, //主题 头部背景颜色
+        containerModularBackColor: #ffffff, //主题 内容div背景颜色
+
+        closeBackColor: rgba(12, 131, 250, 0.15), //可关闭小标签页 背景颜色
+        closeBackColorBefore: #0c83fa, //可关闭小标签页 选中字体颜色
+
+        tabsContentBackColor: #ffffff, //tabs切换 内容背景颜色
+        tabsHeaderBackColor: #f5f7fa, //tabs切换 头部背景颜色
+        tabsFontColor: #333, //tabs切换 字体颜色
+        tabsBorder: 1px solid #e0e0e0, //tabs切换 边框
+        tabsBackColorActive: #ffffff, //tabs切换 选中背景色
+
+        inputBackColor: #ffffff, //input框 背景颜色
+        inputFontColor: #606266, //input框 字体颜色
+        inputBorder: 1px solid #e0e0e0, //input框 边框
+        inputBoxShadow: 0 0 0 1px #dcdfe6 inset, //input框 默认阴影色
+        inputBoxShadow1: 0 0 0 1px #409eff inset, //input框 鼠标放入阴影色-选中阴影色-
+        inputDisabledBack: #f5f7fa, //input框 无法选中背景色
+        inputBorderActive: 1px #409eff solid, //input框 选中边框色
+
+        // tableBackColor:#FFFFFF, //table表格 背景颜色
+        tableFontColor: #333, //table表格 字体颜色
+        // tableHeaderBackColor: #f5f7fa, //table表格 头部背景颜色
+        tableHeaderFontColor: #333, //table表格 头部字体颜色
+        tableBorderBackColoe: #ebeef5, //table表格 边框+边框背景色
+        tableBackColorHover: #fafafa, //table表格 鼠标放入背景色
+        tableIconColor: "", //table表格 icon字体颜色
+
+        treeBorder: 1px solid #e5e6e7, //tree树 边框
+        treeBackColor: #ffffff, //tree树 背景颜色
+        treeFontColor: #606266, //tree树 字体颜色
+        treeIconFontColor: #a8abb2, //tree树 icon字体颜色
+        treeBackColorHover: #f5f7fa, //tree树 鼠标放入字体颜色
+        treeBackColorActive: "", //tree树 选中字体颜色
+
+        dateTimeFontColor: #606266, //时间~日期选择器 字体颜色
+        dateTimeBackColor: rgba(255, 255, 255, 0), //时间~日期选择器 背景颜色
+        dateTimeBoxShadow: 0 0 0 1px #dcdfe6 inset, //时间~日期选择器 默认阴影色
+        dateTimeBoxShadow1: 0 0 0 1px #409eff inset, //时间~日期选择器 鼠标放入阴影色-选中阴影色-
+
+        formFontColor: #333, //form表单 字体颜色
+
+        pagingFontColor: #606266, //分页选择器 字体颜色
+
+        popupBackColor: rgb(255 255 255 / 30%), //全局查岗响应 背景颜色
+        popupBoxShadow: (0px 12px 32px 4px rgba(0, 0, 0, 0.13), 0px 8px 20px rgba(0, 0, 0, 0.13)), //全局查岗响应 阴影颜色
+        popupBorder: 1px rgb(225 225 225 / 100%) solid, //全局查岗响应 边框
+
+        dialogBackColor: rgb(255 255 255 / 100%), //dialog弹窗 背景颜色
+        dialogHeaderBackColor: #fafafa, //dialog弹窗 头部背景颜色
+        dialogTitleFontColor: #333333, //dialog弹窗 title字体颜色
+        dialogIconFontColor: #909399, //dialog弹窗 icon颜色
+    ),
+);

+ 170 - 0
src/assets/styles/transition.scss

@@ -0,0 +1,170 @@
+// global transition css
+
+/* fade */
+
+.fade-enter-active,
+.fade-leave-active {
+    transition: opacity 0.5s;
+}
+
+.fade-enter,
+.fade-leave-active {
+    opacity: 0;
+}
+
+
+/* fade-transform */
+
+// .fade-transform-leave-active,
+// .fade-transform-enter-active {
+//     transition: all 0.5s;
+// }
+// .fade-transform-enter {
+//     opacity: 0;
+//     transform: translateX(-30px);
+// }
+// .fade-transform-leave-to {
+//     opacity: 0;
+//     transform: translateX(30px);
+// }
+
+/* 入场起始样式 */
+
+.fade-transform-enter-from {
+    opacity: 0;
+    /* 透明度 */
+}
+
+
+/* 入场过渡效果 */
+
+.fade-transform-enter-active {
+    transition: 0.5s opacity ease-in;
+    animation: leftFade 0.5s;
+}
+
+
+/* 入场结束样式 */
+
+.fade-transform-enter-to {
+    opacity: 1;
+}
+
+
+/* 出场起始样式 */
+
+.fade-transform-leave-from {
+    opacity: 1;
+    /* 透明度 */
+}
+
+
+/* 出场过渡效果 */
+
+.fade-transform-leave-active {
+    transition: 0.5s opacity ease-out;
+    animation: rightFade 0.5s;
+}
+
+
+/* 出场结束样式 */
+
+.fade-transform-leave-to {
+    opacity: 0;
+}
+
+@keyframes leftFade {
+    /* 进度为 X% 时,元素的横坐标位置 */
+    0% {
+        transform: translateX(30px);
+    }
+    50% {
+        transform: translateX(-20px);
+    }
+    100% {
+        transform: translateX(0px);
+    }
+}
+
+@keyframes rightFade {
+    /* 进度为 X% 时,元素的横坐标位置 */
+    0% {
+        transform: translateX(0px);
+    }
+    100% {
+        transform: translateX(30px);
+    }
+}
+
+// .breadcrumb-enter-active,
+// .breadcrumb-leave-active {
+//     transition: all .5s;
+// }
+// .breadcrumb-enter,
+// .breadcrumb-leave-active {
+//     opacity: 0;
+//     transform: translateX(20px);
+// }
+// .breadcrumb-move {
+//     transition: all .5s;
+// }
+// .breadcrumb-leave-active {
+//     position: absolute;
+// }
+
+/* breadcrumb transition */
+
+
+/* 入场起始样式 */
+
+.breadcrumb-enter-from {
+    opacity: 0;
+    /* 透明度 */
+}
+
+
+/* 入场过渡效果 */
+
+.breadcrumb-enter-active {
+    transition: 0.5s opacity ease-in;
+    animation: leftBreadcrumb 0.5s;
+}
+
+
+/* 入场结束样式 */
+
+.breadcrumb-enter-to {
+    opacity: 1;
+}
+
+
+/* 出场起始样式 */
+
+.breadcrumb-leave-from {
+    opacity: 1;
+    /* 透明度 */
+}
+
+
+/* 出场过渡效果 */
+
+.breadcrumb-leave-active {
+    opacity: 0;
+}
+
+
+/* 出场结束样式 */
+
+.breadcrumb-leave-to {
+    opacity: 0;
+}
+
+@keyframes leftBreadcrumb {
+    /* 进度为 X% 时,元素的横坐标位置 */
+    0% {
+        transform: translateX(50px);
+    }
+    100% {
+        transform: translateX(0px);
+    }
+}

+ 79 - 0
src/assets/styles/variables.module.scss

@@ -0,0 +1,79 @@
+
+// base color
+$blue: #324157;
+$light-blue: #3A71A8;
+$red: #C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow: #FEC171;
+$panGreen: #30B08F;
+
+// 默认菜单主题风格
+$base-menu-color: #ffffff;
+$base-menu-color-active: #f4f4f5;
+$base-menu-background: #304156;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color: rgba(0, 0, 0, 0.7);
+$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;
+*/
+
+$--color-primary: #409EFF;
+$--color-success: #67C23A;
+$--color-warning: #E6A23C;
+$--color-danger: #F56C6C;
+$--color-info: #909399;
+
+$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
+
+
+//第一版深蓝样式风格
+$one-all-font-color: #ffffff;
+$one-header-background-color: linear-gradient(0deg, #052972, #000E30);
+$one-logo-title-color: linear-gradient(0deg, #49E3FF 0%, #35B3FF 100%);
+
+: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;
+  primaryColor: $--color-primary;
+  successColor: $--color-success;
+  dangerColor: $--color-danger;
+  infoColor: $--color-info;
+  warningColor: $--color-warning;
+
+
+  oneAllFontColor: $one-all-font-color;
+  oneLogoTitleColor: $one-logo-title-color;
+  oneHeaderBackgroundColor: $one-header-background-color;
+
+}

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

@@ -0,0 +1,84 @@
+<template>
+  <el-breadcrumb class="app-breadcrumb" separator="/">
+    <transition-group name="breadcrumb">
+      <el-breadcrumb-item v-for="(item, index) in levelList" :key="item.path">
+        <span v-if="item.redirect === 'noRedirect' || index == levelList.length - 1" class="no-redirect">{{ item.meta.title }}</span>
+        <a v-else @click.prevent="handleLink(item)">{{ item.meta.title }}</a>
+      </el-breadcrumb-item>
+    </transition-group>
+  </el-breadcrumb>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { ElMessageBox } from "element-plus";
+import { useRouter, useRoute } from "vue-router";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+import { useUserStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+/*----------------------------------变量声明-----------------------------------*/
+const route = useRoute();
+const router = useRouter();
+const userStore = useUserStore();
+/*----------------------------------公共变量-----------------------------------*/
+const levelList = ref([]);
+
+function getBreadcrumb() {
+  // only show routes with meta.title
+  let matched = route.matched.filter((item) => item.meta && item.meta.title);
+  const first = matched[0];
+  // 判断是否为首页
+  if (!isDashboard(first)) {
+    matched = [{ path: "/index", meta: { title: "首页" } }].concat(matched);
+  }
+
+  levelList.value = matched.filter((item) => item.meta && item.meta.title && item.meta.breadcrumb !== false);
+}
+
+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)
+
+  //编辑
+  if (path == "/index") {
+    router.push({ path: userStore.middlePageData.defaultHomePage });
+  }
+}
+
+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>
+
+.app-breadcrumb.el-breadcrumb {
+  display: inline-block;
+  font-size: 14px;
+  line-height: 50px;
+  margin-left: 8px;
+
+  .no-redirect {
+    cursor: text;
+  }
+}
+</style>

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

@@ -0,0 +1,49 @@
+<template>
+  <div>
+    <template v-for="(item, index) in options">
+      <template v-if="values.includes(item.value)">
+        <span
+          v-if="item.elTagType == 'default' || item.elTagType == ''"
+          :key="item.value"
+          :index="index"
+          :class="item.elTagClass"
+        >{{ item.label }}</span>
+        <el-tag
+          v-else
+          :disable-transitions="true"
+          :key="item.value + ''"
+          :index="index"
+          :type="item.elTagType === 'primary' ? '' : item.elTagType"
+          :class="item.elTagClass"
+        >{{ item.label }}</el-tag>
+      </template>
+    </template>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  // 数据
+  options: {
+    type: Array,
+    default: null,
+  },
+  // 当前的值
+  value: [Number, String, Array],
+})
+
+const values = computed(() => {
+  if (props.value !== null && typeof props.value !== 'undefined') {
+    return Array.isArray(props.value) ? props.value : [String(props.value)];
+  } else {
+    return [];
+  }
+})
+
+</script>
+
+<style scoped>
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+</style>

+ 193 - 0
src/components/FileUpload/index.vue

@@ -0,0 +1,193 @@
+<template>
+  <div class="upload-file">
+    <el-upload
+      multiple
+      :action="uploadFileUrl"
+      :before-upload="handleBeforeUpload"
+      :file-list="fileList"
+      :limit="limit"
+      :on-error="handleUploadError"
+      :on-exceed="handleExceed"
+      :on-success="handleUploadSuccess"
+      :show-file-list="false"
+      :headers="headers"
+      class="upload-file-uploader"
+      ref="upload"
+    >
+      <!-- 上传按钮 -->
+      <el-button type="primary">选取文件</el-button>
+    </el-upload>
+    <!-- 上传提示 -->
+    <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>
+      的文件
+    </div>
+    <!-- 文件列表 -->
+    <transition-group class="upload-file-list el-upload-list el-upload-list--text" name="el-fade-in-linear" tag="ul">
+      <li :key="file.uid" class="el-upload-list__item ele-upload-list__item-content" v-for="(file, index) in fileList">
+        <el-link :href="file.url" :underline="false" target="_blank">
+          <span class="el-icon-document"> {{ file.name || file.url }} </span>
+        </el-link>
+        <div class="ele-upload-list__item-content-action">
+          <el-link :underline="false" @click="handleDelete(index)" type="danger">删除</el-link>
+        </div>
+      </li>
+    </transition-group>
+  </div>
+</template>
+
+<script setup>
+import { getToken } from "@/utils/auth";
+
+const props = defineProps({
+  modelValue: {
+    type: [String, Object, Array],
+    default: "",
+  },
+  // 数量限制
+  limit: {
+    type: Number,
+    default: 5,
+  },
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 5,
+  },
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    //     default: () => ["doc", "xls", "ppt", "txt", "pdf"],
+    default: () => [],
+  },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true,
+  },
+});
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits();
+const uploadFileUrl = ref(import.meta.env.VITE_APP_BASE_API + "/service-file/upload"); // 上传的文件服务器地址
+const headers = ref({ Authorization: localStorage.getItem("token725") });
+const fileList = ref([]);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+
+// 删除文件
+function handleDelete(index) {
+  fileList.value.splice(index, 1);
+  if (props.limit > 1) {
+  } else {
+    emit("update", listToString(fileList.value));
+  }
+}
+
+// 上传成功回调
+function handleUploadSuccess(res, file) {
+  fileList.value.push(res.data);
+  if (props.limit > 1) {
+    emit("update:modelValue", fileList.value);
+  } else {
+    emit("update", listToString(fileList.value));
+  }
+  proxy.$modal.closeLoading();
+}
+
+// 上传前校检格式和大小
+function handleBeforeUpload(file) {
+  // 校检文件类型
+  if (props.fileType.length) {
+    let fileExtension = "";
+    if (file.name.lastIndexOf(".") > -1) {
+      fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+    }
+    const isTypeOk = props.fileType.some((type) => {
+      if (file.type.indexOf(type) > -1) return true;
+      if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+      return false;
+    });
+    if (!isTypeOk) {
+      proxy.$modal.msgError(`文件格式不正确, 请上传${props.fileType.join("/")}格式文件!`);
+      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("正在上传文件,请稍候...");
+}
+
+// 文件个数超出
+function handleExceed() {
+  proxy.$modal.msgError(`上传文件数量不能超过 ${props.limit} 个!`);
+}
+
+// 上传失败
+function handleUploadError(err) {
+  proxy.$modal.msgError("上传文件失败");
+  proxy.$modal.closeLoading();
+}
+
+// 对象转成指定字符串分隔
+function listToString(list, separator) {
+  let strs = "";
+  separator = separator || ",";
+  for (let i in list) {
+    if (undefined !== list[i].url) {
+      strs += list[i].url + separator;
+    }
+  }
+  return strs != "" ? strs.substr(0, strs.length - 1) : "";
+}
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (props.limit > 1) {
+      fileList.value = val;
+    } else {
+      fileList.value = val ? [{ name: val.split("/")[val.split("/").length - 1], url: val }] : [];
+    }
+  },
+  { deep: true, immediate: true }
+);
+
+onMounted(() => {
+  if (props.modelValue) {
+    fileList.value = props.limit > 1 ? props.modelValue : [{ name: props.modelValue.split("/")[props.modelValue.split("/").length - 1], url: props.modelValue }];
+  }
+});
+</script>
+
+<style scoped lang="scss">
+.upload-file-uploader {
+  margin-bottom: 5px;
+}
+.upload-file-list .el-upload-list__item {
+  border: 1px solid #e4e7ed;
+  line-height: 2;
+  margin-bottom: 10px;
+  position: relative;
+}
+.upload-file-list .ele-upload-list__item-content {
+  height: 32px;
+  line-height: 32px;
+  padding: 0 10px 0 10px;
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  color: inherit;
+}
+</style>

+ 181 - 0
src/components/HeaderSearch/index.vue

@@ -0,0 +1,181 @@
+<template>
+  <div :class="{ show: show }" class="header-search">
+    <svg-icon class-name="search-icon" icon-class="search" @click.stop="click" />
+    <el-select ref="headerSearchSelectRef" v-model="search" :remote-method="querySearch" filterable default-first-option remote placeholder="搜索" class="header-search-select" @change="change">
+      <el-option v-for="option in options" :key="option.item.path" :value="option.item" :label="option.item.title.join(' > ')" />
+    </el-select>
+  </div>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { useRouter, useRoute } from "vue-router";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+import { usePermissionStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+import Fuse from "fuse.js";
+import { getNormalPath } from "@/utils/ruoyi";
+import { isHttp } from "@/utils/validate";
+/*----------------------------------变量声明-----------------------------------*/
+const router = useRouter();
+const routes = computed(() => usePermissionStore().routes);
+/*----------------------------------公共变量-----------------------------------*/
+const search = ref("");
+const options = ref([]);
+const searchPool = ref([]);
+const show = ref(false);
+const fuse = ref(undefined);
+const headerSearchSelectRef = ref(null);
+
+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;
+  if (isHttp(path)) {
+    // http(s):// 路径新窗口打开
+    const pindex = path.indexOf("http");
+    window.open(path.substr(pindex, path.length), "_blank");
+  } else {
+    router.push(path);
+  }
+
+  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,
+    maxPatternLength: 32,
+    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],
+    };
+
+    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);
+      }
+    }
+
+    // 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>
+.header-search {
+  font-size: 0 !important;
+
+  .search-icon {
+    cursor: pointer;
+    font-size: 16px;
+    vertical-align: middle;
+  }
+
+  .header-search-select {
+    font-size: 18px;
+    transition: width 0.2s;
+    width: 0;
+    overflow: hidden;
+    background: transparent;
+    border-radius: 0;
+    display: inline-block;
+    vertical-align: middle;
+  }
+  :deep(.el-input__inner) {
+    border-radius: 0;
+    border: 0;
+    padding-left: 0;
+    padding-right: 0;
+    box-shadow: none !important;
+    border-bottom: 1px solid #d9d9d9;
+    vertical-align: middle;
+  }
+
+  &.show {
+    .header-search-select {
+      width: 210px;
+      margin-left: 10px;
+    }
+  }
+}
+</style>

+ 103 - 0
src/components/IconSelect/index.vue

@@ -0,0 +1,103 @@
+<!--
+ * @Descripttion: 
+ * @version: 
+ * @Author: wt
+ * @Date: 2023-02-03 14:53:32
+ * @LastEditors: wt
+ * @LastEditTime: 2023-03-23 17:48:16
+-->
+<template>
+  <div class="icon-body">
+    <el-input
+      v-model="iconName"
+      style="position: relative;"
+      clearable
+      placeholder="请输入图标名称"
+      @clear="filterIcons"
+      @input="filterIcons"
+    >
+      <template #suffix><i class="el-icon-search el-input__icon" /></template>
+    </el-input>
+    <div class="icon-list">
+      <div v-for="(item, index) in iconList" :key="index" @click="selectedIcon(item)">
+        <svg-icon :icon-class="item" style="height: 30px;width: 16px;" />
+        <span>{{ item }}</span>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+import { icons, iconsAPP}  from './requireIcons'
+const props = defineProps({
+mold: {
+    type: Number,
+    default: false,
+  },
+}); //数据双向绑定
+let iconList = ref([])
+const iconName = ref('');
+const emit = defineEmits(['selected']);
+function selectedIcon(name) {
+  emit('selected', name)
+  document.body.click()
+}
+function filterIcons() {
+        console.log(1)
+        if(props.mold == '0'){
+                console.log(2)
+                iconList.value = icons
+                console.log(iconList.value)
+                if (iconName.value) {
+                        iconList.value = icons.filter(item => item.indexOf(iconName.value) !== -1)
+                }
+        }
+        if(props.mold == '1'){
+                iconList.value = iconsAPP
+                if (iconName.value) {
+                        iconList.value = iconsAPP.filter(item => item.indexOf(iconName.value) !== -1)
+                }
+        }
+}
+
+function reset() {
+        console.log(111)
+        iconName.value = ''
+        if(props.mold == '0'){
+                console.log(icons)
+                iconList.value = icons
+        }
+        if(props.mold == '1'){
+                console.log(333)
+                iconList.value = iconsAPP
+        }
+}
+
+
+defineExpose({ reset })
+</script>
+
+<style lang='scss' scoped>
+.icon-body {
+  width: 100%;
+  padding: 10px;
+  .icon-list {
+    height: 200px;
+    overflow-y: scroll;
+    div {
+      height: 30px;
+      line-height: 30px;
+      margin-bottom: -5px;
+      cursor: pointer;
+      width: 33%;
+      float: left;
+    }
+    span {
+      display: inline-block;
+      vertical-align: -0.15em;
+      fill: currentColor;
+      overflow: hidden;
+    }
+  }
+}
+</style>

+ 25 - 0
src/components/IconSelect/requireIcons.js

@@ -0,0 +1,25 @@
+/*
+ * @Descripttion: 
+ * @version: 
+ * @Author: wt
+ * @Date: 2023-03-15 14:25:02
+ * @LastEditors: wt
+ * @LastEditTime: 2023-03-23 17:15:33
+ */
+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);
+}
+
+let iconsAPP = []
+const modules2 =
+    import.meta.glob('./../../assets/icons/APP/*.svg');
+for (const path2 in modules2) {
+    const p2 = path2.split('assets/icons/APP/')[1].split('.svg')[0];
+    iconsAPP.push(p2);
+}
+
+export { icons, iconsAPP }

+ 78 - 0
src/components/ImagePreview/index.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-image
+    :src="`${realSrc}`"
+    fit="cover"
+    :style="`width:${realWidth};height:${realHeight};`"
+    :preview-src-list="realSrcList"
+    append-to-body="true"
+  >
+    <template #error>
+      <div class="image-slot">
+        <el-icon><picture-filled /></el-icon>
+      </div>
+    </template>
+  </el-image>
+</template>
+
+<script setup>
+const props = defineProps({
+  src: {
+    type: String,
+    required: true
+  },
+  width: {
+    type: [Number, String],
+    default: ""
+  },
+  height: {
+    type: [Number, String],
+    default: ""
+  }
+});
+
+const realSrc = computed(() => {
+  let real_src = props.src.split(",")[0];
+  return real_src;
+});
+
+const realSrcList = computed(() => {
+  let real_src_list = props.src.split(",");
+  let srcList = [];
+  real_src_list.forEach(item => {
+    return srcList.push(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>
+.el-image {
+  border-radius: 5px;
+  background-color: #ebeef5;
+  box-shadow: 0 0 5px 1px #ccc;
+  :deep(.el-image__inner) {
+    transition: all 0.3s;
+    cursor: pointer;
+    &:hover {
+      transform: scale(1.2);
+    }
+  }
+  :deep(.image-slot) {
+    display: flex;
+    justify-content: center;
+    align-items: center;
+    width: 100%;
+    height: 100%;
+    color: #909399;
+    font-size: 30px;
+  }
+}
+</style>

+ 208 - 0
src/components/ImageUpload/index.vue

@@ -0,0 +1,208 @@
+<template>
+  <div class="component-upload-image">
+    <el-upload
+      name="file"
+      ref="upload"
+      :list-type="listType"
+      :file-list="fileList"
+      :action="uploadImgUrl"
+      :on-success="handleUploadSuccess"
+      :on-error="handleUploadError"
+      :before-upload="handleBeforeUpload"
+      :limit="limit"
+      :on-exceed="handleExceed"
+      :on-remove="handleRemove"
+      :show-file-list="showFileList"
+      :headers="headers"
+      :on-preview="handlePictureCardPreview"
+      :on-progress="handleProgress"
+      :class="{ hide: fileList.length >= limit }"
+      multiple
+    >
+      <slot name="button"> </slot>
+    </el-upload>
+    <!-- 上传提示 -->
+    <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>
+      的文件
+    </div>
+
+    <el-dialog v-model="dialogVisible" title="预览" width="800px" append-to-body>
+      <img :src="dialogImageUrl" style="display: block; width: 300px; margin: 0 auto" />
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  modelValue: {
+    type: [String, Object, Array],
+    default: "",
+  },
+  // 图片数量限制
+  limit: {
+    type: Number,
+    default: 5,
+  },
+  // 大小限制(MB)
+  fileSize: {
+    type: Number,
+    default: 5,
+  },
+  // 文件类型, 例如['png', 'jpg', 'jpeg']
+  fileType: {
+    type: Array,
+    default: () => ["png", "jpg", "jpeg", "svg"],
+  },
+  // 是否显示提示
+  isShowTip: {
+    type: Boolean,
+    default: true,
+  },
+  //文件列表的类型
+  listType: {
+    type: String,
+    default: "picture-card",
+  },
+  //是否显示已上传文件列表
+  showFileList: {
+    type: Boolean,
+    default: true,
+  },
+  //是否覆盖
+  isCover: {
+    type: Boolean,
+    default: false,
+  },
+});
+
+const { proxy } = getCurrentInstance();
+const emit = defineEmits();
+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 + "/file/upload"); // 上传的图片服务器地址
+const uploadImgUrl = ref(import.meta.env.VITE_APP_BASE_API + "/service-file/upload"); // 上传的图片服务器地址
+const headers = ref({ Authorization: localStorage.getItem("token725") });
+const fileList = ref([]);
+const showTip = computed(() => props.isShowTip && (props.fileType || props.fileSize));
+
+// 删除图片
+function handleRemove(file, files) {
+  if (props.limit > 1) {
+  } else {
+    emit("update", listToString(fileList.value));
+  }
+}
+
+// 上传成功回调
+function handleUploadSuccess(res, uploadFile) {
+  fileList.value.push(res.data);
+  if (props.limit > 1) {
+    emit("update:modelValue", fileList.value);
+  } else {
+    emit("update", listToString(fileList.value));
+  }
+  proxy.$modal.closeLoading();
+}
+
+// 上传前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 (props.fileSize) {
+    const isLt = file.size / 1024 / 1024 < props.fileSize;
+    if (!isLt) {
+      proxy.$modal.msgError(`上传头像图片大小不能超过 ${props.fileSize} MB!`);
+      return false;
+    }
+  }
+  proxy.$modal.loading("正在上传图片,请稍候...");
+}
+
+// 图片个数超出
+function handleExceed(files) {
+  if (props.isCover) {
+    handleBeforeUpload(files[0]);
+    fileList.value = [];
+    emit("update", listToString(fileList.value));
+    setTimeout(() => {
+      proxy.$refs["upload"].handleStart(files[0]);
+      proxy.$refs["upload"].submit();
+    }, 0);
+  } else {
+    proxy.$modal.msgError(`上传图片数量不能超过 ${props.limit} 个!`);
+  }
+}
+
+// 上传失败
+function handleUploadError() {
+  proxy.$modal.msgError("上传图片失败");
+  proxy.$modal.closeLoading();
+}
+
+// 预览
+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) : "";
+}
+
+watch(
+  () => props.modelValue,
+  (val) => {
+    if (props.limit > 1) {
+      fileList.value = val;
+    } else {
+      fileList.value = val ? [{ name: val.split("/")[val.split("/").length - 1], url: val }] : [];
+    }
+  },
+  { deep: false, immediate: false }
+);
+
+onMounted(() => {
+  if (props.modelValue) {
+    fileList.value = props.limit > 1 ? props.modelValue : [{ name: props.modelValue.split("/")[props.modelValue.split("/").length - 1], url: props.modelValue }];
+  }
+});
+</script>
+
+<style scoped lang="scss">
+// .el-upload--picture-card 控制加号部分
+:deep(.hide .el-upload--picture-card) {
+  display: none;
+}
+</style>

+ 112 - 0
src/components/Pagination/index.vue

@@ -0,0 +1,112 @@
+<template>
+  <div :class="{ hidden: hidden }" class="pagination-container">
+    <el-pagination
+      :background="background"
+      v-model:current-page="currentPage"
+      v-model:page-size="pageSize"
+      :layout="layout"
+      :page-sizes="pageSizes"
+      :pager-count="pagerCount"
+      :total="total"
+      @size-change="handleSizeChange"
+      @current-change="handleCurrentChange"
+      :disabled="total <= 0"
+    />
+  </div>
+</template>
+
+<script setup>
+import { scrollTo } from "@/utils/scroll-to";
+
+const props = defineProps({
+  total: {
+    required: true,
+    type: Number,
+  },
+  page: {
+    type: Number,
+    default: 1,
+  },
+  limit: {
+    type: Number,
+    default: 20,
+  },
+  pageSizes: {
+    type: Array,
+    default() {
+      return [10, 20, 30, 40];
+    },
+  },
+  // 移动端页码按钮的数量端默认值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 lang="scss" scoped>
+.pagination-container {
+  display: flex;
+  height: auto;
+  float: none;
+  margin-top: 29px;
+  .el-pagination {
+    display: flex;
+    position: inherit;
+    float: none;
+    margin: 0 0 0 auto;
+  }
+}
+.pagination-container.hidden {
+  display: none;
+}
+</style>

+ 3 - 0
src/components/ParentView/index.vue

@@ -0,0 +1,3 @@
+<template >
+  <router-view />
+</template>

+ 100 - 0
src/components/RightToolbar/index.vue

@@ -0,0 +1,100 @@
+<template>
+  <div class="top-right-btn">
+    <el-row>
+      <el-tooltip
+        class="item"
+        effect="dark"
+        :content="showSearch ? '隐藏搜索' : '显示搜索'"
+        placement="top"
+      >
+        <el-button circle icon="Search" @click="toggleSearch()" />
+      </el-tooltip>
+      <el-tooltip class="item" effect="dark" content="刷新" placement="top">
+        <el-button circle icon="Refresh" @click="refresh()" />
+      </el-tooltip>
+      <el-tooltip
+        class="item"
+        effect="dark"
+        content="显隐列"
+        placement="top"
+        v-if="columns"
+      >
+        <el-button circle icon="Menu" @click="showColumn()" />
+      </el-tooltip>
+    </el-row>
+    <el-dialog :title="title" v-model="open" append-to-body>
+      <el-transfer
+        :titles="['显示', '隐藏']"
+        v-model="value"
+        :data="columns"
+        @change="dataChange"
+      ></el-transfer>
+    </el-dialog>
+  </div>
+</template>
+
+<script setup>
+const props = defineProps({
+  showSearch: {
+    type: Boolean,
+    default: true,
+  },
+  columns: {
+    type: Array,
+  },
+});
+
+const emits = defineEmits(["update:showSearch", "queryTable"]);
+
+// 显隐数据
+const value = ref([]);
+// 弹出层标题
+const title = ref("显示/隐藏");
+// 是否显示弹出层
+const open = ref(false);
+
+// 搜索
+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;
+}
+
+// 显隐列初始默认隐藏列
+for (let item in props.columns) {
+  if (props.columns[item].visible === false) {
+    value.value.push(parseInt(item));
+  }
+}
+</script>
+
+<style lang='scss' scoped>
+:deep(.el-transfer__button) {
+  border-radius: 50%;
+  display: block;
+  margin-left: 0px;
+}
+:deep(.el-transfer__button:first-child) {
+  margin-bottom: 10px;
+}
+
+.my-el-transfer {
+  text-align: center;
+}
+</style>

+ 22 - 0
src/components/Screenfull/index.vue

@@ -0,0 +1,22 @@
+<template>
+  <div>
+    <svg-icon :icon-class="isFullscreen ? 'exit-fullscreen' : 'fullscreen'" @click="toggle" />
+  </div>
+</template>
+
+<script setup>
+import { useFullscreen } from '@vueuse/core'
+
+const { isFullscreen, enter, exit, toggle } = useFullscreen();
+</script>
+
+<style lang='scss' scoped>
+.screenfull-svg {
+  display: inline-block;
+  cursor: pointer;
+  fill: #5a5e66;
+  width: 20px;
+  height: 20px;
+  vertical-align: 10px;
+}
+</style>

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

@@ -0,0 +1,54 @@
+<template>
+  <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 setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { ElMessageBox } from "element-plus";
+import { useRouter, useRoute } from "vue-router";
+import { ref, onMounted, getCurrentInstance } from "vue";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+import { useAppStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+/*----------------------------------变量声明-----------------------------------*/
+const route = useRoute();
+const router = useRouter();
+const appStore = useAppStore();
+const size = computed(() => appStore.size);
+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>

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

@@ -0,0 +1,53 @@
+<template>
+  <svg :class="svgClass" aria-hidden="true">
+    <use :xlink:href="iconName" :fill="color" />
+  </svg>
+</template>
+
+<script>
+export default defineComponent({
+  props: {
+    iconClass: {
+      type: String,
+      required: true
+    },
+    className: {
+      type: String,
+      default: ''
+    },
+    color: {
+      type: String,
+      default: ''
+    },
+  },
+  setup(props) {
+    return {
+      iconName: computed(() => `#icon-${props.iconClass}`),
+      svgClass: computed(() => {
+        if (props.className) {
+          return `svg-icon ${props.className}`
+        }
+        return 'svg-icon'
+      })
+    }
+  }
+})
+</script>
+
+<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;
+  position: relative;
+  fill: currentColor;
+  vertical-align: -2px;
+}
+</style>

+ 10 - 0
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);
+        }
+    },
+};

+ 62 - 0
src/components/TopNav/Link.vue

@@ -0,0 +1,62 @@
+<template>
+  <component :is="type" v-bind="linkProps()">
+    <slot />
+  </component>
+</template>
+
+<script setup>
+import { isExternal } from '@/utils/validate'
+
+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) {
+    if(props.to.indexOf('?')>-1){
+      let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"))
+      let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"))
+      return {
+        href: `${props.to}?userNameSaaS=${userNameSaaS}&passWordSaaS=${passWordSaaS}` ,
+        target: '_blank',
+        rel: 'noopener'
+      }
+    }else{
+      return {
+        href: props.to,
+        target: '_blank',
+        rel: 'noopener'
+      }
+    }
+  }
+  if(props.to.indexOf('?')>-1){
+    let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"))
+    let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"))
+    return {
+      href: `${props.to}?userNameSaaS=${userNameSaaS}&passWordSaaS=${passWordSaaS}` ,
+      target: '_blank',
+    }
+  }else{
+    return {
+      to: props.to
+    }
+  }
+  // return {
+  //   to: props.to
+  // }
+}
+</script>

+ 105 - 0
src/components/TopNav/index.vue

@@ -0,0 +1,105 @@
+<template>
+  <el-menu
+    :default-active="activeMenu"
+    mode="horizontal"
+    @select="handleSelect1"
+    style="padding: 0 1px !important;"
+    :text-color="settingsStore.topFontColor"
+    :active-text-color="settingsStore.topFontColor"
+  >
+    <TopNavItem v-for="(route, index) in topMenus" :key="route.path + index" :item="route" :base-path="route.path"  > </TopNavItem>
+  </el-menu>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { useRouter, useRoute } from "vue-router";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+import TopNavItem from "./topNavItem";
+/*----------------------------------store引入-----------------------------------*/
+import { useSettingsStore, usePermissionStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+/*----------------------------------变量声明-----------------------------------*/
+const route = useRoute();
+const router = useRouter();
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+/*----------------------------------公共变量-----------------------------------*/
+const menuStype = ref({
+
+})
+// 顶部栏初始数
+const activeMenu = computed(() => {
+  const { meta, path } = route;
+  if (meta.activeMenu) {
+    return meta.activeMenu;
+  }
+  return path;
+});
+// 所有的路由信息
+const sidebarRouters = computed(() => permissionStore.sidebarRouters);
+const topbarRouters = computed(() => permissionStore.topbarRouters);
+// 顶部显示菜单
+const topMenus = computed(() => {
+  let topMenus = [];
+  sidebarRouters.value.map((menu) => {
+    if (menu.hidden !== true) {
+      topMenus.push(menu);
+    }
+  });
+ if(topMenus.length>5){
+        // menuStype.value.height = "90vh"
+        // menuStype.value.overflowY = "scroll"
+        // menuStype.value.overflowX = "hidden"
+ }
+  return topMenus;
+});
+
+const handleSelect1 = (key, keyPath) => {
+  if (key.indexOf("http") > -1) {
+    let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"));
+    let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"));
+    if (key.indexOf("?") > -1) {
+      let url = key.split("?")[0];
+      let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"));
+      let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"));
+      window.open(`${url}?userNameSaaS=${userNameSaaS}&passWordSaaS=${passWordSaaS}`, "_blank");
+    } else {
+      window.open(key, "_blank");
+    }
+  } else {
+    router.push({ path: key });
+  }
+};
+//菜单栏超出闪烁修复
+function menuFlicker (){
+        setTimeout(()=>{
+                var arrow = document.getElementsByClassName("el-sub-menu__hide-arrow")
+                if(arrow){
+                        arrow[0].onmouseover  = function(e) {
+                                var ul = document.getElementsByClassName("el-menu--horizontal el-menu--popup-container")[0]
+                                var ulHtml= document.getElementsByClassName("el-menu--horizontal el-menu--popup-container")
+                                if(ulHtml.length > 16){
+                                        ul.style.maxHeight = "80vh"
+                                        ul.style.overflowY = "scroll"
+                                        ul.style.overflowX = "hidden"
+                                }
+                                
+                        };
+                }
+        },1000)
+}
+window.addEventListener('resize', () => {
+        menuFlicker()
+});
+menuFlicker()
+</script>
+
+<style lang="scss">
+@import "@/assets/styles/handle.scss";
+
+:deep(.el-menu) {
+  padding: 0 1px !important;
+}
+</style>

+ 199 - 0
src/components/TopNav/topNavItem.vue

@@ -0,0 +1,199 @@
+<template>
+  <template v-if="hasOneShowingChild(item.children, item) && (!onlyOneChild.children || onlyOneChild.noShowingChildren) && !item.alwaysShow"   class="ff3">
+    <app-link v-if="onlyOneChild.meta" :to="resolvePath(onlyOneChild.path, onlyOneChild.query)"  :target ="targetType(onlyOneChild)">
+      <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.aliasTitle ? onlyOneChild.meta.aliasTitle : onlyOneChild.meta.title }}</span>
+        </template>
+      </el-menu-item>
+    </app-link>
+  </template>
+  <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)" >
+    <template v-if="item.meta" #title>
+      <svg-icon :icon-class="item.meta && item.meta.icon"/>
+      <span class="menu-title" :title="hasTitle(item.meta.aliasTitle ? item.meta.aliasTitle : item.meta.title)">{{ item.meta.aliasTitle ? item.meta.aliasTitle : item.meta.title }}</span>
+    </template>
+    <TopNavItem v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" ></TopNavItem>
+  </el-sub-menu>
+</template>
+<script setup>
+import { useSettingsStore } from "@/store/modules/index";
+import AppLink from "./Link";
+import { isExternal } from "@/utils/validate";
+import { getNormalPath } from "@/utils/ruoyi";
+const props = defineProps({
+  item: {
+    type: Object,
+    required: true,
+  },
+  isNest: {
+    type: Boolean,
+    default: false,
+  },
+  basePath: {
+    type: String,
+    default: "",
+  },
+});
+
+const settingsStore = useSettingsStore();
+const onlyOneChild = ref({});
+function hasOneShowingChild(children = [], parent) {
+  if (!children) {
+    children = [];
+  }
+  const showingChildren = children.filter((item) => {
+    if (item.hidden) {
+      return false;
+    } else {
+      // Temp set(will be used if only has one showing child)
+      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 "";
+  }
+}
+/**
+ * 页面打开方式
+ */
+ function targetType(item){
+        if(item?.meta?.isNew || item?.meta?.isFrame == 0){
+                return "_blank"
+        }
+
+}
+function kk(e){
+        console.log(e)
+        var div = document.getElementsByClassName("el-menu--horizontal")
+        console.log(div.queryS)
+        var ul = document.querySelector(".el-menu--horizontal")
+}
+
+</script>
+<style lang="scss">
+@import "@/assets/styles/handle.scss";
+
+//导航栏样式 开始
+.el-menu-item,
+.el-sub-menu__title {
+  //字体无法选中
+  -webkit-user-select: none;
+  -moz-user-select: none;
+  -ms-user-select: none;
+  user-select: none;
+  height: 48px !important;
+  line-height: 48px !important;
+}
+
+.topmenu-container.el-menu--horizontal,
+.topmenu-container.el-menu--horizontal > a .el-menu-item,
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+  border-bottom: none !important;
+}
+
+.topmenu-container.el-menu--horizontal > a .el-menu-item.is-active,
+.el-menu--horizontal > .el-sub-menu.is-active .el-sub-menu__title {
+  @include background_color("topMenuBackColorHover");
+  @include border_color("topBorderColor");
+}
+
+.topmenu-container.el-menu--horizontal > a .el-menu-item,
+.topmenu-container.el-menu--horizontal > .el-sub-menu .el-sub-menu__title {
+  float: left;
+  height: 55px !important;
+  line-height: 55px !important;
+  display: flex;
+  justify-content: center;
+  padding: 0 20px !important;
+  &:hover {
+    @include background_color("topMenuBackColorHover");
+  }
+
+  > .el-sub-menu__icon-arrow {
+    width: 20px !important;
+    position: relative;
+    top: 0;
+    right: 0;
+    margin-top: 0;
+  }
+}
+
+.svgWrap {
+  width: 20px;
+  overflow: hidden;
+  display: inline-block;
+  .imgSvg {
+    width: 12px;
+    overflow: hidden;
+    transform: translateX(20px);
+  }
+}
+
+.el-popper {
+  .el-menu-item,
+  .el-sub-menu .el-sub-menu__title {
+    color: #000 !important;
+    .svgWrap .imgSvg {
+      filter: drop-shadow(#000 -16px -19px 0px) !important;
+    }
+  }
+
+  .el-menu-item.is-active,
+  .el-sub-menu.is-active > .el-sub-menu__title {
+    color: var(--el-color-primary) !important;
+    .svgWrap .imgSvg {
+      filter: drop-shadow(var(--el-color-primary) -16px -19px 0px) !important;
+    }
+  }
+
+  .el-menu-item:not(.is-disabled):hover {
+    outline: 0;
+    background-color: var(--el-menu-hover-bg-color);
+    .svgWrap .imgSvg {
+      filter: drop-shadow(#337ecc -16px -19px 0px);
+    }
+  }
+}
+
+.el-menu--horizontal .el-menu .el-sub-menu__title:not(.is-disabled):hover {
+  background-color: var(--el-menu-hover-bg-color) !important;
+}
+.menu-title{
+  margin-left:4px;
+}
+</style>

+ 154 - 0
src/components/TreeSelect/index.vue

@@ -0,0 +1,154 @@
+<template>
+  <div class="el-tree-select">
+    <el-select
+      style="width: 100%"
+      v-model="valueId"
+      ref="treeSelect"
+      :filterable="true"
+      :clearable="true"
+      @clear="clearHandle"
+      :filter-method="selectFilterData"
+      :placeholder="placeholder"
+    >
+      <el-option :value="valueId" :label="valueTitle">
+        <el-tree
+          id="tree-option"
+          ref="selectTree"
+          :accordion="accordion"
+          :data="options"
+          :props="objMap"
+          :node-key="objMap.value"
+          :expand-on-click-node="false"
+          :default-expanded-keys="defaultExpandedKey"
+          :filter-node-method="filterNode"
+          @node-click="handleNodeClick"
+        ></el-tree>
+      </el-option>
+    </el-select>
+  </div>
+</template>
+
+<script setup>
+
+const { proxy } = getCurrentInstance();
+const props = defineProps({
+  /* 配置项 */
+  objMap: {
+    type: Object,
+    default: () => {
+      return {
+        value: 'id', // ID字段名
+        label: 'label', // 显示名称
+        children: 'children' // 子级字段名
+      }
+    }
+  },
+  /* 自动收起 */
+  accordion: {
+    type: Boolean,
+    default: () => {
+      return false
+    }
+  },
+  /**当前双向数据绑定的值 */
+  value: {
+    type: [String, Number],
+    default: ''
+  },
+  /**当前的数据 */
+  options: {
+    type: Array,
+    default: () => []
+  },
+  /**输入框内部的文字 */
+  placeholder: {
+    type: String,
+    default: ''
+  }
+})
+
+const emit = defineEmits(['update:value']);
+const valueId = computed({
+  get: () => props.value,
+  set: (val) => {
+    emit('update:value', val)
+  }
+});
+const valueTitle = ref('');
+const defaultExpandedKey = ref([]);
+
+function initHandle() {
+  nextTick(() => {
+    const selectedValue = valueId.value;
+    if(selectedValue !== null && typeof (selectedValue) !== 'undefined') {
+      const node = proxy.$refs.selectTree.getNode(selectedValue)
+      if (node) {
+        valueTitle.value = node.data[props.objMap.label]
+        proxy.$refs.selectTree.setCurrentKey(selectedValue) // 设置默认选中
+        defaultExpandedKey.value = [selectedValue] // 设置默认展开
+      }
+    } else {
+      clearHandle()
+    }
+  })
+}
+function handleNodeClick(node) {
+  valueTitle.value = node[props.objMap.label]
+  valueId.value = node[props.objMap.value];
+  defaultExpandedKey.value = [];
+  proxy.$refs.treeSelect.blur()
+  selectFilterData('')
+}
+function selectFilterData(val) {
+  proxy.$refs.selectTree.filter(val)
+}
+function filterNode(value, data) {
+  if (!value) return true
+  return data[props.objMap['label']].indexOf(value) !== -1
+}
+function clearHandle() {
+  valueTitle.value = ''
+  valueId.value = ''
+  defaultExpandedKey.value = [];
+  clearSelected()
+}
+function clearSelected() {
+  const allNode = document.querySelectorAll('#tree-option .el-tree-node')
+  allNode.forEach((element) => element.classList.remove('is-current'))
+}
+
+onMounted(() => {
+  initHandle()
+})
+
+watch(valueId, () => {
+  initHandle();
+})
+</script>
+
+<style lang='scss' scoped>
+@import "@/assets/styles/variables.module.scss";
+.el-scrollbar .el-scrollbar__view .el-select-dropdown__item {
+  padding: 0;
+  background-color: #fff;
+  height: auto;
+}
+
+.el-select-dropdown__item.selected {
+  font-weight: normal;
+}
+
+ul li .el-tree .el-tree-node__content {
+  height: auto;
+  padding: 0 20px;
+  box-sizing: border-box;
+}
+
+:deep(.el-tree-node__content:hover),
+:deep(.el-tree-node__content:active),
+:deep(.is-current > div:first-child),
+:deep(.el-tree-node__content:focus) {
+  background-color: mix(#fff, $--color-primary, 90%);
+  color: $--color-primary;
+}
+</style>

+ 31 - 0
src/components/iFrame/index.vue

@@ -0,0 +1,31 @@
+<template>
+  <div v-loading="loading" :style="'height:' + height">
+    <iframe 
+      :src="url" 
+      frameborder="no" 
+      style="width: 100%; height: 100%" 
+      scrolling="auto" />
+  </div>
+</template>
+
+<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>

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

@@ -0,0 +1,90 @@
+<template>
+    <div class="layout">
+        <div class="layout-top">
+            <div class="layout-top-left" :style="{marginLeft:props.data.logoLeft}">
+                <img src="@/assets/images/logo.png" />
+                <span>共进科技大厦</span>
+            </div>
+            <div class="layout-top-right" @click="returnPage" v-if="props.data.returnState">
+                <img src="@/assets/images/return.png" />
+                <span class="layout-top-right-text">返回</span>
+            </div>
+        </div>
+        <div class="layout-bg">
+            <img src="@/assets/images/bg.png" />
+        </div>
+    </div>
+</template>
+<script setup>
+import { ref } from "vue";
+const props = defineProps({
+    data:{
+        type: Object,
+        required: true
+    }
+});
+function returnPage() {
+    window.history.go(-1);
+}
+</script>
+
+<style lang="scss">
+.layout {
+    width:100%;
+    height:100%;
+    &-top{
+        width:100%;
+        position: absolute;
+        top:3.2%;
+        left:0;
+        z-index: 1;
+        &-left{
+            width:50%;
+            float: left;
+            img{
+                width:16%;
+                vertical-align: middle;
+            }
+            span{
+                vertical-align: middle;
+                font-size: 4.4rem;
+                font-weight: 700;
+                margin-left:0.4%;
+                color:#fff;
+                letter-spacing: 2px;
+            }
+        }
+        &-right{
+            width:20%;
+            float: right;
+            margin-right:1.6%;
+            text-align: right;
+            img{
+                width:9%;
+                vertical-align: middle;
+            }
+            span{
+                vertical-align: middle;
+                font-size: 2.5rem;
+                margin:0 0 0 2.5%;
+                letter-spacing: 2px;
+                color:#fff;
+            }
+
+        }
+    }
+    &-bg{
+        width:100%;
+        height:100%;
+        img{
+            position: fixed;
+            z-index: -1;
+            width: 100%;
+            height:100%;
+        }
+    }
+}
+
+</style>
+  
+  

+ 69 - 0
src/components/officialAccount/index.vue

@@ -0,0 +1,69 @@
+<template>
+    <div class="officialAccount">
+        <div class="officialAccount-contain">
+            <div class="officialAccount-contain-top">
+                <img src="@/assets/images/gzh.png" />
+            </div>
+            <div class="officialAccount-contain-middle" @click="returnPage">
+                <span>为访客预约成功后能成功接收通知,请首次访客预约用户关注“XXXX”公众号,进行手机号授权,已关注用户可直接点击“下一步”。</span>
+            </div>
+            <div class="officialAccount-contain-bottom" @click="next">
+                <el-button type="primary" style="width: 100%" size="large">
+                    <span style="font-size: 2rem;">下 一 步</span>
+                </el-button>
+            </div>
+        </div>
+    </div>
+</template>
+<script setup>
+import { ref } from "vue";
+const emits = defineEmits(["changeStage"]);
+function next() {
+    emits("changeStage");
+}
+</script>
+
+<style scoped lang="scss">
+.officialAccount {
+    width:100%;
+    height:100%;
+    position: fixed;
+    top:0;
+    left:0;
+    background-color: rgba(0,0,0,0.5);
+    z-index: 9999;
+    &-contain{
+        width:50%;
+        position: absolute;
+        top:20%;
+        left:25%;
+        &-top{
+            display: flex;
+            justify-content: center;
+            align-items: center;
+            margin-bottom:4%;
+            img{
+    
+            }
+        }
+        &-middle{
+            font-size: 2.4rem;
+            margin:0 0 10% 1rem;
+            letter-spacing: 2px;
+            line-height: 4rem;
+            text-align: center;
+            color:#fff;
+        }
+        &-bottom{
+            width:20%;
+            font-size: 2.4rem !important;
+            margin:0 auto;
+            display: flex;
+            justify-content: center;
+        }
+    }
+}
+
+</style>
+  
+  

+ 66 - 0
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;
+}

+ 9 - 0
src/directive/index.js

@@ -0,0 +1,9 @@
+import hasRole from './permission/hasRole'
+import hasPermi from './permission/hasPermi'
+import copyText from './common/copyText'
+
+export default function directive(app){
+  app.directive('hasRole', hasRole)
+  app.directive('hasPermi', hasPermi)
+  app.directive('copyText', copyText)
+}

+ 28 - 0
src/directive/permission/hasPermi.js

@@ -0,0 +1,28 @@
+ /**
+ * v-hasPermi 操作权限处理
+ * Copyright (c) 2019 ruoyi
+ */
+ 
+import useUserStore from '@/store/modules/user'
+
+export default {
+  mounted(el, binding, vnode) {
+    const { value } = binding
+    const all_permission = "*:*:*";
+    const permissions = useUserStore().permissions
+
+    if (value && value instanceof Array && value.length > 0) {
+      const permissionFlag = value
+
+      const hasPermissions = permissions.some(permission => {
+        return all_permission === permission || permissionFlag.includes(permission)
+      })
+
+      if (!hasPermissions) {
+        el.parentNode && el.parentNode.removeChild(el)
+      }
+    } else {
+      throw new Error(`请设置操作权限标签值`)
+    }
+  }
+}

+ 28 - 0
src/directive/permission/hasRole.js

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

+ 32 - 0
src/layout/components/AppMain.vue

@@ -0,0 +1,32 @@
+<template>
+  <section class="app-main">
+    <router-view v-slot="{ Component, route }">
+      <transition name="fade-transform" mode="out-in">
+        <keep-alive :include="cachedViews">
+          <component :is="Component" :key="route.path" />
+        </keep-alive>
+      </transition>
+    </router-view>
+  </section>
+</template>
+
+<script setup>
+import { useTagsViewStore } from "@/store/modules/index";
+
+const route = useRoute();
+const tagsViewStore = useTagsViewStore();
+
+tagsViewStore.addCachedView(route);
+const cachedViews = computed(() => {
+  return tagsViewStore.cachedViews;
+});
+</script>
+
+<style lang="scss">
+// fix css style bug in open el-dialog
+.el-popup-parent--hidden {
+  .fixed-header {
+    padding-right: 17px;
+  }
+}
+</style>

+ 30 - 0
src/layout/components/InnerLink/index.vue

@@ -0,0 +1,30 @@
+<script>
+export default {
+  setup() {
+    const route = useRoute();
+    const link = route.meta.link;
+    if (link === "") {
+      return "404";
+    }
+    let url = link;
+    const height = document.documentElement.clientHeight - 94.5 + "px";
+    const style = { height: height };
+
+    // 返回渲染函数
+    return () =>
+      h(
+        "div",
+        {
+          style: style,
+        },
+        h("iframe", {
+          src: url,
+          frameborder: "no",
+          width: "100%",
+          height: "100%",
+          scrolling: "auto",
+        })
+      );
+  },
+};
+</script>

+ 300 - 0
src/layout/components/Navbar.vue

@@ -0,0 +1,300 @@
+<template>
+  <div
+    class="navbar"
+    :style="{
+      display: 'flex',
+      backgroundColor: settingsStore.topTheme + '!important',
+    }"
+  >
+    <transition
+      class="transition-container"
+      style="float: left; display: flex; height: 100%"
+      name="sidebarLogoFade"
+      v-if="settingsStore.navigationType === 'top-menuMix-type' || settingsStore.navigationType === 'top-menu-type'"
+    >
+      <div class="sidebar-logo-link" key="expand" @click="returnMiddle">
+        <img v-if="middlePage.page.homeLogo" :src="middlePage.page.homeLogo" class="sidebar-logo" style="margin: auto 15px; height: 30px" @click="home" />
+        <span
+          @click="home"
+          class="sidebar-title"
+          :style="{
+            margin: 'auto',
+            marginRight: '15px',
+            fontSize: '22px',
+            fontFamily: 'Microsoft YaHei',
+            fontWeight: 400,
+            'white-space': 'nowrap',
+            color: settingsStore.topFontColor + '!important',
+          }"
+        >
+          {{ middlePage.page.middleTitle }}
+        </span>
+      </div>
+    </transition>
+
+
+    <top-nav class="topmenu-container" v-if="settingsStore.navigationType === 'top-menu-type'" style="min-width: 0; flex: 1; justify-content: center" id="topmenu-container" />
+
+    <div class="right-menu" :style="{ marginLeft: settingsStore.navigationType !== 'top-menu-type' ? 'auto' : '' }">
+      <template v-if="appStore.device !== 'mobile'">
+        <div
+          v-if="middlePage.page.largeScreenUrl"
+          class="right-menu-item"
+          :style="{
+            color: settingsStore.topFontColor + '!important',
+          }"
+          @click="dpUrl"
+        >
+          <svg-icon icon-class="大屏" />
+        </div>
+
+        <header-search
+          class="right-menu-item"
+          :style="{
+            color: settingsStore.topFontColor + '!important',
+          }"
+        />
+
+        <msg-view v-if="middlePage.page.messageStatus === 1" class="right-menu-item"></msg-view>
+      </template>
+      <div class="avatar-container">
+        <el-dropdown
+          @command="handleCommand"
+          class="right-menu-item hover-effect"
+          trigger="click"
+          :style="{
+            color: settingsStore.topFontColor + '!important',
+          }"
+        >
+          <div class="avatar-wrapper">
+            <span>{{ userStore.nickName }}</span>
+
+            <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">
+                <span>布局设置</span>
+              </el-dropdown-item>
+              <router-link to="/system/editMenu" v-hasPermi="['menu:edit']">
+                <el-dropdown-item>菜单编辑</el-dropdown-item>
+              </router-link>
+              <el-dropdown-item divided command="logout">
+                <span>退出登录</span>
+              </el-dropdown-item>
+            </el-dropdown-menu>
+          </template>
+        </el-dropdown>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { ElMessage, ElMessageBox } from "element-plus";
+import { getCurrentInstance, defineComponent, ref, onMounted, watch } from "vue";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+import TopNav from "@/components/TopNav";
+import Screenfull from "@/components/Screenfull";
+import SizeSelect from "@/components/SizeSelect";
+import HeaderSearch from "@/components/HeaderSearch";
+/*----------------------------------store引入-----------------------------------*/
+import { useUserStore, useSettingsStore, useAppStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+/*----------------------------------公共变量-----------------------------------*/
+const router = useRouter();
+const appStore = useAppStore();
+const userStore = useUserStore();
+const settingsStore = useSettingsStore();
+
+const props = defineProps({});
+const emits = defineEmits(["setLayout"]);
+const { proxy } = getCurrentInstance();
+/*----------------------------------变量声明-----------------------------------*/
+const dropdown = ref(null);
+const middlePage = computed(() => userStore.middlePageData); //全局公共变量集
+const navigationType = ref(settingsStore.navigationType); //导航栏模式
+
+function toggleSideBar() {
+  appStore.toggleSideBar();
+}
+
+/**
+ * @下拉菜单触发事件
+ */
+function handleCommand(command) {
+  switch (command) {
+    case "setLayout":
+      emits("setLayout");
+      break;
+    case "logout":
+      logout();
+      break;
+    case "emptyNotice":
+      break;
+    case "seeMore":
+      router.push({ path: "/message/center" });
+      dropdown.value.handleClose();
+      break;
+    default:
+      break;
+  }
+}
+
+/**
+ * @logo点击事件
+ */
+function returnMiddle() {
+  if (localStorage.getItem("middlePageStatus")) {
+    router.push({ path: "/middle" });
+  }
+}
+
+/**
+ * @logo标题点击事件
+ */
+function home() {
+  let data = middlePage.defaultHomePage;
+  if (data.indexOf("http") > 0) {
+    window.open(data, "_blank");
+  } else {
+    router.push({ path: data });
+  }
+}
+
+/**
+ * @大屏跳转按钮点击事件
+ */
+function dpUrl() {
+  window.open(middlePage.page.largeScreenUrl + "?token=" + localStorage.getItem("token725"), "_blank");
+}
+
+/**
+ * @退出按钮点击事件
+ */
+function logout() {
+  ElMessageBox.confirm("确定注销并退出系统吗?", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(() => {
+      localStorage.clear();
+      userStore.logOut().then(() => {
+        location.href = "/login";
+      });
+    })
+    .catch(() => {});
+}
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/styles/variables.module.scss";
+@import "@/assets/styles/handle.scss";
+
+.navbar {
+  height: 56px;
+  overflow: hidden;
+  position: relative;
+  background: #fff;
+  box-shadow: 0 1px 4px rgba(0, 21, 41, 0.08);
+
+  .hamburger-container {
+    line-height: 46px;
+    height: 100%;
+
+    // position: absolute;
+    // right: 0;
+    cursor: pointer;
+    transition: background 0.3s;
+    -webkit-tap-highlight-color: transparent;
+
+    &:hover {
+      background: rgba(0, 0, 0, 0.025);
+    }
+  }
+
+  .topmenu-container {
+    // position: absolute;
+    // left: 50px;
+    // margin: auto;
+    background: transparent;
+  }
+
+  .errLog-container {
+    display: inline-block;
+    vertical-align: top;
+  }
+
+  .right-menu {
+    float: right;
+    height: 100%;
+    line-height: 56px;
+    display: flex;
+
+    &:focus {
+      outline: none;
+    }
+
+    .right-menu-item {
+      display: inline-block;
+      padding: 0 8px;
+      height: 100%;
+      font-size: 16px;
+      @include font_color("fontColor");
+      @include fill_color("fontColor");
+      vertical-align: text-bottom;
+      cursor: pointer;
+
+      &.hover-effect {
+        cursor: pointer;
+        transition: background 0.3s;
+
+        &:hover {
+          background: rgba(0, 0, 0, 0.025);
+        }
+      }
+    }
+
+    .avatar-container {
+      margin-right: 28px;
+
+      .avatar-wrapper {
+        position: relative;
+        line-height: 54px;
+        // margin-top:12px;
+        span {
+          display: inline-block;
+          position: relative;
+          // top:-9px;
+          font-size: 14px;
+        }
+
+        .user-avatar {
+          cursor: pointer;
+          width: 28px;
+          height: 28px;
+          border-radius: 10px;
+        }
+
+        i {
+          cursor: pointer;
+          position: absolute;
+          right: -20px;
+          top: 21px;
+          font-size: 12px;
+        }
+      }
+    }
+  }
+}
+
+.sidebar-title {
+  padding: 0 10px;
+}
+</style>

+ 323 - 0
src/layout/components/Settings/index.vue

@@ -0,0 +1,323 @@
+<template>
+  <el-drawer v-model="showSettings" :withHeader="false" direction="rtl" size="300px">
+    <!-- 导航栏模式 -->
+    <div>
+      <div class="setting-drawer-title">
+        <el-divider>
+          <h3 class="drawer-title">导航栏模式</h3>
+        </el-divider>
+      </div>
+      <div class="setting-drawer-block-checbox">
+        <el-tooltip v-for="(nav, index) in navigationTypeArray" :key="index" class="item" effect="dark" :content="nav.content" placement="bottom">
+          <div class="setting-drawer-block-checbox-item" @click="handleMenuType(nav.boolean)">
+            <svg-icon :icon-class="nav.svg" class="svg" />
+            <div v-if="navigationType === nav.boolean" 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" aria-hidden="true" focusable="false" class style="fill: #000000 !important">
+                  <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>
+        </el-tooltip>
+      </div>
+    </div>
+    <!-- 主题风格设置 -->
+    <div>
+      <div class="setting-drawer-title">
+        <el-divider>
+          <h3 class="drawer-title">主题风格设置</h3>
+        </el-divider>
+      </div>
+      <div class="setting-drawer-block-checbox">
+        <el-tooltip v-for="arr in themeTypeArray" :key="arr" class="item" effect="dark" :content="arr.content" placement="bottom">
+          <div class="setting-drawer-block-checbox-item" @click="handleTheme(arr.boolean)">
+            <svg-icon :icon-class="arr.svg" class="svg" />
+            <div v-if="sideTheme === arr.boolean" 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" aria-hidden="true" focusable="false" class style="fill: #000000 !important">
+                  <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>
+        </el-tooltip>
+      </div>
+    </div>
+    <!-- 顶栏主题设置 -->
+    <div>
+      <div class="setting-drawer-title">
+        <el-divider>
+          <span class="drawer-title">顶栏主题</span>
+        </el-divider>
+
+        <div class="drawer-item">
+          <span>顶栏背景</span>
+          <div style="margin: auto"></div>
+          <span class="comp-style">
+            <el-color-picker v-model="settingsStore.topTheme" show-alpha :predefine="predefineColors" />
+          </span>
+        </div>
+        <div class="drawer-item">
+          <span>顶栏默认字体颜色</span>
+          <div style="margin: auto"></div>
+          <span class="comp-style">
+            <el-color-picker v-model="settingsStore.topFontColor" show-alpha :predefine="predefineColors" />
+          </span>
+        </div>
+      </div>
+    </div>
+
+    <!-- 系统布局配置 -->
+    <div>
+      <div class="setting-drawer-title">
+        <el-divider>
+          <span class="drawer-title">系统布局配置</span>
+        </el-divider>
+
+        <div class="drawer-item">
+          <span>开启 Tags-Views</span>
+          <div style="margin: auto"></div>
+          <span class="comp-style">
+            <el-switch v-model="tagsView" class="drawer-switch" />
+          </span>
+        </div>
+        <div class="drawer-item">
+          <span>动态标题</span>
+          <div style="margin: auto"></div>
+          <span class="comp-style">
+            <el-switch v-model="dynamicTitle" class="drawer-switch" />
+          </span>
+        </div>
+      </div>
+    </div>
+    <el-divider />
+    <!-- 保存设置 -->
+    <div>
+      <el-button type="primary" plain icon="DocumentAdd" @click="saveSetting">保存配置</el-button>
+      <el-button plain icon="Refresh" @click="resetSetting">重置配置</el-button>
+    </div>
+  </el-drawer>
+</template>
+
+<script setup>
+import axios from "axios";
+import variables from "@/assets/styles/variables.module.scss";
+import { ElLoading, ElMessage } from "element-plus";
+import "element-plus/theme-chalk/index.css";
+import { useDynamicTitle } from "@/utils/dynamicTitle";
+import { usePermissionStore, useUserStore, useSettingsStore, useAppStore } from "@/store/modules/index";
+
+const userStore = useUserStore();
+const { proxy } = getCurrentInstance();
+const appStore = useAppStore();
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+const showSettings = ref(false);
+const storeSettings = computed(() => settingsStore);
+const topNav = ref(false);
+/**
+ * @是否需要tagview
+ */
+const tagsView = computed({
+  get: () => storeSettings.value.tagsView,
+  set: (val) => {
+    settingsStore.changeSetting({ key: "tagsView", value: val });
+  },
+});
+
+/**
+ * @是否需要侧边栏的动态网页的title
+ */
+const dynamicTitle = computed({
+  get: () => storeSettings.value.dynamicTitle,
+  set: (val) => {
+    settingsStore.changeSetting({ key: "dynamicTitle", value: val });
+    // 动态设置网页标题
+    useDynamicTitle();
+  },
+});
+
+const predefineColors = ref(["#FFFFFF", "#151515", "#009688", "#5172dc", "#018ffb", "#409eff", "#e74c3c", "#24292e", "#394664", "#001529", "#383f45"]);
+
+/**
+ * @主题风格设置
+ */
+const themeTypeArray = ref([
+  {
+    content: "深色主题",
+    boolean: "theme-dark",
+    svg: "dark",
+    images: "/assets/icons/svg/dark.svg",
+  },
+  {
+    content: "浅色主题",
+    boolean: "theme-light",
+    svg: "light",
+    images: "/assets/icons/svg/light.svg",
+  },
+]);
+const sideTheme = ref(settingsStore.sideTheme);
+function handleTheme(val) {
+  window.document.documentElement.setAttribute("data-theme", val);
+  settingsStore.changeSetting({ key: "sideTheme", value: val });
+  sideTheme.value = val;
+}
+/**
+ * @导航栏模式
+ */
+const navigationTypeArray = ref([
+  {
+    content: "左侧菜单模式",
+    boolean: "left-menu-type",
+    svg: "dark",
+    images: "/assets/icons/svg/dark.svg",
+  },
+  {
+    content: "顶部菜单混合模式",
+    boolean: "top-menuMix-type",
+    svg: "dark",
+    images: "/assets/icons/svg/dark.svg",
+  },
+  {
+    content: "顶部菜单模式",
+    boolean: "top-menu-type",
+    svg: "dark",
+    images: "/assets/icons/svg/dark.svg",
+  },
+]);
+const navigationType = ref(settingsStore.navigationType);
+function handleMenuType(val) {
+  settingsStore.changeSetting({ key: "navigationType", value: val });
+  navigationType.value = val;
+}
+
+function saveSetting() {
+  proxy.$modal.loading("正在保存到本地,请稍候...");
+  let layoutSetting = {
+    tagsView: storeSettings.value.tagsView,
+    dynamicTitle: storeSettings.value.dynamicTitle,
+    sideTheme: storeSettings.value.sideTheme,
+    topTheme: storeSettings.value.topTheme,
+    topFontColor: storeSettings.value.topFontColor,
+    navigationType: storeSettings.value.navigationType,
+    topThemeArray: settingsStore.topThemeArray,
+  };
+  localStorage.setItem("layout-setting", JSON.stringify(layoutSetting));
+
+  setTimeout(proxy.$modal.closeLoading(), 1000);
+}
+
+function resetSetting() {
+  proxy.$modal.loading("正在清除设置缓存并刷新,请稍候...");
+  localStorage.removeItem("layout-setting");
+  setTimeout("window.location.reload()", 1000);
+}
+
+function openSetting() {
+  showSettings.value = true;
+}
+
+defineExpose({
+  openSetting,
+});
+</script>
+
+<style lang="scss" scoped>
+.setting-drawer-title {
+  margin-bottom: 12px;
+  color: rgba(0, 0, 0, 0.85);
+  line-height: 22px;
+  font-weight: bold;
+  .drawer-title {
+    white-space: nowrap;
+    font-size: 16px;
+  }
+}
+.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;
+
+    .svg {
+      width: 48px;
+      height: 48px;
+    }
+
+    .custom-img {
+      width: 48px;
+      height: 38px;
+      border-radius: 5px;
+      box-shadow: 1px 1px 2px #898484;
+    }
+
+    .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;
+    }
+  }
+}
+
+.drawer-item {
+  color: rgba(0, 0, 0, 0.65);
+  // padding: 12px 0;
+  margin: 12px 0;
+  font-size: 14px;
+  font-weight: 500;
+  display: flex;
+  height: 32px;
+  line-height: 32px;
+
+  // .comp-style {
+  //   float: right;
+  //   margin: -3px 8px 0px 0px;
+  // }
+}
+</style>
+
+<style lang="scss" scoped>
+//顶栏主题模块样式
+.vben-setting-theme-picker {
+  display: flex;
+  flex-wrap: wrap;
+  margin: 16px 0;
+  justify-content: space-around;
+
+  .vben-setting-theme-picker__item {
+    width: 20px;
+    height: 20px;
+    cursor: pointer;
+    border: 1px solid #ddd;
+    border-radius: 2px;
+    display: flex;
+
+    span {
+      text-align: center;
+      margin: auto;
+    }
+  }
+  .vben-setting-theme-picker__item--active {
+    border: 1px solid #6a9eff;
+  }
+}
+</style>

+ 62 - 0
src/layout/components/Sidebar/Link.vue

@@ -0,0 +1,62 @@
+<template>
+  <component :is="type" v-bind="linkProps()">
+    <slot />
+  </component>
+</template>
+
+<script setup>
+import { isExternal } from '@/utils/validate'
+
+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) {
+    if(props.to.indexOf('?')>-1){
+      let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"))
+      let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"))
+      return {
+        href: `${props.to}?userNameSaaS=${userNameSaaS}&passWordSaaS=${passWordSaaS}` ,
+        target: '_blank',
+        rel: 'noopener'
+      }
+    }else{
+      return {
+        href: props.to,
+        target: '_blank',
+        rel: 'noopener'
+      }
+    }
+  }
+  if(props.to.indexOf('?')>-1){
+    let userNameSaaS = encodeURIComponent(localStorage.getItem("userName"))
+    let passWordSaaS = encodeURIComponent(localStorage.getItem("passWord"))
+    return {
+      href: `${props.to}?userNameSaaS=${userNameSaaS}&passWordSaaS=${passWordSaaS}` ,
+      target: '_blank',
+    }
+  }else{
+    return {
+      to: props.to
+    }
+  }
+  // return {
+  //   to: props.to
+  // }
+}
+</script>

+ 123 - 0
src/layout/components/Sidebar/Logo.vue

@@ -0,0 +1,123 @@
+<template>
+  <div class="sidebar-logo-container" :class="{ collapse: props.collapse }">
+    <transition name="sidebarLogoFade" v-if="settingsStore.navigationType === 'top-menuMix-type'">
+      <div key="expand" class="sidebar-icon-link">
+        <div class="icon-title" v-if="!props.collapse">导航</div>
+      </div>
+    </transition>
+
+    <transition name="sidebarLogoFade" v-if="settingsStore.navigationType === 'left-menu-type'">
+      <div key="expand" class="sidebar-logo-link" @click="returnMiddleOrHome">
+        <img v-if="middlePage.page.homeLogo" :src="middlePage.page.homeLogo" class="sidebar-logo" />
+        <h1 v-if="!props.collapse" class="sidebar-title">
+          {{ middlePage.page.middleTitle }}
+        </h1>
+      </div>
+    </transition>
+  </div>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { ElMessage, ElMessageBox } from "element-plus";
+import { getCurrentInstance, defineComponent, ref, onMounted, watch } from "vue";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------组件引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+import { usePermissionStore, useUserStore, useSettingsStore, useAppStore } from "@/store/modules/index";
+/*----------------------------------公共方法引入-----------------------------------*/
+/*----------------------------------公共变量-----------------------------------*/
+const router = useRouter();
+const appStore = useAppStore();
+const userStore = useUserStore();
+const settingsStore = useSettingsStore();
+
+const props = defineProps({
+  collapse: {
+    type: Boolean,
+    required: true,
+  },
+});
+const emits = defineEmits(["setLayout"]);
+const { proxy } = getCurrentInstance();
+/*----------------------------------变量声明-----------------------------------*/
+const middlePage = computed(() => userStore.middlePageData); //全局公共变量集
+
+function toggleSideBar() {
+  appStore.toggleSideBar();
+}
+
+function home() {
+  if (middlePage.defaultHomePage.indexOf("http") > 0) {
+    window.open(middlePage.defaultHomePage, "_blank");
+  } else {
+    router.push({ path: middlePage.defaultHomePage });
+  }
+}
+
+function returnMiddleOrHome() {
+  if (localStorage.getItem("middlePageStatus")) {
+    router.push({ path: "/middle" });
+  }
+}
+</script>
+<style lang="scss" scoped>
+.hamburger-container {
+  line-height: 50px;
+  height: 100%;
+  float: right;
+  cursor: pointer;
+  transition: background 0.3s;
+  -webkit-tap-highlight-color: transparent;
+
+  &:hover {
+    background: rgba(0, 0, 0, 0.025);
+  }
+}
+
+.sidebarLogoFade-enter-active {
+  transition: opacity 1.5s;
+}
+
+.sidebarLogoFade-enter,
+.sidebarLogoFade-leave-to {
+  opacity: 0;
+}
+
+.sidebar-logo-container {
+  position: relative;
+  width: 100%;
+  height: 48px;
+  line-height: 48px;
+  text-align: center;
+  overflow: hidden;
+
+  & .sidebar-logo-link {
+    height: 100%;
+    width: 100%;
+
+    & .sidebar-logo {
+      width: 45px;
+      vertical-align: middle;
+      margin-right: 12px;
+    }
+
+    & .sidebar-title {
+      display: inline-block;
+      margin: 0;
+      color: #fff;
+      font-weight: 600;
+      line-height: 50px;
+      font-size: 14px;
+      font-family: Avenir, Helvetica Neue, Arial, Helvetica, sans-serif;
+      vertical-align: middle;
+    }
+  }
+
+  &.collapse {
+    .sidebar-logo {
+      margin-right: 0px;
+    }
+  }
+}
+</style>

+ 118 - 0
src/layout/components/Sidebar/SidebarItem.vue

@@ -0,0 +1,118 @@
+<template>
+  <div v-if="!item.hidden">
+    <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,onlyOneChild)"  :target ="targetType(onlyOneChild)">
+        <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.aliasTitle ? onlyOneChild.meta.aliasTitle : onlyOneChild.meta.title }}</span>
+          </template>
+        </el-menu-item>
+      </app-link>
+    </template>
+
+    <el-sub-menu v-else ref="subMenu" :index="resolvePath(item.path)">
+      <template v-if="item.meta" #title>
+        <svg-icon :icon-class="item.meta && item.meta.icon" class="Vimium-button" />
+        <span class="menu-title" :title="hasTitle(item.meta.aliasTitle ? item.meta.aliasTitle : item.meta.title)">{{ item.meta.aliasTitle ? item.meta.aliasTitle : item.meta.title }}</span>
+      </template>
+      <sidebar-item v-for="child in item.children" :key="child.path" :is-nest="true" :item="child" :base-path="resolvePath(child.path)" class="nest-menu" />
+    </el-sub-menu>
+  </div>
+</template>
+<script setup>
+import AppLink from "./Link";
+import { isExternal } from "@/utils/validate";
+import { getNormalPath } from "@/utils/ruoyi";
+
+const props = defineProps({
+  // route object
+  item: {
+    type: Object,
+    required: true,
+  },
+  isNest: {
+    type: Boolean,
+    default: false,
+  },
+  basePath: {
+    type: String,
+    default: "",
+  },
+});
+
+const onlyOneChild = ref({});
+
+function hasOneShowingChild(children = [], parent) {
+  if (!children) {
+    children = [];
+  }
+  const showingChildren = children.filter((item) => {
+    if (item.hidden) {
+      return false;
+    } else {
+      // Temp set(will be used if only has one showing child)
+      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 targetType(item){
+        if(item?.meta?.isNew || item?.meta?.isFrame == 0){
+                return "_blank"
+        }
+
+}
+function hasTitle(title) {
+  if (title.length > 5) {
+    return title;
+  } else {
+    return "";
+  }
+}
+</script>
+<style lang="scss">
+.imgSvgWrap {
+  width: 30px;
+  overflow: hidden;
+}
+.hideSidebar {
+  .el-sub-menu__title {
+    padding: 0 !important;
+    svg {
+      margin-left: 20px;
+    }
+  }
+}
+
+</style>

+ 58 - 0
src/layout/components/Sidebar/index.vue

@@ -0,0 +1,58 @@
+<template>
+  <div
+    :style="{
+      top:
+        settingsStore.navigationType === 'left-menu-type'
+          ? '0px'
+          : settingsStore.navigationType === 'top-menuMix-type'
+          ? '56px'
+          : settingsStore.navigationType === 'top-menu-type'
+          ? '56px'
+          : '',
+    }"
+  >
+    <logo
+      v-if="settingsStore.navigationType !== 'top-menu-type'"
+      :collapse="isCollapse"
+    />
+    <el-scrollbar class="sidebar-menu-container" wrap-class="scrollbar-wrapper" :class="[settingsStore.navigationType === 'left-menu-type'
+          ? 'left-menu-type' : 'other',]">
+      <el-menu
+        :default-active="activeMenu"
+        :collapse="isCollapse"
+        :unique-opened="true"
+        :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>
+</template>
+
+<script setup>
+import Logo from "./Logo";
+import SidebarItem from "./SidebarItem";
+import { usePermissionStore, useSettingsStore, useAppStore } from "@/store/modules/index";
+
+const route = useRoute();
+const appStore = useAppStore();
+const settingsStore = useSettingsStore();
+const permissionStore = usePermissionStore();
+const sidebarRouters = computed(() => permissionStore.sidebarRouters);
+const isCollapse = computed(() => !appStore.sidebar.opened);
+
+const activeMenu = computed(() => {
+  const { meta, path } = route;
+  // if set path, the sidebar will highlight the path you set
+  if (meta.activeMenu) {
+    return meta.activeMenu;
+  }
+  return path;
+});
+</script>

+ 93 - 0
src/layout/components/TagsView/ScrollPane.vue

@@ -0,0 +1,93 @@
+<template>
+  <el-scrollbar ref="scrollContainer" :vertical="false" class="scroll-container" @wheel.prevent="handleScroll">
+    <slot />
+  </el-scrollbar>
+</template>
+
+<script setup>
+import { useTagsViewStore } from "@/store/modules/index";
+
+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;
+
+  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>
+.scroll-container {
+  white-space: nowrap;
+  position: relative;
+  overflow: hidden;
+  width: 100%;
+}
+</style>

+ 361 - 0
src/layout/components/TagsView/index.vue

@@ -0,0 +1,361 @@
+<template>
+  <div id="tags-view-container" class="tags-view-container">
+    <scroll-pane ref="scrollPaneRef" class="tags-view-wrapper" @scroll="handleScroll">
+      <router-link
+        v-for="tag in visitedViews"
+        :key="tag.path"
+        :data-path="tag.path"
+        :class="isActive(tag) ? 'active' : ''"
+        :to="{ path: tag.path, query: tag.query, fullPath: tag.fullPath }"
+        class="tags-view-item"
+        @click.middle="!isAffix(tag) ? closeSelectedTag(tag) : ''"
+        @contextmenu.prevent="openMenu(tag, $event)"
+      >
+        <div v-if="!isAffix(tag)" style="display: flex">
+          <div style="margin: auto 3px auto 0">{{ tag.title }}</div>
+          <close class="el-icon-close" @click.prevent.stop="closeSelectedTag(tag)" style="display: flex; width: 1em; height: 1em; vertical-align: middle; margin: auto 0" />
+        </div>
+      </router-link>
+    </scroll-pane>
+    <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 setup>
+import ScrollPane from "./ScrollPane";
+import { ElMessageBox } from "element-plus";
+import { getNormalPath } from "@/utils/ruoyi";
+import { useTagsViewStore, usePermissionStore, useUserStore, useSettingsStore, useAppStore } from "@/store/modules/index";
+const TagsView = useTagsViewStore();
+const userStore = useUserStore();
+const visible = ref(false);
+const top = ref(0);
+const left = ref(0);
+const selectedTag = ref({});
+const affixTags = ref([]);
+const scrollPaneRef = ref(null);
+const status = ref(false);
+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 isAffix(tag) {
+  return tag.meta && tag.meta.affix;
+}
+function isFirstView() {
+  try {
+    return selectedTag.value.fullPath === visitedViews.value[1].fullPath || selectedTag.value.fullPath === "/index";
+  } 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 },
+      });
+    }
+    if (route.children) {
+      const tempTags = filterAffixTags(route.children, route.path);
+      if (tempTags.length >= 1) {
+        tags = [...tags, ...tempTags];
+      }
+    }
+  });
+  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);
+  }
+  return false;
+}
+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);
+        }
+      }
+    }
+  });
+}
+function refreshSelectedTag(view) {
+  proxy.$tab.refreshPage(view);
+}
+function closeSelectedTag(view) {
+  if (visitedViews._value.length == 1) {
+    ElMessageBox.confirm("关闭当前页面将注销并退出系统", "提示", {
+      confirmButtonText: "确定",
+      cancelButtonText: "取消",
+      type: "warning",
+    })
+      .then(() => {
+        localStorage.clear();
+        userStore.logOut().then(() => {
+          location.href = "/login";
+        });
+        proxy.$tab.closeAllPage().then(({ visitedViews }) => {
+          if (affixTags.value.some((tag) => tag.path === route.path)) {
+            return;
+          }
+          toLastView(visitedViews, view);
+        });
+      })
+      .catch(() => {});
+  } else {
+    proxy.$tab.closePage(view).then(({ visitedViews }) => {
+      if (isActive(view)) {
+        toLastView(visitedViews, view);
+      } else if (status) {
+        toLastView(visitedViews, view);
+        status.value = false;
+      } else {
+      }
+    });
+  }
+}
+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) {
+  ElMessageBox.confirm("页面全部关闭将注销并退出系统", "提示", {
+    confirmButtonText: "确定",
+    cancelButtonText: "取消",
+    type: "warning",
+  })
+    .then(() => {
+      localStorage.clear();
+      userStore.logOut().then(() => {
+        location.href = "/login";
+      });
+      proxy.$tab.closeAllPage().then(({ visitedViews }) => {
+        if (affixTags.value.some((tag) => tag.path === route.path)) {
+          return;
+        }
+        toLastView(visitedViews, view);
+      });
+    })
+    .catch(() => {});
+}
+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();
+}
+watch(
+  () => TagsView.$state.timeStamp,
+  (val) => {
+    if (TagsView.$state.timeStamp != undefined) {
+      status.value = true;
+      closeSelectedTag(TagsView.visitedViews[TagsView.visitedViews.length - 1]);
+    }
+  },
+  { deep: true, immediate: true }
+);
+</script>
+
+<style lang="scss" scoped>
+@import "@/assets/styles/handle.scss";
+.tags-view-container {
+  height: 40px;
+  line-height: 40px;
+  width: 100%;
+  box-shadow: 0 1px 3px 0 rgba(0, 0, 0, 0.12), 0 0 3px 0 rgba(0, 0, 0, 0.04);
+  .tags-view-wrapper {
+    .tags-view-item {
+      display: inline-block;
+      position: relative;
+      cursor: pointer;
+      height: 28px;
+      line-height: 28px;
+      color: #495060;
+      border: 1px #d9d9d9 solid;
+      // background: rgba(0,0,0,0.06);
+      background: #fff;
+      padding: 0 6px 0 6px;
+      // padding: 0 8px;
+      font-size: 14px;
+      margin-left: 8px;
+      border-radius: 2px;
+      &: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;
+          // display: inline-block;
+          // width: 8px;
+          // height: 8px;
+          // border-radius: 50%;
+          // position: relative;
+          // margin-right: 2px;
+        }
+      }
+    }
+  }
+  .contextmenu {
+    margin: 0;
+    background: #fff;
+    z-index: 3000;
+    position: absolute;
+    list-style-type: none;
+    padding: 5px 0;
+    border-radius: 4px;
+    font-size: 12px;
+    font-weight: 400;
+    color: #333;
+    box-shadow: 2px 2px 3px 0 rgba(0, 0, 0, 0.3);
+    li {
+      margin: 0;
+      padding: 7px 16px;
+      cursor: pointer;
+      &:hover {
+        background: #eee;
+      }
+    }
+  }
+}
+</style>
+
+<style lang="scss">
+//reset element css of el-icon-close
+.tags-view-wrapper {
+  .tags-view-item {
+    .el-icon-close {
+      width: 16px;
+      height: 16px;
+      vertical-align: 2px;
+      border-radius: 50%;
+      text-align: center;
+      transition: all 0.3s cubic-bezier(0.645, 0.045, 0.355, 1);
+      transform-origin: 100% 50%;
+      &:before {
+        transform: scale(0.6);
+        display: inline-block;
+        vertical-align: -3px;
+      }
+      &:hover {
+        background-color: #b4bccc;
+        color: #fff;
+        // width: 12px !important;
+        // height: 12px !important;
+      }
+    }
+  }
+}
+</style>

+ 4 - 0
src/layout/components/index.js

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

+ 124 - 0
src/layout/index.vue

@@ -0,0 +1,124 @@
+<template>
+  <div :class="classObj" class="app-wrapper">
+    <div v-if="device === 'mobile' && sidebar.opened" class="drawer-bg" @click="handleClickOutside" />
+
+    <sidebar v-if="settingsStore.navigationType !== 'top-menu-type'" class="sidebar-container" />
+
+    <navbar v-if="settingsStore.navigationType === 'top-menuMix-type' || settingsStore.navigationType === 'top-menu-type'" @setLayout="setLayout" />
+
+    <div
+      :class="{ hasTagsView: needTagsView, sidebarHide: sidebar.hide }"
+      class="main-container"
+      :style="{
+        marginLeft: settingsStore.navigationType === 'top-menu-type' ? '0 !important' : '',
+        height: `calc(100% - ${settingsStore.setMainHeight()})`,
+        minHeight: `calc(100% - ${settingsStore.setMainHeight()})`,
+      }"
+    >
+      <navbar v-if="settingsStore.navigationType === 'left-menu-type'" @setLayout="setLayout" />
+      <tags-view v-if="needTagsView" />
+      <app-main />
+      <settings ref="settingRef" />
+    </div>
+  </div>
+</template>
+
+<script setup>
+/*----------------------------------依赖引入-----------------------------------*/
+import { useWindowSize } from "@vueuse/core";
+/*----------------------------------接口引入-----------------------------------*/
+/*----------------------------------store引入-----------------------------------*/
+import { useSettingsStore, useAppStore } from "@/store/modules/index";
+/*----------------------------------组件引入-----------------------------------*/
+import Sidebar from "./components/Sidebar/index.vue";
+import { AppMain, Navbar, Settings, TagsView } from "./components";
+/*----------------------------------公共方法引入-----------------------------------*/
+const settingsStore = useSettingsStore();
+
+/*----------------------------------变量声明-----------------------------------*/
+const theme = computed(() => settingsStore.theme);
+const sideTheme = computed(() => settingsStore.sideTheme); //主题风格设置
+const navigationType = ref(settingsStore.navigationType); //导航栏模式
+const needTagsView = computed(() => settingsStore.tagsView); //导航面包屑
+const sidebar = computed(() => useAppStore().sidebar);
+const device = computed(() => useAppStore().device);
+
+const { width, height } = useWindowSize();
+const WIDTH = 992; // refer to Bootstrap's responsive design
+
+const classObj = computed(() => ({
+  hideSidebar: !sidebar.value.opened,
+  openSidebar: sidebar.value.opened,
+  withoutAnimation: sidebar.value.withoutAnimation,
+  mobile: device.value === "mobile",
+}));
+
+watchEffect(() => {
+  if (device.value === "mobile" && sidebar.value.opened) {
+    useAppStore().closeSideBar({ withoutAnimation: false });
+  }
+  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.module.scss";
+
+.app-wrapper {
+  @include clearfix;
+  position: relative;
+  height: 100%;
+  width: 100%;
+
+  &.mobile.openSidebar {
+    position: fixed;
+    top: 0;
+  }
+}
+
+.drawer-bg {
+  background: #000;
+  opacity: 0.3;
+  width: 100%;
+  top: 0;
+  height: 100%;
+  position: absolute;
+  z-index: 999;
+}
+
+.fixed-header {
+  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);
+}
+
+.sidebarHide .fixed-header {
+  width: 100%;
+}
+
+.mobile .fixed-header {
+  width: 100%;
+}
+</style>

+ 100 - 0
src/main.js

@@ -0,0 +1,100 @@
+import { createApp } from 'vue'
+
+import Cookies from 'js-cookie'
+import VueUeditorWrap from 'vue-ueditor-wrap';
+import ElementPlus from 'element-plus'
+import locale from 'element-plus/es/locale/lang/zh-cn' // 中文语言
+// import 'dayjs/locale/zh-cn'
+
+import '@/assets/styles/index.scss' // global css
+
+import App from './App'
+import store from './store'
+import router from './router'
+import directive from './directive' // directive
+import lazyPlugin from 'vue3-lazy'
+// import './mockjs/index'//mockjs
+
+
+
+// 注册指令
+import plugins from './plugins' // plugins
+import { download } from '@/utils/request'
+// svg图标
+import 'virtual:svg-icons-register'
+import SvgIcon from '@/components/SvgIcon'
+import elementIcons from '@/components/SvgIcon/svgicon'
+
+// permission control
+import './permission'
+
+//// 全局JavaScript时间库
+import dayjs from "dayjs";
+import isBetween from 'dayjs/plugin/isBetween';
+import isSameOrBefore from 'dayjs/plugin/isSameOrBefore';
+dayjs.extend(isSameOrBefore)
+dayjs.extend(isBetween);
+
+// 全局js公共方法库
+import regex from '@/utils/regex'// 全局正则表达式库
+import { parseTime, resetForm, addDateRange, handleTree, selectDictLabel, selectDictLabels } from '@/utils/ruoyi'
+
+// 文件上传组件
+import FileUpload from "@/components/FileUpload"
+// 图片上传组件
+import ImageUpload from "@/components/ImageUpload"
+// 图片预览组件
+import ImagePreview from "@/components/ImagePreview"
+// 自定义树选择组件
+import TreeSelect from '@/components/TreeSelect'
+// 字典标签组件
+import DictTag from '@/components/DictTag'
+
+
+
+
+
+
+const app = createApp(App)
+
+// 全局方法挂载
+app.config.globalProperties.$echart = ref('default')
+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
+app.config.globalProperties.dayjs = dayjs
+app.config.globalProperties.regex = regex
+
+
+
+// 全局组件挂载
+app.component('DictTag', DictTag)
+app.component('TreeSelect', TreeSelect)
+app.component('FileUpload', FileUpload)
+app.component('ImageUpload', ImageUpload)
+app.component('ImagePreview', ImagePreview)
+app.use(router)
+app.use(store)
+app.use(plugins)
+app.use(elementIcons)
+app.use(VueUeditorWrap)
+app.use(lazyPlugin, {
+    loading: '/loading.gif',
+    error: '/error.png',
+})
+app.component('svg-icon', SvgIcon)
+
+directive(app)
+
+// 使用element-plus 并且设置全局的大小
+app.use(ElementPlus, {
+    locale: locale,
+    // 支持 large、default、small
+    size: Cookies.get('size') || 'default'
+})
+app.config.warnHandler = () => null //全局去除浏览器告警
+app.mount('#app')

+ 18 - 0
src/permission.js

@@ -0,0 +1,18 @@
+import router from './router'
+import { ElMessage } from 'element-plus'
+import NProgress from 'nprogress'
+import 'nprogress/nprogress.css'
+import { isHttp } from '@/utils/validate'
+import useUserStore from '@/store/modules/user'
+import useSettingsStore from '@/store/modules/settings'
+import usePermissionStore from '@/store/modules/permission'
+NProgress.configure({ showSpinner: false });
+const whiteList = ['/index'];
+router.beforeEach((to, from, next) => {
+    NProgress.start()
+    next(); // 在免登录白名单,直接进入
+})
+
+router.afterEach(() => {
+    NProgress.done()
+})

+ 60 - 0
src/plugins/auth.js

@@ -0,0 +1,60 @@
+import useUserStore from '@/store/modules/user'
+
+function authPermission(permission) {
+  const all_permission = "*:*:*";
+  const permissions = useUserStore().permissions
+  if (permission && permission.length > 0) {
+    return permissions.some(v => {
+      return all_permission === v || v === permission
+    })
+  } else {
+    return false
+  }
+}
+
+function authRole(role) {
+  const super_admin = "admin";
+  const roles = useUserStore().roles
+  if (role && role.length > 0) {
+    return roles.some(v => {
+      return super_admin === v || v === role
+    })
+  } else {
+    return false
+  }
+}
+
+export default {
+  // 验证用户是否具备某权限
+  hasPermi(permission) {
+    return authPermission(permission);
+  },
+  // 验证用户是否含有指定权限,只需包含其中一个
+  hasPermiOr(permissions) {
+    return permissions.some(item => {
+      return authPermission(item)
+    })
+  },
+  // 验证用户是否含有指定权限,必须全部拥有
+  hasPermiAnd(permissions) {
+    return permissions.every(item => {
+      return authPermission(item)
+    })
+  },
+  // 验证用户是否具备某角色
+  hasRole(role) {
+    return authRole(role);
+  },
+  // 验证用户是否含有指定角色,只需包含其中一个
+  hasRoleOr(roles) {
+    return roles.some(item => {
+      return authRole(item)
+    })
+  },
+  // 验证用户是否含有指定角色,必须全部拥有
+  hasRoleAnd(roles) {
+    return roles.every(item => {
+      return authRole(item)
+    })
+  }
+}

+ 77 - 0
src/plugins/cache.js

@@ -0,0 +1,77 @@
+const sessionCache = {
+  set (key, value) {
+    if (!sessionStorage) {
+      return
+    }
+    if (key != null && value != null) {
+      sessionStorage.setItem(key, value)
+    }
+  },
+  get (key) {
+    if (!sessionStorage) {
+      return null
+    }
+    if (key == null) {
+      return null
+    }
+    return sessionStorage.getItem(key)
+  },
+  setJSON (key, jsonValue) {
+    if (jsonValue != null) {
+      this.set(key, JSON.stringify(jsonValue))
+    }
+  },
+  getJSON (key) {
+    const value = this.get(key)
+    if (value != null) {
+      return JSON.parse(value)
+    }
+  },
+  remove (key) {
+    sessionStorage.removeItem(key);
+  }
+}
+const localCache = {
+  set (key, value) {
+    if (!localStorage) {
+      return
+    }
+    if (key != null && value != null) {
+      localStorage.setItem(key, value)
+    }
+  },
+  get (key) {
+    if (!localStorage) {
+      return null
+    }
+    if (key == null) {
+      return null
+    }
+    return localStorage.getItem(key)
+  },
+  setJSON (key, jsonValue) {
+    if (jsonValue != null) {
+      this.set(key, JSON.stringify(jsonValue))
+    }
+  },
+  getJSON (key) {
+    const value = this.get(key)
+    if (value != null) {
+      return JSON.parse(value)
+    }
+  },
+  remove (key) {
+    localStorage.removeItem(key);
+  }
+}
+
+export default {
+  /**
+   * 会话级缓存
+   */
+  session: sessionCache,
+  /**
+   * 本地缓存
+   */
+  local: localCache
+}

+ 97 - 0
src/plugins/common.js

@@ -0,0 +1,97 @@
+/**
+ * 通用js方法封装处理
+ * Copyright (c) 2023 usky
+ */
+
+
+export default {
+    /**
+     * @全局postMessage
+     * @三维模型PostMessage
+     * @向父级页面发送
+     */
+    callFuncInThingJS(funcName, array) {
+        var iframe = document.getElementById("cont-3D-iframe");
+        var message = {
+            funcName: funcName,
+            param: array,
+        };
+        iframe.contentWindow.postMessage(message, "*");
+    },
+
+    /**
+     * @数据处理
+     * @param {需要处理的值或对象} data 
+     * @param {执行的方法名} fun 
+     */
+    dataFilter(data, fun) {
+        if (fun === "toString") {
+            return data ? data.toString() : ""
+        } else if (fun === "splitMapString") {
+            return data != "" && typeof data === "string" ? data.split(",").map(String) : [];
+        } else if (fun === "splitMapNumber") {
+            return data != "" && typeof data === "string" ? data.split(",").map(Number) : [];
+        }
+    },
+
+    /**
+     * @数据递归
+     * @param {需要处理的对象} objAry 
+     * @param {对象键值} key 
+     * @param {新的对象键值} newkey 
+     */
+    recursion(objAry, key, newkey) {
+        console.log(objAry, key, newkey)
+            // 修改树形结构的键值 把items替换为children
+        objAry.forEach((item) => {
+            // 如果它里面的children还是对象的话,就再调用一次这个函数
+            if (typeof item[key] == "object") {
+                // 递归,这里的调用传入的参数都是每一层的children对象!!
+                this.recursion(item[key], key, newkey);
+            }
+            Object.assign(item, {
+                [newkey]: item[key],
+            });
+            delete item[key];
+        });
+    },
+    /**
+     * @数据映射
+     * @param {需要判断的值} value 
+     * @param {需要映射的数据} data 
+     * @param {需要判断的数据值字段} dataValue
+     * @param {需要判断的返回值字段} returnValue
+     * @returns 
+     */
+    mapping(value, data, dataValue, returnValue) {
+        if(typeof(value) == 'string' && value.indexOf(",")>-1){ //value为字符串的id集合
+            let arr = value.split(",")
+            let arrReturnValue = ""
+            for(let i=0;i<arr.length;i++){
+                for (let ii = 0; ii < data.length; ii++) {
+                    if (arr[i] == data[ii][dataValue]) {
+                        arrReturnValue = arrReturnValue ? `${arrReturnValue},${data[ii][returnValue]}` : data[ii][returnValue]
+                    }
+                }
+            }
+            return arrReturnValue
+            
+        }else{
+            for (let i = 0; i < data.length; i++) {
+                if (value == data[i][dataValue]) {
+                    return returnValue ? data[i][returnValue] : data[i];
+                }
+            }
+        }
+        
+    },
+    /**
+     * 获取本地图片
+     * @param {图片路径} url 
+     * @returns 
+     */
+    getAssetsFile(url) {
+        return new URL(`../assets/${url}`,
+            import.meta.url).href
+    }
+}

+ 38 - 0
src/plugins/download.js

@@ -0,0 +1,38 @@
+import axios from 'axios'
+import { ElMessage } from 'element-plus'
+import { saveAs } from 'file-saver'
+import { getToken } from '@/utils/auth'
+import errorCode from '@/utils/errorCode'
+import { blobValidate } from '@/utils/ruoyi'
+
+const baseURL = import.meta.env.VITE_APP_BASE_API
+
+export default {
+  zip(url, name) {
+    var url = baseURL + url
+    axios({
+      method: 'get',
+      url: url,
+      responseType: 'blob',
+      headers: { 'Authorization': 'Bearer ' + getToken() }
+    }).then(async (res) => {
+      const isLogin = await blobValidate(res.data);
+      if (isLogin) {
+        const blob = new Blob([res.data], { type: 'application/zip' })
+        this.saveAs(blob, name)
+      } else {
+        this.printErrMsg(res.data);
+      }
+    })
+  },
+  saveAs(text, name, opts) {
+    saveAs(text, name, opts);
+  },
+  async printErrMsg(data) {
+    const resText = await data.text();
+    const rspObj = JSON.parse(resText);
+    const errMsg = errorCode[rspObj.code] || rspObj.msg || errorCode['default']
+    ElMessage.error(errMsg);
+  }
+}
+

+ 82 - 0
src/plugins/img.js

@@ -0,0 +1,82 @@
+/*
+ * @Descripttion: 
+ * @version: 
+ * @Author: wt
+ * @Date: 2023-04-25 10:59:14
+ * @LastEditors: wt
+ * @LastEditTime: 2023-05-04 15:48:05
+ */
+import projectInfoTitle from "@/assets/images/business/YtIoT/company/project_info_title.png" //项目信息
+import realtimeStatus from "@/assets/images/business/YtIoT/company/realtime_status_title.png"; //当前实时告警
+
+import ui_icon from "@/assets/images/business/YtIoT/ui_icon.png" //用户传输装置icon
+import fc_icon from "@/assets/images/business/YtIoT/fc_icon.png" //消防控制柜icon
+import ef_icon from "@/assets/images/business/YtIoT/ef_icon.png" //电气火灾icon
+import wp0_icon from "@/assets/images/business/YtIoT/wp0_icon.png" //水压icon
+import ll_icon from "@/assets/images/business/YtIoT/ll_icon.png" //液位icon
+import ci_icon from "@/assets/images/business/YtIoT/ci_icon.png" //环境监测icon
+import va_icon from "@/assets/images/business/YtIoT/va_icon.png" //视频监控icon
+import wc_icon from "@/assets/images/business/YtIoT/wc_icon.png" //风机控制柜icon
+import sm_icon from "@/assets/images/business/YtIoT/sm_icon.png" //烟感icon
+
+import ui_large from "@/assets/images/business/YtIoT/status_ui_large.gif" //用户传输装置gif
+import fc_large from "@/assets/images/business/YtIoT/status_fc_large.gif" //消防控制柜gif
+import ef_large from "@/assets/images/business/YtIoT/status_ef_large.gif" //电气火灾gif
+import wp_large from "@/assets/images/business/YtIoT/status_wp_large.gif" //水压gif
+import ll_large from "@/assets/images/business/YtIoT/status_ll_large.gif" //液位gif
+import ci_large from "@/assets/images/business/YtIoT/status_ci_large.gif" //环境监测gif
+import va_large from "@/assets/images/business/YtIoT/status_va_large.gif" //视频监控gif
+import wc_large from "@/assets/images/business/YtIoT/status_wc_large.gif" //风机控制柜gif
+import sm_large from "@/assets/images/business/YtIoT/status_sm_large.gif" //烟感gif
+
+
+import gj1 from "@/assets/images/business/iot/1.png" //火系统
+import gj2 from "@/assets/images/business/iot/2.png" //水系统
+import gj3 from "@/assets/images/business/iot/3.png" //烟感系统
+import gj4 from "@/assets/images/business/iot/4.png" //消防栓
+import gj5 from "@/assets/images/business/iot/5.png" //液位
+import gj6 from "@/assets/images/business/iot/6.png" //rtu
+import gj7 from "@/assets/images/business/iot/7.png" //电气火灾
+import gj8 from "@/assets/images/business/iot/8.png" //防火门
+import gj9 from "@/assets/images/business/iot/9.png" //气体灭火
+import gj10 from "@/assets/images/business/iot/10.png" //人脸识别
+import gj16 from "@/assets/images/business/iot/16.png" //视频监控
+import gj128 from "@/assets/images/business/iot/128.png" //井盖
+
+export default {
+    projectInfoTitle: projectInfoTitle,
+    realtimeStatus: realtimeStatus,
+
+    ui_icon: ui_icon,
+    fc_icon: fc_icon,
+    ef_icon: ef_icon,
+    wp0_icon: wp0_icon,
+    ll_icon: ll_icon,
+    ci_icon: ci_icon,
+    va_icon: va_icon,
+    sm_icon: sm_icon,
+    wc_icon: wc_icon,
+
+    ui_large: ui_large,
+    fc_large: fc_large,
+    ef_large: ef_large,
+    wp_large: wp_large,
+    ll_large: ll_large,
+    ci_large: ci_large,
+    va_large: va_large,
+    wc_large: wc_large,
+    sm_large: sm_large,
+
+    gj1: gj1,
+    gj2: gj2,
+    gj3: gj3,
+    gj4: gj4,
+    gj5: gj5,
+    gj6: gj6,
+    gj7: gj7,
+    gj8: gj8,
+    gj9: gj9,
+    gj10: gj10,
+    gj16: gj16,
+    gj128: gj128,
+}

+ 25 - 0
src/plugins/index.js

@@ -0,0 +1,25 @@
+import tab from './tab'
+import auth from './auth'
+import cache from './cache'
+import modal from './modal'
+import download from './download'
+import img from './img'
+import common from './common'
+
+
+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
+  // 公共获取图片
+  app.config.globalProperties.$img = img
+  // 公共方法
+  app.config.globalProperties.$common = common
+}

+ 82 - 0
src/plugins/modal.js

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

+ 65 - 0
src/plugins/tab.js

@@ -0,0 +1,65 @@
+import useTagsViewStore from '@/store/modules/tagsView'
+import router from '@/router'
+
+export default {
+    // 刷新当前tab页签
+    refreshPage(obj) {
+        const { path, query, matched } = router.currentRoute.value;
+        if (obj === undefined) {
+            matched.forEach((m) => {
+                if (m.components && m.components.default && m.components.default.name) {
+                    if (!['Layout', 'ParentView'].includes(m.components.default.name)) {
+                        obj = { name: m.components.default.name, path: path, query: query };
+                    }
+                }
+            });
+        }
+        return useTagsViewStore().delCachedView(obj).then(() => {
+            const { path, query } = obj
+            router.replace({
+                path: '/redirect' + path,
+                query: query
+            })
+        })
+    },
+    // 关闭当前tab页签,打开新页签
+    closeOpenPage(obj) {
+        useTagsViewStore().delView(router.currentRoute.value);
+        if (obj !== undefined) {
+            return router.push(obj);
+        }
+    },
+    // 关闭指定tab页签
+    closePage(obj) {
+        if (obj === undefined) {
+            return useTagsViewStore().delView(router.currentRoute.value).then(({ lastPath }) => {
+                return router.push(lastPath || '/index');
+            });
+        }
+        return useTagsViewStore().delView(obj);
+    },
+    // 关闭所有tab页签
+    closeAllPage() {
+        return useTagsViewStore().delAllViews();
+    },
+    // 关闭左侧tab页签
+    closeLeftPage(obj) {
+        return useTagsViewStore().delLeftTags(obj || router.currentRoute.value);
+    },
+    // 关闭右侧tab页签
+    closeRightPage(obj) {
+        return useTagsViewStore().delRightTags(obj || router.currentRoute.value);
+    },
+    // 关闭其他tab页签
+    closeOtherPage(obj) {
+        return useTagsViewStore().delOthersViews(obj || router.currentRoute.value);
+    },
+    // 打开tab页签
+    openPage(url) {
+        return router.push(url);
+    },
+    // 修改tab页签
+    updatePage(obj) {
+        return useTagsViewStore().updateVisitedView(obj);
+    }
+}

+ 84 - 0
src/router/index.js

@@ -0,0 +1,84 @@
+import { createWebHistory, createRouter } from 'vue-router'
+
+/**
+ * Note: 路由配置项
+ *
+ * hidden: true                     // 当设置 true 的时候该路由不会再侧边栏出现 如401,login等页面,或者如一些编辑页面/edit/1
+ * alwaysShow: true                 // 当你一个路由下面的 children 声明的路由大于1个时,自动会变成嵌套的模式--如组件页面
+ *                                  // 只有一个时,会将那个子路由当做根路由显示在侧边栏--如引导页面
+ *                                  // 若你想不管路由下面的 children 声明的个数都显示你的根路由
+ *                                  // 你可以设置 alwaysShow: true,这样它就会忽略之前定义的规则,一直显示根路由
+ * redirect: noRedirect             // 当设置 noRedirect 的时候该路由在面包屑导航中不可被点击
+ * name:'router-name'               // 设定路由的名字,一定要填写不然使用<keep-alive>时会出现各种问题
+ * query: '{"id": 1, "name": "ry"}' // 访问路由的默认传递参数
+ * roles: ['admin', 'common']       // 访问路由的角色权限
+ * permissions: ['a:a:a', 'b:b:b']  // 访问路由的菜单权限
+ * meta : {
+    noCache: true                   // 如果设置为true,则不会被 <keep-alive> 缓存(默认 false)
+    title: 'title'                  // 设置该路由在侧边栏和面包屑中展示的名字
+    icon: 'svg-name'                // 设置该路由的图标,对应路径src/assets/icons/svg
+    breadcrumb: false               // 如果设置为false,则不会在breadcrumb面包屑中显示
+    activeMenu: '/system/user'      // 当路由设置了该属性,则会高亮相对应的侧边栏。
+  }
+ */
+
+// 公共路由
+export const constantRoutes = [
+
+    {
+        path: '/',
+        component: () =>
+            import ('@/views/index'),
+        hidden: true
+    },
+    {
+        path: '/index',//首页
+        component: () =>
+            import ('@/views/index'),
+        hidden: true
+    },
+    {
+        path: '/list',//目录页
+        component: () =>
+            import ('@/views/list'),
+        hidden: true
+    },
+    {
+        path: '/validate',// 核验身份
+        component: () =>
+            import ('@/views/validate'),
+        hidden: true
+    },
+    {
+        path: '/fillIn', // 填写表单
+        component: () =>
+            import ('@/views/fillIn'),
+        hidden: true
+    },
+    {
+        path: '/401',
+        component: () =>
+            import ('@/views/common/error/401'),
+        hidden: true
+    }
+
+]
+
+// 动态路由,基于用户权限动态去加载
+export const dynamicRoutes = [
+    
+]
+
+const router = createRouter({
+    history: createWebHistory(),
+    routes: constantRoutes,
+    scrollBehavior(to, from, savedPosition) {
+        if (savedPosition) {
+            return savedPosition
+        } else {
+            return { top: 0 }
+        }
+    },
+});
+
+export default router;

+ 45 - 0
src/settings.js

@@ -0,0 +1,45 @@
+export default {
+    /**
+     * 网页标题
+     */
+    title: import.meta.env.VITE_APP_TITLE,
+    /**
+     * 导航栏模式 左侧菜单模式left-menu-type,顶部菜单混合模式top-menuMix-type,顶部菜单模式top-menu-type
+     */
+    navigationType: 'top-menu-type',
+    /**
+     * 主题 深色主题theme-dark,浅色主题theme-light,深蓝色主题 dark-blue
+     */
+    sideTheme: 'theme-light',
+    /**
+     * 头部颜色
+     */
+    topTheme: '',
+
+    /**
+     * 是否系统布局配置
+     */
+    showSettings: false,
+    /**
+     * 是否显示 tagsView
+     */
+    tagsView: false,
+
+    /**
+     * 是否显示logo
+     */
+    sidebarLogo: true,
+
+    /**
+     * 是否显示动态标题
+     */
+    dynamicTitle: false,
+
+    /**
+     * @type {string | array} 'production' | ['production', 'development']
+     * @description Need show err logs component.
+     * The default is only used in the production env
+     * If you want to also use it in dev, you can pass ['production', 'development']
+     */
+    errorLog: 'production'
+}

+ 13 - 0
src/store/index.js

@@ -0,0 +1,13 @@
+import { createPinia } from "pinia";
+import { createPersistedState } from 'pinia-plugin-persistedstate';
+
+const store = createPinia()
+
+store.use(createPersistedState({
+    serializer: { // 指定参数序列化器
+        serialize: JSON.stringify,
+        deserialize: JSON.parse,
+    },
+}))
+
+export default store

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

@@ -0,0 +1,44 @@
+import Cookies from "js-cookie";
+
+const useAppStore = defineStore("app", {
+  state: () => ({
+    sidebar: {
+      opened: Cookies.get("sidebarStatus") ? !!+Cookies.get("sidebarStatus") : true,
+      withoutAnimation: false,
+      hide: false,
+    },
+    device: "desktop",
+    size: Cookies.get("size") || "default",
+  }),
+  actions: {
+    toggleSideBar(withoutAnimation) {
+      if (this.sidebar.hide) {
+        return false;
+      }
+      this.sidebar.opened = !this.sidebar.opened;
+      this.sidebar.withoutAnimation = withoutAnimation;
+      if (this.sidebar.opened) {
+        Cookies.set("sidebarStatus", 1);
+      } else {
+        Cookies.set("sidebarStatus", 0);
+      }
+    },
+    closeSideBar(withoutAnimation) {
+      Cookies.set("sidebarStatus", 0);
+      this.sidebar.opened = false;
+      this.sidebar.withoutAnimation = withoutAnimation;
+    },
+    toggleDevice(device) {
+      this.device = device;
+    },
+    setSize(size) {
+      this.size = size;
+      Cookies.set("size", size);
+    },
+    toggleSideBarHide(status) {
+      this.sidebar.hide = status;
+    },
+  },
+});
+
+export default useAppStore;

+ 187 - 0
src/store/modules/common.js

@@ -0,0 +1,187 @@
+import dayjs from 'dayjs'
+
+const commonStore = defineStore("common", {
+    state: () => ({
+        sevenDaysDate: dayjs().subtract(7, "day").startOf("day").format("YYYY-MM-DD HH:mm:ss"),
+
+
+        helpCenterList: {
+            menuId: undefined,
+            contentList: {}
+        },//帮助中心公共值存储
+
+        meetingList: {
+            roomName: "",//会议室名称
+        }
+    }),
+    persist: {
+        storage: window.localStorage, // 指定换成地址
+    },
+    actions: {
+        /**
+         * @公共时间 获取当前日期
+         */
+        getTimes() {
+            return {
+                startTime: dayjs().startOf("day").format("YYYY-MM-DD HH:mm:ss"),
+                endTime: dayjs().format("YYYY-MM-DD HH:mm:ss"),
+            };
+        },
+        // 格式化日期对象
+        getNowDate() {
+            var date = new Date();
+            var sign2 = ":";
+            var year = date.getFullYear() // 年
+            var month = date.getMonth() + 1; // 月
+            var day = date.getDate(); // 日
+            var hour = date.getHours(); // 时
+            var minutes = date.getMinutes(); // 分
+            var seconds = date.getSeconds() //秒
+            var weekArr = ['星期一', '星期二', '星期三', '星期四', '星期五', '星期六', '星期天'];
+            var week = weekArr[date.getDay()];
+            // 给一位数的数据前面加 “0”
+            if (month >= 1 && month <= 9) {
+                month = "0" + month;
+            }
+            if (day >= 0 && day <= 9) {
+                day = "0" + day;
+            }
+            if (hour >= 0 && hour <= 9) {
+                hour = "0" + hour;
+            }
+            if (minutes >= 0 && minutes <= 9) {
+                minutes = "0" + minutes;
+            }
+            if (seconds >= 0 && seconds <= 9) {
+                seconds = "0" + seconds;
+            }
+            return year + "-" + month + "-" + day + " " + hour + sign2 + minutes + sign2 + seconds;
+        },
+
+        /**
+         * @处理公共日期格式
+         */
+        formatterDate(date, fmt) {
+            let nowDate = {
+                yyyy: date.getFullYear(), // 年
+                MM: date.getMonth() + 1, // 月份
+                dd: date.getDate(), //日
+                hh: date.getHours(),
+                mm: date.getMinutes(),
+                ss: date.getSeconds(),
+            };
+            if (/(y+)/.test(fmt)) {
+                fmt = fmt.replace(RegExp.$1, (date.getFullYear() + "").substr(4 - RegExp.$1.length));
+            }
+            for (var k in nowDate) {
+                if (new RegExp("(" + k + ")").test(fmt)) {
+                    fmt = fmt.replace(RegExp.$1, RegExp.$1.length == 1 ? nowDate[k] : ("00" + nowDate[k]).substr(("" + nowDate[k]).length));
+                }
+            }
+            return fmt;
+        },
+
+        /**
+         * @禁止选择某个时间段
+         * @type 1 可选90天之前的时间
+         * @type 2 可选当前时间之前所有的时间
+         * @开始时间
+         */
+        startDateDisabled(date, endDate, type) {
+            if (type == 1) {
+                let data = !endDate ? Date.now() : new Date(endDate);
+
+                let data1 = !endDate ? Date.now() - 3600 * 1000 * 24 * 90 : new Date(endDate) - 3600 * 1000 * 24 * 90;
+                return date > data || date < data1;
+            } else if (type == 2) {
+                let data = !endDate ? Date.now() : new Date(endDate);
+                return date > data;
+            }
+        },
+        /**
+         * @禁止选择某个时间段
+         * @type 1 可选90天之前的时间
+         * @type 2 可选当前时间之前所有的时间
+         * @结束时间
+         */
+        endDateDisabled(date, startDate, type) {
+            if (type == 1) {
+                let data = !startDate ? Date.now() - 3600 * 1000 * 24 * 90 : new Date(startDate) - 3600 * 1000 * 24 * 90;
+                return date < data;
+            } else if (type == 2) {
+                let data = !startDate ? Date.now() - 3600 * 1000 * 24 : new Date(startDate) - 3600 * 1000 * 24;
+                return date < data;
+            }
+        },
+
+        /**
+         * @文件下载
+         * @param {*} url
+         */
+        fetchDownloadFile(url) {
+            let time = new Date();
+            time = this.formatterDate(time, "yyyyMMddhhmmss");
+
+            let fileType = url.split("/")[url.split("/").length - 1].split(".")[1];
+            let type = "";
+            if (fileType === "xls") {
+                type = "application/vnd.ms-excel";
+            } else if (fileType === "xlsx") {
+                type = "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet";
+            } else if (fileType === "doc") {
+                type = "application/msword";
+            } else if (fileType === "docx") {
+                type = "application/vnd.openxmlformats-officedocument.wordprocessingml.document";
+            } else if (fileType === "pdf") {
+                type = "application/pdf";
+            } else if (fileType === "ppt") {
+                type = "application/vnd.ms-powerpoint";
+            } else if (fileType === "pptx") {
+                type = "application/vnd.openxmlformats-officedocument.presentationml.presentation";
+            } else if (fileType === "png") {
+                type = "image/png";
+            } else if (fileType === "jpeg" || fileType === "jpg") {
+                type = "image/jpeg";
+            } else if (fileType === "gif") {
+                type = "image/gif";
+            } else if (fileType === "bmp") {
+                type = "image/bmp";
+            } else if (fileType === "html") {
+                type = "text/html";
+            } else if (fileType === "txt") {
+                type = "text/plain";
+            } else if (fileType === "htm") {
+                type = "text/html";
+            } else if (fileType === "rar") {
+                type = "application/x-rar-compressed";
+            } else if (fileType === "zip") {
+                type = "application/zip";
+            } else if (fileType === "bz2") {
+                type = "application/x-bzip2";
+            }
+
+            fetch(url, {
+                method: "get",
+                mode: "cors",
+            })
+                .then((response) => response.blob())
+                .then((res) => {
+                    const downloadUrl = window.URL.createObjectURL(
+                        //new Blob() 对后端返回文件流类型处理
+                        new Blob([res], { type: type })
+                    );
+                    const link = document.createElement("a");
+                    link.href = downloadUrl;
+                    link.setAttribute("download", time);
+                    document.body.appendChild(link);
+                    link.click();
+                    link.remove();
+                })
+                .catch((error) => {
+                    window.open(url);
+                });
+        },
+    },
+});
+
+export default commonStore;

+ 15 - 0
src/store/modules/index.js

@@ -0,0 +1,15 @@
+import usePermissionStore from "./permission";
+import useUserStore from "./user"; //用户信息store
+import useSettingsStore from "./settings";//用户设置store
+import useAppStore from "./app";
+import useTagsViewStore from "./tagsView";
+import commonStore from "./common";
+
+export {
+    useTagsViewStore,
+    usePermissionStore,
+    useUserStore,
+    useSettingsStore,
+    useAppStore,
+    commonStore,
+}

+ 139 - 0
src/store/modules/permission.js

@@ -0,0 +1,139 @@
+import auth from '@/plugins/auth'
+import router, { constantRoutes, dynamicRoutes } from '@/router'
+import { getRouters } from '@/api/menu'
+import Layout from '@/layout/index'
+import ParentView from '@/components/ParentView'
+import InnerLink from '@/layout/components/InnerLink'
+
+// 匹配views里面所有的.vue文件
+const modules =
+    import.meta.glob('./../../views/**/*.vue')
+
+const usePermissionStore = defineStore(
+    'permission', {
+        state: () => ({
+            routes: [],
+            addRoutes: [],
+            defaultRoutes: [],
+            topbarRouters: [],
+            sidebarRouters: []
+        }),
+        actions: {
+            setRoutes(routes) {
+                this.addRoutes = routes
+                this.routes = constantRoutes.concat(routes)
+            },
+            setDefaultRoutes(routes) {
+                this.defaultRoutes = constantRoutes.concat(routes)
+            },
+            setTopbarRoutes(routes) {
+                this.topbarRouters = routes
+            },
+            setSidebarRouters(routes) {
+                this.sidebarRouters = routes
+            },
+            generateRoutes(roles) {
+                return new Promise(resolve => {
+                    // 向后端请求路由数据
+                    getRouters().then(res => {
+                        const sdata = JSON.parse(JSON.stringify(res.data))
+                        const rdata = JSON.parse(JSON.stringify(res.data))
+                        const defaultData = JSON.parse(JSON.stringify(res.data))
+                        const sidebarRoutes = filterAsyncRouter(sdata)
+                        const rewriteRoutes = filterAsyncRouter(rdata, false, true)
+                        const defaultRoutes = filterAsyncRouter(defaultData)
+                        const asyncRoutes = filterDynamicRoutes(dynamicRoutes)
+                        asyncRoutes.forEach(route => { router.addRoute(route) })
+                        this.setRoutes(rewriteRoutes)
+                        this.setSidebarRouters(constantRoutes.concat(sidebarRoutes))
+                        this.setDefaultRoutes(sidebarRoutes)
+                        this.setTopbarRoutes(defaultRoutes)
+                        resolve(rewriteRoutes)
+                    })
+                })
+            }
+        }
+    })
+
+// 遍历后台传来的路由字符串,转换为组件对象
+function filterAsyncRouter(asyncRouterMap, lastRouter = false, type = false) {
+    return asyncRouterMap.filter(route => {
+        if (type && route.children) {
+            route.children = filterChildren(route.children)
+        }
+        if (route.component) {
+            // Layout ParentView 组件特殊处理
+            if (route.component === 'Layout') {
+                route.component = Layout
+            } else if (route.component === 'ParentView') {
+                route.component = ParentView
+            } else if (route.component === 'InnerLink') {
+                route.component = InnerLink
+            } else {
+                route.component = loadView(route.component)
+            }
+        }
+        if (route.children != null && route.children && route.children.length) {
+            route.children = filterAsyncRouter(route.children, route, type)
+        } else {
+            delete route['children']
+            delete route['redirect']
+        }
+        return true
+    })
+}
+
+function filterChildren(childrenMap, lastRouter = false) {
+    var children = []
+
+    childrenMap.forEach((el, index) => {
+        if (el.children && el.children.length) {
+            if (el.component === 'ParentView' && !lastRouter) {
+                el.children.forEach(c => {
+                    c.path = el.path + '/' + c.path
+                    if (c.children && c.children.length) {
+                        children = children.concat(filterChildren(c.children, c))
+                        return
+                    }
+                    children.push(c)
+                })
+                return
+            }
+        }
+        if (lastRouter) {
+            el.path = lastRouter.path + '/' + el.path
+        }
+        children = children.concat(el)
+    })
+    return children
+}
+
+// 动态路由遍历,验证是否具备权限
+export function filterDynamicRoutes(routes) {
+    const res = []
+    routes.forEach(route => {
+        if (route.permissions) {
+            if (auth.hasPermiOr(route.permissions)) {
+                res.push(route)
+            }
+        } else if (route.roles) {
+            if (auth.hasRoleOr(route.roles)) {
+                res.push(route)
+            }
+        }
+    })
+    return res
+}
+
+export const loadView = (view) => {
+    let res;
+    for (const path in modules) {
+        const dir = path.split('views/')[1].split('.vue')[0];
+        if (dir === view) {
+            res = () => modules[path]();
+        }
+    }
+    return res;
+}
+
+export default usePermissionStore

+ 43 - 0
src/store/modules/settings.js

@@ -0,0 +1,43 @@
+import defaultSettings from '@/settings'
+import { useDynamicTitle } from '@/utils/dynamicTitle'
+
+const { sideTheme, showSettings, tagsView, dynamicTitle, navigationType, navigationTypeCopy, topTheme } = defaultSettings
+
+const storageSetting = JSON.parse(localStorage.getItem('layout-setting')) || ''
+
+const useSettingsStore = defineStore(
+    'settings', {
+    state: () => ({
+        title: '',
+        topTheme: storageSetting.topTheme === undefined ? topTheme : storageSetting.topTheme, //头部背景颜色单独设置
+        topFontColor: storageSetting.topFontColor, //头部字体颜色单独设置
+        sideTheme: storageSetting.sideTheme || sideTheme,
+        navigationType: storageSetting.navigationType || navigationType,
+        showSettings: storageSetting.showSettings || showSettings,
+        tagsView: storageSetting.tagsView === undefined ? tagsView : storageSetting.tagsView,
+        dynamicTitle: storageSetting.dynamicTitle === undefined ? dynamicTitle : storageSetting.dynamicTitle,
+
+    }),
+    actions: {
+        // 设置主内容区的高度
+        setMainHeight() {
+            if (this.tagsView) return "96px";
+            else return "56px";
+        },
+
+        // 修改布局设置
+        changeSetting(data) {
+            const { key, value } = data
+            if (this.hasOwnProperty(key)) {
+                this[key] = value
+            }
+        },
+        // 设置网页标题
+        setTitle(title) {
+            this.title = title
+            useDynamicTitle();
+        }
+    }
+})
+
+export default useSettingsStore

+ 171 - 0
src/store/modules/tagsView.js

@@ -0,0 +1,171 @@
+const useTagsViewStore = defineStore(
+    'tags-view', {
+        state: () => ({
+            visitedViews: [],
+            cachedViews: [],
+            timeStamp: undefined,
+        }),
+        actions: {
+            timeChange(time) {
+                this.timeStamp = time
+            },
+            addView(view) {
+
+
+                this.addVisitedView(view)
+                this.addCachedView(view)
+            },
+            addVisitedView(view) {
+                if (this.visitedViews.some(v => v.path === view.path)) return
+                this.visitedViews.push(
+                    Object.assign({}, view, {
+                        title: view.meta.title || 'no-name'
+                    })
+                )
+
+                //去除预览页的历史tab
+                this.visitedViews.forEach((value, index) => {
+                        if (value.name == 'Preview') {
+                            this.visitedViews.splice(index, 1)
+                        }
+                    })
+                    // console.log('this.visitedViews')
+                    // console.log(this.visitedViews)
+
+            },
+            addCachedView(view) {
+                if (this.cachedViews.includes(view.name)) return
+                if (!view.meta.noCache) {
+                    this.cachedViews.push(view.name)
+                }
+            },
+            delView(view) {
+                return new Promise(resolve => {
+                    this.delVisitedView(view)
+                    this.delCachedView(view)
+                    resolve({
+                        visitedViews: [...this.visitedViews],
+                        cachedViews: [...this.cachedViews]
+                    })
+                })
+            },
+            delVisitedView(view) {
+                return new Promise(resolve => {
+                    for (const [i, v] of this.visitedViews.entries()) {
+                        if (v.path === view.path) {
+                            this.visitedViews.splice(i, 1)
+                            break
+                        }
+                    }
+                    resolve([...this.visitedViews])
+                })
+            },
+            delCachedView(view) {
+                return new Promise(resolve => {
+                    const index = this.cachedViews.indexOf(view.name)
+                    index > -1 && this.cachedViews.splice(index, 1)
+                    resolve([...this.cachedViews])
+                })
+            },
+            delOthersViews(view) {
+                return new Promise(resolve => {
+                    this.delOthersVisitedViews(view)
+                    this.delOthersCachedViews(view)
+                    resolve({
+                        visitedViews: [...this.visitedViews],
+                        cachedViews: [...this.cachedViews]
+                    })
+                })
+            },
+            delOthersVisitedViews(view) {
+                return new Promise(resolve => {
+                    this.visitedViews = this.visitedViews.filter(v => {
+                        return v.meta.affix || v.path === view.path
+                    })
+                    resolve([...this.visitedViews])
+                })
+            },
+            delOthersCachedViews(view) {
+                return new Promise(resolve => {
+                    const index = this.cachedViews.indexOf(view.name)
+                    if (index > -1) {
+                        this.cachedViews = this.cachedViews.slice(index, index + 1)
+                    } else {
+                        this.cachedViews = []
+                    }
+                    resolve([...this.cachedViews])
+                })
+            },
+            delAllViews(view) {
+                return new Promise(resolve => {
+                    this.delAllVisitedViews(view)
+                    this.delAllCachedViews(view)
+                    resolve({
+                        visitedViews: [...this.visitedViews],
+                        cachedViews: [...this.cachedViews]
+                    })
+                })
+            },
+            delAllVisitedViews(view) {
+                return new Promise(resolve => {
+                    const affixTags = this.visitedViews.filter(tag => tag.meta.affix)
+                    this.visitedViews = affixTags
+                    resolve([...this.visitedViews])
+                })
+            },
+            delAllCachedViews(view) {
+                return new Promise(resolve => {
+                    this.cachedViews = []
+                    resolve([...this.cachedViews])
+                })
+            },
+            updateVisitedView(view) {
+                for (let v of this.visitedViews) {
+                    if (v.path === view.path) {
+                        v = Object.assign(v, view)
+                        break
+                    }
+                }
+            },
+            delRightTags(view) {
+                return new Promise(resolve => {
+                    const index = this.visitedViews.findIndex(v => v.path === view.path)
+                    if (index === -1) {
+                        return
+                    }
+                    this.visitedViews = this.visitedViews.filter((item, idx) => {
+                        if (idx <= index || (item.meta && item.meta.affix)) {
+                            return true
+                        }
+                        const i = this.cachedViews.indexOf(item.name)
+                        if (i > -1) {
+                            this.cachedViews.splice(i, 1)
+                        }
+                        return false
+                    })
+                    resolve([...this.visitedViews])
+                })
+            },
+            delLeftTags(view) {
+                return new Promise(resolve => {
+                    const index = this.visitedViews.findIndex(v => v.path === view.path)
+                    if (index === -1) {
+                        return
+                    }
+                    this.visitedViews = this.visitedViews.filter((item, idx) => {
+                        if (idx >= index || (item.meta && item.meta.affix)) {
+                            return true
+                        }
+                        const i = this.cachedViews.indexOf(item.name)
+                        if (i > -1) {
+                            this.cachedViews.splice(i, 1)
+                        }
+                        return false
+                    })
+                    resolve([...this.visitedViews])
+                })
+            }
+        }
+    })
+
+export default useTagsViewStore

Неке датотеке нису приказане због велике количине промена