Browse Source

Merge branch 'pre_online' into 01/21VipReset

mo 4 years ago
parent
commit
06e7774ccc
55 changed files with 2592 additions and 373 deletions
  1. 0 0
      dist/index.html
  2. 0 0
      dist/static/css/app.dec6b312.css
  3. 1 1
      dist/static/css/chunk-37df2798.1bcaf403.css
  4. 0 0
      dist/static/css/chunk-5154db7c.2fbf6419.css
  5. 1 0
      dist/static/css/chunk-740f1ffe.753ee7da.css
  6. 1 0
      dist/static/css/chunk-75bdc7b6.53e11166.css
  7. 0 0
      dist/static/css/chunk-76346a13.c63b79b1.css
  8. 0 1
      dist/static/css/chunk-c6e235ce.de0572a0.css
  9. 0 0
      dist/static/css/chunk-ef5e86cc.835fe9a8.css
  10. 0 0
      dist/static/css/chunk-ffdd52e6.6916646b.css
  11. 0 0
      dist/static/js/app.21744403.js
  12. 0 0
      dist/static/js/app.f4f36f6d.js
  13. 0 0
      dist/static/js/chunk-1b4c27ab.37501788.js
  14. 0 0
      dist/static/js/chunk-37df2798.a3683000.js
  15. 0 0
      dist/static/js/chunk-5154db7c.da2dacf1.js
  16. 0 0
      dist/static/js/chunk-740f1ffe.757a2b5c.js
  17. 0 0
      dist/static/js/chunk-75bdc7b6.c5e8fa23.js
  18. 0 0
      dist/static/js/chunk-76346a13.374118c8.js
  19. 0 0
      dist/static/js/chunk-c5936436.8f6982c2.js
  20. 0 0
      dist/static/js/chunk-c6e235ce.3548ca9f.js
  21. 0 0
      dist/static/js/chunk-ffdd52e6.bcc696a7.js
  22. 5 1
      package.json
  23. 8 0
      src/api/buildTeam.js
  24. 48 0
      src/components/auto-height/index.vue
  25. 2 0
      src/components/install.js
  26. 78 44
      src/components/statistic/index.vue
  27. 5 0
      src/constant/index.js
  28. 78 0
      src/helpers/notification.js
  29. 16 0
      src/helpers/uuidv4.js
  30. 12 0
      src/utils/vueFilter.js
  31. 1 1
      src/views/accompanyManager/accompanys.vue
  32. 4 4
      src/views/contentManager/contentOperation.vue
  33. 135 0
      src/views/main/abnormal/index.vue
  34. 74 0
      src/views/main/abnormal/title.vue
  35. 69 0
      src/views/main/api.js
  36. 78 0
      src/views/main/baseinfo/business.vue
  37. 78 0
      src/views/main/baseinfo/curriculum.vue
  38. 77 0
      src/views/main/baseinfo/hr.vue
  39. 140 0
      src/views/main/baseinfo/index.vue
  40. 80 0
      src/views/main/baseinfo/management.vue
  41. 77 0
      src/views/main/baseinfo/operate.vue
  42. 98 0
      src/views/main/baseinfo/student.vue
  43. 68 0
      src/views/main/constant.js
  44. 1 1
      src/views/main/index.vue
  45. 65 0
      src/views/main/reminders/index.vue
  46. 205 0
      src/views/main/schedule-branch/index.vue
  47. 262 0
      src/views/main/schedule-branch/modals/create.vue
  48. 60 0
      src/views/main/schedule-branch/modals/view.vue
  49. 227 0
      src/views/main/teamSchedule/index.vue
  50. 1 1
      src/views/teamBuild/teamSeting/index.vue
  51. 38 8
      src/views/teamDetail/components/studentList.vue
  52. 445 310
      src/views/teamDetail/teamCourseList.vue
  53. 10 0
      src/views/teamDetail/teamList.vue
  54. 41 0
      src/workers/notification.js
  55. 3 1
      vue.config.js

File diff suppressed because it is too large
+ 0 - 0
dist/index.html


File diff suppressed because it is too large
+ 0 - 0
dist/static/css/app.dec6b312.css


+ 1 - 1
dist/static/css/chunk-1b4c27ab.a8e604e5.css → dist/static/css/chunk-37df2798.1bcaf403.css

@@ -1 +1 @@
-.process[data-v-f049a426]{padding:10px;max-height:600px;overflow-y:auto}.dialog-footer[data-v-7a44cd83]{margin-top:20px;display:block;text-align:right}.select[data-v-459482cc]{font-size:14px}.btnList[data-v-459482cc]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.btnList div[data-v-459482cc]{margin-right:15px}
+.process[data-v-f049a426]{padding:10px;max-height:600px;overflow-y:auto}.dialog-footer[data-v-7a44cd83]{margin-top:20px;display:block;text-align:right}.select[data-v-03176ffa]{font-size:14px}.btnList[data-v-03176ffa]{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:center;-ms-flex-align:center;align-items:center}.btnList div[data-v-03176ffa]{margin-right:15px}

File diff suppressed because it is too large
+ 0 - 0
dist/static/css/chunk-5154db7c.2fbf6419.css


+ 1 - 0
dist/static/css/chunk-740f1ffe.753ee7da.css

@@ -0,0 +1 @@
+.v-charts-component-loading{position:absolute;left:0;right:0;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:hsla(0,0%,100%,.9)}.v-charts-mask-status{-webkit-filter:blur(1px);filter:blur(1px)}.v-charts-component-loading .circular{width:42px;height:42px;-webkit-animation:loading-rotate 2s linear infinite;animation:loading-rotate 2s linear infinite}.v-charts-component-loading .path{-webkit-animation:loading-dash 1.5s ease-in-out infinite;animation:loading-dash 1.5s ease-in-out infinite;stroke-dasharray:90,150;stroke-dashoffset:0;stroke-width:2;stroke:#20a0ff;stroke-linecap:round}@-webkit-keyframes loading-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@keyframes loading-rotate{to{-webkit-transform:rotate(1turn);transform:rotate(1turn)}}@-webkit-keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}@keyframes loading-dash{0%{stroke-dasharray:1,200;stroke-dashoffset:0}50%{stroke-dasharray:90,150;stroke-dashoffset:-40px}to{stroke-dasharray:90,150;stroke-dashoffset:-120px}}.v-charts-data-empty{position:absolute;left:0;right:0;top:0;bottom:0;display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-pack:center;-ms-flex-pack:center;justify-content:center;-webkit-box-align:center;-ms-flex-align:center;align-items:center;background-color:hsla(0,0%,100%,.9);color:#888;font-size:14px}

+ 1 - 0
dist/static/css/chunk-75bdc7b6.53e11166.css

@@ -0,0 +1 @@
+.container[data-v-38d950a5]{overflow:hidden}.container .rows>div[data-v-38d950a5]{margin-bottom:20px}.container[data-v-38d950a5] .el-card__body .statistic{margin-bottom:15px;padding:0}

File diff suppressed because it is too large
+ 0 - 0
dist/static/css/chunk-76346a13.c63b79b1.css


+ 0 - 1
dist/static/css/chunk-c6e235ce.de0572a0.css

@@ -1 +0,0 @@
-.itemWrap{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-pack:start;-ms-flex-pack:start;justify-content:flex-start;-webkit-box-align:center;-ms-flex-align:center;align-items:center;-ms-flex-wrap:wrap;flex-wrap:wrap}.itemWrap .item{width:333px;height:137px;padding:22px 28px 20px;-webkit-box-shadow:0 8px 20px 0 rgba(0,0,0,.1);box-shadow:0 8px 20px 0 rgba(0,0,0,.1);border-radius:6px;margin:0 15px 3rem}.itemWrap .item h4{margin-bottom:20px;color:#323c47;font-size:14px}.itemWrap .item .infos,.itemWrap .item h4{display:-webkit-box;display:-ms-flexbox;display:flex;-webkit-box-orient:horizontal;-webkit-box-direction:normal;-ms-flex-direction:row;flex-direction:row;-webkit-box-pack:justify;-ms-flex-pack:justify;justify-content:space-between}.itemWrap .item .infos .sub{font-size:14px;line-height:20px;color:#aaa;margin-bottom:8px}.itemWrap .item .infos .msg{font-size:24px;font-weight:500;color:#444;line-height:28px;text-align:center}

+ 0 - 0
dist/static/css/chunk-ef5e86cc.8be077f0.css → dist/static/css/chunk-ef5e86cc.835fe9a8.css


+ 0 - 0
dist/static/css/chunk-6a24db89.6916646b.css → dist/static/css/chunk-ffdd52e6.6916646b.css


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/app.21744403.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/app.f4f36f6d.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-1b4c27ab.37501788.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-37df2798.a3683000.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-5154db7c.da2dacf1.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-740f1ffe.757a2b5c.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-75bdc7b6.c5e8fa23.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-76346a13.374118c8.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-c5936436.8f6982c2.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-c6e235ce.3548ca9f.js


File diff suppressed because it is too large
+ 0 - 0
dist/static/js/chunk-ffdd52e6.bcc696a7.js


+ 5 - 1
package.json

@@ -22,6 +22,7 @@
     "copy-to-clipboard": "^3.3.1",
     "dayjs": "^1.8.35",
     "default-passive-events": "^1.0.10",
+    "echarts": "^4.8.0",
     "element-ui": "^2.13.2",
     "http-server": "^0.12.3",
     "i": "^0.3.6",
@@ -39,9 +40,11 @@
     "qrcodejs2": "0.0.2",
     "qs": "^6.8.0",
     "swiper": "^6.3.5",
+    "v-charts": "^1.19.0",
     "vue": "2.6.10",
     "vue-amap": "^0.5.10",
     "vue-awesome-swiper": "^4.1.1",
+    "vue-count-to": "^1.0.13",
     "vue-lunar-calendar-pro": "^1.0.14",
     "vue-qr": "^2.2.1",
     "vue-quill-editor": "^3.0.6",
@@ -79,7 +82,8 @@
     "serve-static": "^1.13.2",
     "svg-sprite-loader": "4.1.3",
     "svgo": "1.2.2",
-    "vue-template-compiler": "2.6.10"
+    "vue-template-compiler": "2.6.10",
+    "worker-loader": "^3.0.7"
   },
   "engines": {
     "node": ">=8.9",

+ 8 - 0
src/api/buildTeam.js

@@ -1400,6 +1400,14 @@ export function courseMerge (data) {
   })
 }
 
