Sfoglia il codice sorgente

Merge branch 'master' of http://1.14.104.71:10880/root/ylrz_his_scrm_companyUI

ct 2 giorni fa
parent
commit
e64c9f5c7c
100 ha cambiato i file con 6616 aggiunte e 610 eliminazioni
  1. 19 0
      .env.prod-whhm
  2. 3 1
      package.json
  3. 2 2
      src/api/qw/groupChat.js
  4. 37 0
      src/api/qw/groupChatTransfer.js
  5. 9 0
      src/api/qw/groupChatTransferLog.js
  6. 17 0
      src/api/qw/sopUserLogs.js
  7. 11 0
      src/api/qw/user.js
  8. 84 0
      src/api/system/config.js
  9. BIN
      src/assets/images/cishu_views.png
  10. BIN
      src/assets/images/code.png
  11. BIN
      src/assets/images/customer.png
  12. 39 0
      src/assets/images/dark.svg
  13. BIN
      src/assets/images/default.jpg
  14. BIN
      src/assets/images/eyeoff.png
  15. BIN
      src/assets/images/eyeopen.png
  16. BIN
      src/assets/images/hongbao_views.png
  17. 39 0
      src/assets/images/light.svg
  18. BIN
      src/assets/images/liuliang.png
  19. BIN
      src/assets/images/login-background.jpg
  20. BIN
      src/assets/images/login_bg.png
  21. BIN
      src/assets/images/login_left.png
  22. BIN
      src/assets/images/management_end_left_img.png
  23. BIN
      src/assets/images/member.png
  24. BIN
      src/assets/images/message.png
  25. BIN
      src/assets/images/number_views.png
  26. BIN
      src/assets/images/pass.png
  27. BIN
      src/assets/images/profile.jpg
  28. BIN
      src/assets/images/profile.png
  29. BIN
      src/assets/images/renshu_views.png
  30. BIN
      src/assets/images/salesperson.png
  31. BIN
      src/assets/images/tab_company.png
  32. BIN
      src/assets/images/tab_enterprise.png
  33. BIN
      src/assets/images/topbg.png
  34. BIN
      src/assets/images/user.png
  35. BIN
      src/assets/images/yunlian_logo.png
  36. BIN
      src/assets/images/zcgl_bg.png
  37. 141 145
      src/assets/styles/common.scss
  38. 8 0
      src/assets/styles/element-ui.scss
  39. 1 1
      src/assets/styles/element-variables.scss
  40. 13 188
      src/assets/styles/index.scss
  41. 24 16
      src/assets/styles/sidebar.scss
  42. 53 47
      src/assets/styles/transition.scss
  43. 37 18
      src/assets/styles/variables.scss
  44. 1 1
      src/components/Breadcrumb/index.vue
  45. 179 0
      src/components/Crontab/day.vue
  46. 122 0
      src/components/Crontab/hour.vue
  47. 425 0
      src/components/Crontab/index.vue
  48. 120 0
      src/components/Crontab/min.vue
  49. 128 0
      src/components/Crontab/month.vue
  50. 566 0
      src/components/Crontab/result.vue
  51. 133 0
      src/components/Crontab/second.vue
  52. 167 0
      src/components/Crontab/week.vue
  53. 144 0
      src/components/Crontab/year.vue
  54. 1 1
      src/components/DeviceInfo/Pressure.vue
  55. 1 1
      src/components/DeviceInfo/Pulse.vue
  56. 183 13
      src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue
  57. 27 17
      src/components/DeviceInfo/Sleep.vue
  58. 1 1
      src/components/DeviceInfo/Sports.vue
  59. 1 1
      src/components/DeviceInfo/Temperature.vue
  60. 81 4
      src/components/Editor/index.vue
  61. 4 14
      src/components/Editor/wang.vue
  62. 246 0
      src/components/FileUpload/index.vue
  63. 123 0
      src/components/H5/FormWrapper.vue
  64. 87 0
      src/components/H5/config-item/common-config.vue
  65. 182 0
      src/components/H5/config-item/h5-chat-config-dialog.vue
  66. 287 0
      src/components/H5/config-item/h5-chat-config.vue
  67. 270 0
      src/components/H5/config-item/h5-countdown-config.vue
  68. 20 0
      src/components/H5/config-item/h5-image-config.vue
  69. 22 0
      src/components/H5/config-item/h5-sep-config.vue
  70. 127 0
      src/components/H5/config-item/h5-text-config.vue
  71. 13 0
      src/components/H5/css/base.css
  72. 27 0
      src/components/H5/h5-button.vue
  73. 234 0
      src/components/H5/h5-chat.vue
  74. 79 0
      src/components/H5/h5-countdown.vue
  75. 27 0
      src/components/H5/h5-image.vue
  76. 28 0
      src/components/H5/h5-sep.vue
  77. 27 0
      src/components/H5/h5-text.vue
  78. 405 0
      src/components/H5Editor/index.vue
  79. 132 60
      src/components/ImageUpload/index.vue
  80. 98 7
      src/components/Material/index.vue
  81. 91 6
      src/components/Material/single.vue
  82. 128 0
      src/components/MinimizableDialog/index.vue
  83. 7 1
      src/components/Pagination/index.vue
  84. 54 5
      src/components/RightToolbar/index.vue
  85. 4 7
      src/components/Screenfull/index.vue
  86. 12 14
      src/components/ThemePicker/index.vue
  87. 86 35
      src/components/TopNav/index.vue
  88. 426 0
      src/components/VideoUpload/index.vue
  89. 36 0
      src/components/iFrame/index.vue
  90. 217 0
      src/components/qqMap/index.vue
  91. 63 0
      src/directive/dialog/dialog/drag.js
  92. 33 0
      src/directive/dialog/dialog/dragHeight.js
  93. 29 0
      src/directive/dialog/dialog/dragWidth.js
  94. 63 0
      src/directive/dialog/drag.js
  95. 33 0
      src/directive/dialog/dragHeight.js
  96. 29 0
      src/directive/dialog/dragWidth.js
  97. 21 0
      src/directive/index.js
  98. 1 2
      src/directive/permission/hasPermi.js
  99. 1 2
      src/directive/permission/hasRole.js
  100. 27 0
      src/directive/permission/permission/hasPermi.js

+ 19 - 0
.env.prod-whhm

@@ -0,0 +1,19 @@
+# 页面标题
+VUE_APP_TITLE =惠名SCRM销售端
+# 公司名称
+VUE_APP_COMPANY_NAME =武汉惠名大药房有限责任公司
+# ICP备案号
+VUE_APP_ICP_RECORD =闽ICP备2020016609号-3
+# ICP网站访问地址
+VUE_APP_ICP_URL =https://beian.miit.gov.cn
+# 网站LOG
+VUE_APP_LOG_URL =@/assets/logo/whhm.png
+
+# 生产环境配置
+ENV = 'production'
+
+# FS管理系统/开发环境
+VUE_APP_BASE_API = '/prod-api'
+
+# 路由懒加载
+VUE_CLI_BABEL_TRANSPILE_MODULES = true

+ 3 - 1
package.json

@@ -63,7 +63,7 @@
     "element-ui": "2.15.5",
     "file-saver": "2.0.1",
     "form-making": "^1.2.9",
-    "fuse.js": "3.4.4",
+    "fuse.js": "^3.4.4",
     "js-beautify": "1.10.2",
     "js-cookie": "2.2.0",
     "jsencrypt": "3.0.0-rc.1",
@@ -85,6 +85,8 @@
     "vue-count-to": "1.0.13",
     "vue-cropper": "0.4.9",
     "vue-full-calendar": "^2.8.1-0",
+    "vue-jsonp": "^2.1.0",
+    "vue-meta": "^2.4.0",
     "vue-mobile-audio": "^0.1.3",
     "vue-router": "3.0.2",
     "vue-splitpane": "1.0.4",

+ 2 - 2
src/api/qw/groupChat.js

@@ -31,10 +31,10 @@ export function cogradientGroupChat(corpId) {
 }
 
 
-export function listAll(qwUserIds, corpId) {
+export function listAll(qwUserIds, corpId, sopId) {
   return request({
     url: '/qw/groupChat/listAll',
     method: 'get',
-    params:{qwUserIds, corpId}
+    params:{qwUserIds, corpId, sopId}
   })
 }

+ 37 - 0
src/api/qw/groupChatTransfer.js

@@ -0,0 +1,37 @@
+import request from '@/utils/request'
+
+// 在职成员客户群列表
+export function listOnJob(params){
+  return request({
+    url: '/qw/groupChatTransfer/listOnJob',
+    method: 'get',
+    params
+  })
+}
+
+// 继承在职客户群
+export function transferOnJob(data) {
+  return request({
+    url: '/qw/groupChatTransfer/transferOnJob',
+    method: 'post',
+    data
+  })
+}
+
+// 离职成员客户群列表
+export function list(params){
+  return request({
+    url: '/qw/groupChatTransfer/list',
+    method: 'get',
+    params
+  })
+}
+
+// 继承离职客户群
+export function transfer(data) {
+  return request({
+    url: '/qw/groupChatTransfer/transfer',
+    method: 'post',
+    data
+  })
+}

+ 9 - 0
src/api/qw/groupChatTransferLog.js

@@ -0,0 +1,9 @@
+import request from '@/utils/request'
+
+export function list(params) {
+  return request({
+    url: '/qw/groupChatTransferLog/list',
+    method: 'get',
+    params
+  })
+}

+ 17 - 0
src/api/qw/sopUserLogs.js

@@ -60,3 +60,20 @@ export function repairSopUserLogs(data) {
     data: data
   })
 }
+
+// 修改sopUserLogs
+export function updateLogDate(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/updateLogDate',
+    method: 'post',
+    data: data
+  })
+}
+// 修改sopUserLogs
+export function addGroupChat(data) {
+  return request({
+    url: '/qwSop/sopUserLogs/addGroupChat',
+    method: 'post',
+    data: data
+  })
+}

+ 11 - 0
src/api/qw/user.js

@@ -349,3 +349,14 @@ export function getQwUserListLikeName(params) {
     params: params
   })
 }
+
+/**
+ * 企业微信员工账号 解除绑定 云主机
+ */
+export function qwRestartCloudHost(params) {
+  return request({
+    url: '/qw/user/restartHost',
+    method: 'put',
+    params: params
+  })
+}

+ 84 - 0
src/api/system/config.js

@@ -0,0 +1,84 @@
+import request from '@/utils/request'
+
+// 查询参数列表
+export function listConfig(query) {
+  return request({
+    url: '/system/config/list',
+    method: 'get',
+    params: query
+  })
+}
+
+// 查询参数详细
+export function getConfig(configId) {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'get'
+  })
+}
+
+export function getConfigByKey(configKey) {
+  return request({
+    url: '/system/config/getConfigByKey/' + configKey,
+    method: 'get'
+  })
+}
+// 根据参数键名查询参数值
+export function getConfigKey(configKey) {
+  return request({
+    url: '/system/config/configKey/' + configKey,
+    method: 'get'
+  })
+}
+
+// 新增参数配置
+export function addConfig(data) {
+  return request({
+    url: '/system/config',
+    method: 'post',
+    data: data
+  })
+}
+
+// 修改参数配置
+export function updateConfig(data) {
+  return request({
+    url: '/system/config',
+    method: 'put',
+    data: data
+  })
+}
+
+// 删除参数配置
+export function delConfig(configId) {
+  return request({
+    url: '/system/config/' + configId,
+    method: 'delete'
+  })
+}
+
+// 刷新参数缓存
+export function refreshCache() {
+  return request({
+    url: '/system/config/refreshCache',
+    method: 'delete'
+  })
+}
+
+// 导出参数
+export function exportConfig(query) {
+  return request({
+    url: '/system/config/export',
+    method: 'get',
+    params: query
+  })
+}
+
+
+export function updateConfigByKey(data) {
+  return request({
+    url: '/system/config/updateConfigByKey',
+    method: 'post',
+    data: data
+  })
+}

BIN
src/assets/images/cishu_views.png


BIN
src/assets/images/code.png


BIN
src/assets/images/customer.png


+ 39 - 0
src/assets/images/dark.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" width="48" height="40" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" width="48" height="40" transform="translate(-1190.000000, -136.000000)">
+            <g id="Group-8" width="48" height="40" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5-Copy-5" filter="url(#filter-1)" transform="translate(25.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                    <rect id="Rectangle-18" fill="#303648" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/images/default.jpg


BIN
src/assets/images/eyeoff.png


BIN
src/assets/images/eyeopen.png


BIN
src/assets/images/hongbao_views.png


+ 39 - 0
src/assets/images/light.svg

@@ -0,0 +1,39 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<svg width="52px" height="45px" viewBox="0 0 52 45" version="1.1" 
+    xmlns="http://www.w3.org/2000/svg" 
+    xmlns:xlink="http://www.w3.org/1999/xlink">
+    <defs>
+        <filter x="-9.4%" y="-6.2%" width="118.8%" height="122.5%" filterUnits="objectBoundingBox" id="filter-1">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="1" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.15 0" type="matrix" in="shadowBlurOuter1" result="shadowMatrixOuter1"></feColorMatrix>
+            <feMerge>
+                <feMergeNode in="shadowMatrixOuter1"></feMergeNode>
+                <feMergeNode in="SourceGraphic"></feMergeNode>
+            </feMerge>
+        </filter>
+        <rect id="path-2" x="0" y="0" width="48" height="40" rx="4"></rect>
+        <filter x="-4.2%" y="-2.5%" width="108.3%" height="110.0%" filterUnits="objectBoundingBox" id="filter-4">
+            <feOffset dx="0" dy="1" in="SourceAlpha" result="shadowOffsetOuter1"></feOffset>
+            <feGaussianBlur stdDeviation="0.5" in="shadowOffsetOuter1" result="shadowBlurOuter1"></feGaussianBlur>
+            <feColorMatrix values="0 0 0 0 0   0 0 0 0 0   0 0 0 0 0  0 0 0 0.1 0" type="matrix" in="shadowBlurOuter1"></feColorMatrix>
+        </filter>
+    </defs>
+    <g id="配置面板" stroke="none" stroke-width="1" fill="none" fill-rule="evenodd">
+        <g id="setting-copy-2" transform="translate(-1254.000000, -136.000000)">
+            <g id="Group-8" transform="translate(1167.000000, 0.000000)">
+                <g id="Group-5" filter="url(#filter-1)" transform="translate(89.000000, 137.000000)">
+                    <mask id="mask-3" fill="white">
+                        <use xlink:href="#path-2"></use>
+                    </mask>
+                    <g id="Rectangle-18">
+                        <use fill="black" fill-opacity="1" filter="url(#filter-4)" xlink:href="#path-2"></use>
+                        <use fill="#F0F2F5" fill-rule="evenodd" xlink:href="#path-2"></use>
+                    </g>
+                    <rect id="Rectangle-18" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="16" height="40"></rect>
+                    <rect id="Rectangle-11" fill="#FFFFFF" mask="url(#mask-3)" x="0" y="0" width="48" height="10"></rect>
+                </g>
+            </g>
+        </g>
+    </g>
+</svg>

BIN
src/assets/images/liuliang.png


BIN
src/assets/images/login-background.jpg


BIN
src/assets/images/login_bg.png


BIN
src/assets/images/login_left.png


BIN
src/assets/images/management_end_left_img.png


BIN
src/assets/images/member.png


BIN
src/assets/images/message.png


BIN
src/assets/images/number_views.png


BIN
src/assets/images/pass.png


BIN
src/assets/images/profile.jpg


BIN
src/assets/images/profile.png


BIN
src/assets/images/renshu_views.png


BIN
src/assets/images/salesperson.png


BIN
src/assets/images/tab_company.png


BIN
src/assets/images/tab_enterprise.png


BIN
src/assets/images/topbg.png


BIN
src/assets/images/user.png


BIN
src/assets/images/yunlian_logo.png


BIN
src/assets/images/zcgl_bg.png


+ 141 - 145
src/assets/styles/common.scss

@@ -1,276 +1,272 @@
- /**
- * 通用css样式布局处理
- * Copyright (c) 2019 fs
- */
+/**
+* 通用css样式布局处理
+*/
 
- /** 基础通用 **/
+/** 基础通用 **/
 .pt5 {
-	padding-top: 5px;
+  padding-top: 5px;
 }
 .pr5 {
-	padding-right: 5px;
+  padding-right: 5px;
 }
 .pb5 {
-	padding-bottom: 5px;
+  padding-bottom: 5px;
 }
 .mt5 {
-	margin-top: 5px;
+  margin-top: 5px;
 }
 .mr5 {
-	margin-right: 5px;
+  margin-right: 5px;
 }
 .mb5 {
-	margin-bottom: 5px;
+  margin-bottom: 5px;
 }
 .mb8 {
-	margin-bottom: 8px;
+  margin-bottom: 8px;
 }
 .ml5 {
-	margin-left: 5px;
+  margin-left: 5px;
 }
 .mt10 {
-	margin-top: 10px;
+  margin-top: 10px;
 }
 .mr10 {
-	margin-right: 10px;
+  margin-right: 10px;
 }
 .mb10 {
-	margin-bottom: 10px;
+  margin-bottom: 10px;
 }
 .ml0 {
-	margin-left: 10px;
+  margin-left: 10px;
 }
 .mt20 {
-	margin-top: 20px;
+  margin-top: 20px;
 }
 .mr20 {
-	margin-right: 20px;
+  margin-right: 20px;
 }
 .mb20 {
-	margin-bottom: 20px;
+  margin-bottom: 20px;
 }
 .m20 {
-	margin-left: 20px;
+  margin-left: 20px;
 }
 
-.el-dialog:not(.is-fullscreen){
-	margin-top: 6vh !important;
+.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: 6vh !important;
+}
+
+.el-dialog__wrapper.scrollbar .el-dialog .el-dialog__body {
+  overflow: auto;
+  overflow-x: hidden;
+  max-height: 70vh;
+  padding: 10px 20px 0;
 }
 
 .el-table {
-	.el-table__header-wrapper, .el-table__fixed-header-wrapper {
-		th {
-			word-break: break-word;
-			background-color: #f8f8f9;
-			color: #515a6e;
-			height: 40px;
-			font-size: 13px;
-		}
-	}
-	.el-table__body-wrapper {
-		.el-button [class*="el-icon-"] + span {
-			margin-left: 1px;
-		}
-	}
+  .el-table__header-wrapper, .el-table__fixed-header-wrapper {
+    th {
+      word-break: break-word;
+      background-color: #f8f8f9;
+      color: #515a6e;
+      height: 40px;
+      font-size: 13px;
+    }
+  }
+  .el-table__body-wrapper {
+    .el-button [class*="el-icon-"] + span {
+      margin-left: 1px;
+    }
+  }
 }
 
 /** 表单布局 **/
 .form-header {
-    font-size:15px;
-	color:#6379bb;
-	border-bottom:1px solid #ddd;
-	margin:8px 10px 25px 10px;
-	padding-bottom:5px
+  font-size:15px;
+  color:#6379bb;
+  border-bottom:1px solid #ddd;
+  margin:8px 10px 25px 10px;
+  padding-bottom:5px
 }
 
 /** 表格布局 **/
 .pagination-container {
-	position: relative;
-	height: 25px;
-	margin-bottom: 10px;
-	margin-top: 15px;
-	padding: 10px 20px !important;
+  position: relative;
+  height: 25px;
+  margin-bottom: 10px;
+  margin-top: 15px;
+  padding: 10px 20px !important;
 }
 
 /* tree border */
 .tree-border {
-    margin-top: 5px;
-    border: 1px solid #e5e6e7;
-    background: #FFFFFF none;
-    border-radius:4px;
+  margin-top: 5px;
+  border: 1px solid #e5e6e7;
+  background: #FFFFFF none;
+  border-radius:4px;
 }
 
 .pagination-container .el-pagination {
-	right: 0;
-	position: absolute;
+  right: 0;
+  position: absolute;
+}
+
+@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--mini {
-	color: #409EFF;
-	padding-left: 0;
-	padding-right: 0;
-	width: inherit;
+  padding-left: 0;
+  padding-right: 0;
+  width: inherit;
+}
+
+/** 表格更多操作下拉样式 */
+.el-table .el-dropdown-link {
+  cursor: pointer;
+  color: #409EFF;
+  margin-left: 5px;
+}
+
+.el-table .el-dropdown, .el-icon-arrow-down {
+  font-size: 12px;
 }
 
 .el-tree-node__content > .el-checkbox {
-	margin-right: 8px;
+  margin-right: 8px;
 }
 
 .list-group-striped > .list-group-item {
-	border-left: 0;
-	border-right: 0;
-	border-radius: 0;
-	padding-left: 0;
-	padding-right: 0;
+  border-left: 0;
+  border-right: 0;
+  border-radius: 0;
+  padding-left: 0;
+  padding-right: 0;
 }
 
 .list-group {
-	padding-left: 0px;
-	list-style: none;
+  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;
+  border-bottom: 1px solid #e7eaec;
+  border-top: 1px solid #e7eaec;
+  margin-bottom: -1px;
+  padding: 11px 0px;
+  font-size: 13px;
 }
 
 .pull-right {
-	float: right !important;
+  float: right !important;
 }
 
 .el-card__header {
-	padding: 14px 15px 7px;
-	min-height: 40px;
+  padding: 14px 15px 7px;
+  min-height: 40px;
 }
 
 .el-card__body {
-	padding: 15px 20px 20px 20px;
+  padding: 15px 20px 20px 20px;
 }
 
 .card-box {
-	padding-right: 15px;
-	padding-left: 15px;
-	margin-bottom: 10px;
+  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;
+  background: #20B2AA;
+  border-color: #20B2AA;
+  color: #FFFFFF;
 }
 
 .el-button--cyan:focus,
 .el-button--cyan:hover {
-//   background: #48D1CC;
-//   border-color: #48D1CC;
-//   color: #FFFFFF;
+  background: #48D1CC;
+  border-color: #48D1CC;
+  color: #FFFFFF;
 }
 
 .el-button--cyan {
-//   background-color: #20B2AA;
-//   border-color: #20B2AA;
-//   color: #FFFFFF;
+  background-color: #20B2AA;
+  border-color: #20B2AA;
+  color: #FFFFFF;
 }
 
 /* text color */
 .text-navy {
-	// color: #1ab394;
+  color: #1ab394;
 }
 
 .text-primary {
-	color: inherit;
+  color: inherit;
 }
 
 .text-success {
-	color: #1c84c6;
+  color: #1c84c6;
 }
 
 .text-info {
-	color: #23c6c8;
+  color: #23c6c8;
 }
 
 .text-warning {
-	color: #f8ac59;
+  color: #f8ac59;
 }
 
 .text-danger {
-	color: #ed5565;
+  color: #ed5565;
 }
 
 .text-muted {
-	color: #888888;
+  color: #888888;
 }
 
 /* image */
 .img-circle {
-	border-radius: 50%;
+  border-radius: 50%;
 }
 
 .img-lg {
-	width: 120px;
-	height: 120px;
+  width: 120px;
+  height: 120px;
 }
 
 .avatar-upload-preview {
-	position: absolute;
-	top: 50%;
-	transform: translate(50%, -50%);
-	width: 180px;
-	height: 180px;
-	border-radius: 50%;
-	box-shadow: 0 0 4px #ccc;
-	overflow: hidden;
+  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: .8;
-	color: #fff!important;
-	background: #42b983!important;
+  opacity: .8;
+  color: #fff!important;
+  background: #42b983!important;
 }
 
 .top-right-btn {
-	position: relative;
-	float: right;
-}
-
-/* submenu item */
-.el-menu--horizontal > .el-submenu .el-submenu__title {
-	height: 50px !important;
-	line-height: 50px !important;
+  position: relative;
+  float: right;
 }
-.el-drawer{
-    overflow: scroll
-}
-.avatar-uploader .el-upload {
-    border: 1px dashed #d9d9d9;
-    border-radius: 6px;
-    cursor: pointer;
-    position: relative;
-    overflow: hidden;
-  }
-  .avatar-uploader .el-upload:hover {
-    border-color: #409EFF;
-  }
-  .avatar-uploader-icon {
-    font-size: 28px;
-    color: #8c939d;
-    width: 110px;
-    height: 110px;
-    line-height: 110px;
-    text-align: center;
-  }
- .avatar {
-    width: 110px;
-    height: 110px;
-    display: block;
-  }
-  .el-tabs__item:focus.is-active.is-focus:not(:active){
-	  box-shadow: 0 0 2px 2px #fff inset;
-	  border-radius: 3px;
-  }

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

@@ -82,3 +82,11 @@
 .el-range-separator {
   box-sizing: content-box;
 }
+
+.el-menu--collapse
+> div
+> .el-submenu
+> .el-submenu__title
+.el-submenu__icon-arrow {
+  display: none;
+}

+ 1 - 1
src/assets/styles/element-variables.scss

@@ -4,7 +4,7 @@
 **/
 
 /* theme color */
-$--color-primary: #1aa4ff;
+$--color-primary: #1890ff;
 $--color-success: #13ce66;
 $--color-warning: #ffba00;
 $--color-danger: #ff4949;

+ 13 - 188
src/assets/styles/index.scss

@@ -97,8 +97,8 @@ div:focus {
 }
 
 aside {
-  //background: #eef1f6;
-  //padding: 8px 24px;
+  background: #eef1f6;
+  padding: 8px 24px;
   margin-bottom: 20px;
   border-radius: 2px;
   display: block;
@@ -118,22 +118,22 @@ aside {
     }
   }
 }
-.app-main{
-  background-color: #f0f2f5;
-}
+
 //main-container全局样式
 .app-container {
-  margin: 15px;
-  padding: 15px;
   background-color: #fff;
+  margin: 10px;
+
+  padding: 20px;
 }
+
 .components-container {
-  margin: 20px 20px;
+  margin: 30px 50px;
   position: relative;
 }
 
 .pagination-container {
-  margin-top: 5px;
+  margin-top: 30px;
 }
 
 .text-center {
@@ -175,12 +175,12 @@ aside {
 }
 
 .filter-container {
-  padding-bottom: 5px;
+  padding-bottom: 10px;
 
   .filter-item {
     display: inline-block;
     vertical-align: middle;
-    margin-bottom: 5px;
+    margin-bottom: 10px;
   }
 }
 
@@ -192,182 +192,7 @@ aside {
 .multiselect--active {
   z-index: 1000 !important;
 }
-
-.el-dialog {
-  position: absolute;
-  top: 50%;
-  left: 50%;
-  margin: 0 !important;
-  transform: translate(-50%, -50%);
-  max-height: calc(100% - 30px);
-  max-width: calc(100% - 30px);
-  display: flex;
-  flex-direction: column;
-  .el-dialog__body {
-    padding: 5px 20px;
-    overflow: auto;
-  }
-  .el-dialog-body-custom-height {
-    height: calc(100vh - 214px);
-  }
-}
-
-
-
-.acea-row {
-  display: -webkit-box;
-  display: -moz-box;
-  display: -webkit-flex;
-  display: -ms-flexbox;
-  display: flex;
-  -webkit-box-lines: multiple;
-  -moz-box-lines: multiple;
-  -o-box-lines: multiple;
-  -webkit-flex-wrap: wrap;
-  -ms-flex-wrap: wrap;
-  flex-wrap: wrap;
-  /* 辅助类 */
-}
-
-.acea-row.row-middle {
-  -webkit-box-align: center;
-  -moz-box-align: center;
-  -o-box-align: center;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-}
-
-.acea-row.row-top {
-  -webkit-box-align: start;
-  -moz-box-align: start;
-  -o-box-align: start;
-  -ms-flex-align: start;
-  -webkit-align-items: flex-start;
-  align-items: flex-start;
-}
-
-.acea-row.row-bottom {
-  -webkit-box-align: end;
-  -moz-box-align: end;
-  -o-box-align: end;
-  -ms-flex-align: end;
-  -webkit-align-items: flex-end;
-  align-items: flex-end;
-}
-
-.acea-row.row-center {
-  -webkit-box-pack: center;
-  -moz-box-pack: center;
-  -o-box-pack: center;
-  -ms-flex-pack: center;
-  -webkit-justify-content: center;
-  justify-content: center;
-}
-
-.acea-row.row-right {
-  -webkit-box-pack: end;
-  -moz-box-pack: end;
-  -o-box-pack: end;
-  -ms-flex-pack: end;
-  -webkit-justify-content: flex-end;
-  justify-content: flex-end;
-}
-
-.acea-row.row-left {
-  -webkit-box-pack: start;
-  -moz-box-pack: start;
-  -o-box-pack: start;
-  -ms-flex-pack: start;
-  -webkit-justify-content: flex-start;
-  justify-content: flex-start;
-}
-
-.acea-row.row-between {
-  -webkit-box-pack: justify;
-  -moz-box-pack: justify;
-  -o-box-pack: justify;
-  -ms-flex-pack: justify;
-  -webkit-justify-content: space-between;
-  justify-content: space-between;
-}
-
-.acea-row.row-around {
-  justify-content: space-around;
-  -webkit-justify-content: space-around;
+.el-dialog__footer{
+  border-top: 1px solid #DCDFE6;
 }
 
-.acea-row.row-column-around {
-  -webkit-flex-direction: column;
-  -ms-flex-direction: column;
-  flex-direction: column;
-  justify-content: space-around;
-  -webkit-justify-content: space-around;
-}
-
-.acea-row.row-column {
-  -webkit-box-orient: vertical;
-  -moz-box-orient: vertical;
-  -o-box-orient: vertical;
-  -webkit-flex-direction: column;
-  -ms-flex-direction: column;
-  flex-direction: column;
-}
-
-.acea-row.row-column-between {
-  -webkit-box-orient: vertical;
-  -moz-box-orient: vertical;
-  -o-box-orient: vertical;
-  -webkit-flex-direction: column;
-  -ms-flex-direction: column;
-  flex-direction: column;
-  -webkit-box-pack: justify;
-  -moz-box-pack: justify;
-  -o-box-pack: justify;
-  -ms-flex-pack: justify;
-  -webkit-justify-content: space-between;
-  justify-content: space-between;
-}
-
-/* 上下左右垂直居中 */
-.acea-row.row-center-wrapper {
-  -webkit-box-align: center;
-  -moz-box-align: center;
-  -o-box-align: center;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-  -webkit-box-pack: center;
-  -moz-box-pack: center;
-  -o-box-pack: center;
-  -ms-flex-pack: center;
-  -webkit-justify-content: center;
-  justify-content: center;
-}
-
-/* 上下两边居中对齐 */
-.acea-row.row-between-wrapper {
-  -webkit-box-align: center;
-  -moz-box-align: center;
-  -o-box-align: center;
-  -ms-flex-align: center;
-  -webkit-align-items: center;
-  align-items: center;
-  -webkit-box-pack: justify;
-  -moz-box-pack: justify;
-  -o-box-pack: justify;
-  -ms-flex-pack: justify;
-  -webkit-justify-content: space-between;
-  justify-content: space-between;
-}
- 
-
-.el-drawer__header{
-  margin-bottom: 20px;
-}
-.el-descriptions-item__label.is-bordered-label{
-  font-weight: normal;
-}
-
-
- 

+ 24 - 16
src/assets/styles/sidebar.scss

@@ -3,14 +3,15 @@
   .main-container {
     min-height: 100%;
     transition: margin-left .28s;
-    margin-left: $sideBarWidth;
+    margin-left: $base-sidebar-width;
     position: relative;
   }
 
   .sidebar-container {
+    -webkit-transition: width .28s;
     transition: width 0.28s;
-    width: $sideBarWidth !important;
-    background-color: $menuBg;
+    width: $base-sidebar-width !important;
+    background-color: $base-menu-background;
     height: 100%;
     position: fixed;
     font-size: 0px;
@@ -19,6 +20,8 @@
     left: 0;
     z-index: 1001;
     overflow: hidden;
+    -webkit-box-shadow: 2px 0 6px rgba(0,21,41,.35);
+    box-shadow: 2px 0 6px rgba(0,21,41,.35);
 
     // reset element-ui css
     .horizontal-collapse-transition {
@@ -73,21 +76,29 @@
     .submenu-title-noDropdown,
     .el-submenu__title {
       &:hover {
-        background-color: $menuHover !important;
+        background-color: rgba(0, 0, 0, 0.06) !important;
       }
     }
 
-    .is-active>.el-submenu__title {
-      color: $subMenuActiveText !important;
+    & .theme-dark .is-active > .el-submenu__title {
+      color: $base-menu-color-active !important;
     }
 
     & .nest-menu .el-submenu>.el-submenu__title,
     & .el-submenu .el-menu-item {
-      min-width: $sideBarWidth !important;
-      background-color: $subMenuBg !important;
+      min-width: $base-sidebar-width !important;
 
       &:hover {
-        background-color: $subMenuHover !important;
+        background-color: rgba(0, 0, 0, 0.06) !important;
+      }
+    }
+
+    & .theme-dark .nest-menu .el-submenu>.el-submenu__title,
+    & .theme-dark .el-submenu .el-menu-item {
+      background-color: $base-sub-menu-background !important;
+
+      &:hover {
+        background-color: $base-sub-menu-hover !important;
       }
     }
   }
@@ -124,9 +135,6 @@
           margin-left: 20px;
         }
 
-        .el-submenu__icon-arrow {
-          display: none;
-        }
       }
     }
 
@@ -146,7 +154,7 @@
   }
 
   .el-menu--collapse .el-menu .el-submenu {
-    min-width: $sideBarWidth !important;
+    min-width: $base-sidebar-width !important;
   }
 
   // mobile responsive
@@ -157,14 +165,14 @@
 
     .sidebar-container {
       transition: transform .28s;
-      width: $sideBarWidth !important;
+      width: $base-sidebar-width !important;
     }
 
     &.hideSidebar {
       .sidebar-container {
         pointer-events: none;
         transition-duration: 0.3s;
-        transform: translate3d(-$sideBarWidth, 0, 0);
+        transform: translate3d(-$base-sidebar-width, 0, 0);
       }
     }
   }
@@ -190,7 +198,7 @@
   .el-menu-item {
     &:hover {
       // you can use $subMenuHover
-      background-color: $menuHover !important;
+      background-color: rgba(0, 0, 0, 0.06) !important;
     }
   }
 

+ 53 - 47
src/assets/styles/transition.scss

@@ -1,48 +1,54 @@
-// global transition css
-
-/* fade */
-.fade-enter-active,
-.fade-leave-active {
-  transition: opacity 0.28s;
-}
-
-.fade-enter,
-.fade-leave-active {
-  opacity: 0;
-}
-
-/* fade-transform */
-.fade-transform-leave-active,
-.fade-transform-enter-active {
-  transition: all .5s;
-}
-
-.fade-transform-enter {
-  opacity: 0;
-  transform: translateX(-30px);
-}
-
-.fade-transform-leave-to {
-  opacity: 0;
-  transform: translateX(30px);
-}
-
-/* breadcrumb transition */
-.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;
+// base color
+$blue:#324157;
+$light-blue:#006CFF;
+$red:#C03639;
+$pink: #E65D6E;
+$green: #30B08F;
+$tiffany: #4AB7BD;
+$yellow:#FEC171;
+$panGreen: #30B08F;
+
+// 默认菜单主题风格
+$base-menu-color:#bfcbd9;
+$base-menu-color-active:#f4f4f5;
+$base-menu-background:#07052F;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color:rgba(0,0,0,.70);
+$base-menu-light-background:#ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background:#1f2d3d;
+$base-sub-menu-hover:#001528;
+
+// 自定义暗色菜单风格
+/**
+$base-menu-color:hsla(0,0%,100%,.65);
+$base-menu-color-active:#fff;
+$base-menu-background:#001529;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color:rgba(0,0,0,.70);
+$base-menu-light-background:#ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background:#000c17;
+$base-sub-menu-hover:#001528;
+*/
+
+$base-sidebar-width: 200px;
+
+// the :export directive is the magic sauce for webpack
+// https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
+:export {
+  menuColor: $base-menu-color;
+  menuLightColor: $base-menu-light-color;
+  menuColorActive: $base-menu-color-active;
+  menuBackground: $base-menu-background;
+  menuLightBackground: $base-menu-light-background;
+  subMenuBackground: $base-sub-menu-background;
+  subMenuHover: $base-sub-menu-hover;
+  sideBarWidth: $base-sidebar-width;
+  logoTitleColor: $base-logo-title-color;
+  logoLightTitleColor: $base-logo-light-title-color
 }

+ 37 - 18
src/assets/styles/variables.scss

@@ -1,6 +1,6 @@
 // base color
 $blue:#324157;
-$light-blue:#3A71A8;
+$light-blue: #3a71a8;
 $red:#C03639;
 $pink: #E65D6E;
 $green: #30B08F;
@@ -8,28 +8,47 @@ $tiffany: #4AB7BD;
 $yellow:#FEC171;
 $panGreen: #30B08F;
 
-// sidebar
-$menuText:#bfcbd9;
-$menuActiveText:#409EFF;
-$subMenuActiveText:#f4f4f5; // https://github.com/ElemeFE/element/issues/12951
+// 默认菜单主题风格
+$base-menu-color:#bfcbd9;
+$base-menu-color-active:#f4f4f5;
+$base-menu-background: #304156;
+$base-logo-title-color: #ffffff;
 
-$menuBg:#304156;
-$menuHover:#263445;
+$base-menu-light-color:rgba(0,0,0,.70);
+$base-menu-light-background:#ffffff;
+$base-logo-light-title-color: #001529;
 
-$subMenuBg:#1f2d3d;
-$subMenuHover:#001528;
+$base-sub-menu-background:#1f2d3d;
+$base-sub-menu-hover:#001528;
 
-$sideBarWidth: 200px;
+// 自定义暗色菜单风格
+/**
+$base-menu-color:hsla(0,0%,100%,.65);
+$base-menu-color-active:#fff;
+$base-menu-background:#001529;
+$base-logo-title-color: #ffffff;
+
+$base-menu-light-color:rgba(0,0,0,.70);
+$base-menu-light-background:#ffffff;
+$base-logo-light-title-color: #001529;
+
+$base-sub-menu-background:#000c17;
+$base-sub-menu-hover:#001528;
+*/
+
+$base-sidebar-width: 200px;
 
 // the :export directive is the magic sauce for webpack
 // https://www.bluematador.com/blog/how-to-share-variables-between-js-and-sass
 :export {
-  menuText: $menuText;
-  menuActiveText: $menuActiveText;
-  subMenuActiveText: $subMenuActiveText;
-  menuBg: $menuBg;
-  menuHover: $menuHover;
-  subMenuBg: $subMenuBg;
-  subMenuHover: $subMenuHover;
-  sideBarWidth: $sideBarWidth;
+  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
 }

+ 1 - 1
src/components/Breadcrumb/index.vue

@@ -45,7 +45,7 @@ export default {
       if (!name) {
         return false
       }
-      return name.trim() === '首页'
+      return name.trim() === 'Index'
     },
     handleLink(item) {
       const { redirect, path } = item

+ 179 - 0
src/components/Crontab/day.vue

@@ -0,0 +1,179 @@
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				日,允许的通配符[, - * / L M]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				不指定
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				周期从
+				<el-input-number v-model='cycle01' :min="0" :max="31" /> -
+				<el-input-number v-model='cycle02' :min="0" :max="31" /> 日
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				从
+				<el-input-number v-model='average01' :min="0" :max="31" /> 号开始,每
+				<el-input-number v-model='average02' :min="0" :max="31" /> 日执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="5">
+				每月
+				<el-input-number v-model='workday' :min="0" :max="31" /> 号最近的那个工作日
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="6">
+				本月最后一天
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="7">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 31" :key="item" :value="item">{{item}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			workday: 1,
+			cycle01: 1,
+			cycle02: 2,
+			average01: 1,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-day',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			('day rachange');
+			if (this.radioValue === 1) {
+				this.$emit('update', 'day', '*', 'day');
+				this.$emit('update', 'week', '?', 'day');
+				this.$emit('update', 'month', '*', 'day');
+			} else {
+				if (this.cron.hour === '*') {
+					this.$emit('update', 'hour', '0', 'day');
+				}
+				if (this.cron.min === '*') {
+					this.$emit('update', 'min', '0', 'day');
+				}
+				if (this.cron.second === '*') {
+					this.$emit('update', 'second', '0', 'day');
+				}
+			}
+
+			switch (this.radioValue) {
+				case 2:
+					this.$emit('update', 'day', '?');
+					break;
+				case 3:
+					this.$emit('update', 'day', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 4:
+					this.$emit('update', 'day', this.average01 + '/' + this.average02);
+					break;
+				case 5:
+					this.$emit('update', 'day', this.workday + 'W');
+					break;
+				case 6:
+					this.$emit('update', 'day', 'L');
+					break;
+				case 7:
+					this.$emit('update', 'day', this.checkboxString);
+					break;
+			}
+			('day rachange end');
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'day', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'day', this.averageTotal);
+			}
+		},
+		// 最近工作日值变化时
+		workdayChange() {
+			if (this.radioValue == '5') {
+				this.$emit('update', 'day', this.workday + 'W');
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '7') {
+				this.$emit('update', 'day', this.checkboxString);
+			}
+		},
+		// 父组件传递的week发生变化触发
+		weekChange() {
+			//判断week值与day不能同时为“?”
+			if (this.cron.week == '?' && this.radioValue == '2') {
+				this.radioValue = '1';
+			} else if (this.cron.week !== '?' && this.radioValue != '2') {
+				this.radioValue = '2';
+			}
+		},
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'workdayCheck': 'workdayChange',
+		'checkboxString': 'checkboxChange',
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 1, 31)
+			this.cycle02 = this.checkNum(this.cycle02, 1, 31)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 1, 31)
+			this.average02 = this.checkNum(this.average02, 1, 31)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算工作日格式
+		workdayCheck: function () {
+			this.workday = this.checkNum(this.workday, 1, 31)
+			return this.workday;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 122 - 0
src/components/Crontab/hour.vue

@@ -0,0 +1,122 @@
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				小时,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				周期从
+				<el-input-number v-model='cycle01' :min="0" :max="60" /> -
+				<el-input-number v-model='cycle02' :min="0" :max="60" /> 小时
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				从
+				<el-input-number v-model='average01' :min="0" :max="60" /> 小时开始,每
+				<el-input-number v-model='average02' :min="0" :max="60" /> 小时执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			cycle01: 0,
+			cycle02: 1,
+			average01: 0,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-hour',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			if (this.radioValue === 1) {
+				this.$emit('update', 'hour', '*', 'hour');
+				this.$emit('update', 'day', '*', 'hour');
+			} else {
+				if (this.cron.min === '*') {
+					this.$emit('update', 'min', '0', 'hour');
+				}
+				if (this.cron.second === '*') {
+					this.$emit('update', 'second', '0', 'hour');
+				}
+			}
+			switch (this.radioValue) {
+				case 2:
+					this.$emit('update', 'hour', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 3:
+					this.$emit('update', 'hour', this.average01 + '/' + this.average02);
+					break;
+				case 4:
+					this.$emit('update', 'hour', this.checkboxString);
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '2') {
+				this.$emit('update', 'hour', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'hour', this.averageTotal);
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'hour', this.checkboxString);
+			}
+		}
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange'
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 0, 23)
+			this.cycle02 = this.checkNum(this.cycle02, 0, 23)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 0, 23)
+			this.average02 = this.checkNum(this.average02, 1, 23)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 425 - 0
src/components/Crontab/index.vue

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

+ 120 - 0
src/components/Crontab/min.vue

@@ -0,0 +1,120 @@
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				分钟,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				周期从
+				<el-input-number v-model='cycle01' :min="0" :max="60" /> -
+				<el-input-number v-model='cycle02' :min="0" :max="60" /> 分钟
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				从
+				<el-input-number v-model='average01' :min="0" :max="60" /> 分钟开始,每
+				<el-input-number v-model='average02' :min="0" :max="60" /> 分钟执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			cycle01: 1,
+			cycle02: 2,
+			average01: 0,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-min',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			if (this.radioValue !== 1 && this.cron.second === '*') {
+				this.$emit('update', 'second', '0', 'min');
+			}
+			switch (this.radioValue) {
+				case 1:
+					this.$emit('update', 'min', '*', 'min');
+					this.$emit('update', 'hour', '*', 'min');
+					break;
+				case 2:
+					this.$emit('update', 'min', this.cycle01 + '-' + this.cycle02, 'min');
+					break;
+				case 3:
+					this.$emit('update', 'min', this.average01 + '/' + this.average02, 'min');
+					break;
+				case 4:
+					this.$emit('update', 'min', this.checkboxString, 'min');
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '2') {
+				this.$emit('update', 'min', this.cycleTotal, 'min');
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'min', this.averageTotal, 'min');
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'min', this.checkboxString, 'min');
+			}
+		},
+
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange',
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 0, 59)
+			this.cycle02 = this.checkNum(this.cycle02, 0, 59)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 0, 59)
+			this.average02 = this.checkNum(this.average02, 1, 59)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 128 - 0
src/components/Crontab/month.vue

@@ -0,0 +1,128 @@
+<template>
+	<el-form size='small'>
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				月,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				周期从
+				<el-input-number v-model='cycle01' :min="1" :max="12" /> -
+				<el-input-number v-model='cycle02' :min="1" :max="12" /> 月
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				从
+				<el-input-number v-model='average01' :min="1" :max="12" /> 月开始,每
+				<el-input-number v-model='average02' :min="1" :max="12" /> 月月执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 12" :key="item" :value="item">{{item}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			cycle01: 1,
+			cycle02: 2,
+			average01: 1,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.check
+		}
+	},
+	name: 'crontab-month',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			if (this.radioValue === 1) {
+				this.$emit('update', 'month', '*');
+				this.$emit('update', 'year', '*');
+			} else {
+				if (this.cron.day === '*') {
+					this.$emit('update', 'day', '0', 'month');
+				}
+				if (this.cron.hour === '*') {
+					this.$emit('update', 'hour', '0', 'month');
+				}
+				if (this.cron.min === '*') {
+					this.$emit('update', 'min', '0', 'month');
+				}
+				if (this.cron.second === '*') {
+					this.$emit('update', 'second', '0', 'month');
+				}
+			}
+			switch (this.radioValue) {
+				case 2:
+					this.$emit('update', 'month', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 3:
+					this.$emit('update', 'month', this.average01 + '/' + this.average02);
+					break;
+				case 4:
+					this.$emit('update', 'month', this.checkboxString);
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '2') {
+				this.$emit('update', 'month', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'month', this.averageTotal);
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'month', this.checkboxString);
+			}
+		}
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange'
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 1, 12)
+			this.cycle02 = this.checkNum(this.cycle02, 1, 12)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 1, 12)
+			this.average02 = this.checkNum(this.average02, 1, 12)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 566 - 0
src/components/Crontab/result.vue

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

+ 133 - 0
src/components/Crontab/second.vue