+// 取消合并
+export function cancelCourseMerge (data) {
+  return request({
+    url: api + '/courseSchedule/mergeCourseSplit',
+    method: 'post',
+    data: qs.stringify(data)
+  })
+}
 
 // 查询班级剩余课次
 export function getClassGroupSubCourseNum (data) {

+ 48 - 0
src/components/auto-height/index.vue

@@ -0,0 +1,48 @@
+<template>
+  <div ref="box" class="auto-height" :style="{height: height}">
+    <slot/>
+  </div>
+</template>
+<script>
+export default {
+  name: 'auto-height',
+  props: {
+    pre: {
+      type: Number,
+      default: .6
+    },
+    max: {
+      type: Number,
+      default: 400,
+    },
+    min: {
+      type: Number,
+      default: 200
+    }
+  },
+  methods: {
+   setSize() {
+      const width = this.$refs.box.offsetWidth
+      return Math.max(this.min, Math.min(Math.floor(width * this.pre), this.max)) + 'px'
+    },
+    resize() {
+      window.requestAnimationFrame(() => {
+        this.height = this.setSize()
+      })
+    },
+  },
+  mounted () {
+    window.addEventListener('resize', this.resize, false)
+    const event = new Event('resize')
+    window.dispatchEvent(event)
+  },
+  destroyed() {
+    window.removeEventListener('resize', this.resize, false)
+  },
+  data() {
+    return {
+      height: '400px',
+    }
+  }
+}
+</script>

+ 2 - 0
src/components/install.js

@@ -14,6 +14,7 @@ import descriptions from '@/components/Descriptions/Descriptions.vue'
 import remoteSearch from "@/components/remote-search"
 import tabRouter from '@/components/tab-router'
 import empty from '@/components/empty'
+import autoHeight from '@/components/auto-height'
 import auth from '@/components/Auth'
 
 export default {
@@ -29,6 +30,7 @@ export default {
     Vue.component(remoteSearch.name, remoteSearch)
     Vue.component(tabRouter.name, tabRouter)
     Vue.component(empty.name, empty)
+    Vue.component(autoHeight.name, autoHeight)
     Vue.component(auth.name, auth)
   }
 }

+ 78 - 44
src/components/statistic/index.vue

@@ -1,14 +1,10 @@
 <template>
-  <el-row :gutter="cols" class="statistic">
-    <div v-for="(item, index) in tags" :key="index">
-      <el-col :span="span ? span : (index + 1) % col === 0 ? colSpan : colSpan - 1">
-        <statistic-content :item="item"></statistic-content>
-      </el-col>
-      <el-col :key="index" :span="1" v-if="(index + 1) % col !== 0 && (index + 1) < tags.length">
-        <el-divider direction="vertical"></el-divider>
-      </el-col>
+  <div class="statistic">
+    <div v-for="(item, index) in tags" :style="{flex: (100 / col) + '%'}" class="item" :key="index">
+      <statistic-content :class="item.data.class" v-on="{...item.data.on}" :item="item"/>
+      <el-divider class="divider" v-if="(index + 1) % col !== 0 && (index + 1) < tags.length" direction="vertical"></el-divider>
     </div>
-  </el-row>
+  </div>
 </template>
 <script>
 export default {
@@ -34,7 +30,7 @@ export default {
       },
       render(h) {
         return (
-          <div class="statistic-content">
+          <div onClick={this.item.data.on.click} class="statistic-content">
             {this.item.children}
           </div>
         )
@@ -43,7 +39,7 @@ export default {
   },
   computed: {
     colSpan() {
-      return this.cols / this.col
+      return 24 / this.col
     },
   },
   data() {
@@ -59,46 +55,84 @@ export default {
   },
   methods: {
     setTags() {
-      this.tags = this.$slots.default.filter(item => item.tag === 'statistic-item')
+      this.tags = (this.$slots.default || []).filter(item => item.tag === 'statistic-item').map(item => {
+        item.data = item.data || {on: {
+          click: () => {}
+        }}
+        return item
+      })
     }
   }
 }
 </script>
 <style lang="less" scoped>
-  .statistic{
-    padding: 20px 0;
-    text-align: center;
+.statistic{
+  display: flex;
+  text-align: center;
+  justify-content: space-between;
+  align-items: center;
+  flex-wrap: wrap;
+  padding: 20px 0;
+  .item{
+    display: flex;
+    position: relative;
+  }
+  /deep/ .statistic-content{
     width: 100%;
-    margin: auto!important;
-    /deep/ .el-icon-edit{
-      cursor: pointer;
-      color: #14928A;
-      font-size: 20px;
-    }
-    /deep/ .el-col {
-      display: flex;
-      align-items: center;
-      min-height: 60px;
-      align-items: center;
-      justify-content: center;
-      align-items: center;
-      flex-direction: column;
-    }
-    /deep/ .el-col-1{
-      font-size: 60px;
-    }
-    /deep/ .statistic-content{
-      >span{
-        display: block;
-        line-height: 1.8;
-        color: rgba(0, 0, 0, .85);
-        font-size: 24px;
-        &:first-child{
-          font-size: 14px;
-          font-weight: normal;
-          color: rgba(0, 0, 0, .45);
-        }
+    >span{
+      display: block;
+      line-height: 1.8;
+      color: rgba(0, 0, 0, .85);
+      font-size: 24px;
+      &:first-child{
+        font-size: 14px;
+        font-weight: normal;
+        color: rgba(0, 0, 0, .45);
       }
     }
   }
+  .divider{
+    width: 1px;
+    font-size: 60px;
+    margin: 0;
+    position: absolute;
+    right: 0;
+  }
+}
+  // .statistic{
+  //   padding: 20px 0;
+  //   text-align: center;
+  //   width: 100%;
+  //   margin: auto!important;
+  //   /deep/ .el-icon-edit{
+  //     cursor: pointer;
+  //     color: #14928A;
+  //     font-size: 20px;
+  //   }
+  //   /deep/ .el-col {
+  //     display: flex;
+  //     align-items: center;
+  //     min-height: 60px;
+  //     align-items: center;
+  //     justify-content: center;
+  //     align-items: center;
+  //     flex-direction: column;
+  //   }
+  //   /deep/ .el-col-1{
+  //     font-size: 60px;
+  //   }
+  //   /deep/ .statistic-content{
+  //     >span{
+  //       display: block;
+  //       line-height: 1.8;
+  //       color: rgba(0, 0, 0, .85);
+  //       font-size: 24px;
+  //       &:first-child{
+  //         font-size: 14px;
+  //         font-weight: normal;
+  //         color: rgba(0, 0, 0, .45);
+  //       }
+  //     }
+  //   }
+  // }
 </style>

+ 5 - 0
src/constant/index.js

@@ -206,3 +206,8 @@ export const musicClassType = {
   HIGH_ONLINE:'线上基础技能班',
   MUSIC_NETWORK:"乐团网管课"
 }
+
+export const inspectionItem = {
+  INSPECT:'下校巡查',
+  VISIT:'学员回访'
+}

+ 78 - 0
src/helpers/notification.js

@@ -0,0 +1,78 @@
+import dayjs from 'dayjs'
+import NotificationWorker from 'worker-loader!../workers/notification.js'
+import { createRandom } from '@/helpers/uuidv4'
+
+const clickEvents = {}
+const showEvents = {}
+const errorEvents = {}
+const closeEvents = {}
+
+const noop = () => {}
+
+export const notificationWorker = new NotificationWorker()
+
+const notificationClicked = data => {
+  const activeFn = clickEvents[data.callback_key]
+  if (typeof activeFn === 'function') {
+    activeFn(data)
+  }
+  delete clickEvents[data.callback_key]
+}
+
+const notificationShowed = data => {
+  const activeFn = showEvents[data.callback_key]
+  if (typeof activeFn === 'function') {
+    activeFn(data)
+  }
+  delete showEvents[data.callback_key]
+}
+
+const notificationErrored = data => {
+  const activeFn = errorEvents[data.callback_key]
+  if (typeof activeFn === 'function') {
+    activeFn(data)
+  }
+  delete errorEvents[data.callback_key]
+}
+
+const notificationClosed = data => {
+  const activeFn = closeEvents[data.callback_key]
+  if (typeof activeFn === 'function') {
+    activeFn(data)
+  }
+  delete closeEvents[data.callback_key]
+}
+
+notificationWorker.addEventListener('message', evt => {
+  if (evt.data.type === 'NotificationClicked') {
+    notificationClicked(evt.data.data || {})
+  } else if (evt.data.type === 'NotificationShowed') {
+    notificationShowed(evt.data.data || {})
+  } else if (evt.data.type === 'NotificationErrored') {
+    notificationErrored(evt.data.data || {})
+  } else if (evt.data.type === 'NotificationClosed') {
+    notificationClosed(evt.data.data || {})
+  }
+})
+
+export const createNotification = data => {
+  /**
+   *
+   * @param { Object } 会原样在onClick返回
+   *
+   */
+
+  const { onClick = noop, onShow = noop, onError = noop, onClose = noop, ...rest } = data
+  const timemap = dayjs().valueOf()
+  const callback_key = `${timemap}_${createRandom()}`
+  clickEvents[callback_key] = onClick
+  showEvents[callback_key] = onShow
+  errorEvents[callback_key] = onError
+  closeEvents[callback_key] = onClose
+  notificationWorker.postMessage({
+    ...rest,
+    timemap,
+    callback_key,
+    type: 'create'
+  })
+}

+ 16 - 0
src/helpers/uuidv4.js

@@ -0,0 +1,16 @@
+// from: https://stackoverflow.com/questions/105034/how-to-create-a-guid-uuid
+
+const replaceString = str => {
+  return str.replace(/[xy]/g, function(c) {
+    var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
+    return v.toString(16);
+  })
+}
+
+export default () => {
+  return replaceString('xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx')
+}
+
+export const createRandom = (length = 8) => {
+  return replaceString(''.padEnd(length, 'x'))
+}

+ 12 - 0
src/utils/vueFilter.js

@@ -139,6 +139,13 @@ Vue.filter('dayjsFormat', (value) => {
     return value
   }
 })
+Vue.filter('dayjsFormatWeek', (value) => {
+  if (value) {
+    return dayjs(value).format('YYYY-MM')
+  } else {
+    return value
+  }
+})
 
 Vue.filter('dayjsFormatMinute', (value) => {
   if (value) {
@@ -707,3 +714,8 @@ Vue.filter('rewardModeTypeFormat', value => {
 Vue.filter('journalTypeFormat', value => {
   return constant.journalType[value]
 })
+
+// 日程安排 inspectionItem
+Vue.filter('inspectionItemFormat', value => {
+  return constant.inspectionItem[value]
+})

+ 1 - 1
src/views/accompanyManager/accompanys.vue

@@ -153,7 +153,7 @@
                           value-format="yyyy-MM-dd"
                           placeholder="选择日期"></el-date-picker>
         </el-form-item>
-        <div style="padding-left: 15px; color: red;">课程结束时间不得于,{{ expireForm.tempCoursesExpireDate }}</div>
+        <div style="padding-left: 15px; color: red;">课程结束时间不得于,{{ expireForm.tempCoursesExpireDate }}</div>
       </el-form>
       <div slot="footer"
            class="dialog-footer">

+ 4 - 4
src/views/contentManager/contentOperation.vue

@@ -337,7 +337,7 @@ export default {
     const query = this.$route.query
     let url = ''
     // let url = query.type == 7 ? vaildStudentUrl() + "/#/knowledge" : vaildStudentUrl() + "/#/specialdetail"
-    console.log(query.type)
+    // console.log(query.type)
     if (query.type == 7) {
       url = vaildStudentUrl() + "/#/knowledge"
     } else if (query.type == 8 || query.type == 5) {
@@ -576,7 +576,7 @@ export default {
         if (valid) {
           let form = Object.assign({}, this.form)
           let actionTime = form.actionTime
-          console.log(actionTime)
+          // console.log(actionTime)
           if(actionTime && actionTime.length > 0) {
             form.onlineTime = dayjs(actionTime[0]).format('YYYY-MM-DD HH:mm:ss')
             form.offlineTime = dayjs(actionTime[1]).format('YYYY-MM-DD HH:mm:ss')
@@ -616,7 +616,7 @@ export default {
         this.$router.push({
           path: "/contentManager/contentManager",
           query: {
-            type: this.typeIndex(this.type)
+            tabrouter: this.typeIndex(this.type)
           }
         });
       } else {
@@ -627,7 +627,7 @@ export default {
       this.$router.push({
         path: "/contentManager/contentManager",
         query: {
-          type: this.typeIndex(this.type)
+          tabrouter: this.typeIndex(this.type)
         }
       });
     },

+ 135 - 0
src/views/main/abnormal/index.vue

@@ -0,0 +1,135 @@
+<template>
+  <div>
+    <save-form inline :model="search" @submit="FetchList" @reset="FetchList">
+      <el-form-item prop="organIds">
+        <el-select
+          multiple
+          clearable
+          filterable
+          collapse-tags
+          v-model="search.organIds"
+        >
+          <el-option v-for="(item,index) in selects.branchs"
+            :key="index"
+            :label="item.name"
+            :value="item.id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-button native-type="submit" type="primary">搜索</el-button>
+      <el-button native-type="reset" type="danger">重置</el-button>
+    </save-form>
+    <div class="tags">
+      <el-badge
+        :hidden="listByType[item.type].length === 0"
+        :value="listByType[item.type].length"
+        :max="99"
+        v-for="(item, index) in tags"
+        :key="index"
+      >
+        <el-tag
+          :effect="activeKey === item.type ? 'dark' : 'plain'"
+          @click="activeKey = item.type"
+        >{{item.name}}</el-tag>
+      </el-badge>
+    </div>
+    <empty desc="暂无需要处理异常" v-if="!activeList.length"/>
+    <title-item
+      v-else
+      :type="item[0].isError ? 'error' : 'warning'"
+      v-for="(item, index) in activeList"
+      :key="index"
+      :data="item.map(title => ({name: title.desc, num: title.num}))"
+    >
+      <el-button type="text">立即处理<i class="el-icon-d-arrow-right"/></el-button>
+    </title-item>
+  </div>
+</template>
+<script>
+import { getIndexError } from '@/views/main/api'
+import { createNotification } from '@/helpers/notification'
+import { errorType } from '@/views/main/constant'
+import title from './title'
+export default {
+  components: {
+    'title-item': title
+  },
+  data() {
+    return {
+      activeKey: '',
+      search: {
+        organIds: []
+      },
+      listByType: {},
+      list: [],
+    }
+  },
+  computed: {
+    tags() {
+      const tags = this.list.map(item => ({name: item.desc, type: item.errorType}))
+      if (tags.length && !this.activeKey) {
+        this.activeKey = tags[0].type
+      }
+      return tags
+    },
+    activeList() {
+      const list = this.listByType[this.activeKey] || []
+      return list
+    },
+  },
+  mounted() {
+    this.FetchList()
+    this.$store.dispatch('setBranchs')
+  },
+  methods: {
+    formatData(data) {
+      const list = {}
+      for (const item of data) {
+        const row = errorType[item.errorType] || {}
+        const key = row.parent || item.errorType
+        if (!list[key]) {
+          list[key] = []
+        }
+        list[key].push(
+          {
+            ...item,
+            isError: row.isError
+          }
+        )
+      }
+      return Object.values(list)
+    },
+    async FetchList() {
+      try {
+        const res = await getIndexError({
+          ...this.search,
+          organIds: this.search.organIds.join(',')
+        })
+        this.list = res.data.data
+        const data = {}
+        for (const item of this.list) {
+          data[item.errorType] = this.formatData(item?.result || [])
+        }
+        this.listByType = data
+      } catch (error) {}
+    },
+    send() {
+      createNotification({
+        title: '测试发送通知',
+        body: '您有一条待处理通知,请及时处理',
+        onClick: () => {
+          this.$router.replace('/main/main')
+        }
+      })
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+  .tags{
+    margin-bottom: 20px;
+    >div{
+      margin-right: 20px;
+      cursor: pointer;
+    }
+  }
+</style>

+ 74 - 0
src/views/main/abnormal/title.vue

@@ -0,0 +1,74 @@
+<template>
+  <div class="title" :class="{error: type === 'error', warning: type === 'warning'}">
+    <div>
+      <span v-for="(item, index) in data" :key="index">
+        <span>{{item.name}}</span>
+        <b>{{item.num}}</b>
+      </span>
+    </div>
+    <slot/>
+  </div>
+</template>
+<script>
+export default {
+  props: {
+    type: {
+      type: String,
+      default: 'warning'
+    },
+    data: {
+      type: Array,
+      default: []
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+.title{
+  height: 48px;
+  line-height: 48px;
+  background-color: rgba(0, 0, 0, .02);
+  overflow: hidden;
+  margin-bottom: 20px;
+  display: flex;
+  justify-content: space-between;
+  padding-right: 10px;
+  font-weight: bold;
+  b{
+    font-size: 18px;
+  }
+  &.error {
+    b{
+      color: #ED6F62;
+    }
+    &:before{
+      background-color: #ED6F62;
+    }
+  }
+  &.warning {
+    b{
+      color: #F2A24A;
+    }
+    &:before{
+      background-color: #F2A24A;
+    }
+  }
+  &:before{
+      content: '';
+      display: block;
+      position: absolute;
+      width: 7px;
+      height: 48px;
+      left: 0;
+    }
+  >div{
+    position: relative;
+    padding-left: 20px;
+    font-size: 14px;
+    >span{
+      margin-right: 10px;
+      display: inline-block;
+    }
+  }
+}
+</style>

+ 69 - 0
src/views/main/api.js

@@ -0,0 +1,69 @@
+import request2 from '@/utils/request2'
+
+// 获取首页数据
+export const getIndex = data => request2({
+  url: '/api-web/newIndex',
+  params: data,
+  method: 'get',
+})
+export const getInspectionItem = data => request2({
+  url: '/api-web/inspectionItem/queryPage',
+  params: data,
+  method: 'get',
+})
+// 获取异常处理
+export const getIndexError = data => request2({
+  url: '/api-web/getIndexErrData',
+  params: data,
+  method: 'get',
+})
+
+// 获取事项提醒
+export const getRemindMatterData = data => request2({
+  url: '/api-web/getRemindMatterData',
+  params: data,
+  method: 'get',
+})
+
+// 处理乐团主管日程安排
+export const resetInspectionItem = data => request2({
+  url: '/api-web/inspectionItem/update',
+  data: data,
+  method: 'post',
+  requestType:'form'
+})
+
+// 添加巡查任务
+export const inspectionAdd = data => request2({
+  url: '/api-web/inspection/add',
+  data: data,
+  method: 'post',
+})
+
+// 删除巡查任务
+export const inspectionDelete = data => request2({
+  url: '/api-web/inspection/delete',
+  params: data,
+  method: 'post',
+})
+
+// 查看巡查任务详情
+export const inspectionGetInfo = data => request2({
+  url: '/api-web/inspection/getInfo',
+  params: data,
+  method: 'get',
+})
+
+// 巡查任务列表
+export const inspectionQueryPage = data => request2({
+  url: '/api-web/inspection/queryPage',
+  data: data,
+  method: 'get',
+})
+
+// 修改巡查任务
+export const inspectionUpdate = data => request2({
+  url: '/api-web/inspection/update',
+  data: data,
+  method: 'post',
+})

+ 78 - 0
src/views/main/baseinfo/business.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-card header="业务数据">
+    <statistic class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent" :decimals="2"/>%
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-histogram style="width: 100%;" height="350px" :data="chartData" :data-empty="dataEmpty"></ve-histogram>
+  </el-card>
+</template>
+<script>
+import 'v-charts/lib/style.css'
+import countTo from 'vue-count-to'
+import veHistogram from 'v-charts/lib/histogram.common'
+export default {
+  props: ['data'],
+  components: {
+    've-histogram': veHistogram,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        ACTIVATION_RATE: this.data['ACTIVATION_RATE'] || {},
+        HOMEWORK_CREATE_RATE: this.data['HOMEWORK_CREATE_RATE'] || {},
+        HOMEWORK_SUBMIT_RATE: this.data['HOMEWORK_SUBMIT_RATE'] || {},
+        HOMEWORK_COMMENT_RATE: this.data['HOMEWORK_COMMENT_RATE'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items)
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'ACTIVATION_RATE',
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 78 - 0
src/views/main/baseinfo/curriculum.vue

@@ -0,0 +1,78 @@
+<template>
+  <el-card header="课程数据">
+    <statistic class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent"/>
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-line :settings="{
+      area: true,
+    }" :data="chartData" height="350px" :data-empty="dataEmpty"/>
+  </el-card>
+</template>
+<script>
+import countTo from 'vue-count-to'
+import veLine from 'v-charts/lib/line.common'
+export default {
+  props: ['data'],
+  components: {
+    've-line': veLine,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        MUSIC_GROUP_COURSE: this.data['MUSIC_GROUP_COURSE'] || {},
+        VIP_GROUP_COURSE: this.data['VIP_GROUP_COURSE'] || {},
+        PRACTICE_GROUP_COURSE: this.data['PRACTICE_GROUP_COURSE'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items)
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'MUSIC_GROUP_COURSE',
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 77 - 0
src/views/main/baseinfo/hr.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-card header="人事数据">
+    <statistic class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent"/>
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-histogram style="width: 100%;" height="350px" :data="chartData" :data-empty="dataEmpty"></ve-histogram>
+  </el-card>
+</template>
+<script>
+import countTo from 'vue-count-to'
+import veHistogram from 'v-charts/lib/histogram.common'
+export default {
+  props: ['data'],
+  components: {
+    've-histogram': veHistogram,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        TEACHER_NUM: this.data['TEACHER_NUM'] || {},
+        FULL_TIME_NUM: this.data['FULL_TIME_NUM'] || {},
+        PART_TIME_NUM: this.data['PART_TIME_NUM'] || {},
+        DIMISSION_NUM: this.data['DIMISSION_NUM'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items)
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'TEACHER_NUM',
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 140 - 0
src/views/main/baseinfo/index.vue

@@ -0,0 +1,140 @@
+<template>
+  <div class="container">
+    <save-form inline :model="search" @submit="FetchDetail" @reset="reset">
+      <el-form-item prop="year">
+        <el-date-picker
+          v-model="search.year"
+          type="year"
+          format="yyyy年"
+          :picker-options="{
+            disabledDate(time) {
+              return time.getTime() > Date.now()
+            }
+          }"
+          placeholder="请选择年份">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item prop="organIds">
+        <el-select
+          multiple
+          clearable
+          filterable
+          collapse-tags
+          v-model="search.organIds"
+        >
+          <el-option v-for="(item,index) in selects.branchs"
+            :key="index"
+            :label="item.name"
+            :value="item.id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-button native-type="submit" type="primary">搜索</el-button>
+      <el-button native-type="reset" type="danger">重置</el-button>
+    </save-form>
+    <el-alert type="info" :closable="false" style="margin-bottom: 20px;">
+      每日0点更新前一日数据
+    </el-alert>
+    <empty desc="暂无统计数据" v-if="isEmpty"/>
+    <el-row v-else class="rows" :gutter="20">
+      <el-col :xs="24" :sm="24" :md="12">
+        <operate :data="dataInfo"/>
+      </el-col>
+      <el-col :xs="24" :sm="24" :md="12">
+        <business :data="dataInfo"/>
+      </el-col>
+      <!-- <el-col :xs="24" :sm="24" :md="24" :xl="10">
+        <management :data="dataInfo"/>
+      </el-col> -->
+      <el-col :xs="24" :sm="24" :md="12">
+        <hrdata :data="dataInfo"/>
+      </el-col>
+      <el-col :xs="24" :sm="24" :md="12">
+        <student :data="dataInfo"/>
+      </el-col>
+      <el-col :xs="24" :sm="24" :md="24">
+        <curriculum :data="dataInfo"/>
+      </el-col>
+    </el-row>
+  </div>
+</template>
+<script>
+import { getIndex } from '../api'
+import operate from './operate'
+import business from './business'
+import management from './management'
+import hrdata from './hr'
+import student from './student'
+import curriculum from './curriculum'
+
+import { descs } from '../constant'
+
+export default {
+  components: {
+    operate,
+    business,
+    management,
+    hrdata,
+    student,
+    curriculum,
+  },
+  data () {
+    return {
+      search: {
+        year: '',
+        organIds: []
+      },
+      dataInfo: {},
+      business: {},
+    }
+  },
+  computed: {
+    isEmpty() {
+      return !Object.keys(this.dataInfo).length
+    }
+  },
+  mounted () {
+    this.reset()
+    this.$store.dispatch('setBranchs')
+  },
+  methods: {
+    reset() {
+      this.$set(this.search, 'year', this.$helpers.dayjs())
+      this.$set(this.search, 'organIds', [])
+      this.FetchDetail()
+    },
+    async FetchDetail() {
+      const data = {}
+      try {
+        const res = await getIndex({
+          ...this.search,
+          year: this.$helpers.dayjs(this.search.year).year() || '',
+          organIds: this.search.organIds.join(',')
+        })
+        for (const item of res.data) {
+          data[item.dataType] = {
+            ...item,
+            desc: descs[item.dataType]
+          }
+        }
+      } catch (error) {
+        console.log(error)
+      }
+      this.dataInfo = data
+    },
+  }
+}
+</script>
+<style lang="less" scoped>
+  .container{
+    overflow: hidden;
+    .rows{
+      >div{
+        margin-bottom: 20px;
+      }
+    }
+    /deep/ .el-card__body .statistic {
+      margin-bottom: 15px;
+      padding: 0;
+    }
+  }
+</style>

+ 80 - 0
src/views/main/baseinfo/management.vue

@@ -0,0 +1,80 @@
+<template>
+  <el-card header="经营数据">
+    <statistic :col="5" class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent"/>
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-line :settings="{
+      area: true,
+    }" :data="chartData" height="350px" :data-empty="dataEmpty"/>
+  </el-card>
+</template>
+<script>
+import countTo from 'vue-count-to'
+import veLine from 'v-charts/lib/line.common'
+export default {
+  props: ['data'],
+  components: {
+    've-line': veLine,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        SHOULD_INCOME_MONEY: this.data['SHOULD_INCOME_MONEY'] || {},
+        ANTICIPATED_INCOME_MONEY: this.data['ANTICIPATED_INCOME_MONEY'] || {},
+        SHOULD_EXPEND_MONEY: this.data['SHOULD_EXPEND_MONEY'] || {},
+        ANTICIPATED_EXPEND_MONEY: this.data['ANTICIPATED_EXPEND_MONEY'] || {},
+        REVENUE_MONEY: this.data['REVENUE_MONEY'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items)
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'SHOULD_INCOME_MONEY',
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 77 - 0
src/views/main/baseinfo/operate.vue

@@ -0,0 +1,77 @@
+<template>
+  <el-card header="运营数据">
+    <statistic class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent"/>
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-histogram style="width: 100%;" height="350px" :data="chartData" :data-empty="dataEmpty"></ve-histogram>
+  </el-card>
+</template>
+<script>
+import countTo from 'vue-count-to'
+import veHistogram from 'v-charts/lib/histogram.common'
+export default {
+  props: ['data'],
+  components: {
+    've-histogram': veHistogram,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        SCHOOL: this.data['SCHOOL'] || {},
+        MUSIC_GROUP_NUM: this.data['MUSIC_GROUP_NUM'] || {},
+        MUSIC_GROUP_STUDENT: this.data['MUSIC_GROUP_STUDENT'] || {},
+        OTHER_STUDENT: this.data['OTHER_STUDENT'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items)
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'SCHOOL',
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 98 - 0
src/views/main/baseinfo/student.vue

@@ -0,0 +1,98 @@
+<template>
+  <el-card>
+    <div slot="header" class="clearfix">
+      <span>学员变动</span>
+      <el-button
+        @click="isHistogram = !isHistogram"
+        style="float: right; padding: 0px 0"
+        type="text"
+      >{{isHistogram ? '学员转化漏斗图' : '学员柱状图'}}</el-button>
+    </div>
+    <statistic class="statistic" :cols="0">
+      <statistic-item v-for="(item, key) in items" :key="key" :class="{active: active === key}" @click="active = key">
+        <span>
+          {{item.title}}
+          <el-tooltip v-if="item.desc" :content="item.desc" :open-delay=".3" placement="top">
+            <i style="margin-left: 5px;cursor: pointer;" class="el-icon-warning-outline"/>
+          </el-tooltip>
+        </span>
+        <span>
+          <count-to :endVal="item.percent"/>{{key === 'STUDENT_CONVERSION' ? '%' : ''}}
+        </span>
+      </statistic-item>
+    </statistic>
+    <ve-histogram v-if="isHistogram" style="width: 100%;" height="350px" :data="chartData" :data-empty="dataEmpty"></ve-histogram>
+    <ve-funnel v-else style="width: 100%;" height="350px" :data="funnelData" :data-empty="dataEmpty"></ve-funnel>
+  </el-card>
+</template>
+<script>
+import countTo from 'vue-count-to'
+import veHistogram from 'v-charts/lib/histogram.common'
+import veFunnel from 'v-charts/lib/funnel.common'
+export default {
+  props: ['data'],
+  components: {
+    've-funnel': veFunnel,
+    've-histogram': veHistogram,
+    'count-to': countTo
+  },
+  computed: {
+    items() {
+      return {
+        NEWLY_STUDENT_NUM: this.data['NEWLY_STUDENT_NUM'] || {},
+        QUIT_MUSIC_GROUP_STUDENT_NUM: this.data['QUIT_MUSIC_GROUP_STUDENT_NUM'] || {},
+        STUDENT_CONVERSION: this.data['STUDENT_CONVERSION'] || {},
+      }
+    },
+    chartData() {
+      const data = this.data[this.active] || {}
+      const values = Object.values(this.items).filter(item => item.dataType !== 'STUDENT_CONVERSION')
+      const months = {}
+      for (const item of values) {
+        for (const row of (item.indexMonthData || [])) {
+          const key = this.$helpers.dayjs(row.month).month() + 1 + '月'
+          if (!months[key]) {
+            months[key] = {
+              '月份': key,
+            }
+          }
+          months[key][item.title] = row.percent
+        }
+      }
+      return {
+        columns: ['月份', ...values.map(item => item.title)],
+        rows: Object.values(months)
+      }
+    },
+    funnelData() {
+      const { indexMonthData = [] } = this.data['STUDENT_CONVERSION'] || {}
+      return {
+        columns: ['类型', '数值'],
+        rows: indexMonthData.map(item => ({
+          '类型': item.title,
+          '数值': item.percent
+        }))
+      }
+    },
+    dataEmpty() {
+      return !this.chartData.rows.length
+    },
+  },
+  data () {
+    return {
+      active: 'NEWLY_STUDENT_NUM',
+      isHistogram: true,
+    }
+  },
+}
+</script>
+<style lang="less" scoped>
+  // .statistic{
+  //   /deep/ .statistic-content{
+  //     cursor: pointer;
+  //     &.active > span{
+  //       color: #14928a !important;
+  //     }
+  //   }
+  // }
+</style>

+ 68 - 0
src/views/main/constant.js

@@ -0,0 +1,68 @@
+export const descs = {
+  SCHOOL: '截止到昨日,【进行中】乐团的【合作单位】总数',
+  MUSIC_GROUP_NUM: '截止到昨日,【进行中】乐团总数',
+  MUSIC_GROUP_STUDENT: '截止到昨日,【进行中】乐团【在读】学员总数,分部下去重',
+  OTHER_STUDENT: '截止到昨日,学员有剩余VIP/网管未上课时的人数总数,分部下去重',
+  ACTIVATION_RATE: '截止到昨日,平台注册学员激活率,平台注册学员设置密码人数/注册学员总数',
+  HOMEWORK_CREATE_RATE: '本月截止到昨日,服务指标的作业布置率',
+  HOMEWORK_SUBMIT_RATE: '本月截止到昨日,已布置的作业学员提交率',
+  HOMEWORK_COMMENT_RATE: '本月截止到昨日,学员已提交的作业老师点评率',
+  SHOULD_INCOME_MONEY: '本年度截止到当月所有缴费项目应收金额总和',
+  ANTICIPATED_INCOME_MONEY: '本年度截止到当月已缴费但实际还未产生费用金额总和',
+  SHOULD_EXPEND_MONEY: '本年度截止到当月预计支出费用总和(暂无此数据)',
+  ANTICIPATED_EXPEND_MONEY: '本年度截止到当月应付金额总和(暂无此数据)',
+  REVENUE_MONEY: '本年度截止到当月营收金额总和',
+  TEACHER_NUM: '截止到昨日【非冻结】的老师总数',
+  FULL_TIME_NUM: '截止到昨日【非冻结】且工作类型为【全职】的老师总数',
+  PART_TIME_NUM: '截止到昨日【非冻结】且工作类型为【兼职】的老师总数',
+  DIMISSION_NUM: '已冻结或者截止到昨日已设置【离职时间】且时间在昨日之前的老师总数',
+  NEWLY_STUDENT_NUM: '本年度截止到昨日,新增的乐团【在读】学员总数(去重)',
+  QUIT_MUSIC_GROUP_STUDENT_NUM: '本年度截止到昨日,乐团退团学员总数(去重)',
+  STUDENT_CONVERSION: '度截止到昨日,乐团预报名学员中正式报名缴费的学员购买VIP/网管课的转化率(不包括退团学员)',
+  MUSIC_GROUP_COURSE: '截止到昨日,【已结束】乐团课程总数',
+  VIP_GROUP_COURSE: '截止到昨日,【已结束】VIP课程总数',
+  PRACTICE_GROUP_COURSE: '截止到昨日,【已结束】网管课程总数',
+}
+
+export const errorType = {
+  MUSIC_PATROL_ITEM: {
+    isError: false,
+  },
+  HIGH_CLASS_STUDENT_LESS_THAN_THREE: {
+    isError: true,
+  },
+  STUDENT_NOT_PAYMENT: {
+    isError: true,
+  },
+  STUDENT_APPLY_FOR_QUIT_MUSIC_GROUP: {
+    isError: true
+  },
+  WAIT_CREATE_PAYMENT_CALENDER: {
+    isError: true
+  },
+  COURSE_TRUANT_STUDENT_NUM: {
+    isError: true
+  },
+  COURSE_LEAVE_STUDENT_NUM: {
+    isError: true,
+    parent: 'COURSE_TRUANT_STUDENT_NUM'
+  },
+  TEACHER_EXCEPTION_ATTENDANCE: {
+    isError: true
+  },
+  TEACHER_NOT_A_CLASS: {
+    isError: true,
+    parent: 'TEACHER_EXCEPTION_ATTENDANCE'
+  },
+  TEACHER_LEAVE: {
+    isError: false
+  },
+  TEACHER_EXPECT_SALARY_BE_LOW: {
+    isError: false
+  },
+}
+
+export const matterTypes = {
+  INSPECT: '下校巡查',
+  VISIT: '学员回访',
+}

+ 1 - 1
src/views/main/index.vue

@@ -267,4 +267,4 @@ export default {
     }
   }
 }
-</style>
+</style>

+ 65 - 0
src/views/main/reminders/index.vue

@@ -0,0 +1,65 @@
+<template>
+  <div>
+    <save-form inline :model="search" @submit="FetchList" @reset="FetchList">
+      <el-form-item prop="organIds">
+        <el-select
+          multiple
+          clearable
+          filterable
+          collapse-tags
+          v-model="search.organIds"
+        >
+          <el-option v-for="(item,index) in selects.branchs"
+            :key="index"
+            :label="item.name"
+            :value="item.id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-button native-type="submit" type="primary">搜索</el-button>
+      <el-button native-type="reset" type="danger">重置</el-button>
+    </save-form>
+    <empty desc="暂无需要处理异常" v-if="!list.length"/>
+    <title-item
+      v-else
+      type="warning"
+      v-for="(item, index) in list"
+      :key="index"
+      :data="[{name: item.desc, num: item.num}]"
+    >
+      <el-button @click="$router.push({
+        path: '/business/teamDetail',
+        query: {
+          search: (item.result || []).join(',')
+        }
+      })" type="text">立即处理<i class="el-icon-d-arrow-right"/></el-button>
+    </title-item>
+  </div>
+</template>
+<script>
+import { getRemindMatterData } from '@/views/main/api'
+import title from '../abnormal/title'
+export default {
+  components: {
+    'title-item': title
+  },
+  data() {
+    return {
+      search: {
+        organIds: []
+      },
+      list: [],
+    }
+  },
+  mounted() {
+    this.FetchList()
+  },
+  methods: {
+    async FetchList() {
+      try {
+        const res = await getRemindMatterData()
+        this.list = res.data
+      } catch (error) {}
+    }
+  }
+}
+</script>

+ 205 - 0
src/views/main/schedule-branch/index.vue

@@ -0,0 +1,205 @@
+<template>
+  <div>
+    <save-form inline :model="search" @submit="submit" @reset="reset">
+      <el-form-item prop="organIds">
+        <el-select
+          multiple
+          clearable
+          filterable
+          collapse-tags
+          v-model="search.organIds"
+          placeholder="请选择分部"
+        >
+          <el-option v-for="(item,index) in selects.branchs"
+            :key="index"
+            :label="item.name"
+            :value="item.id"></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="organIds">
+        <el-select
+          v-model.trim="search.teacherIdList"
+          clearable
+          filterable
+          placeholder="请选择老师"
+        >
+          <el-option v-for="(item, index) in selects.teachers"
+            :key="index"
+            :value="item.id"
+            :label="item.realName"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="month">
+        <el-date-picker
+          v-model="search.month"
+          type="month"
+          placeholder="请选择月份">
+        </el-date-picker>
+      </el-form-item>
+      <el-button native-type="submit" type="primary">搜索</el-button>
+      <el-button native-type="reset" type="danger">重置</el-button>
+    </save-form>
+    <el-button type="primary" @click="visible = true">添加任务</el-button>
+    <el-table
+      :data="list"
+      style="width: 100%;margin-top: 20px;"
+      :header-cell-style="{ background: '#EDEEF0', color: '#444' }"
+    >
+      <el-table-column
+        label="分部"
+        prop="organName"
+      ></el-table-column>
+      <el-table-column
+        label="工作周期"
+        prop="month"
+      >
+        <span slot-scope="scope">{{$helpers.dayjs(scope.row.month).format('YYYY-MM')}}</span>
+      </el-table-column>
+      <el-table-column
+        label="乐团主管"
+        prop="userName"
+      ></el-table-column>
+      <el-table-column
+        label="任务事项数量"
+        prop="itemNum"
+      ></el-table-column>
+      <el-table-column
+        label="任务总次数"
+        prop="times"
+      ></el-table-column>
+      <el-table-column
+        label="操作"
+        prop="操作"
+      >
+        <template slot-scope="scope">
+          <el-button type="text" @click="view(scope.row)">查看</el-button>
+          <el-button type="text" @click="edit(scope.row)">修改任务</el-button>
+          <el-button type="text" @click="remove(scope.row.id)">删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination :total.sync="rules.total"
+      :page.sync="rules.page"
+      :limit.sync="rules.limit"
+      :page-sizes="rules.page_size"
+      @pagination="FetchList"
+    />
+    <el-dialog
+      :visible.sync="visible"
+      title="创建任务"
+      width="800px"
+    >
+      <create
+        v-if="visible"
+        @close="visible = false"
+        @submited="FetchList"
+      />
+    </el-dialog>
+    <el-dialog
+      :visible.sync="editVisible"
+      title="修改任务"
+      width="800px"
+    >
+      <create
+        v-if="editVisible && detail"
+        :id="detail.id"
+        @close="editVisible = false"
+        @submited="FetchList"
+      />
+    </el-dialog>
+    <el-dialog
+      :visible.sync="viewVisible"
+      title="查看任务"
+      width="800px"
+    >
+      <view-detail
+        v-if="viewVisible && detail"
+        :id="detail.id"
+        @close="viewVisible = false"
+        @submited="FetchList"
+      />
+    </el-dialog>
+  </div>
+</template>
+<script>
+import pagination from "@/components/Pagination/index";
+import { inspectionQueryPage, inspectionDelete } from '@/views/main/api'
+import create from './modals/create'
+import view from './modals/view'
+import View from './modals/view.vue';
+const initSearch = {
+  organIds: [],
+  month: '',
+}
+export default {
+  components: {
+    create,
+    'view-detail': view,
+    pagination,
+  },
+  data() {
+    return {
+      search: {...initSearch},
+      list: [],
+      visible: false,
+      viewVisible: false,
+      editVisible: false,
+      detail: null,
+      rules: {
+        // 分页规则
+        limit: 10, // 限制显示条数
+        page: 1, // 当前页
+        total: 0, // 总条数
+        page_size: [10, 20, 40, 50] // 选择限制显示条数
+      },
+    }
+  },
+  mounted() {
+    this.FetchList()
+    this.$store.dispatch('setBranchs')
+    this.$store.dispatch('setTeachers')
+  },
+  methods: {
+    submit() {
+      this.FetchList()
+    },
+    reset() {
+      this.rules.page = 1
+      this.search = {...initSearch}
+      this.FetchList()
+    },
+    view(row) {
+      this.viewVisible = true
+      this.detail = row
+    },
+    edit(row) {
+      this.editVisible = true
+      this.detail = row
+    },
+    async remove(id) {
+      try {
+        await this.$confirm('是否确认删除此条数据?', '提示', {
+          type: 'warning'
+        })
+        await inspectionDelete({
+          id
+        })
+        this.$message.success('删除成功')
+        this.FetchList()
+      } catch (error) {}
+    },
+    async FetchList() {
+      try {
+        const res = await inspectionQueryPage({
+          ...this.search,
+          page: this.rules.page,
+          rows: this.rules.limit,
+        })
+        this.list = res.data.rows
+        this.rules.total = res.data.total
+      } catch (error) {}
+    }
+  }
+}
+</script>

+ 262 - 0
src/views/main/schedule-branch/modals/create.vue

@@ -0,0 +1,262 @@
+<template>
+  <div>
+    <el-form ref="form" :model="form" inline>
+      <el-row>
+        <el-col :span="6">
+          <el-form-item
+            label="分部"
+            prop="organId"
+            :rules="[{required: true, message: '请选择分部'}]"
+          >
+            <el-select
+              clearable
+              filterable
+              v-model="form.organId"
+              placeholder="请选择分部"
+            >
+              <el-option v-for="(item,index) in selects.branchs"
+                :key="index"
+                :label="item.name"
+                :value="item.id"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <el-col :span="6">
+          <el-form-item
+            label="工作周期"
+            prop="month"
+            :rules="[{required: true, message: '请选择工作周期'}]"
+          >
+            <el-date-picker
+              v-model="form.month"
+              type="month"
+              placeholder="请选择工作周期">
+            </el-date-picker>
+          </el-form-item>
+        </el-col>
+      </el-row>
+      <el-row class="group" v-for="(groupItem, groupIndex) in form.group" :key="groupIndex">
+        <el-col :span="6">
+          <el-form-item
+            label="乐团主管"
+            :prop="`group.${groupIndex}.userId`"
+            :rules="[{required: true, message: '请选择乐团主管'}]"
+          >
+            <el-select
+              clearable
+              filterable
+              v-model="groupItem.userId"
+              placeholder="请选择乐团主管"
+            >
+              <el-option v-for="(item,index) in technicians"
+                :key="index"
+                :label="item.realName"
+                :value="item.userId"></el-option>
+            </el-select>
+          </el-form-item>
+        </el-col>
+        <template v-for="(matterItem, matterIndex) in groupItem.matter">
+          <el-col :offset="matterIndex === 0 ? 0 : 6" :span="6" :key="groupIndex + '-' + matterIndex">
+            {{groupIndex + '-' + matterIndex}}
+            <el-form-item
+              :label="'任务事项' + (matterIndex + 1)"
+              :prop="`group.${groupIndex}.matter.${matterIndex}.item`"
+            >
+              <el-select
+                clearable
+                filterable
+                v-model="matterItem.item"
+                placeholder="请选择任务事项"
+              >
+                <el-option v-for="(item,index) in matterTypesOptions"
+                  :key="index"
+                  :label="item.label"
+                  :value="item.value"></el-option>
+              </el-select>
+            </el-form-item>
+          </el-col>
+          <el-col :span="6" :key="groupIndex + '-' + matterIndex">
+            <el-form-item
+              :label="'任务次数' + (matterIndex + 1)"
+              :prop="`group.${groupIndex}.matter.${matterIndex}.times`"
+            >
+              <el-input clearable v-model="matterItem.times" placeholder="请输入次数" />
+            </el-form-item>
+          </el-col>
+          <el-col :span="6" :key="groupIndex + '-' + matterIndex">
+            <div class="ctrl">
+              <span>
+                <el-tooltip content="添加任务" placement="top" :open-delay=".5">
+                  <el-button
+                    icon="el-icon-plus"
+                    @click="createMatter(groupItem.matter)"
+                    circle
+                    size="small"
+                  ></el-button>
+                </el-tooltip>
+                <el-tooltip content="删除任务" placement="top" :open-delay=".5">
+                  <el-button
+                    icon="el-icon-minus"
+                    circle
+                    size="small"
+                    @click="removeMatter(groupItem.matter, matterIndex)"
+                    :disabled="groupItem.matter.length <= 1"
+                  ></el-button>
+                </el-tooltip>
+              </span>
+              <el-tooltip  v-if="isCreate" content="删除乐团主管" placement="top" :open-delay=".5">
+                <el-button
+                  icon="el-icon-delete"
+                  circle
+                  type="danger"
+                  size="small"
+                  v-if="matterIndex === 0"
+                  @click="removeGroup(form.group, groupIndex)"
+                  :disabled="form.group.length <= 1"
+                ></el-button>
+              </el-tooltip>
+            </div>
+          </el-col>
+        </template>
+      </el-row>
+      <el-button v-if="isCreate" @click="createGroup" plain block style="width: 100%">添加乐团主管</el-button>
+    </el-form>
+    <div slot="footer" style="text-align: right;margin-top: 20px;">
+      <el-button>取消</el-button>
+      <el-button type="primary" @click="submit">确认</el-button>
+    </div>
+  </div>
+</template>
+<script>
+import { findTechnician } from '@/api/repairManager'
+import { matterTypes } from '@/views/main/constant'
+import { objectToOptions } from '@/utils'
+import { createRandom } from '@/helpers/uuidv4'
+import { inspectionAdd, inspectionGetInfo, inspectionUpdate } from '@/views/main/api'
+const emptyMatter = {
+  item: '',
+  times: ''
+}
+export default {
+  props: ['id'],
+  data() {
+    return {
+      form: {
+        organId: '',
+        group: [{
+          _uuid: createRandom(),
+          userId: '',
+          matter: [{...emptyMatter}]
+        }]
+      },
+      technicians: []
+    }
+  },
+  computed: {
+    matterTypesOptions() {
+      return objectToOptions(matterTypes)
+    },
+    isCreate() {
+      return !this.id
+    },
+  },
+  watch: {
+    async 'form.organId'() {
+      if (this.form.organId) {
+        try {
+          const res = await findTechnician({
+            organId: this.form.organId
+          })
+          this.technicians = res.data
+          this.$set(this.form, 'group', group.map(item => ({...item, userId: ''})))
+        } catch (error) {}
+      }
+    }
+  },
+  async mounted() {
+    try {
+      const res = await inspectionGetInfo({
+        id: this.id
+      })
+      this.form = {
+        ...this.form,
+        organId: res.data.organId,
+        month: res.data.month,
+        group: [{
+          _uuid: createRandom(),
+          userId: res.data.userId,
+          matter: res.data.inspectionItems.map(item => ({item: item.item, times: item.times}))
+        }]
+      }
+    } catch (error) {}
+  },
+  methods: {
+    createGroup() {
+      this.form.group.push({
+        userId: '',
+        matter: [{...emptyMatter}]
+      })
+    },
+    createMatter(matter) {
+      matter.push({...emptyMatter})
+    },
+    removeMatter(matter, index) {
+      matter.splice(index, 1)
+    },
+    async removeGroup(group, index) {
+      try {
+        await this.$confirm('是否确认删除此乐团主管?', '提示', {
+          type: 'warning'
+        })
+        group.splice(index, 1)
+      } catch (error) {}
+    },
+    async submit() {
+      try {
+        this.$refs.form.validate(async valid => {
+          if (valid) {
+            const data = this.form.group.map(item => ({
+              organId: this.form.organId,
+              month: this.form.month,
+              userId: item.userId,
+              inspectionItems: item.matter.map(m => ({
+                ...m,
+                organId: this.form.organId,
+                month: this.form.month,
+              }))
+            }))
+            if (this.isCreate) {
+              await inspectionAdd(data)
+              this.$message.success('创建成功')
+            } else {
+              await inspectionUpdate({
+                ...data[0],
+                id: this.id,
+              })
+              this.$message.success('修改成功')
+            }
+            this.$emit('close')
+            this.$emit('submited')
+          }
+        })
+      } catch (error) {}
+    }
+  }
+}
+</script>
+<style lang="less" scoped>
+.group{
+  background-color: rgba(0, 0, 0, 0.05);
+  padding: 10px;
+  border-radius: 3px;
+  margin-bottom: 10px;
+}
+.ctrl{
+  margin-top: 45px;
+  display: flex;
+  justify-content: space-between;
+  i{
+    font-size: 24px;
+  }
+}
+</style>

+ 60 - 0
src/views/main/schedule-branch/modals/view.vue

@@ -0,0 +1,60 @@
+<template>
+  <div>
+    <descriptions :column="3">
+      <descriptions-item label="分部">{{detail.organName}}</descriptions-item>
+      <descriptions-item label="工作周期">{{$helpers.dayjs(detail.month).format('YYYY-MM')}}</descriptions-item>
+      <descriptions-item label="乐团主管">{{detail.userName}}</descriptions-item>
+      <descriptions-item label="处理意见">{{detail.memo}}</descriptions-item>
+    </descriptions>
+    <el-table
+      :data="detail.inspectionItems || []"
+      style="width: 100%;margin-top: 20px;"
+      max-height="500px"
+      :header-cell-style="{ background: '#EDEEF0', color: '#444' }"
+    >
+      <el-table-column
+        label="任务事项"
+        prop="item"
+      >
+        <template slot-scope="scope">{{matterTypes[scope.row.item]}}</template>
+      </el-table-column>
+      <el-table-column
+        label="任务次数"
+        prop="times"
+      />
+      <el-table-column
+        label="实际安排"
+        prop="plannedTimes"
+      />
+    </el-table>
+    <div slot="footer" style="text-align: right;margin-top: 20px;">
+      <el-button type="primary" @click="$emit('close')">确认</el-button>
+    </div>
+  </div>
+</template>
+<script>
+import { inspectionGetInfo } from '@/views/main/api'
+import { matterTypes } from '@/views/main/constant'
+export default {
+  props: ['id'],
+  data() {
+    return {
+      matterTypes,
+      detail: {},
+    }
+  },
+  mounted() {
+    this.FetchDetail()
+  },
+  methods: {
+    async FetchDetail() {
+      try {
+        const res = await inspectionGetInfo({
+          id: this.id
+        })
+        this.detail = res.data
+      } catch (error) {}
+    }
+  }
+}
+</script>

+ 227 - 0
src/views/main/teamSchedule/index.vue

@@ -0,0 +1,227 @@
+<template>
+  <div>
+    <save-form
+      :inline="true"
+      :model="searchForm"
+      ref="searchForm"
+      @submit="search"
+      @reset="onReSet"
+      save-key="teamSchedule"
+    >
+      <el-form-item prop="organId">
+        <el-select
+          class="multiple"
+          v-model.trim="searchForm.organId"
+          filterable
+          clearable
+          placeholder="请选择分部"
+        >
+          <el-option
+            v-for="(item, index) in selects.branchs"
+            :key="index"
+            :label="item.name"
+            :value="item.id"
+          ></el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item prop="userId">
+        <remote-search :commit="'setEducations'" v-model="searchForm.userId" />
+      </el-form-item>
+      <el-form-item prop="month">
+        <el-date-picker
+          v-model="searchForm.month"
+          value-format="yyyy-MM"
+          type="month"
+          placeholder="请选择月"
+        >
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button native-type="submit" type="primary">搜索</el-button>
+        <el-button native-type="reset" type="danger">重置</el-button>
+      </el-form-item>
+    </save-form>
+    <el-table
+      style="width: 100%"
+      :header-cell-style="{ background: '#EDEEF0', color: '#444' }"
+      :data="tableList"
+    >
+      <el-table-column align="center" prop="organName" label="分部">
+        <template slot-scope="scope">
+          <div>
+            <copy-text>{{ scope.row.organName }}</copy-text>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="month" label="工作周期">
+        <template slot-scope="scope">
+          <div>
+            {{ scope.row.month | dayjsFormatWeek }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="userName" label="乐团主管">
+        <template slot-scope="scope">
+          <div>
+            <copy-text>{{ scope.row.userName }}</copy-text>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="item" label="任务事项">
+        <template slot-scope="scope">
+          <div>
+            {{ scope.row.item | inspectionItemFormat }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column
+        align="center"
+        prop="times"
+        label="任务次数"
+      ></el-table-column>
+      <el-table-column
+        align="center"
+        prop="plannedTimes"
+        label="已安排日程次数"
+      ></el-table-column>
+      <el-table-column
+        align="center"
+        prop="submittedTimes"
+        label="已提交任务数"
+      >
+        <template slot-scope="scope">
+          <div
+            :style="
+              scope.row.times > scope.row.submittedTimes ? 'color:red' : ''
+            "
+          >
+            {{ scope.row.submittedTimes }}
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="memo" label="处理意见" width="220">
+        <template slot-scope="scope">
+          <overflow-text :text="scope.row.memo"></overflow-text>
+        </template>
+      </el-table-column>
+      <el-table-column align="center" prop="studentId" label="操作">
+        <template slot-scope="scope">
+          <div>
+            <el-button type="text"  @click="gotoHander(scope.row)">安排日程</el-button>
+            <auth :auths="['inspectionItem/update']">
+              <el-button
+                type="text"
+                :disabled="scope.row.times < scope.row.submittedTimes"
+                @click="resetLine(scope.row)"
+                >处理方式</el-button
+              >
+            </auth>
+          </div>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      sync
+      :total.sync="rules.total"
+      save-key="teamSchedule"
+      :page.sync="rules.page"
+      :limit.sync="rules.limit"
+      :page-sizes="rules.page_size"
+      @pagination="getList"
+    />
+    <el-dialog title="处理方式" width="700px" :visible.sync="handleVisible">
+      <el-form :model="handleForm">
+        <el-form-item label="请填写处理方式" prop="memo">
+          <el-input
+            type="textarea"
+            :rows="3"
+            v-model="handleForm.memo"
+          ></el-input>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button @click="handleVisible = false">取 消</el-button>
+        <el-button type="primary" @click="submitHandle">确 定</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+<script>
+import { getInspectionItem, resetInspectionItem } from "../api";
+import { permission } from "@/utils/directivePage";
+import pagination from "@/components/Pagination/index";
+export default {
+  components: { pagination },
+  data() {
+    return {
+      searchForm: {
+        userId: "",
+        organId: "",
+        month: "",
+      },
+      handleForm: {
+        memo: "",
+        id: "",
+      },
+      tableList: [],
+      handleVisible: false,
+      rules: {
+        // 分页规则
+        limit: 10, // 限制显示条数
+        page: 1, // 当前页
+        total: 0, // 总条数
+        page_size: [10, 20, 40, 50], // 选择限制显示条数
+      },
+    };
+  },
+  mounted() {
+    this.$store.dispatch("setBranchs");
+    this.getList();
+  },
+  methods: {
+    permission(str) {
+      return permission(str);
+    },
+    onReSet() {
+      this.rules.page = 1;
+      this.$refs.searchForm.resetFields();
+    },
+    search() {
+      this.rules.page = 1;
+      this.getList();
+    },
+    async getList() {
+      try {
+        this.searchForm.page = this.rules.page;
+        this.searchForm.rows = this.rules.limit;
+        const res = await getInspectionItem(this.searchForm);
+        this.tableList = res.data.rows;
+        this.rules.total = res.data.total;
+      } catch (e) {
+        console.log(e);
+      }
+    },
+    resetLine(row) {
+      let { id, memo } = row;
+      this.handleForm = { id, memo };
+      this.handleVisible = true;
+    },
+    async submitHandle() {
+      console.log(this.handleForm);
+      try {
+        const res = await resetInspectionItem(this.handleForm);
+        this.$message.success('提交成功')
+        this.getList()
+        console.log(res);
+      } catch (e) {
+        console.log(e);
+      }
+    },
+    gotoHander(row){
+
+    }
+  },
+};
+</script>
+<style lang="scss" scoped>
+</style>

+ 1 - 1
src/views/teamBuild/teamSeting/index.vue

@@ -311,4 +311,4 @@ export default {
     padding-top: 12px;
   }
 }
-</style>
+</style>

+ 38 - 8
src/views/teamDetail/components/studentList.vue

@@ -15,16 +15,21 @@
         <span>{{ studentListInfo.add }}</span>
       </statistic-item>
       <statistic-item>
+        <span>VIP&网管转化率</span>
+        <span>{{ studentListInfo.courseRate }}</span>
+      </statistic-item>
+    </statistic>
+    <!-- <statistic-item>
         <div style="display: flex;">
           <div>
             <div class="newStudent"
               style="margin-bottom:10px;"
               v-permission="'studentRegistration/insertStudent'"
               @click="addStudentVisible = true">新增学员</div>
-          <!-- <div class="newStudent"
+          <div class="newStudent"
               style="margin-bottom:10px;"
               v-permission="'teamDetails/studentList/QRCode/822'"
-              @click="onCreateQRCode">报名连接</div> -->
+              @click="onCreateQRCode">报名连接</div>
             <div class="newStudent"
               v-permission="'/studentSignin'"
               @click="gotoSignin">点名总览</div>
@@ -36,9 +41,15 @@
               @click="viewTimer">剩余时长明细</div>
           </div>
         </div>
-      </statistic-item>
-    </statistic>
-
+      </statistic-item> -->
+    <div style="margin-bottom: 15px;">
+      <el-button type="primary" v-permission="'studentRegistration/insertStudent'"
+              @click="addStudentVisible = true">新增学员</el-button>
+      <el-button type="primary" v-permission="'/studentSignin'"
+              @click="gotoSignin">点名总览</el-button>
+      <el-button type="primary" v-permission="'studentManage/queryStudentSubTotalCourseTimes'"
+              @click="viewTimer">剩余时长明细</el-button>
+    </div>
     <!-- 搜索类型 -->
     <save-form
     ref='searchForm'
@@ -122,6 +133,16 @@
           <el-option label="否" value="0"></el-option>
         </el-select>
       </el-form-item>
+      <el-form-item prop="hasCourse">
+        <el-select
+          v-model.trim="searchForm.hasCourse"
+          clearable
+          placeholder="VIP/网管是否有课"
+        >
+          <el-option label="是" value="true"></el-option>
+          <el-option label="否" value="false"></el-option>
+        </el-select>
+      </el-form-item>
       <el-form-item>
         <el-button native-type="submit" type="danger">搜索</el-button>
         <el-button type="primary" native-type="reset">重置</el-button>
@@ -221,6 +242,11 @@
             <div>{{ scope.row.isActive ? "是" : "否" }}</div>
           </template>
         </el-table-column>
+        <el-table-column align="center" label="VIP/网管是否有课">
+          <template slot-scope="scope">
+            <div>{{ scope.row.hasCourse ? "是" : "否" }}</div>
+          </template>
+        </el-table-column>
         <el-table-column align="center" label="欠费金额(元)">
           <template slot-scope="scope">
             <div :class="[scope.row.noPaymentAmount > 0 ? 'error' : null]">{{ scope.row.noPaymentAmount | moneyFormat }}
@@ -684,6 +710,7 @@ export default {
         search: "",
         isActive: "",
         classGroupId: null,
+        hasCourse: null
       },
       organizationCourseUnitPriceSettings: [],
       quitForm: {
@@ -710,6 +737,7 @@ export default {
         add: "",
         quit: "",
         studying: "",
+        courseRate: "",
       },
       signList: [],
       mixList: [],
@@ -852,9 +880,9 @@ export default {
       return template[val];
     },
   },
-  activated() {
-    this.init();
-  },
+  // activated() {
+  //   this.init();
+  // },
   async mounted() {
     try {
       const res = await getOrganizationCourseUnitPriceSettings({
@@ -969,6 +997,7 @@ export default {
         search: this.searchForm.search || null,
         isActive: this.searchForm.isActive || null,
         classGroupId: this.searchForm.classGroupId || null,
+        hasCourse: this.searchForm.hasCourse || null
       };
       const options = {
         method: "get",
@@ -1043,6 +1072,7 @@ export default {
         search: this.searchForm.search || null,
         isActive: this.searchForm.isActive || null,
         classGroupId: this.searchForm.classGroupId || null,
+        hasCourse: this.searchForm.hasCourse || null
       };
       getTeamStudentList(obj).then((res) => {
         if (res.code == 200) {

File diff suppressed because it is too large
+ 445 - 310
src/views/teamDetail/teamCourseList.vue


+ 10 - 0
src/views/teamDetail/teamList.vue

@@ -337,6 +337,7 @@ export default {
     closeStudens
   },
   mounted () {
+    this.$set(this.topForm, 'teamName', this.$route.query.search)
     this.init();
   },
   activated () {
@@ -368,6 +369,15 @@ export default {
     reset () {
       this.rules.page = 1;
       this.$refs['topForm'].resetFields();
+      if (this.$route.query.search) {
+        this.$router.replace({
+          path: this.$route.path,
+          query: {
+            ...this.$route.query,
+            search: undefined
+          }
+        })
+      }
       this.getList()
     },
     search () {

+ 41 - 0
src/workers/notification.js

@@ -0,0 +1,41 @@
+const createMessage = async data => {
+  if (self.Notification.permission === 'granted') {
+    const {title, body, ...rest} = data
+      const n = new Notification(title, {
+        body,
+        data: rest
+      })
+      n.addEventListener('click', () => {
+        n.close()
+        self.postMessage({
+          type: 'NotificationClicked',
+          data: rest
+        })
+      })
+      n.addEventListener('show', () => {
+        self.postMessage({
+          type: 'NotificationShowed',
+          data: rest
+        })
+      })
+      n.addEventListener('error', () => {
+        self.postMessage({
+          type: 'NotificationErrored',
+          data: rest
+        })
+      })
+      n.addEventListener('close', () => {
+        self.postMessage({
+          type: 'NotificationClosed',
+          data: rest
+        })
+      })
+   }
+}
+
+self.addEventListener('message', evt => {
+  const {type, ...rest} = evt.data
+  if (type === 'create') {
+    createMessage(rest)
+  }
+})

+ 3 - 1
vue.config.js

@@ -48,12 +48,14 @@ module.exports = {
     }
   },
   devServer: {
-    // port: port,
+    // port: 443,
     open: true,
+    disableHostCheck: true,
     // overlay: {
     //   warnings: false,
     //   errors: true
     // },
+    // https: true,
     proxy: {
       // change xxx-api/login => mock/login
       // detail: https://cli.vuejs.org/config/#devserver-proxy

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