@@ -0,0 +1,133 @@
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				秒,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				周期从
+				<el-input-number v-model='cycle01' :min="0" :max="60" /> -
+				<el-input-number v-model='cycle02' :min="0" :max="60" /> 秒
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				从
+				<el-input-number v-model='average01' :min="0" :max="60" /> 秒开始,每
+				<el-input-number v-model='average02' :min="0" :max="60" /> 秒执行一次
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="item in 60" :key="item" :value="item-1">{{item-1}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 1,
+			cycle01: 1,
+			cycle02: 2,
+			average01: 0,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-second',
+	props: ['check', 'radioParent'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			switch (this.radioValue) {
+				case 1:
+					this.$emit('update', 'second', '*', 'second');
+					this.$emit('update', 'min', '*', 'second');
+					break;
+				case 2:
+					this.$emit('update', 'second', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 3:
+					this.$emit('update', 'second', this.average01 + '/' + this.average02);
+					break;
+				case 4:
+					this.$emit('update', 'second', this.checkboxString);
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '2') {
+				this.$emit('update', 'second', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'second', this.averageTotal);
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'second', this.checkboxString);
+			}
+		},
+		othChange() {
+			// 反解析
+			let ins = this.cron.second
+			('反解析 second', ins);
+			if (ins === '*') {
+				this.radioValue = 1;
+			} else if (ins.indexOf('-') > -1) {
+				this.radioValue = 2
+			} else if (ins.indexOf('/') > -1) {
+				this.radioValue = 3
+			} else {
+				this.radioValue = 4
+				this.checkboxList = ins.split(',')
+			}
+		}
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange',
+		radioParent() {
+			this.radioValue = this.radioParent
+		}
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 0, 59)
+			this.cycle02 = this.checkNum(this.cycle02, 0, 59)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 0, 59)
+			this.average02 = this.checkNum(this.average02, 1, 59)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 167 - 0
src/components/Crontab/week.vue

@@ -0,0 +1,167 @@
+<template>
+	<el-form size='small'>
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="1">
+				周,允许的通配符[, - * / L #]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="2">
+				不指定
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="3">
+				周期从星期
+				<el-input-number v-model='cycle01' :min="1" :max="7" /> -
+				<el-input-number v-model='cycle02' :min="1" :max="7" />
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="4">
+				第
+				<el-input-number v-model='average01' :min="1" :max="4" /> 周的星期
+				<el-input-number v-model='average02' :min="1" :max="7" />
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="5">
+				本月最后一个星期
+				<el-input-number v-model='weekday' :min="1" :max="7" />
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio v-model='radioValue' :label="6">
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple style="width:100%">
+					<el-option v-for="(item,index) of weekList" :key="index" :value="index+1">{{item}}</el-option>
+				</el-select>
+			</el-radio>
+		</el-form-item>
+
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			radioValue: 2,
+			weekday: 1,
+			cycle01: 1,
+			cycle02: 2,
+			average01: 1,
+			average02: 1,
+			checkboxList: [],
+			weekList: ['周一', '周二', '周三', '周四', '周五', '周六', '周日'],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-week',
+	props: ['check', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			if (this.radioValue === 1) {
+				this.$emit('update', 'week', '*');
+				this.$emit('update', 'year', '*');
+			} else {
+				if (this.cron.month === '*') {
+					this.$emit('update', 'month', '0', 'week');
+				}
+				if (this.cron.day === '*') {
+					this.$emit('update', 'day', '0', 'week');
+				}
+				if (this.cron.hour === '*') {
+					this.$emit('update', 'hour', '0', 'week');
+				}
+				if (this.cron.min === '*') {
+					this.$emit('update', 'min', '0', 'week');
+				}
+				if (this.cron.second === '*') {
+					this.$emit('update', 'second', '0', 'week');
+				}
+			}
+			switch (this.radioValue) {
+				case 2:
+					this.$emit('update', 'week', '?');
+					break;
+				case 3:
+					this.$emit('update', 'week', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 4:
+					this.$emit('update', 'week', this.average01 + '#' + this.average02);
+					break;
+				case 5:
+					this.$emit('update', 'week', this.weekday + 'L');
+					break;
+				case 6:
+					this.$emit('update', 'week', this.checkboxString);
+					break;
+			}
+		},
+		// 根据互斥事件,更改radio的值
+
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'week', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'week', this.averageTotal);
+			}
+		},
+		// 最近工作日值变化时
+		weekdayChange() {
+			if (this.radioValue == '5') {
+				this.$emit('update', 'week', this.weekday + 'L');
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '6') {
+				this.$emit('update', 'week', this.checkboxString);
+			}
+		},
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'weekdayCheck': 'weekdayChange',
+		'checkboxString': 'checkboxChange',
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, 1, 7)
+			this.cycle02 = this.checkNum(this.cycle02, 1, 7)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, 1, 4)
+			this.average02 = this.checkNum(this.average02, 1, 7)
+			return this.average01 + '#' + this.average02;
+		},
+		// 最近的工作日(格式)
+		weekdayCheck: function () {
+			this.weekday = this.checkNum(this.weekday, 1, 7)
+			return this.weekday;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str == '' ? '*' : str;
+		}
+	}
+}
+</script>

+ 144 - 0
src/components/Crontab/year.vue

@@ -0,0 +1,144 @@
+<template>
+	<el-form size="small">
+		<el-form-item>
+			<el-radio :label="1" v-model='radioValue'>
+				不填,允许的通配符[, - * /]
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio :label="2" v-model='radioValue'>
+				每年
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio :label="3" v-model='radioValue'>
+				周期从
+				<el-input-number v-model='cycle01' :min='fullYear' /> -
+				<el-input-number v-model='cycle02' :min='fullYear' />
+			</el-radio>
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio :label="4" v-model='radioValue'>
+				从
+				<el-input-number v-model='average01' :min='fullYear' /> 年开始,每
+				<el-input-number v-model='average02' :min='fullYear' /> 年执行一次
+			</el-radio>
+
+		</el-form-item>
+
+		<el-form-item>
+			<el-radio :label="5" v-model='radioValue'>
+				指定
+				<el-select clearable v-model="checkboxList" placeholder="可多选" multiple>
+					<el-option v-for="item in 9" :key="item" :value="item - 1 + fullYear" :label="item -1 + fullYear" />
+				</el-select>
+			</el-radio>
+		</el-form-item>
+	</el-form>
+</template>
+
+<script>
+export default {
+	data() {
+		return {
+			fullYear: 0,
+			radioValue: 1,
+			cycle01: 0,
+			cycle02: 0,
+			average01: 0,
+			average02: 1,
+			checkboxList: [],
+			checkNum: this.$options.propsData.check
+		}
+	},
+	name: 'crontab-year',
+	props: ['check', 'month', 'cron'],
+	methods: {
+		// 单选按钮值变化时
+		radioChange() {
+			if (this.cron.month === '*') {
+				this.$emit('update', 'month', '0', 'year');
+			}
+			if (this.cron.day === '*') {
+				this.$emit('update', 'day', '0', 'year');
+			}
+			if (this.cron.hour === '*') {
+				this.$emit('update', 'hour', '0', 'year');
+			}
+			if (this.cron.min === '*') {
+				this.$emit('update', 'min', '0', 'year');
+			}
+			if (this.cron.second === '*') {
+				this.$emit('update', 'second', '0', 'year');
+			}
+			switch (this.radioValue) {
+				case 1:
+					this.$emit('update', 'year', '');
+					break;
+				case 2:
+					this.$emit('update', 'year', '*');
+					break;
+				case 3:
+					this.$emit('update', 'year', this.cycle01 + '-' + this.cycle02);
+					break;
+				case 4:
+					this.$emit('update', 'year', this.average01 + '/' + this.average02);
+					break;
+				case 5:
+					this.$emit('update', 'year', this.checkboxString);
+					break;
+			}
+		},
+		// 周期两个值变化时
+		cycleChange() {
+			if (this.radioValue == '3') {
+				this.$emit('update', 'year', this.cycleTotal);
+			}
+		},
+		// 平均两个值变化时
+		averageChange() {
+			if (this.radioValue == '4') {
+				this.$emit('update', 'year', this.averageTotal);
+			}
+		},
+		// checkbox值变化时
+		checkboxChange() {
+			if (this.radioValue == '5') {
+				this.$emit('update', 'year', this.checkboxString);
+			}
+		}
+	},
+	watch: {
+		"radioValue": "radioChange",
+		'cycleTotal': 'cycleChange',
+		'averageTotal': 'averageChange',
+		'checkboxString': 'checkboxChange'
+	},
+	computed: {
+		// 计算两个周期值
+		cycleTotal: function () {
+			this.cycle01 = this.checkNum(this.cycle01, this.fullYear, this.fullYear + 100)
+			this.cycle02 = this.checkNum(this.cycle02, this.fullYear + 1, this.fullYear + 101)
+			return this.cycle01 + '-' + this.cycle02;
+		},
+		// 计算平均用到的值
+		averageTotal: function () {
+			this.average01 = this.checkNum(this.average01, this.fullYear, this.fullYear + 100)
+			this.average02 = this.checkNum(this.average02, 1, 10)
+			return this.average01 + '/' + this.average02;
+		},
+		// 计算勾选的checkbox值合集
+		checkboxString: function () {
+			let str = this.checkboxList.join();
+			return str;
+		}
+	},
+	mounted: function () {
+		// 仅获取当前年份
+		this.fullYear = Number(new Date().getFullYear());
+	}
+}
+</script>

+ 1 - 1
src/components/DeviceInfo/Pressure.vue

@@ -25,7 +25,7 @@
                 <el-table-column label="dbp(mmgh)" align="center" prop="dbp" />
                 <el-table-column label="hr(次/分)" align="center" prop="hr" />
             </el-table>
-            <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+            <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
         </div>
     </div>
 </template>

+ 1 - 1
src/components/DeviceInfo/Pulse.vue

@@ -71,7 +71,7 @@
           <el-table-column label="时间" align="center" prop="time" />
           <el-table-column label="脉搏" align="center" prop="pulseRate" />
         </el-table>
-        <pagination v-show="(dataListTotal>0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
+        <pagination v-show="(dataListTotal > 0) && detailsType" :total="dataListTotal" :page.sync="tqueryParams.pageNum" :limit.sync="tqueryParams.pageSize" @pagination="getTableList"/>
       </div>
     </div>
   </template>

+ 183 - 13
src/components/DeviceInfo/SettingDialog/SetInfoDialog.vue

@@ -2,6 +2,7 @@
   <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body :close-on-click-modal="false">
     <el-form ref="form" :model="form" :rules="rules" label-width="80px">
       <el-row>
+        
         <el-col :span="24"
           v-if="type != 'goal' && type != 'languageSet' && type != 'messageSend' && type != 'fallcheckSensitivity' && type != 'measureIntervalHr' && type != 'measureIntervalOther' && type != 'phonebookSync'">
           <el-form-item label="开关设置" prop="flag">
@@ -9,6 +10,51 @@
             </el-switch>
           </el-form-item>
         </el-col>
+        <el-col :span="24" v-if="type == 'medication' && form.flag">
+          <el-form-item label="开始提醒时间" prop="medicationStart" label-width="110px">
+            <el-time-picker
+              value-format="HH:mm:ss"
+              v-model="form.medicationStart"
+              placeholder="请选择开始时间"
+              :picker-options="{
+                selectableRange: '00:00:00 - 23:59:59'
+              }"
+            ></el-time-picker>
+          </el-form-item>
+          <el-form-item label="结束提醒时间" prop="medicationEnd" label-width="110px">
+            <el-time-picker
+              value-format="HH:mm:ss"
+              v-model="form.medicationEnd"
+              placeholder="请选择结束时间"
+              :picker-options="{
+                selectableRange: '00:00:00 - 23:59:59'
+              }"
+            ></el-time-picker>
+          </el-form-item>
+        </el-col>
+        <el-col :span="24" v-if="type == 'study' && form.flag">
+          <el-form-item label="开始提醒时间" prop="studyStart" label-width="110px">
+            <el-time-picker
+              value-format="HH:mm:ss"
+              v-model="form.studyStart"
+              placeholder="请选择开始时间"
+              :picker-options="{
+                selectableRange: '00:00:00 - 23:59:59'
+              }"
+            ></el-time-picker>
+          </el-form-item>
+          <el-form-item label="结束提醒时间" prop="studyEnd" label-width="110px">
+            <el-time-picker
+              value-format="HH:mm:ss"
+              v-model="form.studyEnd"
+              placeholder="请选择结束时间"
+              :picker-options="{
+                selectableRange: '00:00:00 - 23:59:59'
+              }"
+            ></el-time-picker>
+          </el-form-item>
+        </el-col>
+
         <el-col :span="24" v-if="type == 'autolocate' || type == 'datafreq'">
           <el-form-item label="设备省电设置" prop="mode" label-width="105px">
             <el-radio-group v-model="form.mode">
@@ -257,6 +303,8 @@ import {
   fallcheck,
   autolocate,
   datafreq,
+  medication,
+  study,
   lcdgesture,
   hralarm,
   spo2alarm,
@@ -268,7 +316,8 @@ import {
   messageSend,
   fallcheckSensitivity,
   measureIntervalHr,
-  measureIntervalOther
+  measureIntervalOther,
+  getSet
 } from "@/api/watch/deviceInfoSet.js";
 
 export default {
@@ -280,6 +329,50 @@ export default {
         callback();
       }
     };
+    const checkStudyTimes = (rule, value, callback, type) => {
+      if (!value) {
+        return callback(new Error('时间不能为空'));
+      }
+
+      const start = this.form.studyStart;
+      const end = this.form.studyEnd;
+
+      if (type === 'start') {
+        if (end && start && new Date(start) >= new Date(end)) {
+          callback(new Error('开始时间必须小于结束时间'));
+        } else {
+          callback();
+        }
+      } else if (type === 'end') {
+        if (start && new Date(start) > new Date(end)) {
+          callback(new Error('结束时间必须大于开始时间'));
+        } else {
+          callback();
+        }
+      }
+    };
+    const checkMedicationTimes = (rule, value, callback, type) => {
+      if (!value) {
+        return callback(new Error('时间不能为空'));
+      }
+
+      const start = this.form.medicationStart;
+      const end = this.form.medicationEnd;
+
+      if (type === 'start') {
+        if (end && start && new Date(start) >= new Date(end)) {
+          callback(new Error('开始时间必须小于结束时间'));
+        } else {
+          callback();
+        }
+      } else if (type === 'end') {
+        if (start && new Date(start) > new Date(end)) {
+          callback(new Error('结束时间必须大于开始时间'));
+        } else {
+          callback();
+        }
+      }
+    };
     //  检查翻腕亮屏开始时间
     const checkStart = (rule, value, callback) => {
       if (value && this.form.end) {
@@ -475,6 +568,12 @@ export default {
       open: false,
       // 表单参数
       form: {},
+      //
+      param:{
+        flag:null,
+        start:null,
+        end:null
+      },
       // 表单校验
       rules: {
         time: [
@@ -488,6 +587,22 @@ export default {
           { required: true, message: "结束时间不能为空", trigger: ["blur", "change"] },
           { validator: checkEnd, trigger: ["blur", "change"] }
         ],
+        medicationStart: [
+          { required: true, message: "开始时间不能为空", trigger: "blur" },
+          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'start'), trigger: "blur" }
+        ],
+        medicationEnd: [
+          { required: true, message: "结束时间不能为空", trigger: "blur" },
+          { validator: (rule, value, callback) => checkMedicationTimes(rule, value, callback, 'end'), trigger: "blur" }
+        ],
+        studyStart: [
+          { required: true, message: "看课提醒开始时间不能为空", trigger: "blur" },
+          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'start'), trigger: "blur" }
+        ],
+        studyEnd: [
+          { required: true, message: "看课提醒结束时间不能为空", trigger: "blur" },
+          { validator: (rule, value, callback) => checkStudyTimes(rule, value, callback, 'end'), trigger: "blur" }
+        ],
         high: [
           { required: true, message: "正常心率最高值不能为空", trigger: ["blur", "change"] },
           { validator: checkHigh, trigger: ["blur", "change"] }
@@ -617,11 +732,14 @@ export default {
         description: undefined, // 发送设备消息 描述
         fallThreshold: 14000, // 跌倒检测的灵敏度,默认14000
         measureIntervalHr: undefined, // 心率数据测量间隔设置 分钟
+        medicationStart: undefined, // 用药提醒的开始时间
+        medicationEnd: undefined, // 用药提醒的结束时间
+        studyStart: undefined, // 看课提醒的开始时间
+        studyEnd: undefined, // 看课提醒的结束时间
       };
       this.resetForm("form");
       this.btnLoading = false
     },
-    /** 修改按钮操作 */
     handleSet(deviceid, deviceNumber, item) {
       this.reset();
       this.form.deviceid = deviceid
@@ -630,10 +748,35 @@ export default {
       this.title = item.label;
       this.type = item.eventName
     },
+    handleTimeSet(item) {
+      console.log('设置用药提醒发送时间');
+      //查询对应设置
+      this.reset();
+      this.type = item.eventName;
+      this.open = true;
+      getSet({type:this.type}).then(response => {
+        this.param = response.data;
+        console.log("==========================",JSON.stringify(this.param,null,2));
+        this.form.flag = this.param.flag;
+        console.log("==========================开关:"+ this.form.flag);
+        this.title = item.label;
+        if(this.type == 'medication'){
+          this.form.medicationStart = this.param.start;
+          this.form.medicationEnd = this.param.end;
+        } else if(this.type == 'study'){
+          this.form.studyStart = this.param.start;
+          this.form.studyEnd = this.param.end;
+          console.log("==========================开关:"+ JSON.stringify(this.form,null,2));
+        }
+      });
+      
+    },
     /** 提交按钮 */
     submitForm() {
       this.$refs["form"].validate((valid) => {
-        if (valid) {
+        if(this.type=='medication' || this.type=='study'){
+          this.confirmFun()
+        } else if (valid) {
           if (this.form.deviceNumber != undefined) {
             this.confirmFun()
           } else {
@@ -648,16 +791,31 @@ export default {
         fallcheck: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的跌倒检测?`,
         autolocate: `是否确认${flagTxt}设备信息编号为${this.form.deviceNumber}的自动定位?`,
       }
-      this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
-        confirmButtonText: "确定",
-        cancelButtonText: "取消",
-        type: "warning"
-      }).then(() => {
-        this.handleFun()
-      }).then(() => {
-        this.open = false
-        // this.$emit("refreshList")
-      }).catch(() => { });
+      if(this.type=='medication' || this.type=='study'){
+        this.$confirm(`是否确认对所有设备进行以上操作?`, "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.handleFun()
+        }).then(() => {
+          this.open = false
+          // this.$emit("refreshList")
+        }).catch(() => { });
+        
+      } else {
+        this.$confirm(typeTxt[this.type] || `是否确认对设备id为${this.form.deviceNumber}进行以上操作?`, "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.handleFun()
+        }).then(() => {
+          this.open = false
+          // this.$emit("refreshList")
+        }).catch(() => { });
+        }
+      
     },
     handleFun() {
       const options = {
@@ -680,6 +838,18 @@ export default {
           time: this.form.time,
           mode: this.form.mode
         }),
+        // 用药提醒设置
+        medication: () => medication({
+          flag: this.form.flag,
+          start: this.form.flag ? this.form.medicationStart : undefined,
+          end: this.form.flag ? this.form.medicationEnd : undefined
+        }),
+        // 看课提醒设置
+        study: () => study({
+          flag: this.form.flag,
+          start: this.form.flag ? this.form.studyStart : undefined,
+          end: this.form.flag ? this.form.studyEnd : undefined
+        }),
         // 翻腕亮屏设置
         lcdgesture: () => lcdgesture({
           deviceNumber: this.form.deviceNumber,

+ 27 - 17
src/components/DeviceInfo/Sleep.vue

@@ -118,7 +118,7 @@
           </template>
         </el-table-column>
       </el-table>
-      <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+      <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div> 
   </div>
 </template>
@@ -157,10 +157,10 @@ export default {
       startTime: "2024-08-04 20:58:00",
       endTime: "2024-08-05 00:27:00",
       sleepDefaultInfo: {
-        1: { name: "浅睡", color: "#93c0fa", yValue: 7 },
-        2: { name: "深睡", color: "#427ff7", yValue: 3 },
-        3: { name: "清醒", color: "#fadeaf", yValue: 9 },
-        4: { name: "快速眼动", color: "#9899f5", yValue: 12 },
+        4: { name: "浅睡", color: "#93c0fa", yValue: 7 },
+        3: { name: "深睡", color: "#427ff7", yValue: 3 },
+        6: { name: "清醒", color: "#fadeaf", yValue: 9 },
+        7: { name: "快速眼动", color: "#9899f5", yValue: 12 },
       },
       sleepInfo: {
         score: '',
@@ -390,48 +390,58 @@ export default {
 
             //统计
             this.sleepInfo.score = response.data.score==null?0:response.data.score;
+            
             //浅睡
             if (response.data.lightSleep != null) {
-              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = response.data.lightSleep;
 
             } else{
-              this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+              // this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
               this.sleepInfo.light_sleep = 0;
             }
+            this.sportChartOption.series[0].data[0] = { value: response.data.lightSleep, name: '浅睡', itemStyle: { color: "#93c0fa" } };
+
             //深睡
             if (response.data.deepSleep != null) {
-              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = response.data.deepSleep;
 
             } else{
-              this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
+              // this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
               this.sleepInfo.deep_sleep = 0;
             }
+            this.sportChartOption.series[0].data[1] = { value: response.data.deepSleep, name: '深睡', itemStyle: { color: "#427ff7" } };
             //清醒
             if (response.data.weakSleep != null) {
-              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = response.data.weakSleep;
 
             } else{
-              this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
+              // this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
               this.sleepInfo.weak_sleep = 0;
             }
+            this.sportChartOption.series[0].data[2] = { value: response.data.weakSleep, name: '清醒', itemStyle: { color: "#fadeaf" } };
             //快速眼动
             if (response.data.eyemoveSleep != null) {
-              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = response.data.eyemoveSleep;
 
             } else{
-              this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+              // this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
               this.sleepInfo.eyemove_sleep = 0;
             }
-            if (this.sportChart) {
-              this.sportChart.setOption(this.sportChartOption);
-            }
+            this.sportChartOption.series[0].data[3] = { value: response.data.eyemoveSleep, name: '快速眼动', itemStyle: { color: "#9899f5" } };
+            // if (this.sportChart) {
+            //   this.sportChart.setOption(this.sportChartOption);
+            // }
 
           } else {
-            console.warn("睡眠数据返回为空或格式不正确:", response);
+            this.defaultChart();
+            console.warn("睡眠数据返回为空", response);
+          }
+          if (this.sportChart) {
+              this.sportChart.setOption(this.sportChartOption);
           }
         })
         .catch((error) => {

+ 1 - 1
src/components/DeviceInfo/Sports.vue

@@ -30,7 +30,7 @@
       <el-table-column prop="startTime" label="开始时间" />
       <el-table-column prop="type" label="运动类型" />
     </el-table>
-    <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+    <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
     </div>
   </div>
 </template>

+ 1 - 1
src/components/DeviceInfo/Temperature.vue

@@ -26,7 +26,7 @@
                 <el-table-column prop="shellTemp" label="体表温度(℃)" />
                 
             </el-table>
-            <pagination v-show="(total>0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
+            <pagination v-show="(total > 0) && detailsType" :total="total" :page.sync="queryParams.pageNum" :limit.sync="queryParams.pageSize" @pagination="getDataPage"/>
         </div>
         
     </div>

+ 81 - 4
src/components/Editor/index.vue

@@ -1,5 +1,20 @@
 <template>
+  <div>
+    <el-upload
+      :action="uploadUrl"
+      :before-upload="handleBeforeUpload"
+      :on-success="handleUploadSuccess"
+      :on-error="handleUploadError"
+      name="file"
+      :show-file-list="false"
+      :headers="headers"
+      style="display: none"
+      ref="upload"
+      v-if="this.type == 'url'"
+    >
+    </el-upload>
     <div class="editor" ref="editor" :style="styles"></div>
+  </div>
 </template>
 
 <script>
@@ -7,6 +22,7 @@ import Quill from "quill";
 import "quill/dist/quill.core.css";
 import "quill/dist/quill.snow.css";
 import "quill/dist/quill.bubble.css";
+import { getToken } from "@/utils/auth";
 
 export default {
   name: "Editor",
@@ -26,9 +42,28 @@ export default {
       type: Number,
       default: null,
     },
+    /* 只读 */
+    readOnly: {
+      type: Boolean,
+      default: false,
+    },
+    // 上传文件大小限制(MB)
+    fileSize: {
+      type: Number,
+      default: 5,
+    },
+    /* 类型(base64格式、url格式) */
+    type: {
+      type: String,
+      default: "url",
+    }
   },
   data() {
     return {
+      uploadUrl: process.env.VUE_APP_BASE_API + "/common/upload", // 上传的图片服务器地址
+      headers: {
+        Authorization: "Bearer " + getToken()
+      },
       Quill: null,
       currentValue: "",
       options: {
@@ -47,11 +82,11 @@ export default {
             [{ color: [] }, { background: [] }],             // 字体颜色、字体背景颜色
             [{ align: [] }],                                 // 对齐方式
             ["clean"],                                       // 清除文本格式
-            ["image"]                       // 链接、图片、视频
+            ["link", "image", "video"]                       // 链接、图片、视频
           ],
         },
         placeholder: "请输入内容",
-        readOnly: false,
+        readOnly: this.readOnly,
       },
     };
   },
@@ -90,6 +125,18 @@ export default {
     init() {
       const editor = this.$refs.editor;
       this.Quill = new Quill(editor, this.options);
+      // 如果设置了上传地址则自定义图片上传事件
+      if (this.type == 'url') {
+        let toolbar = this.Quill.getModule("toolbar");
+        toolbar.addHandler("image", (value) => {
+          this.uploadType = "image";
+          if (value) {
+            this.$refs.upload.$children[0].$refs.input.click();
+          } else {
+            this.quill.format("image", false);
+          }
+        });
+      }
       this.Quill.pasteHTML(this.currentValue);
       this.Quill.on("text-change", (delta, oldDelta, source) => {
         const html = this.$refs.editor.children[0].innerHTML;
@@ -109,13 +156,43 @@ export default {
         this.$emit("on-editor-change", eventName, ...args);
       });
     },
+    // 上传前校检格式和大小
+    handleBeforeUpload(file) {
+      // 校检文件大小
+      if (this.fileSize) {
+        const isLt = file.size / 1024 / 1024 < this.fileSize;
+        if (!isLt) {
+          this.$message.error(`上传文件大小不能超过 ${this.fileSize} MB!`);
+          return false;
+        }
+      }
+      return true;
+    },
+    handleUploadSuccess(res, file) {
+      // 获取富文本组件实例
+      let quill = this.Quill;
+      // 如果上传成功
+      if (res.code == 200) {
+        // 获取光标所在位置
+        let length = quill.getSelection().index;
+        // 插入图片  res.url为服务器返回的图片地址
+        quill.insertEmbed(length, "image", process.env.VUE_APP_BASE_API + res.fileName);
+        // 调整光标到最后
+        quill.setSelection(length + 1);
+      } else {
+        this.$message.error("图片插入失败");
+      }
+    },
+    handleUploadError() {
+      this.$message.error("图片插入失败");
+    },
   },
 };
 </script>
 
 <style>
 .editor, .ql-toolbar {
-  white-space: pre-wrap!important;
+  white-space: pre-wrap !important;
   line-height: normal !important;
 }
 .quill-img {
@@ -192,4 +269,4 @@ export default {
 .ql-snow .ql-picker.ql-font .ql-picker-item[data-value="monospace"]::before {
   content: "等宽字体";
 }
-</style>
+</style>

+ 4 - 14
src/components/Editor/wang.vue

@@ -10,7 +10,6 @@
     name: 'editoritem',  
     data() {  
       return {
-        index:0,
         uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadWang",
         editor: null
       }  
@@ -34,11 +33,9 @@
             //创建编辑器
             this.editor = new E(that.$refs.editor1 )
             this.editor.config.uploadImgServer = this.uploadUrl;
-            
-             this.editor.config.uploadImgMaxLength = 5
             this.editor.config.uploadImgMaxSize = 2 * 1024 * 1024 // 2M
             this.editor.config.uploadImgAccept = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp']
-            this.editor.config.uploadFileName = 'file'
+            this.editor.config.uploadFileName = 'fileName'
             this.editor.config.zIndex = 1
             this.editor.config.menus = [
               'head',
@@ -102,16 +99,9 @@
                       // 图片上传并返回结果,自定义插入图片的事件(而不是编辑器自动插入图片!!!)
                       // insertImg 是插入图片的函数,editor 是编辑器对象,result 是服务器端返回的结果
                       // 举例:假如上传图片成功后,服务器端返回的是 {url:'....'} 这种格式,即可这样插入图片:
-                      // var url =   result.data[0].url
-                      if(result.data!=null){
-                        for(var i=0;i<result.data.length;i++){
-                            var url =   result.data[i].url
-                            insertImg(url)
-                          }
-                          console.log(url);
-                      }
-                      
-                     
+                      var url =   result.data[0].url
+                      console.log(url);
+                      insertImg(url)
                       // result 必须是一个 JSON 格式字符串!!!否则报错
                 }
             }

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

@@ -0,0 +1,246 @@
+<template>
+  <div class="upload-file">
+    <el-upload
+      :action="uploadFileUrl"
+      :before-upload="handleBeforeUpload"
+      :file-list="fileList"
+      :limit="limit"
+      :on-error="handleUploadError"
+      :on-exceed="handleExceed"
+      :on-success="handleUploadSuccess"
+      :show-file-list="false"
+      class="upload-file-uploader"
+      ref="upload"
+    >
+      <!-- 上传按钮 -->
+      <el-button size="mini" type="primary">选取文件</el-button>
+      <!-- 上传提示 -->
+      <div class="el-upload__tip" slot="tip" v-if="showTip">
+        请上传
+        <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}G</b></template>
+        <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b></template>
+        的文件
+      </div>
+    </el-upload>
+
+    <!-- 文件列表 -->
+    <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="`${baseUrl}${file.url}`" :underline="false" target="_blank">
+          <span class="el-icon-document"> {{ getFileName(file.name) }} </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>
+import {getToken} from "@/utils/auth";
+
+export default {
+  name: "FileUpload",
+  props: {
+    // 值
+    value: [String, Object, Array],
+    // 数量限制
+    limit: {
+      type: Number,
+      default: 5,
+    },
+    // 大小限制(G)
+    fileSize: {
+      type: Number,
+      default: 5,
+    },
+    // 文件类型, 例如['png', 'jpg', 'jpeg']
+    fileType: {
+      type: Array,
+      default: () => ["doc", "xls", "ppt", "txt", "pdf"],
+    },
+    duration: {
+      type: Boolean,
+      default: false,
+    },
+    // 是否显示提示
+    isShowTip: {
+      type: Boolean,
+      default: true
+    }
+  },
+  data() {
+    return {
+      baseUrl: process.env.VUE_APP_BASE_API,
+      uploadFileUrl: process.env.VUE_APP_BASE_API + "/common/uploadOSS", // 上传的图片服务器地址
+      headers: {
+        Authorization: "Bearer " + getToken(),
+      },
+      fileList: [],
+      videoDuration: 0,
+      videoTime: "",
+    };
+  },
+  watch: {
+    value: {
+      handler(val) {
+        if (val) {
+          let temp = 1;
+          // 首先将值转为数组
+          const list = Array.isArray(val) ? val : this.value.split(',');
+          // 然后将数组转为对象数组
+          this.fileList = list.map(item => {
+            if (typeof item === "string") {
+              item = {name: item, url: item};
+            }
+            item.uid = item.uid || new Date().getTime() + temp++;
+            return item;
+          });
+        } else {
+          this.fileList = [];
+          return [];
+        }
+      },
+      deep: true,
+      immediate: true
+    }
+  },
+  computed: {
+    // 是否显示提示
+    showTip() {
+      return this.isShowTip && (this.fileType || this.fileSize);
+    },
+  },
+  methods: {
+    // 上传前校检格式和大小
+    handleBeforeUpload(file) {
+      // 校检文件类型
+      if (this.fileType) {
+        let fileExtension = "";
+        if (file.name.lastIndexOf(".") > -1) {
+          fileExtension = file.name.slice(file.name.lastIndexOf(".") + 1);
+        }
+        const isTypeOk = this.fileType.some((type) => {
+          if (file.type.indexOf(type) > -1) return true;
+          if (fileExtension && fileExtension.indexOf(type) > -1) return true;
+          return false;
+        });
+        if (!isTypeOk) {
+          this.$message.error(`文件格式不正确, 请上传${this.fileType.join("/")}格式文件!`);
+          return false;
+        }
+      }
+      // 校检文件大小
+      if (this.fileSize) {
+        const isLt = file.size / 1024 / 1024 / 1024 < this.fileSize;
+        if (!isLt) {
+          this.$message.error(`上传文件大小不能超过 ${this.fileSize} G!`);
+          return false;
+        }
+      }
+      if (this.duration) {
+        this.getVideoDuration(file);
+      }
+      return true;
+    },
+    getVideoDuration(file) {
+      const videoElement = document.createElement('video');
+      const objectURL = URL.createObjectURL(file);
+      videoElement.src = objectURL;
+
+      videoElement.onloadedmetadata = () => {
+        this.videoDuration = Math.floor(videoElement.duration);  // 秒
+        this.videoTime = this.secondsToTime(this.videoDuration);
+        this.$emit("changeDuration", {duration: this.videoDuration, time: this.videoTime});
+      };
+    },
+    // 将时间字符串转换为秒数
+    timeToSeconds(time) {
+      const [hours, minutes, seconds] = time.split(':').map(Number);
+      return hours * 3600 + minutes * 60 + seconds;
+    },
+
+    // 将秒数转换为时分秒
+    secondsToTime(seconds) {
+      const hours = Math.floor(seconds / 3600);
+      const minutes = Math.floor((seconds % 3600) / 60);
+      const secs = Math.floor(seconds % 60);
+      return `${this.pad(hours)}:${this.pad(minutes)}:${this.pad(secs)}`;
+    },
+    // 补零处理
+    pad(number) {
+      return number < 10 ? `0${number}` : number;
+    },
+    // 计算结束时间
+    calculateEndTime() {
+      if (this.startTime && this.videoDuration) {
+        const startTimeInSeconds = this.timeToSeconds(this.startTime);
+        const endTimeInSeconds = startTimeInSeconds + this.videoDuration;
+
+        this.endTime = this.secondsToTime(endTimeInSeconds);
+      }
+    },
+    // 文件个数超出
+    handleExceed() {
+      this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
+    },
+    // 上传失败
+    handleUploadError(err) {
+      this.$message.error("上传失败, 请重试");
+    },
+    // 上传成功回调
+    handleUploadSuccess(res, file) {
+      this.$message.success("上传成功");
+      this.fileList.push({name: res.url, url: res.url});
+      this.$emit("input", this.listToString(this.fileList));
+    },
+    // 删除文件
+    handleDelete(index) {
+      this.fileList.splice(index, 1);
+      this.$emit("input", this.listToString(this.fileList));
+    },
+    // 获取文件名称
+    getFileName(name) {
+      if (name.lastIndexOf("/") > -1) {
+        return name.slice(name.lastIndexOf("/") + 1).toLowerCase();
+      } else {
+        return "";
+      }
+    },
+    // 对象转成指定字符串分隔
+    listToString(list, separator) {
+      let strs = "";
+      separator = separator || ",";
+      for (let i in list) {
+        strs += list[i].url + separator;
+      }
+      return strs != '' ? strs.substr(0, strs.length - 1) : '';
+    }
+  }
+};
+</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 {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  color: inherit;
+}
+
+.ele-upload-list__item-content-action .el-link {
+  margin-right: 10px;
+}
+</style>

+ 123 - 0
src/components/H5/FormWrapper.vue

@@ -0,0 +1,123 @@
+<template>
+  <el-form ref="form" :model="form" label-width="130px" v-if="form && form.type">
+    <!-- Text component form items -->
+    <text-form-items
+      v-if="form.type === 'h5-text'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
+    <!-- Image component form items -->
+    <image-form-items
+      v-if="form.type === 'h5-image'"
+      :config="form"
+    />
+
+    <!-- Sep component form items -->
+    <sep-form-items
+      v-if="form.type === 'h5-sep'"
+      :config="form"
+    />
+
+    <!-- Countdown component form items -->
+    <countdown-form-items
+      v-if="form.type === 'h5-countdown'"
+      :config="form"
+      @update:config="updateFormConfig"
+    />
+
+    <chat-form-items
+      v-if="form.type === 'h5-chat'"
+      :config="form"
+      @update:config="updateFormConfig"
+      />
+
+    <!-- Common form items for all components -->
+    <common-form-items
+      :config="form"
+      :index="index"
+      :list="list"
+      @bottom-change="handleBottomChange"
+      @delete="handleDelete"
+      @update:config="updateFormConfig"
+    />
+  </el-form>
+</template>
+
+<script>
+import TextFormItems from './config-item/h5-text-config.vue';
+import ImageFormItems from './config-item/h5-image-config.vue';
+import SepFormItems from './config-item/h5-sep-config.vue';
+import CommonFormItems from './config-item/common-config.vue';
+import CountdownFormItems from './config-item/h5-countdown-config.vue';
+import ChatFormItems from './config-item/h5-chat-config.vue'
+export default {
+  name: 'FormWrapper',
+  components: {
+    TextFormItems,
+    ImageFormItems,
+    SepFormItems,
+    CommonFormItems,
+    CountdownFormItems,
+    ChatFormItems
+  },
+  props: {
+    form: {
+      type: Object,
+      default: () => ({})
+    },
+    index: {
+      type: Number,
+      default: -1
+    },
+    list: {
+      type: Array,
+      default: () => []
+    }
+  },
+  methods: {
+    updateFormConfig(newConfig) {
+      console.log("更新表单配置...");
+      // Update form config
+      this.$emit('update:form', newConfig);
+    },
+    handleBottomChange() {
+      // Check if another item is already fixed to bottom
+      let hasFooter = this.list.some((item, idx) =>
+        idx !== this.index && item.classText && item.classText.includes('footer')
+      );
+
+      if(hasFooter) {
+        alert('全局只能允许有一个在底部!');
+        this.form.fixe = false;
+        return;
+      }
+
+      // Update the class
+      let className = "footer";
+      if (this.form.fixe) {
+        this.addClassToItem(className);
+      } else {
+        this.removeClassFromItem(className);
+      }
+    },
+    addClassToItem(className) {
+      if (!this.form.classText.includes(className)) {
+        this.form.classText.push(className);
+      }
+    },
+    removeClassFromItem(className) {
+      this.form.classText = this.form.classText.filter(cls => cls !== className);
+    },
+    handleDelete() {
+      this.$emit('delete', this.index);
+    }
+  }
+}
+</script>
+
+<style>
+.el-form-item__label{
+  text-align: center !important;
+}
+</style>

+ 87 - 0
src/components/H5/config-item/common-config.vue

@@ -0,0 +1,87 @@
+// CommonFormItems.vue - Common form items shared across all components
+<template>
+  <div id="h5-config-common" ref="commonForm">
+    <el-form-item label="是否底部">
+      <el-switch v-model="config.fixe" @change="handleBottom"></el-switch>
+    </el-form-item>
+    <el-form-item label="点击是否加微">
+      <el-switch v-model="config.addWxFun"/>
+    </el-form-item>
+    <el-form-item label="获客链接" v-if="config.addWxFun">
+      <el-select v-model="config.workUrl" filterable @change="val => updateConfig('workUrl', val)" placeholder="请选择">
+        <el-option
+          v-for="item in options"
+          :key="item.value"
+          :label="item.label"
+          :value="item.value">
+        </el-option>
+      </el-select>
+      <el-button type="primary" @click="copyLink">复制链接</el-button>
+    </el-form-item>
+    <el-form-item>
+      <el-button type="danger" @click="handleDelete">删 除</el-button>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+import {listAll} from '@/api/qw/workLink'
+
+export default {
+  name: 'CommonFormItems',
+  props: {
+    config: {
+      type: Object,
+      required: true
+    },
+    index: {
+      type: Number,
+      required: true
+    },
+    list: {
+      type: Array,
+      required: true
+    }
+  },
+  data(){
+    return {
+        options: []
+    }
+  },
+  mounted() {
+    this.loadQwAddr();
+  },
+  methods: {
+    async copyLink() {
+      try {
+        await navigator.clipboard.writeText(this.config.workUrl);
+        this.$message.success("链接已复制到剪贴板"); // 提示成功
+      } catch (err) {
+        console.error("复制失败:", err);
+        this.$message.error("复制失败,请手动复制"); // 提示失败
+      }
+    },
+    loadQwAddr(){
+      listAll({})
+        .then(res=>{
+          this.options = res.data.map(item=>{
+            return {
+              label: item.linkName,
+              value: item.url
+            }
+          })
+        })
+        .catch(err=>console.log(err));
+    },
+    updateConfig(key, value) {
+      this.$emit('update:config', { ...this.config, [key]: value });
+    },
+    handleBottom() {
+      this.$emit('bottom-change');
+    },
+    handleDelete() {
+      this.$emit('delete');
+    }
+  }
+}
+</script>

+ 182 - 0
src/components/H5/config-item/h5-chat-config-dialog.vue

@@ -0,0 +1,182 @@
+<template>
+  <div v-if="localDialog && localDialog.text " :class="visible?'dialog-config-active':'dialog-config'">
+    <div class="question-section">
+      <div class="section-label">提问</div>
+      <el-input v-model="localDialog.text" placeholder="选择问题" style="width: 100%">
+      </el-input>
+    </div>
+
+    <div class="answer-options" v-if="localDialog.options && localDialog.options.length > 0">
+      <div class="section-label">回复</div><el-button
+        type="text"
+        class="config-button"
+        @click="add">
+        新增
+      </el-button>
+
+      <draggable v-model="localDialog.options" @end="">
+        <div v-for="(option, index) in localDialog.options" :key="option.id || index" class="option-item">
+          <div class="option-label">选项{{ index + 1 }}: <el-input v-model="option.text" placeholder="对话" />
+            <br/>
+            <br/>
+            <el-input type="textarea" v-model="option.answer" placeholder="回复"></el-input>
+            <el-button
+              type="text"
+              class="config-button"
+              @click="del(index)">
+              删除
+            </el-button>
+          </div>
+        </div>
+      </draggable>
+    </div>
+    <div class="answer-options" v-else>
+      <el-button
+        type="text"
+        class="config-button"
+        @click="add">
+        新增
+      </el-button>
+    </div>
+
+    <div class="action-buttons">
+      <el-button type="primary" @click="saveAndReturn">保存并返回</el-button>
+    </div>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'DialogConfig',
+  components: { draggable },
+  props: {
+    visible: {
+      type: Boolean,
+      default: false
+    },
+    dialog: {
+      type: Object,
+      required: false,
+      default: () => ({
+        id: 2,
+        sender: 'agent',
+        text: '您的年龄?',
+        options: [
+          { id: 1, text: '45-55岁' },
+          { id: 2, text: '55-60岁' },
+          { id: 3, text: '60-65岁' },
+          { id: 4, text: '65岁以上' }
+        ],
+        userSelection: null
+      })
+    },
+    dialogIndex: {
+      type: Number,
+      required: false
+    }
+  },
+  watch: {
+    dialog: {
+      immediate: true,
+      handler(newVal) {
+        this.localDialog = { ...newVal }
+        console.log('localDialog', this.localDialog)
+      }
+    },
+    visible: {
+      immediate: true,
+      handler(newVal) {
+        this.$nextTick(()=>{
+          // 隐藏通用表单
+          if (newVal) {
+            document.querySelector("#h5-config-common").style.display = "none";
+          } else {
+            document.querySelector("#h5-config-common").style.display = "block";
+          }
+        });
+      }
+    }
+  },
+  data() {
+    return {
+      localDialog: {
+        options:[],
+        ...this.dialog
+      }
+    }
+  },
+  methods: {
+    del(index) {
+      this.localDialog.options.splice(index, 1);
+    },
+    add() {
+      if (!this.localDialog.options) {
+        this.$set(this.localDialog, 'options', []);
+      }
+      this.localDialog.options.push({ text: '', answer: '' ,id: this.localDialog.options.length});
+      console.log(this.localDialog)
+    },
+    saveAndReturn() {
+      // this.$emit('update:dialog', this.localDialog);
+      this.$emit('update:dialog', { ...this.localDialog });
+      this.$emit('update:visible', false)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.dialog-config {
+  padding: 100px;
+  visibility: hidden;
+  display: none;
+}
+
+.dialog-config-active {
+  position: absolute;
+  top: 0;
+  width: 100%;
+  height: 100%;
+  z-index: 9999;
+  background-color: white;
+}
+
+.section-label {
+  font-size: 14px;
+  color: #606266;
+  margin-bottom: 10px;
+}
+
+.question-section, .answer-options {
+  margin-bottom: 25px;
+  z-index: 99999;
+  background-color: white;
+}
+
+.editor-content textarea {
+  width: 100%;
+  min-height: 100px;
+  border: none;
+  outline: none;
+  resize: vertical;
+}
+
+.option-item {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 10px 0;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.config-button {
+  color: #409EFF;
+}
+
+.action-buttons {
+  margin-top: 30px;
+  text-align: center;
+}
+</style>

+ 287 - 0
src/components/H5/config-item/h5-chat-config.vue

@@ -0,0 +1,287 @@
+<template>
+  <div class="qa-dialog-config">
+    <!-- 互动问答内容设置 header -->
+    <div class="config-header">
+      <h3>互动问答内容设置</h3>
+    </div>
+
+    <!-- 问答对话设置 -->
+    <el-form-item label="问答对话">
+      <div
+        v-for="(msg, index) in localConfig.agentMsg"
+        :key="index"
+        class="dialog-item"
+      >
+        <div class="dialog-header">
+          <span>对话{{ msg.id }}</span>
+          <div class="dialog-actions">
+            <el-button type="text" @click="configDialog(msg.id)">设置</el-button>
+            <el-button type="text" class="delete-btn" @click="removeDialog(msg.id)">删除</el-button>
+          </div>
+        </div>
+        <div class="dialog-content">
+          <div class="question">提问: {{ msg.text }}</div>
+        </div>
+      </div>
+
+      <div class="add-dialog">
+        <el-button type="text" @click="addDialog">
+          <i class="el-icon-plus"></i>
+          添加对话(还可以添加{{ 6 - localConfig.agentMsg.length }}项)
+        </el-button>
+      </div>
+    </el-form-item>
+
+    <el-form-item label="客服头像">
+      <div class="avatar-setting">
+        <image-upload v-model="localConfig.style.avatar" :file-type='["png", "jpg", "jpeg", "gif"]' :limit="1"/>
+      </div>
+    </el-form-item>
+
+    <el-form-item label="按钮配色">
+      <el-color-picker v-model="localConfig.style.buttonColor"></el-color-picker>
+    </el-form-item>
+
+    <el-form-item label="按钮文字颜色">
+      <el-color-picker v-model="localConfig.style.btnTextColor"></el-color-picker>
+    </el-form-item>
+
+    <!-- 聊天配置对话框-->
+    <h5-chat-config-dialog :dialog-index="currentDialogIndex" :dialog="currentMsg" :visible="dialogConfigVisible"
+                           @update:visible="dialogConfigVisible=$event"
+                           @update:dialog="updateDialogData"
+    />
+  </div>
+</template>
+
+<script>
+import H5ChatConfigDialog from '@/components/H5/config-item/h5-chat-config-dialog.vue'
+import draggable from 'vuedraggable'
+
+export default {
+  name: 'ChatConfigComponent',
+  components: {
+    draggable,
+    H5ChatConfigDialog
+  },
+  computed: {
+  },
+  props: {
+    config: {
+      type: Object,
+      default: () => ({
+        messages: [{
+          id: 1,
+          sender: 'agent',
+          text: '您好!欢迎报名【中老年健康养生大讲堂】,请仔细答一下问题,便于帮您分配专业的老师进行指导。',
+          welcome: true
+        },
+          {
+            id: 2,
+            sender: 'agent',
+            text: '您的年龄?',
+            options: [
+              { id: 1, text: '45-55岁', answer: '' },
+              { id: 2, text: '55-60岁', answer: '' },
+              { id: 3, text: '60-65岁', answer: '' },
+              { id: 4, text: '65岁以上', answer: '' }
+            ],
+            userSelection: null
+          },
+          {
+            id: 3,
+            sender: 'user',
+            text: '45-55岁',
+            userSelection: { id: 1, text: '45-55岁' }
+          },
+          {
+            id: 4,
+            sender: 'agent',
+            text: '更想通过课程学习到哪些知识?',
+            options: [
+              { id: 1, text: '食疗食补' },
+              { id: 2, text: '经络疏通' },
+              { id: 3, text: '脏腑调养' },
+              { id: 4, text: '启蒙养生' },
+              { id: 5, text: '以上所有' }
+            ],
+            userSelection: null
+          },
+          {
+            id: 5,
+            sender: 'user',
+            text: '食疗食补',
+            userSelection: { id: 1, text: '食疗食补' }
+          }],
+        agentMsg: [],
+        style: {
+          avatar: '',
+          buttonColor: '#409EFF',
+          btnTextColor: ''
+        }
+      })
+    }
+  },
+  data() {
+    return {
+      localConfig: {agentMsg:[],...this.config},
+      currentMsg: null,
+      dialogConfigVisible: false,
+      currentDialogIndex: -1
+    }
+  },
+  watch: {
+    'localConfig.style.avatar': {
+      handler(newVal) {
+        if (newVal) {
+          this.$emit('update:config', this.localConfig)
+        }
+      },
+      deep: true
+    },
+    'localConfig.agentMsg':{
+      handler(newVal) {
+        if (newVal) {
+          setTimeout(()=>{
+            // 根据agentMsg生成Message列表
+            let messages = []
+            let id = 0;
+            this.localConfig.agentMsg?.forEach((msg)=>{
+              id++;
+              if(!msg.options || msg.options.length === 0) {
+                messages.push({...msg,id: id})
+              } else {
+                messages.push({...msg,id: id})
+                messages.push({
+                  id: ++id,
+                  sender: 'user',
+                  text: msg.options[0].text
+                })
+              }
+            })
+            messages.push({
+              id: ++id,
+              sender: 'agent',
+              text: "好的,已经为您分配专业老师。<span style='color:red'>【点击下方按钮】</span>添加老师,获取免费上课链接。!",
+              options: []
+            })
+            this.localConfig.messages = messages;
+            this.$emit('update:config', this.localConfig)
+          },100)
+        }
+      },
+      deep: true
+    }
+  },
+  methods: {
+    addDialog() {
+      if ((6 - this.localConfig.agentMsg.length) <= 0) {
+        alert('最多只能添加6条对话')
+        return
+      }
+      let maxId = Math.max(0,...this.localConfig.agentMsg.map(msg => msg.id));
+      if(!maxId){
+        maxId = 0;
+      }
+      this.localConfig.agentMsg.push({
+        id: maxId + 1,
+        sender: 'agent',
+        text: '新增对话',
+        welcome: true
+      })
+      this.$emit('update:config', this.localConfig)
+    },
+    removeDialog(id) {
+      let index = this.localConfig.agentMsg.findIndex(msg => msg.id === id)
+      this.localConfig.agentMsg.splice(index, 1)
+      this.$emit('update:config', this.localConfig)
+    },
+    configDialog(id) {
+      // 通过 id 找到对应的消息
+      let index = this.localConfig.agentMsg.findIndex(msg => msg.id === id)
+      if (index === -1) {
+        console.error(`Message with id ${id} not found.`)
+        return
+      }
+      this.currentDialogIndex = index
+      this.currentMsg = this.localConfig.agentMsg[index]
+      this.dialogConfigVisible = true
+      this.$set(this.$data, index, this.currentMsg)
+      console.log(this.currentMsg)
+    },
+    updateDialogData(data) {
+      this.localConfig.agentMsg[this.currentDialogIndex] = data
+      this.currentMsg = data
+      this.$emit('update:config', this.localConfig)
+    }
+  }
+}
+</script>
+
+<style scoped>
+.qa-dialog-config {
+  font-family: -apple-system, BlinkMacSystemFont, "Segoe UI", Roboto, "Helvetica Neue", Arial, sans-serif;
+  position: relative;
+}
+
+.config-header {
+  border-bottom: 1px solid #ebeef5;
+  padding-bottom: 10px;
+  margin-bottom: 20px;
+}
+
+.qa-section, .style-section {
+  margin-bottom: 20px;
+}
+
+h4 {
+  font-weight: 500;
+  margin-bottom: 15px;
+}
+
+
+.add-dialog {
+  color: #409EFF;
+  cursor: pointer;
+  margin-top: 5px;
+}
+
+.dialog-item {
+  border: 1px solid #dcdfe6;
+  border-radius: 4px;
+  margin-bottom: 10px;
+}
+
+.dialog-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding: 8px 12px;
+  border-bottom: 1px solid #ebeef5;
+}
+
+.dialog-actions {
+  display: flex;
+}
+
+.delete-btn {
+  color: #F56C6C;
+}
+
+.dialog-content {
+  padding: 12px;
+}
+
+.avatar-setting, .color-setting {
+  display: flex;
+  align-items: center;
+  margin-bottom: 15px;
+}
+
+.setting-label {
+  width: 80px;
+  font-size: 14px;
+}
+
+
+</style>

+ 270 - 0
src/components/H5/config-item/h5-countdown-config.vue

@@ -0,0 +1,270 @@
+<template>
+  <div class="countdown-config">
+    <div class="config-block">
+      <h3>倒计时配置</h3>
+
+      <el-form-item class="time-config" label="时间配置">
+        <div class="button-group">
+          <el-radio-group v-model="localConfig.countdownMode" @change="handleModeChange">
+            <el-radio-button :label="'1'" class="select-button"
+                             :class="{ 'selected': localConfig.countdownMode == '1' }"
+            >固定截止时间
+            </el-radio-button>
+            <el-radio-button :label="'2'" class="select-button"
+                             :class="{ 'selected': localConfig.countdownMode == '2' }"
+            >固定倒计时长
+            </el-radio-button>
+          </el-radio-group>
+        </div>
+
+        <!-- 固定截止时间模式 -->
+        <template v-if="localConfig.countdownMode == 1">
+          <div class="datetime-picker">
+            <el-date-picker
+              v-model="localConfig.endDateTime"
+              type="datetime"
+              placeholder="选择截止日期时间"
+              format="yyyy-MM-dd HH:mm:ss"
+              value-format="yyyy-MM-dd HH:mm:ss"
+              @change="handleEndTimeChange"
+            ></el-date-picker>
+          </div>
+          <div class="help-text">
+            * 设置一个截至日期固定的倒计时,例如,设置2021-03-04 12:00:00,访客进入页面,该倒计时将会倒计至2021-03-04
+            12:00:00结束。
+          </div>
+        </template>
+
+        <!-- 固定倒计时长模式 -->
+        <template v-else>
+          <div class="time-input">
+            <el-input v-model="localConfig.timeDisplay" @change="handleCountMinusChange" placeholder="分钟">
+              <template #append>
+                <i class="el-icon-time"></i>
+              </template>
+            </el-input>
+          </div>
+          <div class="help-text">
+            * 固定时长:设置一个固定时长的倒计时,例如,设置5分钟,访客每次进入页面,倒计时均倒计5分钟,若倒计时间结束,用户再次进入页面,倒计时重新开始计时。
+          </div>
+        </template>
+      </el-form-item>
+
+      <el-form-item class="color-config" label="颜色">
+        <el-color-picker
+          v-model="localConfig.mainColor"
+          show-alpha
+          @change="handleColorChange"
+        ></el-color-picker>
+      </el-form-item>
+
+      <el-form-item class="bg-image-config" label="倒计时底图">
+        <image-upload v-model="localConfig.bgImage" :file-type='["png", "jpg", "jpeg", "gif"]' :limit="1"/>
+      </el-form-item>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'CountdownConfigComponent',
+
+  props: {
+    config: {
+      type: Object,
+      required: false,
+      default: () => ({
+        hours: 0,
+        minutes: 10,
+        seconds: 0,
+        endDateTime: null,
+        timeDisplay: 0,
+        mainColor: '#FF5722',
+        bgImage: ''
+      })
+    }
+  },
+
+  data() {
+    return {
+      localConfig: { ...this.config }
+    }
+  },
+
+  methods: {
+    // 数字补零
+    padZero(num) {
+      return num.toString().padStart(2, '0')
+    },
+
+    // 格式化时间显示
+    formatTimeDisplay(hours, minutes, seconds) {
+      return `${this.padZero(hours)}时 ${this.padZero(minutes)}分 ${this.padZero(seconds)}秒`
+    },
+
+    // 处理倒计时模式变更
+    handleModeChange(value) {
+      this.localConfig.countdownMode = value
+      this.$emit('update:config', { ...this.localConfig })
+    },
+
+    // 处理倒计时长
+    handleCountMinusChange(value) {
+      this.localConfig.timeDisplay = value
+      this.$emit('update:config', { ...this.localConfig })
+    },
+    // 处理截止时间变更
+    handleEndTimeChange(value) {
+      this.localConfig.endDateTime = value
+      this.$emit('update:config', { ...this.localConfig })
+    },
+
+    // 处理颜色变更
+    handleColorChange(value) {
+      this.localConfig.mainColor = value
+      this.$emit('update:config', { ...this.localConfig })
+    }
+  },
+
+  // 监听属性变化
+  watch: {
+    'localConfig.bgImage': {
+      handler(newVal, oldVal) {
+        this.localConfig.bgImage = newVal
+        this.$emit('update:config', { ...this.localConfig })
+      },
+      deep: false
+    }
+  }
+}
+</script>
+
+<style scoped>
+.countdown-config {
+  width: 100%;
+  font-family: "Helvetica Neue", Helvetica, "PingFang SC", "Hiragino Sans GB", "Microsoft YaHei", Arial, sans-serif;
+}
+
+.config-block {
+  padding: 0px 15px;
+}
+
+h3, h4 {
+  font-weight: 500;
+  margin: 15px 0 10px 0;
+  color: #303133;
+}
+
+h3 {
+  font-size: 16px;
+  border-bottom: 1px solid #EBEEF5;
+  padding-bottom: 10px;
+}
+
+h4 {
+  font-size: 14px;
+  margin-top: 20px;
+}
+
+.button-group {
+  margin: 10px 0;
+}
+
+.select-button {
+  margin-right: -1px;
+}
+
+.selected {
+  color: #fff;
+  background-color: #409EFF;
+  border-color: #409EFF;
+}
+
+.datetime-picker, .time-input {
+  margin: 10px 0;
+}
+
+.help-text {
+  color: #909399;
+  font-size: 12px;
+  margin-top: 5px;
+  line-height: 1.5;
+}
+
+.color-config {
+  margin: 15px 0;
+}
+
+.color-preview {
+  height: 30px;
+  border-radius: 4px;
+  margin-top: 10px;
+}
+
+.image-upload-area {
+  border: 1px dashed #d9d9d9;
+  border-radius: 6px;
+  width: 100%;
+  height: 108px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  position: relative;
+  margin: 10px 0;
+  overflow: hidden;
+}
+
+.image-preview {
+  width: 100%;
+  height: 100%;
+  position: relative;
+}
+
+.image-preview img {
+  width: 100%;
+  height: 100%;
+  object-fit: contain;
+}
+
+.image-actions {
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  right: 0;
+  background-color: rgba(0, 0, 0, 0.5);
+  display: flex;
+  justify-content: space-around;
+  padding: 5px 0;
+}
+
+.upload-tip {
+  text-align: center;
+  color: #909399;
+  font-size: 12px;
+}
+
+.upload-tip i {
+  font-size: 28px;
+  color: #C0C4CC;
+  margin-bottom: 5px;
+}
+
+.price-config {
+  margin-top: 15px;
+}
+
+.price-input {
+  display: flex;
+  align-items: center;
+  margin: 10px 0;
+}
+
+.color-label {
+  margin-right: 10px;
+  min-width: 40px;
+}
+
+.price-value {
+  margin: 10px 0 20px 0;
+}
+</style>

+ 20 - 0
src/components/H5/config-item/h5-image-config.vue

@@ -0,0 +1,20 @@
+// ImageFormItems.vue - Form items specific to the h5-image component
+<template>
+  <div>
+    <el-form-item label="图片">
+      <image-upload v-model="config.url" :file-type='["png", "jpg", "jpeg", "gif"]' :limit="1"/>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'ImageFormItems',
+  props: {
+    config: {
+      type: Object,
+      required: true
+    }
+  }
+}
+</script>

+ 22 - 0
src/components/H5/config-item/h5-sep-config.vue

@@ -0,0 +1,22 @@
+<template>
+  <div>
+    <el-form-item label="高度">
+      <el-input v-model="config.style.height" type="number"></el-input>
+    </el-form-item>
+    <el-form-item label="背景颜色">
+      <el-color-picker v-model="config.style.background"></el-color-picker>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'SepFormItems',
+  props: {
+    config: {
+      type: Object,
+      required: true
+    }
+  }
+}
+</script>

+ 127 - 0
src/components/H5/config-item/h5-text-config.vue

@@ -0,0 +1,127 @@
+<template>
+  <div>
+    <el-form-item label="内容">
+      <el-input type="textarea" :row="10" :value="config.content" @input="val => updateConfig('content', val)"/>
+    </el-form-item>
+    <el-form-item label="对齐方式">
+      <el-radio v-model="config.style.textAlign" label="left">右对齐</el-radio>
+      <el-radio v-model="config.style.textAlign" label="center">居中</el-radio>
+      <el-radio v-model="config.style.textAlign" label="right">左对齐</el-radio>
+    </el-form-item>
+    <el-form-item label="背景颜色">
+      <el-color-picker v-model="config.style.background"></el-color-picker>
+    </el-form-item>
+    <el-form-item label="文本颜色">
+      <el-color-picker v-model="config.style.color"></el-color-picker>
+    </el-form-item>
+    <el-form-item label="字号">
+      <el-input placeholder="请输入内容" v-model="style.fontSize"  @input="handleInput">
+        <template slot="append">px</template>
+      </el-input>
+    </el-form-item>
+    <el-form-item label="样式">
+      <el-tooltip class="icon-span" effect="dark" content="加粗" placement="top">
+        <svg @click="fontChange(config.style, 'fontWeight', 'bold')" class="icon svg"
+             viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32"
+             height="32">
+          <path :fill="(config.style.fontWeight === 'bold' ? '#1296db' : '#515151')"
+                d="M768.96 575.072c-22.144-34.112-54.816-56.8-97.984-68.032v-2.176c22.88-10.88 42.112-23.04 57.696-36.48 15.616-12.704 27.584-26.144 35.936-40.288 16.32-29.76 24.128-60.96 23.392-93.632 0-63.872-19.776-115.232-59.328-154.08-39.2-38.464-97.824-58.048-175.84-58.784H215.232v793.728H579.52c62.432 0 114.496-20.864 156.256-62.624 42.112-39.936 63.52-94.176 64.224-162.752 0-41.376-10.336-79.68-31.04-114.88zM344.32 228.832h194.912c43.904 0.736 76.224 11.424 96.896 32.128 21.056 22.144 31.584 49.184 31.584 81.12s-10.528 58.432-31.584 79.488c-20.672 22.848-52.992 34.304-96.896 34.304H344.32V228.832z m304.352 536.256c-20.672 23.584-53.344 35.744-97.984 36.48H344.32v-238.432h206.336c44.64 0.704 77.312 12.512 97.984 35.392 20.672 23.232 31.04 51.168 31.04 83.84 0 31.904-10.336 59.488-31.008 82.72z"></path>
+        </svg>
+      </el-tooltip>
+      <el-tooltip class="icon-span" effect="dark" content="倾斜" placement="top">
+        <svg @click="fontChange(config.style, 'fontStyle', 'oblique')" class="icon svg"
+             viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32"
+             height="32">
+          <path :fill="(config.style.fontStyle === 'oblique' ? '#1296db' : '#515151')"
+                d="M602.530909 162.909091l-87.458909 721.454545H616.727273a34.909091 34.909091 0 0 1 0 69.818182h-279.272728a34.909091 34.909091 0 0 1 0-69.818182h107.287273l87.458909-721.454545H430.545455a34.909091 34.909091 0 0 1 0-69.818182h279.272727a34.909091 34.909091 0 0 1 0 69.818182h-107.287273z"></path>
+        </svg>
+      </el-tooltip>
+      <el-tooltip class="icon-span" effect="dark" content="下划线" placement="top">
+        <svg @click="fontChange(config.style, 'textDecoration', 'underline')" class="icon svg"
+             viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg" width="32"
+             height="32">
+          <path
+            :fill="(config.style.textDecoration && config.style.textDecoration === 'underline' ? '#1296db' : '#515151')"
+            d="M512 811.296a312 312 0 0 0 312-312V89.6h-112v409.696a200 200 0 1 1-400 0V89.6h-112v409.696a312 312 0 0 0 312 312zM864 885.792H160a32 32 0 0 0 0 64h704a32 32 0 0 0 0-64z"></path>
+        </svg>
+      </el-tooltip>
+      <el-tooltip class="icon-span" effect="dark" content="中划线" placement="top">
+        <svg @click="fontChange(config.style, 'textDecoration', 'line-through')"
+             class="icon svg" viewBox="0 0 1024 1024" version="1.1" xmlns="http://www.w3.org/2000/svg"
+             width="32" height="32">
+          <path
+            :fill="(config.style.textDecoration && config.style.textDecoration === 'line-through' ? '#1296db' : '#515151')"
+            d="M604.16 512H819.2v51.2h-117.76c30.72 25.6 46.08 61.44 46.08 102.4 0 51.2-20.48 92.16-61.44 122.88-40.96 30.72-97.28 40.96-163.84 40.96-66.56 0-122.88-15.36-163.84-46.08-46.08-35.84-71.68-87.04-76.8-158.72h66.56c5.12 51.2 25.6 87.04 51.2 112.64 25.6 20.48 66.56 30.72 117.76 30.72 46.08 0 87.04-10.24 117.76-25.6 30.72-20.48 46.08-40.96 46.08-71.68 0-35.84-20.48-66.56-56.32-87.04-10.24-5.12-25.6-10.24-51.2-20.48H204.8v-51.2h204.8c-20.48-5.12-35.84-15.36-46.08-20.48-46.08-25.6-66.56-61.44-66.56-112.64 0-51.2 20.48-92.16 66.56-117.76 35.84-25.6 87.04-35.84 148.48-35.84 66.56 0 117.76 15.36 153.6 46.08 40.96 30.72 61.44 76.8 66.56 138.24H665.6c-5.12-40.96-25.6-71.68-51.2-92.16-25.6-20.48-61.44-30.72-112.64-30.72-40.96 0-76.8 5.12-102.4 20.48-20.48 10.24-35.84 35.84-35.84 71.68 0 30.72 15.36 51.2 51.2 71.68 15.36 10.24 51.2 20.48 107.52 35.84 30.72 10.24 61.44 15.36 81.92 25.6z"></path>
+        </svg>
+      </el-tooltip>
+    </el-form-item>
+  </div>
+</template>
+
+<script>
+export default {
+  name: 'TextFormItems',
+  props: {
+    config: {
+      type: Object,
+      required: true
+    }
+  },
+  methods: {
+    handleInput(value) {
+      // 使用正则表达式过滤非数字字符
+      this.style.fontSize = value.replace(/[^\d]/g, '');
+    },
+    updateConfig(key, value) {
+      this.$emit('update:config', { ...this.config, [key]: value });
+    },
+    fontChange(data, name, value) {
+      // 根据属性类型设置回退值
+      const fallbackValue = {
+        fontWeight: 'normal',  // 加粗恢复为normal
+        fontStyle: 'normal',   // 倾斜恢复为normal
+        textDecoration: 'none' // 下划线/中划线恢复为none
+      }[name];
+
+      if (data[name] === value) {
+        this.$set(data, name, fallbackValue);
+      } else {
+        this.$set(data, name, value);
+      }
+      this.$emit('update:config', { ...this.config });
+    }
+  },
+  data(){
+    return {
+      style:{
+        fontSize: 12
+      },
+      rules: {
+        fontSize: [
+          {
+            validator: (rule, value, callback) => {
+              if (/^\d+$/.test(value)) {
+                callback();
+              } else {
+                callback(new Error('只能输入数字'));
+              }
+            },
+            trigger: 'blur',
+          },
+        ],
+      },
+    }
+  },
+  watch:{
+    'style.fontSize':{
+      handler(val){
+        if(val){
+          console.log(val)
+          this.$set(this.config.style,'fontSize',val+'px');
+        }
+      },
+      deep:true
+    }
+  }
+}
+</script>

+ 13 - 0
src/components/H5/css/base.css

@@ -0,0 +1,13 @@
+.parent-div{
+  border: 2px dashed transparent;
+  /*padding: 5px 0;*/
+}
+.parent-div.active{
+  border-color: #02ff9b;
+}
+.footer{
+  position: absolute;
+  bottom: 0;
+  left: 0;
+  width: 100%;
+}

+ 27 - 0
src/components/H5/h5-button.vue

@@ -0,0 +1,27 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <button class="el-button">{{config.content}}</button>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "h5-button",
+  props: {
+    config: {
+      type: Object,
+      default: () => ({})
+    }
+  }
+};
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+img{
+  width: 100% !important;
+}
+.el-button{
+  width: 100%;
+}
+
+</style>

+ 234 - 0
src/components/H5/h5-chat.vue

@@ -0,0 +1,234 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <div class="chat-container" >
+      <div class="chat-messages">
+        <!-- Messages -->
+        <div v-for="(message, index) in messages" :key="index"
+             :class="['message-wrapper', message.sender === 'user' ? 'user-message' : 'agent-message']">
+          <div class="avatar">
+            <img :src="message.sender === 'user' ? userAvatar : getAgentAvatar" alt="Avatar" />
+          </div>
+          <div class="message-content">
+            <div v-if="message.text" class="message-text" v-html="message.text"></div>
+            <!-- Options buttons -->
+            <div v-if="message.options && message.options.length > 0" class="options-container">
+              <button
+                v-for="(option, optIndex) in message.options"
+                :key="optIndex"
+                :style="{color: config.style.btnTextColor,backgroundColor: config.style.buttonColor}"
+                class="option-button"
+                :class="{ 'selected': isOptionSelected(message.id, option) }">
+                {{ option.text }}
+              </button>
+            </div>
+          </div>
+        </div>
+      </div>
+    </div>
+  </div>
+</template>
+
+<script>
+import agentAvatar from '@/assets/images/customer.png'
+import userAvatar from '@/assets/images/profile.png'
+
+export default {
+  name: "h5-chat",
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          style: {}
+        }
+      }
+    }
+  },
+  created() {
+    console.log("Messages:", this.messages);
+  },
+  data() {
+    return {
+      messages: this.config.messages,
+      userInput: "",
+      userAvatar: userAvatar, // Placeholder for user avatar
+      agentAvatar: agentAvatar, // Placeholder for agent avatar
+      selectedOptions: {}
+    };
+  },
+  watch: {
+    config: {
+      handler(newVal) {
+        console.log("Messages updated:", newVal);
+        this.messages = newVal.messages;
+      },
+      deep: true
+    }
+  },
+  computed: {
+    getAgentAvatar() {
+      if (this.config.style.avatar) {
+        return this.config.style.avatar;
+      }
+      return this.agentAvatar;
+    }
+  },
+  methods: {
+
+    isOptionSelected(messageId, option) {
+      const message = this.messages.find(m => m.id === messageId);
+      return message && message.userSelection && message.userSelection.id === option.id;
+    }
+  }
+};
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+p {
+  white-space: pre-line;
+}
+
+.active {
+  border-color: #02ff9b;
+}
+.chat-container {
+  width: 100%;
+  max-width: 500px;
+  display: flex;
+  flex-direction: column;
+  background-color: #f8f8f8;
+  font-family: Arial, sans-serif;
+}
+
+.chat-messages {
+  flex: 1;
+  overflow-y: auto;
+  padding: 16px;
+  display: flex;
+  flex-direction: column;
+}
+
+.message-wrapper {
+  display: flex;
+  margin-bottom: 16px;
+  max-width: 80%;
+}
+
+.agent-message {
+  align-self: flex-start;
+}
+
+.user-message {
+  align-self: flex-end;
+  flex-direction: row-reverse;
+}
+
+.avatar {
+  font-size: 14px;
+  display: flex;
+  padding-right: 5px;
+  padding-left: 0;
+  margin-bottom: 15px;
+}
+
+.avatar img {
+  width: 36px;
+  height: 36px;
+  border-radius: 50%;
+}
+
+.message-content {
+  display: flex;
+  flex-direction: column;
+}
+
+.message-text {
+  padding: 12px;
+  border-radius: 18px;
+  font-size: 14px;
+  line-height: 1.4;
+  word-break: break-word;
+}
+
+.agent-message .message-text {
+  background-color: white;
+  color: #333;
+  border-top-left-radius: 4px;
+}
+
+.user-message .message-text {
+  background-color: #4a90e2;
+  color: white;
+  border-top-right-radius: 4px;
+}
+
+.options-container {
+  display: flex;
+  flex-wrap: wrap;
+  gap: 8px;
+  margin-top: 8px;
+}
+
+.option-button {
+  background-color: #4a90e2;
+  color: white;
+  border: none;
+  border-radius: 18px;
+  padding: 8px 16px;
+  font-size: 14px;
+  cursor: pointer;
+  transition: background-color 0.2s;
+}
+
+.option-button:hover {
+  background-color: #3a80d2;
+}
+
+.option-button.selected {
+  background-color: #2a70c2;
+}
+
+.input-area {
+  display: flex;
+  padding: 12px;
+  border-top: 1px solid #e0e0e0;
+  background-color: white;
+}
+
+.message-input {
+  flex: 1;
+  padding: 10px;
+  border: 1px solid #e0e0e0;
+  border-radius: 20px;
+  margin-right: 8px;
+  font-size: 14px;
+}
+
+.send-button {
+  background-color: #4a90e2;
+  color: white;
+  border: none;
+  border-radius: 20px;
+  padding: 0 16px;
+  font-size: 14px;
+  cursor: pointer;
+}
+
+.send-button:hover {
+  background-color: #3a80d2;
+}
+
+/* For user selections displayed in chat */
+.user-message .message-content {
+  align-items: flex-end;
+}
+
+.user-selection {
+  background-color: #e6f3ff;
+  color: #4a90e2;
+  padding: 8px 16px;
+  border-radius: 18px;
+  font-size: 14px;
+  margin-top: 4px;
+}
+</style>

+ 79 - 0
src/components/H5/h5-countdown.vue

@@ -0,0 +1,79 @@
+<template>
+  <div :class="config.classText.join(' ')">
+      <div class="countdown2-box"
+           :style="{ backgroundImage: config.bgImage && config.bgImage !== '#' ? `url(${config.bgImage})` : '' }"
+      >
+        距结束<span id="days" :style="{background: config.mainColor}">{{ config.days }}</span>天
+        <span id="hours" :style="{background: config.mainColor}">{{ config.hours }}</span>时
+        <span id="minutes" :style="{background: config.mainColor}">{{ config.minutes }}</span>分
+        <span id="seconds" :style="{background: config.mainColor}">{{ config.seconds }}</span>秒
+      </div>
+  </div>
+</template>
+
+<script>
+
+export default {
+  name: 'h5-countdown',
+  props: {
+    config: {
+      type: Object,
+      default: () => {
+        return {
+          days: 'x',
+          hours: 'x',
+          minutes: 'x',
+          seconds: 'x',
+          classText: ['active'],
+          bgImage: '#',
+          mainColor:''
+        }
+      }
+    }
+  },
+  watch:{
+    'config.bgImage':{
+      handler(val){
+        console.info('bgImage change:',val)
+      },
+      deep:true
+    }
+  },
+  created() {
+    console.info('h5-text loaded:', this.config)
+  }
+}
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+p {
+  white-space: pre-line;
+}
+
+.active {
+  border-color: #02ff9b;
+}
+
+.countdown2-box {
+  background: url('#') no-repeat center;
+  background-size: 100%;
+  width: 100%;
+  height: 54px;
+  line-height: 54px;
+  color: #515A6E;
+  font-size: 14px;
+  text-align: center;
+}
+
+.countdown2-box span {
+  display: inline-block;
+  color: #fff;
+  width: 22px;
+  line-height: 20px;
+  text-align: center;
+  height: 20px;
+  border-radius: 3px;
+  margin: 7px;
+  background: #FF5A29;
+}
+</style>

+ 27 - 0
src/components/H5/h5-image.vue

@@ -0,0 +1,27 @@
+<template>
+  <div :class="config.classText.join(' ')">
+    <img :src="config.url || require('@/assets/images/default.jpg')" :style="config.style" />
+  </div>
+<!--  <img  src="@/assets/images/default.jpg" width="200" height="200" />-->
+</template>
+
+<script>
+export default {
+  name: "h5-image",
+  props: {
+    config: {
+      type: Object,
+      default: () => ({})
+    }
+  }
+};
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+img{
+  width: 100% !important;
+}
+.active{
+  border-color: #02ff9b;
+}
+</style>

+ 28 - 0
src/components/H5/h5-sep.vue

@@ -0,0 +1,28 @@
+<template>
+  <div :class="config.classText.join(' ')">
+  </div>
+</template>
+
+<script>
+export default {
+  name: "h5-sep",
+  props: {
+    config: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  created() {
+    console.info('h5-text loaded:', this.config);
+  }
+};
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+p{
+  white-space: pre-line;
+}
+.active{
+  border-color: #02ff9b;
+}
+</style>

+ 27 - 0
src/components/H5/h5-text.vue

@@ -0,0 +1,27 @@
+<template>
+    <p :class="config.classText.join(' ')" :style="config.style">{{ config.content }}</p>
+</template>
+
+<script>
+export default {
+  name: "h5-text",
+  props: {
+    config: {
+      type: Object,
+      default: () => ({})
+    }
+  },
+  created() {
+    console.info('h5-text loaded:', this.config);
+  }
+};
+</script>
+<style src="./css/base.css"></style>
+<style lang="scss" scoped>
+p{
+  white-space: pre-line;
+}
+.active{
+  border-color: #02ff9b;
+}
+</style>

+ 405 - 0
src/components/H5Editor/index.vue

@@ -0,0 +1,405 @@
+<template>
+  <div class="h5-editor-container">
+    <div class="h5-editor">
+    </div>
+    <el-row :gutter="24">
+      <el-col :span="2" class="button-body">
+        <div v-for="item in componentList">
+          <p/>
+          <div>
+            {{ item.groupName }}
+          </div>
+          <el-button class="button-tag" @click="add(item)" v-for="item in item.comps">
+            <!-- 添加图标和文字 -->
+            <i :class="item.icon" style="margin-right: 5px"></i>
+            {{ item.label }}
+          </el-button>
+        </div>
+      </el-col>
+      <el-col :span="12">
+        <div class="parent-container">
+          <div class="view-body">
+            <draggable v-model="list" @end="end">
+              <div v-for="(item, index) in list" :key="index" @click="select(index)" class="view-item">
+                <component :is="item.type" :config="item"/>
+              </div>
+            </draggable>
+          </div>
+        </div>
+      </el-col>
+      <el-col :span="10" style="overflow-y: auto;height:80vh;">
+        <form-wrapper
+          :form="form"
+          :index="index"
+          :list="list"
+          @update:form="updateForm"
+          @delete="del"
+        />
+      </el-col>
+    </el-row>
+  </div>
+</template>
+
+<script>
+import draggable from 'vuedraggable'
+import H5Text from '@/components/H5/h5-text.vue'
+import H5Image from '@/components/H5/h5-image.vue'
+import H5Button from '@/components/H5/h5-button.vue'
+import H5Sep from '@/components/H5/h5-sep.vue'
+import H5Countdown from '@/components/H5/h5-countdown.vue'
+import FormWrapper from '@/components/H5/FormWrapper.vue'
+import H5Chat from '@/components/H5/h5-chat.vue'
+import AgentAvatar from '@/assets/images/customer.png?inline'
+
+export default {
+  components: {
+    draggable, // 对应模板中的<h5-text>
+    H5Text, // 对应模板中的<h5-text>
+    H5Button, // 对应模板中的<h5-text>
+    H5Image,// 对应模板中的<h5-image>
+    H5Sep,// 对应模板中的<h5-sep>
+    H5Countdown,// 对应模板中的<h5-countdown>
+    FormWrapper,
+    H5Chat
+  },
+  mounted() {
+    this.initData(this.json)
+  },
+  props: {
+    json: {
+      type: String,
+      default: '[]'
+    }
+  },
+  data() {
+    return {
+      componentList: [{
+        groupName: '基础控件',
+        comps: [
+          {
+            type: 'h5-text',
+            label: '文本',
+            icon: 'el-icon-document'         // 文档图标更符合文本语义
+          },
+          {
+            type: 'h5-image',
+            label: '图片',
+            icon: 'el-icon-picture'          // 明确的图片图标
+          },
+          {
+            type: 'h5-sep',
+            label: '分割线',
+            icon: 'el-icon-minus'            // 减号图标模拟分割线效果
+          }
+        ]
+      }, {
+        groupName: '营销控件',
+        comps: [
+          {
+            type: 'h5-countdown',
+            label: '倒计时',
+            icon: 'el-icon-timer'            // 计时器图标表示倒计时
+          },
+          {
+            type: 'h5-chat',
+            label: '互动问答',
+            icon: 'el-icon-chat-square'     // 购物袋表示购买相关
+          }
+        ]
+      }],
+      list: [],
+      index: 0,
+      form: {}
+    }
+  },
+  methods: {
+    initData(json) {
+      this.list = JSON.parse(json)
+    },
+    end() {
+
+    },
+    add(item) {
+      let data = {
+        type: item.type,
+        active: false,
+        name: item.label,
+        fixe: false,
+        classText: ['parent-div'],
+        addWxFun: false,
+        workUrl: ''
+      }
+      if (item.type === 'h5-text') {
+        data.content = '默认文本'
+        data.style = {
+          textAlign: 'left',
+          fontSize: 20,
+          background: '#FFF',
+          color: '#000',
+          fontWeight: 'none',
+          fontStyle: 'none',
+          textDecoration: 'none'
+        }
+      } else if (item.type === 'h5-image') {
+        data.url = null
+      } else if (item.type === 'h5-button') {
+        data.content = '默认文本'
+      } else if (item.type === 'h5-sep') {
+        data.classText = [...data.classText, 'divider']
+        data.style = {
+          height: '10px',
+          background: 'rgb(228, 231, 237)'
+        }
+      } else if (item.type === 'h5-countdown') {
+        data.days = 0
+        data.hours = 0
+        data.minutes = 0
+        data.seconds = 0
+        data.countdownMode = 1
+      } else if (item.type === 'h5-chat') {
+        data = {...data,
+          ...{
+            messages: [
+              {
+                id: 1,
+                sender: "agent",
+                text: "您好!欢迎报名【中老年健康养生大讲堂】,请仔细答一下问题,便于帮您分配专业的老师进行指导。",
+                welcome: true
+              },
+              {
+                id: 2,
+                sender: "agent",
+                text: "您的年龄?",
+                options: [
+                  { id: 1, text: "45-55岁" },
+                  { id: 2, text: "55-60岁" },
+                  { id: 3, text: "60-65岁" },
+                  { id: 4, text: "65岁以上" }
+                ],
+                userSelection: null
+              },
+              {
+                id: 3,
+                sender: "user",
+                text: '45-55岁',
+                userSelection: { id: 1, text: "45-55岁" }
+              },
+              {
+                id: 4,
+                sender: "agent",
+                text: "更想通过课程学习到哪些知识?",
+                options: [
+                  { id: 1, text: "食疗食补" },
+                  { id: 2, text: "经络疏通" },
+                  { id: 3, text: "脏腑调养" },
+                  { id: 4, text: "启蒙养生" },
+                  { id: 5, text: "以上所有" }
+                ],
+                userSelection: null
+              },
+              {
+                id: 5,
+                sender: "user",
+                text: '食疗食补',
+                userSelection: { id: 1, text: "食疗食补" }
+              }
+            ],
+            agentMsg:[{
+              id: 1,
+              sender: "agent",
+              text: "您好!欢迎报名【中老年健康养生大讲堂】,请仔细答一下问题,便于帮您分配专业的老师进行指导。",
+              welcome: true
+            },
+              {
+                id: 2,
+                sender: "agent",
+                text: "您的年龄?",
+                options: [
+                  { id: 1, text: "45-55岁" },
+                  { id: 2, text: "55-60岁" },
+                  { id: 3, text: "60-65岁" },
+                  { id: 4, text: "65岁以上" }
+                ],
+                userSelection: null
+              },
+              {
+                id: 4,
+                sender: "agent",
+                text: "更想通过课程学习到哪些知识?",
+                options: [
+                  { id: 1, text: "食疗食补" },
+                  { id: 2, text: "经络疏通" },
+                  { id: 3, text: "脏腑调养" },
+                  { id: 4, text: "启蒙养生" },
+                  { id: 5, text: "以上所有" }
+                ],
+                userSelection: null
+              }
+              ],
+            style: {
+              avatar: window.location.origin+AgentAvatar,
+              buttonColor: '#409EFF',
+              textColor: ''
+            }
+          }
+        }
+        console.log(data)
+        let h5chat = this.list.some(item => item.type && item.type.includes('h5-chat'))
+        if (h5chat) {
+          alert('全局只能允许有一个互动问答!')
+          return
+        }
+      }
+      console.log(this.list)
+
+      if (this.index !== null && this.index >= 0 && this.index < this.list.length) {
+        this.list.splice(this.index + 1, 0, data)
+      } else {
+        this.list.push(data)
+      }
+    },
+    select(index) {
+      this.index = index
+      let className = 'active'
+      this.clearClass(className)
+      this.addClass(className)
+      this.$set(this.list[index], 'active', true)  // 确保响应式更新
+      // 直接绑定list中的对象(关键修改)
+      this.form = this.list[index]
+    },
+    clearClass(classText) {
+      this.list.forEach(item => {
+        // 推荐使用 $set
+        this.$set(item, 'classText', item.classText.filter(c => c !== classText))
+      })
+    },
+    addClass(classText) {
+      this.list[this.index].classText.push(classText)
+    },
+
+    del() {
+      this.list.splice(this.index, 1)
+      this.form = {}
+    },
+    updateForm(newForm) {
+      // 更新表单数据
+      if (this.index === undefined || !this.list[this.index]) {
+        console.error('Invalid index or list item')
+        return
+      }
+
+      console.log('newForm:', newForm) // 检查 newForm 数据
+      Object.assign(this.form, newForm)
+
+      // 更新列表中的数据
+      for (const key in newForm) {
+        if (Object.hasOwnProperty.call(newForm, key)) {
+          // 如果是嵌套对象,递归更新
+          if (typeof newForm[key] === 'object' && newForm[key] !== null) {
+            for (const nestedKey in newForm[key]) {
+              this.$set(this.list[this.index][key], nestedKey, newForm[key][nestedKey])
+            }
+          } else {
+            this.$set(this.list[this.index], key, newForm[key])
+          }
+        }
+      }
+
+      console.log('Updated list:', this.list[this.index])
+    }
+  }
+}
+</script>
+<style lang="scss" scoped>
+.button-body {
+  text-align: center;
+  margin: 0 auto;
+
+  .button-tag:first-child {
+    margin-top: 0 !important;
+  }
+
+  .button-tag {
+    margin-left: 0;
+    margin-top: 10px;
+  }
+}
+
+/* 外层容器 (父级div) */
+.parent-container {
+  background: #eff3f5;
+  padding: 40px 0;
+  width: 100%;
+
+  /* 关键设置 */
+  height: 80vh;
+  overflow-y: auto; /* 滚动条出在这里 */
+  border: 1px solid #DCDEE2;
+}
+/* 定制滚动条样式 */
+.parent-container::-webkit-scrollbar {
+  width: 6px; /* 滚动条宽度 */
+}
+
+.parent-container::-webkit-scrollbar-track {
+  background: #f1f1f1; /* 滚动条轨道背景色 */
+  border-radius: 3px; /* 轨道圆角 */
+}
+
+.parent-container::-webkit-scrollbar-thumb {
+  background: #888; /* 滚动条滑块颜色 */
+  border-radius: 3px; /* 滑块圆角 */
+}
+
+.parent-container::-webkit-scrollbar-thumb:hover {
+  background: #555; /* 滑块悬停颜色 */
+}
+.view-body {
+  width: 375px; /* 明确具体值(会覆盖下面的width:100%) */
+  height: auto; /* 改掉height:100%,否则会根据父级高度计算*/
+
+  /* 清除非必须设置 */
+  overflow-y: visible; /* 保持默认 */
+
+  /* 其他属性 */
+  margin: 0 auto; /* 代替外层flex的justify-content */
+  background: #fff;
+  position: relative;
+  padding: 0;
+}
+
+.icon.svg {
+  cursor: pointer;
+  margin-left: 20px;
+  width: 20px;
+  height: 20px;
+}
+
+.icon.svg.active {
+  color: #02ff9b;
+}
+
+.icon-span {
+}
+
+.divider {
+  width: 100%;
+  background: #DCDEE2;
+  height: 10px;
+}
+
+.button-tag {
+  /* 新增图标样式 */
+  .el-icon {
+    margin-right: 6px; /* 图标与文本间距 */
+    font-size: 16px; /* 统一图标大小 */
+    vertical-align: -2px; /* 垂直居中补偿 */
+  }
+
+  &.is-plain .el-icon {
+    color: #666; /* 浅色主题下保持可视性 */
+  }
+}
+
+
+</style>

+ 132 - 60
src/components/ImageUpload/index.vue

@@ -1,7 +1,6 @@
 <template>
   <div class="component-upload-image">
     <el-upload
-      multiple
       :action="uploadImgUrl"
       list-type="picture-card"
       :on-success="handleUploadSuccess"
@@ -9,24 +8,23 @@
       :limit="limit"
       :on-error="handleUploadError"
       :on-exceed="handleExceed"
-      ref="imageUpload"
-      :on-remove="handleDelete"
+      name="file"
+      :on-remove="handleRemove"
       :show-file-list="true"
       :file-list="fileList"
       :on-preview="handlePictureCardPreview"
       :class="{hide: this.fileList.length >= this.limit}"
-      :data="{type: 1}"
     >
       <i class="el-icon-plus"></i>
     </el-upload>
 
     <!-- 上传提示 -->
-    <div class="el-upload__tip" slot="tip" v-if="showTip">
+<!--    <div class="el-upload__tip" slot="tip" v-if="showTip">
       请上传
       <template v-if="fileSize"> 大小不超过 <b style="color: #f56c6c">{{ fileSize }}MB</b> </template>
       <template v-if="fileType"> 格式为 <b style="color: #f56c6c">{{ fileType.join("/") }}</b> </template>
       的文件
-    </div>
+    </div> -->
 
     <el-dialog
       :visible.sync="dialogVisible"
@@ -44,7 +42,8 @@
 
 <script>
 import { getToken } from "@/utils/auth";
-import FormMakingCommon from "form-making";
+
+import { Loading } from 'element-ui';
 
 export default {
   props: {
@@ -52,12 +51,12 @@ export default {
     // 图片数量限制
     limit: {
       type: Number,
-      default: 5,
+      default: 10,
     },
     // 大小限制(MB)
     fileSize: {
        type: Number,
-      default: 5,
+      default: 500,
     },
     // 文件类型, 例如['png', 'jpg', 'jpeg']
     fileType: {
@@ -72,13 +71,12 @@ export default {
   },
   data() {
     return {
-      number: 0,
-      uploadList: [],
+      finalQuality:1,
       dialogImageUrl: "",
       dialogVisible: false,
       hideUpload: false,
       baseUrl: process.env.VUE_APP_BASE_API,
-      uploadImgUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS", // 上传的图片服务器地址
+      uploadImgUrl: process.env.VUE_APP_BASE_API+"/common/uploadOSS", // 上传的图片服务器地址
       headers: {
         Authorization: "Bearer " + getToken(),
       },
@@ -94,7 +92,11 @@ export default {
           // 然后将数组转为对象数组
           this.fileList = list.map(item => {
             if (typeof item === "string") {
-              item = { name: item, url: item };
+              if (item.indexOf(this.baseUrl) === -1) {
+                  item = { name: item, url: item };
+              } else {
+                  item = { name: item, url: item };
+              }
             }
             return item;
           });
@@ -114,6 +116,21 @@ export default {
     },
   },
   methods: {
+    // 删除图片
+    handleRemove(file, fileList) {
+      const findex = this.fileList.map(f => f.name).indexOf(file.name);
+      if(findex > -1) {
+        this.fileList.splice(findex, 1);
+        this.$emit("input", this.listToString(this.fileList));
+      }
+    },
+    // 上传成功回调
+    handleUploadSuccess(res) {
+      console.log(res)
+      this.fileList.push({ name: res.url, url: res.url });
+      this.$emit("input", this.listToString(this.fileList));
+      this.loading.close();
+    },
     // 上传前loading加载
     handleBeforeUpload(file) {
       let isImg = false;
@@ -132,64 +149,121 @@ export default {
       }
 
       if (!isImg) {
-        this.$message.error(`文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`);
+        this.$message.error(
+          `文件格式不正确, 请上传${this.fileType.join("/")}图片格式文件!`
+        );
         return false;
       }
-      if (this.fileSize) {
-        const isLt = file.size / 1024 / 1024 < this.fileSize;
-        if (!isLt) {
-          this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} MB!`);
-          return false;
+      return new Promise((resolve, reject) => {
+        if (file.size / 1024 / 1024 > 3) {
+          this.$message.error('上传的图片不能超过3MB');
+          reject();
+          return;
         }
-      }
-      // this.$message.loading("正在上传图片,请稍候...");
+        if (file.size / 1024 / 1024 > 1) {
+          const loadingInstance = Loading.service({ text: '图片内存过大正在压缩图片...' });
+          // 文件大于1MB时进行压缩
+          this.compressImage(file).then((compressedFile) => {
+            loadingInstance.close();
+            if (compressedFile.size / 1024 > 500) {
+              this.$message.error('图片压缩后仍大于500KB');
+              reject();
+            } else {
+              // this.$message.success(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`最终内存大小为: ${(compressedFile.size/1024).toFixed(2)}KB`);
+              resolve(compressedFile);
+            }
+          }).catch((err) => {
+            loadingInstance.close();
+            console.error(err);
+            reject();
+          });
+        } else {
+          resolve(file);
+        }
+        this.loading = this.$loading({
+          lock: true,
+          text: "上传中",
+          background: "rgba(0, 0, 0, 0.7)",
+        });
+      });
+      // if (this.fileSize) {
+      //   const isLt = file.size / 1024  < this.fileSize;
+      //   if (!isLt) {
+      //     this.$message.error(`上传头像图片大小不能超过 ${this.fileSize} KB!`);
+      //     return false;
+      //   }
+      // }
+      
+    },
+    compressImage(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.onload = (event) => {
+          const img = new Image();
+          img.src = event.target.result;
+          img.onload = () => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+            const width = img.width;
+            const height = img.height;
+            canvas.width = width;
+            canvas.height = height;
+            ctx.drawImage(img, 0, 0, width, height);
 
-      this.number++;
+            let quality = 1; // 初始压缩质量
+            let dataURL = canvas.toDataURL('image/jpeg', quality);
+            
+            // 逐步压缩,直到图片大小小于500KB并且压缩质量不再降低
+            while (dataURL.length / 1024 > 500 && quality > 0.1) {
+              quality -= 0.01;
+              dataURL = canvas.toDataURL('image/jpeg', quality);
+            }
+            this.finalQuality = quality; // 存储最终的压缩质量
+            
+            if (dataURL.length / 1024 > 500) {
+              reject(new Error('压缩后图片仍然大于500KB'));
+              return;
+            }
+
+            const arr = dataURL.split(',');
+            const mime = arr[0].match(/:(.*?);/)[1];
+            const bstr = atob(arr[1]);
+            let n = bstr.length;
+            const u8arr = new Uint8Array(n);
+            while (n--) {
+              u8arr[n] = bstr.charCodeAt(n);
+            }
+            const compressedFile = new Blob([u8arr], { type: mime });
+            compressedFile.name = file.name;
+            resolve(compressedFile);
+          };
+          img.onerror = (error) => {
+            reject(error);
+          };
+        };
+        reader.onerror = (error) => {
+          reject(error);
+        };
+      });
     },
     // 文件个数超出
     handleExceed() {
       this.$message.error(`上传文件数量不能超过 ${this.limit} 个!`);
     },
-    // 上传成功回调
-    handleUploadSuccess(res, file) {
-      console.log(res)
-      console.log(file)
-      if (res.code === 200) {
-        this.uploadList.push({ name: file.name, url: res.url });
-        this.uploadedSuccessfully();
-      } else {
-        this.number--;
-        this.$message.closeLoading();
-        this.$message.error(res.msg);
-        this.$refs.imageUpload.handleRemove(file);
-        this.uploadedSuccessfully();
-      }
-    },
-    // 删除图片
-    handleDelete(file) {
-      const findex = this.fileList.map(f => f.name).indexOf(file.name);
-      if(findex > -1) {
-        this.fileList.splice(findex, 1);
-        this.$emit("input", this.listToString(this.fileList));
-      }
-    },
     // 上传失败
     handleUploadError() {
-      this.$message.error("上传图片失败,请重试");
-      this.$message.closeLoading();
-    },
-    // 上传结束处理
-    uploadedSuccessfully() {
-      if (this.number > 0 && this.uploadList.length === this.number) {
-        this.fileList = this.fileList.concat(this.uploadList);
-        this.uploadList = [];
-        this.number = 0;
-        this.$emit("input", this.listToString(this.fileList));
-        //this.$message.closeLoading();
-      }
+      this.$message({
+        type: "error",
+        message: "上传失败",
+      });
+      this.loading.close();
     },
     // 预览
     handlePictureCardPreview(file) {
+      console.log(file)
       this.dialogImageUrl = file.url;
       this.dialogVisible = true;
     },
@@ -198,9 +272,7 @@ export default {
       let strs = "";
       separator = separator || ",";
       for (let i in list) {
-        if (list[i].url) {
-          strs += list[i].url.replace(this.baseUrl, "") + separator;
-        }
+        strs += list[i].url.replace(this.baseUrl, "") + separator;
       }
       return strs != '' ? strs.substr(0, strs.length - 1) : '';
     }

+ 98 - 7
src/components/Material/index.vue

@@ -34,12 +34,14 @@
     </el-dialog>
     <!-- 素材列表 -->
     <el-dialog
+    @close="clos"
       title="图片素材库"
       append-to-body
       :visible.sync="listDialogVisible"
       width="70%"
+
     >
-      <el-container>
+      <el-container style="height: 600px;">
         <el-aside width="unset">
           <div style="margin-bottom: 10px">
             <el-button
@@ -124,7 +126,7 @@
                   :limit.sync="queryParams.pageSize"
                   @pagination="getMaterialList"
                 />
-               
+
             </div>
           </el-card>
         </el-main>
@@ -140,6 +142,7 @@
 <script>
 import { listMaterial, getMaterial, delMaterial, addMaterial, updateMaterial, exportMaterial } from "@/api/store/material";
 import { getAllMaterialGroup,listMaterialGroup, getMaterialGroup, delMaterialGroup, addMaterialGroup, updateMaterialGroup, exportMaterialGroup } from "@/api/store/materialGroup";
+import { Loading } from 'element-ui';
 export default {
   name: 'ImageSelect',
   props: {
@@ -178,6 +181,7 @@ export default {
   },
   data() {
     return {
+      finalQuality:1,
       uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
       dialogVisible: false,
       url: '',
@@ -206,6 +210,9 @@ export default {
     this.getAllMaterialGroup();
   },
   methods: {
+    clos(){
+      this.urls=[];
+    },
     selectGroup(item){
       this.materialGroup=item;
       this.queryParams.groupId=item.groupId;
@@ -344,21 +351,105 @@ export default {
         file.type === 'image/png' ||
         file.type === 'image/gif' ||
         file.type === 'image/jpg'
-      const isLt2M = file.size / 1024 / 1024 < 2
+      // const isLt2M = file.size / 1024 / 1024 < 2
       if (!isPic) {
         this.$message.error('上传图片只能是 JPG、JPEG、PNG、GIF 格式!')
         return false
       }
-      if (!isLt2M) {
-        this.$message.error('上传头像图片大小不能超过 2MB!')
-      }
-      return isPic && isLt2M
+      return new Promise((resolve, reject) => {
+        if (file.size / 1024 / 1024 > 3) {
+          this.$message.error('上传的图片不能超过3MB');
+          reject();
+          return;
+        }
+        if (file.size / 1024 / 1024 > 1) {
+          const loadingInstance = Loading.service({ text: '图片内存过大正在压缩图片...' });
+          // 文件大于1MB时进行压缩
+          this.compressImage(file).then((compressedFile) => {
+            loadingInstance.close();
+            if (compressedFile.size / 1024 > 500) {
+              this.$message.error('图片压缩后仍大于500KB');
+              reject();
+            } else {
+              // this.$message.success(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`最终内存大小为: ${(compressedFile.size/1024).toFixed(2)}KB`);
+              resolve(compressedFile);
+            }
+          }).catch((err) => {
+            loadingInstance.close();
+            console.error(err);
+            reject();
+          });
+        } else {
+          resolve(file);
+        }
+        // this.loading = this.$loading({
+        //   lock: true,
+        //   text: "上传中",
+        //   background: "rgba(0, 0, 0, 0.7)",
+        // });
+      });
+    },
+    compressImage(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.onload = (event) => {
+          const img = new Image();
+          img.src = event.target.result;
+          img.onload = () => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+            const width = img.width;
+            const height = img.height;
+            canvas.width = width;
+            canvas.height = height;
+            ctx.drawImage(img, 0, 0, width, height);
+
+            let quality = 1; // 初始压缩质量
+            let dataURL = canvas.toDataURL('image/jpeg', quality);
+            
+            // 逐步压缩,直到图片大小小于500KB并且压缩质量不再降低
+            while (dataURL.length / 1024 > 500 && quality > 0.1) {
+              quality -= 0.01;
+              dataURL = canvas.toDataURL('image/jpeg', quality);
+            }
+            this.finalQuality = quality; // 存储最终的压缩质量
+            
+            if (dataURL.length / 1024 > 500) {
+              reject(new Error('压缩后图片仍然大于500KB'));
+              return;
+            }
+
+            const arr = dataURL.split(',');
+            const mime = arr[0].match(/:(.*?);/)[1];
+            const bstr = atob(arr[1]);
+            let n = bstr.length;
+            const u8arr = new Uint8Array(n);
+            while (n--) {
+              u8arr[n] = bstr.charCodeAt(n);
+            }
+            const compressedFile = new Blob([u8arr], { type: mime });
+            compressedFile.name = file.name;
+            resolve(compressedFile);
+          };
+          img.onerror = (error) => {
+            reject(error);
+          };
+        };
+        reader.onerror = (error) => {
+          reject(error);
+        };
+      });
     },
     submit() {
+
       this.urls.forEach(item => {
         this.$set(this.value, this.value.length, item)
       })
       this.listDialogVisible = false
+      this.urls = []
     }
   }
 }

+ 91 - 6
src/components/Material/single.vue

@@ -117,7 +117,7 @@
                   :limit.sync="queryParams.pageSize"
                   @pagination="getMaterialList"
                 />
-               
+
             </div>
           </el-card>
         </el-main>
@@ -133,6 +133,7 @@
 <script>
 import { listMaterial, getMaterial, delMaterial, addMaterial, updateMaterial, exportMaterial } from "@/api/store/material";
 import { getAllMaterialGroup,listMaterialGroup, getMaterialGroup, delMaterialGroup, addMaterialGroup, updateMaterialGroup, exportMaterialGroup } from "@/api/store/materialGroup";
+import { Loading } from 'element-ui';
 export default {
   name: 'ImageSelect',
   props: {
@@ -173,6 +174,7 @@ export default {
   },
   data() {
     return {
+      finalQuality:1,
       myValue: this.value,
       uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
       dialogVisible: false,
@@ -329,15 +331,98 @@ export default {
         this.$message.error('上传图片只能是 JPG、JPEG、PNG、GIF 格式!')
         return false
       }
-      if (!isLt2M) {
-        this.$message.error('上传头像图片大小不能超过 2MB!')
-      }
-      return isPic && isLt2M
+      return new Promise((resolve, reject) => {
+        if (file.size / 1024 / 1024 > 3) {
+          this.$message.error('上传的图片不能超过3MB');
+          reject();
+          return;
+        }
+        if (file.size / 1024 / 1024 > 1) {
+          const loadingInstance = Loading.service({ text: '图片内存过大正在压缩图片...' });
+          // 文件大于1MB时进行压缩
+          this.compressImage(file).then((compressedFile) => {
+            loadingInstance.close();
+            if (compressedFile.size / 1024 > 500) {
+              this.$message.error('图片压缩后仍大于500KB');
+              reject();
+            } else {
+              // this.$message.success(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`图片压缩成功,最终质量为: ${this.finalQuality.toFixed(2)}`);
+              console.log(`最终内存大小为: ${(compressedFile.size/1024).toFixed(2)}KB`);
+              resolve(compressedFile);
+            }
+          }).catch((err) => {
+            loadingInstance.close();
+            console.error(err);
+            reject();
+          });
+        } else {
+          resolve(file);
+        }
+        //  this.loading = this.$loading({
+        //   lock: true,
+        //   text: "上传中",
+        //   background: "rgba(0, 0, 0, 0.7)",
+        // });
+      });
+    },
+    compressImage(file) {
+      return new Promise((resolve, reject) => {
+        const reader = new FileReader();
+        reader.readAsDataURL(file);
+        reader.onload = (event) => {
+          const img = new Image();
+          img.src = event.target.result;
+          img.onload = () => {
+            const canvas = document.createElement('canvas');
+            const ctx = canvas.getContext('2d');
+            const width = img.width;
+            const height = img.height;
+            canvas.width = width;
+            canvas.height = height;
+            ctx.drawImage(img, 0, 0, width, height);
+
+            let quality = 1; // 初始压缩质量
+            let dataURL = canvas.toDataURL('image/jpeg', quality);
+
+            // 逐步压缩,直到图片大小小于500KB并且压缩质量不再降低
+            while (dataURL.length / 1024 > 500 && quality > 0.1) {
+              quality -= 0.01;
+              dataURL = canvas.toDataURL('image/jpeg', quality);
+            }
+            this.finalQuality = quality; // 存储最终的压缩质量
+
+            if (dataURL.length / 1024 > 500) {
+              reject(new Error('压缩后图片仍然大于500KB'));
+              return;
+            }
+
+            const arr = dataURL.split(',');
+            const mime = arr[0].match(/:(.*?);/)[1];
+            const bstr = atob(arr[1]);
+            let n = bstr.length;
+            const u8arr = new Uint8Array(n);
+            while (n--) {
+              u8arr[n] = bstr.charCodeAt(n);
+            }
+            const compressedFile = new Blob([u8arr], { type: mime });
+            compressedFile.name = file.name;
+            resolve(compressedFile);
+          };
+          img.onerror = (error) => {
+            reject(error);
+          };
+        };
+        reader.onerror = (error) => {
+          reject(error);
+        };
+      });
     },
     submit() {
       this.myValue = this.urls[0]
       this.$emit('input', this.urls[0])
       this.listDialogVisible = false
+    this.urls = []
     }
   }
 }
@@ -364,6 +449,6 @@ export default {
   }
   .group-item{
     margin: 5px;
-    
+
   }
 </style>

+ 128 - 0
src/components/MinimizableDialog/index.vue

@@ -0,0 +1,128 @@
+<template>
+  <div>
+    <!-- 正常弹窗 -->
+    <el-dialog
+      v-show="!minimized"
+      :visible.sync="visibleSync"
+      :title="title"
+      v-bind="$attrs"
+      v-on="$listeners"
+    >
+      <template #title>
+        <span>{{ title }}</span>
+        <i
+          class="el-icon-minus"
+          style="cursor:pointer;float:right;margin-right:35px;"
+          @click.stop="minimize"
+        ></i>
+      </template>
+      <slot />
+      <template #footer>
+        <slot name="footer" />
+      </template>
+    </el-dialog>
+
+    <!-- 最小化悬浮卡片 -->
+    <div
+      v-show="minimized"
+      class="minimized-dialog"
+      ref="minCard"
+      @click="restore"
+    >
+      <i :class="minIcon"></i>
+      <span>{{ minTitle || title }}</span>
+    </div>
+  </div>
+</template>
+
+<script>
+export default {
+  name: "MinimizableDialog",
+  props: {
+    visible: {
+      type: Boolean,
+      required: true
+    },
+    title: {
+      type: String,
+      default: ""
+    },
+    minTitle: {
+      type: String,
+      default: ""
+    },
+    minIcon: {
+      type: String,
+      default: "el-icon-files"
+    }
+  },
+  data() {
+    return {
+      minimized: false,
+      visibleSync: this.visible
+    };
+  },
+  watch: {
+    visible(val) {
+      this.visibleSync = val;
+      if (val) this.minimized = false;
+    },
+    visibleSync(val) {
+      this.$emit("update:visible", val);
+    }
+  },
+  mounted() {
+    this.moveMinCardToBody();
+  },
+  beforeDestroy() {
+    this.removeMinCardFromBody();
+  },
+  methods: {
+    moveMinCardToBody() {
+      if (this.$refs.minCard && typeof window !== 'undefined') {
+        document.body.appendChild(this.$refs.minCard);
+      }
+    },
+    removeMinCardFromBody() {
+      if (this.$refs.minCard && this.$refs.minCard.parentNode) {
+        this.$refs.minCard.parentNode.removeChild(this.$refs.minCard);
+      }
+    },
+    minimize() {
+      this.minimized = true;
+      this.visibleSync = false;
+      this.$emit("minimize");
+    },
+    restore() {
+      this.minimized = false;
+      this.visibleSync = true;
+      this.$emit("restore");
+    },
+  }
+};
+</script>
+
+<style scoped>
+.minimized-dialog {
+  position: fixed;
+  right: 30px;
+  bottom: 30px;
+  background: #409EFF;
+  color: #fff;
+  padding: 10px 18px;
+  border-radius: 20px;
+  box-shadow: 0 2px 8px rgba(0,0,0,0.15);
+  cursor: pointer;
+  z-index: 1000;
+  display: flex;
+  align-items: center;
+  font-size: 15px;
+}
+.minimized-dialog:hover {
+  box-shadow: 0 4px 16px rgba(0,0,0,0.25);
+}
+.minimized-dialog i {
+  margin-right: 8px;
+  font-size: 18px;
+}
+</style>

+ 7 - 1
src/components/Pagination/index.vue

@@ -6,6 +6,7 @@
       :page-size.sync="pageSize"
       :layout="layout"
       :page-sizes="pageSizes"
+      :pager-count="pagerCount"
       :total="total"
       v-bind="$attrs"
       @size-change="handleSizeChange"
@@ -35,9 +36,14 @@ export default {
     pageSizes: {
       type: Array,
       default() {
-        return [5,10, 20, 30, 50,100]
+        return [10, 20, 30, 50]
       }
     },
+    // 移动端页码按钮的数量端默认值5
+    pagerCount: {
+      type: Number,
+      default: document.body.clientWidth < 992 ? 5 : 7
+    },
     layout: {
       type: String,
       default: 'total, sizes, prev, pager, next, jumper'

+ 54 - 5
src/components/RightToolbar/index.vue

@@ -1,4 +1,3 @@
-<!-- @author Shiyn/   huangmx 20200807优化-->
 <template>
   <div class="top-right-btn">
     <el-row>
@@ -8,31 +7,81 @@
       <el-tooltip class="item" effect="dark" content="刷新" placement="top">
         <el-button size="mini" circle icon="el-icon-refresh" @click="refresh()" />
       </el-tooltip>
+      <el-tooltip class="item" effect="dark" content="显隐列" placement="top" v-if="columns">
+        <el-button size="mini" circle icon="el-icon-menu" @click="showColumn()" />
+      </el-tooltip>
     </el-row>
+    <el-dialog :title="title" :visible.sync="open" append-to-body>
+      <el-transfer
+        :titles="['显示', '隐藏']"
+        v-model="value"
+        :data="columns"
+        @change="dataChange"
+      ></el-transfer>
+    </el-dialog>
   </div>
 </template>
 <script>
 export default {
   name: "RightToolbar",
   data() {
-    return {};
+    return {
+      // 显隐数据
+      value: [],
+      // 弹出层标题
+      title: "显示/隐藏",
+      // 是否显示弹出层
+      open: false,
+    };
   },
   props: {
     showSearch: {
       type: Boolean,
       default: true,
     },
+    columns: {
+      type: Array,
+    },
+  },
+  created() {
+    // 显隐列初始默认隐藏列
+    for (let item in this.columns) {
+      if (this.columns[item].visible === false) {
+        this.value.push(parseInt(item));
+      }
+    }
   },
-
   methods: {
-    //搜索
+    // 搜索
     toggleSearch() {
       this.$emit("update:showSearch", !this.showSearch);
     },
-    //刷新
+    // 刷新
     refresh() {
       this.$emit("queryTable");
     },
+    // 右侧列表元素变化
+    dataChange(data) {
+      for (var item in this.columns) {
+        const key = this.columns[item].key;
+        this.columns[item].visible = !data.includes(key);
+      }
+    },
+    // 打开显隐列dialog
+    showColumn() {
+      this.open = true;
+    },
   },
 };
 </script>
+<style lang="scss" scoped>
+::v-deep .el-transfer__button {
+  border-radius: 50%;
+  padding: 12px;
+  display: block;
+  margin-left: 0px;
+}
+::v-deep .el-transfer__button:first-child {
+  margin-bottom: 10px;
+}
+</style>

+ 4 - 7
src/components/Screenfull/index.vue

@@ -22,11 +22,8 @@ export default {
   },
   methods: {
     click() {
-      if (!screenfull.enabled) {
-        this.$message({
-          message: 'you browser can not work',
-          type: 'warning'
-        })
+      if (!screenfull.isEnabled) {
+        this.$message({ message: '你的浏览器不支持全屏', type: 'warning' })
         return false
       }
       screenfull.toggle()
@@ -35,12 +32,12 @@ export default {
       this.isFullscreen = screenfull.isFullscreen
     },
     init() {
-      if (screenfull.enabled) {
+      if (screenfull.isEnabled) {
         screenfull.on('change', this.change)
       }
     },
     destroy() {
-      if (screenfull.enabled) {
+      if (screenfull.isEnabled) {
         screenfull.off('change', this.change)
       }
     }

+ 12 - 14
src/components/ThemePicker/index.vue

@@ -31,19 +31,21 @@ export default {
       immediate: true
     },
     async theme(val) {
+      await this.setTheme(val)
+    }
+  },
+  created() {
+    if(this.defaultTheme !== ORIGINAL_THEME) {
+      this.setTheme(this.defaultTheme)
+    }
+  },
+
+  methods: {
+    async setTheme(val) {
       const oldVal = this.chalk ? this.theme : ORIGINAL_THEME
       if (typeof val !== 'string') return
       const themeCluster = this.getThemeCluster(val.replace('#', ''))
       const originalCluster = this.getThemeCluster(oldVal.replace('#', ''))
-      console.log(themeCluster, originalCluster)
-
-      const $message = this.$message({
-        message: '  Compiling the theme',
-        customClass: 'theme-message',
-        type: 'success',
-        duration: 0,
-        iconClass: 'el-icon-loading'
-      })
 
       const getHandler = (variable, id) => {
         return () => {
@@ -81,12 +83,8 @@ export default {
       })
 
       this.$emit('change', val)
+    },
 
-      $message.close()
-    }
-  },
-
-  methods: {
     updateStyle(style, oldCluster, newCluster) {
       let newStyle = style
       oldCluster.forEach((color, index) => {

+ 86 - 35
src/components/TopNav/index.vue

@@ -5,15 +5,15 @@
     @select="handleSelect"
   >
     <template v-for="(item, index) in topMenus">
-      <el-menu-item :index="item.path" :key="index" v-if="index < visibleNumber"
+      <el-menu-item :style="{'--theme': theme}" :index="item.path" :key="index" v-if="index < visibleNumber"
         ><svg-icon :icon-class="item.meta.icon" />
         {{ item.meta.title }}</el-menu-item
       >
     </template>
 
     <!-- 顶部菜单超出数量折叠 -->
-    <el-submenu index="more" v-if="topMenus.length > visibleNumber">
-      <template slot="title">更多功能</template>
+    <el-submenu :style="{'--theme': theme}" index="more" v-if="topMenus.length > visibleNumber">
+      <template slot="title">更多菜单</template>
       <template v-for="(item, index) in topMenus">
         <el-menu-item
           :index="item.path"
@@ -34,18 +34,31 @@ export default {
   data() {
     return {
       // 顶部栏初始数
-      visibleNumber: 5,
+      visibleNumber: 8,
       // 是否为首次加载
       isFrist: false,
+      // 当前激活菜单的 index
+      currentIndex: undefined
     };
   },
   computed: {
+    theme() {
+      return this.$store.state.settings.theme;
+    },
     // 顶部显示菜单
     topMenus() {
-      return this.routers.map((menu) => ({
-        ...menu,
-        children: undefined,
-      }));
+      let topMenus = [];
+      this.routers.map((menu) => {
+        if (menu.hidden !== true) {
+          // 兼容顶部栏一级菜单内部跳转
+          if (menu.path === "/") {
+              topMenus.push(menu.children[0]);
+          } else {
+              topMenus.push(menu);
+          }
+        }
+      });
+      return topMenus;
     },
     // 所有的路由信息
     routers() {
@@ -57,7 +70,13 @@ export default {
       this.routers.map((router) => {
         for (var item in router.children) {
           if (router.children[item].parentPath === undefined) {
-            router.children[item].path = router.path + "/" + router.children[item].path;
+            if(router.path === "/") {
+              router.children[item].path = "/redirect/" + router.children[item].path;
+            } else {
+              if(!this.ishttp(router.children[item].path)) {
+                router.children[item].path = router.path + "/" + router.children[item].path;
+              }
+            }
             router.children[item].parentPath = router.path;
           }
           childrenMenus.push(router.children[item]);
@@ -68,7 +87,7 @@ export default {
     // 默认激活的菜单
     activeMenu() {
       const path = this.$route.path;
-      let activePath = this.routers[0].path;
+      let activePath = this.defaultRouter();
       if (path.lastIndexOf("/") > 0) {
         const tmpPath = path.substring(1, path.length);
         activePath = "/" + tmpPath.substring(0, tmpPath.indexOf("/"));
@@ -79,32 +98,51 @@ export default {
           activePath = "index";
         }
       }
-      this.activeRoutes(activePath);
+      var routes = this.activeRoutes(activePath);
+      if (routes.length === 0) {
+        activePath = this.currentIndex || this.defaultRouter()
+        this.activeRoutes(activePath);
+      }
       return activePath;
     },
   },
+  beforeMount() {
+    window.addEventListener('resize', this.setVisibleNumber)
+  },
+  beforeDestroy() {
+    window.removeEventListener('resize', this.setVisibleNumber)
+  },
   mounted() {
-    this.setVisibleNumber();
+    //this.setVisibleNumber();
   },
   methods: {
     // 根据宽度计算设置显示栏数
     setVisibleNumber() {
-      const width = document.body.getBoundingClientRect().width - 200;
-      const elWidth = this.$el.getBoundingClientRect().width;
-      const menuItemNodes = this.$el.children;
-      const menuWidth = Array.from(menuItemNodes).map(
-        (i) => i.getBoundingClientRect().width
-      );
-      this.visibleNumber = (
-        parseInt(width - elWidth) / parseInt(menuWidth)
-      ).toFixed(0);
+      const width = document.body.getBoundingClientRect().width / 3;
+      this.visibleNumber = parseInt(width / 85);
+    },
+    // 默认激活的路由
+    defaultRouter() {
+      let router;
+      Object.keys(this.routers).some((key) => {
+        if (!this.routers[key].hidden) {
+          router = this.routers[key].path;
+          return true;
+        }
+      });
+      return router;
     },
     // 菜单选择事件
     handleSelect(key, keyPath) {
-      if (key.indexOf("http://") !== -1 || key.indexOf("https://") !== -1) {
+      this.currentIndex = key;
+      if (this.ishttp(key)) {
         // http(s):// 路径新窗口打开
         window.open(key, "_blank");
+      } else if (key.indexOf("/redirect") !== -1) {
+        // /redirect 路径内部打开
+        this.$router.push({ path: key.replace("/redirect", "") });
       } else {
+        // 显示左侧联动菜单
         this.activeRoutes(key);
       }
     },
@@ -118,27 +156,40 @@ export default {
           }
         });
       }
-      // console.log(routes)
-      this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
+      if(routes.length > 0) {
+        this.$store.commit("SET_SIDEBAR_ROUTERS", routes);
+      }
+      return routes;
     },
+	ishttp(url) {
+      return url.indexOf('http://') !== -1 || url.indexOf('https://') !== -1
+    }
   },
 };
 </script>
 
-<style lang="scss" scoped>
-.el-menu--horizontal > .el-menu-item {
+<style lang="scss">
+.topmenu-container.el-menu--horizontal > .el-menu-item {
   float: left;
-  height: 50px;
-  line-height: 50px;
-  margin: 0;
-  border-bottom: 3px solid transparent;
-  color: #999093;
-  padding: 0 5px;
-  margin: 0 10px;
+  height: 50px !important;
+  line-height: 50px !important;
+  color: #999093 !important;
+  padding: 0 5px !important;
+  margin: 0 10px !important;
 }
 
-.el-menu--horizontal > .el-menu-item.is-active {
-  border-bottom: 3px solid #14a5ff;
+.topmenu-container.el-menu--horizontal > .el-menu-item.is-active, .el-menu--horizontal > .el-submenu.is-active .el-submenu__title {
+  border-bottom: 2px solid #{'var(--theme)'} !important;
   color: #303133;
 }
+
+/* submenu item */
+.topmenu-container.el-menu--horizontal > .el-submenu .el-submenu__title {
+  float: left;
+  height: 50px !important;
+  line-height: 50px !important;
+  color: #999093 !important;
+  padding: 0 5px !important;
+  margin: 0 10px !important;
+}
 </style>

+ 426 - 0
src/components/VideoUpload/index.vue

@@ -0,0 +1,426 @@
+<template>
+  <div>
+    <el-form-item label="视频上传">
+      <div class="upload_video" id="upload_video">
+        <el-upload
+          class="upload-demo"
+          ref="upload"
+          action="#"
+          :http-request="uploadVideoToTxPcdn"
+          accept=".mp4"
+          :limit="1"
+          :on-remove="handleRemove"
+          :on-change="handleChange"
+          :auto-upload="false"
+          :key="uploadKey"
+        >
+          <el-button slot="trigger" size="small" type="primary" >选取视频</el-button>
+          <el-button style="margin-left: 10px;" size="small" type="success" @click="submitUpload">点击上传</el-button>
+          <!-- 仅当showControl为true时显示视频库选取按钮 -->
+          <el-button v-if="showControl" style="margin-left: 10px;" size="small" type="success" @click="openVideoLibrary">视频库选取</el-button>
+          <!-- 线路一 -->
+          <div class="progress-container">
+            <span class="progress-label">线路一</span>
+            <el-progress
+              style="margin-top: 5px;"
+              class="progress"
+              :text-inside="true"
+              :stroke-width="18"
+              :percentage="txProgress"
+              status="success">
+            </el-progress>
+          </div>
+
+          <!-- 线路二 -->
+          <div class="progress-container">
+            <span class="progress-label">线路二</span>
+            <el-progress
+              style="margin-top: 5px;"
+              class="progress"
+              :text-inside="true"
+              :stroke-width="18"
+              :percentage="hwProgress"
+              status="success">
+            </el-progress>
+          </div>
+          <div slot="tip" class="el-upload__tip">只能上传mp4文件,且不超过500M</div>
+        </el-upload>
+      </div>
+    </el-form-item>
+    <el-form-item label="视频播放">
+      <video v-if="videoUrl" ref="myvideo" :src="videoUrl" id="video" width="100%" height="300px" controls></video>
+      <div v-if="fileName">视频文件名: {{ fileName }}</div>
+      <div v-if="fileKey">文件Key: {{ fileKey }}</div>
+      <div v-if="fileSize">文件大小(MB): {{ (fileSize / (1024 * 1024)).toFixed(2) }} MB</div>
+    </el-form-item>
+    <!-- 仅当showControl为true时显示播放线路选择器 -->
+    <el-form-item v-if="showControl" label="播放线路">
+      <el-radio-group v-model="localUploadType">
+        <el-radio :label="1" >线路一</el-radio>
+        <el-radio :label="2" >线路二</el-radio>
+        <!--        <el-radio :label="3" >线路三</el-radio>-->
+      </el-radio-group>
+    </el-form-item>
+
+    <!-- 视频库选择对话框 -->
+    <el-dialog title="视频库选择" :visible.sync="libraryOpen" width="900px" append-to-body>
+      <!-- 搜索条件 -->
+      <el-form :inline="true" :model="libraryQueryParams" class="library-search">
+        <el-form-item label="素材名称">
+          <el-input
+            v-model="libraryQueryParams.resourceName"
+            placeholder="请输入素材名称"
+            clearable
+            size="small"
+            @keyup.enter.native="handleLibraryQuery"
+          />
+        </el-form-item>
+        <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleLibraryQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetLibraryQuery">重置</el-button>
+        </el-form-item>
+      </el-form>
+
+      <!-- 视频列表 -->
+      <el-table v-loading="libraryLoading" :data="libraryList" @row-click="handleLibrarySelect" highlight-current-row>
+        <el-table-column label="素材名称" align="center" prop="resourceName" />
+        <el-table-column label="文件名称" align="center" prop="fileName" />
+        <el-table-column label="缩略图" align="center">
+          <template slot-scope="scope">
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover"
+            >
+              <img alt="" slot="reference" :src="scope.row.thumbnail" style="width: 80px; height: 50px" />
+              <img alt="" :src="scope.row.thumbnail" style="max-width: 150px;" />
+            </el-popover>
+          </template>
+        </el-table-column>
+        <el-table-column label="视频时长" align="center">
+          <template slot-scope="scope">
+            <span>{{ formatDuration(scope.row.duration) }}</span>
+          </template>
+        </el-table-column>
+      </el-table>
+
+      <!-- 分页 -->
+      <pagination
+        v-show="libraryTotal>0"
+        :total="libraryTotal"
+        :page.sync="libraryQueryParams.pageNum"
+        :limit.sync="libraryQueryParams.pageSize"
+        @pagination="getLibraryList"
+      />
+
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="confirmVideoSelection">确 定</el-button>
+        <el-button @click="cancelVideoSelection">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {getSignature, uploadHuaWeiObs, uploadHuaWeiVod} from "@/api/common";
+import {getThumbnail} from "@/api/course/userVideo";
+import TcVod from "vod-js-sdk-v6";
+import { uploadObject } from "@/utils/cos.js";
+import { uploadToOBS } from "@/utils/obs.js";
+import Pagination from "@/components/Pagination";
+import { listVideoResource } from '@/api/course/videoResource';
+
+export default {
+  components: {
+    Pagination
+  },
+  props: {
+    videoUrl: {
+      type: String,
+      default: "",
+    },
+    fileKey: {
+      type: String,
+      default: "",
+    },
+    fileSize: {
+      type: Number,
+      default: null,
+    },
+    fileName: {
+      type: String,
+      default: "",
+    },
+    line_1: {
+      type: String,
+      default: "",
+    },
+    line_2: {
+      type: String,
+      default: "",
+    },
+    line_3: {
+      type: String,
+      default: "",
+    },
+    thumbnail: {
+      type: String,
+      default: "",
+    },
+    uploadType: {
+      type: Number,
+      default: null,
+    },
+    type: {
+      type: Number,
+      default: null,
+    },
+    isPrivate: {
+      type: Number,
+      default: 0,
+    },
+
+    // 使用一个变量控制显示,默认为true显示所有控制项
+    showControl: {
+      type: Boolean,
+      default: true,
+    }
+  },
+  data() {
+    return {
+      videoName:'',
+      isHidden : false,
+      localUploadType: this.uploadType,
+      duration: 0,
+      fileId: "",
+      uploadTypeOptions: [
+        { dictLabel: "线路一", dictValue: 1 }, // 腾讯pcdn
+        { dictLabel: "线路二", dictValue: 2 }, // 华为云obs
+        // { dictLabel: "华为云VOD", dictValue: 4 },
+        // { dictLabel: "腾讯云VOD", dictValue: 5 },
+      ],
+      fileList: [],
+      txProgress: 0,
+      hwProgress: 0,
+      uploadLoading: false,
+      uploadKey: 0,
+      // 视频库选择相关数据
+      libraryOpen: false,
+      libraryLoading: false,
+      libraryTotal: 0,
+      libraryList: [],
+      selectedVideo: null,
+      libraryQueryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        resourceName: null
+      }
+    };
+  },
+  watch: {
+    localUploadType(newType) {
+      this.$emit("update:uploadType", newType);
+      this.$emit("change");
+    },
+    uploadType(newType) {
+      this.localUploadType = newType;
+    },
+  },
+  methods: {
+    // 打开视频库对话框
+    openVideoLibrary() {
+      this.libraryOpen = true;
+      this.selectedVideo = null;
+      this.getLibraryList();
+    },
+    handleChange(file, fileList) {
+      this.fileList = fileList;
+      this.getVideoDuration(file.raw);
+    },
+    getVideoDuration(file) {
+      const video = document.createElement("video");
+      video.preload = "metadata";
+      video.onloadedmetadata = () => {
+        window.URL.revokeObjectURL(video.src);
+        this.duration = parseInt(video.duration.toFixed(2));
+        console.log("视频时长=========>",this.duration);
+        this.$emit("video-duration", this.duration);
+        console.log("文件大小=====>",file.size);
+        this.$emit("update:fileSize", file.size);
+      };
+      video.src = URL.createObjectURL(file);
+    },
+    async submitUpload() {
+      if (this.fileList.length < 1) {
+        return this.$message.error("请先选取视频,再进行上传");
+      }
+      await this.getFirstThumbnail();
+      //同时上传个线路
+      await this.uploadVideoToTxPcdn();
+      await this.uploadVideoToHwObs();
+      this.$emit("update:fileName", this.fileList[0].name);
+    },
+    //获取第一帧封面
+    async getFirstThumbnail(){
+      const file = this.fileList[0].raw;
+      getThumbnail(file).then(response => {
+        console.log("获取到第一帧为封面======>",response.url)
+        this.$emit("update:thumbnail", response.url);
+      })
+    },
+    //更新华为线路进度条
+    updateHwProgress(progress) {
+      this.hwProgress = progress;
+    },
+    //更新腾讯线路进度条
+    updateTxProgress(progressData) {
+      this.txProgress = Math.round(progressData.percent * 100);
+    },
+    //上传腾讯云Pcdn
+    async uploadVideoToTxPcdn() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadObject(file, this.updateTxProgress,this.type);
+        console.log("腾讯COS返回========>",data);
+        console.log("isPrivate=======>",this.isPrivate)
+        let line_1='' ;
+        if (this.isPrivate===0){
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }else {
+          line_1 = `${process.env.VUE_APP_VIDEO_LINE_1}${data.urlPath}`;
+        }
+
+        let urlPathWithoutFirstSlash = data.urlPath.substring(1);
+        this.$emit("update:fileKey", urlPathWithoutFirstSlash);
+
+        console.log("文件key",urlPathWithoutFirstSlash);
+        console.log("组装URL========>",line_1);
+        this.$emit("update:videoUrl", line_1);
+        this.$emit("update:line_1", line_1);
+        // this.$emit("update:line_2", line_2);
+        this.$message.success("线路一上传成功");
+      } catch (error) {
+        this.$message.error("线路一上传失败");
+      }
+    },
+    //上传华为云Obs
+    async uploadVideoToHwObs() {
+      try {
+        const file = this.fileList[0].raw;
+        const data = await uploadToOBS(file, this.updateHwProgress,this.type);
+        console.log("华为OBS返回========>",data);
+        let line_2='' ;
+        if (this.isPrivate===0){
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }else {
+          line_2 = `${process.env.VUE_APP_VIDEO_LINE_2}/${data.urlPath}`;
+        }
+        // this.$emit("update:videoUrl", data);
+        this.$emit("update:line_2", line_2);
+        this.$message.success("线路二上传成功");
+      } catch (error) {
+        this.$message.error("线路二上传失败");
+      }
+    },
+    handleRemove(file, fileList) {
+      console.log(file, fileList.length);
+    },
+    resetUpload() {
+      // 重置内部状态
+      this.txProgress = 0;
+      this.hwProgress = 0;
+      this.fileList = [];
+      this.uploadKey++;
+    },
+    /** 查询视频库列表 */
+    getLibraryList() {
+      this.libraryLoading = true;
+      listVideoResource(this.libraryQueryParams).then(response => {
+        this.libraryList = response.rows;
+        this.libraryTotal = response.total;
+        this.libraryLoading = false;
+      });
+    },
+    /** 搜索视频库按钮操作 */
+    handleLibraryQuery() {
+      this.libraryQueryParams.pageNum = 1;
+      this.getLibraryList();
+    },
+    /** 重置视频库查询按钮操作 */
+    resetLibraryQuery() {
+      this.libraryQueryParams = {
+        pageNum: 1,
+        pageSize: 10,
+        resourceName: null
+      };
+      this.handleLibraryQuery();
+    },
+    /** 视频库选择行点击 */
+    handleLibrarySelect(row) {
+      this.selectedVideo = row;
+    },
+    /** 格式化视频时长 */
+    formatDuration(seconds) {
+      if (!seconds) return '00:00';
+
+      const minutes = Math.floor(seconds / 60);
+      const remainingSeconds = seconds % 60;
+
+      return `${minutes.toString().padStart(2, '0')}:${remainingSeconds.toString().padStart(2, '0')}`;
+    },
+    /** 确认选择视频 */
+    confirmVideoSelection() {
+      if (!this.selectedVideo) {
+        this.$message.warning("请选择一个视频");
+        return;
+      }
+
+      // 更新组件内部数据
+      this.$emit("update:fileName", this.selectedVideo.fileName);
+      this.$emit("update:thumbnail", this.selectedVideo.thumbnail);
+      this.$emit("update:line_1", this.selectedVideo.line1);
+      this.$emit("update:line_2", this.selectedVideo.line2);
+      this.$emit("update:line_3", this.selectedVideo.line3);
+      this.$emit("update:fileSize", this.selectedVideo.fileSize);
+      this.$emit("update:fileKey", this.selectedVideo.fileKey);
+      this.$emit("update:uploadType", this.selectedVideo.uploadType);
+      this.$emit("video-duration", this.selectedVideo.duration);
+
+      // 设置预览URL
+      this.$emit("update:videoUrl", this.selectedVideo.videoUrl);
+
+      // 题目
+      this.$emit("selectProjects", this.selectedVideo.projectIds)
+
+      this.libraryOpen = false;
+    },
+    /** 取消视频选择 */
+    cancelVideoSelection() {
+      this.libraryOpen = false;
+      this.selectedVideo = null;
+    }
+  }
+};
+</script>
+
+<style>
+.progress-container {
+  margin-bottom: 5px; /* 进度条之间的间距 */
+}
+
+.progress-label {
+  display: block;
+  font-weight: bold;
+  font-size: 13px;
+  color: #303331; /* 标签颜色,可以根据需要调整 */
+}
+
+/* 视频库选择对话框样式 */
+.library-search {
+  margin-bottom: 15px;
+}
+
+.el-table .el-table__row:hover {
+  cursor: pointer;
+}
+</style>

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

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

+ 217 - 0
src/components/qqMap/index.vue

@@ -0,0 +1,217 @@
+<template>
+  <div class="app-container map-contain">
+    <el-dialog title="选择坐标" :visible.sync="mapOpen" @close="handleClose" width="1200px">
+      <div>
+        <div>
+          <el-form ref="form" :model="form" label-width="50px">
+            <el-row>
+              <el-col :span="6" >
+                <el-form-item label="地址" prop="lng">
+                  <el-input v-model="form.address" readonly="readonly" />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6"  >
+                <el-form-item label="经度" prop="lng">
+                  <el-input v-model="form.lng"  />
+                </el-form-item>
+              </el-col>
+              <el-col :span="6" >
+                <el-form-item label="纬度" prop="lat">
+                  <el-input v-model="form.lat"  />
+                </el-form-item>
+              </el-col>
+            </el-row>
+          </el-form>
+          <div class="map-coord">
+            <el-select
+              class="address-select"
+              v-model="searchValue"
+              filterable
+              remote
+              value-key="id"
+              placeholder="请输入关键词"
+              :remote-method="remoteMethod"
+              @change="changeArea"
+              :loading="loading">
+              <el-option
+                v-for="item in areaOptions"
+                :key="item.id"
+                :label="item.title"
+                :value="item.location"
+              />
+            </el-select>
+            <div v-if="mapOpen" id="mapCoord"></div>
+          </div>
+        </div>
+        <div id="container"></div>
+      </div>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="closeMap">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import markerDefault from '@/assets/images/markerDefault.png';
+
+export default {
+  data() {
+    return {
+      map: null, // 地图实例
+      markerDefault: markerDefault, // 默认图片
+      mapOpen: false,
+      markerLayer: null,
+      form: {
+        address: '',
+        lat: 36.060249948,
+        lng: 103.826524828,
+      },
+      searchValue: '', // 地图搜索值
+      loading: false,
+      areaOptions: [] // 地图搜索地址
+    }
+  },
+  methods: {
+    init() {
+      this.form.address = ''
+      // 默认省政府
+      this.form.lat = 36.060249948
+      this.form.lng = 103.826524828
+      this.form.map = null
+      this.form.markerLayer = null
+      this.mapOpen = true
+      this.$nextTick(() => {
+        let that = this
+        // 初始化地图
+        this.map = new TMap.Map('mapCoord', {
+          center: new TMap.LatLng(this.form.lat,this.form.lng), // 地图显示中心点
+          zoom:18	// 缩放级别
+        });
+        this.getAddress()
+        this.creatPoint()
+      
+        this.map.on("click",function(evt){
+          if(evt.poi) {
+            that.form.address = evt.poi.name
+          } else {
+            that.form.address = ''
+          }
+          that.form.lat = evt.latLng.getLat().toFixed(6);
+          that.form.lng= evt.latLng.getLng().toFixed(6);
+          that.getAddress()
+          that.creatPoint()
+        })
+
+      })
+    },
+    // 逆地址解析
+    getAddress() {
+      let location = this.form.lat + ',' + this.form.lng
+      this.$jsonp("https://apis.map.qq.com/ws/geocoder/v1/", {
+        location: location,
+        key: "UZQBZ-SYQL3-LYF3K-Y7FAL-N3656-2DBJ4",
+        output:"jsonp"
+      })
+      .then(res => {
+        // 结合知名地点形成的描述性地址,更具人性化特点
+        this.form.address = res.result.formatted_addresses.recommend
+      })
+    },
+    // 绘点
+    creatPoint(){
+      if (this.markerLayer) {
+        this.markerLayer.setMap(null);
+        this.markerLayer = null;
+      }
+      this.markerLayer = new TMap.MultiMarker({
+        map: this.map,  //指定地图容器
+        //样式定义
+        styles: {
+          //创建一个styleId为"myStyle"的样式(styles的子属性名即为styleId)
+          "myStyle": new TMap.MarkerStyle({ 
+            "width": 25,  // 点标记样式宽度(像素)
+            "height": 35, // 点标记样式高度(像素)
+            "src": markerDefault,  //图片路径
+            "anchor": { x: 16, y: 32 }  
+          }) 
+        },
+        //点标记数据数组
+        geometries: [{
+          "id": "1",   //点标记唯一标识,后续如果有删除、修改位置等操作,都需要此id
+          "styleId": 'myStyle',  //指定样式id
+          "position": new TMap.LatLng(this.form.lat,this.form.lng),  //点标记坐标位置
+          "properties": {//自定义属性
+            "title": "marker1"
+          }
+        }]
+      });
+    },
+    // 地图搜索事件
+    remoteMethod(query) {
+      if (query !== '') {
+        this.loading = true
+        this.$jsonp("https://apis.map.qq.com/ws/place/v1/suggestion/", {
+          region: "兰州",
+          keyword: query,
+          key: "UZQBZ-SYQL3-LYF3K-Y7FAL-N3656-2DBJ4",
+          output:"jsonp"
+        })
+        .then(res => {
+          this.loading = false
+          if(res.data.length > 0) {
+            this.areaOptions = res.data
+          }
+        })
+        .catch(err => {
+          this.loading = false
+        });
+      } else {
+        this.options = [];
+      }
+    },
+    // 选择地区
+    changeArea() {
+      if(this.searchValue) {
+        this.form.lat = this.searchValue.lat
+        this.form.lng = this.searchValue.lng
+        this.map.setCenter(new TMap.LatLng(this.searchValue.lat,this.searchValue.lng));
+        this.creatPoint()
+        this.getAddress()
+      }
+    },
+    // 提交数据
+    closeMap(){
+      this.map.destroy();
+      this.mapOpen=false;
+      this.$emit('refreshDataList',this.form)
+    },
+    // 关闭弹窗事件
+    handleClose() {
+      this.map.destroy();
+      this.mapOpen=false;
+    }
+  }
+}
+</script>
+
+<style lang="scss" scoped>
+  .map-contain{
+    /deep/.el-dialog{
+      margin-top: 2vh !important;
+    }
+  }
+  #mapCoord{
+    width: 100%;
+    height: 70vh;
+  }
+  .map-coord{
+    position: relative;
+    /deep/.address-select{
+      position: absolute;
+      top: 10px;
+      left: 10px;
+      z-index: 2000;
+    }
+  }
+</style>

+ 63 - 0
src/directive/dialog/dialog/drag.js

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

+ 33 - 0
src/directive/dialog/dialog/dragHeight.js

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

+ 29 - 0
src/directive/dialog/dialog/dragWidth.js

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

+ 63 - 0
src/directive/dialog/drag.js

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

+ 33 - 0
src/directive/dialog/dragHeight.js

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

+ 29 - 0
src/directive/dialog/dragWidth.js

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

+ 21 - 0
src/directive/index.js

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

+ 1 - 2
src/directive/permission/hasPermi.js

@@ -1,6 +1,5 @@
  /**
- * 操作权限处理
- * Copyright (c) 2019 fs
+ * v-hasPermi 操作权限处理
  */
  
 import store from '@/store'

+ 1 - 2
src/directive/permission/hasRole.js

@@ -1,6 +1,5 @@
  /**
- * 角色权限处理
- * Copyright (c) 2019 fs
+ * v-hasRole 角色权限处理
  */
  
 import store from '@/store'

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

@@ -0,0 +1,27 @@
+ /**
+ * v-hasPermi 操作权限处理
+ */
+ 
+import store from '@/store'
+
+export default {
+  inserted(el, binding, vnode) {
+    const { value } = binding
+    const all_permission = "*:*:*";
+    const permissions = store.getters && store.getters.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(`请设置操作权限标签值`)
+    }
+  }
+}

Some files were not shown because too many files changed in this diff