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

Merge remote-tracking branch 'origin/master'

yjwang 1 месяц назад
Родитель
Сommit
b81633c191
39 измененных файлов с 18110 добавлено и 716 удалено
  1. 8 0
      src/api/course/courseRedPacketLog.js
  2. 8 0
      src/api/course/courseWatchLog.js
  3. 17 0
      src/api/qw/externalContact.js
  4. 9 0
      src/api/qw/externalContactTransferLog.js
  5. 17 0
      src/api/qw/friendWelcome.js
  6. 22 0
      src/api/qw/groupChat.js
  7. 16 0
      src/api/qw/sop.js
  8. 8 0
      src/api/qw/user.js
  9. 115 0
      src/api/system/employeeStats.js
  10. 1074 0
      src/components/TreeSelect/dateRange.vue
  11. 978 0
      src/components/TreeSelect/index.vue
  12. 25 5
      src/views/course/courseRedPacketLog/index.vue
  13. 1 0
      src/views/course/courseRedPacketLog/myCourseRedPacketLog.vue
  14. 506 0
      src/views/course/courseWatchLog/deptWatchLog.vue
  15. 1 1
      src/views/course/courseWatchLog/index.vue
  16. 8 2
      src/views/course/courseWatchLog/watchLog.vue
  17. 1 1
      src/views/course/sop/index.vue
  18. 1929 0
      src/views/qw/externalContact/deptIndex.vue
  19. 439 0
      src/views/qw/externalContactLoss/deptLossIndex.vue
  20. 524 0
      src/views/qw/externalContactTransfer/deptTransferIndex.vue
  21. 318 0
      src/views/qw/externalContactTransferLog/deptTransferLogIndex.vue
  22. 474 0
      src/views/qw/externalContactUnassigned/deptUnassignedIndex.vue
  23. 1278 0
      src/views/qw/friendWelcome/deptFriendWelcome.vue
  24. 5 5
      src/views/qw/friendWelcome/indexNew.vue
  25. 241 35
      src/views/qw/friendWelcome/myWelcome.vue
  26. 260 0
      src/views/qw/groupChat/deptGroupIndex.vue
  27. 261 0
      src/views/qw/groupChat/myGroupChat.vue
  28. 1826 0
      src/views/qw/sop/deptSop.vue
  29. 1857 0
      src/views/qw/sop/mySop.vue
  30. 1 1
      src/views/qw/sop/sop.vue
  31. 0 322
      src/views/qw/sopLogs/sopLogsListOld.vue
  32. 1129 0
      src/views/qw/user/cuDeptIdIndex.vue
  33. 397 344
      src/views/qw/user/index.vue
  34. 701 0
      src/views/qw/welcome/deptWelcomeIndex.vue
  35. 701 0
      src/views/qw/welcome/myWelcome.vue
  36. 1673 0
      src/views/statistics/index.vue
  37. 435 0
      src/views/statistics/section/channel.vue
  38. 416 0
      src/views/statistics/section/index.vue
  39. 431 0
      src/views/statistics/section/today.vue

+ 8 - 0
src/api/course/courseRedPacketLog.js

@@ -69,6 +69,14 @@ export function delCourseRedPacketLog(logId) {
   })
 }
 
+
+export function retryCourseRedPacketLog(logId) {
+  return request({
+    url: '/course/courseRedPacketLog/retryCourseRedPacketLog/' + logId,
+    method: 'put'
+  })
+}
+
 // 导出短链课程看课记录
 export function exportCourseRedPacketLog(query) {
   return request({

+ 8 - 0
src/api/course/courseWatchLog.js

@@ -9,6 +9,14 @@ export function listCourseWatchLog(query) {
   })
 }
 
+export function deptListCourseWatchLog(query) {
+  return request({
+    url: '/course/courseWatchLog/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
 export function myListCourseWatchLog(query) {
   return request({
     url: '/course/courseWatchLog/myList',

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

@@ -8,6 +8,23 @@ export function listExternalContact(query) {
     params: query
   })
 }
+
+// 查询企业微信客户列表
+export function getRepeat(query) {
+  return request({
+    url: '/qw/externalContact/getRepeat',
+    method: 'get',
+    params: query
+  })
+}
+
+export function myDeptExtList(query) {
+  return request({
+    url: '/qw/externalContact/myDeptExtList',
+    method: 'get',
+    params: query
+  })
+}
 export function myList(query) {
   return request({
     url: '/qw/externalContact/myList',

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

@@ -9,6 +9,15 @@ export function listExternalContactTransferLog(query) {
   })
 }
 
+export function listTransferLogDeptList(query) {
+  return request({
+    url: '/qw/externalContactTransferLog/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
+
 export function syncTransferLog(id) {
   return request({
     url: '/qw/externalContactTransferLog/sync/' + id,

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

@@ -8,6 +8,23 @@ export function listFriendWelcome(query) {
     params: query
   })
 }
+
+export function listMyFriendWelcome(query) {
+  return request({
+    url: '/qw/friendWelcome/myList',
+    method: 'get',
+    params: query
+  })
+}
+
+export function deptListFriendWelcome(query) {
+  return request({
+    url: '/qw/friendWelcome/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
 // 查询好友欢迎语列表
 export function myList(query) {
   return request({

+ 22 - 0
src/api/qw/groupChat.js

@@ -8,6 +8,20 @@ export function listGroupChat(query) {
     params: query
   })
 }
+export function listGroupChatDeptList(query) {
+  return request({
+    url: '/qw/groupChat/deptList',
+    method: 'get',
+    params: query
+  })
+}
+export function listGroupChatMyList(query) {
+  return request({
+    url: '/qw/groupChat/myList',
+    method: 'get',
+    params: query
+  })
+}
 
 // 查询客户群详情详细
 export function getGroupChat(chatId) {
@@ -30,6 +44,14 @@ export function cogradientGroupChat(corpId) {
   })
 }
 
+export function cogradientMyGroupChat(corpId) {
+  return request({
+    url: '/qw/groupChat/cogradientMyGroupChat/'+corpId,
+    method: 'get',
+  })
+}
+
+
 
 export function listAll(qwUserIds, corpId, sopId) {
   return request({

+ 16 - 0
src/api/qw/sop.js

@@ -8,6 +8,22 @@ export function listSop(query) {
     params: query
   })
 }
+
+export function listMySop(query) {
+  return request({
+    url: '/qw/sop/myList',
+    method: 'get',
+    params: query
+  })
+}
+export function listDeptSop(query) {
+  return request({
+    url: '/qw/sop/deptList',
+    method: 'get',
+    params: query
+  })
+}
+
 // 查询企微sop模板列表
 export function listAiChatSop(query) {
   return request({

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

@@ -9,6 +9,14 @@ export function staffListUser(query) {
   })
 }
 
+export function myDepartListUser(query) {
+  return request({
+    url: '/qw/user/myDepartList',
+    method: 'get',
+    params: query
+  })
+}
+
 // 查询企微用户列表
 export function listUser(query) {
   return request({

+ 115 - 0
src/api/system/employeeStats.js

@@ -0,0 +1,115 @@
+import request from '@/utils/request'
+
+// 查询员工完成统计列表
+export function listEmployeeStats(query) {
+  return request({
+    url: '/stats/seller/pageList',
+    method: 'post',
+    data: query
+  })
+}
+
+export function listInlineList(query) {
+  return request({
+    url: '/stats/inline/pageList',
+    method: 'post',
+    data: query
+  })
+}
+
+export function exportInlineList(param){
+  return request({
+    url: '/stats/inline/export',
+    method: 'post',
+    data: param
+  })
+}
+export function exportSellerData(param){
+  return request({
+    url: '/stats/seller/export',
+    method: 'post',
+    data: param
+  })
+}
+
+export function listPeriodList(query) {
+  return request({
+    url: '/stats/period/pageList',
+    method: 'post',
+    data: query
+  })
+}
+
+export function exportPeriodData(query){
+  return request({
+    url: '/stats/period/export',
+    method: 'post',
+    data: query
+  })
+}
+
+export function exportTodayData(query) {
+  return request({
+    url: '/stats/everyDay/export',
+    method: 'post',
+    data: query
+  })
+}
+
+export function listTodayList(query) {
+  return request({
+    url: '/stats/everyDay/pageList',
+    method: 'post',
+    data: query
+  })
+}
+
+// 查询员工完成统计详细
+export function getEmployeeStats(id) {
+  return request({
+    url: '/system/employeeStats/' + id,
+    method: 'get'
+  })
+}
+
+// 获取员工列表
+export function getEmployeeList() {
+  return request({
+    url: '/system/employee/list',
+    method: 'get'
+  })
+}
+
+// 获取频道列表
+export function getChannelList() {
+  return request({
+    url: '/system/channel/list',
+    method: 'get'
+  })
+}
+
+// 导出员工完成统计
+export function exportEmployeeStats(query) {
+  return request({
+    url: '/system/employeeStats/export',
+    method: 'post',
+    data: query
+  })
+}
+
+// 获取SOP任务数据
+export function getSOPTaskData(param){
+  return request({
+    url: '/stats/sopTaskData',
+    method: 'post',
+    data: param
+  })
+}
+
+// 获取部门数据
+export function getDeptData(param){
+  return request({
+    url: '/stats/getDeptData',
+    method: 'get'
+  })
+}

+ 1074 - 0
src/components/TreeSelect/dateRange.vue

@@ -0,0 +1,1074 @@
+<template>
+  <div class="task-select-tree" :style="{ width: componentWidth }">
+    <el-popover
+      ref="popover"
+      placement="bottom-start"
+      :width="popoverComputedWidth"
+      trigger="click"
+      v-model="popoverVisible"
+      popper-class="task-select-tree-popper"
+      :disabled="disabled"
+      @show="onPopoverShow"
+    >
+      <div class="popover-content-wrapper">
+        <el-date-picker v-model="dateRange"
+                        size="small"
+                        style="width: 220px"
+                        value-format="yyyy-MM-dd"
+                        type="daterange"
+                        range-separator="-"
+                        start-placeholder="营期开始日期"
+                        end-placeholder="营期结束日期"
+        @change="dateRangeChange"/>
+        &nbsp;<button type="primary" @click="dateRangeChange(dateRange)">查询</button>
+        <br/>
+        <br/>
+        <el-input
+          v-if="filterable"
+          placeholder="输入关键字过滤"
+          v-model="filterText"
+          size="mini"
+          clearable
+          class="filter-input"
+          @input="handleFilterDebounced"
+        >
+        </el-input>
+
+        <div class="tree-container" :style="{ height: treeHeight + 'px' }">
+          <div
+            ref="virtualList"
+            class="virtual-tree-list"
+            @scroll="handleScroll"
+            :style="{ height: treeHeight + 'px', overflow: 'auto' }"
+          >
+            <div
+              class="virtual-content"
+              :style="{
+                height: totalHeight + 'px',
+                position: 'relative',
+                paddingTop: offsetY + 'px'
+              }"
+            >
+              <div
+                v-for="(item, index) in visibleItems"
+                :key="`${item.key}-${item.level}-${index}`"
+                :style="{
+                  height: itemHeight + 'px',
+                  paddingLeft: (item.level * indentSize) + 'px',
+                  display: 'flex',
+                  alignItems: 'center',
+                  position: 'relative'
+                }"
+                :class="[
+                  'virtual-tree-node',
+                  {
+                    'is-leaf': item.isLeaf,
+                    'is-checked': checkedKeysSet.has(item.key) && (item.isLeaf || parentSelectable || !checkStrictly),
+                    'is-disabled': item.nodeDisabled || checkboxDisabled(item) // Combined disabled state for row styling
+                  }
+                ]"
+                @click="handleNodeClick(item)"
+              >
+                <div class="node-expand-container">
+                  <i
+                    v-if="!item.isLeaf"
+                    :class="[
+                      'node-expand-icon',
+                      item.expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'
+                    ]"
+                    @click.stop="toggleNodeExpand(item)"
+                  ></i>
+                  <span v-else class="node-expand-placeholder"></span>
+                </div>
+
+                <el-checkbox
+                  :value="checkedKeysSet.has(item.key)"
+                  :disabled="checkboxDisabled(item)"
+                  @change="handleCheckboxChange(item, $event)"
+                  @click.stop
+                  class="node-checkbox"
+                ></el-checkbox>
+
+                <span class="node-label" :title="item.label">{{ item.label }}</span>
+
+                <span v-if="item.isLeaf && showNodeDetail" class="node-detail-text">
+                  (ID: {{ item.originalData.qwUserId || item.originalData.id }}{{ item.originalData.taskExecDate ? ' ' + item.originalData.taskExecDate : '' }})
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="popover-footer">
+          <span class="selected-count">已选择: {{ displaySelectedCount }}</span>
+          <el-button size="mini" @click="handleClearInPopover">清空</el-button>
+          <el-button size="mini" type="primary" @click="handleSelectAllToggle">{{ isCurrentSelectionAll ? '取消全选' : '全选' }}</el-button>
+          <el-button type="primary" size="mini" @click="handleConfirm">确定</el-button>
+        </div>
+      </div>
+
+      <div
+        slot="reference"
+        :class="['select-trigger-wrapper', { 'is-disabled': disabled, 'is-active': popoverVisible }]"
+        @mouseenter="inputHovering = true"
+        @mouseleave="inputHovering = false"
+      >
+        <div class="tags-or-value-container">
+          <template v-if="multiple && displaySelectedNodes.length > 0">
+            <el-tag
+              v-for="node in displaySelectedNodes"
+              :key="node.key"
+              type="info"
+              size="small"
+              closable
+              :disable-transitions="true"
+              @close.stop="removeTag(node)"
+              class="trigger-tag"
+            >
+              {{ node.label }}
+            </el-tag>
+            <el-tag
+              v-if="displaySelectedCount > maxDisplayTags && displaySelectedNodes.length === maxDisplayTags"
+              type="info"
+              size="small"
+              class="trigger-tag"
+            >
+              + {{ displaySelectedCount - maxDisplayTags }}
+            </el-tag>
+          </template>
+          <span v-else-if="!multiple && displaySelectedCount > 0" class="single-value-display">
+            {{ singleSelectedLabel }}
+          </span>
+          <span v-else class="placeholder-text">{{ placeholder }}</span>
+        </div>
+
+        <span class="icons-container">
+          <i
+            v-if="showClearIcon"
+            class="el-icon-circle-close clear-icon"
+            @click.stop="handleClearOnTrigger"
+          ></i>
+          <i :class="['el-icon-arrow-up', 'arrow-icon', { 'is-reverse': !popoverVisible }]"></i>
+        </span>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<script>
+// 防抖函数
+function debounce(func, wait) {
+  let timeout;
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout);
+      func(...args);
+    };
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+  };
+}
+
+export default {
+  name: 'OptimizedSelectTree',
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    rawData: {
+      type: Array,
+      required: true
+    },
+    placeholder: {
+      type: String,
+      default: '请选择'
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    disabled: { // Global disabled for the component
+      type: Boolean,
+      default: false
+    },
+    checkStrictly: {
+      type: Boolean,
+      default: false // Important for parent-child linkage
+    },
+    filterable: {
+      type: Boolean,
+      default: true
+    },
+    dateRange: {
+      type: Array,
+      default: null
+    },
+    maxDisplayTags: {
+      type: Number,
+      default: 2
+    },
+    componentWidth: {
+      type: String,
+      default: '100%'
+    },
+    popoverWidth: {
+      type: [String, Number],
+      default: 'auto'
+    },
+    treeProps: {
+      type: Object,
+      default: () => ({
+        children: 'children',
+        label: 'label',
+        isLeaf: 'isLeaf',
+        disabled: 'disabled' // Key for disabled state in rawData
+      })
+    },
+    nodeKey: {
+      type: String,
+      default: 'id'
+    },
+    defaultExpandAll: {
+      type: Boolean,
+      default: false
+    },
+    showNodeDetail: {
+      type: Boolean,
+      default: true
+    },
+    treeHeight: {
+      type: Number,
+      default: 280
+    },
+    returnLeafOnly: {
+      // This prop's effect on output is now overridden.
+      // It might still be used by consumers for other logic if they rely on its original meaning.
+      type: Boolean,
+      default: true
+    },
+    parentSelectable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      popoverVisible: false,
+      filterText: '',
+      inputHovering: false,
+      triggerElRect: null,
+      itemHeight: 36,
+      indentSize: 24,
+      startIndex: 0,
+      endIndex: 0,
+      scrollTop: 0,
+      visibleItemCount: 0,
+      flattenedNodes: [],
+      nodeMap: new Map(),
+      checkedKeysSet: new Set(),
+      expandedKeysSet: new Set(),
+      updateTimer: null
+    };
+  },
+  computed: {
+    targetLeafNodes() {
+      if (this.filterText.trim()) {
+        return this.currentResultLeafNodes;
+      }
+      return this.allSelectableLeafNodes;
+    },
+
+    isCurrentSelectionAll() {
+      const nodes = this.targetLeafNodes;
+      if (nodes.length === 0) return false;
+      return nodes.every(node => this.checkedKeysSet.has(node.key));
+    },
+    popoverComputedWidth() {
+      if (this.popoverWidth === 'auto') {
+        return this.triggerElRect ? this.triggerElRect.width : 300;
+      }
+      return parseInt(this.popoverWidth, 10);
+    },
+    totalHeight() {
+      return this.visibleNodes.length * this.itemHeight;
+    },
+    offsetY() {
+      return this.startIndex * this.itemHeight;
+    },
+    visibleNodes() {
+      let nodes = this.flattenedNodes;
+      if (this.filterText.trim()) {
+        nodes = this.applyFilter(this.flattenedNodes);
+      }
+
+      const visible = [];
+      for (const node of nodes) {
+        if (this.isNodeCurrentlyVisible(node)) {
+          visible.push(node);
+        }
+      }
+      return visible;
+    },
+    visibleItems() {
+      const items = this.visibleNodes.slice(this.startIndex, this.endIndex);
+      return items.map(node => ({
+        ...node,
+        expanded: this.expandedKeysSet.has(node.key)
+      }));
+    },
+    // Actual count of selected leaf items
+    displaySelectedCount() {
+      let leafCount = 0;
+      this.checkedKeysSet.forEach(key => {
+        const node = this.nodeMap.get(key);
+        if (node && node.isLeaf) {
+          leafCount++;
+        }
+      });
+      return leafCount;
+    },
+    displaySelectedNodes() {
+      if (!this.multiple) return [];
+      const resultNodes = [];
+      Array.from(this.checkedKeysSet)
+        .map(key => this.nodeMap.get(key))
+        .filter(Boolean)
+        .forEach(node => {
+          // Always display tags for leaf nodes that are selected.
+          if (node.isLeaf) {
+            resultNodes.push(node);
+          }
+        });
+      return resultNodes.slice(0, this.maxDisplayTags);
+    },
+    singleSelectedLabel() {
+      if (this.multiple || this.value.length === 0) return '';
+      // 'value' prop should only contain leaf keys due to emitChange modification.
+      // For single select, value should have at most one key.
+      const selectedKey = String(this.value[0]);
+      const node = this.nodeMap.get(selectedKey);
+      // We expect 'value' to contain a leaf key.
+      return node ? node.label : '';
+    },
+    showClearIcon() {
+      return (
+        !this.disabled &&
+        this.inputHovering &&
+        this.displaySelectedCount > 0 && // Based on leaf count now
+        !this.multiple
+      );
+    },
+    // 计算当前搜索结果中的所有叶子节点
+    currentResultLeafNodes() {
+      return this.visibleNodes.filter(node => node.isLeaf && !this.checkboxDisabled(node));
+    },
+    // 判断当前搜索结果是否全部选中
+    isAllCurrentResultsSelected() {
+      const leafNodes = this.currentResultLeafNodes;
+      if (leafNodes.length === 0) return false;
+      return leafNodes.every(node => this.checkedKeysSet.has(node.key));
+    }, // 新增:计算所有可选叶子节点
+    allSelectableLeafNodes() {
+      return this.flattenedNodes.filter(node => node.isLeaf && !this.checkboxDisabled(node));
+    },
+
+    // 新增:判断是否所有数据都被选中
+    isAllDataSelected() {
+      const allLeafNodes = this.allSelectableLeafNodes;
+      if (allLeafNodes.length === 0) return false;
+      return allLeafNodes.every(node => this.checkedKeysSet.has(node.key));
+    }
+  },
+  watch: {
+    rawData: {
+      handler(newData) {
+        this.processRawData(newData);
+        this.syncCheckedKeysFromValue();
+      },
+      immediate: true
+    },
+    value: {
+      handler() {
+        this.syncCheckedKeysFromValue();
+      },
+      deep: true,
+    },
+    filterText: {
+      handler() {
+        this.resetScroll();
+      }
+    },
+    popoverVisible(isVisible) {
+      if (isVisible) {
+        this.$nextTick(() => {
+          this.calculateVisibleRange();
+        });
+      }
+    }
+  },
+  created() {
+    this.handleFilterDebounced = debounce(this.filterTextChange, 300);
+    this.visibleItemCount = Math.ceil(this.treeHeight / this.itemHeight) + 5;
+  },
+  methods: {
+    dateRangeChange(e){
+      this.$emit("dateRangeChange",e)
+    },
+    filterTextChange() {
+      this.resetScroll();
+    },
+    processRawData(data) {
+      if (!Array.isArray(data)) {
+        this.flattenedNodes = [];
+        this.nodeMap.clear();
+        return;
+      }
+
+      const flattened = [];
+      const nodeMap = new Map();
+      const expandedKeys = new Set();
+
+      const processNode = (node, level = 0, parentKey = null) => {
+        const children = node[this.treeProps.children] || [];
+        const hasChildren = children.length > 0;
+        const key = node[this.nodeKey];
+
+        if (key === undefined || key === null) {
+          console.warn('Node lacks a unique key:', node);
+          return;
+        }
+        const strKey = String(key);
+
+        let label = node[this.treeProps.label];
+        if (typeof label !== 'string' || label.trim() === '') {
+          label = node.name || node.title || node.text || `Node-${strKey}`;
+        }
+
+        const nodeDisabledByData = !!node[this.treeProps.disabled];
+
+        const processedNode = {
+          key: strKey,
+          label: String(label).trim(),
+          level,
+          isLeaf: !hasChildren,
+          nodeDisabled: nodeDisabledByData,
+          parentKey: parentKey ? String(parentKey) : null,
+          childrenKeys: hasChildren ? children.map(child => String(child[this.nodeKey])).filter(k => k !== undefined && k !== null) : [],
+          originalData: node,
+          hasChildren
+        };
+
+        flattened.push(processedNode);
+        nodeMap.set(processedNode.key, processedNode);
+
+        if (this.defaultExpandAll && hasChildren && !nodeDisabledByData) {
+          expandedKeys.add(processedNode.key);
+        }
+
+        if (hasChildren) {
+          children.forEach(child => processNode(child, level + 1, strKey));
+        }
+      };
+
+      data.forEach(node => processNode(node));
+
+      this.flattenedNodes = flattened;
+      this.nodeMap = nodeMap;
+      this.expandedKeysSet = new Set(expandedKeys);
+    },
+
+    isNodeCurrentlyVisible(node, currentExpandedSet = this.expandedKeysSet) {
+      if (node.level === 0) return true;
+      let current = node;
+      while (current.parentKey) {
+        const parent = this.nodeMap.get(current.parentKey);
+        if (!parent || !currentExpandedSet.has(parent.key)) {
+          return false;
+        }
+        current = parent;
+      }
+      return true;
+    },
+
+    applyFilter(nodesToFilter) {
+      const searchText = this.filterText.toLowerCase().trim();
+      if (!searchText) return nodesToFilter;
+
+      const matchedNodeKeys = new Set();
+      const newExpandedKeys = new Set(this.expandedKeysSet);
+
+      nodesToFilter.forEach(node => {
+        if (node.label.toLowerCase().includes(searchText)) {
+          matchedNodeKeys.add(node.key);
+          let parentKey = node.parentKey;
+          while (parentKey) {
+            const parentNode = this.nodeMap.get(parentKey);
+            if (parentNode) {
+              newExpandedKeys.add(parentKey);
+              parentKey = parentNode.parentKey;
+            } else {
+              break;
+            }
+          }
+        }
+      });
+
+      const filteredResult = nodesToFilter.filter(node => {
+        return this.hasMatchInSubtreeOrSelf(node, searchText);
+      });
+
+      this.expandedKeysSet = newExpandedKeys;
+      return filteredResult;
+    },
+    hasMatchInSubtreeOrSelf(node, searchText) {
+      if (node.label.toLowerCase().includes(searchText)) {
+        return true;
+      }
+      if (!node.isLeaf) {
+        for (const childKey of node.childrenKeys) {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode && this.hasMatchInSubtreeOrSelf(childNode, searchText)) {
+            return true;
+          }
+        }
+      }
+      return false;
+    },
+    checkboxDisabled(item) {
+      if (item.nodeDisabled) return true;
+      if (this.checkStrictly && !item.isLeaf && !this.parentSelectable) {
+        return true;
+      }
+      return false;
+    },
+
+    calculateVisibleRange() {
+      const containerHeight = this.treeHeight;
+      const totalItems = this.visibleNodes.length;
+
+      const newStartIndex = Math.floor(this.scrollTop / this.itemHeight);
+      const visibleCountInViewport = Math.ceil(containerHeight / this.itemHeight);
+
+      const buffer = 5;
+      this.startIndex = Math.max(0, newStartIndex - buffer);
+      this.endIndex = Math.min(totalItems, newStartIndex + visibleCountInViewport + buffer);
+    },
+    handleScroll(e) {
+      const newScrollTop = e.target.scrollTop;
+      if (newScrollTop !== this.scrollTop) {
+        this.scrollTop = newScrollTop;
+        if (!this.scrollRAF) {
+          this.scrollRAF = requestAnimationFrame(() => {
+            this.calculateVisibleRange();
+            this.scrollRAF = null;
+          });
+        }
+      }
+    },
+    resetScroll() {
+      this.scrollTop = 0;
+      if (this.$refs.virtualList) {
+        this.$refs.virtualList.scrollTop = 0;
+      }
+      this.$nextTick(() => {
+        this.calculateVisibleRange();
+      });
+    },
+    toggleNodeExpand(node) {
+      if (node.isLeaf || node.nodeDisabled) return;
+      const key = node.key;
+      if (this.expandedKeysSet.has(key)) {
+        this.expandedKeysSet.delete(key);
+      } else {
+        this.expandedKeysSet.add(key);
+      }
+      this.expandedKeysSet = new Set(this.expandedKeysSet);
+      this.$nextTick(() => this.calculateVisibleRange());
+    },
+    handleNodeClick(node) {
+      if (node.nodeDisabled) return;
+
+      if (!node.isLeaf && !this.parentSelectable) {
+        this.toggleNodeExpand(node);
+        return;
+      }
+      if (!node.isLeaf) {
+        this.toggleNodeExpand(node);
+        return;
+      }
+
+      if (!this.checkboxDisabled(node)) {
+        const isCurrentlyChecked = this.checkedKeysSet.has(node.key);
+        this.handleCheckboxChange(node, !isCurrentlyChecked);
+      }
+    },
+    handleCheckboxChange(node, checked) {
+      if (this.checkboxDisabled(node)) return;
+
+      const newCheckedKeys = new Set(this.checkedKeysSet);
+      const canNodeItselfBeInSet = node.isLeaf || this.parentSelectable || !this.checkStrictly;
+
+      if (checked) {
+        if (!this.multiple) {
+          newCheckedKeys.clear();
+        }
+        if (canNodeItselfBeInSet) {
+          newCheckedKeys.add(node.key);
+        }
+        if (!this.checkStrictly) {
+          this.selectAllChildren(node, newCheckedKeys);
+          this.checkParentSelection(node, newCheckedKeys);
+        }
+      } else {
+        if (canNodeItselfBeInSet) {
+          newCheckedKeys.delete(node.key);
+        }
+        if (!this.checkStrictly) {
+          this.unselectAllChildren(node, newCheckedKeys);
+          this.uncheckParentSelection(node, newCheckedKeys);
+        }
+      }
+      this.checkedKeysSet = newCheckedKeys;
+      this.emitChange();
+    },
+    selectAllChildren(node, checkedKeys) {
+      if (node.childrenKeys && node.childrenKeys.length > 0) {
+        node.childrenKeys.forEach(childKey => {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode && !childNode.nodeDisabled) {
+            const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
+            if(canChildBeInSet){
+              checkedKeys.add(childKey);
+            }
+            if (!childNode.isLeaf) {
+              this.selectAllChildren(childNode, checkedKeys);
+            }
+          }
+        });
+      }
+    },
+    unselectAllChildren(node, checkedKeys) {
+      if (node.childrenKeys && node.childrenKeys.length > 0) {
+        node.childrenKeys.forEach(childKey => {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode) {
+            const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
+            if(canChildBeInSet){
+              checkedKeys.delete(childKey);
+            }
+            if (!childNode.isLeaf) {
+              this.unselectAllChildren(childNode, checkedKeys);
+            }
+          }
+        });
+      }
+    },
+    checkParentSelection(node, checkedKeys) {
+      if (!node.parentKey || this.checkStrictly) return;
+
+      const parentNode = this.nodeMap.get(node.parentKey);
+      if (parentNode && !parentNode.nodeDisabled && (this.parentSelectable || !this.checkStrictly)) {
+        const allChildrenChecked = parentNode.childrenKeys.every(childKey => {
+          const child = this.nodeMap.get(childKey);
+          return !child || child.nodeDisabled || checkedKeys.has(childKey);
+        });
+
+        if (allChildrenChecked) {
+          checkedKeys.add(parentNode.key);
+          this.checkParentSelection(parentNode, checkedKeys);
+        }
+      }
+    },
+    uncheckParentSelection(node, checkedKeys) {
+      if (!node.parentKey || this.checkStrictly) return;
+      const parentNode = this.nodeMap.get(node.parentKey);
+      if (parentNode && (this.parentSelectable || !this.checkStrictly)) {
+        if (checkedKeys.has(parentNode.key)) {
+          checkedKeys.delete(parentNode.key);
+          this.uncheckParentSelection(parentNode, checkedKeys);
+        }
+      }
+    },
+    emitChange() {
+      let finalKeys = [];
+      const currentInternalCheckedKeys = Array.from(this.checkedKeysSet);
+
+      // ALWAYS return only leaf nodes in the emitted value.
+      currentInternalCheckedKeys.forEach(key => {
+        const node = this.nodeMap.get(key);
+        if (node && node.isLeaf) {
+          finalKeys.push(key);
+        }
+      });
+
+      finalKeys = [...new Set(finalKeys)]; // Deduplicate
+      const finalNodes = finalKeys.map(k => this.nodeMap.get(k)).filter(Boolean);
+
+      this.$emit('input', finalKeys);
+      this.$emit('change', finalKeys, finalNodes);
+    },
+    syncCheckedKeysFromValue() {
+      const newCheckedKeys = new Set();
+      if (Array.isArray(this.value)) {
+        this.value.forEach(key => {
+          const strKey = String(key);
+          const node = this.nodeMap.get(strKey);
+          if (node) {
+            newCheckedKeys.add(strKey);
+            if (!node.isLeaf && !this.checkStrictly) {
+              this.selectAllChildren(node, newCheckedKeys);
+              this.checkParentSelection(node, newCheckedKeys);
+            } else if (node.isLeaf && !this.checkStrictly) {
+              this.checkParentSelection(node, newCheckedKeys);
+            }
+          }
+        });
+      }
+      this.checkedKeysSet = newCheckedKeys;
+
+      if(this.popoverVisible) {
+        this.$nextTick(() => this.calculateVisibleRange());
+      }
+    },
+    handleConfirm() {
+      this.emitChange();
+      this.popoverVisible = false;
+    },
+    handleClearInPopover() {
+      this.checkedKeysSet.clear();
+      this.emitChange();
+    },
+    handleClearOnTrigger() {
+      if (this.disabled) return;
+      this.checkedKeysSet.clear();
+      this.emitChange();
+      this.popoverVisible = false;
+    },
+    // 新增:处理全选/取消全选功能
+    handleSelectAllToggle() {
+      if (this.disabled) return;
+
+      const leafNodes = this.targetLeafNodes;
+      if (leafNodes.length === 0) return;
+
+      // 根据当前状态决定是全选还是取消全选
+      const shouldSelect = !this.isCurrentSelectionAll;
+
+      if (!this.multiple && shouldSelect) {
+        // 单选模式下,只选择第一个叶子节点
+        const firstLeaf = leafNodes[0];
+        if (firstLeaf) {
+          this.handleCheckboxChange(firstLeaf, true);
+        }
+        return;
+      }
+
+      const newCheckedKeys = new Set(this.checkedKeysSet);
+
+      leafNodes.forEach(node => {
+        if (this.checkboxDisabled(node)) return;
+
+        if (!this.checkStrictly) {
+          // 如果不是严格模式,需要处理父子节点关系
+          this.handleCheckboxChange(node, shouldSelect);
+        } else {
+          // 严格模式下直接添加或删除
+          if (shouldSelect) {
+            newCheckedKeys.add(node.key);
+          } else {
+            newCheckedKeys.delete(node.key);
+          }
+        }
+      });
+
+      if (this.checkStrictly) {
+        this.checkedKeysSet = newCheckedKeys;
+        this.emitChange();
+      }
+    },
+
+    // 新增:处理下拉菜单选择事件
+    handleSelectAllCommand(command) {
+      this.handleSelectAllToggle(command);
+    },
+    removeTag(nodeToRemove) {
+      if (this.disabled || this.checkboxDisabled(nodeToRemove)) return;
+      // nodeToRemove here will be a leaf node because displaySelectedNodes only gives leaf nodes
+      this.handleCheckboxChange(nodeToRemove, false);
+    },
+    onPopoverShow() {
+      if (this.$refs.popover && this.$refs.popover.$refs.reference) {
+        this.triggerElRect = this.$refs.popover.$refs.reference.getBoundingClientRect();
+      }
+      this.$nextTick(() => {
+        this.resetScroll();
+        if (this.filterable && this.$refs.popover && this.$refs.popover.$el) {
+          const inputEl = this.$refs.popover.$el.querySelector('.filter-input input');
+          if (inputEl) inputEl.focus();
+        }
+      });
+    }
+  },
+  beforeDestroy() {
+    if (this.updateTimer) clearTimeout(this.updateTimer);
+    if (this.scrollRAF) cancelAnimationFrame(this.scrollRAF);
+  }
+};
+</script>
+
+<style scoped>
+.task-select-tree {
+  display: inline-block;
+  position: relative;
+  font-size: 14px;
+}
+
+.select-trigger-wrapper {
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  box-sizing: border-box;
+  color: #606266;
+  display: flex;
+  align-items: center;
+  min-height: 32px; /* Element UI default input height */
+  padding: 0 30px 0 10px;
+  position: relative;
+  transition: border-color .2s cubic-bezier(.645,.045,.355,1);
+  width: 100%;
+  cursor: pointer;
+  overflow: hidden; /* To contain tags */
+}
+
+.select-trigger-wrapper.is-disabled {
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+
+.select-trigger-wrapper:hover:not(.is-disabled) {
+  border-color: #c0c4cc;
+}
+
+.select-trigger-wrapper.is-active:not(.is-disabled) {
+  border-color: #409eff;
+}
+
+.tags-or-value-container {
+  flex-grow: 1;
+  display: flex;
+  flex-wrap: wrap; /* Allow tags to wrap */
+  gap: 4px; /* Space between tags */
+  align-items: center;
+  overflow: hidden; /* Hide overflowed tags/text */
+  padding: 2px 0; /* Minimal padding for tags */
+  min-height: 28px; /* Ensure container has some height */
+}
+
+
+.single-value-display,
+.placeholder-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 28px; /* Align with tags/input field */
+}
+
+.single-value-display {
+  color: #606266;
+}
+
+.placeholder-text {
+  color: #c0c4cc;
+}
+
+.icons-container {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  display: flex;
+  align-items: center;
+  color: #c0c4cc;
+  height: 100%;
+}
+
+.clear-icon {
+  cursor: pointer;
+  margin-right: 5px;
+  display: none; /* Hidden by default */
+  font-size: 14px;
+}
+
+.select-trigger-wrapper:hover .clear-icon {
+  display: inline-block; /* Show on hover */
+}
+.select-trigger-wrapper.is-disabled .clear-icon {
+  display: none !important; /* Never show if component is disabled */
+}
+
+
+.arrow-icon {
+  transition: transform .3s;
+  font-size: 14px;
+}
+
+.arrow-icon.is-reverse {
+  transform: rotate(180deg);
+}
+
+.tree-container {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background-color: #fff;
+  margin-top: 8px; /* If filter input is present */
+}
+
+.virtual-tree-list {
+  position: relative; /* For absolute positioning of virtual-content if needed */
+}
+
+.virtual-content {
+  width: 100%; /* Ensure full width */
+}
+
+.virtual-tree-node {
+  box-sizing: border-box;
+  cursor: pointer;
+  user-select: none;
+  border-bottom: 1px solid #f5f7fa; /* Lighter border */
+  transition: background-color 0.2s;
+  min-height: 36px; /* Ensure consistency with itemHeight */
+}
+.virtual-tree-node:last-child {
+  border-bottom: none;
+}
+
+.virtual-tree-node:hover:not(.is-disabled) {
+  background-color: #f5f7fa;
+}
+
+.virtual-tree-node.is-checked:not(.is-disabled) {
+  /* color: #409eff; */
+}
+.virtual-tree-node.is-checked .node-label {
+  /* font-weight: bold; */
+}
+
+
+.virtual-tree-node.is-disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+  background-color: #f5f7fa;
+}
+.virtual-tree-node.is-disabled .node-label {
+  color: #c0c4cc;
+}
+
+
+.node-expand-container {
+  width: 20px;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.node-expand-icon {
+  cursor: pointer;
+  color: #c0c4cc;
+  transition: transform 0.2s, color 0.2s;
+  font-size: 12px;
+}
+
+.node-expand-icon:hover {
+  color: #409eff;
+}
+
+.node-expand-placeholder {
+  width: 12px;
+  height: 12px;
+  display: block;
+}
+
+
+.node-checkbox {
+  margin: 0 8px 0 4px;
+  flex-shrink: 0;
+}
+
+.node-label {
+  flex: 1;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 1.4;
+  font-weight: normal;
+  padding-right: 5px;
+}
+
+.node-detail-text {
+  font-size: 12px;
+  color: #909399;
+  margin-left: 8px;
+  flex-shrink: 0;
+  white-space: nowrap;
+}
+
+.popover-content-wrapper {
+  max-width: 100%;
+}
+
+.popover-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
+  margin-top: 10px;
+}
+
+.selected-count {
+  font-size: 12px;
+  color: #909399;
+}
+
+.filter-input {
+  margin-bottom: 8px;
+}
+
+.trigger-tag {
+  margin: 1px 0;
+  max-width: 150px;
+}
+
+.trigger-tag >>> .el-tag__content {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>
+
+<style>
+/* Global styles for popper, if needed */
+.task-select-tree-popper.el-popover {
+  padding: 12px;
+}
+
+.task-select-tree-popper .el-checkbox__input.is-disabled .el-checkbox__inner {
+  background-color: #edf2fc;
+  border-color: #dcdfe6;
+  cursor: not-allowed;
+}
+
+.task-select-tree-popper .el-checkbox__input.is-checked .el-checkbox__inner {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+.task-select-tree-popper .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner {
+  background-color: #f2f6fc;
+  border-color: #dcdfe6;
+}
+</style>

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

@@ -0,0 +1,978 @@
+<template>
+  <div class="task-select-tree" :style="{ width: componentWidth }">
+    <el-popover
+      ref="popover"
+      placement="bottom-start"
+      :width="popoverComputedWidth"
+      trigger="click"
+      v-model="popoverVisible"
+      popper-class="task-select-tree-popper"
+      :disabled="disabled"
+      @show="onPopoverShow"
+    >
+      <div class="popover-content-wrapper">
+        <el-input
+          v-if="filterable"
+          placeholder="输入关键字过滤"
+          v-model="filterText"
+          size="mini"
+          clearable
+          class="filter-input"
+          @input="handleFilterDebounced"
+        >
+        </el-input>
+
+        <div class="tree-container" :style="{ height: treeHeight + 'px' }">
+          <div
+            ref="virtualList"
+            class="virtual-tree-list"
+            @scroll="handleScroll"
+            :style="{ height: treeHeight + 'px', overflow: 'auto' }"
+          >
+            <div
+              class="virtual-content"
+              :style="{
+                height: totalHeight + 'px',
+                position: 'relative',
+                paddingTop: offsetY + 'px'
+              }"
+            >
+              <div
+                v-for="(item, index) in visibleItems"
+                :key="`${item.key}-${item.level}-${index}`"
+                :style="{
+                  height: itemHeight + 'px',
+                  paddingLeft: (item.level * indentSize) + 'px',
+                  display: 'flex',
+                  alignItems: 'center',
+                  position: 'relative'
+                }"
+                :class="[
+                  'virtual-tree-node',
+                  {
+                    'is-leaf': item.isLeaf,
+                    'is-checked': checkedKeysSet.has(item.key) && (item.isLeaf || parentSelectable || !checkStrictly),
+                    'is-disabled': item.nodeDisabled || checkboxDisabled(item) // Combined disabled state for row styling
+                  }
+                ]"
+                @click="handleNodeClick(item)"
+              >
+                <div class="node-expand-container">
+                  <i
+                    v-if="!item.isLeaf"
+                    :class="[
+                      'node-expand-icon',
+                      item.expanded ? 'el-icon-caret-bottom' : 'el-icon-caret-right'
+                    ]"
+                    @click.stop="toggleNodeExpand(item)"
+                  ></i>
+                  <span v-else class="node-expand-placeholder"></span>
+                </div>
+
+                <el-checkbox
+                  :value="checkedKeysSet.has(item.key)"
+                  :disabled="checkboxDisabled(item)"
+                  @change="handleCheckboxChange(item, $event)"
+                  @click.stop
+                  class="node-checkbox"
+                ></el-checkbox>
+
+                <span class="node-label" :title="item.label">{{ item.label }}</span>
+
+                <span v-if="item.isLeaf && showNodeDetail" class="node-detail-text">
+                  (ID: {{ item.originalData.qwUserId || item.originalData.id }}{{ item.originalData.taskExecDate ? ' ' + item.originalData.taskExecDate : '' }})
+                </span>
+              </div>
+            </div>
+          </div>
+        </div>
+
+        <div class="popover-footer">
+          <span class="selected-count">已选择: {{ displaySelectedCount }}</span>
+          <el-button size="mini" @click="handleClearInPopover">清空</el-button>
+          <el-button type="primary" size="mini" @click="handleConfirm">确定</el-button>
+        </div>
+      </div>
+
+      <div
+        slot="reference"
+        :class="['select-trigger-wrapper', { 'is-disabled': disabled, 'is-active': popoverVisible }]"
+        @mouseenter="inputHovering = true"
+        @mouseleave="inputHovering = false"
+      >
+        <div class="tags-or-value-container">
+          <template v-if="multiple && displaySelectedNodes.length > 0">
+            <el-tag
+              v-for="node in displaySelectedNodes"
+              :key="node.key"
+              type="info"
+              size="small"
+              closable
+              :disable-transitions="true"
+              @close.stop="removeTag(node)"
+              class="trigger-tag"
+            >
+              {{ node.label }}
+            </el-tag>
+            <el-tag
+              v-if="displaySelectedCount > maxDisplayTags && displaySelectedNodes.length === maxDisplayTags"
+              type="info"
+              size="small"
+              class="trigger-tag"
+            >
+              + {{ displaySelectedCount - maxDisplayTags }}
+            </el-tag>
+          </template>
+          <span v-else-if="!multiple && displaySelectedCount > 0" class="single-value-display">
+            {{ singleSelectedLabel }}
+          </span>
+          <span v-else class="placeholder-text">{{ placeholder }}</span>
+        </div>
+
+        <span class="icons-container">
+          <i
+            v-if="showClearIcon"
+            class="el-icon-circle-close clear-icon"
+            @click.stop="handleClearOnTrigger"
+          ></i>
+          <i :class="['el-icon-arrow-up', 'arrow-icon', { 'is-reverse': !popoverVisible }]"></i>
+        </span>
+      </div>
+    </el-popover>
+  </div>
+</template>
+
+<script>
+// 防抖函数
+function debounce(func, wait) {
+  let timeout;
+  return function executedFunction(...args) {
+    const later = () => {
+      clearTimeout(timeout);
+      func(...args);
+    };
+    clearTimeout(timeout);
+    timeout = setTimeout(later, wait);
+  };
+}
+
+export default {
+  name: 'OptimizedSelectTree',
+  props: {
+    value: {
+      type: Array,
+      default: () => []
+    },
+    rawData: {
+      type: Array,
+      required: true
+    },
+    placeholder: {
+      type: String,
+      default: '请选择'
+    },
+    multiple: {
+      type: Boolean,
+      default: false
+    },
+    disabled: { // Global disabled for the component
+      type: Boolean,
+      default: false
+    },
+    checkStrictly: {
+      type: Boolean,
+      default: false // Important for parent-child linkage
+    },
+    filterable: {
+      type: Boolean,
+      default: true
+    },
+    maxDisplayTags: {
+      type: Number,
+      default: 2
+    },
+    componentWidth: {
+      type: String,
+      default: '100%'
+    },
+    popoverWidth: {
+      type: [String, Number],
+      default: 'auto'
+    },
+    treeProps: {
+      type: Object,
+      default: () => ({
+        children: 'children',
+        label: 'label',
+        isLeaf: 'isLeaf',
+        disabled: 'disabled' // Key for disabled state in rawData
+      })
+    },
+    nodeKey: {
+      type: String,
+      default: 'id'
+    },
+    defaultExpandAll: {
+      type: Boolean,
+      default: false
+    },
+    showNodeDetail: {
+      type: Boolean,
+      default: true
+    },
+    treeHeight: {
+      type: Number,
+      default: 280
+    },
+    returnLeafOnly: {
+      // This prop's effect on output is now overridden.
+      // It might still be used by consumers for other logic if they rely on its original meaning.
+      type: Boolean,
+      default: true
+    },
+    parentSelectable: {
+      type: Boolean,
+      default: false
+    }
+  },
+  data() {
+    return {
+      popoverVisible: false,
+      filterText: '',
+      inputHovering: false,
+      triggerElRect: null,
+      itemHeight: 36,
+      indentSize: 24,
+      startIndex: 0,
+      endIndex: 0,
+      scrollTop: 0,
+      visibleItemCount: 0,
+      flattenedNodes: [],
+      nodeMap: new Map(),
+      checkedKeysSet: new Set(),
+      expandedKeysSet: new Set(),
+      updateTimer: null
+    };
+  },
+  computed: {
+    popoverComputedWidth() {
+      if (this.popoverWidth === 'auto') {
+        return this.triggerElRect ? this.triggerElRect.width : 300;
+      }
+      return parseInt(this.popoverWidth, 10);
+    },
+    totalHeight() {
+      return this.visibleNodes.length * this.itemHeight;
+    },
+    offsetY() {
+      return this.startIndex * this.itemHeight;
+    },
+    visibleNodes() {
+      let nodes = this.flattenedNodes;
+      if (this.filterText.trim()) {
+        nodes = this.applyFilter(this.flattenedNodes);
+      }
+
+      const visible = [];
+      for (const node of nodes) {
+        if (this.isNodeCurrentlyVisible(node)) {
+          visible.push(node);
+        }
+      }
+      return visible;
+    },
+    visibleItems() {
+      const items = this.visibleNodes.slice(this.startIndex, this.endIndex);
+      return items.map(node => ({
+        ...node,
+        expanded: this.expandedKeysSet.has(node.key)
+      }));
+    },
+    // Actual count of selected leaf items
+    displaySelectedCount() {
+      let leafCount = 0;
+      this.checkedKeysSet.forEach(key => {
+        const node = this.nodeMap.get(key);
+        if (node && node.isLeaf) {
+          leafCount++;
+        }
+      });
+      return leafCount;
+    },
+    displaySelectedNodes() {
+      if (!this.multiple) return [];
+      const resultNodes = [];
+      Array.from(this.checkedKeysSet)
+        .map(key => this.nodeMap.get(key))
+        .filter(Boolean)
+        .forEach(node => {
+          // Always display tags for leaf nodes that are selected.
+          if (node.isLeaf) {
+            resultNodes.push(node);
+          }
+        });
+      return resultNodes.slice(0, this.maxDisplayTags);
+    },
+    singleSelectedLabel() {
+      if (this.multiple || this.value.length === 0) return '';
+      // 'value' prop should only contain leaf keys due to emitChange modification.
+      // For single select, value should have at most one key.
+      const selectedKey = String(this.value[0]);
+      const node = this.nodeMap.get(selectedKey);
+      // We expect 'value' to contain a leaf key.
+      return node ? node.label : '';
+    },
+    showClearIcon() {
+      return (
+        !this.disabled &&
+        this.inputHovering &&
+        this.displaySelectedCount > 0 && // Based on leaf count now
+        !this.multiple
+      );
+    }
+  },
+  watch: {
+    rawData: {
+      handler(newData) {
+        this.processRawData(newData);
+        this.syncCheckedKeysFromValue();
+      },
+      immediate: true
+    },
+    value: {
+      handler() {
+        this.syncCheckedKeysFromValue();
+      },
+      deep: true,
+    },
+    filterText: {
+      handler() {
+        this.resetScroll();
+      }
+    },
+    popoverVisible(isVisible) {
+      if (isVisible) {
+        this.$nextTick(() => {
+          this.calculateVisibleRange();
+        });
+      }
+    }
+  },
+  created() {
+    this.handleFilterDebounced = debounce(this.filterTextChange, 300);
+    this.visibleItemCount = Math.ceil(this.treeHeight / this.itemHeight) + 5;
+  },
+  methods: {
+    filterTextChange() {
+      this.resetScroll();
+    },
+    processRawData(data) {
+      if (!Array.isArray(data)) {
+        this.flattenedNodes = [];
+        this.nodeMap.clear();
+        return;
+      }
+
+      const flattened = [];
+      const nodeMap = new Map();
+      const expandedKeys = new Set();
+
+      const processNode = (node, level = 0, parentKey = null) => {
+        const children = node[this.treeProps.children] || [];
+        const hasChildren = children.length > 0;
+        const key = node[this.nodeKey];
+
+        if (key === undefined || key === null) {
+          console.warn('Node lacks a unique key:', node);
+          return;
+        }
+        const strKey = String(key);
+
+        let label = node[this.treeProps.label];
+        if (typeof label !== 'string' || label.trim() === '') {
+          label = node.name || node.title || node.text || `Node-${strKey}`;
+        }
+
+        const nodeDisabledByData = !!node[this.treeProps.disabled];
+
+        const processedNode = {
+          key: strKey,
+          label: String(label).trim(),
+          level,
+          isLeaf: !hasChildren,
+          nodeDisabled: nodeDisabledByData,
+          parentKey: parentKey ? String(parentKey) : null,
+          childrenKeys: hasChildren ? children.map(child => String(child[this.nodeKey])).filter(k => k !== undefined && k !== null) : [],
+          originalData: node,
+          hasChildren
+        };
+
+        flattened.push(processedNode);
+        nodeMap.set(processedNode.key, processedNode);
+
+        if (this.defaultExpandAll && hasChildren && !nodeDisabledByData) {
+          expandedKeys.add(processedNode.key);
+        }
+
+        if (hasChildren) {
+          children.forEach(child => processNode(child, level + 1, strKey));
+        }
+      };
+
+      data.forEach(node => processNode(node));
+
+      this.flattenedNodes = flattened;
+      this.nodeMap = nodeMap;
+      this.expandedKeysSet = new Set(expandedKeys);
+    },
+
+    isNodeCurrentlyVisible(node, currentExpandedSet = this.expandedKeysSet) {
+      if (node.level === 0) return true;
+      let current = node;
+      while (current.parentKey) {
+        const parent = this.nodeMap.get(current.parentKey);
+        if (!parent || !currentExpandedSet.has(parent.key)) {
+          return false;
+        }
+        current = parent;
+      }
+      return true;
+    },
+
+    applyFilter(nodesToFilter) {
+      const searchText = this.filterText.toLowerCase().trim();
+      if (!searchText) return nodesToFilter;
+
+      const matchedNodeKeys = new Set();
+      const newExpandedKeys = new Set(this.expandedKeysSet);
+
+      nodesToFilter.forEach(node => {
+        if (node.label.toLowerCase().includes(searchText)) {
+          matchedNodeKeys.add(node.key);
+          let parentKey = node.parentKey;
+          while (parentKey) {
+            const parentNode = this.nodeMap.get(parentKey);
+            if (parentNode) {
+              newExpandedKeys.add(parentKey);
+              parentKey = parentNode.parentKey;
+            } else {
+              break;
+            }
+          }
+        }
+      });
+
+      const filteredResult = nodesToFilter.filter(node => {
+        return this.hasMatchInSubtreeOrSelf(node, searchText);
+      });
+
+      this.expandedKeysSet = newExpandedKeys;
+      return filteredResult;
+    },
+    hasMatchInSubtreeOrSelf(node, searchText) {
+      if (node.label.toLowerCase().includes(searchText)) {
+        return true;
+      }
+      if (!node.isLeaf) {
+        for (const childKey of node.childrenKeys) {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode && this.hasMatchInSubtreeOrSelf(childNode, searchText)) {
+            return true;
+          }
+        }
+      }
+      return false;
+    },
+    checkboxDisabled(item) {
+      if (item.nodeDisabled) return true;
+      if (this.checkStrictly && !item.isLeaf && !this.parentSelectable) {
+        return true;
+      }
+      return false;
+    },
+
+    calculateVisibleRange() {
+      const containerHeight = this.treeHeight;
+      const totalItems = this.visibleNodes.length;
+
+      const newStartIndex = Math.floor(this.scrollTop / this.itemHeight);
+      const visibleCountInViewport = Math.ceil(containerHeight / this.itemHeight);
+
+      const buffer = 5;
+      this.startIndex = Math.max(0, newStartIndex - buffer);
+      this.endIndex = Math.min(totalItems, newStartIndex + visibleCountInViewport + buffer);
+    },
+    handleScroll(e) {
+      const newScrollTop = e.target.scrollTop;
+      if (newScrollTop !== this.scrollTop) {
+        this.scrollTop = newScrollTop;
+        if (!this.scrollRAF) {
+          this.scrollRAF = requestAnimationFrame(() => {
+            this.calculateVisibleRange();
+            this.scrollRAF = null;
+          });
+        }
+      }
+    },
+    resetScroll() {
+      this.scrollTop = 0;
+      if (this.$refs.virtualList) {
+        this.$refs.virtualList.scrollTop = 0;
+      }
+      this.$nextTick(() => {
+        this.calculateVisibleRange();
+      });
+    },
+    toggleNodeExpand(node) {
+      if (node.isLeaf || node.nodeDisabled) return;
+      const key = node.key;
+      if (this.expandedKeysSet.has(key)) {
+        this.expandedKeysSet.delete(key);
+      } else {
+        this.expandedKeysSet.add(key);
+      }
+      this.expandedKeysSet = new Set(this.expandedKeysSet);
+      this.$nextTick(() => this.calculateVisibleRange());
+    },
+    handleNodeClick(node) {
+      if (node.nodeDisabled) return;
+
+      if (!node.isLeaf && !this.parentSelectable) {
+        this.toggleNodeExpand(node);
+        return;
+      }
+      if (!node.isLeaf) {
+        this.toggleNodeExpand(node);
+        return;
+      }
+
+      if (!this.checkboxDisabled(node)) {
+        const isCurrentlyChecked = this.checkedKeysSet.has(node.key);
+        this.handleCheckboxChange(node, !isCurrentlyChecked);
+      }
+    },
+    handleCheckboxChange(node, checked) {
+      if (this.checkboxDisabled(node)) return;
+
+      const newCheckedKeys = new Set(this.checkedKeysSet);
+      const canNodeItselfBeInSet = node.isLeaf || this.parentSelectable || !this.checkStrictly;
+
+      if (checked) {
+        if (!this.multiple) {
+          newCheckedKeys.clear();
+        }
+        if (canNodeItselfBeInSet) {
+          newCheckedKeys.add(node.key);
+        }
+        if (!this.checkStrictly) {
+          this.selectAllChildren(node, newCheckedKeys);
+          this.checkParentSelection(node, newCheckedKeys);
+        }
+      } else {
+        if (canNodeItselfBeInSet) {
+          newCheckedKeys.delete(node.key);
+        }
+        if (!this.checkStrictly) {
+          this.unselectAllChildren(node, newCheckedKeys);
+          this.uncheckParentSelection(node, newCheckedKeys);
+        }
+      }
+      this.checkedKeysSet = newCheckedKeys;
+      this.emitChange();
+    },
+    selectAllChildren(node, checkedKeys) {
+      if (node.childrenKeys && node.childrenKeys.length > 0) {
+        node.childrenKeys.forEach(childKey => {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode && !childNode.nodeDisabled) {
+            const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
+            if(canChildBeInSet){
+              checkedKeys.add(childKey);
+            }
+            if (!childNode.isLeaf) {
+              this.selectAllChildren(childNode, checkedKeys);
+            }
+          }
+        });
+      }
+    },
+    unselectAllChildren(node, checkedKeys) {
+      if (node.childrenKeys && node.childrenKeys.length > 0) {
+        node.childrenKeys.forEach(childKey => {
+          const childNode = this.nodeMap.get(childKey);
+          if (childNode) {
+            const canChildBeInSet = childNode.isLeaf || this.parentSelectable || !this.checkStrictly;
+            if(canChildBeInSet){
+              checkedKeys.delete(childKey);
+            }
+            if (!childNode.isLeaf) {
+              this.unselectAllChildren(childNode, checkedKeys);
+            }
+          }
+        });
+      }
+    },
+    checkParentSelection(node, checkedKeys) {
+      if (!node.parentKey || this.checkStrictly) return;
+
+      const parentNode = this.nodeMap.get(node.parentKey);
+      if (parentNode && !parentNode.nodeDisabled && (this.parentSelectable || !this.checkStrictly)) {
+        const allChildrenChecked = parentNode.childrenKeys.every(childKey => {
+          const child = this.nodeMap.get(childKey);
+          return !child || child.nodeDisabled || checkedKeys.has(childKey);
+        });
+
+        if (allChildrenChecked) {
+          checkedKeys.add(parentNode.key);
+          this.checkParentSelection(parentNode, checkedKeys);
+        }
+      }
+    },
+    uncheckParentSelection(node, checkedKeys) {
+      if (!node.parentKey || this.checkStrictly) return;
+      const parentNode = this.nodeMap.get(node.parentKey);
+      if (parentNode && (this.parentSelectable || !this.checkStrictly)) {
+        if (checkedKeys.has(parentNode.key)) {
+          checkedKeys.delete(parentNode.key);
+          this.uncheckParentSelection(parentNode, checkedKeys);
+        }
+      }
+    },
+    emitChange() {
+      let finalKeys = [];
+      const currentInternalCheckedKeys = Array.from(this.checkedKeysSet);
+
+      // ALWAYS return only leaf nodes in the emitted value.
+      currentInternalCheckedKeys.forEach(key => {
+        const node = this.nodeMap.get(key);
+        if (node && node.isLeaf) {
+          finalKeys.push(key);
+        }
+      });
+
+      finalKeys = [...new Set(finalKeys)]; // Deduplicate
+      const finalNodes = finalKeys.map(k => this.nodeMap.get(k)).filter(Boolean);
+
+      this.$emit('input', finalKeys);
+      this.$emit('change', finalKeys, finalNodes);
+    },
+    syncCheckedKeysFromValue() {
+      const newCheckedKeys = new Set();
+      if (Array.isArray(this.value)) {
+        this.value.forEach(key => {
+          const strKey = String(key);
+          const node = this.nodeMap.get(strKey);
+          if (node) {
+            // Value should ideally contain leaf keys if controlled by this component.
+            // If an external value has a parent key, handle it for internal consistency.
+            newCheckedKeys.add(strKey);
+            if (!node.isLeaf && !this.checkStrictly) {
+              this.selectAllChildren(node, newCheckedKeys);
+              this.checkParentSelection(node, newCheckedKeys);
+            } else if (node.isLeaf && !this.checkStrictly) {
+              // If a leaf is added from value, ensure its parent's state is updated
+              this.checkParentSelection(node, newCheckedKeys);
+            }
+          }
+        });
+      }
+      this.checkedKeysSet = newCheckedKeys;
+
+      if(this.popoverVisible) {
+        this.$nextTick(() => this.calculateVisibleRange());
+      }
+    },
+    handleConfirm() {
+      this.emitChange(); // emitChange now handles filtering for leaf nodes
+      this.popoverVisible = false;
+    },
+    handleClearInPopover() {
+      this.checkedKeysSet.clear();
+      this.emitChange();
+    },
+    handleClearOnTrigger() {
+      if (this.disabled) return;
+      this.checkedKeysSet.clear();
+      this.emitChange();
+      this.popoverVisible = false;
+    },
+    removeTag(nodeToRemove) {
+      if (this.disabled || this.checkboxDisabled(nodeToRemove)) return;
+      // nodeToRemove here will be a leaf node because displaySelectedNodes only gives leaf nodes
+      this.handleCheckboxChange(nodeToRemove, false);
+    },
+    onPopoverShow() {
+      if (this.$refs.popover && this.$refs.popover.$refs.reference) {
+        this.triggerElRect = this.$refs.popover.$refs.reference.getBoundingClientRect();
+      }
+      this.$nextTick(() => {
+        this.resetScroll();
+        if (this.filterable && this.$refs.popover && this.$refs.popover.$el) {
+          const inputEl = this.$refs.popover.$el.querySelector('.filter-input input');
+          if (inputEl) inputEl.focus();
+        }
+      });
+    }
+  },
+  beforeDestroy() {
+    if (this.updateTimer) clearTimeout(this.updateTimer);
+    if (this.scrollRAF) cancelAnimationFrame(this.scrollRAF);
+  }
+};
+</script>
+
+<style scoped>
+.task-select-tree {
+  display: inline-block;
+  position: relative;
+  font-size: 14px;
+}
+
+.select-trigger-wrapper {
+  background-color: #fff;
+  border-radius: 4px;
+  border: 1px solid #dcdfe6;
+  box-sizing: border-box;
+  color: #606266;
+  display: flex;
+  align-items: center;
+  min-height: 32px; /* Element UI default input height */
+  padding: 0 30px 0 10px;
+  position: relative;
+  transition: border-color .2s cubic-bezier(.645,.045,.355,1);
+  width: 100%;
+  cursor: pointer;
+  overflow: hidden; /* To contain tags */
+}
+
+.select-trigger-wrapper.is-disabled {
+  background-color: #f5f7fa;
+  border-color: #e4e7ed;
+  color: #c0c4cc;
+  cursor: not-allowed;
+}
+
+.select-trigger-wrapper:hover:not(.is-disabled) {
+  border-color: #c0c4cc;
+}
+
+.select-trigger-wrapper.is-active:not(.is-disabled) {
+  border-color: #409eff;
+}
+
+.tags-or-value-container {
+  flex-grow: 1;
+  display: flex;
+  flex-wrap: wrap; /* Allow tags to wrap */
+  gap: 4px; /* Space between tags */
+  align-items: center;
+  overflow: hidden; /* Hide overflowed tags/text */
+  padding: 2px 0; /* Minimal padding for tags */
+  min-height: 28px; /* Ensure container has some height */
+}
+
+
+.single-value-display,
+.placeholder-text {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 28px; /* Align with tags/input field */
+}
+
+.single-value-display {
+  color: #606266;
+}
+
+.placeholder-text {
+  color: #c0c4cc;
+}
+
+.icons-container {
+  position: absolute;
+  right: 8px;
+  top: 50%;
+  transform: translateY(-50%);
+  display: flex;
+  align-items: center;
+  color: #c0c4cc;
+  height: 100%;
+}
+
+.clear-icon {
+  cursor: pointer;
+  margin-right: 5px;
+  display: none; /* Hidden by default */
+  font-size: 14px;
+}
+
+.select-trigger-wrapper:hover .clear-icon {
+  display: inline-block; /* Show on hover */
+}
+.select-trigger-wrapper.is-disabled .clear-icon {
+  display: none !important; /* Never show if component is disabled */
+}
+
+
+.arrow-icon {
+  transition: transform .3s;
+  font-size: 14px;
+}
+
+.arrow-icon.is-reverse {
+  transform: rotate(180deg);
+}
+
+.tree-container {
+  border: 1px solid #ebeef5;
+  border-radius: 4px;
+  background-color: #fff;
+  margin-top: 8px; /* If filter input is present */
+}
+
+.virtual-tree-list {
+  position: relative; /* For absolute positioning of virtual-content if needed */
+}
+
+.virtual-content {
+  width: 100%; /* Ensure full width */
+}
+
+.virtual-tree-node {
+  box-sizing: border-box;
+  cursor: pointer;
+  user-select: none;
+  border-bottom: 1px solid #f5f7fa; /* Lighter border */
+  transition: background-color 0.2s;
+  min-height: 36px; /* Ensure consistency with itemHeight */
+}
+.virtual-tree-node:last-child {
+  border-bottom: none;
+}
+
+.virtual-tree-node:hover:not(.is-disabled) {
+  background-color: #f5f7fa;
+}
+
+.virtual-tree-node.is-checked:not(.is-disabled) {
+  /* color: #409eff; */
+}
+.virtual-tree-node.is-checked .node-label {
+  /* font-weight: bold; */
+}
+
+
+.virtual-tree-node.is-disabled {
+  color: #c0c4cc;
+  cursor: not-allowed;
+  background-color: #f5f7fa;
+}
+.virtual-tree-node.is-disabled .node-label {
+  color: #c0c4cc;
+}
+
+
+.node-expand-container {
+  width: 20px;
+  height: 100%;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  flex-shrink: 0;
+}
+
+.node-expand-icon {
+  cursor: pointer;
+  color: #c0c4cc;
+  transition: transform 0.2s, color 0.2s;
+  font-size: 12px;
+}
+
+.node-expand-icon:hover {
+  color: #409eff;
+}
+
+.node-expand-placeholder {
+  width: 12px;
+  height: 12px;
+  display: block;
+}
+
+
+.node-checkbox {
+  margin: 0 8px 0 4px;
+  flex-shrink: 0;
+}
+
+.node-label {
+  flex: 1;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  line-height: 1.4;
+  font-weight: normal;
+  padding-right: 5px;
+}
+
+.node-detail-text {
+  font-size: 12px;
+  color: #909399;
+  margin-left: 8px;
+  flex-shrink: 0;
+  white-space: nowrap;
+}
+
+.popover-content-wrapper {
+  max-width: 100%;
+}
+
+.popover-footer {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  padding-top: 10px;
+  border-top: 1px solid #ebeef5;
+  margin-top: 10px;
+}
+
+.selected-count {
+  font-size: 12px;
+  color: #909399;
+}
+
+.filter-input {
+  margin-bottom: 8px;
+}
+
+.trigger-tag {
+  margin: 1px 0;
+  max-width: 150px;
+}
+
+.trigger-tag >>> .el-tag__content {
+  overflow: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+}
+</style>
+
+<style>
+/* Global styles for popper, if needed */
+.task-select-tree-popper.el-popover {
+  padding: 12px;
+}
+
+.task-select-tree-popper .el-checkbox__input.is-disabled .el-checkbox__inner {
+  background-color: #edf2fc;
+  border-color: #dcdfe6;
+  cursor: not-allowed;
+}
+
+.task-select-tree-popper .el-checkbox__input.is-checked .el-checkbox__inner {
+  background-color: #409eff;
+  border-color: #409eff;
+}
+.task-select-tree-popper .el-checkbox__input.is-disabled.is-checked .el-checkbox__inner {
+  background-color: #f2f6fc;
+  border-color: #dcdfe6;
+}
+</style>

+ 25 - 5
src/views/course/courseRedPacketLog/index.vue

@@ -100,12 +100,23 @@
           v-hasPermi="['course:courseRedPacketLog:export']"
         >导出</el-button>
       </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleRetry"
+        >重新发送</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
     <el-tabs type="card" v-model="activeName" @tab-click="handleClick">
       <el-tab-pane label="全部订单" name="00"></el-tab-pane>
       <el-tab-pane label="待发送" name="0"></el-tab-pane>
       <el-tab-pane label="已发送" name="1"></el-tab-pane>
+      <el-tab-pane label="待补发" name="2"></el-tab-pane>
     </el-tabs>
     <el-table border v-loading="loading" :data="courseRedPacketLogList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />
@@ -142,7 +153,16 @@
       </el-table-column>
       <el-table-column label="企微账号" align="center" prop="qwUserName" />
       <el-table-column label="创建时间" align="center" prop="createTime" />
-
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+          <template slot-scope="scope">
+            <el-button v-if=""
+              size="mini"
+              type="text"
+              @click="handleRetry(scope.row)"
+            >重新发送</el-button>
+          </template>
+        </el-table-column>
+      </el-table>
     </el-table>
 
     <pagination
@@ -187,7 +207,7 @@
 </template>
 
 <script>
-import { courseList,videoList,listCourseRedPacketLog, myListCourseRedPacketLogNew, getCourseRedPacketLog, delCourseRedPacketLog, addCourseRedPacketLog, updateCourseRedPacketLog, exportCourseRedPacketLog } from "@/api/course/courseRedPacketLog";
+import { courseList,videoList,listCourseRedPacketLog,retryCourseRedPacketLog, myListCourseRedPacketLogNew, getCourseRedPacketLog, delCourseRedPacketLog, addCourseRedPacketLog, updateCourseRedPacketLog, exportCourseRedPacketLog } from "@/api/course/courseRedPacketLog";
 import { getCompanyList } from "@/api/company/company";
 
 export default {
@@ -369,14 +389,14 @@ export default {
       });
     },
     /** 删除按钮操作 */
-    handleDelete(row) {
+    handleRetry(row) {
       const logIds = row.logId || this.ids;
-      this.$confirm('是否确认删除短链课程看课记录编号为"' + logIds + '"的数据项?', "警告", {
+      this.$confirm('是否确认重新发送红包?', "警告", {
           confirmButtonText: "确定",
           cancelButtonText: "取消",
           type: "warning"
         }).then(function() {
-          return delCourseRedPacketLog(logIds);
+          return retryCourseRedPacketLog(logIds);
         }).then(() => {
           this.getList();
           this.msgSuccess("删除成功");

+ 1 - 0
src/views/course/courseRedPacketLog/myCourseRedPacketLog.vue

@@ -98,6 +98,7 @@
       <el-tab-pane label="全部订单" name="00"></el-tab-pane>
       <el-tab-pane label="待发送" name="0"></el-tab-pane>
       <el-tab-pane label="已发送" name="1"></el-tab-pane>
+      <el-tab-pane label="待补发" name="2"></el-tab-pane>
     </el-tabs>
     <el-table border v-loading="loading" :data="courseRedPacketLogList" @selection-change="handleSelectionChange">
       <el-table-column type="selection" width="55" align="center" />

+ 506 - 0
src/views/course/courseWatchLog/deptWatchLog.vue

@@ -0,0 +1,506 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="会员ID" prop="userId">
+        <el-input
+          v-model="queryParams.userId"
+          placeholder="请输入会员ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="会员昵称" prop="nickName">
+        <el-input
+          v-model="queryParams.nickName"
+          placeholder="请输入会员昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="企微客户昵称" prop="nickName" >
+        <el-input
+          v-model="queryParams.externalUserName"
+          placeholder="请输入企微客户昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="所属销售" prop="companyUserId">
+        <el-select v-model="queryParams.companyUserId" clearable filterable remote
+                   placeholder="请输入关键词" :remote-method="loadCompanyUserOptions"
+                   v-select-load-more="loadMoreCompanyUserOptions"
+                   :loading="companyUserOptionsLoading">
+          <el-option
+            v-for="item in companyUserOptions"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="课程" prop="courseId">
+        <el-select filterable  v-model="queryParams.courseId" placeholder="请选择课程"  clearable size="small" @change="courseChange(queryParams.courseId)">
+          <el-option
+            v-for="dict in courseLists"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="小节" prop="videoId">
+        <el-select filterable  v-model="queryParams.videoId" placeholder="请选择小节"  clearable size="small">
+          <el-option
+            v-for="dict in videoList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="企微ID" prop="qwUserUserId">
+        <el-input
+          v-model="queryParams.qwUserUserId"
+          placeholder="请输入所属企微ID"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="企微员工名称" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入所属企微员工名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+<!--      <el-form-item label="部门名称" prop="deptName">-->
+<!--        <el-input-->
+<!--          v-model="queryParams.deptName"-->
+<!--          placeholder="请输入部门名称"-->
+<!--          clearable-->
+<!--          size="small"-->
+<!--          @keyup.enter.native="handleQuery"-->
+<!--        />-->
+<!--      </el-form-item>-->
+      <el-form-item label="营期时间" prop="scheduleTime">
+        <el-date-picker
+          v-model="scheduleTime"
+          type="daterange"
+          size="small"
+          style="width: 240px"
+          value-format="yyyy-MM-dd"
+          range-separator="-"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          @change="handleScheduleTimeChange">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="createChange"></el-date-picker>
+      </el-form-item>
+      <el-form-item label="最新更新时间" prop="updateTime">
+        <el-date-picker v-model="updateTime" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange"
+                        range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="updateChange"></el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+<!--    <el-row :gutter="10" class="mb8">-->
+<!--      <el-col :span="1.5">-->
+<!--        <el-button-->
+<!--          type="warning"-->
+<!--          plain-->
+<!--          icon="el-icon-download"-->
+<!--          size="mini"-->
+<!--          :loading="exportLoading"-->
+<!--          @click="handleExport"-->
+<!--          v-hasPermi="['course:courseWatchLog:export']"-->
+<!--        >导出</el-button>-->
+<!--      </el-col>-->
+<!--      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>-->
+<!--    </el-row>-->
+
+    <el-tabs type="card" v-model="activeName" @tab-click="handleClickX">
+      <el-tab-pane label="全部" name="00"></el-tab-pane>
+      <el-tab-pane v-for="(item,index) in logTypeOptions" :label="item.dictLabel" :name="item.dictValue"></el-tab-pane>
+    </el-tabs>
+    <el-table border v-loading="loading" :data="courseWatchLogList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="记录编号" align="center" prop="logId" />
+      <el-table-column label="企微客户" align="center" prop="externalUserName"/>
+      <el-table-column label="会员ID" align="center" prop="userId" />
+      <el-table-column label="会员昵称" align="center" prop="fsNickName">
+        <template slot-scope="scope">
+          <div style="display: flex;white-space: nowrap">
+            <div style="margin: auto">
+              {{scope.row.fsNickName}}
+            </div>
+            <el-popover
+              placement="right"
+              title=""
+              trigger="hover">
+              <img slot="reference" :src="scope.row.fsAvatar" style="width: 30px;height: 30px">
+              <img :src="scope.row.fsAvatar" style="max-width: 200px;max-height: 200px">
+            </el-popover>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="课程名称" align="center" prop="courseName" />
+      <el-table-column label="小节名称" align="center" prop="videoName" />
+      <el-table-column label="记录类型" align="center" prop="logType">
+        <template slot-scope="scope">
+          <dict-tag :options="logTypeOptions" :value="scope.row.logType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="播放时长" align="center" prop="duration" />
+      <el-table-column label="所属销售" align="center" prop="companyUserName" />
+<!--      <el-table-column label="所属公司" align="center" prop="companyName" />-->
+      <el-table-column label="企微员工名称" align="center" prop="qwUserName" />
+<!--      <el-table-column label="所属发送方式" align="center" prop="sendType" />-->
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="更新时间" align="center" prop="updateTime" />
+      <el-table-column label="完课时间" align="center" prop="finishTime" />
+      <el-table-column label="营期时间" align="center" prop="campPeriodTime" />
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+  </div>
+</template>
+
+<script>
+import { deptListCourseWatchLog, getCourseWatchLog, delCourseWatchLog, addCourseWatchLog, updateCourseWatchLog, exportCourseWatchLog } from "@/api/course/courseWatchLog";
+import { courseList,videoList } from '../../../api/course/courseRedPacketLog'
+import { getCompanyUserListLikeName } from "@/api/company/companyUser";
+import {getTask} from "@/api/common";
+export default {
+  name: "CourseWatchLog",
+  data() {
+    return {
+      activeName:"00",
+      createTime:null,
+      updateTime:null,
+      courseLists:[],
+      videoList:[],
+      logTypeOptions:[],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 短链课程看课记录表格数据
+      courseWatchLogList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        nickName: null,
+        videoId: null,
+        logType: null,
+        qwExternalContactId: null,
+        externalUserName:null,
+        duration: null,
+        qwUserId: null,
+        qwUserName: null, //企微名称
+        qwUserUserId: null, //企微id
+        deptName: null, //部门名称
+        companyUserId: null,
+        companyId: null,
+        courseId: null,
+        sTime:null,
+        eTime:null,
+        upSTime:null,
+        upETime:null,
+        scheduleStartTime: null,
+        scheduleEndTime: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      },
+      scheduleTime: null,
+      // 员工选项列表
+      companyUserOptionsParams: {
+        name: undefined,
+        hasNextPage: false,
+        pageNum: 1,
+        pageSize: 10
+      },
+      companyUserOptionsLoading: false,
+      companyUserOptions: [],
+    };
+  },
+  created() {
+    courseList().then(response => {
+      this.courseLists = response.list;
+    });
+    this.getList();
+    this.getDicts("sys_course_watch_log_type").then(response => {
+      this.logTypeOptions = response.data;
+    });
+  },
+  methods: {
+    courseChange(row){
+      this.queryParams.videoId=null;
+      if(row === ''){
+        this.videoList=[];
+        return
+      }
+      videoList(row).then(response => {
+        this.videoList=response.list
+      });
+    },
+    createChange() {
+      if (this.createTime != null) {
+        this.queryParams.sTime = this.createTime[0];
+        this.queryParams.eTime = this.createTime[1];
+      } else {
+        this.queryParams.sTime = null;
+        this.queryParams.eTime = null;
+      }
+    },
+
+    updateChange(){
+      if (this.updateTime != null) {
+        this.queryParams.upSTime = this.updateTime[0];
+        this.queryParams.upETime = this.updateTime[1];
+      } else {
+        this.queryParams.upSTime = null;
+        this.queryParams.upETime = null;
+      }
+    },
+    handleClickX(tab,event){
+      this.activeName=tab.name;
+      if(tab.name=="00"){
+        this.queryParams.logType=null;
+      }else{
+        this.queryParams.logType=tab.name;
+      }
+      this.getList()
+    },
+    /** 查询短链课程看课记录列表 */
+    getList() {
+      this.loading = true;
+      if(this.queryParams.logType == "10"){
+        this.queryParams.logType = null;
+      }
+      deptListCourseWatchLog(this.queryParams).then(response => {
+        this.courseWatchLogList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        logId: null,
+        userId: null,
+        videoId: null,
+        logType: null,
+        createTime: null,
+        updateTime: null,
+        qwExternalContactId: null,
+        externalUserName:null,
+        duration: null,
+        qwUserId: null,
+        companyUserId: null,
+        companyId: null,
+        courseId: null,
+        scheduleStartTime: null,
+        scheduleEndTime: null,
+      };
+      this.scheduleTime=null;
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.createTime = null;
+      this.scheduleTime = null;
+      this.queryParams.sTime = null;
+      this.queryParams.eTime = null;
+      this.queryParams.upSTime = null;
+      this.queryParams.upETime = null;
+      this.queryParams.scheduleStartTime = null;
+      this.queryParams.scheduleEndTime = null;
+      this.scheduleTime=null;
+      this.updateTime=null;
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.logId)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加短链课程看课记录";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const logId = row.logId || this.ids
+      getCourseWatchLog(logId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改短链课程看课记录";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.logId != null) {
+            updateCourseWatchLog(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addCourseWatchLog(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const logIds = row.logId || this.ids;
+      this.$confirm('是否确认删除短链课程看课记录编号为"' + logIds + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delCourseWatchLog(logIds);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const that = this
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有短链课程看课记录数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportCourseWatchLog(queryParams);
+        }).then(response => {
+          if (response.code === 200){
+            that.msgSuccess(response.msg);
+            that.taskId = response.data;
+            that.time = setInterval(function(){
+              //查订单
+              getTask(that.taskId).then(res => {
+                if(res.data.status === 1){
+                  that.exportLoading = false;
+                  clearTimeout(that.time)
+                  that.time = null;
+                  that.download(res.data.fileUrl);
+                }
+              });
+            },10000);
+          } else {
+            that.msgError(response.msg)
+            that.exportLoading = false
+          }
+        }).catch(() => {});
+    },
+    handleScheduleTimeChange(val) {
+      if (val) {
+        this.queryParams.scheduleStartTime = val[0];
+        this.queryParams.scheduleEndTime = val[1];
+      } else {
+        this.queryParams.scheduleStartTime = null;
+        this.queryParams.scheduleEndTime = null;
+      }
+    },
+    /**
+     * 根据名称模糊查询用户列表
+     * @param query 参数
+     */
+    loadCompanyUserOptions(query) {
+      this.companyUserOptions = [];
+      if (query === '') {
+        return;
+      }
+
+      this.companyUserOptionsParams.pageNum = 1
+      this.companyUserOptionsParams.name = query
+      this.companyUserOptionsLoading = true;
+      this.getCompanyUserListLikeName()
+    },
+    /**
+     * 获取员工列表
+     */
+    getCompanyUserListLikeName() {
+      getCompanyUserListLikeName(this.companyUserOptionsParams).then(response => {
+        this.companyUserOptions = [...this.companyUserOptions, ...response.data.list]
+        this.companyUserOptionsParams.hasNextPage = response.data.hasNextPage
+        this.companyUserOptionsLoading = false;
+      });
+    },
+    /**
+     * 加载更多员工选项
+     */
+    loadMoreCompanyUserOptions() {
+      if (!this.companyUserOptionsParams.hasNextPage) {
+        return;
+      }
+
+      this.companyUserOptionsParams.pageNum += 1
+      this.getCompanyUserListLikeName()
+    },
+  }
+};
+</script>

+ 1 - 1
src/views/course/courseWatchLog/index.vue

@@ -209,7 +209,7 @@ export default {
         upETime:null,
         scheduleStartTime: null,
         scheduleEndTime: null,
-        sendType:'1',
+        sendType:null,
       },
       // 表单参数
       form: {},

+ 8 - 2
src/views/course/courseWatchLog/watchLog.vue

@@ -267,7 +267,13 @@
         <el-table-column label="转帐金额" align="center" prop="amount" />
         <el-table-column label="状态" align="center" prop="status" >
           <template slot-scope="scope">
-            <el-tag>{{ scope.row.status === 0 ? "发送中" : "已完成" }}</el-tag>
+            <el-tag>
+              {{
+                scope.row.status === 0 ? "发送中" :
+                scope.row.status === 2 ? "待补发" :
+                "已完成"
+              }}
+            </el-tag>
           </template>
         </el-table-column>
         <el-table-column label="所属企微" align="center" prop="qwUserName" />
@@ -382,7 +388,7 @@ export default {
         upETime:null,
         scheduleStartTime: null,
         scheduleEndTime: null,
-        sendType:'1',
+        sendType:null,
       },
       // 表单参数
       form: {},

+ 1 - 1
src/views/course/sop/index.vue

@@ -308,7 +308,7 @@
     </el-dialog>
 
     <!--  执行详情  -->
-    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="70%" style="font-weight: bolder">
+    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="90%" style="font-weight: bolder">
       <sop-logs-details ref="sopLogsDetails"></sop-logs-details>
     </el-drawer>
 

+ 1929 - 0
src/views/qw/externalContact/deptIndex.vue

@@ -0,0 +1,1929 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="企微公司" prop="corpId">
+        <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+          <el-option
+            v-for="dict in myQwCompanyList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="客户名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入客户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="销售企微昵称" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入销售企微昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户类别" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择用户类别" clearable size="small">
+          <el-option
+            v-for="dict in typeOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="性别" prop="gender">
+        <el-select v-model="queryParams.gender" placeholder="状态" clearable size="small">
+          <el-option
+            v-for="dict in genderOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="客户等级" prop="level">
+        <el-select v-model="queryParams.level" placeholder="客户等级" clearable size="small">
+          <el-option
+            v-for="dict in ratingType"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="等级升降" prop="levelType">
+        <el-select v-model="queryParams.levelType" placeholder="等级升降" clearable size="small">
+          <el-option
+            v-for="dict in ratingUpFall"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+
+      <el-form-item label="电话号码" prop="remarkMobiles">
+        <el-input
+          v-model="queryParams.remarkMobiles"
+          placeholder="请输入备注电话号码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="来源" prop="addWay">
+
+        <el-select v-model="queryParams.addWay" placeholder="来源" clearable size="small">
+          <el-option
+            v-for="dict in addWayOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="状态" prop="status">
+
+        <el-select v-model="queryParams.status" placeholder="状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="转接状态" prop="addWay">
+
+        <el-select v-model="queryParams.transferStatus" placeholder="转接状态" clearable size="small">
+          <el-option
+            v-for="dict in transferStatusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否绑小程序" prop="isBindMini">
+        <el-select v-model="queryParams.isBindMini" placeholder="是否绑定小程序" clearable size="small" @change="handleQuery" >
+          <el-option
+            v-for="dict in isBindMiniOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="标签" prop="tagIds">
+        <!--        <el-select v-model="selectTags" remote multiple placeholder="请选择" filterable  style="width: 100%;">-->
+        <!--          <el-option-->
+        <!--            v-for="dict in tagList"-->
+        <!--            :label="dict.name"-->
+        <!--            :value="dict.tagId">-->
+        <!--          </el-option>-->
+        <!--        </el-select>-->
+
+        <div @click="hangleChangeTags()" style="cursor: pointer; border: 1px solid #e6e6e6; background-color: white; overflow: hidden; flex-grow: 1;width: 250px">
+          <div style="min-height: 35px; max-height: 200px; overflow-y: auto;">
+            <el-tag type="success"
+                    closable
+                    :disable-transitions="false"
+                    v-for="list in this.selectTags"
+                    :key="list.tagId"
+                    @close="handleCloseTags(list)"
+                    style="margin: 3px;"
+            >{{list.name}}
+            </el-tag>
+          </div>
+        </div>
+
+
+      </el-form-item>
+      <el-form-item label="备注" prop="remark">
+        <el-input
+          v-model="queryParams.remark"
+          placeholder="请输入备注"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="绑定员工" prop="companyUserName">
+        <el-input
+          v-model="queryParams.companyUserName"
+          placeholder="请输入员工名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="添加时间" prop="createTime">
+        <el-date-picker v-model="createTime" size="small" style="width: 220px" value-format="yyyy-MM-dd" type="daterange" range-separator="-" start-placeholder="开始日期" end-placeholder="结束日期" @change="change"></el-date-picker>
+      </el-form-item>
+
+
+
+
+      <el-form-item label="流失时间" prop="lossTime">
+        <el-date-picker clearable size="small"
+                        v-model="queryParams.lossTime"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择流失时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="删除时间" prop="delTime">
+        <el-date-picker clearable size="small"
+                        v-model="queryParams.delTime"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择删除时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <!--      <el-col :span="1.5">
+              <el-button
+                type="primary"
+                plain
+                icon="el-icon-plus"
+                size="mini"
+                @click="handleAdd"
+                v-hasPermi="['qw:externalContact:add']"
+              >同步</el-button>
+            </el-col> -->
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-edit"
+          size="mini"
+          @click="handleBatchUpdateNotes"
+          v-hasPermi="['qw:externalContact:edit']"
+        >批量修改备注</el-button>
+      </el-col>
+
+      <!-- <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:externalContact:export']"
+        >导出</el-button>
+      </el-col> -->
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="addUserTag"
+          v-hasPermi="['qw:externalContact:addTag']"
+        >批量添加标签</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="delUserTag"
+          v-hasPermi="['qw:externalContact:delTag']"
+        >批量移除标签</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="updateTalk"
+        >批量更改交流状态</el-button>
+      </el-col>
+      <!--       <el-col :span="1.5">-->
+      <!--        <el-button-->
+      <!--          type="primary"-->
+      <!--          plain-->
+      <!--          size="mini"-->
+      <!--          @click="setUserCourseSop"-->
+      <!--          v-hasPermi="['qw:externalContact:setCourseSop']"-->
+      <!--        >批量设置课程SOP</el-button>-->
+      <!--      </el-col>-->
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-tabs type="card" v-model="isBindActiveName" @tab-click="handleClickX">
+      <el-tab-pane label="全部" name="all"></el-tab-pane>
+      <!--      <el-tab-pane label="已绑定CRM" name="isBind"></el-tab-pane>-->
+      <!--      <el-tab-pane label="未绑定CRM" name="noBind"></el-tab-pane>-->
+    </el-tabs>
+
+    <el-table v-loading="loading" :data="externalContactList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="企微客户ID" align="center" prop="id" />
+      <el-table-column label="企微客户头像" align="center" prop="avatar" width="100px">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.avatar" width="60px">
+            <img :src="scope.row.avatar" style="max-width: 200px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="企微客户名称"  prop="name" width="110px"/>
+      <el-table-column label="客户称呼"  prop="stageStatus" width="110px"/>
+      <el-table-column label="销售企微昵称" align="center" prop="qwUserName" width="120px"/>
+      <el-table-column label="企微部门" align="center" prop="departmentName" width="120px"/>
+      <el-table-column label="用户类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <dict-tag :options="typeOptions" :value="scope.row.type"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="gender">
+        <template slot-scope="scope">
+          <dict-tag :options="genderOptions" :value="scope.row.gender"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="描述信息" align="center" prop="description" />
+      <el-table-column label="标签" align="center" prop="tagIdsName" width="250px">
+        <template slot-scope="scope">
+          <div v-for="name in scope.row.tagIdsName"  style="display: inline;">
+            <el-tag type="success">{{ name }}</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="状态" align="center" prop="status" width="120px" >
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="客户等级" align="center" prop="level" width="120px" >
+        <template slot-scope="scope">
+          <dict-tag :options="ratingType" :value="scope.row.level"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="等级状态" align="center" prop="levelType" width="120px" >
+        <template slot-scope="scope">
+          <dict-tag :options="ratingUpFall" :value="scope.row.levelType"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="绑定员工" align="center" prop="companyUserName" />
+      <el-table-column label="添加时间" align="center" prop="createTime" width="100px" />
+      <el-table-column label="流失时间" align="center" prop="lossTime" width="100px" />
+      <el-table-column label="删除时间" align="center" prop="delTime" width="100px" />
+      <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
+        <template slot-scope="scope">
+          <div v-for="i in JSON.parse(scope.row.remarkMobiles)" :key="i">{{i}}</div>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
+      <el-table-column label="来源" align="center" prop="addWay" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="addWayOptions" :value="scope.row.addWay"/>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="转接状态" align="center" prop="transferStatus" width="100px" >
+        <template slot-scope="scope">
+          <dict-tag :options="transferStatusOptions" :value="scope.row.transferStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="企业id" align="center" prop="corpId" />
+      <el-table-column label="是否重粉" align="center" prop="isRepeat" fixed="right">
+        <template slot-scope="scope">
+          <el-tag type="success" v-if="scope.row.isRepeat == 0">否</el-tag>
+          <el-tag type="danger" v-if="scope.row.isRepeat == 1">是</el-tag>
+          <el-button
+            v-show="scope.row.isRepeat == 1"
+            size="mini"
+            type="text"
+            @click="openRepeat(scope.row)"
+          >查看</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否绑小程序" width="100px" align="center" fixed="right">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.fsUserId" >已绑定</el-tag>
+          <el-tag v-else type="info"> 未绑定</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="修改" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            v-show="scope.row.status==0||scope.row.status==2"
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:externalContact:edit']"
+          >修改备注</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-user-solid"
+            @click="handleAppellation(scope.row)"
+          >修改客户称呼</el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+        <template slot-scope="scope">
+          <!--          <el-button-->
+          <!--            size="mini"-->
+          <!--            type="text"-->
+          <!--            icon="el-icon-edit-outline"-->
+          <!--            @click="handleUpdateCustomer(scope.row)"-->
+          <!--            >-->
+          <!--            <span v-if="scope.row.customerId">换绑CRM</span>-->
+          <!--            <span v-else>绑定CRM</span>-->
+          <!--          </el-button>-->
+
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit-outline"
+            @click="handleUpdateUser(scope.row)"
+          >
+            <span v-if="scope.row.fsUserId">换绑会员</span>
+            <span v-else>绑定会员</span>
+          </el-button>
+
+          <el-button
+            v-show="scope.row.fsUserId"
+            size="mini"
+            type="text"
+            icon="el-icon-thumb"
+            @click="handleUnBindUserId(scope.row)"
+            v-hasPermi="['qw:externalContact:unBindUserId']"
+          >
+            <span>解除会员绑定</span>
+          </el-button>
+
+
+          <el-button v-show="scope.row.customerId"
+            size="mini"
+            type="text"
+            icon="el-icon-paperclip"
+            @click="handleShow(scope.row)"
+          >CRM客户详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            @click="handledetails(scope.row)"
+          >AI获取用户信息
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-show="scope.row.fsUserId"
+            @click="handleUserdetails(scope.row)"
+          >会员详情
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-show="scope.row.fsUserId"
+            @click="healthHandledetails(scope.row)"
+          >健康档案
+          </el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination-more
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
+      <customer-details  ref="customerDetails" @refreshList="refreshList"/>
+    </el-drawer>
+
+
+    <!--  搜索标签   -->
+    <el-dialog :title="changeTagDialog.title" :visible.sync="changeTagDialog.open" width="1000px"  append-to-body>
+
+      <div>搜索标签:
+        <el-input v-model="queryTagParams.name" placeholder="请输入标签名称" clearable size="small" style="width: 200px;margin-right: 10px" />
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(queryTagParams.name)">搜索</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <div v-for="item in tagGroupList" :key="item.id"  >
+        <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+          <span class="name-background">{{ item.name }}</span>
+        </div>
+        <div class="tag-container">
+          <a
+            v-for="tagItem in item.tag"
+            class="tag-box"
+            @click="tagSelection(tagItem)"
+            :class="{ 'tag-selected': tagItem.isSelected }"
+          >
+            {{ tagItem.name }}
+          </a>
+        </div>
+      </div>
+
+      <pagination
+        v-show="tagTotal>0"
+        :total="tagTotal"
+        :page.sync="queryTagParams.pageNum"
+        :limit.sync="queryTagParams.pageSize"
+        @pagination="getPageListTagGroup"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="tagSubmitForm()">确 定</el-button>
+        <el-button @click="tagCancel()">取消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="批量添加客户备注" :visible.sync="notesOpen.open" width="800px" append-to-body>
+      <el-card>
+        <el-row>
+          <el-col>
+            <el-radio-group v-model="notesOpen.nameType" style="margin-bottom: 2%">
+              <el-radio :label="1">
+                客户名称添加在【新备注】【前】
+              </el-radio>
+              <el-radio :label="2">
+                客户名称添加在【新备注】【后】
+              </el-radio>
+              <el-radio :label="3">
+                不添加客户名称
+              </el-radio>
+            </el-radio-group>
+          </el-col>
+          <el-col >
+            <el-radio-group v-model="notesOpen.type">
+              <el-radio
+                :label="1"
+              >添加【新备注】在最【前】面</el-radio>
+              <el-radio
+                :label="2"
+              >添加【新备注】在最【后】面</el-radio>
+              <el-radio
+                :label="3"
+              >替换所有备注</el-radio>
+            </el-radio-group>
+          </el-col>
+          <el-col>
+            <el-input v-model="notesOpen.notes" placeholder="请输入客户备注(最多20个字,含已有的)" clearable size="small"
+                      maxlength="20" show-word-limit style="width: 500px;margin-top: 3%" />
+            <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+              <i class="el-icon-info"></i>
+              由于企业微信官方限制,备注最多20个字,且自动会去除末尾超出的字
+            </div>
+          </el-col>
+        </el-row>
+      </el-card>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="notesSubmitForm()">确 定</el-button>
+        <el-button @click="notesCancel()">取消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="批量添加标签" :visible.sync="tagOpen" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagChange.tagName" placeholder="请输入标签名称" clearable size="small" style="width: 200px;margin-right: 10px" />
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(tagChange.tagName)">搜索</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <el-form ref="form" :model="addTagForm"  label-width="80px">
+        <div v-for="item in tagGroupList" :key="item.id" >
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ item.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in item.tag"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </el-form>
+      <pagination
+        v-show="tagTotal>0"
+        :total="tagTotal"
+        :page.sync="queryTagParams.pageNum"
+        :limit.sync="queryTagParams.pageSize"
+        @pagination="getPageListTagGroup"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="addTagSubmitForm()">确 定</el-button>
+        <el-button @click="addTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="批量移除标签" :visible.sync="tagDelOpen" width="800px" append-to-body>
+      <div>搜索标签:
+        <el-input v-model="tagChange.tagName" placeholder="请输入标签名称" clearable size="small" style="width: 200px;margin-right: 10px" />
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleSearchTags(tagChange.tagName)">搜索</el-button>
+        <el-button type="primary" icon="el-icon-plus" size="mini" @click="cancelSearchTags">重置</el-button>
+      </div>
+      <el-form ref="form" :model="addTagForm"  label-width="80px">
+        <div v-for="item in tagGroupList" :key="item.id" >
+          <div style="font-size: 20px;margin-top: 20px;margin-bottom: 20px;">
+            <span class="name-background">{{ item.name }}</span>
+          </div>
+          <div class="tag-container">
+            <a
+              v-for="tagItem in item.tag"
+              class="tag-box"
+              @click="tagSelection(tagItem)"
+              :class="{ 'tag-selected': tagItem.isSelected }"
+            >
+              {{ tagItem.name }}
+            </a>
+          </div>
+        </div>
+      </el-form>
+      <pagination
+        v-show="tagTotal>0"
+        :total="tagTotal"
+        :page.sync="queryTagParams.pageNum"
+        :limit.sync="queryTagParams.pageSize"
+        @pagination="getPageListTagGroup"
+      />
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="tagDelSubmitForm()">确 定</el-button>
+        <el-button @click="DelTagCancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 添加或修改企业微信客户对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="700px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+
+
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" type="textarea" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="描述信息" prop="description">
+          <el-input v-model="form.description" type="textarea" :rows="3" placeholder="请输入描述信息" />
+        </el-form-item>
+
+        <el-form-item label="备注电话号码" prop="remarkMobiles">
+
+          <el-tag
+            :key="tag"
+            v-for="tag in remarkMobiles"
+            closable
+            :disable-transitions="false"
+            @close="handleClose(tag)">
+            {{tag}}
+          </el-tag>
+          <el-input
+            style="width:110px"
+            class="input-new-tag"
+            v-if="inputVisible"
+            v-model="inputValue"
+            ref="saveTagInput"
+            size="small"
+            @keyup.enter.native="handleInputConfirm"
+            @blur="handleInputConfirm"
+          >
+          </el-input>
+          <el-button v-else class="button-new-tag" size="small" style="width: 110px" @click="showInput">新增电话</el-button>
+
+        </el-form-item>
+
+
+        <el-form-item label="备注企业名称" prop="remarkCorpName">
+          <el-input v-model="form.remarkCorpName" placeholder="请输入备注企业名称" />
+        </el-form-item>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog :title="callOpen.title" :visible.sync="callOpen.open" width="500px" append-to-body>
+      <el-form ref="callOpenFrom" :model="callOpenFrom" :rules="callOpenRule" label-width="110px">
+        <el-form-item label="客户称呼" prop="stageStatus">
+          <el-input v-model="callOpenFrom.stageStatus" placeholder="请输入客户称呼" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" >
+        <el-button type="primary" @click="submitCallOpenFrom">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 绑定客户   -->
+    <el-dialog :title="bindCustomer.title" :visible.sync="bindCustomer.open"  width="1200px" append-to-body>
+      <mycustomer ref="mycustomer"  @bindCustomerId="bindCustomerId"></mycustomer>
+    </el-dialog>
+
+    <!--    设置一个课程sop-->
+    <el-dialog :title="setSop.title" :visible.sync="setSop.open"  width="1200px" append-to-body>
+      <SopDialog ref="SopDialog"  @bindCourseSop="bindCourseSop"></SopDialog>
+    </el-dialog>
+
+    <el-dialog :title="user.title" :visible.sync="user.open" width="800px" append-to-body>
+      <selectUser ref="selectUser" @bindMiniCustomerId="bindMiniCustomerId"></selectUser>
+    </el-dialog>
+
+    <el-dialog :title="info.title" :visible.sync="info.open"   width="1100px" append-to-body>
+      <info  ref="Details" />
+    </el-dialog>
+
+    <el-dialog
+      :title="resultTitle"
+      :visible.sync="resultDialogVisible"
+      width="50%"
+      custom-class="feedback-dialog"
+    >
+      <pre style="white-space: pre-wrap; font-family: inherit;">{{ resultMessage }}</pre>
+      <span slot="footer" class="dialog-footer">
+    <el-button @click="resultDialogVisible = false">关闭</el-button>
+  </span>
+    </el-dialog>
+
+    <el-dialog
+      title="企微账号"
+      :visible.sync="repeat.open"
+      width="50%"
+      custom-class="feedback-dialog"
+    >
+      <el-row style="padding: 20px 10px">
+        <el-tag v-for="item in repeat.list">{{item.qwUserName}}</el-tag>
+      </el-row>
+    </el-dialog>
+    <el-drawer
+      :with-header="false"
+      size="75%"
+      :title="showHealth.title" :visible.sync="showHealth.open">
+      <healthRecordDetails  ref="Details" />
+    </el-drawer>
+    <el-drawer
+      :with-header="false"
+      size="75%"
+      :title="showUser.title"
+      :visible.sync="showUser.open">
+      <userDetails  ref="userDetails" />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import {
+  addExternalContact,
+  addTag,
+  batchUpdateExternalContactNotes,
+  bindUserId,
+  delExternalContact,
+  delTag,
+  editbindCustomer,
+  exportExternalContact,
+  getCustomerCourseSop,
+  getExternalContact,
+  listExternalContact,
+  setCustomerCourseSop,
+  setCustomerCourseSopList,
+  unBindUserId,
+  updateExternalContact,
+  getRepeat,
+  updateExternalContactCall, myDeptExtList
+} from '../../../api/qw/externalContact'
+import {getMyQwCompanyList} from "@/api/qw/user";
+import {getId} from "@/api/store/healthRecord";
+import {listTag, searchTags,} from "@/api/qw/tag";
+import {allListTagGroup} from "@/api/qw/tagGroup";
+import mycustomer from '@/views/qw/externalContact/mycustomer'
+import customerDetails from '@/views/qw/externalContact/customerDetails'
+import SopDialog from '@/views/course/sop/SopDialog.vue'
+import selectUser from "@/views/qw/externalContact/selectUser.vue";
+import info from "@/views/qw/externalContact/info.vue";
+import {editTalk} from "@/api/qw/externalContactInfo";
+import healthRecordDetails from '@/views/store/components/healthRecordDetails.vue'
+import userDetails from '@/views/store/components/userDetails.vue';
+import PaginationMore from "../../../components/PaginationMore/index.vue";
+
+export default {
+  name: "deptExternalContact",
+  components:{PaginationMore, mycustomer,customerDetails,SopDialog,selectUser,info,healthRecordDetails,userDetails},
+  data() {
+    return {
+
+      resultDialogVisible: false,
+      resultMessage: '',
+      resultTitle:'',
+      repeat: {
+        open: false,
+        loading: true,
+        param:{},
+        list:[]
+      },
+
+      user:{
+        open:false,
+        title:"修改客户"
+      },
+      userForm:{
+        id:null,
+        fsUserId:null,
+      },
+      info:{
+        title:"用户信息",
+        open:false,
+      },
+
+      isBindMiniOptions:[
+        {dictLabel:"已绑定",dictValue:'isBindMini'},
+        {dictLabel:"未绑定",dictValue:'noBindMini'},
+      ],
+      //标签弹窗选择
+      tagChange:{
+        open:false,
+        index:null,
+      },
+      sTime:null,
+      eTime:null,
+      createTime:null,
+      // 遮罩层
+      loading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      tagOpen:false,
+      notesOpen: {
+        type:1,
+        nameType:3,
+        open:false,
+        notes:null,
+      },
+      tagDelOpen:false,
+      // 选中数组
+      ids: [],
+      isBindActiveName:"all",
+      remarkMobiles: [],
+      inputVisible: false,
+      inputValue: '',
+      // 非单个禁用
+      single: true,
+      tagGroupList: [],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信客户表格数据
+      externalContactList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 用户类别字典
+      typeOptions: [],
+      ratingType: [],
+      ratingUpFall: [],
+      // 性别字典
+      genderOptions: [],
+
+      addTagForm:{
+        userIds:[],
+        tagIds:[]
+      },
+
+      myQwCompanyList:[],
+      show:{
+        title:"客户详情",
+        open:false,
+      },
+      showHealth:{
+        title:"健康档案",
+        open:false,
+      },
+      showTongue:{
+        title:"舌苔报告",
+        open:false,
+      },
+      showUser:{
+        title:"会员详情",
+        open:false,
+      },
+
+      //存储选择的客户
+      chooseCustomerSOP:null,
+
+      setSop:{
+        title:"选择课节SOP",
+        open:false,
+        //区分单选1还是多选2
+        type:null,
+      },
+      //合成的客户-课节SOP参数
+      customerCourseForm:{},
+
+      //查询是否已经设置过客户-某个课节的SOP
+      customerCourseFormLogs:{},
+      //绑定客户
+      bindCustomer:{
+        title:null,
+        open:false,
+      },
+      callOpen:{
+        open:false,
+        title: '修改客户称呼',
+      },
+      callOpenFrom:{
+        id:null,
+        stageStatus:null,
+      },
+      callOpenRule:{
+        stageStatus:[{required:true,message:"员工称呼不能为空",trigger:"blur"}]
+      },
+      //绑定的参数表
+      qwFormCustomer:{
+        externalContactId:null,
+        customerId:null,
+      },
+      // 来源字典
+      addWayOptions: [],
+
+      //标签
+      changeTagDialog:{
+        title:"",
+        open:false,
+      },
+
+      queryTagParams:{
+        pageNum: 1,
+        pageSize: 10,
+        total:0,
+        name:null,
+        corpId:null,
+      },
+
+      tagTotal:0,
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        qwUserName:null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        qwUserId:null,
+        gender: null,
+        description: null,
+        tagIds: null,
+        remark:null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null,
+        status:null,
+        transferStatus:null,
+        isBind:null,
+        isBindMini:null,
+        lossTime:null,
+        sTime:null,
+        eTime:null,
+        createTime:null,
+        level:null,
+        levelType:null
+      },
+      selectTags:[],
+      // 表单参数
+      form: {},
+      tagList:[],
+      transferStatusOptions:[],
+      statusOptions:[],
+      // 表单校验
+      rules: {
+      },
+      tongueReportParams: {
+        userId: null,
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_qw_externalContact_type").then(response => {
+      this.typeOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_rating_type").then(response => {
+      this.ratingType = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_rating_upFall").then(response => {
+      this.ratingUpFall = response.data;
+    });
+
+    getMyQwCompanyList().then(response => {
+      this.myQwCompanyList = response.data;
+      if(this.myQwCompanyList!=null){
+        this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+
+        var listTagFrom={corpId:this.queryParams.corpId}
+        listTag(listTagFrom).then(response => {
+          this.tagList = response.rows;
+        });
+        this.getList();
+
+      }
+    });
+
+
+    this.getDicts("sys_sex").then(response => {
+      this.genderOptions = response.data;
+    });
+    this.getDicts("sys_qw_externalContact_addWay").then(response => {
+      this.addWayOptions = response.data;
+    });
+
+
+
+    this.getDicts("sys_qw_external_contact_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("sys_qw_transfer_status").then(response => {
+      this.transferStatusOptions = response.data;
+    });
+
+  },
+  methods: {
+    async healthHandledetails(row) {
+      try {
+        console.log(row.fsUserId);
+        console.log(row);
+
+        // 先尝试获取健康记录数据
+        const res = await getId(row.fsUserId);
+        console.log(res);
+
+        // 只有获取数据成功后才打开对话框
+        this.showHealth.open = true;
+
+        // 确保对话框DOM已更新
+        await this.$nextTick();
+        this.$refs.Details.getDetails(res.data);
+
+      } catch (error) {
+        console.error('获取健康档案失败:', error);
+        //this.$message.error('获取健康档案失败: ' + (error.message || '该用户还未填写健康档案'));
+      }
+    },
+    async handleUserdetails(row){
+      this.showUser.open = true;
+      try {
+        await this.$nextTick();
+        this.$refs.userDetails.getDetails(row.fsUserId);
+      } catch (error) {
+        console.error('获取会员详情失败:', error);
+      }
+    },
+    change(){
+      if(this.createTime!=null){
+        this.queryParams.sTime=this.createTime[0];
+        this.queryParams.eTime=this.createTime[1];
+      }else{
+        this.queryParams.sTime=null;
+        this.queryParams.eTime=null;
+      }
+
+    },
+    updateCorpId(){
+      var listTagFrom={corpId:this.queryParams.corpId}
+      listTag(listTagFrom).then(response => {
+        this.tagList = response.rows;
+      });
+      this.getList();
+    },
+    /** 查询企业微信客户列表 */
+    getList() {
+      this.loading = true;
+      myDeptExtList(this.queryParams).then(response => {
+        this.externalContactList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    bindMiniCustomerId(row){
+      console.log(row)
+      this.userForm.fsUserId=row;
+      bindUserId(this.userForm).then(res=>{
+        if (res.code==200){
+          this.$message.success('绑定成功')
+        }else {
+          this.$message.error('绑定失败:',res.msg)
+        }
+        this.getList()
+        this.user.open=false;
+      })
+    },
+    /** 查看客户详情 */
+    handleShow(row){
+      this.show.open=true;
+      var that=this;
+      const tab = "visit";
+      setTimeout(() => {
+        that.$refs.customerDetails.getDetails(row.customerId);
+        that.$refs.customerDetails.handleClick(tab);
+
+      }, 200);
+    },
+
+    handledetails(row){
+      this.info.open=true;
+      setTimeout(() => {
+        this.$refs.Details.getDetails(row.id);
+      }, 1);
+    },
+
+    closeInfo(){
+      this.info.open=false
+    },
+    handleClickX(tab, event) {
+
+      this.queryParams.isBind=tab.name;
+      this.handleQuery();
+    },
+
+    handleClose(tag) {
+      this.remarkMobiles.splice(this.remarkMobiles.indexOf(tag), 1);
+    },
+    showInput() {
+      this.inputVisible = true;
+      this.$nextTick(_ => {
+        this.$refs.saveTagInput.$refs.input.focus();
+      });
+    },
+    handleInputConfirm() {
+      let inputValue = this.inputValue;
+      if (inputValue) {
+        this.remarkMobiles.push(inputValue);
+      }
+      this.inputVisible = false;
+      this.inputValue = '';
+    },
+
+    handleBatchUpdateNotes(){
+
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要添加备注的客户');
+      }
+
+      this.notesOpen.open=true;
+
+    },
+
+    addUserTag(){
+
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要添加标签的客户');
+      }
+
+      this.getPageListTagGroup();
+
+      setTimeout(() => {
+
+        for (let i = 0; i < this.tagGroupList.length; i++) {
+          for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+            this.tagGroupList[i].tag[x].isSelected=false;
+          }
+        }
+      }, 200);
+
+
+      this.tagOpen = true;
+
+    },
+
+
+    getPageListTagGroup(){
+      this.queryTagParams.corpId=this.queryParams.corpId
+      allListTagGroup(this.queryTagParams).then(response => {
+        this.tagGroupList = response.rows;
+        this.tagTotal = response.total;
+      });
+    },
+
+    delUserTag(){
+
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要移除标签的客户');
+      }
+      this.getPageListTagGroup();
+
+      setTimeout(() => {
+        for (let i = 0; i < this.tagGroupList.length; i++) {
+          for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+            this.tagGroupList[i].tag[x].isSelected=false;
+          }
+        }
+      }, 200);
+
+      this.tagDelOpen = true;
+
+    },
+
+
+    //搜索的标签
+    hangleChangeTags(){
+
+      this.changeTagDialog.title="搜索的标签"
+      this.changeTagDialog.open=true;
+
+      // 获取 tagListFormIndex 中的所有 tagId,用于快速查找
+      const selectedTagIds = new Set(
+        (this.selectTags || []).map(tagItem => tagItem?.tagId)
+      );
+
+      this.queryTagParams.name=null;
+
+      this.getPageListTagGroup();
+
+      setTimeout(() => {
+        for (let i = 0; i < this.tagGroupList.length; i++) {
+          for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+            this.tagGroupList[i].tag[x].isSelected = selectedTagIds.has(this.tagGroupList[i].tag[x].tagId);
+          }
+        }
+      }, 200);
+
+
+    },
+
+    //删除一些选择的标签
+    handleCloseTags(list){
+      const ls = this.selectTags.findIndex(t => t.tagId === list.tagId);
+      if (ls !== -1) {
+        this.selectTags.splice(ls, 1);
+        this.selectTags = [...this.selectTags];
+      }
+
+      if (this.selectTags!=null && this.selectTags.length>0){
+        // 确保 this.form.tags 是数组
+        if (!this.queryParams.tagIds) {
+          this.queryParams.tagIds = []; // 如果未定义,初始化
+        } else {
+          this.queryParams.tagIds = []; // 清空已有数据
+        }
+
+        // 遍历并添加 tagId
+        this.selectTags.forEach(tag => {
+          if (tag.tagId) { // 确保 tagId 存在
+            this.queryParams.tagIds.push(tag.tagId);
+          }
+        });
+        this.queryParams.tagIds=this.queryParams.tagIds.join(",");
+      }else {
+        this.queryParams.tagIds=null;
+      }
+
+    },
+
+    //重新获取页面数据
+    refreshList(){
+      this.getList();
+    },
+
+    //批量设置课程sop
+    setUserCourseSop(){
+
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要设置课程SOP的客户');
+      }
+
+      this.$confirm('批量设置客户课节SOP可能会存在重复,确定要批量设置吗?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.setSop.open = true;
+        this.setSop.type = 2;
+      })
+        .catch(() => {
+          // 可以处理用户点击“取消”的逻辑
+        });
+
+    },
+    tagSelection(row){
+
+      row.isSelected= !row.isSelected;
+      this.$forceUpdate();
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    addTagCancel() {
+
+      this.tagOpen = false;
+
+      this.addTagForm={
+        userIds:[],
+        tagIds:[]
+      };
+    },
+
+    DelTagCancel() {
+      this.tagDelOpen = false;
+
+      this.addTagForm={
+        userIds:[],
+        tagIds:[]
+      };
+    },
+    addTagSubmitForm(){
+
+      for (let i = 0; i < this.tagGroupList.length; i++) {
+        for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+          if(this.tagGroupList[i].tag[x].isSelected==true){
+            this.addTagForm.tagIds.push(this.tagGroupList[i].tag[x].tagId)
+          }
+
+        }
+      }
+      if(this.addTagForm.tagIds==[]||this.addTagForm.tagIds==null||this.addTagForm.tagIds==""){
+        return  this.$message('请选择标签');
+      }
+
+      this.addTagForm.corpId=this.queryParams.corpId
+      this.addTagForm.userIds=this.ids;
+
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '正在执行中请稍后~~请不要刷新页面!!',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+
+      addTag(this.addTagForm).then(response => {
+
+        this.tagOpen = false;
+        loadingRock.close();
+        this.addTagForm={
+          userIds:[],
+          tagIds:[]
+        };
+
+        this.resultMessage = response.msg;
+        this.resultDialogVisible = true; // 显示弹窗
+        this.resultTitle = '批量添加标签结果';
+
+      }).finally(res=>{
+
+
+        this.getList()
+        loadingRock.close();
+      });
+
+    },
+    tagDelSubmitForm(){
+      for (let i = 0; i < this.tagGroupList.length; i++) {
+        for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+          if(this.tagGroupList[i].tag[x].isSelected==true){
+            this.addTagForm.tagIds.push(this.tagGroupList[i].tag[x].tagId)
+          }
+
+        }
+      }
+      if(this.addTagForm.tagIds==[]||this.addTagForm.tagIds==null||this.addTagForm.tagIds==""){
+        return  this.$message('请选择标签');
+      }
+      this.addTagForm.corpId=this.queryParams.corpId
+      this.addTagForm.userIds=this.ids;
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '正在执行中请稍后~~请不要刷新页面!!',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+
+      delTag(this.addTagForm).then(response => {
+
+        this.tagDelOpen = false;
+        loadingRock.close();
+        this.addTagForm={
+          userIds:[],
+          tagIds:[]
+        };
+
+        this.resultMessage = response.msg;
+        this.resultDialogVisible = true; // 显示弹窗
+        this.resultTitle = '批量删除标签结果';
+
+      }).finally(res=>{
+
+        this.getList()
+        loadingRock.close();
+      });
+    },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        userId: null,
+        externalUserId: null,
+        name: null,
+        companyUserId:null,
+        customerId:null,
+        avatar: null,
+        type: null,
+        gender: null,
+        remark: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null,
+        transferStatus:null,
+        status:null,
+        sTime:null,
+        eTime:null,
+        createTime:null,
+        transferTime:null,
+        transferNum:null,
+        lossTime:null,
+        delTime:null,
+        state:null,
+        wayId:null,
+        stageStatus:null,
+        customerName:null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+
+      if (this.selectTags!=null && this.selectTags.length>0){
+        // 确保 this.form.tags 是数组
+        if (!this.queryParams.tagIds) {
+          this.queryParams.tagIds = []; // 如果未定义,初始化
+        } else {
+          this.queryParams.tagIds = []; // 清空已有数据
+        }
+
+        // 遍历并添加 tagId
+        this.selectTags.forEach(tag => {
+          if (tag.tagId) { // 确保 tagId 存在
+            this.queryParams.tagIds.push(tag.tagId);
+          }
+        });
+        this.queryParams.tagIds=this.queryParams.tagIds.join(",");
+      }else {
+        this.queryParams.tagIds=null;
+      }
+
+
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    handleSearchTags(name){
+
+      searchTags({name:name,corpId:this.queryParams.corpId}).then(response => {
+        this.tagGroupList = response.rows;
+      });
+
+    },
+
+    cancelSearchTags(){
+      this.resetSearchQueryTag()
+
+      this.getPageListTagGroup();
+    },
+    notesSubmitForm(){
+
+      if (this.notesOpen.notes==null || this.notesOpen.notes==""){
+        return  this.$message.error("请输入备注内容");
+      }
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '正在执行中请稍后~~请不要刷新页面!!',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+
+      batchUpdateExternalContactNotes({userIds:this.ids,notes:this.notesOpen.notes,type:this.notesOpen.type,nameType:this.notesOpen.nameType}).then(res => {
+
+        this.resultMessage = res.msg;
+        this.resultDialogVisible = true; // 显示弹窗
+        this.resultTitle = '批量修改备注结果';
+
+      }).finally(res=>{
+        this.getList();
+        loadingRock.close();
+        this.notesCancel();
+      })
+
+    },
+
+    //确定选择标签
+    tagSubmitForm(){
+
+      for (let i = 0; i < this.tagGroupList.length; i++) {
+        for (let x = 0; x < this.tagGroupList[i].tag.length; x++) {
+          if (this.tagGroupList[i].tag[x].isSelected === true) {
+
+            if (!this.selectTags) {
+              this.selectTags = [];
+            }
+
+            // 检查当前 tag 是否已经存在于 tagListFormIndex[index] 中
+            let tagExists = this.selectTags.some(
+              tag => tag.id === this.tagGroupList[i].tag[x].id
+            );
+
+            // 如果 tag 不存在于 tagListFormIndex[index] 中,则新增
+            if (!tagExists) {
+              this.selectTags.push(this.tagGroupList[i].tag[x]);
+            }
+          }
+        }
+      }
+      if (!this.selectTags || this.selectTags.length === 0) {
+        return this.$message('请选择标签');
+      }
+
+      this.changeTagDialog.open = false;
+    },
+
+    //取消选择标签
+    tagCancel(){
+      this.changeTagDialog.open = false;
+    },
+    //取消备注
+    notesCancel(){
+      this.notesOpen={
+        open: false,
+        notes: null,
+        type: 1,
+        nameType:3,
+      }
+    },
+
+    resetSearchQueryTag(){
+
+      this.queryTagParams= {
+        pageNum: 1,
+        pageSize: 10,
+        total:0,
+        name:null,
+      };
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue;
+      this.selectTags=[];
+      this.createTime=null;
+      this.queryParams.sTime=null;
+      this.queryParams.eTime=null;
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.loading=true;
+      this.form.corpId=this.queryParams.corpId
+      addExternalContact(this.form).then(response => {
+        this.msgSuccess("同步成功");
+        this.getList();
+      }).finally(()=>{
+        this.loading=false;
+      });
+
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getExternalContact(id).then(response => {
+        this.form = response.data;
+        if(this.form.remarkMobiles!=null){
+          this.remarkMobiles=JSON.parse(this.form.remarkMobiles)
+        }else{
+          this.remarkMobiles=[]
+        }
+
+        this.open = true;
+        this.title = "修改企业微信客户";
+      });
+    },
+
+    handleAppellation(val){
+      this.callOpen.open=true;
+      this.callOpenFrom.stageStatus=val.stageStatus;
+      this.callOpenFrom.id=val.id;
+    },
+
+    /** 绑定客户操作 */
+    handleUpdateCustomer(row){
+      this.bindCustomer.title="绑定客户"
+      this.bindCustomer.open=true;
+      this.form.id=row.id
+      this.form.externalUserId=row.externalUserId
+      this.form.name=row.name
+    },
+
+    handleUpdateUser(row){
+      this.user.title="绑定客户"
+      this.user.open=true;
+      this.userForm.id=row.id;
+    },
+
+    handleUnBindUserId(val){
+
+      this.$confirm(
+        '确认解绑客户:<span style="color: green;">' + val.name + '' +
+        '</span> 的小程序用户?<br><span style="color: red;">【ps:可能会导致客户无法看课】</span>',
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+          dangerouslyUseHTMLString: true // 允许使用 HTML 字符串
+        }
+      ).then(() => {
+        return unBindUserId(val.id);
+      }).then(response => {
+        this.getList();
+        this.msgSuccess("解绑成功");
+      }).finally(res=>{
+        this.getList();
+      })
+    },
+
+    bindCustomerId(row){
+
+      console.log("row",row)
+      // this.qwFormCustomer.customerId=row;
+      this.form.customerId=row;
+      this.form.corpId=this.queryParams.corpId;
+      this.msgWarning("绑定中.....同步信息中.....");
+
+      editbindCustomer(this.form).then(res=>{
+        //清空表单
+        this.reset();
+        this.bindCustomer.open = false;
+        this.msgSuccess("绑定成功");
+        this.getList();
+
+      })
+
+    },
+    //设置一个SOP
+    setCourseSOP(row) {
+
+      // 检查 row.miniUserId 是否为 null
+      if (row.miniUserId === null || row.miniUserId === undefined) {
+        return this.$confirm('当前客户【CRM客户详情】中 未绑定小程序客户,请先绑定', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).catch(error => {
+          this.msgWarning("操作取消:", error);
+        });
+      } else {
+        this.chooseCustomerSOP = row;
+        this.setSop.open = true;
+        this.setSop.type=1;
+      }
+    },
+
+    //选择课程SOP
+    // 用于设置 customerCourseForm 和 customerCourseFormLogs 的共同属性
+    setCommonProperties(form, row) {
+      form.qwUserid = this.chooseCustomerSOP.userId;
+      form.companyUserId = this.chooseCustomerSOP.companyUserId;
+      form.externalUserId = this.chooseCustomerSOP.externalUserId;
+      form.customerId = this.chooseCustomerSOP.customerId;
+      form.miniUserId = this.chooseCustomerSOP.miniUserId;
+      form.businessId = row.businessId;
+    },
+
+    bindCourseSop(row,days) {
+
+      if (this.setSop.type==2){
+        this.setSop.open = false;
+        this.loading=true;
+        this.msgWarning("设定中.....同步信息中.....");
+
+        setCustomerCourseSopList({ids:this.ids,fsCourseSopId:row.id,days:days}).then(res=>{
+
+          let msg=" 批量设置成功数【" + res.successNum + "】,<br>"
+
+          if (res.failCRM.length>0){
+            msg+="失败的客户【" + res.failCRM + "】,原因是未绑定CRM客户。<br>"
+          }
+          if (res.failMiNi.length>0){
+            msg+="失败的客户【" + res.failMiNi + "】,原因是CRM中未绑定小程序客户。<br>"
+          }
+          if (res.failCompany.length>0){
+            msg+="失败的客户【" + res.failCompany + "】,原因是客户没有所属成员。<br>"
+          }
+
+
+          return this.$confirm(msg, "提示", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning",
+            dangerouslyUseHTMLString: true // 允许使用HTML标签
+          }).catch(error => {
+            this.msgSuccess("操作完成~");
+          });
+
+        }).finally(()=>{
+          this.loading = false;
+          this.getList();
+        })
+      }else if (this.setSop.type==1){
+
+        // 设置 customerCourseFormLogs 的属性
+        this.setCommonProperties(this.customerCourseFormLogs, row);
+
+        // 设置 customerCourseForm 的属性
+        this.setCommonProperties(this.customerCourseForm, row);
+        this.customerCourseForm.sopId = row.id;
+        this.customerCourseForm.sopType = row.sopType;
+        this.customerCourseForm.setting = row.setting;
+        this.customerCourseForm.days = days;
+
+        // 执行异步操作
+        getCustomerCourseSop(this.customerCourseFormLogs)
+          .then(res => {
+            if (res) {
+              return this.$confirm('当前客户已设置过相同课程课节SOP,确定还要再次设置吗?', "警告", {
+                confirmButtonText: "确定",
+                cancelButtonText: "取消",
+                type: "warning"
+              });
+            } else {
+              return Promise.resolve(); // 如果没有设置过,直接执行后续操作
+            }
+          })
+          .then(() => {
+            this.loading = true;
+            this.setSop.open = false;
+            this.msgSuccess("设定中.....同步信息中.....");
+
+            return setCustomerCourseSop(this.customerCourseForm);
+          })
+          .then(() => {
+            this.msgSuccess("设定成功");
+          })
+          .catch(error => {
+            this.msgWarning("操作取消:", error);
+          })
+          .finally(() => {
+            this.loading = false;
+            this.getList();
+          });
+      }
+    },
+    submitCallOpenFrom(){
+
+      this.$refs["callOpenFrom"].validate(valid => {
+        if (valid) {
+
+          if (this.callOpenFrom.id != null && this.callOpenFrom.stageStatus != null) {
+            updateExternalContactCall(this.callOpenFrom).then(res=>{
+              this.$message.success('修改成功');
+              this.callOpen.open=false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            this.form.remarkMobiles=JSON.stringify(this.remarkMobiles)
+            updateExternalContact(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addExternalContact(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企业微信客户编号为"' + ids + '"的数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return delExternalContact(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("删除成功");
+      }).catch(() => {});
+    },
+
+    updateTalk(row){
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认批量更改用户信息为非首次交流', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return editTalk(ids);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("成功");
+      }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信客户数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportExternalContact(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
+    },
+    openRepeat(row){
+      this.repeat.param = {
+        externalUserId: row.externalUserId,
+        corpId: row.corpId,
+        userId: row.userId,
+      };
+      getRepeat(this.repeat.param).then(e => {
+        this.repeat.open = true;
+        this.repeat.loading = false;
+        this.repeat.list = e.data;
+      })
+
+    },
+  }
+};
+</script>
+<style scoped>
+/* CSS 样式 */
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+/* CSS 样式 */
+.tag-container {
+  display: flex;
+  flex-wrap: wrap; /* 超出宽度时自动换行 */
+  gap: 8px; /* 设置标签之间的间距 */
+}
+.name-background {
+  display: inline-block;
+  background-color: #abece6; /* 背景颜色 */
+  padding: 4px 8px; /* 调整内边距,让背景包裹文字 */
+  border-radius: 4px; /* 可选:设置圆角 */
+}
+.tag-box {
+  padding: 8px 12px;
+  border: 1px solid #989797;
+  border-radius: 4px;
+  cursor: pointer;
+  display: inline-block;
+}
+
+.tag-selected {
+  background-color: #00bc98;
+  color: #fff;
+  border-color: #00bc98;
+}
+
+.el-tag + .el-tag {
+  margin-left: 10px;
+}
+
+
+
+.button-new-tag {
+  margin-left: 10px;
+  height: 32px;
+  line-height: 30px;
+  padding-top: 0;
+  padding-bottom: 0;
+}
+.input-new-tag {
+  width: 90px;
+  margin-left: 10px;
+  vertical-align: bottom;
+}
+
+.feedback-dialog {
+  width: 100%;
+  max-width: 1000px;
+  max-height: 80vh; /* 限制最大高度为视窗高度的 80% */
+  overflow-y: auto; /* 超出时显示垂直滚动条 */
+  padding: 20px;
+  box-sizing: border-box; /* 确保 padding 不影响总宽度 */
+}
+</style>

+ 439 - 0
src/views/qw/externalContactLoss/deptLossIndex.vue

@@ -0,0 +1,439 @@
+deptLossIndex<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="80px">
+      <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+      </el-form-item>
+      <el-form-item label="客户名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入客户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="员工名称" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入企微员工名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:externalContact:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+    <el-tabs type="card" v-model="queryParams.status" @tab-click="handleClickX">
+      <el-tab-pane label="流失" name="3"></el-tab-pane>
+      <el-tab-pane label="删除" name="4"></el-tab-pane>
+    </el-tabs>
+    <el-table v-loading="loading" :data="externalContactList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="企微员工账号" align="center" prop="userId" width="120px"/>
+      <el-table-column label="企微员工名称" align="center" prop="qwUserName" width="120px"/>
+      <el-table-column label="客户名称" align="center" prop="name" />
+      <el-table-column label="头像" align="center" prop="avatar" width="100px">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.avatar" width="60px">
+            <img :src="scope.row.avatar" style="max-width: 200px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <dict-tag :options="typeOptions" :value="scope.row.type"/>
+        </template>
+      </el-table-column>
+       <el-table-column label="流失时间" align="center" prop="lossTime" v-if="queryParams.status==3"/>
+       <el-table-column label="删除时间" align="center" prop="delTime" v-if="queryParams.status==4"/>
+      <el-table-column label="状态" align="center" prop="status" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否绑定CRM" width="110px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.customerId"
+            type="success"
+            size="mini"
+            plain>
+            已绑定
+          </el-button>
+          <el-button
+            v-else
+            type="danger"
+            size="mini"
+            plain>
+            未绑定
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+        <template slot-scope="scope">
+          <el-button v-if="scope.row.customerId"
+                     size="mini"
+                     type="text"
+                     icon="el-icon-paperclip"
+                     @click="handleShow(scope.row)"
+          >CRM客户详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改企业微信客户对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="属于用户账号" prop="userId">
+          <el-input v-model="form.userId" placeholder="请输入属于用户账号" />
+        </el-form-item>
+        <el-form-item label="外部联系人账号" prop="externalUserId">
+          <el-input v-model="form.externalUserId" placeholder="请输入外部联系人账号" />
+        </el-form-item>
+        <el-form-item label="名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入名称" />
+        </el-form-item>
+        <el-form-item label="头像" prop="avatar">
+          <el-input v-model="form.avatar" placeholder="请输入头像" />
+        </el-form-item>
+        <el-form-item label="用户类别" prop="type">
+          <el-select v-model="form.type" placeholder="请选择用户类别">
+            <el-option
+              v-for="dict in typeOptions"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+        <el-form-item label="性别" prop="gender">
+          <el-input v-model="form.gender" placeholder="请输入性别" />
+        </el-form-item>
+        <el-form-item label="备注" prop="remark">
+          <el-input v-model="form.remark" placeholder="请输入备注" />
+        </el-form-item>
+        <el-form-item label="描述信息" prop="description">
+          <el-input v-model="form.description" placeholder="请输入描述信息" />
+        </el-form-item>
+        <el-form-item label="标签id" prop="tagIds">
+          <el-input v-model="form.tagIds" placeholder="请输入标签id" />
+        </el-form-item>
+        <el-form-item label="备注电话号码" prop="remarkMobiles">
+          <el-input v-model="form.remarkMobiles" placeholder="请输入备注电话号码" />
+        </el-form-item>
+        <el-form-item label="备注企业名称" prop="remarkCorpName">
+          <el-input v-model="form.remarkCorpName" placeholder="请输入备注企业名称" />
+        </el-form-item>
+        <el-form-item label="来源" prop="addWay">
+          <el-input v-model="form.addWay" placeholder="请输入来源" />
+        </el-form-item>
+
+        <el-form-item label="企业id" prop="corpId">
+          <el-input v-model="form.corpId" placeholder="请输入企业id" />
+        </el-form-item>
+        <el-form-item label="公司id" prop="companyId">
+          <el-input v-model="form.companyId" placeholder="请输入公司id" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+
+
+    <el-drawer size="75%" :title="show.title" :visible.sync="show.open">
+      <customer-details  ref="customerDetails" />
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import { myDeptExtList,listExternalContact, getExternalContact, delExternalContact, addExternalContact, updateExternalContact, exportExternalContact } from "@/api/qw/externalContact";
+import { listTag, getTag, delTag, addTag, updateTag, exportTag } from "@/api/qw/tag";
+import customerDetails from '@/views/qw/externalContact/customerDetails'
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+export default {
+  name: "deptLossIndex",
+  components:{customerDetails},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      myQwCompanyList:[],
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信客户表格数据
+      externalContactList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      show:{
+        title:"客户详情",
+        open:false,
+      },
+      // 用户类别字典
+      typeOptions: [],
+      // 性别字典
+      genderOptions: [],
+      // 来源字典
+      addWayOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        qwUserName:null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null,
+        status:"3",
+        transferStatus:null
+      },
+      // 表单参数
+      form: {},
+      tagList:[],
+      transferStatusOptions:[],
+      statusOptions:[],
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+    });
+    this.getDicts("sys_qw_externalContact_type").then(response => {
+      this.typeOptions = response.data;
+    });
+    this.getDicts("sys_sex").then(response => {
+      this.genderOptions = response.data;
+    });
+    this.getDicts("sys_qw_externalContact_addWay").then(response => {
+      this.addWayOptions = response.data;
+    });
+    listTag().then(response => {
+      this.tagList = response.rows;
+    });
+    this.getDicts("sys_qw_external_contact_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("sys_qw_transfer_status").then(response => {
+      this.transferStatusOptions = response.data;
+    });
+  },
+  methods: {
+    updateCorpId(){
+           this.getList();
+     },
+    /** 查询企业微信客户列表 */
+    getList() {
+      this.loading = true;
+      myDeptExtList(this.queryParams).then(response => {
+        this.externalContactList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    handleClickX(tab, event) {
+
+       this.queryParams.status=tab.name;
+
+      this.handleQuery();
+    },
+
+
+    /** 查看客户详情 */
+    handleShow(row){
+      this.show.open=true;
+      var that=this;
+      const tab = "visit";
+      setTimeout(() => {
+        that.$refs.customerDetails.getDetails(row.customerId);
+        that.$refs.customerDetails.handleClick(tab);
+
+      }, 200);
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        userId: null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        remark: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue;
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+     addExternalContact(this.form).then(response => {
+       this.msgSuccess("同步成功");
+       this.getList();
+     });
+
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getExternalContact(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改企业微信客户";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        this.form.corpId=this.queryParams.corpId;
+        if (valid) {
+          if (this.form.id != null) {
+            updateExternalContact(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addExternalContact(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企业微信客户编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delExternalContact(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信客户数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportExternalContact(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 524 - 0
src/views/qw/externalContactTransfer/deptTransferIndex.vue

@@ -0,0 +1,524 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="110px">
+
+<!--      <el-form-item label="外部联系人账号" prop="externalUserId">-->
+<!--        <el-input-->
+<!--          v-model="queryParams.externalUserId"-->
+<!--          placeholder="外部联系人账号"-->
+<!--          clearable-->
+<!--          size="small"-->
+<!--          @keyup.enter.native="handleQuery"-->
+<!--        />-->
+<!--      </el-form-item>-->
+      <el-form-item label="企微公司" prop="corpId">
+            <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+              <el-option
+                v-for="dict in myQwCompanyList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="dict.dictValue"
+              />
+            </el-select>
+      </el-form-item>
+      <el-form-item label="客户名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入客户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="所属员工" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入所属员工名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="用户类别" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择用户类别" clearable size="small">
+          <el-option
+            v-for="dict in typeOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="客户等级" prop="level">
+        <el-select v-model="queryParams.level" placeholder="客户等级" clearable size="small">
+          <el-option
+            v-for="dict in ratingType"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="性别" prop="gender">
+        <el-input
+          v-model="queryParams.gender"
+          placeholder="请输入性别"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="转接状态" prop="addWay">
+        <el-select v-model="queryParams.transferStatus" placeholder="转接状态" clearable size="small">
+          <el-option
+            v-for="dict in transferStatusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="标签" prop="tagIds">
+        <el-select v-model="selectTags" remote multiple placeholder="请选择" filterable  style="width: 100%;">
+          <el-option
+            v-for="dict in tagList"
+            :label="dict.name"
+            :value="dict.tagId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="添加时间" prop="createTime">
+        <el-date-picker clearable size="small"
+                        v-model="queryParams.createTime"
+                        type="date"
+                        value-format="yyyy-MM-dd"
+                        placeholder="选择添加时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+   <!--   <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:externalContact:add']"
+        >同步</el-button>
+      </el-col> -->
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:externalContact:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="handleTransfer"
+          v-hasPermi="['qw:externalContact:transfer']"
+        >分配客户</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="externalContactList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+
+      <el-table-column label="所属员工" align="center" prop="qwUserName" width="120px"/>
+      <el-table-column label="员工部门" align="center" prop="departmentName" width="120px"/>
+<!--      <el-table-column label="外部联系人账号" align="center" prop="externalUserId" width="120px"/>-->
+      <el-table-column label="客户名称" align="center" prop="name" />
+      <el-table-column label="头像" align="center" prop="avatar" width="100px">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.avatar" width="60px">
+            <img :src="scope.row.avatar" style="max-width: 200px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <dict-tag :options="typeOptions" :value="scope.row.type"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="gender">
+        <template slot-scope="scope">
+          <dict-tag :options="genderOptions" :value="scope.row.gender"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="描述信息" align="center" prop="description" />
+      <el-table-column label="标签" align="center" prop="tagIds" width="150px">
+        <template slot-scope="scope">
+          <div v-for="i in JSON.parse(scope.row.tagIds)" :key="i" style="display: inline;">
+          <el-tag type="success" v-for="ii in tagList" :key="ii.id" style="margin: 3px;" v-if="ii.tagId==i">{{ii.name}}</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="客户等级" align="center" prop="level" width="120px" >
+        <template slot-scope="scope">
+          <dict-tag :options="ratingType" :value="scope.row.level"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="添加时间" align="center" prop="createTime" width="100px"/>
+      <el-table-column label="状态" align="center" prop="status" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="转接状态" align="center" prop="transferStatus" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="transferStatusOptions" :value="scope.row.transferStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="企业id" align="center" prop="corpId" />
+      <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
+        <template slot-scope="scope">
+          <span v-for="i in JSON.parse(scope.row.remarkMobiles)" :key="i">{{i}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
+      <el-table-column label="来源" align="center" prop="addWay" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="addWayOptions" :value="scope.row.addWay"/>
+        </template>
+      </el-table-column>
+      <!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:externalContact:edit']"
+          >修改</el-button>
+
+        </template>
+      </el-table-column> -->
+    </el-table>
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改企业微信客户对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <div style="background-color: rgb(239, 250, 255); margin: 10px;padding: 15px;">
+          <div>可将选中的客户转接给其他员工,进行后续服务</div>
+          <div>注意:90天内客户只能被转接一次,一个客户最多只能被转接两次</div>
+        </div>
+
+        <el-form-item label="接替员工" prop="userId">
+<!--          <el-button type="success" v-if="this.nickName">{{ nickName }}</el-button>-->
+          <el-input style="width: 150px" disabled>
+            <template slot="prefix">
+              <el-button
+                plain
+                size="small"
+                type="success"
+                v-if="this.nickName">
+                {{ nickName }}
+              </el-button>
+            </template>
+          </el-input>
+        </el-form-item>
+		<el-form-item label="消息内容" prop="content">
+		  <el-input v-model="form.content" placeholder="请输入内容" />
+      <div style="color: #999;font-size: 14px;display: flex;align-items: center;">
+        <i class="el-icon-info"></i>
+        自定义转接的时候发给客户的消息内容(选填)ps:不填则是官方默认话术
+      </div>
+		</el-form-item>
+
+        <el-card>
+          <qwUserSelectOne ref="qwUserSelectOne" @selectUser="selectUser"></qwUserSelectOne>
+        </el-card>
+
+
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { myDeptExtList,transfer,listExternalContact, getExternalContact, delExternalContact, addExternalContact, updateExternalContact, exportExternalContact } from "@/api/qw/externalContact";
+import { listTag, getTag, delTag, addTag, updateTag, exportTag } from "@/api/qw/tag";
+import { qwUserList } from "@/api/qw/user";
+import qwUserSelectOne from '@/views/qw/user/qwUserSelectOne.vue'
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "deptTransferIndex",
+  components: { qwUserSelectOne },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      myQwCompanyList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信客户表格数据
+      externalContactList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 用户类别字典
+      typeOptions: [],
+      ratingType: [],
+      selectTags:[],
+      // 性别字典
+      genderOptions: [],
+      // 来源字典
+      addWayOptions: [],
+      nickName:null,
+      qwUserList:[],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null,
+        status:0,
+        transferStatus:null
+      },
+      // 表单参数
+      form: {},
+      tagList:[],
+      transferStatusOptions:[],
+      statusOptions:[],
+      // 表单校验
+      rules: {
+        userId: [{ required: true, message: '请选择接替员工', trigger: 'blur' }],
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+              listTag({corpId:this.queryParams.corpId}).then(response => {
+                this.tagList = response.rows;
+              });
+              qwUserList(this.queryParams.corpId).then(response => {
+                this.qwUserList = response.rows;
+              });
+
+              this.getList();
+            }
+    });
+
+    this.getDicts("sys_qw_externalContact_type").then(response => {
+      this.typeOptions = response.data;
+    });
+    this.getDicts("sys_user_sex").then(response => {
+      this.genderOptions = response.data;
+    });
+    this.getDicts("sys_qw_externalContact_addWay").then(response => {
+      this.addWayOptions = response.data;
+    });
+    this.getDicts("sys_qw_sop_rating_type").then(response => {
+      this.ratingType = response.data;
+    });
+
+
+    this.getDicts("sys_qw_external_contact_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("sys_qw_transfer_status").then(response => {
+      this.transferStatusOptions = response.data;
+    });
+
+  },
+  methods: {
+    updateCorpId(){
+      listTag({corpId:this.queryParams.corpId}).then(response => {
+        this.tagList = response.rows;
+      });
+      qwUserList(this.queryParams.corpId).then(response => {
+        this.qwUserList = response.rows;
+      });
+      this.getList();
+     },
+    /** 查询企业微信客户列表 */
+    getList() {
+      this.loading = true;
+      myDeptExtList(this.queryParams).then(response => {
+        this.externalContactList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        userId: null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        remark: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+
+    selectUser(row){
+      this.form.userId=row.id
+	  console.log(row)
+      this.nickName=row.qwUserName
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.tagIds=this.selectTags.join(',')
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.selectTags=[];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+     addExternalContact(this.form).then(response => {
+       this.msgSuccess("同步成功");
+       this.getList();
+     });
+
+    },
+
+    handleTransfer(row) {
+      this.reset();
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要分配的客户');
+      }
+
+      setTimeout(() => {
+                    this.$refs.qwUserSelectOne.getDetails(this.queryParams.corpId);
+       }, 1);
+      this.open = true;
+      this.title = "分配客户";
+
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+
+
+        if (valid) {
+            var form={
+              ids:this.ids,
+              userId:this.form.userId,
+              corpId:this.queryParams.corpId,
+			  content:this.form.content,
+            }
+            transfer(form).then(response => {
+              this.msgSuccess(response.msg);
+              this.open = false;
+              this.getList();
+            });
+
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企业微信客户编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delExternalContact(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信客户数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportExternalContact(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 318 - 0
src/views/qw/externalContactTransferLog/deptTransferLogIndex.vue

@@ -0,0 +1,318 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+      </el-form-item>
+      <el-form-item label="员工姓名" prop="companyUserNickName">
+        <el-input
+        style="width:220px"
+          v-model="queryParams.companyUserNickName"
+          placeholder="请输入员工姓名"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+
+
+      <el-form-item label="状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择状态" clearable size="small">
+          <el-option
+            v-for="dict in statusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:externalContactTransferLog:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="snyc"
+          v-hasPermi="['qw:externalContactTransferLog:snyc']"
+        >同步</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="externalContactTransferLogList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+<!--      <el-table-column label="账号" align="center" prop="id" />-->
+      <el-table-column label="原员工" align="center" prop="handoverQwUserName" />
+      <el-table-column label="客户名称" align="center" prop="name" />
+      <el-table-column label="接替员工" align="center" prop="companyUserNickName" />
+      <el-table-column label="员工部门" align="center" prop="deptName" />
+      <el-table-column label="转接时间" align="center" prop="createTime" />
+      <el-table-column label="状态" align="center" prop="status">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改转接记录对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="500px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="80px">
+        <el-form-item label="企业id" prop="corpId">
+          <el-input v-model="form.corpId" placeholder="请输入企业id" />
+        </el-form-item>
+        <el-form-item label="公司id" prop="companyId">
+          <el-input v-model="form.companyId" placeholder="请输入公司id" />
+        </el-form-item>
+        <el-form-item label="员工id" prop="companyUserId">
+          <el-input v-model="form.companyUserId" placeholder="请输入员工id" />
+        </el-form-item>
+        <el-form-item label="企微外部联系人id" prop="externalUserId">
+          <el-input v-model="form.externalUserId" placeholder="请输入企微外部联系人id" />
+        </el-form-item>
+        <el-form-item label="客户id" prop="customerId">
+          <el-input v-model="form.customerId" placeholder="请输入客户id" />
+        </el-form-item>
+        <el-form-item label="外部联系人id" prop="externalContactId">
+          <el-input v-model="form.externalContactId" placeholder="请输入外部联系人id" />
+        </el-form-item>
+        <el-form-item label="状态" prop="status">
+          <el-select v-model="form.status" placeholder="请选择状态">
+            <el-option
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            ></el-option>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import { listTransferLogDeptList,syncTransferLog,listExternalContactTransferLog, getExternalContactTransferLog, delExternalContactTransferLog, addExternalContactTransferLog, updateExternalContactTransferLog, exportExternalContactTransferLog } from "@/api/qw/externalContactTransferLog";
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+export default {
+  name: "deptTransferLogIndex",
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 转接记录表格数据
+      externalContactTransferLogList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      myQwCompanyList:[],
+      // 状态字典
+      statusOptions: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        corpId: null,
+        companyId: null,
+        companyUserId: null,
+        externalUserId: null,
+        customerId: null,
+        externalContactId: null,
+        status: null,
+        companyUserNickName:null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+    });
+    this.getDicts("sys_qw_transfer_status").then(response => {
+      this.statusOptions = response.data;
+    });
+  },
+  methods: {
+    updateCorpId(){
+           this.getList();
+     },
+    /** 查询转接记录列表 */
+    getList() {
+      this.loading = true;
+      listTransferLogDeptList(this.queryParams).then(response => {
+        this.externalContactTransferLogList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    snyc(){
+      syncTransferLog(this.queryParams.corpId).then(response => {
+        this.msgSuccess("同步成功");
+        this.getList();
+      });
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        corpId: null,
+        companyId: null,
+        companyUserId: null,
+        externalUserId: null,
+        customerId: null,
+        externalContactId: null,
+        status: null,
+        createTime: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "添加转接记录";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getExternalContactTransferLog(id).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改转接记录";
+      });
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+            updateExternalContactTransferLog(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addExternalContactTransferLog(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除转接记录编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delExternalContactTransferLog(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有转接记录数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportExternalContactTransferLog(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 474 - 0
src/views/qw/externalContactUnassigned/deptUnassignedIndex.vue

@@ -0,0 +1,474 @@
+
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="110px">
+      <el-form-item label="企微公司" prop="corpId">
+                      <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+                        <el-option
+                          v-for="dict in myQwCompanyList"
+                          :key="dict.dictValue"
+                          :label="dict.dictLabel"
+                          :value="dict.dictValue"
+                        />
+                      </el-select>
+      </el-form-item>
+      <el-form-item label="原所属员工" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入原所属员工名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="客户名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入客户名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="用户类别" prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择用户类别" clearable size="small">
+          <el-option
+            v-for="dict in typeOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="性别" prop="gender">
+        <el-input
+          v-model="queryParams.gender"
+          placeholder="请输入性别"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="转接状态" prop="addWay">
+        <el-select v-model="queryParams.transferStatus" placeholder="转接状态" clearable size="small">
+          <el-option
+            v-for="dict in transferStatusOptions"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:externalContact:add']"
+        >同步</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:externalContact:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          @click="handleTransfer"
+          v-hasPermi="['qw:externalContact:transfer']"
+        >分配客户</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="externalContactList" @selection-change="handleSelectionChange" border>
+      <el-table-column type="selection" width="55" align="center" />
+
+      <el-table-column label="原所属员工" align="center" prop="qwUserName" width="120px"/>
+      <el-table-column label="原员工部门" align="center" prop="departmentName" width="120px"/>
+      <el-table-column label="外部联系人账号" align="center" prop="externalUserId" width="120px"/>
+      <el-table-column label="客户名称" align="center" prop="name" />
+      <el-table-column label="头像" align="center" prop="avatar" width="120px">
+        <template slot-scope="scope">
+          <el-popover
+            placement="right"
+            title=""
+            trigger="hover">
+            <img slot="reference" :src="scope.row.avatar" width="100px">
+            <img :src="scope.row.avatar" style="max-width: 150px;">
+          </el-popover>
+        </template>
+      </el-table-column>
+      <el-table-column label="用户类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <dict-tag :options="typeOptions" :value="scope.row.type"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="性别" align="center" prop="gender">
+        <template slot-scope="scope">
+          <dict-tag :options="genderOptions" :value="scope.row.gender"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注" align="center" prop="remark" />
+      <el-table-column label="描述信息" align="center" prop="description" />
+      <el-table-column label="标签" align="center" prop="tagIds" width="150px">
+        <template slot-scope="scope">
+          <div v-for="i in JSON.parse(scope.row.tagIds)" :key="i" style="display: inline;">
+          <el-tag type="success" v-for="ii in tagList" :key="ii.id" style="margin: 3px;" v-if="ii.tagId==i">{{ii.name}}</el-tag>
+          </div>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注电话号码" align="center" prop="remarkMobiles" width="150px">
+        <template slot-scope="scope">
+          <span v-for="i in JSON.parse(scope.row.remarkMobiles)" :key="i">{{i}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="备注企业名称" align="center" prop="remarkCorpName" />
+      <el-table-column label="来源" align="center" prop="addWay" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="addWayOptions" :value="scope.row.addWay"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="状态" align="center" prop="status" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="statusOptions" :value="scope.row.status"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="转接状态" align="center" prop="transferStatus" width="100px">
+        <template slot-scope="scope">
+          <dict-tag :options="transferStatusOptions" :value="scope.row.transferStatus"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="企业id" align="center" prop="corpId" />
+
+      <!-- <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:externalContact:edit']"
+          >修改</el-button>
+
+        </template>
+      </el-table-column> -->
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改企业微信客户对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="800px"  append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px" style="height: 100%">
+        <div style="background-color: rgb(239, 250, 255); margin: 10px;padding: 15px;">
+          <div>原跟进成员离职时间不能超过1年且离职前一年内至少登录过一次企业微信</div>
+          <div>接替成员最近一年内至少登陆过一次企业微信。</div>
+        </div>
+        <el-form-item label="接替员工:" prop="userId">
+
+          <el-input style="width: 150px" disabled>
+            <template slot="prefix">
+              <el-button
+                plain
+                size="small"
+                type="success"
+                v-if="this.nickName">
+                {{ nickName }}
+              </el-button>
+            </template>
+          </el-input>
+
+
+        </el-form-item>
+        <el-card>
+        <qwUserSelectOne ref="qwUserSelectOne" @selectUser="selectUser"></qwUserSelectOne>
+        </el-card>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  addUnassigned,
+  resignedTransfer,
+  listExternalContact,
+  getExternalContact,
+  delExternalContact,
+  addExternalContact,
+  updateExternalContact,
+  exportExternalContact,
+  myDeptExtList
+} from "../../../api/qw/externalContact";
+import { listTag, getTag, delTag, addTag, updateTag, exportTag } from "@/api/qw/tag";
+// import { qwUserList } from "@/api/qw/user";
+import qwUserSelectOne from "@/views/qw/user/qwUserSelectOne.vue";
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "deptUnassignedIndex",
+  components:{qwUserSelectOne},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企业微信客户表格数据
+      externalContactList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      // 用户类别字典
+      typeOptions: [],
+      myQwCompanyList:[],
+      genderOptions: [],
+      // 来源字典
+      addWayOptions: [],
+      qwUserList:[],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        userId: null,
+        qwUserName: null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null,
+        status:1,
+        transferStatus:null
+      },
+      nickName:null,
+      // 表单参数
+      form: {
+      },
+      tagList:[],
+      transferStatusOptions:[],
+      statusOptions:[],
+      // 表单校验
+      rules: {
+        userId:[   { required: true, message: "接替员工不能为空", trigger: "blur" }]
+
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              listTag({corpId:this.queryParams.corpId}).then(response => {
+                this.tagList = response.rows;
+              });
+              this.getList();
+            }
+    });
+    this.getDicts("sys_qw_externalContact_type").then(response => {
+      this.typeOptions = response.data;
+    });
+    this.getDicts("sys_sex").then(response => {
+      this.genderOptions = response.data;
+    });
+    this.getDicts("sys_qw_externalContact_addWay").then(response => {
+      this.addWayOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_external_contact_status").then(response => {
+      this.statusOptions = response.data;
+    });
+    this.getDicts("sys_qw_transfer_status").then(response => {
+      this.transferStatusOptions = response.data;
+    });
+    // qwUserList().then(response => {
+    //   this.qwUserList = response.rows;
+    // });
+
+  },
+  methods: {
+    updateCorpId(){
+      listTag({corpId:this.queryParams.corpId}).then(response => {
+        this.tagList = response.rows;
+      });
+      this.getList();
+    },
+    getList() {
+      this.loading = true;
+      myDeptExtList(this.queryParams).then(response => {
+        this.externalContactList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        userId: null,
+        externalUserId: null,
+        name: null,
+        avatar: null,
+        type: null,
+        gender: null,
+        remark: null,
+        description: null,
+        tagIds: null,
+        remarkMobiles: null,
+        remarkCorpName: null,
+        addWay: null,
+        operUserid: null,
+        corpId: null,
+        companyId: null
+      };
+      this.resetForm("form");
+    },
+
+    selectUser(row){
+      this.form.userId=row.id
+      // console.log("row",row)
+      this.nickName=row.nickName
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.form.corpId=this.queryParams.corpId;
+     addUnassigned(this.form).then(response => {
+       this.msgSuccess("同步成功");
+       this.getList();
+     });
+
+    },
+
+    handleTransfer(row) {
+      this.reset();
+      if(this.ids==null||this.ids==""){
+        return  this.$message('请选择需要分配的客户');
+      }
+      setTimeout(() => {
+              this.$refs.qwUserSelectOne.getDetails(this.queryParams.corpId);
+      }, 1);
+      this.open = true;
+      this.title = "分配客户";
+
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.nickName=null;
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+            var form={
+              ids:this.ids,
+              userId:this.form.userId,
+              corpId:this.queryParams.corpId,
+            }
+            resignedTransfer(form).then(response => {
+              this.msgSuccess(response.msg);
+              this.open = false;
+              this.getList();
+            });
+
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企业微信客户编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delExternalContact(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企业微信客户数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportExternalContact(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>

+ 1278 - 0
src/views/qw/friendWelcome/deptFriendWelcome.vue

@@ -0,0 +1,1278 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
+      <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+      </el-form-item>
+      <el-form-item label="使用的成员" >
+        <el-select v-model="queryParams.qwUserIds" filterable  clearable placeholder="公司员工"   size="small">
+          <el-option
+             v-for="dict in companyUserList"
+             :key="dict.qwUserId"
+             :label="dict.nickName"
+             :value="dict.qwUserId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="是否开启分时段" prop="isDayparting">
+        <el-select v-model="queryParams.isDayparting" placeholder="请选择" clearable size="small">
+          <el-option v-for="dict in allowSelectOptions" :key="dict.dictValue" :label="dict.dictLabel"  :value="dict.dictValue"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="创建时间" prop="createdTime">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.createdTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择创建时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="更新时间" prop="updateTieme">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.updateTieme"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择更新时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item label="是否发送欢迎语" prop="isSendMsg">
+        <el-select v-model="queryParams.isSendMsg" placeholder="请选择" clearable size="small" >
+          <el-option v-for="dict in allowSelectOptions" :key="dict.dictValue" :label="dict.dictLabel"  :value="dict.dictValue"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+        <el-button
+          type="primary"
+          plain
+          size="mini"
+          icon="el-icon-plus"
+          @click="handleAdd"
+          v-hasPermi="['qw:friendWelcome:add']"
+        >新增</el-button>
+      </el-form-item>
+    </el-form>
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+
+    <el-table v-loading="loading" :data="friendWelcomeList" @selection-change="handleSelectionChange" border>
+      <el-table-column label="消息内容" align="left" prop="welcomeText"  width="400px" >
+        <template slot-scope="scope">
+          <span style="color:rgb(19, 154, 50);" v-if="scope.row.isDayparting==='1'">[共 {{JSON.parse(scope.row.daypartingItemlist).length+1}} 时段]</span>
+          <span style="color:rgb(19, 154, 50);" v-else >[默认时段]</span>
+          <el-tooltip class="item" effect="dark" :content="scope.row.welcomeText" placement="top">
+            <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+              <span>{{ scope.row.welcomeText }}</span>
+            </div>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column label="使用成员" align="center" prop="qwUserIds">
+      <template slot-scope="scope">
+          <div v-for="id in JSON.parse(scope.row.qwUserIds)" :key="id" style="display: inline;" class="text-container">
+            <el-tag type="success" v-for="list in companyUserList" :key="list.qwUserId" style="margin: 3px;" v-if="list.id==id">{{list.qwUserName}}</el-tag>
+          </div>
+      </template>
+      </el-table-column>
+      <el-table-column label="是否开启分时段" align="center" prop="isDayparting">
+        <template slot-scope="scope">
+          <dict-tag :options="allowSelectOptions" :value="scope.row.isDayparting"></dict-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="是否发送欢迎语" align="center" prop="isSendMsg" >
+        <template slot-scope="scope">
+          <dict-tag :options="allowSelectOptions" :value="scope.row.isSendMsg"></dict-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180"/>
+      <el-table-column label="更新时间" align="center" prop="updateTime" width="180"/>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:friendWelcome:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:friendWelcome:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改好友欢迎语对话框 -->
+    <el-dialog :title="title" :visible.sync="open" v-loading="loading" width="1700px" append-to-body>
+      <div style="width: 1130px" class="app-container">
+        <div>
+          <span style="font-size: 15px">基础信息</span>
+          <el-divider></el-divider>
+          <el-alert
+            title="同一个销售的同一个企业微信账号,如果创建了多条记录,则以最新创建的那一条为准!!"
+            type="info"
+            :closable="false"
+            show-icon>
+          </el-alert>
+        </div>
+        <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+
+          <el-form-item label="选择使用成员:" prop="qwUserIds" style="margin-top: 2%">
+            <div>
+              <el-button
+                size="medium"
+                icon="el-icon-circle-plus-outline"
+                plain
+                @click="handlelistUser">请选择使用成员</el-button>
+            </div>
+            <div>
+              <el-tag
+                style="margin-left: 5px"
+                size="medium"
+                :key="list.id"
+                v-for="list in userSelectList"
+                closable
+                :disable-transitions="false"
+                @close="handleClosegroupUser(list)">
+                {{list.qwUserName}}({{list.nickName}})
+              </el-tag>
+            </div>
+          </el-form-item>
+          <el-form-item label="是否发送欢迎语">
+            <el-switch
+              v-model="form.isSendMsg"
+              active-color="#13ce66"
+              inactive-color="#ff4949"
+              active-value="1"
+              inactive-value="2">
+            </el-switch>
+            <span v-if="form.isSendMsg == '1'" style="margin-left: 10px;color: #13ce66">允许</span>
+            <span v-if="form.isSendMsg == '2'" style="margin-left: 10px;color: #ff4949">不允许</span>
+
+          </el-form-item>
+          <div>
+            <span style="font-size: 15px">发送欢迎语</span>
+            <el-divider></el-divider>
+          </div>
+          <el-form-item label="默认欢迎语:" prop="welcomeText">
+              <el-input v-model="form.welcomeText" type="textarea"  :rows="12" maxlength="1300" show-word-limit placeholder="请输入消息内容"/>
+            <!-- 附件和链接列表 -->
+            <el-row>
+              <el-col>
+                <div v-for="(item, index) in form.attachments" :key="index" style="background-color: #f5f7fa;padding: 5px;border: 1px solid  #d9d9d9;">
+                  <div slot="header" style="display: flex;justify-content: space-between;align-items: center; ">
+                    <div style="flex: 1;">
+                    <span v-if="item.msgtype === 'image'">【图片】: {{ item.image.pic_url }}</span>
+                    <span v-if="item.msgtype === 'link'">【链接】: {{ item.link.title }}-{{item.link.desc}}</span>
+                    <span v-if="item.msgtype === 'miniprogram'">【小程序】: {{ item.miniprogram.title }}</span>
+                    </div>
+                    <div style="  display: flex;gap: 10px;">
+                      <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-edit"
+                        style="float: left;"
+                        @click="editFileItem(item,index,-1)"
+                      >修改</el-button>
+                      <el-button
+                        size="mini"
+                        type="text"
+                        icon="el-icon-delete"
+                        style="float: right;"
+                        @click="removeFileItem(item,index,-1)"
+                      >删除</el-button>
+                  </div>
+                  </div>
+                </div>
+              </el-col>
+            </el-row>
+
+            <el-dropdown @command="(command) => handleCommand(command, -1)" trigger="click" placement="top-start">
+              <el-dropdown-menu slot="dropdown" style="width: 120px;">
+                <el-dropdown-item command="image">
+                  <i class="el-icon-picture" style="margin-right: 10px;"></i>图片
+                </el-dropdown-item>
+                <el-dropdown-item command="link">
+                  <i class="el-icon-link" style="margin-right: 10px;"></i>链接
+                </el-dropdown-item>
+                <el-dropdown-item command="miniprogram">
+                  <i class="el-icon-link" style="margin-right: 10px;"></i>小程序
+                </el-dropdown-item>
+              </el-dropdown-menu>
+
+              <span class="el-dropdown-link">
+                <el-link icon="el-icon-paperclip" type="text" style="color: rgb(24, 144, 255)">
+                  添加附件(最多9个)
+                </el-link>
+              </span>
+            </el-dropdown>
+
+            </el-form-item>
+
+          <el-form-item label="分时段欢迎语">
+            <el-switch
+              v-model="form.isDayparting"
+              active-color="#13ce66"
+              inactive-color="#ff4949"
+              active-value="1"
+              inactive-value="2">
+            </el-switch>
+            <span v-if="form.isDayparting == '1'" style="margin-left: 10px;color: #13ce66">已启用</span>
+            <span v-if="form.isDayparting == '2'" style="margin-left: 10px;color: #ff4949">已禁用</span>
+          </el-form-item>
+
+          <el-form-item v-if="form.isDayparting == '1'" >
+            <div style="background-color:#ecf8fe;width: 100%;border: 1px solid #dcdfe6">
+              <span style="margin-left:20px;font-size: 15px;display: block" >注意:1、员工上下班不同时间段可设置不同欢迎语;</span>
+              <span style="margin-left:65px;font-size: 15px;display: block" >2、分时段之外的时间将发送已允许的默认欢迎语。</span>
+              <span style="margin-left:65px;font-size: 15px;display: block" >3、不设置分时段欢迎语则使用已允许的默认欢迎语</span>
+              <span style="margin-left:65px;font-size: 15px;display: block" >4、若员工没有设置过欢迎语则不会发送</span>
+            </div>
+          </el-form-item>
+          <el-form-item v-if="form.isDayparting == '2'">
+            <div style="background-color:#ecf8fe;width: 100%;border: 1px solid #dcdfe6">
+              <span style="margin-left:20px;font-size: 15px;display: block" >注意:1、新建欢迎语最多可发送1条文字消息和9个附件;</span>
+              <span style="margin-left:65px;font-size: 15px;display: block" >2、文字消息和附件不能同时为空,当两者均填写时用户会收到多条消息;</span>
+              <span style="margin-left:65px;font-size: 15px;display: block" >3、欢迎语将在客户加为好友后20秒内下发,因网络延迟可能造成发送不成功</span>
+            </div>
+          </el-form-item>
+          <div  v-if="form.isDayparting == '1'"  v-for="(item, index) in form.daypartingItemlist"    :key="index" >
+            <el-form-item :label="`时段 ${index + 1}:`">
+                <el-row>
+                  <el-col style="width: 965px">
+                    <div style="background-color: #fbfbfb;padding: 10px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
+                          <el-form ref="friendWelcomeItemForm"  :rules="itemRules" :model="item">
+                          <div style="display: flex; gap: 10px;">
+                            <el-form-item label="发起时间:" prop="week" style="flex: 8;">
+                              <el-select v-model="item.week" remote multiple placeholder="请选择" filterable style="width: 580px">
+                                <el-option
+                                  v-for="dict in weekOptions"
+                                  :key="dict.value"
+                                  :label="dict.label"
+                                  :value="dict.value">
+                                </el-option>
+                              </el-select>
+                            </el-form-item>
+                            <el-form-item prop="startTime" style="flex: 1;">
+                              <el-time-select style="width: 120px;"
+                                              placeholder="起始时间"
+                                              v-model="item.startTime"
+                                              :picker-options="{
+                                            start: '00:00',
+                                            step: '00:15',
+                                            end: '24:00'
+                                          }">
+                              </el-time-select>
+                            </el-form-item>
+                            <el-form-item prop="endTime" style="flex: 1;">
+                              <el-time-select style="width: 120px;"
+                                              placeholder="结束时间"
+                                              v-model="item.endTime"
+                                              :picker-options="{
+                                            start: '00:00',
+                                            step: '00:15',
+                                            end: '24:00',
+                                            minTime: item.startTime
+                                          }">
+                              </el-time-select>
+                            </el-form-item>
+                          </div>
+
+                        <el-form-item style="margin-top: 20px" prop="welcomeText">
+                          <el-input v-model="item.welcomeText" type="textarea" :rows="12" maxlength="1300" show-word-limit placeholder="请输入消息内容"/>
+                          <!-- 附件和链接列表 -->
+                          <el-row>
+                            <el-col>
+                              <div v-for="(attachment, attachIndex) in item.attachments" :key="attachIndex" style="background-color: #f5f7fa;padding: 5px;border: 1px solid  #d9d9d9;">
+                                <div slot="header" style="  display: flex;justify-content: space-between;align-items: center; ">
+                                  <div style="flex: 1;">
+                                    <span v-if="attachment.msgtype === 'image'">【图片】: {{ attachment.image.pic_url }}</span>
+                                    <span v-if="attachment.msgtype === 'link'">【链接】: {{ attachment.link.title }}-{{attachment.link.desc}}</span>
+                                    <span v-if="attachment.msgtype === 'miniprogram'">【小程序】: {{ attachment.miniprogram.title }}</span>
+                                  </div>
+                                  <div style="  display: flex;gap: 10px;">
+                                    <el-button
+                                      size="mini"
+                                      type="text"
+                                      icon="el-icon-edit"
+                                      style="float: left;"
+                                      @click="editFileItem(attachment,attachIndex,index)"
+                                    >修改</el-button>
+                                    <el-button
+                                      size="mini"
+                                      type="text"
+                                      icon="el-icon-delete"
+                                      style="float: right;"
+                                      @click="removeFileItem(attachment,attachIndex,index)"
+                                    >删除</el-button>
+                                  </div>
+                                </div>
+                              </div>
+                            </el-col>
+                          </el-row>
+
+                          <el-dropdown @command="(command) => handleCommand(command, index)" trigger="click" placement="top-start">
+                            <el-dropdown-menu slot="dropdown" style="width: 120px;">
+                              <el-dropdown-item command="image">
+                                <i class="el-icon-picture" style="margin-right: 10px;"></i>图片
+                              </el-dropdown-item>
+                              <el-dropdown-item command="link">
+                                <i class="el-icon-link" style="margin-right: 10px;"></i>链接
+                              </el-dropdown-item>
+                              <el-dropdown-item command="miniprogram">
+                                <i class="el-icon-link" style="margin-right: 10px;"></i>小程序
+                              </el-dropdown-item>
+                            </el-dropdown-menu>
+
+                            <span class="el-dropdown-link">
+                            <el-link icon="el-icon-paperclip" type="text" style="color: rgb(24, 144, 255)">
+                              添加附件(最多9个)
+                            </el-link>
+                            </span>
+                          </el-dropdown>
+                        </el-form-item>
+                      </el-form>
+                    </div>
+                  </el-col>
+                  <el-col style="width: 15px;">
+                    <el-link v-if="form.daypartingItemlist.length>1" icon="el-icon-delete-solid" @click="delItemList(index)" type="text" style="color: rgb(24, 144, 255);margin-top: 350px" ></el-link>
+                  </el-col>
+                </el-row>
+            </el-form-item>
+          </div>
+          <div  v-if="form.isDayparting == '1'" style="margin-left: 10%">
+            <el-link type="primary" class="el-icon-plus" :underline="false" @click='addItemList()'>添加其他分时段欢迎语</el-link>
+          </div>
+
+        </el-form>
+      </div>
+
+      <div slot="footer" class="dialog-footer" style="text-align: center">
+        <el-button type="primary"  @click="submitForm">确定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 选择成员账号弹窗   -->
+    <el-dialog :title="listUser.title" :visible.sync="listUser.open" width="1600px"  append-to-body>
+      <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
+    </el-dialog>
+
+    <el-dialog :title="welcomeItem.title" :visible.sync="welcomeItem.open" style="width: 1300px;height: 100%" append-to-body>
+      <el-form ref="fileFrom" :model="fileFrom" :rules="fuleRules" label-width="110px">
+        <div v-if="welcomeItem.type==='image'">
+          <el-form-item label="图片:" prop="imagePicUrl">
+            <ImageUpload v-model="fileFrom.imagePicUrl"  type="image" :num="10" :width="150" :height="150"  disabled/>
+          </el-form-item>
+        </div>
+        <div v-if="welcomeItem.type==='link'">
+
+          <el-form-item label="选择课程">
+            <el-select  v-model="fileFrom.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(fileFrom,welcomeItem.index,welcomeItem.itemIndex)">
+              <el-option
+                v-for="dict in courseList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+            <el-select  v-model="fileFrom.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(fileFrom, welcomeItem.index, welcomeItem.itemIndex,welcomeItem.type)" >
+              <el-option
+                v-for="dict in videoList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="图文标题:" prop="linkTitle">
+            <el-input v-model="fileFrom.linkTitle" :rows="2" maxlength="42"  show-word-limit placeholder="请输入图文消息标题,最长为42字" />
+          </el-form-item>
+          <el-form-item label="图文封面:" prop="linkPicUrl">
+            <ImageUpload v-model="fileFrom.linkPicUrl"  type="image" :num="10" :width="150" :height="150" />
+          </el-form-item>
+          <el-form-item label="图文的描述:" prop="linkDesc">
+            <el-input v-model="fileFrom.linkDesc" :rows="4" maxlength="170" show-word-limit type="textarea" placeholder="请输入内容,,最长为170字" />
+          </el-form-item>
+          <div v-if="fileFrom.videoId==null" style="margin-top: 1%">
+            <el-form-item label="图文链接:"  label-width="100px" >
+              <el-input v-model="fileFrom.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+            </el-form-item>
+          </div>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="图文链接:"  label-width="100px" >
+              <el-tag type="warning" v-model="fileFrom.linkUrl='待生成'">选择的课程小节 即为卡片链接地址</el-tag>
+            </el-form-item>
+          </div>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="课节过期时间" style="margin-top: 1%" required label-width="110px">
+              <el-row>
+                <el-input-number  v-model="fileFrom.expiresDays"  :min="1" :max="9999" ></el-input-number>
+                (天)
+              </el-row>
+              <el-row>
+                <span class="tip">默认为30天</span>
+              </el-row>
+            </el-form-item>
+          </div>
+<!--          <el-form-item label="图文链接:" prop="linkUrl">-->
+<!--            <el-input v-model="fileFrom.linkUrl" :rows="2"  placeholder="选择了课程小节会自动设置地址" />-->
+<!--          </el-form-item>-->
+        </div>
+        <div v-if="welcomeItem.type==='miniprogram'">
+
+          <el-form-item label="选择课程">
+            <el-select  v-model="fileFrom.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(fileFrom,welcomeItem.index,welcomeItem.itemIndex)">
+              <el-option
+                v-for="dict in courseList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+            <el-select  v-model="fileFrom.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(fileFrom, welcomeItem.index, welcomeItem.itemIndex,welcomeItem.type)" >
+              <el-option
+                v-for="dict in videoList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+          </el-form-item>
+
+          <el-form-item label="小程序标题:" prop="miniprogramTitle">
+            <el-input v-model="fileFrom.miniprogramTitle" :rows="2" maxlength="64" placeholder="请输入小程序消息标题,最长为64字节" @input="checkByteLength(fileFrom)" />
+          </el-form-item>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="小程序链接:"  label-width="100px" >
+              <el-tag type="warning" v-model="fileFrom.miniprogramPage='待生成'">选择的课程小节 即为卡片小程序链接地址</el-tag>
+            </el-form-item>
+          </div>
+          <el-form-item label="appid" prop="miniprogramAppid" v-show="false" >
+            <el-input v-model="fileFrom.miniprogramAppid='wx73f85f8d62769119' " disabled />
+          </el-form-item>
+
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="课节过期时间" style="margin-top: 1%" required label-width="110px">
+              <el-row>
+                <el-input-number  v-model="fileFrom.expiresDays"  :min="1" :max="9999" ></el-input-number>
+                (天)
+              </el-row>
+              <el-row>
+                <span class="tip">默认为30天</span>
+              </el-row>
+            </el-form-item>
+          </div>
+          <!--          <el-form-item label="图文链接:" prop="linkUrl">-->
+          <!--            <el-input v-model="fileFrom.linkUrl" :rows="2"  placeholder="选择了课程小节会自动设置地址" />-->
+          <!--          </el-form-item>-->
+        </div>
+      </el-form>
+      <div slot="footer" class="dialog-footer" style="text-align: center">
+        <el-button type="primary" @click="confirmUpload">确定</el-button>
+        <el-button type="primary" @click="cancelUpload">取消</el-button>
+      </div>
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import { deptListFriendWelcome,listFriendWelcome, getFriendWelcome, delFriendWelcome, addFriendWelcome, updateFriendWelcome, exportFriendWelcome } from "@/api/qw/friendWelcome";
+import qwUserList from '@/views/qw/user/qwUserList.vue'
+import ImageUploadWeclome from '@/views/qw/friendWelcome/ImageUploadWeclome.vue'
+import ImageUpload from '@/views/qw/material/ImageUpload.vue'
+import { getQwAllUserList } from '@/api/company/companyUser'
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+import {courseList, videoList} from "@/api/qw/sop";
+export default {
+  name: "deptFriendWelcome",
+  components: { ImageUpload, qwUserList,ImageUploadWeclome},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      myQwCompanyList:[],
+      //选择成员列表
+      listUser:{
+        title:"",
+        open:false
+      },
+      //选择成员列表
+      userSelectList:[],
+      fileFrom:{
+        imagePicUrl:null,
+        linkTitle:null,
+        linkPicUrl:null,
+        linkDesc:null,
+        linkUrl:null,
+        videoId:null,
+        courseId:null,
+        expiresDays:30,
+        miniprogramTitle:null,
+        miniprogramPage:null,
+        miniprogramPicUrl:null,
+        miniprogramAppid:null,
+      },
+      courseList:[],
+      videoList:[],
+      fuleRules:{
+        imagePicUrl:[ { required: true, message: "图片不能为空", trigger: "submit" }],
+        linkTitle:[ { required: true, message: "图文标题不能为空", trigger: "submit" }],
+        linkUrl:[ { required: true, message: "图文链接不能为空", trigger: "submit" }],
+        miniprogramTitle:[ { required: true, message: "图文链接不能为空", trigger: "submit" }],
+      },
+
+
+      weekOptions: [{
+        value: 1,
+        label: '星期一'
+      }, {
+        value: 2,
+        label: '星期二'
+      }, {
+        value: 3,
+        label: '星期三'
+      }, {
+        value: 4,
+        label: '星期四'
+      }, {
+        value: 5,
+        label: '星期五'
+      }
+        , {
+          value: 6,
+          label: '星期六'
+        }
+        , {
+          value: 7,
+          label: '星期天'
+        }],
+
+      welcomeItem:{
+        open: false,
+        title: '',
+        type: '',
+        index: -1,
+        itemIndex: -1
+      },
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      //是否
+      allowSelectOptions:[],
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 好友欢迎语表格数据
+      friendWelcomeList: [],
+      //账号列表
+      companyUserList:[],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        qwUserIds: [],
+        companyId: null,
+        welcomeText: null,
+        isDayparting: null,
+        createTime: null,
+        updateTime: null,
+        isSendMsg: null,
+        corpId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        qwUserIds: [
+          { required: true, message: "发送企业群发消息的成员账号不能为空", trigger: "submit" }
+        ],
+        welcomeText:[
+          { required: true, message: "消息内容不能为空噢", trigger: "submit" }
+        ],
+
+      },
+      itemRules: {
+        week: [
+          { required: true, message: '请选择发起时间的星期', trigger: 'submit' }
+        ],
+        startTime: [
+          { required: true, message: '请选择开始时间', trigger: 'submit' },
+        ],
+        endTime: [
+          { required: true, message: '请选择结束时间', trigger: 'submit' },
+
+        ],
+        welcomeText: [
+          { required: true, message: '消息内容不能为空噢', trigger: 'submit' }
+        ]
+      }
+
+    };
+  },
+  created() {
+    //账号列表
+
+
+    //是否允许发送
+    this.getDicts("sys_qw_allow_select").then(response => {
+      this.allowSelectOptions = response.data;
+    });
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+    getMyQwCompanyList().then(response => {
+
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              getQwAllUserList(this.myQwCompanyList[0].dictValue).then(response => {
+                this.companyUserList = response.data;
+              });
+              this.getList();
+            }
+    });
+  },
+  watch:{
+    userSelectList(newList) {
+      this.form.qwUserIds =newList.map(item =>item.id);
+    }
+  },
+  methods: {
+    updateCorpId(){
+        this.getList();
+        getQwAllUserList(this.queryParams.corpId).then(response => {
+        this.companyUserList = response.data;
+      });
+     },
+    /** 查询好友欢迎语列表 */
+    getList() {
+      this.loading = true;
+
+      deptListFriendWelcome(this.queryParams).then(response => {
+
+        // 将处理后的数据赋值给 friendWelcomeList
+        this.friendWelcomeList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    // // 检查字节长度
+    checkByteLength(fileFrom) {
+      const text = fileFrom.miniprogramTitle;
+      const byteLength = this.getByteLength(text); // 获取当前字节数
+
+      // 如果字节数超过64,截断输入内容
+      if (byteLength > 64) {
+        this.$set(fileFrom, 'miniprogramTitle', this.truncateTextByByteLength(text,64));
+      }
+    },
+
+    // 计算字符串的字节数
+    getByteLength(text) {
+      return new Blob([text]).size; // 使用 Blob 计算字节数
+    },
+
+    // 根据字节数截断字符串
+    truncateTextByByteLength(text, maxByteLength) {
+      let byteLength = 0;
+      let result = "";
+
+      for (let i = 0; i < text.length; i++) {
+        const char = text[i];
+        const charByteLength = this.getByteLength(char); // 获取当前字符的字节数
+
+        // 如果加上当前字符的字节数后不超过限制,则添加到结果中
+        if (byteLength + charByteLength <= maxByteLength) {
+          result += char;
+          byteLength += charByteLength;
+        } else {
+          break; // 超过限制时停止
+        }
+      }
+
+      return result;
+    },
+
+    //选择群发的企业成员账号
+    handlelistUser(){
+      setTimeout(() => {
+        this.$refs.QwUserList.getDetails(this.queryParams.corpId);
+      }, 1);
+      this.listUser.title="选择企业成员"
+      this.listUser.open=true;
+    },
+    //选择的成员账号列表
+    selectUserList(list){
+
+      this.listUser.open=false;
+
+      // 3. 遍历要添加的 list,逐条判断是否存在重复
+      list.forEach(newItem => {
+        // some() 判断是否存在相同 id
+        const isExist = this.userSelectList.some(oldItem => oldItem.id === newItem.id);
+        if (!isExist) {
+          // 不存在重复的,才添加
+          this.userSelectList.push(newItem);
+        }
+      });
+      // //用于显示
+      // this.userSelectList=list;
+
+    },
+    //删除一些选择了的账号
+    handleClosegroupUser(list){
+
+      // 假设 list 对象具有一个 id 属性
+      const index = this.userSelectList.findIndex(t => t.id === list.id);
+      if (index !== -1) {
+        this.userSelectList.splice(index,1);
+      }
+    },
+    //附件选择
+    handleCommand(command,itemIndex){
+
+      if (this.form.attachments.length >=9 && itemIndex===-1) {
+        return this.$message.error('附件数量已达上限,无法添加更多附件');
+      }
+
+      if (this.isDayparting==='1' && this.form.daypartingItemlist[itemIndex].attachments.length>=9){
+        return this.$message.error('附件数量已达上限,无法添加更多附件');
+      }
+
+      this.welcomeItem = {
+        open: true,
+        title: this.getTitleByCommand(command),
+        type: command,
+        index: itemIndex === -1 ? this.form.attachments.length : this.form.daypartingItemlist[itemIndex].attachments.length,
+        itemIndex
+      };
+    },
+
+    getTitleByCommand(command) {
+      switch (command) {
+        case 'image':
+          return '添加图片';
+        case 'link':
+          return '添加链接';
+        case 'miniprogram':
+          return '添加小程序';
+      }
+    },
+
+
+    courseChange(fileFrom,index,itemIndex){
+
+      // 清空 videoId 选择
+      this.$set(fileFrom, 'videoId', null);
+      // 清空 videoList
+      this.videoList = [];
+
+      if (fileFrom.courseId != null) {
+        // 查找选中的课程对应的 label 和 dictImgUrl
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === fileFrom.courseId);
+
+        if (selectedCourse) {
+          // 设置 linkTitle 和 linkImageUrl
+          this.$set(fileFrom, 'linkTitle', selectedCourse.dictLabel);
+          this.$set(fileFrom, 'linkPicUrl', selectedCourse.dictImgUrl);
+        }
+
+
+        // 获取新的 videoList
+        videoList(fileFrom.courseId).then(response => {
+          this.videoList = response.list;
+        });
+
+      }
+      //
+      // // 更新对应的数据层级
+      // if (itemIndex === -1) {
+      //   // 更新 form.attachments
+      //   this.$set(this.form.attachments, index, {
+      //     ...this.form.attachments[index],
+      //     courseId: fileFrom.courseId,
+      //     videoId: null, // 因为已清空
+      //     title: fileFrom.linkTitle,
+      //     picurl: fileFrom.linkPicUrl
+      //   });
+      // } else {
+      //
+      //   this.$set(this.form.daypartingItemlist[itemIndex].attachments, index, {
+      //     ...this.form.daypartingItemlist[itemIndex].attachments[index],
+      //     courseId: fileFrom.courseId,
+      //     videoId: null, // 因为已清空
+      //     title: fileFrom.linkTitle,
+      //     picurl: fileFrom.linkPicUrl
+      //   });
+      // }
+    },
+    videoIdChange(fileFrom,index, itemIndex,type){
+      //选择了课程小节则 默认绑上
+      if (fileFrom.videoId != null) {
+        // 根据 videoId 获取相关信息(假设有相关的 API 调用)
+        let  selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === fileFrom.videoId);
+        if (selectedVideo && type==='link') {
+          this.$set(fileFrom, 'linkDesc', selectedVideo.dictLabel);
+          this.$set(fileFrom, 'expiresDays', 30);
+        }
+        if (selectedVideo && type==='miniprogram') {
+          this.$set(fileFrom, 'miniprogramTitle', this.truncateTextByByteLength(selectedVideo.dictLabel,64));
+          this.$set(fileFrom, 'expiresDays', 30);
+        }
+
+      }
+
+      // // 更新对应的数据层级
+      // if (itemIndex === -1) {
+      //   // 更新 form.attachments
+      //   this.$set(this.form.attachments, index, {
+      //     ...this.form.attachments[index],
+      //     videoId: fileFrom.videoId,
+      //     desc: fileFrom.linkDesc,
+      //   });
+      // } else {
+      //   // 更新 form.daypartingItemlist[itemIndex].attachments
+      //   this.$set(this.form.daypartingItemlist[itemIndex].attachments, index, {
+      //     ...this.form.daypartingItemlist[itemIndex].attachments[index],
+      //     videoId: fileFrom.videoId,
+      //     desc: fileFrom.linkDesc,
+      //   });
+      //
+      // }
+
+    },
+    //修改附件
+    editFileItem(item, index, itemIndex){
+
+      this.welcomeItem = {
+        open: true,
+        title: this.getEditTitleByMsgType(item.msgtype),
+        type: item.msgtype,
+        index,
+        itemIndex
+      };
+      if (item.msgtype === 'image') {
+        this.fileFrom.imagePicUrl = item.image.pic_url;
+      } else if (item.msgtype === 'link') {
+        this.fileFrom.linkTitle = item.link.title;
+        this.fileFrom.linkPicUrl = item.link.picurl;
+        this.fileFrom.linkDesc = item.link.desc;
+        this.fileFrom.linkUrl = item.link.url;
+        this.fileFrom.videoId = item.link.videoId;
+        this.fileFrom.courseId = item.link.courseId;
+        this.fileFrom.expiresDays = item.link.expiresDays;
+
+        videoList(item.link.courseId).then(response => {
+          this.videoList = response.list;
+        });
+
+
+      }else if (item.msgtype === 'miniprogram') {
+        this.fileFrom.miniprogramAppid = 'wx73f85f8d62769119';
+        this.fileFrom.miniprogramTitle = item.miniprogram.title;
+        this.fileFrom.miniprogramPicUrl = "待生成";
+        this.fileFrom.miniprogramPage = "待生成";
+        this.fileFrom.videoId = item.miniprogram.videoId;
+        this.fileFrom.courseId = item.miniprogram.courseId;
+        this.fileFrom.expiresDays = item.miniprogram.expiresDays;
+
+        videoList(item.miniprogram.courseId).then(response => {
+          this.videoList = response.list;
+        });
+
+      }
+
+
+    },
+
+
+
+    getEditTitleByMsgType(msgType) {
+      switch (msgType) {
+        case 'image':
+          return '编辑图片';
+        case 'link':
+          return '编辑链接';
+        case 'miniprogram':
+          return '编辑小程序';
+      }
+    },
+
+    //删除附件
+    removeFileItem(data,index, itemIndex) {
+
+      if (itemIndex === -1) {
+        this.form.attachments.splice(index, 1);
+      } else {
+        this.form.daypartingItemlist[itemIndex].attachments.splice(index, 1);
+      }
+    },
+
+    //添加分时段欢迎语
+    addItemList(){
+      this.form.daypartingItemlist.push({welcomeText:null,attachments:[],week:[1,2,3,4,5,6,7],startTime:null,endTime:null})
+    },
+
+
+    //删除某一个分时段欢迎语
+    delItemList(index){
+      this.form.daypartingItemlist.splice(index,1)
+    },
+
+    //提交附件
+    confirmUpload(fileFrom) {
+
+      const { type, index, itemIndex } = this.welcomeItem;
+      let attachment = {};
+      if (type === 'image') {
+        attachment = {
+          msgtype: 'image',
+          image: {
+            pic_url: this.fileFrom.imagePicUrl
+          }
+        };
+      } else if (type === 'link') {
+        attachment = {
+          msgtype: 'link',
+          link: {
+            title: this.fileFrom.linkTitle,
+            picurl: this.fileFrom.linkPicUrl,
+            desc: this.fileFrom.linkDesc,
+            url: this.fileFrom.linkUrl,
+            courseId:this.fileFrom.courseId,
+            videoId:this.fileFrom.videoId,
+            expiresDays:this.fileFrom.expiresDays,
+          }
+        };
+      }else if (type==='miniprogram'){
+        attachment = {
+          msgtype: 'miniprogram',
+          miniprogram: {
+            title: this.fileFrom.miniprogramTitle,
+            pic_media_id: "待查询",
+            appid: "wx73f85f8d62769119",
+            page: this.fileFrom.miniprogramPage,
+            courseId:this.fileFrom.courseId,
+            videoId:this.fileFrom.videoId,
+            expiresDays:this.fileFrom.expiresDays,
+          }
+        };
+      }
+
+      if (itemIndex === -1) {
+        // 默认欢迎语附件处理
+        if (index < this.form.attachments.length) {
+          // 存在附件则更新
+          this.form.attachments.splice(index, 1, attachment);
+        } else {
+          // 不存在附件则插入
+          this.form.attachments.push(attachment);
+        }
+
+      } else {
+        // 分时段欢迎语附件处理
+        if (index < this.form.daypartingItemlist[itemIndex].attachments.length) {
+          // 存在附件则更新
+          this.form.daypartingItemlist[itemIndex].attachments.splice(index, 1, attachment);
+        } else {
+          // 不存在附件则插入
+          this.form.daypartingItemlist[itemIndex].attachments.push(attachment);
+        }
+      }
+
+      this.resetFileFrom();
+    },
+
+    //取消附件
+    cancelUpload() {
+      this.resetFileFrom();
+      this.welcomeItem.open = false;
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    //重置附件表单
+    resetFileFrom() {
+      this.fileFrom = {
+        imagePicUrl: null,
+        linkTitle: null,
+        linkPicUrl: null,
+        linkDesc: null,
+        linkUrl: null,
+        videoId:null,
+        courseId:null,
+        miniprogramTitle:null,
+        miniprogramPage:null,
+        miniprogramPicUrl:null,
+        miniprogramAppid:null,
+      };
+
+      this.welcomeItem={
+        open: false,
+        title: '',
+        type: '',
+        index: -1,
+        itemIndex: -1
+      } // 重置编辑索引
+
+      this.videoList=[];
+    },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        qwUserIds: [],
+        companyId: null,
+        //默认欢迎语附件
+        attachments: [],
+        createdTime: null,
+        updateTieme: null,
+        isSendMsg:'1',
+        welcomeText: '',
+        isDayparting: '2',
+        //分时段欢迎语
+        daypartingItemlist:[{welcomeText:null,attachments:[],week:[1,2,3,4,5,6,7],startTime:null,endTime:null}],
+      };
+      this.userSelectList=[]
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      getQwAllUserList(this.queryParams.corpId).then(response => {
+        this.companyUserList = response.data;
+      });
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.title = "新建好友欢迎语";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const id = row.id || this.ids
+      getFriendWelcome(id).then(response => {
+
+
+        let data = response.row;
+
+        // 转换 attachments
+        if (typeof data.attachments === 'string') {
+          data.attachments = JSON.parse(data.attachments);
+        }
+
+        // 转换 daypartingItemlist
+        if (typeof data.daypartingItemlist === 'string') {
+          data.daypartingItemlist = JSON.parse(data.daypartingItemlist);
+        }
+
+        // 转换 daypartingItemlist 中的每个项目的 attachments 和 week
+        if (Array.isArray(data.daypartingItemlist)) {
+          data.daypartingItemlist = data.daypartingItemlist.map(item => {
+            return {
+              ...item,
+              attachments: typeof item.attachments === 'string' ? JSON.parse(item.attachments) : item.attachments,
+              week: typeof item.week === 'string' ? JSON.parse(item.week) : item.week
+            };
+          });
+        }
+
+        // 赋值给表单
+        this.form = data;
+        this.userSelectList=this.form.userSelectList
+
+        this.open = true;
+        this.title = "修改好友欢迎语";
+      });
+    },
+    /** 提交按钮 验证 from表单*/
+    submitForm() {
+
+      this.loading=true;
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          this.form.corpId=this.queryParams.corpId;
+          //有分时段欢迎语 验证分时段表单
+          if (this.form.isDayparting==='1'){
+            const itemForms = this.$refs.friendWelcomeItemForm;
+            if (Array.isArray(itemForms)) {
+              let allValid = true;
+
+              itemForms.forEach((itemFormRef, index) => {
+                itemFormRef.validate((itemValid) => {
+                  if (!itemValid) {
+                    this.loading=false;
+                    allValid = false;
+                  } else {
+                    const { startTime, endTime } = itemFormRef.model;
+                    if (startTime && endTime && startTime > endTime) {
+                      this.$message.error(`时段 ${index + 1} 的开始时间不能大于结束时间`);
+                      allValid = false;
+                      this.loading=false;
+
+                    }
+                  }
+
+                  if (index === itemForms.length - 1 && allValid) {
+                    //全表单验证通过
+                    this.commitForm();
+                  }
+                });
+              });
+            }
+          }else {
+            //只有默认的
+            this.commitForm();
+
+          }
+
+        }
+      });
+    },
+
+    //提交form表单
+    commitForm(){
+
+      // 深拷贝表单数据
+      const requestData = { ...this.form };
+      // 将 `form.attachments` 转为 JSON 字符串
+      requestData.attachments = JSON.stringify(this.form.attachments);
+      requestData.qwUserIds = JSON.stringify(this.form.qwUserIds);
+
+      // 遍历 `daypartingItemlist`,将其中的 `attachments` 和 `week` 转为 JSON 字符串
+      requestData.daypartingItemlist = JSON.stringify(requestData.daypartingItemlist.map(item => ({
+          ...item,
+        }))
+      );
+
+
+      if (this.form.id != null) {
+        updateFriendWelcome(requestData).then(response => {
+          this.msgSuccess("修改成功");
+          this.loading=false;
+          this.open = false;
+          this.getList();
+        }).catch(error => {
+          this.open=false;
+          this.loading = false;
+        });
+      } else {
+        addFriendWelcome(requestData).then(response => {
+          this.msgSuccess("新增成功");
+          this.loading=false;
+          this.open = false;
+          this.getList();
+        }).catch(error => {
+          this.open=false;
+          this.loading = false;
+        });
+      }
+
+
+      // 重置表单 无论成功还是失败-都重置表单
+      this.reset();
+    },
+
+    //处理时间
+    // formatTime(date) {
+    //   const hours = date.getHours().toString().padStart(2, '0');
+    //   const minutes = date.getMinutes().toString().padStart(2, '0');
+    //   const seconds = date.getSeconds().toString().padStart(2, '0');
+    //   return `${hours}:${minutes}:${seconds}`;
+    // },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除好友欢迎语编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delFriendWelcome(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有好友欢迎语数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportFriendWelcome(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    }
+  }
+};
+</script>
+
+<style>
+.text-container {
+  max-height: 7.5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+</style>

+ 5 - 5
src/views/qw/friendWelcome/indexNew.vue

@@ -350,9 +350,9 @@
                               <el-dropdown-item command="link">
                                 <i class="el-icon-link" style="margin-right: 10px;"></i>链接
                               </el-dropdown-item>
-<!--                              <el-dropdown-item command="miniprogram">-->
-<!--                                <i class="el-icon-link" style="margin-right: 10px;"></i>小程序-->
-<!--                              </el-dropdown-item>-->
+                              <el-dropdown-item command="miniprogram">
+                                <i class="el-icon-link" style="margin-right: 10px;"></i>小程序
+                              </el-dropdown-item>
                             </el-dropdown-menu>
 
                             <span class="el-dropdown-link">
@@ -385,11 +385,11 @@
     </el-dialog>
 
     <!-- 选择成员账号弹窗   -->
-    <el-dialog :title="listUser.title" :visible.sync="listUser.open" style="width: 1600px;height: 100%" append-to-body>
+    <el-dialog :title="listUser.title" :visible.sync="listUser.open" width="1600px" append-to-body>
       <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
     </el-dialog>
 
-    <el-dialog :title="welcomeItem.title" :visible.sync="welcomeItem.open" style="width: 1300px;height: 100%" append-to-body>
+    <el-dialog :title="welcomeItem.title" :visible.sync="welcomeItem.open" width="1300px" append-to-body>
       <el-form ref="fileFrom" :model="fileFrom" :rules="fuleRules" label-width="110px">
         <div v-if="welcomeItem.type==='image'">
           <el-form-item label="图片:" prop="imagePicUrl">

+ 241 - 35
src/views/qw/friendWelcome/index.vue → src/views/qw/friendWelcome/myWelcome.vue

@@ -96,7 +96,7 @@
       </el-table-column>
       <el-table-column label="创建时间" align="center" prop="createTime" width="180"/>
       <el-table-column label="更新时间" align="center" prop="updateTime" width="180"/>
-      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
         <template slot-scope="scope">
           <el-button
             size="mini"
@@ -131,9 +131,8 @@
           <span style="font-size: 15px">基础信息</span>
           <el-divider></el-divider>
           <el-alert
-            title="注意事项"
-            type="warning"
-            description="这是提示~根据特色还不知道写点啥"
+            title="同一个销售的同一个企业微信账号,如果创建了多条记录,则以最新创建的那一条为准!!"
+            type="info"
             :closable="false"
             show-icon>
           </el-alert>
@@ -186,7 +185,7 @@
                   <div slot="header" style="display: flex;justify-content: space-between;align-items: center; ">
                     <div style="flex: 1;">
                     <span v-if="item.msgtype === 'image'">【图片】: {{ item.image.pic_url }}</span>
-                    <span v-if="item.msgtype === 'link'">【链接】: {{ item.link.title }}</span>
+                    <span v-if="item.msgtype === 'link'">【链接】: {{ item.link.title }}-{{item.link.desc}}</span>
                     <span v-if="item.msgtype === 'miniprogram'">【小程序】: {{ item.miniprogram.title }}</span>
                     </div>
                     <div style="  display: flex;gap: 10px;">
@@ -264,7 +263,7 @@
                 <el-row>
                   <el-col style="width: 965px">
                     <div style="background-color: #fbfbfb;padding: 10px;  border: 1px solid #e6e6e6; margin-bottom: 20px;">
-                                        <el-form ref="friendWelcomeItemForm"  :rules="itemRules" :model="item">
+                          <el-form ref="friendWelcomeItemForm"  :rules="itemRules" :model="item">
                           <div style="display: flex; gap: 10px;">
                             <el-form-item label="发起时间:" prop="week" style="flex: 8;">
                               <el-select v-model="item.week" remote multiple placeholder="请选择" filterable style="width: 580px">
@@ -310,7 +309,7 @@
                                 <div slot="header" style="  display: flex;justify-content: space-between;align-items: center; ">
                                   <div style="flex: 1;">
                                     <span v-if="attachment.msgtype === 'image'">【图片】: {{ attachment.image.pic_url }}</span>
-                                    <span v-if="attachment.msgtype === 'link'">【链接】: {{ attachment.link.title }}</span>
+                                    <span v-if="attachment.msgtype === 'link'">【链接】: {{ attachment.link.title }}-{{attachment.link.desc}}</span>
                                     <span v-if="attachment.msgtype === 'miniprogram'">【小程序】: {{ attachment.miniprogram.title }}</span>
                                   </div>
                                   <div style="  display: flex;gap: 10px;">
@@ -377,7 +376,7 @@
     </el-dialog>
 
     <!-- 选择成员账号弹窗   -->
-    <el-dialog :title="listUser.title" :visible.sync="listUser.open" style="width: 1600px;height: 100%" append-to-body>
+    <el-dialog :title="listUser.title" :visible.sync="listUser.open" width="1600px"  append-to-body>
       <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
     </el-dialog>
 
@@ -390,6 +389,25 @@
         </div>
         <div v-if="welcomeItem.type==='link'">
 
+          <el-form-item label="选择课程">
+            <el-select  v-model="fileFrom.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(fileFrom,welcomeItem.index,welcomeItem.itemIndex)">
+              <el-option
+                v-for="dict in courseList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+            <el-select  v-model="fileFrom.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(fileFrom, welcomeItem.index, welcomeItem.itemIndex,welcomeItem.type)" >
+              <el-option
+                v-for="dict in videoList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+          </el-form-item>
+
           <el-form-item label="图文标题:" prop="linkTitle">
             <el-input v-model="fileFrom.linkTitle" :rows="2" maxlength="42"  show-word-limit placeholder="请输入图文消息标题,最长为42字" />
           </el-form-item>
@@ -399,19 +417,80 @@
           <el-form-item label="图文的描述:" prop="linkDesc">
             <el-input v-model="fileFrom.linkDesc" :rows="4" maxlength="170" show-word-limit type="textarea" placeholder="请输入内容,,最长为170字" />
           </el-form-item>
-          <el-form-item label="图文链接:" prop="linkUrl">
-            <el-input v-model="fileFrom.linkUrl" :rows="2"  placeholder="请输入图文链接" />
-          </el-form-item>
+          <div v-if="fileFrom.videoId==null" style="margin-top: 1%">
+            <el-form-item label="图文链接:"  label-width="100px" >
+              <el-input v-model="fileFrom.linkUrl" placeholder="请输入链接地址" style="width: 90%;"/>
+            </el-form-item>
+          </div>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="图文链接:"  label-width="100px" >
+              <el-tag type="warning" v-model="fileFrom.linkUrl='待生成'">选择的课程小节 即为卡片链接地址</el-tag>
+            </el-form-item>
+          </div>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="课节过期时间" style="margin-top: 1%" required label-width="110px">
+              <el-row>
+                <el-input-number  v-model="fileFrom.expiresDays"  :min="1" :max="9999" ></el-input-number>
+                (天)
+              </el-row>
+              <el-row>
+                <span class="tip">默认为30天</span>
+              </el-row>
+            </el-form-item>
+          </div>
+<!--          <el-form-item label="图文链接:" prop="linkUrl">-->
+<!--            <el-input v-model="fileFrom.linkUrl" :rows="2"  placeholder="选择了课程小节会自动设置地址" />-->
+<!--          </el-form-item>-->
         </div>
         <div v-if="welcomeItem.type==='miniprogram'">
 
-          <el-form-item label="小程序标题:" prop="miniprogramTitle" >
-            <el-input v-model="fileFrom.miniprogramTitle" :rows="2" maxlength="64"  show-word-limit placeholder="请输入小程序消息标题,最长为64字" />
+          <el-form-item label="选择课程">
+            <el-select  v-model="fileFrom.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange(fileFrom,welcomeItem.index,welcomeItem.itemIndex)">
+              <el-option
+                v-for="dict in courseList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
+            <el-select  v-model="fileFrom.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange(fileFrom, welcomeItem.index, welcomeItem.itemIndex,welcomeItem.type)" >
+              <el-option
+                v-for="dict in videoList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="parseInt(dict.dictValue)"
+              />
+            </el-select>
           </el-form-item>
-        </div>
 
-      </el-form>
+          <el-form-item label="小程序标题:" prop="miniprogramTitle">
+            <el-input v-model="fileFrom.miniprogramTitle" :rows="2" maxlength="64" placeholder="请输入小程序消息标题,最长为64字节" @input="checkByteLength(fileFrom)" />
+          </el-form-item>
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="小程序链接:"  label-width="100px" >
+              <el-tag type="warning" v-model="fileFrom.miniprogramPage='待生成'">选择的课程小节 即为卡片小程序链接地址</el-tag>
+            </el-form-item>
+          </div>
+          <el-form-item label="appid" prop="miniprogramAppid" v-show="false" >
+            <el-input v-model="fileFrom.miniprogramAppid='wx73f85f8d62769119' " disabled />
+          </el-form-item>
 
+          <div v-if="fileFrom.videoId!=null">
+            <el-form-item label="课节过期时间" style="margin-top: 1%" required label-width="110px">
+              <el-row>
+                <el-input-number  v-model="fileFrom.expiresDays"  :min="1" :max="9999" ></el-input-number>
+                (天)
+              </el-row>
+              <el-row>
+                <span class="tip">默认为30天</span>
+              </el-row>
+            </el-form-item>
+          </div>
+          <!--          <el-form-item label="图文链接:" prop="linkUrl">-->
+          <!--            <el-input v-model="fileFrom.linkUrl" :rows="2"  placeholder="选择了课程小节会自动设置地址" />-->
+          <!--          </el-form-item>-->
+        </div>
+      </el-form>
       <div slot="footer" class="dialog-footer" style="text-align: center">
         <el-button type="primary" @click="confirmUpload">确定</el-button>
         <el-button type="primary" @click="cancelUpload">取消</el-button>
@@ -422,7 +501,7 @@
 </template>
 
 <script>
-import { listFriendWelcome, getFriendWelcome, delFriendWelcome, addFriendWelcome, updateFriendWelcome, exportFriendWelcome } from "@/api/qw/friendWelcome";
+import { listMyFriendWelcome,listFriendWelcome, getFriendWelcome, delFriendWelcome, addFriendWelcome, updateFriendWelcome, exportFriendWelcome } from "@/api/qw/friendWelcome";
 import qwUserList from '@/views/qw/user/qwUserList.vue'
 import ImageUploadWeclome from '@/views/qw/friendWelcome/ImageUploadWeclome.vue'
 import ImageUpload from '@/views/qw/material/ImageUpload.vue'
@@ -430,7 +509,7 @@ import { getQwAllUserList } from '@/api/company/companyUser'
 import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
 import {courseList, videoList} from "@/api/qw/sop";
 export default {
-  name: "FriendWelcome",
+  name: "myWelcome",
   components: { ImageUpload, qwUserList,ImageUploadWeclome},
   data() {
     return {
@@ -460,6 +539,7 @@ export default {
         linkUrl:null,
         videoId:null,
         courseId:null,
+        expiresDays:30,
         miniprogramTitle:null,
         miniprogramPage:null,
         miniprogramPicUrl:null,
@@ -471,7 +551,7 @@ export default {
         imagePicUrl:[ { required: true, message: "图片不能为空", trigger: "submit" }],
         linkTitle:[ { required: true, message: "图文标题不能为空", trigger: "submit" }],
         linkUrl:[ { required: true, message: "图文链接不能为空", trigger: "submit" }],
-        miniprogramTitle:[ { required: true, message: "小程序标题不能为空", trigger: "submit" }],
+        miniprogramTitle:[ { required: true, message: "图文链接不能为空", trigger: "submit" }],
       },
 
 
@@ -603,7 +683,7 @@ export default {
     getList() {
       this.loading = true;
 
-      listFriendWelcome(this.queryParams).then(response => {
+      listMyFriendWelcome(this.queryParams).then(response => {
 
         // 将处理后的数据赋值给 friendWelcomeList
         this.friendWelcomeList = response.rows;
@@ -611,7 +691,42 @@ export default {
         this.loading = false;
       });
     },
+    // // 检查字节长度
+    checkByteLength(fileFrom) {
+      const text = fileFrom.miniprogramTitle;
+      const byteLength = this.getByteLength(text); // 获取当前字节数
+
+      // 如果字节数超过64,截断输入内容
+      if (byteLength > 64) {
+        this.$set(fileFrom, 'miniprogramTitle', this.truncateTextByByteLength(text,64));
+      }
+    },
+
+    // 计算字符串的字节数
+    getByteLength(text) {
+      return new Blob([text]).size; // 使用 Blob 计算字节数
+    },
 
+    // 根据字节数截断字符串
+    truncateTextByByteLength(text, maxByteLength) {
+      let byteLength = 0;
+      let result = "";
+
+      for (let i = 0; i < text.length; i++) {
+        const char = text[i];
+        const charByteLength = this.getByteLength(char); // 获取当前字符的字节数
+
+        // 如果加上当前字符的字节数后不超过限制,则添加到结果中
+        if (byteLength + charByteLength <= maxByteLength) {
+          result += char;
+          byteLength += charByteLength;
+        } else {
+          break; // 超过限制时停止
+        }
+      }
+
+      return result;
+    },
 
     //选择群发的企业成员账号
     handlelistUser(){
@@ -635,10 +750,9 @@ export default {
           this.userSelectList.push(newItem);
         }
       });
-
       // //用于显示
       // this.userSelectList=list;
-      //
+
     },
     //删除一些选择了的账号
     handleClosegroupUser(list){
@@ -670,16 +784,98 @@ export default {
     },
 
     getTitleByCommand(command) {
-        switch (command) {
-          case 'image':
-            return '添加图片';
-          case 'link':
-            return '添加链接';
-          case 'miniprogram':
-            return '添加小程序';
+      switch (command) {
+        case 'image':
+          return '添加图片';
+        case 'link':
+          return '添加链接';
+        case 'miniprogram':
+          return '添加小程序';
       }
     },
 
+
+    courseChange(fileFrom,index,itemIndex){
+
+      // 清空 videoId 选择
+      this.$set(fileFrom, 'videoId', null);
+      // 清空 videoList
+      this.videoList = [];
+
+      if (fileFrom.courseId != null) {
+        // 查找选中的课程对应的 label 和 dictImgUrl
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === fileFrom.courseId);
+
+        if (selectedCourse) {
+          // 设置 linkTitle 和 linkImageUrl
+          this.$set(fileFrom, 'linkTitle', selectedCourse.dictLabel);
+          this.$set(fileFrom, 'linkPicUrl', selectedCourse.dictImgUrl);
+        }
+
+
+        // 获取新的 videoList
+        videoList(fileFrom.courseId).then(response => {
+          this.videoList = response.list;
+        });
+
+      }
+      //
+      // // 更新对应的数据层级
+      // if (itemIndex === -1) {
+      //   // 更新 form.attachments
+      //   this.$set(this.form.attachments, index, {
+      //     ...this.form.attachments[index],
+      //     courseId: fileFrom.courseId,
+      //     videoId: null, // 因为已清空
+      //     title: fileFrom.linkTitle,
+      //     picurl: fileFrom.linkPicUrl
+      //   });
+      // } else {
+      //
+      //   this.$set(this.form.daypartingItemlist[itemIndex].attachments, index, {
+      //     ...this.form.daypartingItemlist[itemIndex].attachments[index],
+      //     courseId: fileFrom.courseId,
+      //     videoId: null, // 因为已清空
+      //     title: fileFrom.linkTitle,
+      //     picurl: fileFrom.linkPicUrl
+      //   });
+      // }
+    },
+    videoIdChange(fileFrom,index, itemIndex,type){
+      //选择了课程小节则 默认绑上
+      if (fileFrom.videoId != null) {
+        // 根据 videoId 获取相关信息(假设有相关的 API 调用)
+        let  selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === fileFrom.videoId);
+        if (selectedVideo && type==='link') {
+          this.$set(fileFrom, 'linkDesc', selectedVideo.dictLabel);
+          this.$set(fileFrom, 'expiresDays', 30);
+        }
+        if (selectedVideo && type==='miniprogram') {
+          this.$set(fileFrom, 'miniprogramTitle', this.truncateTextByByteLength(selectedVideo.dictLabel,64));
+          this.$set(fileFrom, 'expiresDays', 30);
+        }
+
+      }
+
+      // // 更新对应的数据层级
+      // if (itemIndex === -1) {
+      //   // 更新 form.attachments
+      //   this.$set(this.form.attachments, index, {
+      //     ...this.form.attachments[index],
+      //     videoId: fileFrom.videoId,
+      //     desc: fileFrom.linkDesc,
+      //   });
+      // } else {
+      //   // 更新 form.daypartingItemlist[itemIndex].attachments
+      //   this.$set(this.form.daypartingItemlist[itemIndex].attachments, index, {
+      //     ...this.form.daypartingItemlist[itemIndex].attachments[index],
+      //     videoId: fileFrom.videoId,
+      //     desc: fileFrom.linkDesc,
+      //   });
+      //
+      // }
+
+    },
     //修改附件
     editFileItem(item, index, itemIndex){
 
@@ -697,7 +893,6 @@ export default {
         this.fileFrom.linkPicUrl = item.link.picurl;
         this.fileFrom.linkDesc = item.link.desc;
         this.fileFrom.linkUrl = item.link.url;
-
         this.fileFrom.videoId = item.link.videoId;
         this.fileFrom.courseId = item.link.courseId;
         this.fileFrom.expiresDays = item.link.expiresDays;
@@ -706,6 +901,7 @@ export default {
           this.videoList = response.list;
         });
 
+
       }else if (item.msgtype === 'miniprogram') {
         this.fileFrom.miniprogramAppid = 'wx73f85f8d62769119';
         this.fileFrom.miniprogramTitle = item.miniprogram.title;
@@ -719,10 +915,13 @@ export default {
           this.videoList = response.list;
         });
 
-
       }
+
+
     },
 
+
+
     getEditTitleByMsgType(msgType) {
       switch (msgType) {
         case 'image':
@@ -734,7 +933,6 @@ export default {
       }
     },
 
-
     //删除附件
     removeFileItem(data,index, itemIndex) {
 
@@ -760,7 +958,6 @@ export default {
     confirmUpload(fileFrom) {
 
       const { type, index, itemIndex } = this.welcomeItem;
-
       let attachment = {};
       if (type === 'image') {
         attachment = {
@@ -776,7 +973,10 @@ export default {
             title: this.fileFrom.linkTitle,
             picurl: this.fileFrom.linkPicUrl,
             desc: this.fileFrom.linkDesc,
-            url: this.fileFrom.linkUrl
+            url: this.fileFrom.linkUrl,
+            courseId:this.fileFrom.courseId,
+            videoId:this.fileFrom.videoId,
+            expiresDays:this.fileFrom.expiresDays,
           }
         };
       }else if (type==='miniprogram'){
@@ -784,9 +984,12 @@ export default {
           msgtype: 'miniprogram',
           miniprogram: {
             title: this.fileFrom.miniprogramTitle,
-            pic_media_id: this.fileFrom.miniprogramPicUrl,
-            appid: this.fileFrom.miniprogramAppid,
+            pic_media_id: "待查询",
+            appid: "wx73f85f8d62769119",
             page: this.fileFrom.miniprogramPage,
+            courseId:this.fileFrom.courseId,
+            videoId:this.fileFrom.videoId,
+            expiresDays:this.fileFrom.expiresDays,
           }
         };
       }
@@ -800,6 +1003,7 @@ export default {
           // 不存在附件则插入
           this.form.attachments.push(attachment);
         }
+
       } else {
         // 分时段欢迎语附件处理
         if (index < this.form.daypartingItemlist[itemIndex].attachments.length) {
@@ -849,6 +1053,8 @@ export default {
         index: -1,
         itemIndex: -1
       } // 重置编辑索引
+
+      this.videoList=[];
     },
 
     // 表单重置

+ 260 - 0
src/views/qw/groupChat/deptGroupIndex.vue

@@ -0,0 +1,260 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="queryParams.corpId" placeholder="企微公司" size="small" @change="updateCorpId()">
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+      </el-form-item>
+      <el-form-item label="群名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入群名"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群主(后台)" prop="ownerName">
+        <el-input
+          v-model="queryParams.ownerName"
+          placeholder="请输入群主名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群主(企微)" prop="qwName">
+        <el-input
+          v-model="queryParams.qwName"
+          placeholder="请输入群主名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群公告" prop="notice">
+        <el-input
+          v-model="queryParams.notice"
+          placeholder="请输入群公告"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群跟进状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择客户群跟进状态" clearable size="small">
+          <el-option label="请选择状态"
+                     v-for="dict in groupChatStatusOptions"
+                      :label="dict.dictLabel"
+                      :key="dict.dictValue"
+                      :value="dict.dictValue"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+<!--          <el-button type="warning" icon="el-icon-magic-stick" size="mini" @click="getCogradientGroupChat">同步客户群</el-button>-->
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="groupChatList" border>
+      <el-table-column label="群名" align="center" prop="name" >
+        <template slot-scope="scope">
+          <i class="el-icon-s-comment"></i>
+          {{scope.row.name}}
+        </template>
+      </el-table-column>
+      <el-table-column label="群主(后台)" align="center" prop="ownerName" />
+      <el-table-column label="群主(企微)" align="center" prop="qwName" />
+      <el-table-column label="群公告" align="center" prop="notice" />
+      <el-table-column label="群跟进状态" align="center" prop="status" >
+        <template slot-scope="scope">
+          <span v-for="(item,index) in groupChatStatusOptions" v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="群人数" align="center" prop="groupSizeByChartUser"></el-table-column>
+      <el-table-column label="当日入群数" align="center" prop="todayJoinByChartUser"></el-table-column>
+      <el-table-column label="群创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-notebook-2"
+            @click="hangdleDitels(scope.row)"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-drawer :title="title" :visible.sync="open" width="100%" size="85%">
+      <group_chat_user ref="GroupChatUser" :rowDetailFrom="this.form"></group_chat_user>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import {
+  listGroupChatDeptList,
+  listGroupChat,
+  getGroupChat,
+  cogradientGroupChat
+} from '@/api/qw/groupChat'
+import Group_chat_user from '@/views/qw/groupChatUser/groupChatUser'
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "deptGroupIndex",
+  components: { Group_chat_user },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      myQwCompanyList:[],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 客户群详情表格数据
+      groupChatList: [],
+      //客户群跟进状态
+      groupChatStatusOptions:[],
+
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        owner: null,
+        ownerName: null,
+        qwName: null,
+        notice: null,
+        memberVersion: null,
+        status: null,
+        companyId: null,
+        corpId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+     });
+    //群跟进状态字典
+    this.getDicts("sys_qw_groupChat_status").then(response => {
+      this.groupChatStatusOptions = response.data;
+    });
+  },
+  methods: {
+
+     updateCorpId(){
+         this.getList();
+      },
+    /** 查询客户群列表 */
+    getList() {
+      this.loading = true;
+      listGroupChatDeptList(this.queryParams).then(response => {
+        this.groupChatList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    //同步客户群(暂用)
+    getCogradientGroupChat(){
+      this.loading = true;
+      cogradientGroupChat(this.queryParams.corpId).then(response=>{
+        if (response.code == 200) {
+          this.msgSuccess("同步成功");
+          this.getList();
+        } else {
+          this.msgError(response.msg);
+        }
+        this.loading = false;
+      })
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        chatId: null,
+        id: null,
+        name: null,
+        owner: null,
+        notice: null,
+        memberVersion: null,
+        status: "0",
+        companyId: null,
+        corpId: null,
+        createTime: null,
+        updateTime: null,
+        quitScene:null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    //详情
+    hangdleDitels(row){
+
+      this.reset();
+      this.open = true;
+      this.title = "客户群详情";
+      this.form=row;
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+
+  }
+};
+</script>

+ 261 - 0
src/views/qw/groupChat/myGroupChat.vue

@@ -0,0 +1,261 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="90px">
+      <el-form-item label="企微公司" prop="corpId">
+          <el-select v-model="queryParams.corpId" placeholder="企微公司" size="small" @change="updateCorpId()">
+            <el-option
+              v-for="dict in myQwCompanyList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="dict.dictValue"
+            />
+          </el-select>
+      </el-form-item>
+      <el-form-item label="群名" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入群名"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群主(后台)" prop="ownerName">
+        <el-input
+          v-model="queryParams.ownerName"
+          placeholder="请输入群主名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群主(企微)" prop="qwName">
+        <el-input
+          v-model="queryParams.qwName"
+          placeholder="请输入群主名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群公告" prop="notice">
+        <el-input
+          v-model="queryParams.notice"
+          placeholder="请输入群公告"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="群跟进状态" prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择客户群跟进状态" clearable size="small">
+          <el-option label="请选择状态"
+                     v-for="dict in groupChatStatusOptions"
+                      :label="dict.dictLabel"
+                      :key="dict.dictValue"
+                      :value="dict.dictValue"/>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+          <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+          <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+          <el-button type="warning" icon="el-icon-magic-stick" size="mini" @click="getCogradientGroupChat">同步我的客户群</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-table v-loading="loading" :data="groupChatList" border>
+      <el-table-column label="群名" align="center" prop="name" >
+        <template slot-scope="scope">
+          <i class="el-icon-s-comment"></i>
+          {{scope.row.name}}
+        </template>
+      </el-table-column>
+      <el-table-column label="群主(后台)" align="center" prop="ownerName" />
+      <el-table-column label="群主(企微)" align="center" prop="qwName" />
+      <el-table-column label="群公告" align="center" prop="notice" />
+      <el-table-column label="群跟进状态" align="center" prop="status" >
+        <template slot-scope="scope">
+          <span v-for="(item,index) in groupChatStatusOptions" v-if="scope.row.status==item.dictValue">{{item.dictLabel}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="群人数" align="center" prop="groupSizeByChartUser"></el-table-column>
+      <el-table-column label="当日入群数" align="center" prop="todayJoinByChartUser"></el-table-column>
+      <el-table-column label="群创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-notebook-2"
+            @click="hangdleDitels(scope.row)"
+          >详情</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <el-drawer :title="title" :visible.sync="open" width="100%" size="85%">
+      <group_chat_user ref="GroupChatUser" :rowDetailFrom="this.form"></group_chat_user>
+    </el-drawer>
+  </div>
+</template>
+
+<script>
+import {
+  cogradientMyGroupChat,
+  listGroupChatMyList,
+  listGroupChat,
+  getGroupChat,
+  cogradientGroupChat
+} from '@/api/qw/groupChat'
+import Group_chat_user from '@/views/qw/groupChatUser/groupChatUser'
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "myGroupChat",
+  components: { Group_chat_user },
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      myQwCompanyList:[],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 客户群详情表格数据
+      groupChatList: [],
+      //客户群跟进状态
+      groupChatStatusOptions:[],
+
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        name: null,
+        owner: null,
+        ownerName: null,
+        qwName: null,
+        notice: null,
+        memberVersion: null,
+        status: null,
+        companyId: null,
+        corpId: null,
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+     });
+    //群跟进状态字典
+    this.getDicts("sys_qw_groupChat_status").then(response => {
+      this.groupChatStatusOptions = response.data;
+    });
+  },
+  methods: {
+
+     updateCorpId(){
+         this.getList();
+      },
+    /** 查询客户群列表 */
+    getList() {
+      this.loading = true;
+      listGroupChatMyList(this.queryParams).then(response => {
+        this.groupChatList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+
+    //同步客户群(暂用)
+    getCogradientGroupChat(){
+      this.loading = true;
+      cogradientMyGroupChat(this.queryParams.corpId).then(response=>{
+        if (response.code == 200) {
+          this.msgSuccess("同步成功");
+          this.getList();
+        } else {
+          this.msgError(response.msg);
+        }
+        this.loading = false;
+      })
+    },
+
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        chatId: null,
+        id: null,
+        name: null,
+        owner: null,
+        notice: null,
+        memberVersion: null,
+        status: "0",
+        companyId: null,
+        corpId: null,
+        createTime: null,
+        updateTime: null,
+        quitScene:null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+
+    //详情
+    hangdleDitels(row){
+
+      this.reset();
+      this.open = true;
+      this.title = "客户群详情";
+      this.form=row;
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+
+  }
+};
+</script>

+ 1826 - 0
src/views/qw/sop/deptSop.vue

@@ -0,0 +1,1826 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="企微公司" prop="corpId">
+        <el-select v-model="queryParams.corpId" placeholder="企微公司" size="small" @change="updateCorpId()">
+          <el-option
+            v-for="dict in myQwCompanyList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="成员" prop="qwUserIds">
+        <el-select v-model="queryParams.qwUserIds" filterable clearable placeholder="选择成员" size="small">
+          <el-option
+            v-for="dict in companyUserList"
+            :key="dict.id"
+            :label="dict.qwUserName"
+            :value="dict.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="规则编号" prop="id">
+        <el-input
+          v-model="queryParams.id"
+          placeholder="请输入规则名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="类型 " prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类型" clearable size="small"  @change="updateCorpId()" >
+          <el-option
+            :key="1"
+            :label="'个微'"
+            :value="1"
+          />
+          <el-option
+            :key="2"
+            :label="'企微'"
+            :value="2"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="规则状态 " prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择规则状态" clearable size="small"  @change="updateCorpId()" >
+          <el-option
+            v-for="dict in sysSopStatus"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入规则名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.createTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择创建时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:sop:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['qw:sop:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:sop:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          size="mini"
+          plain
+          type="warning"
+          icon="el-icon-edit"
+          :disabled="multiple"
+          @click="handleExecute"
+          v-hasPermi="['qw:sop:execute']"
+        >批量执行SOP</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+    <el-table v-loading="loading" border :data="sopList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="规则编号" align="center" prop="id"  width="160"/>
+      <el-table-column label="规则名称" align="center" prop="name" width="150" />
+      <el-table-column label="规则状态" align="center" prop="status" width="150">
+        <template slot-scope="scope">
+          <dict-tag :options="sysSopStatus" :value="scope.row.status"></dict-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <el-tag type="success" v-if="scope.row.type==1">个微</el-tag>
+          <el-tag type="primary" v-else-if="scope.row.type==2">企微<span v-if="scope.row.filterMode == 2">(群聊)</span></el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="发送方式" align="center" prop="sendType">
+        <template slot-scope="scope">
+          <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="成员" align="center" prop="qwUserIds" width="150">
+        <template slot-scope="scope">
+          <!-- 点击事件绑定到整个 div -->
+          <div @click="toggleRow(scope.row)" v-if="scope.row.filterMode == 1">
+            <!-- 显示当前行的成员数量 -->
+            <span v-if="!expandedRows[scope.row.id]">
+            点击展开 ({{ scope.row.qwUserIds.split(',').length }} 人)
+          </span>
+            <!-- 展开时显示所有成员 -->
+            <div v-show="expandedRows[scope.row.id]">
+              <div v-if="scope.row.type==2" v-for="id in scope.row.qwUserIds.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in companyUserList" :key="list.qwUserId" style="margin: 3px;"
+                        v-if="list.id==id">
+                  {{ list.qwUserName }}
+                </el-tag>
+              </div>
+              <div v-if="scope.row.type==1" v-for="id in scope.row.qwUserIds.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in companyUserLists" :key="list.userId" style="margin: 3px;"
+                        v-if="list.userId==id">
+                  {{ list.nickName }}
+                </el-tag>
+              </div>
+            </div>
+          </div>
+          <div @click="toggleRow(scope.row)" v-if="scope.row.filterMode == 2">
+            <!-- 显示当前行的成员数量 -->
+            <span v-if="!expandedRows[scope.row.id]">
+            点击展开 ({{ scope.row.chatId.split(',').length }} 个)
+          </span>
+            <!-- 展开时显示所有成员 -->
+            <div v-show="expandedRows[scope.row.id]">
+              <div v-for="id in scope.row.chatId.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in chatList" :key="list.chatId" style="margin: 3px;"
+                        v-if="list.chatId == id">
+                  {{ list.name }}
+                </el-tag>
+              </div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="模板" align="center" prop="tempId" >
+        <template slot-scope="scope">
+            <div v-for="id in tempList" v-if="id.id==scope.row.tempId">{{id.name}}</div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="新客户自动创建sop" align="center" prop="isAutoSop" width="140">
+        <template slot-scope="scope">
+          <el-switch
+            v-if="scope.row.sendType==2 || scope.row.sendType==4"
+            v-model="scope.row.isAutoSop"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+            :active-value="1"
+            :inactive-value="2"
+            @change="switchAutoSopChange(scope.row,scope.row.isAutoSop)">
+          </el-switch>
+          <span v-if="scope.row.sendType!=1 && scope.row.isAutoSop == '1'" style="margin-left: 10px;color: #13ce66">已开启</span>
+          <span v-if="scope.row.sendType!=1 && scope.row.isAutoSop == '2'" style="margin-left: 10px;color: #ff4949">已关闭</span>
+          <span v-if="scope.row.sendType==1" style="margin-left: 10px;color: rgb(250,114,3)">企微接口无法设置</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="过期时间" align="center" prop="expiryTime" >
+        <template slot-scope="scope">
+          {{scope.row.expiryTime}} 小时
+        </template>
+      </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createBy" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180"/>
+      <el-table-column label="修改规则状态时间" align="center" prop="stopTime" width="180"/>
+      <el-table-column label="查看操作" align="center" class-name="small-padding fixed-width"  width="200" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.status==2||scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4 "
+            size="mini"
+            type="text"
+            @click="selectSchedule(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >营期</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            style="color: green;"
+            @click="handleScheduleDetail(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >执行详情</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            @click="handleUpdate(scope.row,2)"
+            v-hasPermi="['qw:sop:edit']"
+          >查看规则</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            style="color: green;"
+            @click="handleQueryDetails(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >查看模板</el-button>
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            @click="voice(scope.row.id)"-->
+<!--          >语音</el-button>-->
+
+        </template>
+      </el-table-column>
+      <el-table-column label="修改操作" align="center" class-name="small-padding fixed-width"  width="100px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleUpdateOutTime(scope.row)"
+          >修改规则</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-if="scope.row.filterMode == 2 && (scope.row.status==2||scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4)"
+            @click="handleSendMsg(scope.row)"
+          >一键发群
+          </el-button>
+          <el-button
+            v-if="scope.row.status == 1  "
+            size="mini"
+            type="text"
+            @click="handleUpdate(scope.row,1)"
+            v-hasPermi="['qw:sop:edit']"
+          >修改规则</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            style="color: red;"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:sop:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改企微sop对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="规则名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入规则名称" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="parseInt(dict.dictValue)"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+         <el-form-item label="类别" prop="type">
+         <el-radio-group v-model="form.type">
+             <el-radio
+               :label="1"
+             >个微</el-radio>
+             <el-radio
+               :label="2"
+             >企微</el-radio>
+         </el-radio-group>
+         </el-form-item>
+
+<!--        <div v-if="form.type==2">-->
+
+<!--        <el-form-item label="推送方式 ">-->
+<!--          <el-radio-group v-model="form.sendType" @input="handleSendTypeChange">-->
+<!--            <el-radio-->
+<!--              v-for="dict in sysQwSopType"-->
+<!--              :key="dict.dictValue"-->
+<!--              :label="parseInt(dict.dictValue)"-->
+<!--            >{{dict.dictLabel}}</el-radio>-->
+<!--          </el-radio-group>-->
+<!--        </el-form-item>-->
+<!--          <el-form-item label="选择员工" prop="qwUserIds" style="margin-top: 2%">-->
+<!--            <div>-->
+<!--              <el-button-->
+<!--                size="medium"-->
+<!--                icon="el-icon-circle-plus-outline"-->
+<!--                plain-->
+<!--                @click="handlelistUser(form.type,form.sendType)">请选择使用员工</el-button>-->
+<!--            </div>-->
+<!--            <div>-->
+<!--              <el-tag-->
+<!--                style="margin-left: 5px"-->
+<!--                size="medium"-->
+<!--                :key="id"-->
+<!--                v-for="id in userSelectList"-->
+<!--                closable-->
+<!--                :disable-transitions="false"-->
+<!--                @close="handleClosegroupUser(id)">-->
+<!--                <span v-for="list in companyUserList" :key="list.qwUserId" v-if="list.id==id">{{list.qwUserName}}</span>-->
+<!--              </el-tag>-->
+<!--            </div>-->
+<!--          </el-form-item>-->
+<!--          <el-form-item label="标签规则" prop="filterType">-->
+<!--            <el-radio-group v-model="form.filterType">-->
+<!--              <el-radio-->
+<!--                :label="1"-->
+<!--              >含全部标签</el-radio>-->
+<!--              <el-radio-->
+<!--                :label="2"-->
+<!--              >含任意标签</el-radio>-->
+<!--            </el-radio-group>-->
+<!--          </el-form-item>-->
+<!--          <el-form-item label="选择的标签" prop="tags">-->
+<!--            <el-select v-model="tags" remote multiple placeholder="请选择" filterable  style="width: 100%;">-->
+<!--              <el-option-->
+<!--                v-for="dict in tagList"-->
+<!--                :label="dict.name"-->
+<!--                :value="dict.tagId">-->
+<!--              </el-option>-->
+<!--            </el-select>-->
+
+<!--          </el-form-item>-->
+<!--          <el-form-item label="排除的标签" prop="excludeTags">-->
+<!--            <el-select v-model="excludeTags" remote multiple placeholder="请选择" filterable  style="width: 100%;">-->
+<!--              <el-option-->
+<!--                v-for="dict in tagList"-->
+<!--                :label="dict.name"-->
+<!--                :value="dict.tagId">-->
+<!--              </el-option>-->
+<!--            </el-select>-->
+<!--          </el-form-item>-->
+<!--        </div>-->
+<!--        <div v-if="form.type==1">-->
+<!--          <el-form-item label="推送方式 ">-->
+<!--            <el-tag type="success" v-model="form.sendType=2">AI插件</el-tag>-->
+<!--          </el-form-item>-->
+<!--        </div>-->
+
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable size="small"
+            v-model="form.startTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择开始时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="模板" prop="tempId">
+          <el-select v-model="form.tempId"  @focus="selectListSopTemp(form.sendType)" placeholder="请选择模板" v-loading="tempListLoading"   >
+            <el-option
+              v-for="dict in tempList"
+              :label="dict.name"
+              :value="dict.id">
+            </el-option>
+            <div v-if="tempListLoading" slot="prefix" class="select-prefix">正在查询相应模板...</div>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog :title="listUser.title" :visible.sync="listUser.open" width="1300px"  append-to-body>
+      <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
+    </el-dialog>
+
+    <!-- 单独的修改时间   -->
+    <el-dialog :title="outTimeOpen.title" :visible.sync="outTimeOpen.open" width="500px">
+
+       <el-col>
+
+         <el-row style="margin-top: 3%">
+           <span>过期时间:</span>
+           <el-input-number  v-model="outTimeOpen.expiryTime"  :min="1" :max="100" ></el-input-number>
+           (小时)
+         </el-row>
+
+         <el-row  style="margin-top: 3%">
+           <span>是否开启客户评级:</span>
+             <el-switch
+               v-model="outTimeOpen.isRating"
+               active-color="#13ce66"
+               inactive-color="#ff4949"
+               :active-value="1"
+               :inactive-value="2">
+             </el-switch>
+             <span v-if="outTimeOpen.isRating == '1'" style="margin-left: 10px;color: #13ce66">已开启</span>
+             <span v-if="outTimeOpen.isRating == '2'" style="margin-left: 10px;color: #ff4949">已关闭</span>
+         </el-row>
+         <el-row style="margin-top: 3%">
+           <span>小转天数:</span>
+           <el-input-number  v-model="outTimeOpen.minConversionDay"></el-input-number>
+           (天)
+         </el-row>
+         <el-row style="margin-top: 3%">
+           <span>大转天数:</span>
+           <el-input-number  v-model="outTimeOpen.maxConversionDay"></el-input-number>
+           (天)
+         </el-row>
+         <el-row style="margin-top: 3%">
+           <span>发课开始天数:</span>
+           <el-input-number  v-model="outTimeOpen.courseDay"  :min="1" :max="100" ></el-input-number>
+           (天)
+           <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-top: 2%">
+             <i class="el-icon-info"></i>
+             作用于【催课看板】和【看课记录】的统计,由多少天开始计算。比如 模板里 第一天是先导课,第二天是正课,此处设置天数为2,则统计从第二天开始。如果无视模板天数,此处设置几天,则统计面板就从第几天开始统计
+           </div>
+         </el-row>
+         <el-row style="margin-top: 3%" >
+
+           <el-button type="primary" icon="el-icon-search" size="mini" @click="handleUpdateExpiryTime">确定修改</el-button>
+           <el-button icon="el-icon-refresh" size="mini" @click="resetUpdateExpiryTime">取消</el-button>
+         </el-row>
+
+
+       </el-col>
+    </el-dialog>
+    <!-- 单独的修改时间   -->
+    <el-dialog title="语音记录" :visible.sync="voiceForm.open" width="70%" append-to-body>
+      <el-row style="height: 600px">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:sop:add']"
+        >新增</el-button>
+        <el-table border :data="voiceForm.list">
+          <el-table-column label="员工" align="center" prop="companyUserName" />
+          <el-table-column label="名称" align="center" prop="name" />
+          <el-table-column label="天数" align="center" prop="dayNum">
+            <template slot-scope="scope">
+              第{{scope.row.dayNum}}天
+            </template>
+          </el-table-column>
+          <el-table-column label="发送时间" align="center" prop="time" />
+          <el-table-column label="语音文本" align="center" prop="voiceTxt" />
+          <el-table-column label="语音时长" align="center" prop="duration">
+            <template slot-scope="scope">
+              {{scope.row.duration}}秒
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+          v-show="voiceForm.total>0"
+          :total="voiceForm.total"
+          :page.sync="voiceForm.queryParams.pageNum"
+          :limit.sync="voiceForm.queryParams.pageSize"
+          @pagination="voice(voiceForm.queryParams.id)"
+        />
+      </el-row>
+    </el-dialog>
+
+    <el-dialog :title="autoSopOpen.title" :visible.sync="autoSopOpen.open" width="1300px"  append-to-body>
+        <el-form ref="autoSopTimeFrom" :model="form.autoSopTime" :rules="autoSopTimeRules">
+          <el-form-item label="自动类型" prop="type"     style="margin: 2%; display: flex; align-items: center;">
+            <el-radio-group v-model="form.autoSopTime.autoSopType">
+              <el-radio
+                :label="1"
+              >当天开始</el-radio>
+              <el-radio
+                :label="2"
+              >次日开始</el-radio>
+              <el-radio
+                :label="3"
+              >分营期开始(正课)</el-radio>
+              <el-radio
+                :label="4"
+              >分营期开始(先导)</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <div style="display: flex; align-items: center; flex-wrap: nowrap;">
+            <div v-if="form.autoSopTime.autoSopType==1" style="display: flex; align-items: center">
+              <el-form-item
+                label="起始时间"
+                prop="autoStartTime"
+                label-width="100px"
+                style="margin: 2% 0;align-items: center;">
+                <el-time-select
+                  style="width: 120px;"
+                  placeholder="起始时间"
+                  v-model="form.autoSopTime.autoStartTime"
+                  :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00'
+                }">
+                </el-time-select>
+              </el-form-item>
+              <el-form-item
+                label="结束时间"
+                prop="autoEndTime"
+                label-width="100px"
+                style="margin: 2% 0; align-items: center; ">
+                <el-time-select
+                  style="width: 120px;"
+                  placeholder="结束时间"
+                  v-model="form.autoSopTime.autoEndTime"
+                  :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00',
+                  minTime: form.autoSopTime.autoEndTime
+                }">
+                </el-time-select>
+              </el-form-item>
+            </div>
+          </div>
+          <el-form-item  v-if="form.autoSopTime.autoSopType==1" label="过期消息是否发送" prop="autoSopSend"     style="margin: 2%; display: flex; align-items: center;">
+            <el-radio-group v-model="form.autoSopTime.autoSopSend">
+              <el-radio
+                :label="1"
+              >是</el-radio>
+              <el-radio
+                :label="2"
+              >否</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <div style="display: flex; align-items: center; flex-wrap: nowrap;" v-if="form.autoSopTime.autoSopType==3 || form.autoSopTime.autoSopType==4">
+            <el-form-item
+              label="选择一个或多个日期"
+              prop="autoDateTime"
+              label-width="150px">
+              <el-date-picker
+                size="large"
+                type="dates"
+                style="width: 600px"
+                v-model="form.autoSopTime.autoDateTime"
+                value-format="yyyy-MM-dd"
+                placeholder="选择一个或多个日期【ps:请先选择开始时间,不得小于任务开始时间】"
+                @change="sortSelectedDates(form.autoSopTime.autoDateTime)"
+                :disabled="!form.startTime">
+              </el-date-picker>
+            </el-form-item>
+          </div>
+
+
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==1"
+            title="起始时间-结束时间之内的,当天立即创建SOP,时间之外的 次日创建SOP"
+            type="warning"
+            style="font-size: 30px; margin-top: 3%;"
+            :closable="false"
+            show-icon>
+          </el-alert>
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==3"
+            type="warning"
+            style="margin-top: 3%;"
+            :closable="false"
+            show-icon>
+            <template #title>
+              <span style="font-size: 23px; line-height: 1.5;">
+                  此功能用于正课区分营期,可以选择你想要开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                  例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                  用户1月1日-1月4日添加(添加上对应标签)则会进入1月5日的营期,1月5日-1月9日添加则会进入1月10日营期,1月5日自动给用户发送第一节课程
+              </span>
+            </template>
+          </el-alert>
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==4"
+            title=""
+            type="warning"
+            style="font-size: 30px; margin-top: 3%;"
+            :closable="false"
+            show-icon>
+            <template #title>
+              <span style="font-size: 23px; line-height: 1.5;">
+                    此功能用于先导课区分营期,可以选择先导课开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                    例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                    用户1月1日-1月4日添加(添加上对应标签)则会进入1月1日的营期,1月5日-1月9日添加则会进入1月5日营期,自动给用户匹配上对应应该听到天数的时间节点
+              </span>
+            </template>
+          </el-alert>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="submitAutoSopTimeFrom">确 定</el-button>
+          <el-button @click="cancelAutoSopTime">取 消</el-button>
+        </div>
+    </el-dialog>
+    <!--  执行详情  -->
+    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="85%" style="font-weight: bolder">
+      <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
+    </el-drawer>
+    <el-dialog :title="sendMsgOpen.title" :visible.sync="sendMsgOpen.open"  width="1000px" append-to-body>
+      <el-form ref="msgForm" :model="msgForm" :rules="msgRules" label-width="100px">
+        <el-form-item label="群">
+          <el-select  v-model="msgForm.chatIds" placeholder="请选择群" size="mini" multiple>
+            <el-option
+              v-for="chatId in sendMsgOpen.chatIds"
+              :key="chatId"
+              :label="chatList.filter(e => e.chatId == chatId) && chatList.filter(e => e.chatId == chatId).length > 0 ? chatList.filter(e => e.chatId == chatId)[0].name : ''"
+              :value="chatId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发送类型">
+          <el-radio-group v-model="msgForm.sendType">
+            <el-radio :label="1">群</el-radio>
+            <el-radio :label="2">群成员</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="选择课程">
+          <el-select  v-model="msgForm.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange()">
+            <el-option
+              v-for="dict in courseList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+          <el-select  v-model="msgForm.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange()"  >
+            <el-option
+              v-for="dict in videoList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+          <el-select  v-model="msgForm.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;">
+            <el-option
+              v-for="dict in sysFsSopWatchStatus"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="规则" prop="setting"  >
+          <div v-for="(item, index) in setting" :key="index" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+            <el-row>
+              <el-col :span="22">
+                <el-form :model="item" label-width="70px">
+                  <el-form-item label="内容类别" style="margin: 2%">
+                    <el-radio-group  v-model="item.contentType">
+                      <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType"  @change="handleContentTypeChange()">{{item.dictLabel}}</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                  <el-form-item label="内容" style="margin-bottom: 2%" >
+                    <el-input v-if="item.contentType == 1 " v-model="item.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                    <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1"  :width="150" :height="150" />
+
+                    <div v-if="item.contentType == 3 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:"  label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:"   label-width="100px" >
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:"   label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                        </el-form-item>
+                        <el-form-item label="链接地址:"  label-width="100px" >
+                          <el-tag type="warning" v-model="item.isBindUrl=1">选择的课程小节 即为卡片链接地址</el-tag>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                    <div v-if="item.contentType == 4">
+
+                    </div>
+                    <div v-if="item.contentType == 5 ">
+
+                      <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.fileUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
+                          :before-upload="beforeAvatarUploadFile">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
+                          {{item.fileUrl}}
+                        </el-link>
+                      </el-form-item>
+
+                    </div>
+
+                    <div v-if="item.contentType == 6 ">
+                      <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.videoUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
+                          :before-upload="beforeAvatarUploadVideo">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <video v-if="item.videoUrl"
+                               :src="item.videoUrl"
+                               controls style="width: 200px;height: 100px">
+                        </video>
+                      </el-form-item>
+                    </div>
+                    <div v-if="item.contentType == 7 ">
+                      <el-input
+                        v-model="item.value"
+                        type="textarea" :rows="3" maxlength="66" show-word-limit
+                        placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                        @input="handleInputVideoText(item.value,item)"/>
+                    </div>
+
+                    <div v-if="item.contentType == 10 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:"  label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:"   label-width="100px" >
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:"   label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                        </el-form-item>
+                        <el-form-item label="链接地址:"  label-width="100px" >
+                          <el-tag type="warning" >链接地址自动生成</el-tag>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                  </el-form-item>
+
+                  <el-form-item label="添加短链" v-if="item.contentType == 1 "  >
+                    <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!msgForm.videoId">
+                      <el-switch
+                        v-model="item.isBindUrl"
+                        :disabled="!msgForm.videoId"
+                        active-color="#13ce66"
+                        inactive-color="#DCDFE6"
+                        active-value="1"
+                        inactive-value="2">
+                      </el-switch>
+                    </el-tooltip>
+
+                    <span v-if="item.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                    <span v-if="item.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                  </el-form-item>
+                  <el-form-item label="课节过期时间" v-if="item.isBindUrl == '1'
+                                                          && item.contentType != 2
+                                                          && item.contentType != 5
+                                                          && item.contentType != 6
+                                                          && item.contentType != 8
+                                                          && item.contentType != 9
+                                                          && item.contentType != 10"
+                                style="margin-top: 1%" label-width="100px">
+                    <el-row>
+                      <el-input-number  v-model="item.expiresDays"  :min="1" :max="100" ></el-input-number>
+                      (天)
+                    </el-row>
+                    <el-row>
+                      <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                    </el-row>
+                  </el-form-item>
+                </el-form>
+              </el-col>
+              <el-col :span="1" :offset="1">
+                <i class="el-icon-delete" @click="delSetList(index)" style="margin-top: 20px;" v-if="setting.length>1"></i>
+              </el-col>
+            </el-row>
+          </div>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList()'  >添加内容</el-link>
+
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitMsgForm">确 定</el-button>
+        <el-button @click="cancelMsgForm">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listDeptSop,
+  addSop,
+  courseList,
+  videoList,
+  delSop,
+  exportSop,
+  getSopVoiceList,
+  listSop,
+  updateAutoSopTime,
+  updateSop,
+  updateSopStatus,
+  updateStatus
+} from "@/api/qw/sop";
+import {listSopTemp} from "@/api/qw/sopTemp";
+import {getQwAllUserList, listUser} from '@/api/company/companyUser'
+import qwUserList from '@/views/qw/user/qwUserList.vue'
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import CustomerGroupDetails from '@/views/qw/groupMsg/customerGroupDetails.vue'
+import sopLogsDetails from '@/views/qw/sopLogs/sopLogsList.vue'
+import {listTag,} from "@/api/qw/tag";
+import {sendMsgSop} from "@/api/qw/sopUserLogsInfo";
+import {getMyQwCompanyList} from "@/api/qw/user";
+import {allList} from "@/api/qw/groupChat";
+
+export default {
+  name: "deptSop",
+    components: { CustomerGroupDetails, qwUserList,ImageUpload,sopLogsDetails},
+  data() {
+    return {
+      // 存储每一行的展开状态
+      expandedRows: {},
+      //模板查询
+      tempListLoading:false,
+      // 遮罩层
+      loading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      //选中的map
+      selectionMap:{},
+      myQwCompanyList:[],
+      //销售员工列表
+      companyUserLists:[],
+      videoList:[],
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      msgForm:{
+        videoId:null,
+        courseId:null,
+        courseType:null,
+        userIdParam:null,
+        sendType:1,
+        setting:null,
+        ids:null,
+        sopId: null,
+        startTime: null,
+        chatIds: [],
+        isRegister:2
+      },
+      sendMsgOpen:{
+        title:'一键批量群发',
+        open:false,
+        row: {},
+        ids:null,
+      },
+      msgRules:{},
+      courseList:[],
+      // videoList:[],
+      tags:null,
+      excludeTags:null,
+      // 非单个禁用
+      single: true,
+      setting:[],
+      tagList:[],
+      tempList:[],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企微sop表格数据
+      sopList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      companyUserList:[],
+      // 状态字典
+      statusOptions: [],
+      //sop状态
+      sysSopStatus: [],
+
+      autoSopOpen:{
+        title:'',
+        open:false,
+        id:null,
+        isAutoSop:null,
+      },
+      outTimeOpen:{
+        title:'',
+        open:false,
+        id:null,
+        tempId:null,
+        expiryTime:null,
+        isRating:null,
+        isSampSend:null,
+      },
+      voiceForm:{
+        list:[],
+        open:false,
+        total:0,
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          id: null,
+        },
+      },
+
+      //企微SOP发送类型
+      sysQwSopType: [],
+      chatList: [],
+
+      //SOP课程观看状态
+      sysFsSopWatchStatus:[],
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        id: null,
+        name: null,
+        status: null,
+        sendType:null,
+        type: null,
+        qwUserIds: null,
+        setting: null,
+        createBy: null,
+        corpId: null,
+        createTime: null
+      },
+      sopLogsDialog:{
+        title:'',
+        open:false,
+        sopLogsForm:[],
+      },
+      // 表单参数
+      form: {
+        autoSopTime:{ autoSopType:2,autoStartTime:'00:00',autoEndTime:'24:00',autoSopSend:2,autoDateTime:[]},
+      },
+      userSelectList:[],
+      listUser:{
+        title:"",
+        open:false
+      },
+      // 表单校验
+      rules: {
+        name:[ { required: true, message: "名称不能为空", trigger: "submit" }],
+        type:[ { required: true, message: "不能为空", trigger: "submit" }],
+        sendType:[ { required: true, message: "不能为空", trigger: "submit" }],
+        startTime:[ { required: true, message: "开始时间不能为空", trigger: "submit" }],
+        tempId:[ { required: true, message: "模板不能为空", trigger: "submit" }],
+      },
+      autoSopTimeRules:{
+        autoSopType:[ { required: true, message: "选项不能为空", trigger: "submit" }],
+        autoStartTime:[ { required: true, message: "起始时间不能为空", trigger: "submit" }],
+        autoEndTime:[ { required: true, message: "结束时间不能为空", trigger: "submit" }],
+        autoDateTime:[ { required: true, message: "日期不能为空", trigger: "submit" }],
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_sop_status").then(response => {
+      this.sysSopStatus = response.data;
+    });
+
+
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+
+
+    listUser().then(res => {
+        this.companyUserLists = res.rows;
+      }
+    );
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+
+    getMyQwCompanyList().then(response => {
+      this.myQwCompanyList = response.data;
+      if(this.myQwCompanyList!=null){
+        this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+        this.refreshData(this.queryParams.corpId);
+        this.getList();
+      }
+    });
+
+
+
+  },
+  watch:{
+    userSelectList(newList) {
+      this.form.qwUserIds = newList.map(item => item.id);
+    }
+  },
+  methods: {
+    voice(id){
+      this.voiceForm.queryParams.id = id;
+      getSopVoiceList(this.voiceForm.queryParams).then(res => {
+        this.voiceForm.list = res.rows;
+        this.voiceForm.total = res.total;
+        this.voiceForm.open = true;
+      })
+    },
+    // 单元格点击事件
+    handleCellClick(row, column, cell, event) {
+      // 判断是否为规则编号列
+      if (column.property === 'id') {
+        this.handleRowClick(row); // 触发规则编号点击事件
+      }
+    },
+    // handleUpdateTags(){
+    //
+    // },
+    // handleUpdateQwUser(){
+    //
+    // },
+    // handleUpdateSopTemp(){
+    //
+    // },
+    handleRowClick(row, column, event) {
+      // 判断状态是否符合条件
+      if (row.status == 2 || row.status == 0 || row.status == 3 || row.status == 4) {
+        this.handleUpdate(row, 2);
+      }
+    },
+    // 切换某一行的展开状态
+    toggleRow(row) {
+      this.$set(this.expandedRows, row.id, !this.expandedRows[row.id]);
+    },
+    getSwitchVal(status) {
+      return [2, 3, 4].includes(status) ? 2 : 0;
+    },
+
+    onSwitchChange(row, val) {
+      this.loading=true;
+      // ① 调用接口更新后端
+      updateSopStatus({ id: row.id, status: val })
+        .then(() => {
+          row.status = val;
+          this.loading=false;
+          this.$message.success("切换成功!");
+        })
+        .catch(() => {
+          this.loading=false;
+          this.$message.error("操作失败,请重试");
+        });
+    },
+
+    switchSopStatusChange(row,checked){
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '暂停中-请勿刷新页面-重复点击',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+
+      updateSopStatus({id:row,status:checked}).then(response => {
+        this.$message.success("修改成功");
+        this.getList();
+      }).finally(res=>{
+        loadingRock.close();
+      })
+
+
+    },
+
+    //新客户自动创建sop
+    switchAutoSopChange(row,checked){
+
+      this.form.startTime=row.startTime;
+      this.form.autoSopTime=JSON.parse(row.autoSopTime);
+
+      if (checked==1) {
+        this.autoSopOpen.title='请设置相应时间段'
+        this.autoSopOpen.open=true;
+        this.autoSopOpen.id=row.id;
+        this.autoSopOpen.isAutoSop=1;
+      }else {
+        this.form.autoSopTime.createTime=this.formatDateTo24HourString(new Date());
+        updateAutoSopTime({id:row.id,isAutoSop:checked,autoSopTime: JSON.stringify(this.form.autoSopTime)}).then(response => {
+          this.getList();
+        })
+      }
+
+
+    },
+     updateCorpId(){
+       this.reset();
+       this.refreshData(this.queryParams.corpId);
+       this.queryParams.qwUserId = null;
+       this.getList();
+     },
+
+    //刷新部分数据
+    refreshData(row){
+      getQwAllUserList(row).then(response => {
+        this.companyUserList = response.data;
+      });
+
+      listTag({corpId:row}).then(response => {
+        this.tagList = response.rows;
+      });
+
+      if (row != null) {
+        allList(row).then(e => {
+          this.chatList = e.data;
+        })
+      }
+    },
+
+    //查询模板
+    selectListSopTemp(type){
+      this.tempListLoading = true; // 开始查询,显示加载提示
+      listSopTemp({sendType:type}).then(response => {
+        this.tempList = response.rows;
+        this.tempListLoading = false;
+      });
+    },
+    handlelistUser(type,sendType){
+
+       setTimeout(() => {
+         this.$refs.QwUserList.getDetails(this.queryParams.corpId,type,sendType);
+       }, 1);
+       this.listUser.title="选择企业成员"
+       this.listUser.open=true;
+
+    },
+    selectUserList(list){
+      this.listUser.open=false;
+      list.forEach(obj => {
+        if (!this.userSelectList.some(item => item == obj.id)) {
+          this.userSelectList.push(obj.id);
+        }
+      });
+
+    },
+
+    //修改过期时间
+    handleUpdateOutTime(val){
+        this.outTimeOpen.title="修改过期时间/评级";
+        this.outTimeOpen.id=val.id;
+        this.outTimeOpen.tempId=val.tempId;
+        this.outTimeOpen.expiryTime=val.expiryTime;
+        this.outTimeOpen.isRating=val.isRating;
+        this.outTimeOpen.minConversionDay=val.minConversionDay;
+        this.outTimeOpen.maxConversionDay=val.maxConversionDay;
+        this.outTimeOpen.courseDay=val.courseDay;
+        this.outTimeOpen.isSampSend=val.isSampSend;
+        this.outTimeOpen.open=true;
+    },
+
+    handleUpdateExpiryTime(){
+      updateSop({id:this.outTimeOpen.id,tempId:this.outTimeOpen.tempId,expiryTime:this.outTimeOpen.expiryTime,
+        isRating: this.outTimeOpen.isRating,minConversionDay: this.outTimeOpen.minConversionDay,maxConversionDay: this.outTimeOpen.maxConversionDay,
+        courseDay: this.outTimeOpen.courseDay,isSampSend:this.outTimeOpen.isSampSend}).then(response => {
+        this.msgSuccess("修改成功");
+        this.resetUpdateExpiryTime()
+        this.getList();
+      });
+    },
+    resetUpdateExpiryTime(){
+      this.outTimeOpen={
+          title:'',
+          open:false,
+          id:null,
+          expiryTime:null,
+          courseDay:null,
+          minConversionDay:null,
+          maxConversionDay:null,
+          isRating:null,
+          isSampSend:null,
+      }
+    },
+    addContent(index){
+      // this.setting[index].content.push({type:1})
+      if (this.form.sendType==2){
+        this.setting[index].content.push({ type: 1 });
+      }else {
+        if (this.setting[index].content.length < 9) {
+          this.setting[index].content.push({ type: 1 });
+        } else {
+          this.$message({
+            message: '最多只能添加 9 个内容',
+            type: 'warning'
+          });
+        }
+      }
+
+    },
+
+    //选择变动时的变动
+    handleSendTypeChange(val){
+      this.tempList=[];
+      this.form.tempId=null;
+      if (val==1) {
+        // 遍历 this.setting 数组并清空每个对象的 content 属性
+        this.setting.forEach(item => {
+          if (item.content.length > 9) {
+            item.content = item.content.slice(0, 9); // 保留前 9 个元素
+          }
+        });
+      }
+    },
+    delContent(index,contentIndex){
+      this.setting[index].content.splice(contentIndex,1)
+    },
+
+    //添加SOP规则类型
+    handleCommand(command){
+      this.setting.push({sopType:command,ruleType:null,name:null,type:1,content:[],isBindUrl:1,url:null,day:"0",hour:"0",minute:"0",time:""})
+    },
+
+    //添加课程SOP
+    handleCrouseCommand(command,val){
+      console.log("command",command)
+      console.log("val",val)
+    },
+
+    delSetting(index){
+      this.setting.splice(index,1)
+    },
+    handleClosegroupUser(list){
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+    /** 查询企微sop列表 */
+    getList() {
+      this.loading = true;
+      listDeptSop(this.queryParams).then(response => {
+
+        this.sopList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+
+
+        listSopTemp().then(res => {
+          this.tempList = res.rows;
+        });
+
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    // courseChange(){
+    //   videoList(this.form.courseId).then(response => {
+    //     this.videoList = response.list;
+    //   });
+    // },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        status: 1,
+        sendType:2,
+        type: 2,
+        filterType:2,
+        qwUserIds: null,
+        corpId: null,
+        setting: null,
+        createBy: null,
+        createTime: null,
+        isAutoSop:null,
+        autoSopTime:{ autoSopType:2,autoStartTime:'00:00',autoEndTime:'24:00',autoSopSend:2,autoDateTime:[]},
+      };
+      this.resetForm("form");
+      this.tags = null;
+      this.excludeTags = null;
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+      this.refreshData(this.queryParams.corpId);
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.selectionMap=selection;
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.$router.push('/qw/sop/addSop/'+this.queryParams.corpId)
+      // this.open = true;
+      // this.setting=[]
+      // this.userSelectList=[]
+      // this.title = "添加企微sop";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row,type) {
+      this.$router.push('/qw/sop/updateSop/'+row.id+'/'+type)
+    },
+
+
+    /**
+    * 查看SOP任务内营期
+    */
+    selectSchedule(row){
+      // type 2:我的sop 1:部门sop/销售公司sop
+      const query = {
+        id: row.id,
+        name: row.name,
+        tempId: row.tempId,
+        filterMode: row.filterMode,
+        corpId: row.corpId,
+        type:1,
+      }
+      // 使用 params 传递参数
+      this.$router.push({
+        path: `/qw/sopUserLogs/sopUserLogsSchedule/${query.id}`,
+        query // 如果需要传递更多参数,可以使用 query
+      });
+    },
+
+
+    /**
+    * 查看营期内详情
+    */
+    handleScheduleDetail(row){
+        this.sopLogsDialog.title='规则执行详情';
+        this.sopLogsDialog.open=true;
+        this.sopLogsDialog.sopLogsForm=row;
+    },
+
+    /**
+    * 查看模板
+    */
+    handleQueryDetails(row){
+      this.$router.push(`/qw/sopTemp/updateSopTemp/${row.tempId}/3`)
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if(this.userSelectList.length<=0){
+            return this.$message("请选择员工")
+          }
+          this.form.qwUserIds = this.userSelectList.join(",");
+          this.form.corpId=this.queryParams.corpId;
+          if (this.tags!=null && this.tags.length>0 ){
+            this.form.tags=(this.tags).toString()
+          }else {
+            return  this.$message.error("选择的标签不能为空!!请选择筛选的标签")
+          }
+          if (this.excludeTags!=null){
+            this.form.excludeTags=(this.excludeTags).toString()
+          }
+
+          this.form.setting=JSON.stringify(this.setting)
+          if (this.form.id != null) {
+            updateSop(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addSop(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    sortSelectedDates(dates){
+
+      if (!this.form.startTime) {
+        this.$message.error("请先选择开始时间!不得小于任务开始时间");
+        this.form.autoSopTime.autoDateTime = [];
+        return;
+      }
+
+      // 1. 过滤掉 < startTime 的日期
+      const validDates = dates.filter(date => new Date(date) >= new Date(this.form.startTime));
+
+      // 2. 如果有被过滤掉的日期,提示用户
+      if (validDates.length < dates.length) {
+        this.$message.warning(`已自动移除小于 ${this.form.startTime} 的日期!`);
+      }
+
+      // 3. 升序排序
+      validDates.sort((a, b) => new Date(a) - new Date(b));
+
+      // 4. 更新数据
+      this.form.autoSopTime.autoDateTime = validDates;
+    },
+
+    submitAutoSopTimeFrom(){
+
+      if (this.form.autoSopTime.autoSopType==3 || this.form.autoSopTime.autoSopType==4){
+        if(this.form.autoSopTime.autoDateTime==null || this.form.autoSopTime.autoDateTime=="") {
+          return  this.$message.error("选择的日期不能为空!!请选择日期")
+        }
+      }
+
+      this.$refs["autoSopTimeFrom"].validate(valid => {
+        if (valid) {
+          this.autoSopOpen.open=false
+          this.form.autoSopTime.createTime=this.formatDateTo24HourString(new Date());
+          updateAutoSopTime({id:this.autoSopOpen.id,isAutoSop:this.autoSopOpen.isAutoSop,autoSopTime: JSON.stringify(this.form.autoSopTime)}).then(response => {
+            this.msgSuccess("修改成功");
+          });
+        }
+        this.getList();
+        this.reset();
+      });
+    },
+
+    formatDateTo24HourString(date) {
+      let year = date.getFullYear();
+      let month = ('0' + (date.getMonth() + 1)).slice(-2); // 月份需要加 1 并补零
+      let day = ('0' + date.getDate()).slice(-2); // 日需要补零
+      let hours = ('0' + date.getHours()).slice(-2); // 小时需要补零
+      let minutes = ('0' + date.getMinutes()).slice(-2); // 分钟需要补零
+      let seconds = ('0' + date.getSeconds()).slice(-2); // 秒需要补零
+
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    cancelAutoSopTime(){
+      this.autoSopOpen.open=false
+      this.getList();
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企微sop编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delSop(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /**
+    * 批量执行SOP任务
+    */
+    handleExecute(){
+
+        const ids = this.selectionMap
+          .filter(item => item.status === 1)
+          .map(item => item.id);
+
+        if (ids.length === 0) {
+          this.msgError("选择的任务已经执行或正在执行,请选择待执行的任务");
+          return;
+        }
+
+        this.$confirm('是否确认立即执行sop编号为"' + ids + '"的数据项?', "警告【只能立即执行(待执行的)】", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+            this.loading = true;
+            // 执行更新操作
+            return updateStatus(ids); // 返回一个Promise,直接在此链式调用
+        }).then(res => {
+          let msg = "";
+
+          // 判断成功和失败的任务,并显示消息
+          if (res.suc.length > 0) {
+            msg += "执行成功的SOP任务【" + res.suc.join(", ") + "】。<br>"; // 优化:将数组转换为字符串
+          }
+          if (res.err.length > 0) {
+            msg += "失败的SOP任务【" + res.err.join(", ") + "】,原因是已经执行或正在执行。<br>";
+          }
+
+          // 显示确认框,显示成功与失败信息
+          return this.$confirm(msg, "提示", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning",
+            dangerouslyUseHTMLString: true // 允许HTML标签
+          });
+
+        }).then(() => {
+          // 操作完成后刷新列表,并显示成功信息
+          this.getList();
+          this.msgSuccess("执行完成");
+        }).catch(() => {
+          // 处理任何异常,操作取消或者失败时显示提示信息
+          this.msgError("操作失败,请重试");
+        }).finally(() => {
+          this.loading = false; // 操作完成后关闭加载状态
+        });
+
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企微sop数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportSop(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+    courseChange() {
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && this.setting[i].contentType == 3 && this.msgForm.courseId != null) {
+            this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+        }
+
+      }
+      videoList(this.msgForm.courseId).then(response => {
+        this.videoList=response.list;
+      });
+    },
+    handleSendMsg(row){
+      let chatList = row.chatId ? row.chatId.split(",") : [];
+      this.sendMsgOpen.id = row.id;
+      this.sendMsgOpen.row = row;
+      this.sendMsgOpen.chatIds = chatList;
+      this.msgForm.chatIds = chatList;
+      this.sendMsgOpen.open = true;
+    },
+    handleContentTypeChange() {
+
+      //如果是链接的才上
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && this.setting[i].contentType == 3 && this.msgForm.courseId != null) {
+            this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+        }
+
+      }
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && this.setting[i].contentType == 3  && this.msgForm.videoId != null) {
+            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+    handleAvatarSuccessFile(res, file, item) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, item) {
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'videoUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+
+    beforeAvatarUploadVideo(file){
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleInputVideoText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    delSetList(index){
+      this.setting.splice(index,1)
+    },
+    addSetList(){
+      const newSetting = {
+        contentType:'1',
+        value: '',
+      };
+      // 将新设置项添加到 content.setting 数组中
+      this.setting.push(newSetting);
+
+    },
+    submitMsgForm(){
+      this.$refs["msgForm"].validate(valid => {
+        if (valid) {
+          this.msgForm.setting=JSON.stringify(this.setting)
+          this.msgForm.sopId=this.sendMsgOpen.row.id;
+          this.msgForm.filterMode=this.sendMsgOpen.row.filterMode;
+
+          if (this.setting.length <= 0) {
+            return this.$message.error("请添加规则")
+          }
+          if (this.msgForm.courseId===null || this.msgForm.courseId===''){
+            return this.$message.error("课程不能为空")
+          }
+
+          if (this.msgForm.videoId===null || this.msgForm.videoId===''){
+            return this.$message.error("课节不能为空")
+          }
+
+          if (this.msgForm.courseType===null || this.msgForm.courseType===''){
+            return this.$message.error("消息类型不能为空")
+          }
+
+          for (let i = 0; i < this.setting.length; i++) {
+            if (this.setting[i].contentType == 1 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("内容不能为空")
+            }
+            if (this.setting[i].contentType == 2 && (this.setting[i].imgUrl == null || this.setting[i].imgUrl == "")) {
+              return this.$message.error("图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkTitle == null || this.setting[i].linkTitle == "")) {
+              return this.$message.error("链接标题不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkDescribe == null || this.setting[i].linkDescribe == "")) {
+              return this.$message.error("链接描述不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkImageUrl == null || this.setting[i].linkImageUrl == "")) {
+              return this.$message.error("链接图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && this.setting[i].type == 1 && (this.setting[i].linkUrl == null || this.setting[i].linkUrl == "")) {
+              return this.$message.error("链接地址不能为空")
+            }
+            if (this.setting[i].contentType == 5 && (this.setting[i].fileUrl == null || this.setting[i].fileUrl == "")) {
+              return this.$message.error("文件不能为空")
+            }
+            if (this.setting[i].contentType == 6 && (this.setting[i].videoUrl == null || this.setting[i].videoUrl == "")) {
+              return this.$message.error("视频不能为空")
+            }
+            if (this.setting[i].contentType == 7 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("语音不能为空")
+            }
+          }
+
+          this.sendMsgOpen.open = false;
+
+          const loading = this.$loading({
+            lock: true,
+            text: '正在执行中请稍后~~请不要刷新页面!!',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+
+          sendMsgSop(this.msgForm).then(response => {
+            this.msgSuccess("一键群发成功");
+            loading.close();
+            this.setting=[];
+            this.msgForm = {
+              videoId:null,
+              courseId:null,
+              courseType:null,
+              setting:null,
+              isRegister:2,
+              sendType:1,
+              filterMode:2,
+
+            }
+            this.getList();
+          }).finally(()=>{
+            loading.close();
+          });
+
+        }
+      });
+    },
+    cancelMsgForm(){
+      this.sendMsgOpen.open = false;
+      this.resetSendMsgSop();
+    },
+    videoIdChange() {
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && this.setting[i].contentType == 3  && this.msgForm.videoId != null) {
+            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+  }
+};
+</script>
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  padding: 0 4px;
+  text-align:center;
+  display: block;
+}
+.custom-input /deep/ .el-input__icon {
+  line-height: 20px;
+}
+.el-button--text{
+  cursor: pointer;
+}
+</style>

+ 1857 - 0
src/views/qw/sop/mySop.vue

@@ -0,0 +1,1857 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="企微公司" prop="corpId">
+        <el-select v-model="queryParams.corpId" placeholder="企微公司" size="small" @change="updateCorpId()">
+          <el-option
+            v-for="dict in myQwCompanyList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="成员" prop="qwUserIds">
+        <el-select v-model="queryParams.qwUserIds" filterable clearable placeholder="选择成员" size="small">
+          <el-option
+            v-for="dict in companyUserList"
+            :key="dict.id"
+            :label="dict.qwUserName"
+            :value="dict.id"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="规则编号" prop="id">
+        <el-input
+          v-model="queryParams.id"
+          placeholder="请输入规则名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="类型 " prop="type">
+        <el-select v-model="queryParams.type" placeholder="请选择类型" clearable size="small"  @change="updateCorpId()" >
+          <el-option
+            :key="1"
+            :label="'个微'"
+            :value="1"
+          />
+          <el-option
+            :key="2"
+            :label="'企微'"
+            :value="2"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="规则状态 " prop="status">
+        <el-select v-model="queryParams.status" placeholder="请选择规则状态" clearable size="small"  @change="updateCorpId()" >
+          <el-option
+            v-for="dict in sysSopStatus"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+
+      <el-form-item label="名称" prop="name">
+        <el-input
+          v-model="queryParams.name"
+          placeholder="请输入规则名称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="创建时间" prop="createTime">
+        <el-date-picker clearable size="small"
+          v-model="queryParams.createTime"
+          type="date"
+          value-format="yyyy-MM-dd"
+          placeholder="选择创建时间">
+        </el-date-picker>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:sop:add']"
+        >新增</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="danger"
+          plain
+          icon="el-icon-delete"
+          size="mini"
+          :disabled="multiple"
+          @click="handleDelete"
+          v-hasPermi="['qw:sop:remove']"
+        >删除</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          type="success"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:sop:export']"
+        >导出</el-button>
+      </el-col>
+      <el-col :span="1.5">
+        <el-button
+          size="mini"
+          plain
+          type="warning"
+          icon="el-icon-edit"
+          :disabled="multiple"
+          @click="handleExecute"
+          v-hasPermi="['qw:sop:execute']"
+        >批量执行SOP</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+    <el-table v-loading="loading" border :data="sopList" @selection-change="handleSelectionChange">
+      <el-table-column type="selection" width="55" align="center" />
+      <el-table-column label="规则编号" align="center" prop="id"  width="160"/>
+      <el-table-column label="规则名称" align="center" prop="name" width="150" />
+      <el-table-column label="规则状态" align="center" prop="status" width="150">
+        <template slot-scope="scope">
+          <dict-tag :options="sysSopStatus" :value="scope.row.status"></dict-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="类别" align="center" prop="type">
+        <template slot-scope="scope">
+          <el-tag type="success" v-if="scope.row.type==1">个微</el-tag>
+          <el-tag type="primary" v-else-if="scope.row.type==2">企微<span v-if="scope.row.filterMode == 2">(群聊)</span></el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="发送方式" align="center" prop="sendType">
+        <template slot-scope="scope">
+          <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="成员" align="center" prop="qwUserIds" width="150">
+        <template slot-scope="scope">
+          <!-- 点击事件绑定到整个 div -->
+          <div @click="toggleRow(scope.row)" v-if="scope.row.filterMode == 1">
+            <!-- 显示当前行的成员数量 -->
+            <span v-if="!expandedRows[scope.row.id]">
+            点击展开 ({{ scope.row.qwUserIds.split(',').length }} 人)
+          </span>
+            <!-- 展开时显示所有成员 -->
+            <div v-show="expandedRows[scope.row.id]">
+              <div v-if="scope.row.type==2" v-for="id in scope.row.qwUserIds.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in companyUserList" :key="list.qwUserId" style="margin: 3px;"
+                        v-if="list.id==id">
+                  {{ list.qwUserName }}
+                </el-tag>
+              </div>
+              <div v-if="scope.row.type==1" v-for="id in scope.row.qwUserIds.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in companyUserLists" :key="list.userId" style="margin: 3px;"
+                        v-if="list.userId==id">
+                  {{ list.nickName }}
+                </el-tag>
+              </div>
+            </div>
+          </div>
+          <div @click="toggleRow(scope.row)" v-if="scope.row.filterMode == 2">
+            <!-- 显示当前行的成员数量 -->
+            <span v-if="!expandedRows[scope.row.id]">
+            点击展开 ({{ scope.row.chatId.split(',').length }} 个)
+          </span>
+            <!-- 展开时显示所有成员 -->
+            <div v-show="expandedRows[scope.row.id]">
+              <div v-for="id in scope.row.chatId.split(',')" :key="id"
+                   style="display: inline;" class="text-container">
+                <el-tag type="success" v-for="list in chatList" :key="list.chatId" style="margin: 3px;"
+                        v-if="list.chatId == id">
+                  {{ list.name }}
+                </el-tag>
+              </div>
+            </div>
+          </div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="模板" align="center" prop="tempId" >
+        <template slot-scope="scope">
+            <div v-for="id in tempList" v-if="id.id==scope.row.tempId">{{id.name}}</div>
+        </template>
+      </el-table-column>
+
+      <el-table-column label="新客户自动创建sop" align="center" prop="isAutoSop" width="140">
+        <template slot-scope="scope">
+          <el-switch
+            v-if="scope.row.sendType==2 || scope.row.sendType==4"
+            v-model="scope.row.isAutoSop"
+            active-color="#13ce66"
+            inactive-color="#ff4949"
+            :active-value="1"
+            :inactive-value="2"
+            @change="switchAutoSopChange(scope.row,scope.row.isAutoSop)">
+          </el-switch>
+          <span v-if="scope.row.sendType!=1 && scope.row.isAutoSop == '1'" style="margin-left: 10px;color: #13ce66">已开启</span>
+          <span v-if="scope.row.sendType!=1 && scope.row.isAutoSop == '2'" style="margin-left: 10px;color: #ff4949">已关闭</span>
+          <span v-if="scope.row.sendType==1" style="margin-left: 10px;color: rgb(250,114,3)">企微接口无法设置</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="过期时间" align="center" prop="expiryTime" >
+        <template slot-scope="scope">
+          {{scope.row.expiryTime}} 小时
+        </template>
+      </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createBy" />
+      <el-table-column label="创建时间" align="center" prop="createTime" width="180"/>
+      <el-table-column label="修改规则状态时间" align="center" prop="stopTime" width="180"/>
+      <el-table-column label="查看操作" align="center" class-name="small-padding fixed-width"  width="200" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            v-if="scope.row.status==2||scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4 "
+            size="mini"
+            type="text"
+            @click="selectSchedule(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >营期</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            style="color: green;"
+            @click="handleScheduleDetail(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >执行详情</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            @click="handleUpdate(scope.row,2)"
+            v-hasPermi="['qw:sop:edit']"
+          >查看规则</el-button>
+          <el-button
+            v-if="scope.row.status==2 || scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4  "
+            size="mini"
+            type="text"
+            style="color: green;"
+            @click="handleQueryDetails(scope.row)"
+            v-hasPermi="['qw:sop:list']"
+          >查看模板</el-button>
+<!--          <el-button-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            @click="voice(scope.row.id)"-->
+<!--          >语音</el-button>-->
+
+        </template>
+      </el-table-column>
+      <el-table-column label="修改操作" align="center" class-name="small-padding fixed-width"  width="100px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            @click="handleUpdateOutTime(scope.row)"
+          >修改规则</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            v-if="scope.row.filterMode == 2 && (scope.row.status==2||scope.row.status==0 || scope.row.status == 3 || scope.row.status == 4)"
+            @click="handleSendMsg(scope.row)"
+          >一键发群
+          </el-button>
+          <el-button
+            v-if="scope.row.status == 1  "
+            size="mini"
+            type="text"
+            @click="handleUpdate(scope.row,1)"
+            v-hasPermi="['qw:sop:edit']"
+          >修改规则</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            style="color: red;"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:sop:remove']"
+          >删除</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改企微sop对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="1000px" append-to-body>
+      <el-form ref="form" :model="form" :rules="rules" label-width="100px">
+        <el-form-item label="规则名称" prop="name">
+          <el-input v-model="form.name" placeholder="请输入规则名称" />
+        </el-form-item>
+        <el-form-item label="状态">
+          <el-radio-group v-model="form.status">
+            <el-radio
+              v-for="dict in statusOptions"
+              :key="dict.dictValue"
+              :label="parseInt(dict.dictValue)"
+            >{{dict.dictLabel}}</el-radio>
+          </el-radio-group>
+        </el-form-item>
+         <el-form-item label="类别" prop="type">
+         <el-radio-group v-model="form.type">
+             <el-radio
+               :label="1"
+             >个微</el-radio>
+             <el-radio
+               :label="2"
+             >企微</el-radio>
+         </el-radio-group>
+         </el-form-item>
+
+<!--        <div v-if="form.type==2">-->
+
+<!--        <el-form-item label="推送方式 ">-->
+<!--          <el-radio-group v-model="form.sendType" @input="handleSendTypeChange">-->
+<!--            <el-radio-->
+<!--              v-for="dict in sysQwSopType"-->
+<!--              :key="dict.dictValue"-->
+<!--              :label="parseInt(dict.dictValue)"-->
+<!--            >{{dict.dictLabel}}</el-radio>-->
+<!--          </el-radio-group>-->
+<!--        </el-form-item>-->
+<!--          <el-form-item label="选择员工" prop="qwUserIds" style="margin-top: 2%">-->
+<!--            <div>-->
+<!--              <el-button-->
+<!--                size="medium"-->
+<!--                icon="el-icon-circle-plus-outline"-->
+<!--                plain-->
+<!--                @click="handlelistUser(form.type,form.sendType)">请选择使用员工</el-button>-->
+<!--            </div>-->
+<!--            <div>-->
+<!--              <el-tag-->
+<!--                style="margin-left: 5px"-->
+<!--                size="medium"-->
+<!--                :key="id"-->
+<!--                v-for="id in userSelectList"-->
+<!--                closable-->
+<!--                :disable-transitions="false"-->
+<!--                @close="handleClosegroupUser(id)">-->
+<!--                <span v-for="list in companyUserList" :key="list.qwUserId" v-if="list.id==id">{{list.qwUserName}}</span>-->
+<!--              </el-tag>-->
+<!--            </div>-->
+<!--          </el-form-item>-->
+<!--          <el-form-item label="标签规则" prop="filterType">-->
+<!--            <el-radio-group v-model="form.filterType">-->
+<!--              <el-radio-->
+<!--                :label="1"-->
+<!--              >含全部标签</el-radio>-->
+<!--              <el-radio-->
+<!--                :label="2"-->
+<!--              >含任意标签</el-radio>-->
+<!--            </el-radio-group>-->
+<!--          </el-form-item>-->
+<!--          <el-form-item label="选择的标签" prop="tags">-->
+<!--            <el-select v-model="tags" remote multiple placeholder="请选择" filterable  style="width: 100%;">-->
+<!--              <el-option-->
+<!--                v-for="dict in tagList"-->
+<!--                :label="dict.name"-->
+<!--                :value="dict.tagId">-->
+<!--              </el-option>-->
+<!--            </el-select>-->
+
+<!--          </el-form-item>-->
+<!--          <el-form-item label="排除的标签" prop="excludeTags">-->
+<!--            <el-select v-model="excludeTags" remote multiple placeholder="请选择" filterable  style="width: 100%;">-->
+<!--              <el-option-->
+<!--                v-for="dict in tagList"-->
+<!--                :label="dict.name"-->
+<!--                :value="dict.tagId">-->
+<!--              </el-option>-->
+<!--            </el-select>-->
+<!--          </el-form-item>-->
+<!--        </div>-->
+<!--        <div v-if="form.type==1">-->
+<!--          <el-form-item label="推送方式 ">-->
+<!--            <el-tag type="success" v-model="form.sendType=2">AI插件</el-tag>-->
+<!--          </el-form-item>-->
+<!--        </div>-->
+
+        <el-form-item label="开始时间" prop="startTime">
+          <el-date-picker clearable size="small"
+            v-model="form.startTime"
+            type="date"
+            value-format="yyyy-MM-dd"
+            placeholder="选择开始时间">
+          </el-date-picker>
+        </el-form-item>
+        <el-form-item label="模板" prop="tempId">
+          <el-select v-model="form.tempId"  @focus="selectListSopTemp(form.sendType)" placeholder="请选择模板" v-loading="tempListLoading"   >
+            <el-option
+              v-for="dict in tempList"
+              :label="dict.name"
+              :value="dict.id">
+            </el-option>
+            <div v-if="tempListLoading" slot="prefix" class="select-prefix">正在查询相应模板...</div>
+          </el-select>
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+    <el-dialog :title="listUser.title" :visible.sync="listUser.open" width="1300px"  append-to-body>
+      <qwUserList ref="QwUserList" @selectUserList="selectUserList"></qwUserList>
+    </el-dialog>
+
+    <!-- 单独的修改时间   -->
+    <el-dialog :title="outTimeOpen.title" :visible.sync="outTimeOpen.open" width="500px">
+
+       <el-row>
+
+         <el-col style="margin-top: 3%">
+           <span>过期时间:</span>
+           <el-input-number  v-model="outTimeOpen.expiryTime"  :min="1" :max="100" ></el-input-number>
+           (小时)
+         </el-col>
+
+         <el-col  style="margin-top: 3%">
+           <span>是否开启客户评级:</span>
+             <el-switch
+               v-model="outTimeOpen.isRating"
+               active-color="#13ce66"
+               inactive-color="#ff4949"
+               :active-value="1"
+               :inactive-value="2">
+             </el-switch>
+             <span v-if="outTimeOpen.isRating == '1'" style="margin-left: 10px;color: #13ce66">已开启</span>
+             <span v-if="outTimeOpen.isRating == '2'" style="margin-left: 10px;color: #ff4949">已关闭</span>
+         </el-col>
+         <el-col style="margin-top: 3%">
+           <span>小转天数:</span>
+           <el-input-number  v-model="outTimeOpen.minConversionDay"></el-input-number>
+           (天)
+         </el-col>
+         <el-col style="margin-top: 3%">
+           <span>大转天数:</span>
+           <el-input-number  v-model="outTimeOpen.maxConversionDay"></el-input-number>
+           (天)
+         </el-col>
+         <el-col style="margin-top: 3%">
+           <span>发课开始天数:</span>
+           <el-input-number  v-model="outTimeOpen.courseDay"  :min="1" :max="100" ></el-input-number>
+           (天)
+           <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-top: 2%">
+             <i class="el-icon-info"></i>
+             作用于【催课看板】和【看课记录】的统计,由多少天开始计算。比如 模板里 第一天是先导课,第二天是正课,此处设置天数为2,则统计从第二天开始。如果无视模板天数,此处设置几天,则统计面板就从第几天开始统计
+           </div>
+         </el-col>
+         <el-col style="margin-top: 3%">
+           <el-card>
+             <el-radio-group v-model="outTimeOpen.isSampSend">
+               <el-row :gutter="20">
+                 <el-col>
+                   <el-radio
+                     border size="medium"
+                     :label="1"
+                     style="font-size: 16px; margin: 10px 0;font-weight: bold"
+                   >【官方群发】 按照【营期+插件补发】的形式发</el-radio>
+                   <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-top: 2%;margin-left: 3%">
+                     <i class="el-icon-question"></i>
+                     按照一个营期一个通用链接的形式,仅【营期详情】中(官方群发许可)为【是】的能用于官方群发,为【否】的按照 【sop插件】补发(ps:未注册用户全部不走官方群发,早上5点统一任务补发(官方群发的较少,补发及时))
+                   </div>
+                 </el-col>
+                 <el-col>
+                   <el-radio
+                     border size="medium"
+                     :label="2"
+                     style="font-size: 16px; margin: 10px 0;font-weight: bold"
+                   >【官方群发】 按照【营期+官方单链】的形式发</el-radio>
+                   <div style="color: #999;font-size: 14px;display: flex;align-items: center;margin-top: 2%;margin-left: 3%">
+                     <i class="el-icon-question"></i>
+                     按照一个营期一个通用链接的形式,仅【营期详情】中(官方群发许可)为【是】的能用于官方群发,为【否】的按照 【官方一对一链接】(ps:未注册用户也走官方群发,7点开始查询补发(官方群发较多,补发相对不及时))
+                   </div>
+                 </el-col>
+
+               </el-row>
+             </el-radio-group>
+           </el-card>
+         </el-col>
+         <el-row style="margin-top: 3%" >
+
+           <el-button type="primary" icon="el-icon-search" size="mini" @click="handleUpdateExpiryTime">确定修改</el-button>
+           <el-button icon="el-icon-refresh" size="mini" @click="resetUpdateExpiryTime">取消</el-button>
+         </el-row>
+
+
+       </el-row>
+    </el-dialog>
+    <!-- 单独的修改时间   -->
+    <el-dialog title="语音记录" :visible.sync="voiceForm.open" width="70%" append-to-body>
+      <el-row style="height: 600px">
+        <el-button
+          type="primary"
+          plain
+          icon="el-icon-plus"
+          size="mini"
+          @click="handleAdd"
+          v-hasPermi="['qw:sop:add']"
+        >新增</el-button>
+        <el-table border :data="voiceForm.list">
+          <el-table-column label="员工" align="center" prop="companyUserName" />
+          <el-table-column label="名称" align="center" prop="name" />
+          <el-table-column label="天数" align="center" prop="dayNum">
+            <template slot-scope="scope">
+              第{{scope.row.dayNum}}天
+            </template>
+          </el-table-column>
+          <el-table-column label="发送时间" align="center" prop="time" />
+          <el-table-column label="语音文本" align="center" prop="voiceTxt" />
+          <el-table-column label="语音时长" align="center" prop="duration">
+            <template slot-scope="scope">
+              {{scope.row.duration}}秒
+            </template>
+          </el-table-column>
+        </el-table>
+        <pagination
+          v-show="voiceForm.total>0"
+          :total="voiceForm.total"
+          :page.sync="voiceForm.queryParams.pageNum"
+          :limit.sync="voiceForm.queryParams.pageSize"
+          @pagination="voice(voiceForm.queryParams.id)"
+        />
+      </el-row>
+    </el-dialog>
+
+    <el-dialog :title="autoSopOpen.title" :visible.sync="autoSopOpen.open" width="1300px"  append-to-body>
+        <el-form ref="autoSopTimeFrom" :model="form.autoSopTime" :rules="autoSopTimeRules">
+          <el-form-item label="自动类型" prop="type"     style="margin: 2%; display: flex; align-items: center;">
+            <el-radio-group v-model="form.autoSopTime.autoSopType">
+              <el-radio
+                :label="1"
+              >当天开始</el-radio>
+              <el-radio
+                :label="2"
+              >次日开始</el-radio>
+              <el-radio
+                :label="3"
+              >分营期开始(正课)</el-radio>
+              <el-radio
+                :label="4"
+              >分营期开始(先导)</el-radio>
+            </el-radio-group>
+          </el-form-item>
+          <div style="display: flex; align-items: center; flex-wrap: nowrap;">
+            <div v-if="form.autoSopTime.autoSopType==1" style="display: flex; align-items: center">
+              <el-form-item
+                label="起始时间"
+                prop="autoStartTime"
+                label-width="100px"
+                style="margin: 2% 0;align-items: center;">
+                <el-time-select
+                  style="width: 120px;"
+                  placeholder="起始时间"
+                  v-model="form.autoSopTime.autoStartTime"
+                  :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00'
+                }">
+                </el-time-select>
+              </el-form-item>
+              <el-form-item
+                label="结束时间"
+                prop="autoEndTime"
+                label-width="100px"
+                style="margin: 2% 0; align-items: center; ">
+                <el-time-select
+                  style="width: 120px;"
+                  placeholder="结束时间"
+                  v-model="form.autoSopTime.autoEndTime"
+                  :picker-options="{
+                  start: '00:00',
+                  step: '00:15',
+                  end: '24:00',
+                  minTime: form.autoSopTime.autoEndTime
+                }">
+                </el-time-select>
+              </el-form-item>
+            </div>
+          </div>
+          <el-form-item  v-if="form.autoSopTime.autoSopType==1" label="过期消息是否发送" prop="autoSopSend"     style="margin: 2%; display: flex; align-items: center;">
+            <el-radio-group v-model="form.autoSopTime.autoSopSend">
+              <el-radio
+                :label="1"
+              >是</el-radio>
+              <el-radio
+                :label="2"
+              >否</el-radio>
+            </el-radio-group>
+          </el-form-item>
+
+          <div style="display: flex; align-items: center; flex-wrap: nowrap;" v-if="form.autoSopTime.autoSopType==3 || form.autoSopTime.autoSopType==4">
+            <el-form-item
+              label="选择一个或多个日期"
+              prop="autoDateTime"
+              label-width="150px">
+              <el-date-picker
+                size="large"
+                type="dates"
+                style="width: 600px"
+                v-model="form.autoSopTime.autoDateTime"
+                value-format="yyyy-MM-dd"
+                placeholder="选择一个或多个日期【ps:请先选择开始时间,不得小于任务开始时间】"
+                @change="sortSelectedDates(form.autoSopTime.autoDateTime)"
+                :disabled="!form.startTime">
+              </el-date-picker>
+            </el-form-item>
+          </div>
+
+
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==1"
+            title="起始时间-结束时间之内的,当天立即创建SOP,时间之外的 次日创建SOP"
+            type="warning"
+            style="font-size: 30px; margin-top: 3%;"
+            :closable="false"
+            show-icon>
+          </el-alert>
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==3"
+            type="warning"
+            style="margin-top: 3%;"
+            :closable="false"
+            show-icon>
+            <template #title>
+              <span style="font-size: 23px; line-height: 1.5;">
+                  此功能用于正课区分营期,可以选择你想要开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                  例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                  用户1月1日-1月4日添加(添加上对应标签)则会进入1月5日的营期,1月5日-1月9日添加则会进入1月10日营期,1月5日自动给用户发送第一节课程
+              </span>
+            </template>
+          </el-alert>
+          <el-alert
+            v-if="form.autoSopTime.autoSopType==4"
+            title=""
+            type="warning"
+            style="font-size: 30px; margin-top: 3%;"
+            :closable="false"
+            show-icon>
+            <template #title>
+              <span style="font-size: 23px; line-height: 1.5;">
+                    此功能用于先导课区分营期,可以选择先导课开始营期的具体时间,选择后在此期间添加的用户将会自动进入对应营期。
+                    例如今天1月1日,选择时间是1月1日,1月5日,1月10日。
+                    用户1月1日-1月4日添加(添加上对应标签)则会进入1月1日的营期,1月5日-1月9日添加则会进入1月5日营期,自动给用户匹配上对应应该听到天数的时间节点
+              </span>
+            </template>
+          </el-alert>
+        </el-form>
+        <div slot="footer" class="dialog-footer">
+          <el-button type="primary" @click="submitAutoSopTimeFrom">确 定</el-button>
+          <el-button @click="cancelAutoSopTime">取 消</el-button>
+        </div>
+    </el-dialog>
+    <!--  执行详情  -->
+    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="85%" style="font-weight: bolder">
+      <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
+    </el-drawer>
+    <el-dialog :title="sendMsgOpen.title" :visible.sync="sendMsgOpen.open"  width="1000px" append-to-body>
+      <el-form ref="msgForm" :model="msgForm" :rules="msgRules" label-width="100px">
+        <el-form-item label="群">
+          <el-select  v-model="msgForm.chatIds" placeholder="请选择群" size="mini" multiple>
+            <el-option
+              v-for="chatId in sendMsgOpen.chatIds"
+              :key="chatId"
+              :label="chatList.filter(e => e.chatId == chatId) && chatList.filter(e => e.chatId == chatId).length > 0 ? chatList.filter(e => e.chatId == chatId)[0].name : ''"
+              :value="chatId"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="发送类型">
+          <el-radio-group v-model="msgForm.sendType">
+            <el-radio :label="1">群</el-radio>
+            <el-radio :label="2">群成员</el-radio>
+          </el-radio-group>
+        </el-form-item>
+        <el-form-item label="选择课程">
+          <el-select  v-model="msgForm.courseId" placeholder="请选择课程" style=" margin-right: 10px;" size="mini"  @change="courseChange()">
+            <el-option
+              v-for="dict in courseList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+          <el-select  v-model="msgForm.videoId" placeholder="请选择小节" size="mini" style=" margin-right: 10px;" @change="videoIdChange()"  >
+            <el-option
+              v-for="dict in videoList"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+          <el-select  v-model="msgForm.courseType" placeholder="请选择消息类型" size="mini" style=" margin-right: 10px;">
+            <el-option
+              v-for="dict in sysFsSopWatchStatus"
+              :key="dict.dictValue"
+              :label="dict.dictLabel"
+              :value="parseInt(dict.dictValue)"
+            />
+          </el-select>
+        </el-form-item>
+        <el-form-item label="规则" prop="setting"  >
+          <div v-for="(item, index) in setting" :key="index" style="background-color: #fdfdfd; border: 1px solid #e6e6e6; margin-bottom: 20px;">
+            <el-row>
+              <el-col :span="22">
+                <el-form :model="item" label-width="70px">
+                  <el-form-item label="内容类别" style="margin: 2%">
+                    <el-radio-group  v-model="item.contentType">
+                      <el-radio   :label="item.dictValue" v-for="item in sysQwSopAiContentType"  @change="handleContentTypeChange()">{{item.dictLabel}}</el-radio>
+                    </el-radio-group>
+                  </el-form-item>
+                  <el-form-item label="内容" style="margin-bottom: 2%" >
+                    <el-input v-if="item.contentType == 1 " v-model="item.value" type="textarea" :rows="3" placeholder="内容" style="width: 90%;margin-top: 10px;"/>
+
+                    <ImageUpload v-if="item.contentType == 2 " v-model="item.imgUrl" type="image" :num="1"  :width="150" :height="150" />
+
+                    <div v-if="item.contentType == 3 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:"  label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:"   label-width="100px" >
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:"   label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                        </el-form-item>
+                        <el-form-item label="链接地址:"  label-width="100px" >
+                          <el-tag type="warning" v-model="item.isBindUrl=1">选择的课程小节 即为卡片链接地址</el-tag>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                    <div v-if="item.contentType == 4">
+
+                    </div>
+                    <div v-if="item.contentType == 5 ">
+
+                      <el-form-item label="上传文件:" prop="fileUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.fileUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessFile(res, file, item)"
+                          :before-upload="beforeAvatarUploadFile">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <el-link v-if="item.fileUrl" type="primary" :href="downloadUrl(item.fileUrl)" download>
+                          {{item.fileUrl}}
+                        </el-link>
+                      </el-form-item>
+
+                    </div>
+
+                    <div v-if="item.contentType == 6 ">
+                      <el-form-item label="上传视频:" prop="videoUrl" label-width="100px">
+                        <el-upload
+                          v-model="item.videoUrl"
+                          class="avatar-uploader"
+                          :action="uploadUrl"
+                          :show-file-list="false"
+                          :on-success="(res, file) => handleAvatarSuccessVideo(res, file, item)"
+                          :before-upload="beforeAvatarUploadVideo">
+                          <i class="el-icon-plus avatar-uploader-icon"></i>
+                        </el-upload>
+                        <video v-if="item.videoUrl"
+                               :src="item.videoUrl"
+                               controls style="width: 200px;height: 100px">
+                        </video>
+                      </el-form-item>
+                    </div>
+                    <div v-if="item.contentType == 7 ">
+                      <el-input
+                        v-model="item.value"
+                        type="textarea" :rows="3" maxlength="66" show-word-limit
+                        placeholder="输入要转为语音的内容" style="width: 90%;margin-top: 10px;"
+                        @input="handleInputVideoText(item.value,item)"/>
+                    </div>
+
+                    <div v-if="item.contentType == 10 ">
+                      <el-card class="box-card">
+                        <el-form-item label="链接标题:"  label-width="100px">
+                          <el-input v-model="item.linkTitle" placeholder="请输入链接标题" style="width: 90%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接描述:"   label-width="100px" >
+                          <el-input type="textarea" :rows="3" v-model="item.linkDescribe" placeholder="请输入链接描述" style="width: 90%;margin-top: 1%;"/>
+                        </el-form-item>
+                        <el-form-item label="链接封面:"   label-width="100px">
+                          <ImageUpload v-model="item.linkImageUrl" type="image" :num="1" :file-size="2" :width="150" :height="150" style="margin-top: 1%;" />
+                        </el-form-item>
+                        <el-form-item label="链接地址:"  label-width="100px" >
+                          <el-tag type="warning" >链接地址自动生成</el-tag>
+                        </el-form-item>
+                      </el-card>
+                    </div>
+                  </el-form-item>
+
+                  <el-form-item label="添加短链" v-if="item.contentType == 1 "  >
+                    <el-tooltip content="请先根据课程选定课程小节之后再添加" effect="dark" :disabled="!!msgForm.videoId">
+                      <el-switch
+                        v-model="item.isBindUrl"
+                        :disabled="!msgForm.videoId"
+                        active-color="#13ce66"
+                        inactive-color="#DCDFE6"
+                        active-value="1"
+                        inactive-value="2">
+                      </el-switch>
+                    </el-tooltip>
+
+                    <span v-if="item.isBindUrl == '1'" style="margin-left: 10px; color: #13ce66">添加URL</span>
+                    <span v-if="item.isBindUrl == '2'" style="margin-left: 10px; color: #b1b4ba">不加URL</span>
+                  </el-form-item>
+                  <el-form-item label="课节过期时间" v-if="item.isBindUrl == '1'
+                                                          && item.contentType != 2
+                                                          && item.contentType != 5
+                                                          && item.contentType != 6
+                                                          && item.contentType != 8
+                                                          && item.contentType != 9
+                                                          && item.contentType != 10"
+                                style="margin-top: 1%" label-width="100px">
+                    <el-row>
+                      <el-input-number  v-model="item.expiresDays"  :min="1" :max="100" ></el-input-number>
+                      (天)
+                    </el-row>
+                    <el-row>
+                      <span class="tip">填写0或不填时,默认为系统配置的默认时间</span>
+                    </el-row>
+                  </el-form-item>
+                </el-form>
+              </el-col>
+              <el-col :span="1" :offset="1">
+                <i class="el-icon-delete" @click="delSetList(index)" style="margin-top: 20px;" v-if="setting.length>1"></i>
+              </el-col>
+            </el-row>
+          </div>
+          <el-link type="primary" class="el-icon-plus" :underline="false" @click='addSetList()'  >添加内容</el-link>
+
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitMsgForm">确 定</el-button>
+        <el-button @click="cancelMsgForm">取 消</el-button>
+      </div>
+    </el-dialog>
+  </div>
+</template>
+
+<script>
+import {
+  listMySop,
+  addSop,
+  courseList,
+  videoList,
+  delSop,
+  exportSop,
+  getSopVoiceList,
+  listSop,
+  updateAutoSopTime,
+  updateSop,
+  updateSopStatus,
+  updateStatus
+} from "@/api/qw/sop";
+import {listSopTemp} from "@/api/qw/sopTemp";
+import {getQwAllUserList, listUser} from '@/api/company/companyUser'
+import qwUserList from '@/views/qw/user/qwUserList.vue'
+import ImageUpload from "@/views/qw/sop/ImageUpload";
+import CustomerGroupDetails from '@/views/qw/groupMsg/customerGroupDetails.vue'
+import sopLogsDetails from '@/views/qw/sopLogs/sopLogsList.vue'
+import {listTag,} from "@/api/qw/tag";
+import {sendMsgSop} from "@/api/qw/sopUserLogsInfo";
+import {getMyQwCompanyList} from "@/api/qw/user";
+import {allList} from "@/api/qw/groupChat";
+
+export default {
+  name: "Sop",
+    components: { CustomerGroupDetails, qwUserList,ImageUpload,sopLogsDetails},
+  data() {
+    return {
+      // 存储每一行的展开状态
+      expandedRows: {},
+      //模板查询
+      tempListLoading:false,
+      // 遮罩层
+      loading: false,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      //选中的map
+      selectionMap:{},
+      myQwCompanyList:[],
+      //销售员工列表
+      companyUserLists:[],
+      videoList:[],
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      uploadUrlByVoice:process.env.VUE_APP_BASE_API+"/common/uploadOSSByHOOKVoice",
+      msgForm:{
+        videoId:null,
+        courseId:null,
+        courseType:null,
+        userIdParam:null,
+        sendType:1,
+        setting:null,
+        ids:null,
+        sopId: null,
+        startTime: null,
+        chatIds: [],
+        isRegister:2
+      },
+      sendMsgOpen:{
+        title:'一键批量群发',
+        open:false,
+        row: {},
+        ids:null,
+      },
+      msgRules:{},
+      courseList:[],
+      // videoList:[],
+      tags:null,
+      excludeTags:null,
+      // 非单个禁用
+      single: true,
+      setting:[],
+      tagList:[],
+      tempList:[],
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 企微sop表格数据
+      sopList: [],
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+      companyUserList:[],
+      // 状态字典
+      statusOptions: [],
+      //sop状态
+      sysSopStatus: [],
+
+      autoSopOpen:{
+        title:'',
+        open:false,
+        id:null,
+        isAutoSop:null,
+      },
+      outTimeOpen:{
+        title:'',
+        open:false,
+        id:null,
+        tempId:null,
+        expiryTime:null,
+        isRating:null,
+        isSampSend:null,
+      },
+      voiceForm:{
+        list:[],
+        open:false,
+        total:0,
+        queryParams: {
+          pageNum: 1,
+          pageSize: 10,
+          id: null,
+        },
+      },
+
+      //企微SOP发送类型
+      sysQwSopType: [],
+      chatList: [],
+
+      //SOP课程观看状态
+      sysFsSopWatchStatus:[],
+
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        id: null,
+        name: null,
+        status: null,
+        sendType:null,
+        type: null,
+        qwUserIds: null,
+        setting: null,
+        createBy: null,
+        corpId: null,
+        createTime: null
+      },
+      sopLogsDialog:{
+        title:'',
+        open:false,
+        sopLogsForm:[],
+      },
+      // 表单参数
+      form: {
+        autoSopTime:{ autoSopType:2,autoStartTime:'00:00',autoEndTime:'24:00',autoSopSend:2,autoDateTime:[]},
+      },
+      userSelectList:[],
+      listUser:{
+        title:"",
+        open:false
+      },
+      // 表单校验
+      rules: {
+        name:[ { required: true, message: "名称不能为空", trigger: "submit" }],
+        type:[ { required: true, message: "不能为空", trigger: "submit" }],
+        sendType:[ { required: true, message: "不能为空", trigger: "submit" }],
+        startTime:[ { required: true, message: "开始时间不能为空", trigger: "submit" }],
+        tempId:[ { required: true, message: "模板不能为空", trigger: "submit" }],
+      },
+      autoSopTimeRules:{
+        autoSopType:[ { required: true, message: "选项不能为空", trigger: "submit" }],
+        autoStartTime:[ { required: true, message: "起始时间不能为空", trigger: "submit" }],
+        autoEndTime:[ { required: true, message: "结束时间不能为空", trigger: "submit" }],
+        autoDateTime:[ { required: true, message: "日期不能为空", trigger: "submit" }],
+      }
+    };
+  },
+  created() {
+
+    this.getDicts("sys_sop_status").then(response => {
+      this.sysSopStatus = response.data;
+    });
+
+
+    this.getDicts("sys_qwSopAi_contentType").then(response => {
+      this.sysQwSopAiContentType = response.data;
+    });
+    this.getDicts("sys_fs_sop_watch_status").then(response => {
+      this.sysFsSopWatchStatus = response.data;
+    });
+    this.getDicts("sys_company_status").then(response => {
+      this.statusOptions = response.data;
+    });
+
+    this.getDicts("sys_qw_sop_type").then(response => {
+      this.sysQwSopType = response.data;
+    });
+
+
+    listUser().then(res => {
+        this.companyUserLists = res.rows;
+      }
+    );
+
+    courseList().then(response => {
+      this.courseList = response.list;
+    });
+
+
+    getMyQwCompanyList().then(response => {
+      this.myQwCompanyList = response.data;
+      if(this.myQwCompanyList!=null){
+        this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+        this.refreshData(this.queryParams.corpId);
+        this.getList();
+      }
+    });
+
+
+
+  },
+  watch:{
+    userSelectList(newList) {
+      this.form.qwUserIds = newList.map(item => item.id);
+    }
+  },
+  methods: {
+    voice(id){
+      this.voiceForm.queryParams.id = id;
+      getSopVoiceList(this.voiceForm.queryParams).then(res => {
+        this.voiceForm.list = res.rows;
+        this.voiceForm.total = res.total;
+        this.voiceForm.open = true;
+      })
+    },
+    // 单元格点击事件
+    handleCellClick(row, column, cell, event) {
+      // 判断是否为规则编号列
+      if (column.property === 'id') {
+        this.handleRowClick(row); // 触发规则编号点击事件
+      }
+    },
+    // handleUpdateTags(){
+    //
+    // },
+    // handleUpdateQwUser(){
+    //
+    // },
+    // handleUpdateSopTemp(){
+    //
+    // },
+    handleRowClick(row, column, event) {
+      // 判断状态是否符合条件
+      if (row.status == 2 || row.status == 0 || row.status == 3 || row.status == 4) {
+        this.handleUpdate(row, 2);
+      }
+    },
+    // 切换某一行的展开状态
+    toggleRow(row) {
+      this.$set(this.expandedRows, row.id, !this.expandedRows[row.id]);
+    },
+    getSwitchVal(status) {
+      return [2, 3, 4].includes(status) ? 2 : 0;
+    },
+
+    onSwitchChange(row, val) {
+      this.loading=true;
+      // ① 调用接口更新后端
+      updateSopStatus({ id: row.id, status: val })
+        .then(() => {
+          row.status = val;
+          this.loading=false;
+          this.$message.success("切换成功!");
+        })
+        .catch(() => {
+          this.loading=false;
+          this.$message.error("操作失败,请重试");
+        });
+    },
+
+    switchSopStatusChange(row,checked){
+
+      let loadingRock = this.$loading({
+        lock: true,
+        text: '暂停中-请勿刷新页面-重复点击',
+        spinner: 'el-icon-loading',
+        background: 'rgba(0, 0, 0, 0.7)'
+      });
+
+
+      updateSopStatus({id:row,status:checked}).then(response => {
+        this.$message.success("修改成功");
+        this.getList();
+      }).finally(res=>{
+        loadingRock.close();
+      })
+
+
+    },
+
+    //新客户自动创建sop
+    switchAutoSopChange(row,checked){
+
+      this.form.startTime=row.startTime;
+      this.form.autoSopTime=JSON.parse(row.autoSopTime);
+
+      if (checked==1) {
+        this.autoSopOpen.title='请设置相应时间段'
+        this.autoSopOpen.open=true;
+        this.autoSopOpen.id=row.id;
+        this.autoSopOpen.isAutoSop=1;
+      }else {
+        this.form.autoSopTime.createTime=this.formatDateTo24HourString(new Date());
+        updateAutoSopTime({id:row.id,isAutoSop:checked,autoSopTime: JSON.stringify(this.form.autoSopTime)}).then(response => {
+          this.getList();
+        })
+      }
+
+
+    },
+     updateCorpId(){
+       this.reset();
+       this.refreshData(this.queryParams.corpId);
+       this.queryParams.qwUserId = null;
+       this.getList();
+     },
+
+    //刷新部分数据
+    refreshData(row){
+      getQwAllUserList(row).then(response => {
+        this.companyUserList = response.data;
+      });
+
+      listTag({corpId:row}).then(response => {
+        this.tagList = response.rows;
+      });
+
+      if (row != null) {
+        allList(row).then(e => {
+          this.chatList = e.data;
+        })
+      }
+    },
+
+    //查询模板
+    selectListSopTemp(type){
+      this.tempListLoading = true; // 开始查询,显示加载提示
+      listSopTemp({sendType:type}).then(response => {
+        this.tempList = response.rows;
+        this.tempListLoading = false;
+      });
+    },
+    handlelistUser(type,sendType){
+
+       setTimeout(() => {
+         this.$refs.QwUserList.getDetails(this.queryParams.corpId,type,sendType);
+       }, 1);
+       this.listUser.title="选择企业成员"
+       this.listUser.open=true;
+
+    },
+    selectUserList(list){
+      this.listUser.open=false;
+      list.forEach(obj => {
+        if (!this.userSelectList.some(item => item == obj.id)) {
+          this.userSelectList.push(obj.id);
+        }
+      });
+
+    },
+
+    //修改过期时间
+    handleUpdateOutTime(val){
+        this.outTimeOpen.title="修改过期时间/评级";
+        this.outTimeOpen.id=val.id;
+        this.outTimeOpen.tempId=val.tempId;
+        this.outTimeOpen.expiryTime=val.expiryTime;
+        this.outTimeOpen.isRating=val.isRating;
+        this.outTimeOpen.minConversionDay=val.minConversionDay;
+        this.outTimeOpen.maxConversionDay=val.maxConversionDay;
+        this.outTimeOpen.courseDay=val.courseDay;
+        this.outTimeOpen.isSampSend=val.isSampSend;
+        this.outTimeOpen.open=true;
+    },
+
+    handleUpdateExpiryTime(){
+      updateSop({id:this.outTimeOpen.id,tempId:this.outTimeOpen.tempId,expiryTime:this.outTimeOpen.expiryTime,
+        isRating: this.outTimeOpen.isRating,minConversionDay: this.outTimeOpen.minConversionDay,maxConversionDay: this.outTimeOpen.maxConversionDay,
+        courseDay: this.outTimeOpen.courseDay,isSampSend:this.outTimeOpen.isSampSend}).then(response => {
+        this.msgSuccess("修改成功");
+        this.resetUpdateExpiryTime()
+        this.getList();
+      });
+    },
+    resetUpdateExpiryTime(){
+      this.outTimeOpen={
+          title:'',
+          open:false,
+          id:null,
+          expiryTime:null,
+          courseDay:null,
+          minConversionDay:null,
+          maxConversionDay:null,
+          isRating:null,
+          isSampSend:null,
+      }
+    },
+    addContent(index){
+      // this.setting[index].content.push({type:1})
+      if (this.form.sendType==2){
+        this.setting[index].content.push({ type: 1 });
+      }else {
+        if (this.setting[index].content.length < 9) {
+          this.setting[index].content.push({ type: 1 });
+        } else {
+          this.$message({
+            message: '最多只能添加 9 个内容',
+            type: 'warning'
+          });
+        }
+      }
+
+    },
+
+    //选择变动时的变动
+    handleSendTypeChange(val){
+      this.tempList=[];
+      this.form.tempId=null;
+      if (val==1) {
+        // 遍历 this.setting 数组并清空每个对象的 content 属性
+        this.setting.forEach(item => {
+          if (item.content.length > 9) {
+            item.content = item.content.slice(0, 9); // 保留前 9 个元素
+          }
+        });
+      }
+    },
+    delContent(index,contentIndex){
+      this.setting[index].content.splice(contentIndex,1)
+    },
+
+    //添加SOP规则类型
+    handleCommand(command){
+      this.setting.push({sopType:command,ruleType:null,name:null,type:1,content:[],isBindUrl:1,url:null,day:"0",hour:"0",minute:"0",time:""})
+    },
+
+    //添加课程SOP
+    handleCrouseCommand(command,val){
+      console.log("command",command)
+      console.log("val",val)
+    },
+
+    delSetting(index){
+      this.setting.splice(index,1)
+    },
+    handleClosegroupUser(list){
+      const index = this.userSelectList.findIndex(t => t === list);
+      if (index !== -1) {
+        this.userSelectList.splice(index, 1);
+      }
+    },
+    /** 查询企微sop列表 */
+    getList() {
+      this.loading = true;
+      listMySop(this.queryParams).then(response => {
+
+        this.sopList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+
+
+        listSopTemp().then(res => {
+          this.tempList = res.rows;
+        });
+
+      });
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+
+    // courseChange(){
+    //   videoList(this.form.courseId).then(response => {
+    //     this.videoList = response.list;
+    //   });
+    // },
+
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        name: null,
+        status: 1,
+        sendType:2,
+        type: 2,
+        filterType:2,
+        qwUserIds: null,
+        corpId: null,
+        setting: null,
+        createBy: null,
+        createTime: null,
+        isAutoSop:null,
+        autoSopTime:{ autoSopType:2,autoStartTime:'00:00',autoEndTime:'24:00',autoSopSend:2,autoDateTime:[]},
+      };
+      this.resetForm("form");
+      this.tags = null;
+      this.excludeTags = null;
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+      this.refreshData(this.queryParams.corpId);
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.selectionMap=selection;
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.$router.push('/qw/sop/addSop/'+this.queryParams.corpId)
+      // this.open = true;
+      // this.setting=[]
+      // this.userSelectList=[]
+      // this.title = "添加企微sop";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row,type) {
+      this.$router.push('/qw/sop/updateSop/'+row.id+'/'+type)
+    },
+
+
+    /**
+    * 查看SOP任务内营期
+    */
+    selectSchedule(row){
+      // type 2:我的sop 1:部门sop/销售公司sop
+      const query = {
+        id: row.id,
+        name: row.name,
+        tempId: row.tempId,
+        filterMode: row.filterMode,
+        corpId: row.corpId,
+        type:2,
+      }
+      // 使用 params 传递参数
+      this.$router.push({
+        path: `/qw/sopUserLogs/sopUserLogsSchedule/${query.id}`,
+        query // 如果需要传递更多参数,可以使用 query
+      });
+    },
+
+
+    /**
+    * 查看营期内详情
+    */
+    handleScheduleDetail(row){
+        this.sopLogsDialog.title='规则执行详情';
+        this.sopLogsDialog.open=true;
+        this.sopLogsDialog.sopLogsForm=row;
+    },
+
+    /**
+    * 查看模板
+    */
+    handleQueryDetails(row){
+      this.$router.push(`/qw/sopTemp/updateSopTemp/${row.tempId}/3`)
+    },
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if(this.userSelectList.length<=0){
+            return this.$message("请选择员工")
+          }
+          this.form.qwUserIds = this.userSelectList.join(",");
+          this.form.corpId=this.queryParams.corpId;
+          if (this.tags!=null && this.tags.length>0 ){
+            this.form.tags=(this.tags).toString()
+          }else {
+            return  this.$message.error("选择的标签不能为空!!请选择筛选的标签")
+          }
+          if (this.excludeTags!=null){
+            this.form.excludeTags=(this.excludeTags).toString()
+          }
+
+          this.form.setting=JSON.stringify(this.setting)
+          if (this.form.id != null) {
+            updateSop(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addSop(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    sortSelectedDates(dates){
+
+      if (!this.form.startTime) {
+        this.$message.error("请先选择开始时间!不得小于任务开始时间");
+        this.form.autoSopTime.autoDateTime = [];
+        return;
+      }
+
+      // 1. 过滤掉 < startTime 的日期
+      const validDates = dates.filter(date => new Date(date) >= new Date(this.form.startTime));
+
+      // 2. 如果有被过滤掉的日期,提示用户
+      if (validDates.length < dates.length) {
+        this.$message.warning(`已自动移除小于 ${this.form.startTime} 的日期!`);
+      }
+
+      // 3. 升序排序
+      validDates.sort((a, b) => new Date(a) - new Date(b));
+
+      // 4. 更新数据
+      this.form.autoSopTime.autoDateTime = validDates;
+    },
+
+    submitAutoSopTimeFrom(){
+
+      if (this.form.autoSopTime.autoSopType==3 || this.form.autoSopTime.autoSopType==4){
+        if(this.form.autoSopTime.autoDateTime==null || this.form.autoSopTime.autoDateTime=="") {
+          return  this.$message.error("选择的日期不能为空!!请选择日期")
+        }
+      }
+
+      this.$refs["autoSopTimeFrom"].validate(valid => {
+        if (valid) {
+          this.autoSopOpen.open=false
+          this.form.autoSopTime.createTime=this.formatDateTo24HourString(new Date());
+          updateAutoSopTime({id:this.autoSopOpen.id,isAutoSop:this.autoSopOpen.isAutoSop,autoSopTime: JSON.stringify(this.form.autoSopTime)}).then(response => {
+            this.msgSuccess("修改成功");
+          });
+        }
+        this.getList();
+        this.reset();
+      });
+    },
+
+    formatDateTo24HourString(date) {
+      let year = date.getFullYear();
+      let month = ('0' + (date.getMonth() + 1)).slice(-2); // 月份需要加 1 并补零
+      let day = ('0' + date.getDate()).slice(-2); // 日需要补零
+      let hours = ('0' + date.getHours()).slice(-2); // 小时需要补零
+      let minutes = ('0' + date.getMinutes()).slice(-2); // 分钟需要补零
+      let seconds = ('0' + date.getSeconds()).slice(-2); // 秒需要补零
+
+      return `${year}-${month}-${day} ${hours}:${minutes}:${seconds}`;
+    },
+    cancelAutoSopTime(){
+      this.autoSopOpen.open=false
+      this.getList();
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const ids = row.id || this.ids;
+      this.$confirm('是否确认删除企微sop编号为"' + ids + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delSop(ids);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+    /**
+    * 批量执行SOP任务
+    */
+    handleExecute(){
+
+        const ids = this.selectionMap
+          .filter(item => item.status === 1)
+          .map(item => item.id);
+
+        if (ids.length === 0) {
+          this.msgError("选择的任务已经执行或正在执行,请选择待执行的任务");
+          return;
+        }
+
+        this.$confirm('是否确认立即执行sop编号为"' + ids + '"的数据项?', "警告【只能立即执行(待执行的)】", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+            this.loading = true;
+            // 执行更新操作
+            return updateStatus(ids); // 返回一个Promise,直接在此链式调用
+        }).then(res => {
+          let msg = "";
+
+          // 判断成功和失败的任务,并显示消息
+          if (res.suc.length > 0) {
+            msg += "执行成功的SOP任务【" + res.suc.join(", ") + "】。<br>"; // 优化:将数组转换为字符串
+          }
+          if (res.err.length > 0) {
+            msg += "失败的SOP任务【" + res.err.join(", ") + "】,原因是已经执行或正在执行。<br>";
+          }
+
+          // 显示确认框,显示成功与失败信息
+          return this.$confirm(msg, "提示", {
+            confirmButtonText: "确定",
+            cancelButtonText: "取消",
+            type: "warning",
+            dangerouslyUseHTMLString: true // 允许HTML标签
+          });
+
+        }).then(() => {
+          // 操作完成后刷新列表,并显示成功信息
+          this.getList();
+          this.msgSuccess("执行完成");
+        }).catch(() => {
+          // 处理任何异常,操作取消或者失败时显示提示信息
+          this.msgError("操作失败,请重试");
+        }).finally(() => {
+          this.loading = false; // 操作完成后关闭加载状态
+        });
+
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企微sop数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(() => {
+          this.exportLoading = true;
+          return exportSop(queryParams);
+        }).then(response => {
+          this.download(response.msg);
+          this.exportLoading = false;
+        }).catch(() => {});
+    },
+    courseChange() {
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && this.setting[i].contentType == 3 && this.msgForm.courseId != null) {
+            this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+        }
+
+      }
+      videoList(this.msgForm.courseId).then(response => {
+        this.videoList=response.list;
+      });
+    },
+    handleSendMsg(row){
+      let chatList = row.chatId ? row.chatId.split(",") : [];
+      this.sendMsgOpen.id = row.id;
+      this.sendMsgOpen.row = row;
+      this.sendMsgOpen.chatIds = chatList;
+      this.msgForm.chatIds = chatList;
+      this.sendMsgOpen.open = true;
+    },
+    handleContentTypeChange() {
+
+      //如果是链接的才上
+      if (this.msgForm.courseId != null ) {
+        const selectedCourse = this.courseList.find(course => parseInt(course.dictValue) === this.msgForm.courseId);
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的标题/封面上值
+          if (selectedCourse && this.setting[i].contentType == 3 && this.msgForm.courseId != null) {
+            this.$set(this.setting[i], 'linkTitle', selectedCourse.dictLabel);
+            this.$set(this.setting[i], 'linkImageUrl', selectedCourse.dictImgUrl);
+          }
+
+        }
+
+      }
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && this.setting[i].contentType == 3  && this.msgForm.videoId != null) {
+            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+    handleAvatarSuccessFile(res, file, item) {
+      if (res.code === 200) {
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'fileUrl', res.url);
+      } else {
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+
+    handleAvatarSuccessVideo(res, file, item) {
+      if(res.code==200){
+        // 使用 $set 确保响应式更新
+        this.$set(item, 'videoUrl', res.url);
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+
+    beforeAvatarUploadVideo(file){
+      const isLt30M = file.size / 1024 / 1024 < 10;
+      const isMP4 = file.type === 'video/mp4';
+
+      if (!isMP4) {
+        this.$message.error('仅支持上传 MP4 格式的视频文件!');
+        return false;
+      }
+
+      if (!isLt30M) {
+        this.$message.error('上传大小不能超过 10MB!');
+        return false;
+      }
+
+      return true;
+    },
+
+    handleInputVideoText(value,content){
+      // 允许的字符:中文、英文(大小写)、数字和指定标点符号(,。!?)
+      const regex = /^[\u4e00-\u9fa5,。!?,!?]+$/;
+
+      // 删除不符合条件的字符
+      const filteredValue = value.split('').filter(char => regex.test(char)).join('');
+
+      this.$set(content, 'value', filteredValue);
+
+    },
+    delSetList(index){
+      this.setting.splice(index,1)
+    },
+    addSetList(){
+      const newSetting = {
+        contentType:'1',
+        value: '',
+      };
+      // 将新设置项添加到 content.setting 数组中
+      this.setting.push(newSetting);
+
+    },
+    submitMsgForm(){
+      this.$refs["msgForm"].validate(valid => {
+        if (valid) {
+          this.msgForm.setting=JSON.stringify(this.setting)
+          this.msgForm.sopId=this.sendMsgOpen.row.id;
+          this.msgForm.filterMode=this.sendMsgOpen.row.filterMode;
+
+          if (this.setting.length <= 0) {
+            return this.$message.error("请添加规则")
+          }
+          if (this.msgForm.courseId===null || this.msgForm.courseId===''){
+            return this.$message.error("课程不能为空")
+          }
+
+          if (this.msgForm.videoId===null || this.msgForm.videoId===''){
+            return this.$message.error("课节不能为空")
+          }
+
+          if (this.msgForm.courseType===null || this.msgForm.courseType===''){
+            return this.$message.error("消息类型不能为空")
+          }
+
+          for (let i = 0; i < this.setting.length; i++) {
+            if (this.setting[i].contentType == 1 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("内容不能为空")
+            }
+            if (this.setting[i].contentType == 2 && (this.setting[i].imgUrl == null || this.setting[i].imgUrl == "")) {
+              return this.$message.error("图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkTitle == null || this.setting[i].linkTitle == "")) {
+              return this.$message.error("链接标题不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkDescribe == null || this.setting[i].linkDescribe == "")) {
+              return this.$message.error("链接描述不能为空")
+            }
+            if (this.setting[i].contentType == 3 && (this.setting[i].linkImageUrl == null || this.setting[i].linkImageUrl == "")) {
+              return this.$message.error("链接图片不能为空")
+            }
+            if (this.setting[i].contentType == 3 && this.setting[i].type == 1 && (this.setting[i].linkUrl == null || this.setting[i].linkUrl == "")) {
+              return this.$message.error("链接地址不能为空")
+            }
+            if (this.setting[i].contentType == 5 && (this.setting[i].fileUrl == null || this.setting[i].fileUrl == "")) {
+              return this.$message.error("文件不能为空")
+            }
+            if (this.setting[i].contentType == 6 && (this.setting[i].videoUrl == null || this.setting[i].videoUrl == "")) {
+              return this.$message.error("视频不能为空")
+            }
+            if (this.setting[i].contentType == 7 && (this.setting[i].value == null || this.setting[i].value == "")) {
+              return this.$message.error("语音不能为空")
+            }
+          }
+
+          this.sendMsgOpen.open = false;
+
+          const loading = this.$loading({
+            lock: true,
+            text: '正在执行中请稍后~~请不要刷新页面!!',
+            spinner: 'el-icon-loading',
+            background: 'rgba(0, 0, 0, 0.7)'
+          });
+
+          sendMsgSop(this.msgForm).then(response => {
+            this.msgSuccess("一键群发成功");
+            loading.close();
+            this.setting=[];
+            this.msgForm = {
+              videoId:null,
+              courseId:null,
+              courseType:null,
+              setting:null,
+              isRegister:2,
+              sendType:1,
+              filterMode:2,
+
+            }
+            this.getList();
+          }).finally(()=>{
+            loading.close();
+          });
+
+        }
+      });
+    },
+    cancelMsgForm(){
+      this.sendMsgOpen.open = false;
+      this.resetSendMsgSop();
+    },
+    videoIdChange() {
+      if (this.msgForm.videoId != null ) {
+        // 查找选中的课节对应的 label
+        const selectedVideo = this.videoList.find(course => parseInt(course.dictValue) === this.msgForm.videoId);
+
+        for (let i = 0; i < this.setting.length; i++) {
+          //响应式直接给链接的描述上值
+          if (selectedVideo && this.setting[i].contentType == 3  && this.msgForm.videoId != null) {
+            this.$set(this.setting[i], 'linkDescribe', selectedVideo.dictLabel);
+          }
+        }
+      }
+    },
+  }
+};
+</script>
+<style scoped>
+.custom-input /deep/ .el-input__inner {
+  height: 20px;
+  padding: 0 4px;
+  text-align:center;
+  display: block;
+}
+.custom-input /deep/ .el-input__icon {
+  line-height: 20px;
+}
+.el-button--text{
+  cursor: pointer;
+}
+</style>

+ 1 - 1
src/views/qw/sop/sop.vue

@@ -653,7 +653,7 @@
       </div>
     </el-dialog>
     <!--  执行详情  -->
-    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="70%" style="font-weight: bolder">
+    <el-drawer :title="sopLogsDialog.title" :visible.sync="sopLogsDialog.open" size="85%" style="font-weight: bolder">
       <sopLogsDetails ref="sopLogsDetails" :rowDetailFrom="sopLogsDialog.sopLogsForm"></sopLogsDetails>
     </el-drawer>
 

+ 0 - 322
src/views/qw/sopLogs/sopLogsListOld.vue

@@ -1,322 +0,0 @@
-<template>
-  <div class="app-container">
-    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="120px">
-      <el-form-item label="企微员工昵称" prop="qwUserName">
-        <el-input
-          v-model="queryParams.qwUserName"
-          placeholder="请输入企微员工昵称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="客户昵称" prop="externalUserName">
-        <el-input
-          v-model="queryParams.externalUserName"
-          placeholder="请输入企微客户昵称"
-          clearable
-          size="small"
-          @keyup.enter.native="handleQuery"
-        />
-      </el-form-item>
-      <el-form-item label="发送(成员)状态" prop="sendStatus">
-        <el-select v-model="queryParams.sendStatus" placeholder="请选择发送(成员)状态" clearable size="small">
-          <el-option
-            v-for="dict in sysQwSopLogsStatus"
-            :key="dict.dictValue"
-            :label="dict.dictLabel"
-            :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-<!--      <el-form-item label="预计发送时间" prop="sendTime">-->
-<!--        <el-date-picker clearable size="small"-->
-<!--          v-model="queryParams.sendTime"-->
-<!--          type="date"-->
-<!--          value-format="yyyy-MM-dd"-->
-<!--          placeholder="选择预计发送时间">-->
-<!--        </el-date-picker>-->
-<!--      </el-form-item>-->
-      <el-form-item label="接收(客户)状态" prop="receivingStatus">
-        <el-select v-model="queryParams.receivingStatus" placeholder="请选择接收(客户)状态" clearable size="small">
-          <el-option
-            v-for="dict in groupMsgSendStatusOptions"
-            :key="dict.dictValue"
-            :label="dict.dictLabel"
-            :value="dict.dictValue"
-          />
-        </el-select>
-      </el-form-item>
-
-      <el-form-item>
-        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
-        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
-      </el-form-item>
-    </el-form>
-
-    <el-row :gutter="10" class="mb8">
-      <el-col :span="1.5">
-        <el-button
-          type="warning"
-          plain
-          icon="el-icon-download"
-          size="mini"
-          :loading="exportLoading"
-          @click="handleExport"
-          v-hasPermi="['qw:qwSopLogs:export']"
-        >导出</el-button>
-      </el-col>
-      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
-    </el-row>
-
-    <el-table v-loading="loading" :data="qwSopLogsList">
-      <el-table-column label="发送类型" align="center" prop="sendType" width="80">
-        <template slot-scope="scope">
-          <dict-tag :options="sysQwSopType" :value="scope.row.sendType"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="企微成员昵称" align="center" prop="qwUserName" />
-      <el-table-column label="系统后台昵称" align="center" prop="userName" />
-      <el-table-column label="客户昵称" align="center" prop="externalUserName" />
-      <el-table-column label="发送的消息" align="center" prop="contentJson" >
-        <template slot-scope="scope">
-          <el-button type="text" @click="showContentDialog(scope.row.contentJson)">
-            查看详情
-          </el-button>
-        </template>
-      </el-table-column>
-      <el-table-column label="成员状态" align="center" prop="sendStatus" >
-        <template slot-scope="scope">
-          <dict-tag :options="sysQwSopLogsStatus" :value="scope.row.sendStatus"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="预计发送时间" align="center" prop="sendTime" width="180"/>
-      <el-table-column label="客户" align="center" prop="receivingStatus" >
-        <template slot-scope="scope">
-          <dict-tag :options="groupMsgSendStatusOptions" :value="scope.row.receivingStatus"/>
-        </template>
-      </el-table-column>
-      <el-table-column label="消息ID" align="center" prop="msgId" />
-    </el-table>
-
-    <el-dialog :visible.sync="contentDialog.isDialogVisible" title="消息详情" width="30%" append-to-body>
-      <el-card v-for="(item, index) in contentDialog.selectedContentJson" :key="index" class="box-card" style="margin-top: 2%">
-        <div slot="header" class="clearfix">
-          <span>类型:</span>
-          <span v-if="item.type===1">文本</span>
-          <span v-if="item.type===2">图片</span>
-        </div>
-
-        <div v-if="item.type===1" v-html="item.value"></div>
-        <div v-if="item.type===2">
-          <el-image style="width: 100px; height: 100px"  :src="item.value"
-                    fit="contain" @click="openImageViewer(item.value)" />
-        </div>
-
-      </el-card>
-      <span slot="footer" class="dialog-footer">
-        <el-button @click="contentDialog.isDialogVisible = false">关闭</el-button>
-      </span>
-    </el-dialog>
-
-    <!-- 大图预览对话框 -->
-    <el-dialog
-      :visible.sync="dialogVisible"
-      :modal="false"
-      width="500"
-      append-to-body>
-      <img
-        :src="this.dialogImageUrl"
-        style="display: block; max-width: 100%; margin: 0 auto"
-      />
-    </el-dialog>
-
-    <pagination
-      v-show="total>0"
-      :total="total"
-      :page.sync="queryParams.pageNum"
-      :limit.sync="queryParams.pageSize"
-      @pagination="getList"
-    />
-  </div>
-</template>
-
-<script>
-import { listQwSopLogs, exportQwSopLogs, listQwSopLogsList } from '@/api/qw/sopLogs'
-
-export default {
-  name: "sopLogsList",
-  props:{
-    rowDetailFrom:{},
-  },
-
-  watch:{
-    rowDetailFrom:{
-      handler(newVal){
-        // 当formData变化时重新查询
-        this.getList(newVal);
-      },
-      deep: true
-    }
-  },
-  data() {
-    return {
-
-      //图片放大
-      dialogVisible: false,
-      dialogImageUrl:null,
-
-      // 遮罩层
-      loading: true,
-      // 导出遮罩层
-      exportLoading: false,
-      // 选中数组
-      ids: [],
-      // 非单个禁用
-      single: true,
-      // 非多个禁用
-      multiple: true,
-      // 显示搜索条件
-      showSearch: true,
-      // 总条数
-      total: 0,
-      // 企业微信SOP  定时任务表格数据
-      qwSopLogsList: [],
-
-      //成员状态
-      sysQwSopLogsStatus:[],
-
-      //客户接收状态
-      groupMsgSendStatusOptions:[],
-
-      //企微SOP发送类型
-      sysQwSopType: [],
-
-      //发送的消息
-      contentDialog:{
-        isDialogVisible:false,
-        selectedContentJson: [],
-      },
-      // 弹出层标题
-      title: "",
-      // 是否显示弹出层
-      open: false,
-      // 查询参数
-      queryParams: {
-        pageNum: 1,
-        pageSize: 10,
-        qwUserName: null,
-        externalUserName: null,
-        sendStatus: null,
-        sendTime: null,
-        corpId:null,
-        receivingStatus: null,
-        sendType: null,
-      },
-      // 表单参数
-      form: {},
-      // 表单校验
-      rules: {
-      }
-    };
-  },
-  created() {
-    this.getList(this.rowDetailFrom);
-
-    //成员状态
-    this.getDicts("sys_qw_sopLogs_status").then(response => {
-      this.sysQwSopLogsStatus = response.data;
-    });
-
-    //客户接收状态
-    this.getDicts("sys_qw_groupMsg_SendStatus").then(response => {
-      this.groupMsgSendStatusOptions = response.data;
-    });
-
-    //发送消息类型
-    this.getDicts("sys_qw_sop_type").then(response => {
-      this.sysQwSopType = response.data;
-    });
-
-
-  },
-  methods: {
-    /** 查询企业微信SOP  定时任务列表 */
-    getList(val) {
-
-
-
-      this.queryParams.sopId = val.id || this.rowDetailFrom.id;
-      this.queryParams.corpId= val.corpId || this.rowDetailFrom.corpId;
-      this.loading = true;
-
-      listQwSopLogsList(this.queryParams).then(response => {
-
-        this.qwSopLogsList = response.rows;
-        this.total = response.total;
-        this.loading = false;
-      });
-    },
-    // 取消按钮
-    cancel() {
-      this.open = false;
-      this.reset();
-    },
-    // 表单重置
-    reset() {
-      this.form = {
-        id: null,
-        qwUserName: null,
-        externalUserName: null,
-        logType: null,
-        contentJson: null,
-        sendStatus: null,
-        sendTime: null,
-        companyId: null,
-        receivingStatus: null,
-        msgId: null,
-        sendType: null,
-        sopId: null
-      };
-      this.resetForm("form");
-    },
-
-    openImageViewer(url) {
-      // 打开大图预览对话框
-      this.dialogImageUrl=url
-      this.dialogVisible = true;
-    },
-    //查看发送的消息体
-    showContentDialog(contentJson) {
-
-      this.contentDialog.selectedContentJson = JSON.parse(contentJson);
-      this.contentDialog.isDialogVisible = true;
-    },
-    /** 搜索按钮操作 */
-    handleQuery() {
-      this.queryParams.pageNum = 1;
-      this.getList(this.rowDetailFrom);
-    },
-    /** 重置按钮操作 */
-    resetQuery() {
-      this.resetForm("queryForm");
-      this.handleQuery();
-    },
-    /** 导出按钮操作 */
-    handleExport() {
-      const queryParams = this.queryParams;
-      this.$confirm('是否确认导出所有企业微信SOP  定时任务数据项?', "警告", {
-          confirmButtonText: "确定",
-          cancelButtonText: "取消",
-          type: "warning"
-        }).then(() => {
-          this.exportLoading = true;
-          return exportQwSopLogs(queryParams);
-        }).then(response => {
-          this.download(response.msg);
-          this.exportLoading = false;
-        }).catch(() => {});
-    }
-  }
-};
-</script>

+ 1129 - 0
src/views/qw/user/cuDeptIdIndex.vue

@@ -0,0 +1,1129 @@
+<template>
+
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="100px">
+      <el-form-item label="企微主体" prop="corpId">
+        <el-select v-model="queryParams.corpId" placeholder="企微主体" size="small" @change="updateCorpId()">
+          <el-option
+            v-for="dict in myQwCompanyList"
+            :key="dict.dictValue"
+            :label="dict.dictLabel"
+            :value="dict.dictValue"
+          />
+        </el-select>
+      </el-form-item>
+      <el-form-item label="企微账号" prop="qwUserId">
+        <el-input
+          v-model="queryParams.qwUserId"
+          placeholder="请输入企微账号"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="企微昵称" prop="qwUserName">
+        <el-input
+          v-model="queryParams.qwUserName"
+          placeholder="请输入企微昵称"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+
+      <el-form-item label="授权码" prop="appKey">
+        <el-input
+          v-model="queryParams.appKey"
+          placeholder="请输入授权码"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item label="员工状态" prop="isDel">
+        <el-select v-model="queryParams.isDel" placeholder="请选择员工状态" clearable>
+          <el-option
+            v-for="item in optionsStatus"
+            :key="item.value"
+            :label="item.label"
+            :value="item.value">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          :loading="exportLoading"
+          @click="handleExport"
+          v-hasPermi="['qw:user:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table border v-loading="loading" :data="userList" @selection-change="handleSelectionChange">
+      <el-table-column label="企微成员ID" align="center" prop="id" />
+      <el-table-column label="企微账号" align="center" prop="qwUserId" />
+      <el-table-column label="企微昵称" align="center" prop="qwUserName" />
+      <el-table-column label="员工称呼" align="center" prop="welcomeText" />
+      <el-table-column label="所属部门" align="center" prop="isDel">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.isDel == 0" type="success">正常</el-tag>
+          <el-tag v-else type="error">离职</el-tag>
+        </template>
+      </el-table-column>
+      <el-table-column label="联系我二维码" align="center" prop="contactWay" >
+        <template slot-scope="scope">
+          <el-image
+            v-if="scope.row.contactWay!=null"
+            style="width: 100px; height: 100px"
+            :src="scope.row.contactWay"
+            fit="contain"
+            @click="openImageViewer(scope.row.contactWay)"/>
+        </template>
+      </el-table-column>
+      <el-table-column label="绑定的AI客服" align="center" prop="fastGptRoleName" />
+      <el-table-column label="授权码" align="center" prop="appKey" />
+     <el-table-column label="ai状态" align="center" prop="loginStatus">
+        <template slot-scope="scope">
+          <el-tag v-if="scope.row.ipadStatus == 1" type="success">在线</el-tag>
+          <el-tag v-else type="danger">离线</el-tag>
+        </template>
+      </el-table-column>
+<!--      <el-table-column label="插件状态" align="center" prop="toolStatus">-->
+<!--        <template slot-scope="scope">-->
+<!--          <el-tag v-if="scope.row.toolStatus == 1" type="success">在线</el-tag>-->
+<!--          <el-tag v-else type="danger">离线</el-tag>-->
+<!--        </template>-->
+<!--      </el-table-column>-->
+<!--      <el-table-column label="插件版本" align="center" prop="version"/>-->
+      <el-table-column label="服务器地址" align="center" prop="loginCodeUrl">
+        <template slot-scope="scope">
+          <el-tooltip class="item" effect="dark" :content="scope.row.loginCodeUrl" placement="top">
+            <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+              <span>{{ scope.row.loginCodeUrl }}</span>
+            </div>
+          </el-tooltip>
+        </template>
+      </el-table-column>
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="120px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-user-solid"
+            plain
+            @click="handleAppellation(scope.row)"
+          >
+            修改员工称呼
+          </el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-user-solid"
+            plain
+            @click="handleAutoRemark(scope.row)"
+          >
+            完课备注修改
+          </el-button>
+
+          <el-button
+            v-if="scope.row.serverStatus==1&&scope.row.ipadStatus!=1"
+            size="mini"
+            type="text"
+            icon="el-icon-sunny"
+            plain
+            @click="handleLoginQwCode(scope.row)"
+            v-hasPermi="['qw:user:login']"
+          >
+            登录企微
+          </el-button>
+         <el-button
+			v-if="scope.row.serverStatus==1&&scope.row.ipadStatus==1"
+            size="mini"
+            type="text"
+            icon="el-icon-moon"
+            plain
+            @click="handleLoginOutQwStatus(scope.row)"
+            v-hasPermi="['qw:user:login']"
+          >
+            退出企微
+          </el-button>
+		<el-button
+			v-if="scope.row.ipadStatus==1"
+		   size="mini"
+		   type="text"
+		   icon="el-icon-moon"
+		   plain
+		   @click="handleTwoCode(scope.row)"
+		   v-hasPermi="['qw:user:login']">
+		   二次验证
+		 </el-button>
+		 <el-button
+		 	v-if="scope.row.serverStatus!=1"
+		    size="mini"
+		    type="text"
+		    icon="el-icon-moon"
+		    plain
+		    @click="handleGetQwIpad(scope.row)"
+		    v-hasPermi="['qw:user:login']"
+		  >
+		    获取Ai主机
+		  </el-button>
+		  <el-button
+		  	v-if="scope.row.serverStatus==1 && scope.row.ipadStatus!=1"
+		     size="mini"
+		     type="text"
+		     icon="el-icon-moon"
+		     plain
+		     @click="handleDelQwIpad(scope.row)"
+		     v-hasPermi="['qw:user:login']"
+		   >
+		     解绑Ai主机
+		   </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="主机" align="center" class-name="small-padding fixed-width" width="110px" fixed="right">
+        <template slot-scope="scope">
+
+          <el-button
+            v-if="scope.row.appKey==null"
+            size="mini"
+            type="text"
+            icon="el-icon-s-check"
+            plain
+            v-hasPermi="['qw:user:authAppKey']"
+            @click="uploadAuthorizeKey2(scope.row)"
+          >授权key
+          </el-button>
+          <el-button
+            v-if="scope.row.loginCodeUrl==null && scope.row.appKey !=null"
+            size="mini"
+            type="text"
+            icon="el-icon-sunny"
+            plain
+            @click="handleBindCloudHost(scope.row)"
+            v-hasPermi="['qw:user:loginIp']"
+          >
+            绑定主机
+          </el-button>
+<!--          <el-button-->
+<!--            v-if="scope.row.loginCodeUrl!=null"-->
+<!--            size="mini"-->
+<!--            type="text"-->
+<!--            icon="el-icon-video-camera-solid"-->
+<!--            plain-->
+<!--            @click="handleCloudAP(scope.row.loginCodeUrl)"-->
+<!--            v-hasPermi="['qw:user:cloudAP']"-->
+<!--          >-->
+<!--            获取主机帐密-->
+<!--          </el-button>-->
+          <el-button
+            v-if="scope.row.loginCodeUrl!=null"
+            size="mini"
+            type="text"
+            icon="el-icon-moon"
+            plain
+            @click="handleUnbindCloudHost(scope.row)"
+            v-hasPermi="['qw:user:loginIpOut']"
+          >
+            解除主机
+          </el-button>
+        </template>
+      </el-table-column>
+      <el-table-column label="AI客服" align="center" class-name="small-padding fixed-width" width="100px" fixed="right">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-connection"
+            plain
+            v-if="scope.row.fastGptRoleName!=null"
+            @click="bindFastGptRole(scope.row)"
+          >换绑AI客服</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            plain
+            icon="el-icon-link"
+            v-else
+            @click="bindFastGptRole(scope.row)"
+          >绑定AI客服</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-unlock"
+            plain
+            v-if="scope.row.fastGptRoleName!=null"
+            @click="relieveFastGptRole(scope.row)"
+          >解绑AI客服</el-button>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 绑定AI客服-->
+    <el-dialog :title="bindAiTitle" :visible.sync="bindAiOpen" width="1200px" append-to-body>
+      <fast-gpt-role ref="fastGptRole" @refreshFastGptList="refreshFastGptList" ></fast-gpt-role>
+    </el-dialog>
+
+<!--    <el-dialog :visible.sync="updateIp.open" width="600px" append-to-body>-->
+<!--      <el-form ref="updateIpForm" :model="updateIpForm" :rules="updateIpRule" label-width="100px">-->
+<!--        <el-form-item label="新云主机IP" prop="Ip">-->
+<!--          <el-input v-model="updateIpForm.newIp" placeholder="请输入新IP" />-->
+<!--        </el-form-item>-->
+<!--      </el-form>-->
+<!--      <div slot="footer" class="dialog-footer" >-->
+<!--        <el-button type="primary" @click="submitUpdateIpForm">确 定</el-button>-->
+<!--      </div>-->
+<!--    </el-dialog>-->
+
+    <el-dialog title="云主机信息" :visible.sync="cloudAPOpen.open" append-to-body>
+      <el-card class="box-card">
+        <div slot="header" class="clearfix">
+          <span>账号:{{cloudAPOpen.admin}}</span>
+        </div>
+        <div slot="header" class="clearfix">
+          <span>密码:{{cloudAPOpen.passWord}}</span>
+        </div>
+      </el-card>
+    </el-dialog>
+
+    <el-dialog :title="callOpen.title" :visible.sync="callOpen.open" width="500px" append-to-body>
+      <el-form ref="callOpenFrom" :model="callOpenFrom" :rules="callOpenRule" label-width="110px">
+        <el-form-item label="员工称呼" prop="welcomeText">
+          <el-input v-model="callOpenFrom.welcomeText" placeholder="请输入员工称呼" />
+        </el-form-item>
+      </el-form>
+      <div slot="footer" class="dialog-footer" >
+        <el-button type="primary" @click="submitCallOpenFrom">确 定</el-button>
+      </div>
+    </el-dialog>
+
+
+    <el-dialog :title="editRemarkOpen.title" :visible.sync="editRemarkOpen.open" width="500px" append-to-body>
+      <el-form ref="callOpenFrom" :model="editRemarkOpen" label-width="110px">
+        <el-radio-group v-model="editRemarkOpen.isSendMsg">
+          <el-card>
+            <el-row :gutter="20">
+              <el-col>
+                <el-radio
+                  :label="1"
+                  style="font-size: 16px; margin: 10px 0;"
+                >添加【完课备注】在最【旧备注-(前面)】</el-radio>
+              </el-col>
+              <el-col>
+                <el-radio
+                  :label="2"
+                  style="font-size: 16px; margin: 10px 0;"
+                >添加【完课备注】在最【旧备注-(后面)】</el-radio>
+              </el-col>
+              <el-col>
+                <el-radio
+                  :label="3"
+                  style="font-size: 16px; margin: 10px 0;"
+                >使用简洁版备注【*日期完】,在【旧备注-前面】</el-radio>
+              </el-col>
+              <el-col>
+                <el-radio
+                  :label="4"
+                  style="font-size: 16px; margin: 10px 0;"
+                >使用简洁版备注【*日期完】,在【旧备注-后面】</el-radio>
+              </el-col>
+              <el-col>
+                <el-radio
+                  :label="5"
+                  style="font-size: 16px; margin: 10px 0;"
+                >不用完课备注</el-radio>
+              </el-col>
+            </el-row>
+          </el-card>
+        </el-radio-group>
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitEditRemarkOpenFrom">确 定</el-button>
+      </div>
+    </el-dialog>
+
+    <el-dialog title="授权key" :visible.sync="authorizeKeyOpen" width="500px" append-to-body>
+        <el-form ref="authorizeKeyFrom" :model="authorizeKeyFrom" :rules="authorizeKeyRule" label-width="110px">
+            <el-form-item label="授权的key值" prop="appKey">
+              <el-input v-model="authorizeKeyFrom.appKey" placeholder="请输入授权key" type="Number"/>
+            </el-form-item>
+        </el-form>
+            <div slot="footer" class="dialog-footer" >
+              <el-button type="primary" @click="submitAuthorizeKeyForm">确 定</el-button>
+            </div>
+    </el-dialog>
+    <!--二维码   -->
+    <el-dialog
+      title="企微二次认证"
+      :visible.sync="qwLoginTwo.open"
+      width="600px"
+      append-to-body
+      custom-class="qr-login-dialog"
+    >
+      <div class="qr-login-container">
+        <div class="image-wrapper" v-loading="imageLoading" >
+          <el-image
+            :src="'data:image/png;base64,' +qwLoginTwo.codeUrl"
+            style="display: block; margin: 0 auto; width: 300px; height: 300px;"
+          />
+        </div>
+        <p class="qr-login-instructions">二次验证二维码</p>
+		</div>
+		<div slot="footer" class="dialog-footer" >
+		  <el-button type="primary" @click="qwLoginTwo.open=false">确 定</el-button>
+		</div>
+    </el-dialog>
+
+	<el-dialog
+	  :title="qwLogin.title"
+	  :visible.sync="qwLogin.open"
+	  width="600px"
+	  append-to-body
+	  custom-class="qr-login-dialog"
+	>
+	  <div class="qr-login-container">
+	    <div class="image-wrapper" v-loading="imageLoading" >
+	      <el-image
+	        :src="'data:image/png;base64,' +qwLogin.codeUrl"
+	        style="display: block; margin: 0 auto; width: 300px; height: 300px;"
+	      />
+	    </div>
+	    <p class="qr-login-instructions">使用企业微信扫码授权登录</p>
+	  </div>
+	</el-dialog>
+
+	<el-dialog
+	  title="输入企微验证码"
+	  :visible.sync="qwCode.open"
+	  width="600px"
+	  append-to-body>
+	 <el-form  :model="qwCode"  label-width="80px" @submit.native.prevent="handleSubmit">
+	     <el-form-item label="验证码" prop="companyName">
+	       <el-input v-model="qwCode.code" placeholder="输入企微6位验证码" />
+	     </el-form-item>
+	</el-form>
+
+	<div slot="footer" class="dialog-footer">
+	  <el-button type="primary" @click="submitCodeForm">确 定</el-button>
+
+	</div>
+	</el-dialog>
+
+
+
+    <!-- 大图预览对话框 -->
+    <el-dialog
+      :visible.sync="dialogVisible"
+      :modal="false"
+      width="1200"
+      append-to-body>
+      <img
+        :src="this.dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+
+  </div>
+</template>
+
+<script>
+import {
+  updateUser,
+  exportUser,
+  getMyQwCompanyList,
+  relieveFastGptRoleById,
+  loginQwIpad,
+  loginQwCodeMsg,
+  twoCode,
+  twoCodeStatus,
+  qrCodeStatus,
+  getQwIpad,
+  delQwIpad,
+  qrCodeVerify,
+  outLoginQwIpad,
+  handleAllocateRemoteHost,
+  qwBindCloudHost, qwUnbindCloudHost, handleAuthAppKey, handleInputAuthAppKey, selectCloudAP, myDepartListUser
+} from '../../../api/qw/user'
+import fastGptRole from "@/views/fastGpt/fastGptRole/fastGptRole";
+
+export default {
+  name: "cuDeptIdIndex",
+  components: { fastGptRole},
+  data() {
+    return {
+      updateIp:{
+        open:false,
+        title: "修改云主机IP"
+      },
+      updateIpForm:{
+        id:null,
+        newIp:null,
+      },
+      authorizeKeyOpen:false,
+      authorizeKeyFrom:{
+        id:null,
+        appKey:null,
+        qwUserId:null,
+        qwUserName:null
+      },
+      updateIpRule:{},
+      newIp:null,
+      //放大图片
+      dialogImageUrl:null,
+      dialogVisible:false,
+      optionsStatus: [{
+        value: 0,
+        label: '正常'
+      }, {
+        value: 2,
+        label: '离职'
+      }],
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      //公司列表
+      myQwCompanyList:[],
+      // 企微用户表格数据
+      userList: [],
+      allowSelectOptions:[],
+      // 弹出层标题
+      bindAiTitle: "",
+      bindAiOpen: false,
+      qwLogin:{
+        title:"",
+        open:false,
+        codeUrl:null,
+        code:null,
+        appKey:null,
+      },
+	 qwLoginTwo:{
+        title:"",
+        open:false,
+        codeUrl:null,
+        code:null,
+        appKey:null,
+     },
+	qwCode:{
+        title:"",
+        open:false,
+        code:null,
+      },
+      cloudAPOpen:{
+        open:false,
+        admin:null,
+        passWord:null,
+      },
+      callOpen:{
+        open:false,
+        title: '修改员工称呼',
+
+      },
+      callOpenFrom:{
+        id:null,
+        welcomeText:null,
+      },
+
+      editRemarkOpen: {
+        open: false,
+        title: '修改员工自动给完课客户打备注的规则',
+        id:null,
+        isSendMsg:null,
+      },
+
+      twoCodeInterval:null,
+      loginQwInterval:null,
+
+      imageLoading: true, // 控制加载状态
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        qwUserId: null,
+        corpId: null,
+        qwUserName: null,
+      },
+	  qwUserId:null,
+      companyUserList:[],
+      // 表单参数
+      form: {
+        isSendMsg: '2',
+      },
+      authorizeKeyRule:{
+        appKey:[{required:true,message:"授权码不能为空",trigger:"blur"}]
+      },
+      callOpenRule:{
+        welcomeText:[{required:true,message:"员工称呼不能为空",trigger:"blur"}]
+      },
+      // 表单校验
+      rules: {
+      },
+      //欢迎语表单校验
+      weclomeRules:{
+        welcomeText:[{required:true,message:"消息文本不能为空",trigger:"blur"}]
+      },
+    };
+  },
+  created() {
+
+    getMyQwCompanyList().then(response => {
+      this.myQwCompanyList = response.data;
+      if(this.myQwCompanyList!=null){
+        this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+        this.getList();
+      }
+    });
+
+
+  },
+  watch: {
+    // 监听弹窗的可见性变化
+    'qwLogin.open'(newVal) {
+      if (!newVal) {
+        // 如果弹窗关闭,清除定时器
+        clearInterval(this.loginQwInterval);
+
+      }
+    },
+  },
+  methods: {
+    getList() {
+      this.loading = true;
+      myDepartListUser(this.queryParams).then(response => {
+        this.userList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+
+    },
+    updateCorpId(){
+      this.reset();
+      this.getList();
+    },
+
+    //绑定AI客服
+    bindFastGptRole(row){
+      this.bindAiTitle="绑定AI客服";
+      this.bindAiOpen=true;
+      setTimeout(() => {
+        this.$refs.fastGptRole.handleBindAiData(row)
+      }, 200);
+
+    },
+
+    handleAppellation(val){
+      this.callOpen.open=true;
+      this.callOpenFrom.welcomeText=val.welcomeText;
+      this.callOpenFrom.id=val.id;
+    },
+
+    handleAutoRemark(val){
+      this.editRemarkOpen.open=true;
+      this.editRemarkOpen.id = val.id;
+      this.editRemarkOpen.isSendMsg = val.isSendMsg;
+    },
+    //登录
+    handleLoginQwCode(val){
+      if (val.appKey==null || val.appKey===''){
+        return this.$message.warning("没有授权码,无法登录企业微信,请授权");
+      }
+      loginQwIpad({qwUserId:val.id}).then(res => {
+		this.qwUserId=val.id;
+        this.qwLogin.code=null;
+        this.imageLoading=false;
+		console.log(res)
+		if(res.msg=="success"){
+			this.qwLogin.codeUrl=res.qrCode64
+			this.qwLogin.open=true;
+			this.loginQwPolling();
+		}else{
+			this.$message.success(res.msg);
+			this.getList()
+		}
+
+      })
+    },
+
+	handleTwoCode(val){
+
+		twoCode({ qwUserId: val.id }).then(res => {
+		console.log(res)
+		this.qwLoginTwo.open=true;
+		this.qwLoginTwo.codeUrl=res.qrCode
+
+        });
+
+
+	},
+	twoCodePolling() {
+	  this.twoCodeInterval = setInterval(() => {
+	    twoCodeStatus({ qwUserId: this.qwUserId }).then(res => {
+			console.log(res)
+
+	      if (res.msg==104001) {
+			this.$message.success('登录成功');
+			this.clearDl()
+	        clearInterval(this.loginQwInterval);
+	      }else if(res.msg==100004){
+			  this.clearDl()
+		  }
+	    });
+	  }, 3000);
+	},
+    loginQwPolling() {
+      this.loginQwInterval = setInterval(() => {
+        qrCodeStatus({ qwUserId: this.qwUserId }).then(res => {
+			console.log(res)
+			if (res.msg==22) {
+				this.$message.success('账号企业不一致请重新扫码登录');
+				this.clearDl();
+			}
+          if (res.msg==104001) {
+			this.$message.success('登录成功');
+			this.clearDl()
+
+          }else if(res.msg==100004){
+			  this.qwCode.open=true;
+			  clearInterval(this.loginQwInterval);
+		  }
+        });
+      }, 3000);
+    },
+	submitCodeForm(){
+
+		qrCodeVerify({ code: this.qwCode.code,qwUserId: this.qwUserId }).then(res => {
+			console.log(res)
+
+			this.$message.success('验证成功账号信息确认中。。。。');
+			this.qwCode.open=false;
+			 this.loginQwInterval = setTimeout(() => {
+			     qrCodeStatus({ qwUserId: this.qwUserId }).then(res => {
+			         console.log(res);
+			         if (res.msg == 23) {
+			             this.$message.error('账号不一致请重新扫码登录');
+			             this.clearDl();
+			         }
+					 if (res.msg == 22) {
+					     this.$message.error('账号企业不一致请重新扫码登录');
+					     this.clearDl();
+					 }
+			         if (res.msg == 104001) {
+			             this.$message.success('登录成功');
+			             this.clearDl();
+			         }
+			     });
+			 }, 4000);
+		});
+	},
+
+	clearDl(){
+		this.qwCode.open=false;
+		this.qwLogin.open=false;
+		clearInterval(this.loginQwInterval);
+		this.getList()
+	},
+
+
+
+    //退出
+    handleLoginOutQwStatus(val){
+      outLoginQwIpad({qwUserId: val.id}).then(res => {
+
+        this.$message.success("退出登录成功");
+		this.getList()
+      })
+
+    },
+
+
+	handleGetQwIpad(val){
+    getQwIpad({ qwUserId: val.id }).then(res => {
+      this.$message.success("获取主机成功");
+      this.getList();
+    }).catch(error => {
+      console.log(error);
+      if (error.code  === 501) {
+        this.$confirm(
+          '当前区域没有多余的名额,将为你分配异地名额,会导致企业微信需要扫脸重新登录,并且半个小时后需要进行验证',
+          '提示',
+          {
+            confirmButtonText: '确定',
+            cancelButtonText: '取消',
+            type: 'warning',
+            dangerouslyUseHTMLString: true
+          }
+        ).then(() => {
+          return handleAllocateRemoteHost({ qwUserId: val.id });
+        }).then(res => {
+          this.$message.success('异地主机分配成功');
+          this.getList();
+        }).catch(() => {
+          this.$message.info('已取消异地主机分配');
+        });
+      } else {
+        this.$message.error('获取主机失败');
+      }
+    });
+	},
+	handleDelQwIpad(val){
+	  delQwIpad({qwUserId: val.id}).then(res => {
+	    this.$message.success("解绑主机成功");
+		this.getList()
+	  })
+	},
+    //传验证码
+    handleLoginQwCodeMsg(){
+      loginQwCodeMsg({appKey: this.qwLogin.appKey,code:this.qwLogin.code}).then(res => {
+        this.qwLogin.open=false;
+        this.$message.success("登录成功");
+      })
+    },
+
+    validateCode() {
+      // 只允许输入数字并限制长度为6
+      this.qwLogin.code = this.qwLogin.code.replace(/\D/g, "").slice(0, 6);
+    },
+
+    handleCloudAP(urlAP){
+
+      selectCloudAP({ipAddress:urlAP}).then(res => {
+        this.cloudAPOpen.open=true
+        this.cloudAPOpen.admin=res.data.apAdmin;
+        this.cloudAPOpen.passWord=res.data.apPassword;
+      })
+    },
+
+    handleUnbindCloudHost(val){
+
+      const appKey=val.appKey;
+
+      this.$confirm(
+        '确定要给企微账号:<span style="color: green;">' +val.qwUserId + '' +
+        '</span><br>企微昵称:<span style="color: red;">【' + val.qwUserName + '】</span>' +
+        '</span><br><span style="color: orange;">解绑【Ps:解绑后此云主机可能会分配给他人】</span></span>',
+        "警告",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+          dangerouslyUseHTMLString: true // 允许使用 HTML 字符串
+        }
+      ).then(() => {
+        return  qwUnbindCloudHost(appKey);
+      }).then(response => {
+        this.$message.success('解绑成功');
+      }).finally(res=>{
+        this.getList();
+      })
+
+    },
+
+
+    handleAuthorizeKey(val){
+      this.authorizeKeyFrom.id=val.id;
+      this.authorizeKeyFrom.qwUserId=val.qwUserId;
+      this.authorizeKeyFrom.qwUserName=val.qwUserName;
+      this.authorizeKeyOpen=true;
+
+    },
+
+    submitAuthorizeKeyForm(){
+      this.$refs["authorizeKeyFrom"].validate(valid => {
+        if (valid) {
+          if (this.authorizeKeyFrom.id != null && this.authorizeKeyFrom.appKey != null) {
+            this.uploadAuthorizeKey();
+          }
+        }
+      });
+    },
+    submitCallOpenFrom(){
+
+      this.$refs["callOpenFrom"].validate(valid => {
+        if (valid) {
+
+          if (this.callOpenFrom.id != null && this.callOpenFrom.welcomeText != null) {
+            updateUser(this.callOpenFrom).then(res=>{
+              this.$message.success('修改成功');
+              this.callOpen.open=false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+
+    submitEditRemarkOpenFrom(){
+      if (this.editRemarkOpen.id != null && this.editRemarkOpen.isSendMsg != null) {
+        updateUser(this.editRemarkOpen).then(res => {
+          this.$message.success('修改成功');
+          this.editRemarkOpen.open = false;
+          this.getList();
+        });
+      }else {
+        this.$message.error("请选择条件")
+      }
+    },
+
+
+    uploadAuthorizeKey(){
+      this.$confirm(
+        '确定要给企微账号:<span style="color: green;">' + this.authorizeKeyFrom.qwUserId + '' +
+        '</span><br>企微昵称:<span style="color: red;">【' + this.authorizeKeyFrom.qwUserName + '】</span>' +
+        '</span><br>授权key:<span style="color: #04adf6;">【' + this.authorizeKeyFrom.appKey + '】</span>?',
+        "警告",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+          dangerouslyUseHTMLString: true // 允许使用 HTML 字符串
+        }
+      ).then(() => {
+        this.authorizeKeyOpen=false;
+        return handleInputAuthAppKey(this.authorizeKeyFrom);
+      }).then(response => {
+        this.msgSuccess("授权key完成");
+      }).finally(res=>{
+        this.resetAuthorizeKeyFrom();
+        this.getList();
+      })
+
+    },
+
+    uploadAuthorizeKey2(val){
+      const id=val.id;
+      this.$confirm(
+        '确定要给企微账号:<span style="color: green;">' +val.qwUserId + '' +
+        '</span><br>企微昵称:<span style="color: red;">【' + val.qwUserName + '】</span>' +
+        '</span><br>授权key</span>?',
+        "警告",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+          dangerouslyUseHTMLString: true // 允许使用 HTML 字符串
+        }
+      ).then(() => {
+        return handleAuthAppKey({id:id});
+      }).then(response => {
+        this.msgSuccess("授权key完成");
+      }).finally(res=>{
+        this.resetAuthorizeKeyFrom();
+        this.getList();
+      })
+    },
+
+    handleBindCloudHost(val){
+
+      if (val.appKey == null || val.appKey == '') {
+        return this.$message.warning('没有授权码,无法绑定主机,请联系管理员');
+      }
+
+      const appKey=val.appKey;
+
+      this.$confirm(
+        '确定要给企微账号:<span style="color: green;">' +val.qwUserId + '' +
+        '</span><br>企微昵称:<span style="color: red;">【' + val.qwUserName + '】</span>' +
+        '</span><br><span style="color: dodgerblue;">绑定云主机?</span></span>',
+        "警告",
+        {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning",
+          dangerouslyUseHTMLString: true // 允许使用 HTML 字符串
+        }
+      ).then(() => {
+        return qwBindCloudHost(appKey);
+      }).then(response => {
+        this.$message.success('绑定成功,请登录云主机进行配置~~');
+      }).finally(res=>{
+        this.getList();
+      })
+
+
+    },
+
+    openImageViewer(url) {
+      // 打开大图预览对话框
+      this.dialogImageUrl=url
+      this.dialogVisible = true;
+    },
+    //刷新页面
+    refreshFastGptList(){
+      this.bindAiOpen=false;
+      this.getList();
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        id: null,
+        qwUserId: null,
+        corpId: null,
+        qwUserName: null,
+      };
+      this.resetForm("form");
+
+    },
+
+    //重置授权
+    resetAuthorizeKeyFrom(){
+      this.authorizeKeyFrom={
+          id:null,
+          appKey:null,
+          qwUserId:null,
+          qwUserName:null
+      };
+    },
+    //重置登录
+    resetQwLogin(){
+      this.qwLogin={
+        title:"",
+        open:false,
+        codeUrl:null,
+        code:null,
+        corpId:null,
+        qwUserId:null,
+      }
+      this.qwLogin.open=false;
+      this.loading=false;
+      this.getList();
+    },
+    //解绑AI客服
+    relieveFastGptRole(row){
+      this.$confirm('是否确认解绑AI客服?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return relieveFastGptRoleById(row.id);
+      }).then(() => {
+        this.getList();
+        this.msgSuccess("解绑成功");
+      })
+
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId=this.myQwCompanyList[0].dictValue;
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+
+
+    /** 提交按钮 */
+    submitForm() {
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          if (this.form.id != null) {
+
+            // updateUser(this.form).then(response => {
+            //   this.msgSuccess("绑定成功");
+            //   this.open = false;
+            //   this.getList();
+            //
+            // });
+          } else {
+            // addUser(this.form).then(response => {
+            //   this.msgSuccess("新增成功");
+            //   this.open = false;
+            //   this.getList();
+            // });
+          }
+        }
+      });
+    },
+
+    /** 导出按钮操作 */
+    handleExport() {
+      const queryParams = this.queryParams;
+      this.$confirm('是否确认导出所有企微用户数据项?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(() => {
+        this.exportLoading = true;
+        return exportUser(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+        this.exportLoading = false;
+      }).catch(() => {});
+    }
+  }
+};
+</script>
+<style>
+.text-container {
+  max-height: 7.5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.qr-login-dialog .el-dialog__body {
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  text-align: center;
+  padding: 20px;
+}
+
+.qr-login-container {
+  width: 100%;
+}
+
+.qr-login-instructions {
+  font-size: 14px;
+  color: #666;
+  margin: 10px 0;
+}
+
+.verification-code-input {
+  margin-top: 15px;
+  width: 80%;
+}
+
+</style>

Разница между файлами не показана из-за своего большого размера
+ 397 - 344
src/views/qw/user/index.vue


+ 701 - 0
src/views/qw/welcome/deptWelcomeIndex.vue

@@ -0,0 +1,701 @@
+<template>
+  <div>
+  <div class="app-container">
+    <el-alert
+      title="注意事项"
+      type="warning"
+      description="因企业微信接口限制,1、在【企业微信后台】群欢迎语素材库进行新增/编辑/删除操作,后台不会同步更新,建议在后台入群欢迎语处进行管理。
+       2、 注意:企业微信入群欢迎语素材库中,最多容纳100个素材,包括后台管理的以及企业微信后台管理的。
+       3、除图片外,其他附件素材均只有三天时效,若要续用,请点击修改-》直接点击-确认后,刷新时效"
+      :closable="false"
+      center
+      show-icon>
+    </el-alert>
+  </div>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px" >
+      <el-form-item label="企微公司" prop="corpId">
+            <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+              <el-option
+                v-for="dict in myQwCompanyList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="dict.dictValue"
+              />
+            </el-select>
+       </el-form-item>
+      <el-form-item label="消息内容" prop="createName">
+        <el-input
+          v-model="queryParams.textContent"
+          placeholder="请输入要搜索的消息文本内容"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+      <el-form-item>
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            size="mini"
+            icon="el-icon-plus"
+            @click="handleAdd"
+            v-hasPermi="['qw:welcome:add']"
+          >新增</el-button>
+        </el-col>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="welcomeList" border>
+      <el-table-column label="欢迎语类型" align="center" prop="welcomeType" >
+        <template slot-scope="scope">
+          <span   v-for="(item, index) in welcomeTypeOptions"    v-if="scope.row.welcomeType==item.dictValue">{{item.dictLabel}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="欢迎消息文本" align="left" width="600px" >
+        <template slot-scope="scope">
+          <div class="flex-container">
+            <el-tooltip class="item" effect="dark" :content="scope.row.textContent" placement="top">
+              <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+                <span>文本消息:</span>{{ scope.row.textContent }}
+              </div>
+            </el-tooltip>
+            <div v-if="scope.row.welcomeType == '1'">
+              <div style="float: left">
+              <span>图片:</span>
+              <el-image v-if="scope.row.imagePicUrl"
+                style="width: 50px; height: 50px"
+                :src="scope.row.imagePicUrl"
+                fit="contain" />
+                <span v-if="!scope.row.imagePicUrl">无图片</span>
+              </div>
+            </div>
+            <div v-if="scope.row.welcomeType=='2'">
+              <div style="float: left;margin-top: 10px">
+                <el-tooltip class="item" effect="dark" :content="scope.row.linkTitle" placement="top">
+                  <div style="float: left; overflow-y: hidden; height: 50px">
+                    <span>图文标题:</span>{{ scope.row.linkTitle }}
+                  </div>
+                </el-tooltip>
+                <div>
+                  <el-image v-if="scope.row.linkPicurl"
+                            style="width: 50px; height: 50px"
+                            :src="scope.row.linkPicurl"
+                            fit="contain" />
+                </div>
+
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='3'">
+              <div style="float: left">
+                <span>小程序标题:</span>{{scope.row.miniprogramTitle}}
+                <div>
+                  <el-image v-if="scope.row.miniprogramPicUrl"
+                            style="width: 50px; height: 50px"
+                            :src="scope.row.miniprogramPicUrl"
+                            fit="contain" />
+                </div>
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='4'">
+              <div style="float: left">
+                <span>文件:</span>
+                <span>请查看详情</span>
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='5'">
+              <div style="float: left">
+                <span>视频:</span>
+                <span>请查看详情</span>
+              </div>
+            </div>
+          </div>
+        </template>
+
+      </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createName" />
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-notebook-2"
+            @click="hangdleDitels(scope.row)"
+            v-hasPermi="['qw:welcome:query']"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:welcome:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:welcome:remove']"
+          >删除</el-button>
+
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改入群欢迎语管理对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <div v-if="!form.welcomeId">
+          <el-form-item label="欢迎语类型" prop="welcomeType">
+              <el-radio-group v-model="form.welcomeType">
+                <el-radio :label="item.dictValue" v-for="item in welcomeTypeOptions">{{item.dictLabel}}</el-radio>
+              </el-radio-group>
+          </el-form-item>
+        </div>
+        <el-form-item label="消息文本内容" prop="textContent">
+          <el-input v-model="form.textContent" type="textarea" :rows="6" maxlength="1300" show-word-limit placeholder="请输入消息文本内容(图片,视频等附件不上传,则默认为纯文本欢迎语)" />
+        </el-form-item>
+        <div v-if="form.welcomeType=='1'">
+          <el-form-item label="图片" prop="imagePicUrl">
+            <ImageUpload v-model="form.imagePicUrl"  type="image" :num="10" :width="150" :height="150"  disabled/>
+          </el-form-item>
+        </div>
+        <div v-if="form.welcomeType=='2'">
+          <el-form-item label="图文标题" prop="linkTitle" required>
+            <el-input v-model="form.linkTitle" type="textarea" :rows="2" maxlength="42" show-word-limit placeholder="请输入图文消息标题,最长为42字节" />
+          </el-form-item>
+          <el-form-item label="上传图文封面" prop="linkPicurl">
+            <ImageUpload v-model="form.linkPicurl"  type="image" :num="10" :width="150" :height="150" />
+          </el-form-item>
+          <el-form-item label="图文的描述" prop="linkDesc">
+            <el-input v-model="form.linkDesc" type="textarea" :rows="3" maxlength="170" show-word-limit placeholder="请输入内容,,最长为170字节" />
+          </el-form-item>
+          <el-form-item label="图文的链接" prop="linkUrl" required>
+<!--            <ImageUpload v-model="form.linkUrl"   type="image" :num="10" :width="150" :height="150" />-->
+            <el-input v-model="form.linkUrl" placeholder="请输入图文的URL链接" />
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='3'">
+          <el-form-item label="小程序标题" prop="miniprogramTitle">
+            <el-input v-model="form.miniprogramTitle" type="textarea" maxlength="21" show-word-limit placeholder="请输入小程序消息标题,最长为21字节"  />
+          </el-form-item>
+          <el-form-item label="上传小程序封面" prop="miniprogramPicUrl">
+            <ImageUpload v-model="form.miniprogramPicUrl"  type="image" :num="10" :width="150" :height="150" />
+          </el-form-item>
+          <el-form-item label="小程序appid" prop="miniprogramAppid">
+            <el-input v-model="form.miniprogramAppid" placeholder="请输入小程序appid,必须是关联到企业的小程序应用" />
+          </el-form-item>
+          <el-form-item label="小程序page路径" prop="miniprogramPage">
+            <el-input v-model="form.miniprogramPage" placeholder="请输入小程序page路径" />
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='4'">
+          <el-form-item label="上传文件" prop="fileUrl">
+            <el-upload v-if="form.fileUrl==null || form.welcomeId"
+                       v-model="form.fileUrl"
+                       class="avatar-uploader"
+                       :action="uploadUrl"
+                       :show-file-list="false"
+                       :on-success="handleAvatarSuccessFile"
+                       :before-upload="beforeAvatarUploadFile">
+              <i class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <el-link v-if="form.fileUrl" type="primary" :href="downloadUrl(form.fileUrl)" download>
+              {{form.fileUrl}}
+            </el-link>
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='5'">
+          <el-form-item label="上传视频" prop="videoUrl">
+            <el-upload v-if="form.videoUrl==null || form.welcomeId"
+                       v-model="form.videoUrl"
+                       class="avatar-uploader"
+                       :action="uploadUrl"
+                       :show-file-list="false"
+                       :on-success="handleAvatarSuccessVideo"
+                       :before-upload="beforeAvatarUploadVideo">
+              <i class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <video v-if="form.videoUrl"
+                   :src="form.videoUrl"
+                   controls style="width: 200px;height: 100px">
+            </video>
+          </el-form-item>
+        </div>
+        <div v-if="!form.welcomeId">
+          <el-form-item label="是否通知成员" prop="notify">
+            <el-radio-group v-model="form.notify">
+              <el-radio :label="item.dictValue" v-for="item in sysNoticeStatus">{{item.dictLabel}}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </div>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 大图预览对话框 -->
+    <el-dialog
+      :visible.sync="dialogVisible"
+      :modal="false"
+      width="1200"
+      append-to-body>
+      <img
+        :src="this.dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+  </div>
+
+    <!--详情    -->
+  <el-drawer :title="details.title" :visible.sync="details.open" size="45%" append-to-body>
+      <el-card style="background-color:  rgb(240 242 245)">
+        <el-descriptions :column="1" border  :labelStyle="{width: '130px'}" >
+          <el-descriptions-item label="欢迎语企微id:">
+            <span>{{form.templateId}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="消息文本内容:">
+            <span class="custom-span">{{form.textContent}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图片" v-if="form.welcomeType=='1'">
+            <el-image
+              v-if="form.imagePicUrl"
+              style="width: 100px; height: 100px"
+              :src="form.imagePicUrl"
+              fit="contain"
+              @click="openImageViewer(form.imagePicUrl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文标题:" v-if="form.welcomeType=='2'">
+            <span class="custom-span-title">{{form.linkTitle}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文封面:" v-if="form.welcomeType=='2'">
+            <el-image
+              v-if="form.linkPicurl"
+              style="width: 100px; height: 100px"
+              :src="form.linkPicurl"
+              fit="contain"
+              @click="openImageViewer(form.linkPicurl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文的描述:" v-if="form.welcomeType=='2'">
+            <span class="custom-span">{{form.linkDesc}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文的链接:" v-if="form.welcomeType=='2'">
+            <el-link>{{form.linkUrl}}</el-link>
+<!--            <el-image-->
+<!--              v-if="form.linkUrl"-->
+<!--              style="width: 100px; height: 100px"-->
+<!--              :src="form.linkUrl"-->
+<!--              fit="contain"-->
+<!--              @click="openImageViewer(form.linkUrl)"/>-->
+          </el-descriptions-item>
+          <el-descriptions-item label="小程序标题:" v-if="form.welcomeType=='3'">{{form.miniprogramTitle}}</el-descriptions-item>
+          <el-descriptions-item label="小程序封面:" v-if="form.welcomeType=='3'">
+            <el-image
+              v-if="form.miniprogramPicUrl"
+              style="width: 100px; height: 100px"
+              :src="form.miniprogramPicUrl"
+              fit="contain"
+              @click="openImageViewer(form.miniprogramPicUrl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="小程序appid:" v-if="form.welcomeType=='3'">{{form.miniprogramAppid}}</el-descriptions-item>
+          <el-descriptions-item label="小程序page路径:" v-if="form.welcomeType=='3'">{{form.miniprogramPage}}</el-descriptions-item>
+          <el-descriptions-item label="企微临时文件mediaId:" v-if="form.welcomeType=='4'">{{form.fileMediaId}}</el-descriptions-item>
+          <el-descriptions-item label="文件:" v-if="form.welcomeType=='4'">
+            <el-link v-if="form.fileUrl" type="primary" :href="form.fileUrl" download>
+              {{form.fileUrl}}
+            </el-link>
+          </el-descriptions-item>
+          <el-descriptions-item label="视频临时文件mediaId:" v-if="form.welcomeType=='5'">{{form.videoMediaId}}</el-descriptions-item>
+          <el-descriptions-item label="视频:" v-if="form.welcomeType=='5'">
+            <video v-if="form.videoUrl"
+                   :src="form.videoUrl"
+                   controls style="width: 200px;height: 100px">
+            </video>
+          </el-descriptions-item>
+          <el-descriptions-item label="是否通知成员:" >
+            <!-- 使用 v-if 来筛选并显示匹配的项 -->
+            <span v-if="findMatchingItem(sysNoticeStatus, form.notify)">
+            {{ findMatchingItem(sysNoticeStatus, form.notify).dictLabel }}
+          </span>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人:">{{form.createName}}</el-descriptions-item>
+
+        </el-descriptions>
+      </el-card>
+  </el-drawer>
+
+
+  </div>
+</template>
+
+<script>
+import { listWelcome, getWelcome, delWelcome, addWelcome, updateWelcome, exportWelcome } from "@/api/qw/welcome";
+import ImageUpload from "@/views/qw/material/ImageUpload";
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "deptWelcomeIndex",
+  components: {ImageUpload},
+
+  data() {
+    return {
+      //上传地址
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      //放大图片
+      dialogImageUrl:null,
+      dialogVisible:false,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      myQwCompanyList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 入群欢迎语管理表格数据
+      welcomeList: [],
+      //欢迎语类型
+      welcomeTypeOptions:[],
+      //通知类型
+      sysNoticeStatus:[].map(item => {
+          return { ...item, dictValue: parseInt(item.dictValue, 10) }; // 转换为整数
+      }),
+      formData:null,
+      //文件1图片,文件,视频,音频,图文的封面
+      FileOne:'',
+
+      //文件2 用来存上传图文的图片
+      FileTwo:'',
+
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+
+      details:{
+        title:"",
+        open:false,
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        templateId: null,
+        welcomeType: null,
+        textContent: null,
+        imageMediaId: null,
+        imagePicUrl: null,
+        linkTitle: null,
+        linkPicurl: null,
+        linkDesc: null,
+        linkUrl: null,
+        miniprogramTitle: null,
+        miniprogramPicMediaId: null,
+        miniprogramPicUrl: null,
+        miniprogramAppid: null,
+        miniprogramPage: null,
+        fileMediaId: null,
+        videoMediaId: null,
+        fileUrl: null,
+        notify: null,
+        corpId: null,
+        videoUrl: null,
+        companyId: null,
+        createName: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        welcomeType:[
+          {required:true,message:"类型不能为空",trigger:"blur"}
+        ],
+        textContent:[
+          {required:true,message:"消息文本不能为空",trigger:"blur"}
+        ],
+        linkUrl:[
+          {required:true,message:"链接不能为空",trigger:"blur"}
+        ]
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+        });
+    //文本类型字典
+    this.getDicts("sys_qw_welcome_type").then(response => {
+      this.welcomeTypeOptions = response.data;
+    });
+
+    this.getDicts("sys_company_status").then(response => {
+      this.sysNoticeStatus = response.data;
+    });
+
+  },
+  methods: {
+    updateCorpId(){
+           this.getList();
+    },
+    /** 查询入群欢迎语管理列表 */
+    getList() {
+      this.loading = true;
+      listWelcome(this.queryParams).then(response => {
+        this.welcomeList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    handleAvatarSuccessFile(res, file) {
+      console.log("filefile",file)
+      if(res.code==200){
+        this.form.fileUrl=res.url;
+        // this.FileOne=file
+        this.FileOne=file.raw
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    handleAvatarSuccessVideo(res, file) {
+
+      if(res.code==200){
+        this.form.videoUrl=res.url;
+        // this.self.$forceUpdate()
+        this.FileOne=file.raw
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    beforeAvatarUploadVideo(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    openImageViewer(url) {
+      // 打开大图预览对话框
+      this.dialogImageUrl=url
+      this.dialogVisible = true;
+    },
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+    findMatchingItem(items, value) {
+      // 遍历 items 数组以找到匹配项
+      return items.find(item => item.dictValue == value) || null;
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        welcomeId: null,
+        templateId: null,
+        welcomeType: null,
+        textContent: null,
+        imageMediaId: null,
+        imagePicUrl: null,
+        linkTitle: null,
+        linkPicurl: null,
+        linkDesc: null,
+        linkUrl: null,
+        miniprogramTitle: null,
+        miniprogramPicMediaId: null,
+        miniprogramPicUrl: null,
+        miniprogramAppid: null,
+        miniprogramPage: null,
+        fileMediaId: null,
+        fileUrl: null,
+        videoMediaId: null,
+        videoUrl: null,
+        notify: null,
+        corpId: null,
+        companyId: null,
+        updateTime: null,
+        createTime: null,
+        createName: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.welcomeId)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.form.welcomeType="1"
+      this.form.notify="1"
+      this.title = "添加入群欢迎语";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const welcomeId = row.welcomeId || this.ids
+      getWelcome(welcomeId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改入群欢迎语";
+      });
+    },
+
+    hangdleDitels(row){
+      this.reset();
+      const welcomeId = row.welcomeId || this.ids
+      getWelcome(welcomeId).then(response => {
+        this.form = response.data;
+        this.details.open = true;
+        this.details.title = "欢迎语详情";
+      });
+    },
+    // async urlToFile(url) {
+    //   const response = await fetch(url);
+    //   const data = await response.blob();
+    //   const filename = url.split('/').pop();
+    //   return new File([data], filename, { type: data.type });
+    // },
+
+    /** 提交按钮 */
+    submitForm() {
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          this.form.corpId=this.queryParams.corpId;
+          if (this.form.welcomeId != null) {
+            updateWelcome(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWelcome(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const welcomeIds = row.welcomeId || this.ids;
+      this.$confirm('是否确认删除入群欢迎语编号为"' + welcomeIds + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWelcome(welcomeIds,row.templateId);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+
+  }
+};
+</script>
+<style scoped>
+.flex-container {
+  display: flex; /* 启用Flexbox布局 */
+  flex-direction: column; /* 子元素垂直排列 */
+  align-items: flex-start; /* 子元素在主轴上(水平)从左到右排列 */
+}
+
+.flex-container > div {
+  width: 100%; /* 确保每个子div宽度充满容器宽度 */
+}
+.text-container {
+  max-height: 8.5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.text-container-title{
+  margin-top: 10px;
+  max-height: 5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.custom-span {
+  display: block; /* 确保元素是块级元素 */
+  height: 150px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+.custom-span-title {
+  display: block; /* 确保元素是块级元素 */
+  height: 45px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+</style>

+ 701 - 0
src/views/qw/welcome/myWelcome.vue

@@ -0,0 +1,701 @@
+<template>
+  <div>
+  <div class="app-container">
+    <el-alert
+      title="注意事项"
+      type="warning"
+      description="因企业微信接口限制,1、在【企业微信后台】群欢迎语素材库进行新增/编辑/删除操作,后台不会同步更新,建议在后台入群欢迎语处进行管理。
+       2、 注意:企业微信入群欢迎语素材库中,最多容纳100个素材,包括后台管理的以及企业微信后台管理的。
+       3、除图片外,其他附件素材均只有三天时效,若要续用,请点击修改-》直接点击-确认后,刷新时效"
+      :closable="false"
+      center
+      show-icon>
+    </el-alert>
+  </div>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" :inline="true" v-show="showSearch" label-width="68px" >
+      <el-form-item label="企微公司" prop="corpId">
+            <el-select v-model="queryParams.corpId" placeholder="企微公司"  size="small" @change="updateCorpId()">
+              <el-option
+                v-for="dict in myQwCompanyList"
+                :key="dict.dictValue"
+                :label="dict.dictLabel"
+                :value="dict.dictValue"
+              />
+            </el-select>
+       </el-form-item>
+      <el-form-item label="消息内容" prop="createName">
+        <el-input
+          v-model="queryParams.textContent"
+          placeholder="请输入要搜索的消息文本内容"
+          clearable
+          size="small"
+          @keyup.enter.native="handleQuery"
+        />
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+      <el-form-item>
+        <el-col :span="1.5">
+          <el-button
+            type="primary"
+            plain
+            size="mini"
+            icon="el-icon-plus"
+            @click="handleAdd"
+            v-hasPermi="['qw:welcome:add']"
+          >新增</el-button>
+        </el-col>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <el-table v-loading="loading" :data="welcomeList" border>
+      <el-table-column label="欢迎语类型" align="center" prop="welcomeType" >
+        <template slot-scope="scope">
+          <span   v-for="(item, index) in welcomeTypeOptions"    v-if="scope.row.welcomeType==item.dictValue">{{item.dictLabel}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column label="欢迎消息文本" align="left" width="600px" >
+        <template slot-scope="scope">
+          <div class="flex-container">
+            <el-tooltip class="item" effect="dark" :content="scope.row.textContent" placement="top">
+              <div style="display: -webkit-box; -webkit-box-orient: vertical; -webkit-line-clamp: 3; overflow: hidden; text-overflow: ellipsis;">
+                <span>文本消息:</span>{{ scope.row.textContent }}
+              </div>
+            </el-tooltip>
+            <div v-if="scope.row.welcomeType == '1'">
+              <div style="float: left">
+              <span>图片:</span>
+              <el-image v-if="scope.row.imagePicUrl"
+                style="width: 50px; height: 50px"
+                :src="scope.row.imagePicUrl"
+                fit="contain" />
+                <span v-if="!scope.row.imagePicUrl">无图片</span>
+              </div>
+            </div>
+            <div v-if="scope.row.welcomeType=='2'">
+              <div style="float: left;margin-top: 10px">
+                <el-tooltip class="item" effect="dark" :content="scope.row.linkTitle" placement="top">
+                  <div style="float: left; overflow-y: hidden; height: 50px">
+                    <span>图文标题:</span>{{ scope.row.linkTitle }}
+                  </div>
+                </el-tooltip>
+                <div>
+                  <el-image v-if="scope.row.linkPicurl"
+                            style="width: 50px; height: 50px"
+                            :src="scope.row.linkPicurl"
+                            fit="contain" />
+                </div>
+
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='3'">
+              <div style="float: left">
+                <span>小程序标题:</span>{{scope.row.miniprogramTitle}}
+                <div>
+                  <el-image v-if="scope.row.miniprogramPicUrl"
+                            style="width: 50px; height: 50px"
+                            :src="scope.row.miniprogramPicUrl"
+                            fit="contain" />
+                </div>
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='4'">
+              <div style="float: left">
+                <span>文件:</span>
+                <span>请查看详情</span>
+              </div>
+            </div>
+
+            <div v-if="scope.row.welcomeType=='5'">
+              <div style="float: left">
+                <span>视频:</span>
+                <span>请查看详情</span>
+              </div>
+            </div>
+          </div>
+        </template>
+
+      </el-table-column>
+      <el-table-column label="创建人" align="center" prop="createName" />
+      <el-table-column label="创建时间" align="center" prop="createTime" />
+      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
+        <template slot-scope="scope">
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-notebook-2"
+            @click="hangdleDitels(scope.row)"
+            v-hasPermi="['qw:welcome:query']"
+          >详情</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-edit"
+            @click="handleUpdate(scope.row)"
+            v-hasPermi="['qw:welcome:edit']"
+          >修改</el-button>
+          <el-button
+            size="mini"
+            type="text"
+            icon="el-icon-delete"
+            @click="handleDelete(scope.row)"
+            v-hasPermi="['qw:welcome:remove']"
+          >删除</el-button>
+
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+
+    <!-- 添加或修改入群欢迎语管理对话框 -->
+    <el-dialog :title="title" :visible.sync="open" width="800px" append-to-body>
+
+      <el-form ref="form" :model="form" :rules="rules" label-width="120px">
+        <div v-if="!form.welcomeId">
+          <el-form-item label="欢迎语类型" prop="welcomeType">
+              <el-radio-group v-model="form.welcomeType">
+                <el-radio :label="item.dictValue" v-for="item in welcomeTypeOptions">{{item.dictLabel}}</el-radio>
+              </el-radio-group>
+          </el-form-item>
+        </div>
+        <el-form-item label="消息文本内容" prop="textContent">
+          <el-input v-model="form.textContent" type="textarea" :rows="6" maxlength="1300" show-word-limit placeholder="请输入消息文本内容(图片,视频等附件不上传,则默认为纯文本欢迎语)" />
+        </el-form-item>
+        <div v-if="form.welcomeType=='1'">
+          <el-form-item label="图片" prop="imagePicUrl">
+            <ImageUpload v-model="form.imagePicUrl"  type="image" :num="10" :width="150" :height="150"  disabled/>
+          </el-form-item>
+        </div>
+        <div v-if="form.welcomeType=='2'">
+          <el-form-item label="图文标题" prop="linkTitle" required>
+            <el-input v-model="form.linkTitle" type="textarea" :rows="2" maxlength="42" show-word-limit placeholder="请输入图文消息标题,最长为42字节" />
+          </el-form-item>
+          <el-form-item label="上传图文封面" prop="linkPicurl">
+            <ImageUpload v-model="form.linkPicurl"  type="image" :num="10" :width="150" :height="150" />
+          </el-form-item>
+          <el-form-item label="图文的描述" prop="linkDesc">
+            <el-input v-model="form.linkDesc" type="textarea" :rows="3" maxlength="170" show-word-limit placeholder="请输入内容,,最长为170字节" />
+          </el-form-item>
+          <el-form-item label="图文的链接" prop="linkUrl" required>
+<!--            <ImageUpload v-model="form.linkUrl"   type="image" :num="10" :width="150" :height="150" />-->
+            <el-input v-model="form.linkUrl" placeholder="请输入图文的URL链接" />
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='3'">
+          <el-form-item label="小程序标题" prop="miniprogramTitle">
+            <el-input v-model="form.miniprogramTitle" type="textarea" maxlength="21" show-word-limit placeholder="请输入小程序消息标题,最长为21字节"  />
+          </el-form-item>
+          <el-form-item label="上传小程序封面" prop="miniprogramPicUrl">
+            <ImageUpload v-model="form.miniprogramPicUrl"  type="image" :num="10" :width="150" :height="150" />
+          </el-form-item>
+          <el-form-item label="小程序appid" prop="miniprogramAppid">
+            <el-input v-model="form.miniprogramAppid" placeholder="请输入小程序appid,必须是关联到企业的小程序应用" />
+          </el-form-item>
+          <el-form-item label="小程序page路径" prop="miniprogramPage">
+            <el-input v-model="form.miniprogramPage" placeholder="请输入小程序page路径" />
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='4'">
+          <el-form-item label="上传文件" prop="fileUrl">
+            <el-upload v-if="form.fileUrl==null || form.welcomeId"
+                       v-model="form.fileUrl"
+                       class="avatar-uploader"
+                       :action="uploadUrl"
+                       :show-file-list="false"
+                       :on-success="handleAvatarSuccessFile"
+                       :before-upload="beforeAvatarUploadFile">
+              <i class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <el-link v-if="form.fileUrl" type="primary" :href="downloadUrl(form.fileUrl)" download>
+              {{form.fileUrl}}
+            </el-link>
+          </el-form-item>
+        </div>
+
+        <div v-if="form.welcomeType=='5'">
+          <el-form-item label="上传视频" prop="videoUrl">
+            <el-upload v-if="form.videoUrl==null || form.welcomeId"
+                       v-model="form.videoUrl"
+                       class="avatar-uploader"
+                       :action="uploadUrl"
+                       :show-file-list="false"
+                       :on-success="handleAvatarSuccessVideo"
+                       :before-upload="beforeAvatarUploadVideo">
+              <i class="el-icon-plus avatar-uploader-icon"></i>
+            </el-upload>
+            <video v-if="form.videoUrl"
+                   :src="form.videoUrl"
+                   controls style="width: 200px;height: 100px">
+            </video>
+          </el-form-item>
+        </div>
+        <div v-if="!form.welcomeId">
+          <el-form-item label="是否通知成员" prop="notify">
+            <el-radio-group v-model="form.notify">
+              <el-radio :label="item.dictValue" v-for="item in sysNoticeStatus">{{item.dictLabel}}</el-radio>
+            </el-radio-group>
+          </el-form-item>
+        </div>
+
+      </el-form>
+      <div slot="footer" class="dialog-footer">
+        <el-button type="primary" @click="submitForm">确 定</el-button>
+        <el-button @click="cancel">取 消</el-button>
+      </div>
+    </el-dialog>
+
+    <!-- 大图预览对话框 -->
+    <el-dialog
+      :visible.sync="dialogVisible"
+      :modal="false"
+      width="1200"
+      append-to-body>
+      <img
+        :src="this.dialogImageUrl"
+        style="display: block; max-width: 100%; margin: 0 auto"
+      />
+    </el-dialog>
+  </div>
+
+    <!--详情    -->
+  <el-drawer :title="details.title" :visible.sync="details.open" size="45%" append-to-body>
+      <el-card style="background-color:  rgb(240 242 245)">
+        <el-descriptions :column="1" border  :labelStyle="{width: '130px'}" >
+          <el-descriptions-item label="欢迎语企微id:">
+            <span>{{form.templateId}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="消息文本内容:">
+            <span class="custom-span">{{form.textContent}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图片" v-if="form.welcomeType=='1'">
+            <el-image
+              v-if="form.imagePicUrl"
+              style="width: 100px; height: 100px"
+              :src="form.imagePicUrl"
+              fit="contain"
+              @click="openImageViewer(form.imagePicUrl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文标题:" v-if="form.welcomeType=='2'">
+            <span class="custom-span-title">{{form.linkTitle}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文封面:" v-if="form.welcomeType=='2'">
+            <el-image
+              v-if="form.linkPicurl"
+              style="width: 100px; height: 100px"
+              :src="form.linkPicurl"
+              fit="contain"
+              @click="openImageViewer(form.linkPicurl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文的描述:" v-if="form.welcomeType=='2'">
+            <span class="custom-span">{{form.linkDesc}}</span>
+          </el-descriptions-item>
+          <el-descriptions-item label="图文的链接:" v-if="form.welcomeType=='2'">
+            <el-link>{{form.linkUrl}}</el-link>
+<!--            <el-image-->
+<!--              v-if="form.linkUrl"-->
+<!--              style="width: 100px; height: 100px"-->
+<!--              :src="form.linkUrl"-->
+<!--              fit="contain"-->
+<!--              @click="openImageViewer(form.linkUrl)"/>-->
+          </el-descriptions-item>
+          <el-descriptions-item label="小程序标题:" v-if="form.welcomeType=='3'">{{form.miniprogramTitle}}</el-descriptions-item>
+          <el-descriptions-item label="小程序封面:" v-if="form.welcomeType=='3'">
+            <el-image
+              v-if="form.miniprogramPicUrl"
+              style="width: 100px; height: 100px"
+              :src="form.miniprogramPicUrl"
+              fit="contain"
+              @click="openImageViewer(form.miniprogramPicUrl)"/>
+          </el-descriptions-item>
+          <el-descriptions-item label="小程序appid:" v-if="form.welcomeType=='3'">{{form.miniprogramAppid}}</el-descriptions-item>
+          <el-descriptions-item label="小程序page路径:" v-if="form.welcomeType=='3'">{{form.miniprogramPage}}</el-descriptions-item>
+          <el-descriptions-item label="企微临时文件mediaId:" v-if="form.welcomeType=='4'">{{form.fileMediaId}}</el-descriptions-item>
+          <el-descriptions-item label="文件:" v-if="form.welcomeType=='4'">
+            <el-link v-if="form.fileUrl" type="primary" :href="form.fileUrl" download>
+              {{form.fileUrl}}
+            </el-link>
+          </el-descriptions-item>
+          <el-descriptions-item label="视频临时文件mediaId:" v-if="form.welcomeType=='5'">{{form.videoMediaId}}</el-descriptions-item>
+          <el-descriptions-item label="视频:" v-if="form.welcomeType=='5'">
+            <video v-if="form.videoUrl"
+                   :src="form.videoUrl"
+                   controls style="width: 200px;height: 100px">
+            </video>
+          </el-descriptions-item>
+          <el-descriptions-item label="是否通知成员:" >
+            <!-- 使用 v-if 来筛选并显示匹配的项 -->
+            <span v-if="findMatchingItem(sysNoticeStatus, form.notify)">
+            {{ findMatchingItem(sysNoticeStatus, form.notify).dictLabel }}
+          </span>
+          </el-descriptions-item>
+          <el-descriptions-item label="创建人:">{{form.createName}}</el-descriptions-item>
+
+        </el-descriptions>
+      </el-card>
+  </el-drawer>
+
+
+  </div>
+</template>
+
+<script>
+import { listWelcome, getWelcome, delWelcome, addWelcome, updateWelcome, exportWelcome } from "@/api/qw/welcome";
+import ImageUpload from "@/views/qw/material/ImageUpload";
+import { getMyQwUserList,getMyQwCompanyList } from "@/api/qw/user";
+
+export default {
+  name: "myWelcome",
+  components: {ImageUpload},
+
+  data() {
+    return {
+      //上传地址
+      uploadUrl:process.env.VUE_APP_BASE_API+"/common/uploadOSS",
+      //放大图片
+      dialogImageUrl:null,
+      dialogVisible:false,
+      // 遮罩层
+      loading: true,
+      // 导出遮罩层
+      exportLoading: false,
+      // 选中数组
+      ids: [],
+      myQwCompanyList:[],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 入群欢迎语管理表格数据
+      welcomeList: [],
+      //欢迎语类型
+      welcomeTypeOptions:[],
+      //通知类型
+      sysNoticeStatus:[].map(item => {
+          return { ...item, dictValue: parseInt(item.dictValue, 10) }; // 转换为整数
+      }),
+      formData:null,
+      //文件1图片,文件,视频,音频,图文的封面
+      FileOne:'',
+
+      //文件2 用来存上传图文的图片
+      FileTwo:'',
+
+      // 弹出层标题
+      title: "",
+      // 是否显示弹出层
+      open: false,
+
+      details:{
+        title:"",
+        open:false,
+      },
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        templateId: null,
+        welcomeType: null,
+        textContent: null,
+        imageMediaId: null,
+        imagePicUrl: null,
+        linkTitle: null,
+        linkPicurl: null,
+        linkDesc: null,
+        linkUrl: null,
+        miniprogramTitle: null,
+        miniprogramPicMediaId: null,
+        miniprogramPicUrl: null,
+        miniprogramAppid: null,
+        miniprogramPage: null,
+        fileMediaId: null,
+        videoMediaId: null,
+        fileUrl: null,
+        notify: null,
+        corpId: null,
+        videoUrl: null,
+        companyId: null,
+        createName: null
+      },
+      // 表单参数
+      form: {},
+      // 表单校验
+      rules: {
+        welcomeType:[
+          {required:true,message:"类型不能为空",trigger:"blur"}
+        ],
+        textContent:[
+          {required:true,message:"消息文本不能为空",trigger:"blur"}
+        ],
+        linkUrl:[
+          {required:true,message:"链接不能为空",trigger:"blur"}
+        ]
+      }
+    };
+  },
+  created() {
+    getMyQwCompanyList().then(response => {
+            this.myQwCompanyList = response.data;
+            if(this.myQwCompanyList!=null){
+              this.queryParams.corpId=this.myQwCompanyList[0].dictValue
+              this.getList();
+            }
+        });
+    //文本类型字典
+    this.getDicts("sys_qw_welcome_type").then(response => {
+      this.welcomeTypeOptions = response.data;
+    });
+
+    this.getDicts("sys_company_status").then(response => {
+      this.sysNoticeStatus = response.data;
+    });
+
+  },
+  methods: {
+    updateCorpId(){
+           this.getList();
+    },
+    /** 查询入群欢迎语管理列表 */
+    getList() {
+      this.loading = true;
+      listWelcome(this.queryParams).then(response => {
+        this.welcomeList = response.rows;
+        this.total = response.total;
+        this.loading = false;
+      });
+    },
+    handleAvatarSuccessFile(res, file) {
+      console.log("filefile",file)
+      if(res.code==200){
+        this.form.fileUrl=res.url;
+        // this.FileOne=file
+        this.FileOne=file.raw
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    handleAvatarSuccessVideo(res, file) {
+
+      if(res.code==200){
+        this.form.videoUrl=res.url;
+        // this.self.$forceUpdate()
+        this.FileOne=file.raw
+      }
+      else{
+        this.msgError(res.msg);
+      }
+    },
+    beforeAvatarUploadFile(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    beforeAvatarUploadVideo(file){
+      const isLt1M = file.size / 1024 / 1024 < 10;
+      if (!isLt1M) {
+        this.$message.error('上传大小不能超过 10MB!');
+      }
+      return isLt1M;
+    },
+    openImageViewer(url) {
+      // 打开大图预览对话框
+      this.dialogImageUrl=url
+      this.dialogVisible = true;
+    },
+    //下载文件
+    downloadUrl(materialUrl) {
+      // 直接返回文件 URL
+      return materialUrl;
+    },
+    findMatchingItem(items, value) {
+      // 遍历 items 数组以找到匹配项
+      return items.find(item => item.dictValue == value) || null;
+    },
+    // 取消按钮
+    cancel() {
+      this.open = false;
+      this.reset();
+    },
+    // 表单重置
+    reset() {
+      this.form = {
+        welcomeId: null,
+        templateId: null,
+        welcomeType: null,
+        textContent: null,
+        imageMediaId: null,
+        imagePicUrl: null,
+        linkTitle: null,
+        linkPicurl: null,
+        linkDesc: null,
+        linkUrl: null,
+        miniprogramTitle: null,
+        miniprogramPicMediaId: null,
+        miniprogramPicUrl: null,
+        miniprogramAppid: null,
+        miniprogramPage: null,
+        fileMediaId: null,
+        fileUrl: null,
+        videoMediaId: null,
+        videoUrl: null,
+        notify: null,
+        corpId: null,
+        companyId: null,
+        updateTime: null,
+        createTime: null,
+        createName: null
+      };
+      this.resetForm("form");
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      this.queryParams.pageNum = 1;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.resetForm("queryForm");
+      this.queryParams.corpId= this.myQwCompanyList[0].dictValue
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.welcomeId)
+      this.single = selection.length!==1
+      this.multiple = !selection.length
+    },
+    /** 新增按钮操作 */
+    handleAdd() {
+      this.reset();
+      this.open = true;
+      this.form.welcomeType="1"
+      this.form.notify="1"
+      this.title = "添加入群欢迎语";
+    },
+    /** 修改按钮操作 */
+    handleUpdate(row) {
+      this.reset();
+      const welcomeId = row.welcomeId || this.ids
+      getWelcome(welcomeId).then(response => {
+        this.form = response.data;
+        this.open = true;
+        this.title = "修改入群欢迎语";
+      });
+    },
+
+    hangdleDitels(row){
+      this.reset();
+      const welcomeId = row.welcomeId || this.ids
+      getWelcome(welcomeId).then(response => {
+        this.form = response.data;
+        this.details.open = true;
+        this.details.title = "欢迎语详情";
+      });
+    },
+    // async urlToFile(url) {
+    //   const response = await fetch(url);
+    //   const data = await response.blob();
+    //   const filename = url.split('/').pop();
+    //   return new File([data], filename, { type: data.type });
+    // },
+
+    /** 提交按钮 */
+    submitForm() {
+
+      this.$refs["form"].validate(valid => {
+        if (valid) {
+          this.form.corpId=this.queryParams.corpId;
+          if (this.form.welcomeId != null) {
+            updateWelcome(this.form).then(response => {
+              this.msgSuccess("修改成功");
+              this.open = false;
+              this.getList();
+            });
+          } else {
+            addWelcome(this.form).then(response => {
+              this.msgSuccess("新增成功");
+              this.open = false;
+              this.getList();
+            });
+          }
+        }
+      });
+    },
+    /** 删除按钮操作 */
+    handleDelete(row) {
+      const welcomeIds = row.welcomeId || this.ids;
+      this.$confirm('是否确认删除入群欢迎语编号为"' + welcomeIds + '"的数据项?', "警告", {
+          confirmButtonText: "确定",
+          cancelButtonText: "取消",
+          type: "warning"
+        }).then(function() {
+          return delWelcome(welcomeIds,row.templateId);
+        }).then(() => {
+          this.getList();
+          this.msgSuccess("删除成功");
+        }).catch(() => {});
+    },
+
+  }
+};
+</script>
+<style scoped>
+.flex-container {
+  display: flex; /* 启用Flexbox布局 */
+  flex-direction: column; /* 子元素垂直排列 */
+  align-items: flex-start; /* 子元素在主轴上(水平)从左到右排列 */
+}
+
+.flex-container > div {
+  width: 100%; /* 确保每个子div宽度充满容器宽度 */
+}
+.text-container {
+  max-height: 8.5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.text-container-title{
+  margin-top: 10px;
+  max-height: 5em; /* 设置最大高度为6行,根据字体大小调整 */
+  overflow-y: auto; /* 内容超出时显示滚动条 */
+  line-height: 1.5em; /* 行高设置,确保每行高度一致 */
+}
+.custom-span {
+  display: block; /* 确保元素是块级元素 */
+  height: 150px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+.custom-span-title {
+  display: block; /* 确保元素是块级元素 */
+  height: 45px; /* 设置固定高度 */
+  overflow-y: auto; /* 超出高度时显示滚动条 */
+  word-wrap: break-word; /* 自动换行 */
+  word-break: break-all; /* 在必要时进行换行 */
+}
+</style>

+ 1673 - 0
src/views/statistics/index.vue

@@ -0,0 +1,1673 @@
+<template>
+  <div class="statistics-dashboard">
+    <!-- 数据概览 (Data Overview) -->
+    <el-card class="overview-section" shadow="never">
+      <div slot="header" class="header">
+        <span>数据概览</span>
+      </div>
+
+      <el-row :gutter="20">
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-user-solid"></i>
+              分公司数量
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="dealderCount" :duration="3600" class="card-panel-num" /></div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-user"></i>
+              销售数量
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="groupMgrCount" :duration="3600" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-shopping-cart-full"></i>
+              会员数量
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="memberCount" :duration="3600" class="card-panel-num" />
+              <span class="highlight-today-add">+{{todayIncreaseUserNum}}</span>
+            </div>
+            <div class="card-badge">
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-shopping-cart-full"></i>
+              企微数量
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="qwMemberNum" :duration="3600" class="card-panel-num" /></div>
+            <div class="card-badge">
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-money"></i>
+              可用余额
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="balance" :duration="3600" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <span>今日消耗</span>
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="todayComsumption" :duration="3600" class="card-panel-num" />
+            </div>
+            <div class="card-sub">
+              <span>昨日消耗(元)</span>
+              <span class="sub-value">
+                <count-to :start-val="0" :end-val="yesterdayComsumption" :duration="3600" class="card-panel-num" />
+              </span>
+            </div>
+            <el-progress :percentage="percentage" :show-text="false" color="#409EFF"></el-progress>
+            <div class="card-desc">{{remainMessage}}</div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <span class="cdn-label">CDN</span>
+              今日
+            </div>
+            <div class="card-value highlight">{{formatBytes(this.todayTraffic)}}
+            </div>
+            <div class="card-sub">
+              <span>本月</span>
+              <span class="sub-value">{{formatBytes(this.thisMonthTraffic)}}</span>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-message"></i>
+              短信剩余条数
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="smsRemainCount" :duration="3600" class="card-panel-num" />
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              平台今日看课人数
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="todayWatchUserCount" :duration="3600" class="card-panel-num" />
+            </div>
+            <div class="card-sub">
+              <span>配额上限</span>
+              <span class="sub-value">
+                <count-to :start-val="0" :end-val="todayWatchUserCount" :duration="3600" class="card-panel-num" />/<count-to :start-val="0" :end-val="versionLimit" :duration="3600" class="card-panel-num" /></span>
+            </div>
+            <el-progress :percentage="todayWatchUserCount/versionLimit" :show-text="false" color="#409EFF"></el-progress>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-shopping-cart-full"></i>
+              订单总数
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="orderTotalNum" :duration="3600" class="card-panel-num" />
+              <span class="highlight-today-add">+{{todayOrderNum}}</span>
+            </div>
+            <div class="card-badge">
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-shopping-cart-full"></i>
+              收款总数
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="recvTotalNum" :duration="3600" class="card-panel-num" />
+              <span class="highlight-today-add">+{{recvTodayNum}}</span>
+            </div>
+            <div class="card-badge">
+            </div>
+          </div>
+        </el-col>
+        <el-col :span="3">
+          <div class="data-card">
+            <div class="card-title">
+              <i class="el-icon-shopping-cart-full"></i>
+              商品总数
+            </div>
+            <div class="card-value highlight">
+              <count-to :start-val="0" :end-val="goodsTotalNum" :duration="3600" class="card-panel-num" />
+              <span class="highlight-today-add">+{{todayGoodsNum}}</span>
+            </div>
+            <div class="card-badge">
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </el-card>
+
+    <el-row :gutter="20" class="charts-section">
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header" class="chart-header">
+            <span>本月订单数</span>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="dot viewer-dot"></span>
+                <span>订单数</span>
+              </div>
+              <div class="legend-item">
+                <span class="dot complete-dot"></span>
+                <span>订单金额</span>
+              </div>
+            </div>
+          </div>
+          <div ref="viewerOrderChart" class="chart-container"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header" class="chart-header">
+            <span>本月收款数</span>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="dot viewer-dot"></span>
+                <span>收款数</span>
+              </div>
+              <div class="legend-item">
+                <span class="dot complete-dot"></span>
+                <span>收款金额</span>
+              </div>
+            </div>
+          </div>
+          <div ref="viewerReceiveChart" class="chart-container"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+    <br/>
+    <!-- 分析概览 (Analysis Overview) -->
+    <div class="analysis-section" shadow="never">
+      <div slot="header" class="header">
+        <span>分析概览</span>
+        <div class="tab-group">
+          <el-radio-group v-model="queryTime" size="medium" @change="handleAnalysis">
+            <el-radio-button label="今日"></el-radio-button>
+            <el-radio-button label="昨日"></el-radio-button>
+            <el-radio-button label="本周"></el-radio-button>
+            <el-radio-button label="本月"></el-radio-button>
+            <el-radio-button label="上月"></el-radio-button>
+          </el-radio-group>
+        </div>
+
+        <div class="action-group">
+          <el-radio-group v-model="userTypeText" @change="handleUserType">
+            <el-radio-button label="个微"></el-radio-button>
+            <el-radio-button label="企微"></el-radio-button>
+          </el-radio-group>
+
+          <el-dropdown @command="handleAutoRefresh" trigger="click">
+            <el-button size="small" plain>
+              自动刷新
+              <i class="el-icon-arrow-down el-icon--right"></i>
+            </el-button>
+            <el-dropdown-menu slot="dropdown">
+              <el-dropdown-item :command="0" :class="{ 'is-active': !autoRefreshInterval }">关闭</el-dropdown-item>
+              <el-dropdown-item :command="5" :class="{ 'is-active': autoRefreshInterval === 5 }">5分钟</el-dropdown-item>
+              <el-dropdown-item :command="10" :class="{ 'is-active': autoRefreshInterval === 10 }">10分钟</el-dropdown-item>
+              <el-dropdown-item :command="15" :class="{ 'is-active': autoRefreshInterval === 15 }">15分钟</el-dropdown-item>
+            </el-dropdown-menu>
+          </el-dropdown>
+          <el-button size="small" plain icon="el-icon-refresh" type="primary" @click="manualRefresh">手动刷新</el-button>
+<!--          <el-button size="small" type="primary" @click="refresh">刷新</el-button>-->
+        </div>
+      </div>
+    </div>
+    <div>
+      <el-row :gutter="20">
+        <el-col :span="12" style="position: relative">
+          <div class="analysis-card-check" :class="selectedDiv===0?'analysis-card-check-selected color':''" @click="handleToggleDiv(0)">
+            <div class="analysis-card">
+              <div class="card-icon"><i class="el-icon-monitor"></i></div>
+              <div class="card-content">
+                <div class="card-row">
+                  <span>观看人数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="watchUserCount" :duration="3600" class="card-panel-num" />
+                  </span>
+                </div>
+                <div class="card-row">
+                  <span>完播人数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="completedUserCount" :duration="3600" class="card-panel-num" />
+                  </span>
+                </div>
+                <div class="card-row">
+                  <span>完播率</span>
+                  <span class="highlight">{{completedRate}}%</span>
+                </div>
+              </div>
+            </div>
+            <div class="analysis-card">
+              <div class="card-icon"><i class="el-icon-video-play"></i></div>
+              <div class="card-content">
+                <div class="card-row">
+                  <span>观看次数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="watchCount" :duration="3600" class="card-panel-num" /></span>
+                </div>
+                <div class="card-row">
+                  <span>完播次数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="completedCount" :duration="3600" class="card-panel-num" />
+                  </span>
+                </div>
+                <div class="card-row">
+                  <span>视频完播率</span>
+                  <span class="highlight">{{watchRate}}%</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="6" style="position: relative">
+          <div class="analysis-card-check" :class="selectedDiv===1?'analysis-card-check-selected color':''"  @click="handleToggleDiv(1)">
+            <div class="analysis-card">
+              <div class="card-icon"><i class="el-icon-headset"></i></div>
+              <div class="card-content">
+                <div class="card-row">
+                  <span>答题人数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="answerMemberCount" :duration="3600" class="card-panel-num" />
+                  </span>
+                </div>
+                <div class="card-row">
+                  <span>正确人数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="correctUserCount" :duration="3600" class="card-panel-num" />
+                    </span>
+                </div>
+                <div class="card-row">
+                  <span>正确率</span>
+                  <span class="highlight">{{correctRate}}%</span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+
+        <el-col :span="6" style="position: relative">
+          <div class="analysis-card-check" :class="selectedDiv===2?'analysis-card-check-selected color':''"  @click="handleToggleDiv(2)">
+            <div class="analysis-card">
+              <div class="card-icon"><i class="el-icon-present"></i></div>
+              <div class="card-content">
+                <div class="card-row">
+                  <span>答题红包个数</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="rewardCount" :duration="3600" class="card-panel-num" />
+                    </span>
+                </div>
+                <div class="card-row">
+                  <span>答题红包金额(元)</span>
+                  <span class="highlight">
+                    <count-to :start-val="0" :end-val="rewardMoney" :duration="3600" class="card-panel-num" /></span>
+                </div>
+              </div>
+            </div>
+          </div>
+        </el-col>
+      </el-row>
+    </div>
+
+    <!-- 图表区域 (Charts Area) -->
+    <transition name="fade">
+      <el-row :gutter="20" class="charts-section" v-show="selectedDiv===0">
+        <el-col :span="12">
+          <el-card shadow="never">
+            <div slot="header" class="chart-header">
+              <span>会员观看、完播人数趋势图</span>
+              <div class="legend">
+                <div class="legend-item">
+                  <span class="dot viewer-dot"></span>
+                  <span>观看人数</span>
+                </div>
+                <div class="legend-item">
+                  <span class="dot complete-dot"></span>
+                  <span>完播人数</span>
+                </div>
+              </div>
+              <el-button size="small" plain class="view-more">平台每日统计 <i class="el-icon-arrow-right"></i></el-button>
+            </div>
+            <div ref="viewerChart" class="chart-container"></div>
+          </el-card>
+        </el-col>
+
+        <el-col :span="12">
+          <el-card shadow="never">
+            <div slot="header" class="chart-header">
+              <span>经销商会员观看TOP10</span>
+              <div class="legend">
+                <el-radio-group v-model="viewerType" size="small" @change="handleDealerChartData">
+                  <el-radio-button label="0">按观看人数</el-radio-button>
+                  <el-radio-button label="1">按完播人数</el-radio-button>
+                </el-radio-group>
+              </div>
+              <el-button size="small" plain class="view-more">经销商统计 <i class="el-icon-arrow-right"></i></el-button>
+            </div>
+            <div ref="dealerChart" class="chart-container"></div>
+          </el-card>
+        </el-col>
+      </el-row>
+    </transition>
+    <transition name="fade">
+    <el-row :gutter="20" class="charts-section" v-show="selectedDiv===1">
+    <el-card shadow="never">
+      <div slot="header" class="chart-header">
+        <span>课程观看TOP10</span>
+        <div class="legend">
+          <el-radio-group v-model="viewerType" size="small" @change="handleCourseWatchChart">
+            <el-radio-button label="0">按观看人数</el-radio-button>
+            <el-radio-button label="1">按完播人数</el-radio-button>
+            <el-radio-button label="2">按答题人数</el-radio-button>
+            <el-radio-button label="3">按正确人数</el-radio-button>
+          </el-radio-group>
+        </div>
+        <div class="legend">
+          <el-radio-group v-model="delerSort" @change="handleCourseWatchChart">
+            <el-radio label="DESC">前10名</el-radio>
+            <el-radio label="ASC">倒数10名</el-radio>
+          </el-radio-group>
+        </div>
+        <div class="legend">
+          <div class="legend-item">
+            <span class="dot viewer-dot"></span>
+            <span>观看人数</span>
+          </div>
+          <div class="legend-item">
+            <span class="dot complete-dot"></span>
+            <span>完播人数</span>
+          </div>
+          <div class="legend-item">
+            <span class="dot" style="background-color: #E6A23C"></span>
+            <span>答题人数</span>
+          </div>
+          <div class="legend-item">
+            <span class="dot" style="background-color: #F56C6C"></span>
+            <span>正确人数</span>
+          </div>
+        </div>
+        <el-button size="small" plain class="view-more">经销商统计 <i class="el-icon-arrow-right"></i></el-button>
+      </div>
+      <div ref="courseWatchChart" class="chart-container"></div>
+    </el-card>
+    </el-row>
+    </transition>
+
+    <transition name="fade">
+    <el-row :gutter="20" class="charts-section" v-show="selectedDiv===2">
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header" class="chart-header">
+            <span>答题红包金额TOP10</span>
+            <div class="legend">
+              <el-radio-group v-model="dataType" size="small" @change="handleAnswerRedPackViewerChart">
+                <el-radio-button label="0">按经销商排行</el-radio-button>
+                <el-radio-button label="1">按课程排行</el-radio-button>
+              </el-radio-group>
+            </div>
+            <el-button size="small" plain class="view-more">红包记录 <i class="el-icon-arrow-right"></i></el-button>
+          </div>
+          <div ref="answerRedPackViewerChart" class="chart-container"></div>
+        </el-card>
+      </el-col>
+      <el-col :span="12">
+        <el-card shadow="never">
+          <div slot="header" class="chart-header">
+            <span>答题红包金额趋势图</span>
+            <div class="legend">
+              <div class="legend-item">
+                <span class="dot viewer-dot"></span>
+                <span>答题红包金额</span>
+              </div>
+          </div>
+            <el-button size="small" plain class="view-more">红包记录 <i class="el-icon-arrow-right"></i></el-button>
+          </div>
+          <div ref="answerRedPackMoneyViewerChart" class="chart-container"></div>
+        </el-card>
+      </el-col>
+    </el-row>
+    </transition>
+  </div>
+</template>
+
+<script>
+import * as echarts from 'echarts'
+import CountTo from "vue-count-to";
+import {
+  analysisPreview,
+  authorizationInfo,
+  dealerAggregated, deaMemberTopTen, rechargeComsumption, rewardMoneyTopTen, rewardMoneyTrend,
+  smsBalance, thisMonthOrderCount, thisMonthRecvCount, trafficLog,
+  watchCourseTopTen, watchEndPlayTrend
+} from "@/api/statistics/statistics";
+import dayjs from 'dayjs';
+
+
+const viewCharOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '观看人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    },
+    {
+      name: '完播人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#67C23A'
+      }
+    }
+  ]
+}
+
+const thisMonthOrderCountOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '订单数',
+      type: 'line',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    },
+    {
+      name: '订单金额',
+      type: 'line',
+      data: [],
+      itemStyle: {
+        color: '#67C23A'
+      }
+    }
+  ]
+}
+
+const thisMonthRecvCountOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', '10', '11', '12', '13', '14', '15', '16', '17', '18', '19', '20', '21', '22', '23']
+  },
+  yAxis: {
+    type: 'value'
+  },
+  series: [
+    {
+      name: '收款数',
+      type: 'line',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    },
+    {
+      name: '收款金额',
+      type: 'line',
+      data: [],
+      itemStyle: {
+        color: '#67C23A'
+      }
+    }
+  ]
+}
+const dealerOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'value'
+  },
+  yAxis: {
+    type: 'category',
+    data: []
+  },
+  series: [
+    {
+      name: '观看人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    }
+  ]
+}
+
+const courseWatchOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '8%',
+    top: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: [],
+    axisLabel: {
+      interval: 0,
+      rotate: 30,
+      fontSize: 10,
+      width: 100,
+      overflow: 'truncate'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    splitLine: {
+      lineStyle: {
+        type: 'dashed'
+      }
+    }
+  },
+  series: [
+    {
+      name: '观看人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    },
+    {
+      name: '完播人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#67C23A'
+      }
+    },
+    {
+      name: '答题人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#E6A23C'
+      }
+    },
+    {
+      name: '正确人数',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#F56C6C'
+      }
+    }
+  ]
+}
+
+const lineChartOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'cross' // 改为 'cross' 更适合折线图
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '8%', // 如果x轴标签旋转,可能需要更大的 bottom
+    top: '5%',    // 增加一点顶部空间给可能的 Y 轴名称
+    containLabel: true
+  },
+  xAxis: {
+    type: 'time', // X轴类型改为 'time'
+    // data: [], // 时间轴不需要单独设置 data,数据在 series 中提供
+    axisLabel: {
+      // interval: 0, // 时间轴通常自动处理间隔,可以先移除或注释掉
+      rotate: 30,   // 保留旋转,如果标签可能重叠
+      fontSize: 10,
+      // width: 100, // width 和 overflow 对于时间轴可能行为不同,按需调整
+      // overflow: 'truncate',
+      formatter: null // ECharts 会自动格式化时间,如需特定格式可用 function 或字符串模板
+    }
+  },
+  yAxis: {
+    type: 'value',
+    name: '金额 (元)', // 添加 Y 轴名称
+    nameLocation: 'end', // 名称位置
+    nameTextStyle: {
+      align: 'right',
+      padding: [0, 10, 0, 0] // 调整名称与轴线的距离
+    },
+    splitLine: {
+      lineStyle: {
+        type: 'dashed'
+      }
+    },
+    axisLabel: {
+      formatter: '{value} 元' // 可选:给 Y 轴刻度添加单位
+    }
+  },
+  series: [
+    {
+      name: '答题红包金额',
+      type: 'line', // 系列类型改为 'line'
+      data: [
+      ],
+      itemStyle: { // 控制数据点(标记)的样式
+        color: '#409EFF'
+      },
+      lineStyle: { // 控制线的样式
+        color: '#409EFF'
+      },
+      smooth: false, // 是否平滑曲线,可设为 true
+      symbol: 'circle', // 数据点标记形状,'emptyCircle', 'rect', 'roundRect', 'triangle', 'diamond', 'pin', 'arrow', 'none'
+      symbolSize: 4   // 数据点标记大小
+    }
+  ]
+};
+
+
+const redPackageOption = {
+  tooltip: {
+    trigger: 'axis',
+    axisPointer: {
+      type: 'shadow'
+    }
+  },
+  grid: {
+    left: '3%',
+    right: '4%',
+    bottom: '8%',
+    top: '3%',
+    containLabel: true
+  },
+  xAxis: {
+    type: 'category',
+    data: [],
+    axisLabel: {
+      interval: 0,
+      rotate: 30,
+      fontSize: 10,
+      width: 100,
+      overflow: 'truncate'
+    }
+  },
+  yAxis: {
+    type: 'value',
+    splitLine: {
+      lineStyle: {
+        type: 'dashed'
+      }
+    }
+  },
+  series: [
+    {
+      name: '答题红包金额',
+      type: 'bar',
+      data: [],
+      itemStyle: {
+        color: '#409EFF'
+      }
+    }
+  ]
+}
+export default {
+  name: 'StatisticsDashboard',
+  components: {CountTo},
+  data() {
+    return {
+      percentage: 0,
+      // 预测message
+      remainMessage: '',
+      // 当天使用流量
+      todayTraffic: 0,
+      // 当月使用流量
+      thisMonthTraffic: 0,
+      dataType: '0',
+      delerSort: 'DESC',
+      smsRemainCount: 0,
+      viewerType: '0',
+      viewerChart: null,
+      userTypeText: '个微',
+      userType: 1,
+      dealerChart: null,
+      // 分公司数量
+      dealderCount: 0,
+      // 销售数量
+      groupMgrCount: 0,
+      // 会员总数量
+      memberCount: 0,
+      // 企微数量
+      qwMemberNum: 0,
+      // 正常会员数量
+      normalNum: 0,
+      // 黑名单会员数量
+      blackNum: 0,
+      // 观看人数
+      watchUserCount: 0,
+      // 完播人数
+      completedUserCount: 0,
+      // 完播率
+      completedRate: 0,
+      // 观看次数
+      watchCount:0,
+      // 完播次数
+      completedCount: 0,
+      // 视频完播率
+      watchRate: 0,
+      // 答题人数
+      answerMemberCount: 0,
+      // 正确人数
+      correctUserCount: 0,
+      correctRate: 0.0,
+      // 答题红包个数
+      rewardCount: 0,
+      // 答题红包金额
+      rewardMoney: 0.0,
+      queryTime: '今日',
+      todayWatchUserCount: 0,
+      versionLimit: 0,
+      /// 选中的分析概览
+      selectedDiv: 0,
+      filterType: 0,
+      answerRedPackViewerChart: null,
+      answerRedPackMoneyViewerChart: null,
+      todayComsumption: 0,
+      yesterdayComsumption: 0,
+      balance: 0,
+      autoRefreshInterval: null,
+      // 今日新增用户数
+      todayIncreaseUserNum: 0,
+      // 订单总数
+      orderTotalNum: 0,
+      // 今日新增订单数
+      todayOrderNum: 0,
+      // 收款总数
+      recvTotalNum: 0,
+      // 今日收款总数
+      recvTodayNum: 0,
+      // 商品总数
+      goodsTotalNum: 0,
+      // 今日商品总数
+      todayGoodsNum: 0
+    }
+  },
+  mounted() {
+    this.$nextTick(() => {
+      this.initViewerChart()
+      this.initDealerChart()
+      this.initCourseWatchChart();
+      this.initAnswerRedPackViewerChart();
+      this.initAnswerRedPackMoneyViewerChart();
+      this.initThisMonthOrderChart();
+      this.initThisMonthRecvChart()
+
+      // 监听窗口大小变化,重新渲染图表
+      window.addEventListener('resize', () => {
+        this.viewerChart && this.viewerChart.resize()
+        this.dealerChart && this.dealerChart.resize()
+      })
+    })
+  },
+  created() {
+    this.refresh();
+  },
+  methods: {
+    handleUserType(){
+      if(this.userTypeText === '个微'){
+        this.userType = 1
+      }else{
+        this.userType = 2
+      }
+
+      this.refresh()
+    },
+    /**
+     * 计算余额预计可持续的天数
+     * @param {number} balance - 当前账户余额
+     * @param {number} todayConsumption - 今日消耗金额
+     * @param {number} yesterdayConsumption - 昨日消耗金额
+     * @return {Object} 包含天数和进度百分比的对象
+     */
+    calculateRemainingDays(balance, todayConsumption, yesterdayConsumption) {
+      // 如果今日和昨日消耗都为0,则无法预测(避免除以0)
+      if (todayConsumption === 0 && yesterdayConsumption === 0) {
+        return {
+          days: Infinity,
+          percentage: 0,
+          message: '暂无消耗数据'
+        };
+      }
+
+      // 计算每日平均消耗量
+      const avgDailyConsumption = (todayConsumption + yesterdayConsumption) / 2;
+
+      // 如果平均消耗为0,则无法预测
+      if (avgDailyConsumption === 0) {
+        return {
+          days: Infinity,
+          percentage: 0,
+          message: '暂无消耗数据'
+        };
+      }
+
+      // 计算剩余天数(向下取整)
+      const remainingDays = Math.floor(balance / avgDailyConsumption);
+
+      // 计算进度条百分比,最大为100
+      // 这里假设100天是满值,可以根据需要调整
+      const maxDays = 100;
+      const percentage = Math.min(100, Math.max(0, Math.round((remainingDays / maxDays) * 100)));
+
+      let message = '';
+      if (remainingDays > 365) {
+        message = '预测余额充足';
+      } else {
+        message = `预测不足${remainingDays}天`;
+      }
+
+      return {
+        days: remainingDays,
+        percentage: 100 - percentage,
+        message: message
+      };
+    },
+    /**
+     * 将字节数转换为合适的单位表示(Byte、KB、MB、GB、TB)
+     * @param {number} bytes - 字节数
+     * @param {number} [decimals=2] - 小数点后保留的位数
+     * @returns {string} 格式化后的字符串,包含数值和单位
+     */
+    formatBytes(bytes, decimals = 2) {
+      if (bytes === 0) return '0 Byte';
+
+      const k = 1024;
+      const sizes = ['Byte', 'KB', 'MB', 'GB', 'TB'];
+
+      // 计算合适的单位级别
+      const i = Math.floor(Math.log(bytes) / Math.log(k));
+
+      // 转换为对应单位的值
+      const value = bytes / Math.pow(k, i);
+
+      // 格式化为指定小数位的字符串
+      return parseFloat(value.toFixed(decimals)) + ' ' + sizes[Math.min(i, sizes.length - 1)];
+    },
+    // 手动刷新
+    manualRefresh() {
+      this.refresh();
+    },
+    // 处理自动刷新选项
+    handleAutoRefresh(command) {
+      // 清除之前的定时器
+      if (this.timer) {
+        clearInterval(this.timer);
+        this.timer = null;
+      }
+
+      // 设置新的刷新间隔
+      this.autoRefreshInterval = parseInt(command);
+
+      // 如果间隔大于0,设置新的定时器
+      if (this.autoRefreshInterval > 0) {
+        this.timer = setInterval(() => {
+          this.refresh();
+        }, this.autoRefreshInterval * 60 * 1000); // 转换为毫秒
+
+        this.$message.success(`已设置${this.autoRefreshInterval}分钟自动刷新`);
+      } else {
+        this.$message.info('已关闭自动刷新');
+      }
+    },
+    refresh() {
+      rechargeComsumption().then(res=>{
+        if(res.code === 200){
+          this.balance = res.data.balance;
+          this.todayComsumption = res.data.todayComsumption;
+          this.yesterdayComsumption = res.data.yesterdayComsumption;
+          let calculateRemainingDays1 = this.calculateRemainingDays(this.balance,this.todayComsumption,this.yesterdayComsumption);
+          this.percentage = calculateRemainingDays1.percentage;
+          this.remainMessage = calculateRemainingDays1.message;
+        }
+      });
+
+      trafficLog().then(res=>{
+        if(res.code === 200) {
+          this.todayTraffic = res.data.today;
+          this.thisMonthTraffic = res.data.thisMonth;
+        }
+      })
+
+      dealerAggregated().then(res=>{
+        if(res.code === 200){
+          this.dealderCount = res.data.dealderCount??0;
+          this.groupMgrCount = res.data.groupMgrCount??0;
+          this.memberCount = res.data.memberCount??0;
+          this.qwMemberNum = res.data.qwMemberNum??0;
+          this.normalNum = res.data.normalNum??0;
+          this.blackNum = res.data.blackNum??0;
+          this.todayIncreaseUserNum = res.data.todayIncreaseUserNum??0;
+          this.orderTotalNum = res.data.orderTotalNum??0;
+          this.todayOrderNum = res.data.todayOrderNum??0;
+          this.recvTotalNum = res.data.recvTotalNum??0;
+          this.recvTodayNum = res.data.recvTodayNum??0;
+          this.goodsTotalNum = res.data.goodsTotalNum??0;
+          this.todayGoodsNum = res.data.todayGoodsNum??0;
+        }
+      })
+      let param = this.getParam();
+
+      // 获取当前日期时间
+      const today = dayjs();
+      param.startTime = this.formatDate(today);
+      param.endTime = this.formatDate(today);
+      analysisPreview(param).then(res=>{
+        if(res.code === 200){
+          this.watchUserCount = res.data.watchUserCount;
+          this.completedUserCount = res.data.completedUserCount;
+          this.completedRate = res.data.completedRate;
+          this.watchCount = res.data.watchCount;
+          this.completedCount = res.data.completedCount;
+          this.answerMemberCount = res.data.answerMemberCount;
+          this.correctUserCount = res.data.correctUserCount;
+          this.correctRate = res.data.correctRate;
+          this.rewardCount = res.data.rewardCount;
+          this.rewardMoney = res.data.rewardMoney;
+          this.watchRate = res.data.watchRate;
+        }
+      })
+      smsBalance().then(res=>{
+        if(res.code === 200){
+          if(res.data == null) {
+            this.smsRemainCount = 0;
+          } else {
+            this.smsRemainCount = res.data;
+          }
+        }
+      })
+      authorizationInfo().then(res=>{
+        if(res.code === 200){
+          this.todayWatchUserCount = res.data.todayWatchUserCount;
+          this.versionLimit = res.data.versionLimit;
+        }
+      })
+
+      this.handleCourseWatchChart()
+      this.handleViewChartData()
+
+      // 经销商会员观看TOP10
+      this.handleDealerChartData()
+
+      this.handleAnswerRedPackViewerChart()
+
+      this.handleAnswerRedPackMoneyViewerChart()
+
+      this.handleThisMonthRecvCount();
+      this.handleThisMonthOrderCount();
+
+    },
+    /**
+     * 将数字添加千位分隔符
+     * @param {number|string} num - 需要格式化的数字
+     * @return {string} 添加千位分隔符后的字符串
+     */
+     formatNumberWithCommas(num) {
+      if (num === null || num === undefined || isNaN(Number(num))) {
+        return '0';
+      }
+
+      const numStr = String(num);
+
+      // 处理负数
+      const isNegative = numStr.startsWith('-');
+      const absNumStr = isNegative ? numStr.slice(1) : numStr;
+
+      // 分离整数部分和小数部分
+      const parts = absNumStr.split('.');
+      const integerPart = parts[0];
+      const decimalPart = parts.length > 1 ? '.' + parts[1] : '';
+
+      const formattedInteger = integerPart.replace(/\B(?=(\d{3})+(?!\d))/g, ',');
+
+      return (isNegative ? '-' : '') + formattedInteger + decimalPart;
+    },
+    handleToggleDiv(selected){
+      this.selectedDiv = selected;
+
+      if (selected === 1) {
+        this.$nextTick(() => {
+          if (this.courseWatchChart) {
+            this.courseWatchChart.resize();
+          } else {
+          }
+        });
+      }
+      else if (selected === 0) {
+        this.$nextTick(() => {
+          if (this.viewerChart) this.viewerChart.resize();
+          if (this.dealerChart) this.dealerChart.resize();
+        });
+      } else if (selected === 2) {
+        this.$nextTick(() => {
+          if (this.answerRedPackViewerChart) this.answerRedPackViewerChart.resize();
+          if (this.answerRedPackMoneyViewerChart) this.answerRedPackMoneyViewerChart.resize();
+        });
+      }
+      if(this.selectedDiv === 0){
+        this.handleViewChartData()
+        this.handleDealerChartData()
+      } else if(this.selectedDiv === 1) {
+        this.handleCourseWatchChart()
+      } else if(this.selectedDiv === 2) {
+        this.handleAnswerRedPackViewerChart()
+        this.handleAnswerRedPackMoneyViewerChart()
+      }
+    },
+    formatDate(date) {
+      return dayjs(date).format('YYYY-MM-DD');
+    },
+
+    getParam(){
+      let param = {
+        startTime: '',
+        endTime: '',
+        userType: this.userType
+      };
+      // 获取当前日期时间
+      const today = dayjs();
+
+      let type = 0;
+      if (this.queryTime === '今日') {
+        param.startTime = this.formatDate(today);
+        param.endTime = this.formatDate(today);
+        type = 0;
+      } else if (this.queryTime === '昨日') {
+        const yesterday = today.subtract(1, 'day');
+        param.startTime = this.formatDate(yesterday);
+        param.endTime = this.formatDate(yesterday);
+        type = 1;
+      } else if (this.queryTime === '本周') {
+        param.startTime = this.formatDate(today.startOf('week'));
+        param.endTime = this.formatDate(today.endOf('week'));
+        type = 2;
+      } else if (this.queryTime === '本月') {
+        param.startTime = this.formatDate(today.startOf('month'));
+        param.endTime = this.formatDate(today.endOf('month'));
+        type = 3;
+      } else if (this.queryTime === '上月') {
+        const lastMonth = today.subtract(1, 'month');
+        param.startTime = this.formatDate(lastMonth.startOf('month'));
+        param.endTime = this.formatDate(lastMonth.endOf('month'));
+        type = 4;
+      } else {
+        console.warn(`未知的 queryTime: ${this.queryTime}, 默认使用今日`);
+        param.startTime = this.formatDate(today);
+        param.endTime = this.formatDate(today);
+      }
+      param.type = type;
+      param.sort = this.delerSort;
+      return param;
+    },
+    // 分析概览
+    handleAnalysis(e){
+
+      let param = this.getParam();
+      analysisPreview(param).then(res=>{
+        if(res.code === 200){
+          this.watchUserCount = res.data.watchUserCount;
+          this.completedUserCount = res.data.completedUserCount;
+          this.completedRate = res.data.completedRate;
+          this.watchCount = res.data.watchCount;
+          this.completedCount = res.data.completedCount;
+          this.answerMemberCount = res.data.answerMemberCount;
+          this.correctUserCount = res.data.correctUserCount;
+          this.correctRate = res.data.correctRate;
+          this.rewardCount = res.data.rewardCount;
+          this.rewardMoney = res.data.rewardMoney;
+          this.watchRate = res.data.watchRate;
+        }
+      })
+
+      if(this.selectedDiv === 0){
+        this.handleViewChartData()
+        this.handleDealerChartData()
+      } else if(this.selectedDiv === 1) {
+        this.handleCourseWatchChart()
+      } else if(this.selectedDiv === 2) {
+        this.handleAnswerRedPackViewerChart()
+        this.handleAnswerRedPackMoneyViewerChart()
+      }
+    },
+    handleAnswerRedPackViewerChart(){
+      let param = this.getParam();
+      param = {...param,statisticalType:this.viewerType,dataType: this.dataType};
+      rewardMoneyTopTen(param).then(res=>{
+        if(res.code === 200){
+          let data = res.data;
+          let companyNameList = data.map(e=>e.companyName)
+          let courseNameList = data.map(e=>e.courseName)
+          let rewardMoneyList = data.map(e=>e.rewardMoney)
+          if(this.dataType === '0'){
+            redPackageOption.xAxis.data = companyNameList;
+          }else{
+            redPackageOption.xAxis.data = courseNameList;
+          }
+          redPackageOption.series[0].data = rewardMoneyList;
+
+          this.answerRedPackViewerChart.setOption(redPackageOption)
+        }
+      })
+    },
+    handleAnswerRedPackMoneyViewerChart(){
+      let param = this.getParam();
+      param = {...param,statisticalType:this.viewerType,dataType: this.dataType};
+      rewardMoneyTrend(param).then(res=>{
+        if(res.code === 200){
+          let data = res.data;
+          let option = data.map(e=>[e.x,e.rewardMoney])
+          lineChartOption.series[0].data = option;
+
+          this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
+        }
+      })
+    },
+    handleCourseWatchChart() {
+      let param = this.getParam();
+      param = {...param,statisticalType:this.viewerType};
+      watchCourseTopTen(param).then(res=>{
+        if(res.code === 200){
+          let data = res.data;
+          let watchUserCountList = data.map(e=>e.watchUserCount);
+          let completedUserCountList = data.map(e=>e.completedUserCount);
+          let answerUserCountList = data.map(e=>e.answerUserCount);
+          let correctUserCountList = data.map(e=>e.correctUserCount);
+          let courseNameList = data.map(e=>e.courseName);
+          courseWatchOption.xAxis.data = courseNameList;
+          courseWatchOption.series[0].data = watchUserCountList;
+          courseWatchOption.series[1].data = completedUserCountList;
+          courseWatchOption.series[2].data = answerUserCountList;
+          courseWatchOption.series[3].data = correctUserCountList;
+          this.courseWatchChart.setOption(courseWatchOption)
+        }
+      })
+    },
+    handleDealerChartData(){
+      let param = this.getParam();
+
+      // 经销商会员观看TOP10
+      deaMemberTopTen({...param,statisticalType: this.viewerType}).then(res=>{
+        if(res.code === 200){
+          let data = res.data;
+          let companyNameList = data.map(e=>e.companyName);
+          let watchUserList = data.map(e=>e.watchUserCount);
+          dealerOption.yAxis.data = companyNameList;
+          dealerOption.series[0].data = watchUserList;
+
+          this.dealerChart.setOption(dealerOption)
+        }
+      })
+
+    },
+    handleThisMonthOrderCount(){
+      thisMonthOrderCount().then(res=>{
+        if(res.code === 200){
+          let dates = res.dates;
+          let orderCount = res.orderCount;
+          let payPrice = res.payPrice;
+
+          thisMonthOrderCountOption.series[0].data = orderCount;
+          thisMonthOrderCountOption.series[1].data = payPrice;
+          thisMonthOrderCountOption.xAxis.data = dates;
+
+          this.thisMonthOrderChart.setOption(thisMonthOrderCountOption)
+        }
+      })
+    },
+    handleThisMonthRecvCount(){
+      thisMonthRecvCount().then(res=>{
+        if(res.code === 200){
+          let dates = res.dates;
+          let orderCount = res.orderCount;
+          let payMoney = res.payMoney;
+
+          thisMonthRecvCountOption.series[0].data = orderCount;
+          thisMonthRecvCountOption.series[1].data = payMoney;
+          thisMonthRecvCountOption.xAxis.data = dates;
+          this.thisMonthRecvChart.setOption(thisMonthRecvCountOption)
+        }
+      })
+    },
+    handleViewChartData(){
+      let param = this.getParam();
+
+      watchEndPlayTrend({...param}).then(res=>{
+        if(res.code === 200){
+          let data = res.data;
+          let watchUserCountList = data.map(e=>e.watchUserCount);
+          let completedUserCountList = data.map(e=>e.completedUserCount);
+          let xAxis = data.map(e=>e.x);
+          viewCharOption.series[0].data = watchUserCountList;
+          viewCharOption.series[1].data = completedUserCountList;
+          viewCharOption.xAxis.data = xAxis;
+
+          this.viewerChart.setOption(viewCharOption);
+        }
+      })
+
+    },
+    initThisMonthOrderChart(){
+      this.thisMonthOrderChart = echarts.init(this.$refs.viewerOrderChart)
+      this.thisMonthOrderChart.setOption(thisMonthOrderCountOption)
+    },
+    initThisMonthRecvChart(){
+      this.thisMonthRecvChart = echarts.init(this.$refs.viewerReceiveChart)
+      this.thisMonthRecvChart.setOption(thisMonthOrderCountOption)
+    },
+    initViewerChart() {
+      this.viewerChart = echarts.init(this.$refs.viewerChart)
+      this.viewerChart.setOption(viewCharOption)
+    },
+    initDealerChart() {
+      this.dealerChart = echarts.init(this.$refs.dealerChart)
+
+      this.dealerChart.setOption(dealerOption)
+    },
+    initCourseWatchChart() {
+      this.courseWatchChart = echarts.init(this.$refs.courseWatchChart)
+
+      this.courseWatchChart.setOption(courseWatchOption)
+    },
+    initAnswerRedPackViewerChart(){
+      this.answerRedPackViewerChart = echarts.init(this.$refs.answerRedPackViewerChart)
+
+      this.answerRedPackViewerChart.setOption(redPackageOption)
+    },
+    initAnswerRedPackMoneyViewerChart(){
+      this.answerRedPackMoneyViewerChart = echarts.init(this.$refs.answerRedPackMoneyViewerChart)
+
+      this.answerRedPackMoneyViewerChart.setOption(lineChartOption)
+    }
+  },
+
+  beforeDestroy() {
+    // 组件销毁时清除定时器
+    if (this.timer) {
+      clearInterval(this.timer);
+      this.timer = null;
+    }
+    // window.removeEventListener('resize', this.resizeHandler)
+    this.viewerChart && this.viewerChart.dispose()
+    this.dealerChart && this.dealerChart.dispose()
+  }
+}
+</script>
+
+<style scoped>
+.highlight-today-add{
+  color:green;font-size:17px;font-weight: normal;
+}
+.action-group .el-button + .el-button,
+.action-group .el-dropdown {
+  margin-left: 10px;
+}
+.is-active {
+  color: #409EFF;
+  font-weight: bold;
+}
+::v-deep .el-radio-button__inner:hover {
+  color: #409EFF; /* 鼠标悬浮时的文字颜色,可以根据需要调整 */
+}
+::v-deep .el-radio-button.is-active .el-radio-button__inner {
+  background-color: #409EFF; /* 选中时的背景色 */
+  border-color: #409EFF;     /* 选中时的边框色 */
+  color: #FFFFFF;           /* 选中时的文字颜色 (通常是白色) */
+  box-shadow: -1px 0 0 0 #409EFF; /* 处理按钮间的连接缝隙 */
+}
+/* 如果需要,也可以修改非选中状态下的聚焦(focus)或悬浮(hover)样式 */
+/* 例如,让非选中按钮悬浮时边框和文字也变蓝 */
+::v-deep .el-radio-button:not(.is-active) .el-radio-button__inner:hover {
+  color: #409EFF;
+  /* border-color: #b3d8ff;  Element UI 默认悬浮边框色,可以按需修改 */
+}
+/* 聚焦时的外框,如果需要的话 */
+::v-deep .el-radio-button:focus:not(.is-checked) .el-radio-button__inner {
+  /* border-color: #409EFF; */ /* Element UI 默认的 focus 颜色通常关联主题色 */
+  /* box-shadow: 0 0 2px 2px rgba(64, 158, 255, 0.2); */ /* 示例 focus 光晕 */
+}
+.statistics-dashboard {
+  padding: 20px;
+  background-color: #f5f7fa;
+}
+
+.overview-section,
+.analysis-section {
+  margin-bottom: 20px;
+  border-radius: 4px;
+}
+
+.header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+  font-size: 16px;
+  font-weight: 500;
+}
+
+.data-card {
+  background-color: #fff;
+  border-radius: 4px;
+  padding: 15px;
+  height: 100px;
+  display: flex;
+  flex-direction: column;
+  position: relative;
+  transition: background-color 0.3s ease-in-out;
+}
+.data-card:hover{
+  border: 1px solid #4592ff;
+  background-color: #e7f1ff;
+}
+
+.card-title {
+  color: #606266;
+  font-size: 14px;
+  margin-bottom: 10px;
+}
+
+.card-value {
+  font-size: 24px;
+  font-weight: bold;
+  margin-top: auto;
+}
+
+.highlight {
+  color: #409EFF;
+}
+
+.card-sub {
+  display: flex;
+  justify-content: space-between;
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+}
+
+.card-desc {
+  font-size: 12px;
+  color: #909399;
+  margin-top: 5px;
+}
+
+.card-badge {
+  position: absolute;
+  top: 15px;
+  right: 15px;
+  background: #f0f9eb;
+  color: #67c23a;
+  padding: 2px 5px;
+  border-radius: 4px;
+}
+
+.cdn-label {
+  background-color: #409EFF;
+  color: white;
+  padding: 2px 5px;
+  border-radius: 4px;
+  margin-right: 5px;
+  font-size: 12px;
+}
+
+.tab-group {
+  display: flex;
+  gap: 10px;
+}
+
+.action-group {
+  display: flex;
+  gap: 10px;
+}
+
+.analysis-card {
+  border-radius: 4px;
+  padding: 20px;
+  display: flex;
+  align-items: center;
+}
+
+.card-icon {
+  width: 50px;
+  height: 50px;
+  background-color: rgba(64, 158, 255, 0.1);
+  border-radius: 8px;
+  display: flex;
+  justify-content: center;
+  align-items: center;
+  font-size: 24px;
+  color: #409EFF;
+  margin-right: 20px;
+}
+
+.card-content {
+  display: flex;
+}
+
+.card-row {
+  display: flex;
+  justify-content: center;
+  justify-items: center;
+  flex-direction: column;
+  padding: 10px;
+  .highlight{
+    text-align: center;
+    margin-top: 1em;
+
+    font-family: BebasNeue;
+    color: #1677ff;
+    font-size: 26px;
+    line-height: 42px;
+    font-weight: 400;
+    margin-top: 8px;
+  }
+  font-size: 15px;
+  color: #000;
+
+}
+
+.charts-section {
+  margin-top: 20px;
+}
+
+.chart-header {
+  display: flex;
+  justify-content: space-between;
+  align-items: center;
+}
+
+.view-more {
+  font-size: 12px;
+}
+
+.legend {
+  display: flex;
+  gap: 15px;
+}
+
+.legend-item {
+  display: flex;
+  align-items: center;
+  font-size: 12px;
+}
+
+.dot {
+  width: 10px;
+  height: 10px;
+  border-radius: 50%;
+  margin-right: 5px;
+}
+
+.viewer-dot {
+  background-color: #409EFF;
+}
+
+.complete-dot {
+  background-color: #67C23A;
+}
+
+.chart-container {
+  height: 350px;
+  width: 100%;
+}
+.analysis-card-check{
+  display: flex;
+  flex-direction: row;
+  border: 1px solid transparent;
+  background-color: #fff;
+  border-radius: 4px;
+}
+.analysis-card-check:hover{
+  cursor: pointer;
+}
+.analysis-card-check-selected:after{
+  content: "";
+  display: block;
+  border-width: 15px;
+  position: absolute;
+  bottom: -30px;
+  left: 50%;
+  margin-left: -32px;
+  border-style: solid dashed dashed solid;
+  border-color: #4592FF transparent transparent transparent;
+  font-size: 0;
+  line-height: 0;
+  z-index:1;
+}
+.analysis-card-check-selected:before{
+  content: "";
+  display: block;
+  border-width: 15px;
+  position: absolute;
+  bottom: -30px;
+  left: 50%;
+  margin-left: -32px;
+  border-style: solid dashed dashed solid;
+  border-color: #4592FF transparent transparent transparent;
+  font-size: 0;
+  line-height: 0;
+  z-index:1;
+}
+.analysis-card-check-selected{
+  border: 1px solid #4592FF;
+  background-color: #e7f1ff;
+}
+.color{
+  position: relative;
+  border: 1px solid #4592FF;
+  background-color: #e7f1ff;
+}
+.color:after {
+  bottom: -27px;
+  border-color: #E7F1FF transparent transparent transparent;
+}
+.legend-group{
+
+}
+</style>

+ 435 - 0
src/views/statistics/section/channel.vue

@@ -0,0 +1,435 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="公司名" prop="companyId">
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          placeholder="请选择销售"
+          :parentSelectable="true"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          @change="handleMultiChange"
+        ></select-tree>
+      </el-form-item>
+      <el-form-item label="销售" prop="nickName" v-if="queryParams.companyId">
+        <el-select v-model="queryParams.companyUserId" remote
+                   placeholder="请选择"
+                   filterable clearable
+                   style="width: 100%;"
+                   @keyup.enter.native="handleQuery"
+                   @change="handleCompanyUserId"
+        >
+          <el-option
+            v-for="dict in companyUserList"
+            :key="`${dict.nickName} - ${dict.userName}`"
+            :label="`${dict.nickName} - ${dict.userName}`"
+            :value="dict.userId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="dateRange">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="yyyy-MM-dd"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item label="SOP任务" prop="channel" label-width="70px">
+        <date-range
+          v-model="selectedMultipleTasks"
+          :raw-data="channelList"
+          placeholder="请选择多个任务"
+          :parentSelectable="true"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          :show-node-detail="true"
+          @change="handleMultiChange"
+          @dateRangeChange="dateRangeChange"
+        ></date-range>
+      </el-form-item>
+      <el-form-item label="包含未发课" label-width="120px">
+        <el-checkbox v-model="queryParams.includeSend0">
+        </el-checkbox>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['system:stats:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 警告提示 -->
+    <el-alert
+      title="待激活人数依赖进粉时的自动标记和员工手动标记是可变的,相应的目标人数和进线完播率受此影响;为了不受进粉期人数变化影响,第二天新进报名产生的观后数据不做统计;辅助工作台人数不一致可能是受进粉影响,也有可能是员工删除了好友或者取消了客户频道报名。"
+      type="warning"
+      :closable="false"
+      show-icon
+      class="mb8">
+    </el-alert>
+
+    <el-table border v-loading="loading" :data="statsList" @selection-change="handleSelectionChange" show-summary :summary-method="getSummaries">
+      <!-- 未上线部分 -->
+      <el-table-column prop="periodName" label="sop营期" width="180" align="center"/>
+<!--      <el-table-column prop="deptName" label="所属部门" width="180" align="center"/>-->
+<!--      <el-table-column prop="companyUserName" label="员工"  />-->
+      <el-table-column prop="sopTaskName" label="sop任务名"  />
+      <el-table-column prop="trainCampNum" label="营期人数"  align="center" />
+      <el-table-column prop="sendNum" label="发课数"  align="center" />
+      <el-table-column prop="notRegisteredNum" label="待看课"  align="center" />
+      <el-table-column prop="registeredNum" label="上线数"  align="center" />
+      <el-table-column prop="interruptNum" label="看课中断"  align="center" />
+      <el-table-column prop="completedNum" label="已完课"  align="center" />
+      <el-table-column prop="qwRepeatNum" label="企微重粉"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.qwRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="userRepeatNum" label="看课重粉(小程序)"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.userRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="regRate" label="上线率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.regRate === 'number'">
+            {{ (scope.row.regRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+
+      <el-table-column prop="finishedRate" label="完课率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.finishedRate === 'number'">
+            {{ (scope.row.finishedRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+      <!--        汇总-->
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  listEmployeeStats,
+  getEmployeeList,
+  getChannelList,
+  exportEmployeeStats,
+  getSOPTaskData,
+  listPeriodList,
+  getDeptData, exportSellerData, exportPeriodData
+} from '@/api/system/employeeStats'
+import {getCompanyList} from "@/api/company/company";
+import {getUserList} from "@/api/company/companyUser";
+import SelectTree from "@/components/TreeSelect/index.vue";
+import dateRange from '@/components/TreeSelect/dateRange.vue'
+
+export default {
+  name: "EmployeeStats",
+  components: {SelectTree,dateRange},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      companys:[],
+      selectedMultipleTasks: [],
+      selectedCompanyList: [],
+      deptList: [],
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 员工统计表格数据
+      statsList: [],
+      // 员工列表
+      employeeList: [],
+      companyUserList: [],
+      // 频道列表
+      channelList: [],
+      // 日期范围
+      dateRange: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        channel: null,
+        startDate: null,
+        endDate: null,
+        companyId: null,
+        periodList: [],
+        // 包含未发课数
+        includeSend0: false
+      },
+      // 总体统计
+      totalStats: {
+        totalEmployees: 0,
+        onlineEmployees: 0,
+        offlineEmployees: 0,
+        completionRate: '0.00'
+      }
+    };
+  },
+  created() {
+    this.getList();
+
+    // getCompanyList().then(response => {
+    //   this.companys = response.data;
+    // });
+
+    getSOPTaskData({}).then(response => {
+      this.channelList = response.data;
+    });
+    getDeptData().then(response => {
+      this.deptList = response.data;
+    })
+  },
+  methods: {
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '合计';
+          return;
+        }
+
+        const values = data.map(item => Number(item[column.property]));
+
+        if (['trainCampNum', 'sendNum', 'notRegisteredNum', 'registeredNum', 'interruptNum', 'completedNum', 'qwRepeatNum', 'userRepeatNum'].includes(column.property)) {
+          if (!values.every(value => isNaN(value))) {
+            sums[index] = values.reduce((prev, curr) => {
+              const value = Number(curr);
+              if (!isNaN(value)) {
+                return prev + curr;
+              } else {
+                return prev;
+              }
+            }, 0);
+          } else {
+            sums[index] = 'N/A';
+          }
+        }
+        else if (column.property === 'regRate') {
+          const totalRegistered = data.reduce((sum, item) => sum + (Number(item.registeredNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalRegistered / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else if (column.property === 'finishedRate') {
+          const totalCompleted = data.reduce((sum, item) => sum + (Number(item.completedNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalCompleted / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else {
+          sums[index] = '';
+        }
+      });
+
+      return sums;
+    },
+    handleCompanyUserId(val){
+      if(val == null || val === '') {
+        this.queryParams.companyUserId = null;
+        this.queryParams.userIds = [];
+      }
+    },
+    dateRangeChange(e){
+      getSOPTaskData({
+        startDate: e[0],
+        endDate: e[1]
+      }).then(response => {
+        this.channelList = response.data;
+      });
+    },
+    handleMultiChange(e){
+
+    },
+    handleSeller(){
+      if(this.queryParams.companyId != null) {
+        getUserList(this.queryParams.companyId).then(res=>{
+          if(res.code === 200) {
+            this.companyUserList = res.data
+          }
+        })
+      } else {
+        this.queryParams.companyUserId = null;
+      }
+    },
+    /** 查询员工统计列表 */
+    getList() {
+      this.loading = true;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      }else {
+        this.queryParams.userIds = [];
+      }
+
+      listPeriodList(this.queryParams).then(response => {
+        console.log(response)
+        this.statsList = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 获取员工列表 */
+    getEmployeeList() {
+      listPeriodList().then(response => {
+        this.employeeList = response.data || [];
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      console.log(this.selectedCompanyList == null || this.selectedCompanyList.length < 0)
+      if(this.selectedCompanyList == null || this.selectedCompanyList.length <= 0) {
+        this.$message.error("公司和时间为必选!");
+        return;
+      }
+      this.queryParams.pageNum = 1;
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.selectedMultipleTasks = [];
+      this.selectedCompanyList = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      }else {
+        this.queryParams.userIds = [];
+      }
+      const queryParams = this.queryParams;
+
+      this.$confirm('是否确认导出当前查询数据?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportPeriodData(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+      })
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.stats-card {
+  text-align: center;
+  margin-bottom: 16px;
+
+  .stats-number {
+    font-size: 24px;
+    font-weight: bold;
+    color: #409EFF;
+
+    &.online {
+      color: #67C23A;
+    }
+
+    &.offline {
+      color: #F56C6C;
+    }
+
+    &.rate {
+      color: #E6A23C;
+    }
+  }
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+::v-deep .el-table .el-table__header th {
+  background-color: #f5f7fa;
+}
+
+::v-deep .el-alert {
+  margin-bottom: 16px;
+}
+</style>

+ 416 - 0
src/views/statistics/section/index.vue

@@ -0,0 +1,416 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="公司" prop="companyId">
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          :parentSelectable="true"
+          placeholder="请选择销售"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          @change="handleMultiChange"
+        ></select-tree>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="dateRange">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="yyyy-MM-dd"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item label="SOP任务" prop="channel" label-width="70px">
+        <date-range
+          v-model="selectedMultipleTasks"
+          :raw-data="channelList"
+          placeholder="请选择多个任务"
+          :parentSelectable="true"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          :show-node-detail="true"
+        @change="handleMultiChange"
+        @dateRangeChange="dateRangeChange"
+        ></date-range>
+      </el-form-item>
+      <el-form-item label="包含未发课" label-width="120px">
+        <el-checkbox v-model="queryParams.includeSend0">
+        </el-checkbox>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['system:stats:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 警告提示 -->
+    <el-alert
+      title="待激活人数依赖进粉时的自动标记和员工手动标记是可变的,相应的目标人数和进线完播率受此影响;为了不受进粉期人数变化影响,第二天新进报名产生的观后数据不做统计;辅助工作台人数不一致可能是受进粉影响,也有可能是员工删除了好友或者取消了客户频道报名。"
+      type="warning"
+      :closable="false"
+      show-icon
+      class="mb8">
+    </el-alert>
+
+    <el-table border v-loading="loading" :data="statsList" @selection-change="handleSelectionChange" show-summary :summary-method="getSummaries">
+      <!-- 未上线部分 -->
+        <el-table-column prop="deptName" label="所属部门" width="180" align="center"/>
+        <el-table-column prop="companyUserName" label="员工"  />
+      <el-table-column prop="trainCampNum" label="营期人数"  align="center" />
+      <el-table-column prop="sendNum" label="发课数"  align="center" />
+      <el-table-column prop="notRegisteredNum" label="待看课"  align="center" />
+      <el-table-column prop="registeredNum" label="上线数"  align="center" />
+      <el-table-column prop="interruptNum" label="看课中断"  align="center" />
+      <el-table-column prop="completedNum" label="已完课"  align="center" />
+      <el-table-column prop="qwRepeatNum" label="企微重粉"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.qwRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="userRepeatNum" label="看课重粉(小程序)"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.userRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="regRate" label="上线率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.regRate === 'number'">
+            {{ (scope.row.regRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+
+      <el-table-column prop="finishedRate" label="完课率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.finishedRate === 'number'">
+            {{ (scope.row.finishedRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  listEmployeeStats,
+  getEmployeeList,
+  getChannelList,
+  exportEmployeeStats,
+  getSOPTaskData,
+  getDeptData,
+  exportSellerData
+} from '@/api/system/employeeStats'
+import {getCompanyList} from "@/api/company/company";
+import {getUserList} from "@/api/company/companyUser";
+import SelectTree from "@/components/TreeSelect/index.vue";
+import dateRange from '@/components/TreeSelect/dateRange.vue'
+
+export default {
+  name: "EmployeeStats",
+  components: {SelectTree,dateRange},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      companys:[],
+      selectedMultipleTasks: [],
+      selectedCompanyList: [],
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 员工统计表格数据
+      statsList: [],
+      // 员工列表
+      employeeList: [],
+      companyUserList: [],
+      // 频道列表
+      channelList: [],
+      // 日期范围
+      dateRange: [],
+      deptList: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        channel: null,
+        startDate: null,
+        endDate: null,
+        companyId: null,
+        periodList: [],
+        // 包含未发课数
+        includeSend0: false
+      },
+      // 总体统计
+      totalStats: {
+        totalEmployees: 0,
+        onlineEmployees: 0,
+        offlineEmployees: 0,
+        completionRate: '0.00'
+      }
+    };
+  },
+  created() {
+    this.getList();
+
+    // getCompanyList().then(response => {
+    //   this.companys = response.data;
+    // });
+
+    getSOPTaskData({}).then(response => {
+      this.channelList = response.data;
+    });
+
+    getDeptData().then(response => {
+      this.deptList = response.data;
+    })
+  },
+  methods: {
+    dateRangeChange(e){
+      getSOPTaskData({
+        startDate: e[0],
+        endDate: e[1]
+      }).then(response => {
+        this.channelList = response.data;
+      });
+    },
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '合计';
+          return;
+        }
+
+        const values = data.map(item => Number(item[column.property]));
+
+        if (['trainCampNum', 'sendNum', 'notRegisteredNum', 'registeredNum', 'interruptNum', 'completedNum', 'qwRepeatNum', 'userRepeatNum'].includes(column.property)) {
+          if (!values.every(value => isNaN(value))) {
+            sums[index] = values.reduce((prev, curr) => {
+              const value = Number(curr);
+              if (!isNaN(value)) {
+                return prev + curr;
+              } else {
+                return prev;
+              }
+            }, 0);
+          } else {
+            sums[index] = 'N/A';
+          }
+        }
+        else if (column.property === 'regRate') {
+          const totalRegistered = data.reduce((sum, item) => sum + (Number(item.registeredNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalRegistered / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else if (column.property === 'finishedRate') {
+          const totalCompleted = data.reduce((sum, item) => sum + (Number(item.completedNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalCompleted / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else {
+          sums[index] = '';
+        }
+      });
+
+      return sums;
+    },
+    handleCompanyUserId(val){
+      if(val == null || val === '') {
+        this.queryParams.companyUserId = null;
+        this.queryParams.userIds = [];
+      }
+    },
+    handleMultiChange(e){
+
+    },
+    handleSeller(){
+      if(this.queryParams.companyId != null) {
+        getUserList(this.queryParams.companyId).then(res=>{
+          if(res.code === 200) {
+            this.companyUserList = res.data
+          }
+        })
+      } else {
+        this.queryParams.companyUserId = null;
+      }
+    },
+    /** 查询员工统计列表 */
+    getList() {
+      this.loading = true;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      }else {
+        this.queryParams.userIds = [];
+      }
+
+      listEmployeeStats(this.queryParams).then(response => {
+        this.statsList = response.data.list;
+        this.total = response.data.total;
+
+      }).finally(()=>{
+        this.loading = false;
+      })
+    },
+    /** 获取员工列表 */
+    getEmployeeList() {
+      getEmployeeList().then(response => {
+        this.employeeList = response.data || [];
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      if(this.selectedCompanyList == null || this.selectedCompanyList.length <= 0) {
+        this.$message.error("公司和时间为必选!");
+        return;
+      }
+      this.queryParams.pageNum = 1;
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.selectedMultipleTasks = [];
+      this.selectedCompanyList = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      }else {
+        this.queryParams.userIds = [];
+      }
+      const queryParams = this.queryParams;
+
+      this.$confirm('是否确认导出当前查询数据?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportSellerData(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+      })
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.stats-card {
+  text-align: center;
+  margin-bottom: 16px;
+
+  .stats-number {
+    font-size: 24px;
+    font-weight: bold;
+    color: #409EFF;
+
+    &.online {
+      color: #67C23A;
+    }
+
+    &.offline {
+      color: #F56C6C;
+    }
+
+    &.rate {
+      color: #E6A23C;
+    }
+  }
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+::v-deep .el-table .el-table__header th {
+  background-color: #f5f7fa;
+}
+
+::v-deep .el-alert {
+  margin-bottom: 16px;
+}
+</style>

+ 431 - 0
src/views/statistics/section/today.vue

@@ -0,0 +1,431 @@
+<template>
+  <div class="app-container">
+    <el-form :model="queryParams" ref="queryForm" size="small" :inline="true" v-show="showSearch" label-width="68px">
+      <el-form-item label="公司名" prop="companyId">
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          placeholder="请选择销售"
+          :parentSelectable="true"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          @change="handleMultiChange"
+        ></select-tree>
+      </el-form-item>
+      <el-form-item label="销售" prop="nickName" v-if="queryParams.companyId">
+        <el-select v-model="queryParams.companyUserId" remote
+                   placeholder="请选择"
+                   filterable clearable
+                   style="width: 100%;"
+                   @keyup.enter.native="handleQuery"
+                   @change="handleCompanyUserId"
+        >
+          <el-option
+            v-for="dict in companyUserList"
+            :key="`${dict.nickName} - ${dict.userName}`"
+            :label="`${dict.nickName} - ${dict.userName}`"
+            :value="dict.userId">
+          </el-option>
+        </el-select>
+      </el-form-item>
+      <el-form-item label="时间范围" prop="dateRange">
+        <el-date-picker
+          v-model="dateRange"
+          type="daterange"
+          range-separator="至"
+          start-placeholder="开始日期"
+          end-placeholder="结束日期"
+          value-format="yyyy-MM-dd"
+          style="width: 240px"
+        />
+      </el-form-item>
+      <el-form-item label="SOP任务" prop="channel" label-width="70px">
+        <date-range
+          v-model="selectedMultipleTasks"
+          :raw-data="channelList"
+          placeholder="请选择多个任务"
+          :parentSelectable="true"
+          :multiple="true"
+          component-width="300px"
+          :max-display-tags="3"
+          :check-strictly="false"
+          :return-leaf-only="false"
+          :show-node-detail="true"
+          @change="handleMultiChange"
+          @dateRangeChange="dateRangeChange"
+        ></date-range>
+      </el-form-item>
+      <el-form-item label="包含未发课" label-width="120px">
+        <el-checkbox v-model="queryParams.includeSend0">
+        </el-checkbox>
+      </el-form-item>
+      <el-form-item>
+        <el-button type="primary" icon="el-icon-search" size="mini" @click="handleQuery">搜索</el-button>
+        <el-button icon="el-icon-refresh" size="mini" @click="resetQuery">重置</el-button>
+      </el-form-item>
+    </el-form>
+
+    <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          plain
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['system:stats:export']"
+        >导出</el-button>
+      </el-col>
+      <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
+    </el-row>
+
+    <!-- 警告提示 -->
+    <el-alert
+      title="待激活人数依赖进粉时的自动标记和员工手动标记是可变的,相应的目标人数和进线完播率受此影响;为了不受进粉期人数变化影响,第二天新进报名产生的观后数据不做统计;辅助工作台人数不一致可能是受进粉影响,也有可能是员工删除了好友或者取消了客户频道报名。"
+      type="warning"
+      :closable="false"
+      show-icon
+      class="mb8">
+    </el-alert>
+
+    <el-table border v-loading="loading" :data="statsList" @selection-change="handleSelectionChange" show-summary :summary-method="getSummaries">
+      <!-- 未上线部分 -->
+      <el-table-column prop="dataDate" label="日期" width="180" align="center"/>
+      <el-table-column prop="trainCampNum" label="营期人数"  align="center" />
+      <el-table-column prop="sendNum" label="发课数"  align="center" />
+      <el-table-column prop="notRegisteredNum" label="待看课"  align="center" />
+      <el-table-column prop="registeredNum" label="上线数"  align="center" />
+      <el-table-column prop="interruptNum" label="看课中断"  align="center" />
+      <el-table-column prop="completedNum" label="已完课"  align="center" />
+      <el-table-column prop="qwRepeatNum" label="企微重粉"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.qwRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="userRepeatNum" label="看课重粉(小程序)"  align="center" >
+        <template slot-scope="scope">
+          <span>{{scope.row.userRepeatNum || 0}}</span>
+        </template>
+      </el-table-column>
+      <el-table-column prop="regRate" label="上线率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.regRate === 'number'">
+            {{ (scope.row.regRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+
+      <el-table-column prop="finishedRate" label="完课率" align="center">
+        <template slot-scope="scope">
+          <span v-if="typeof scope.row.finishedRate === 'number'">
+            {{ (scope.row.finishedRate * 100).toFixed(2) }}%
+          </span>
+          <span v-else>
+            0.00%
+          </span>
+        </template>
+      </el-table-column>
+      <!--        汇总-->
+    </el-table>
+
+    <pagination
+      v-show="total>0"
+      :total="total"
+      :page.sync="queryParams.pageNum"
+      :limit.sync="queryParams.pageSize"
+      @pagination="getList"
+    />
+  </div>
+</template>
+
+<script>
+import {
+  listTodayList,
+  getEmployeeList,
+  getChannelList,
+  exportEmployeeStats,
+  getSOPTaskData,
+  getDeptData, exportSellerData, listPeriodList, exportTodayData
+} from '@/api/system/employeeStats'
+import {getCompanyList} from "@/api/company/company";
+import {getUserList} from "@/api/company/companyUser";
+import SelectTree from "@/components/TreeSelect/index.vue";
+import dateRange from '@/components/TreeSelect/dateRange.vue'
+
+export default {
+  name: "EmployeeStats",
+  components: {SelectTree,dateRange},
+  data() {
+    return {
+      // 遮罩层
+      loading: true,
+      companys:[],
+      selectedMultipleTasks: [],
+      selectedCompanyList: [],
+      deptList: [],
+      // 选中数组
+      ids: [],
+      // 非单个禁用
+      single: true,
+      // 非多个禁用
+      multiple: true,
+      // 显示搜索条件
+      showSearch: true,
+      // 总条数
+      total: 0,
+      // 员工统计表格数据
+      statsList: [],
+      // 员工列表
+      employeeList: [],
+      companyUserList: [],
+      // 频道列表
+      channelList: [],
+      // 日期范围
+      dateRange: [],
+      // 查询参数
+      queryParams: {
+        pageNum: 1,
+        pageSize: 10,
+        channel: null,
+        startDate: null,
+        endDate: null,
+        companyId: null,
+        periodList: [],
+        // 包含未发课数
+        includeSend0: false
+      },
+      // 总体统计
+      totalStats: {
+        totalEmployees: 0,
+        onlineEmployees: 0,
+        offlineEmployees: 0,
+        completionRate: '0.00'
+      }
+    };
+  },
+  created() {
+    this.getList();
+
+    // getCompanyList().then(response => {
+    //   this.companys = response.data;
+    // });
+
+    getSOPTaskData({}).then(response => {
+      this.channelList = response.data;
+    });
+
+    getDeptData().then(response => {
+      this.deptList = response.data;
+    })
+
+  },
+  methods: {
+    getSummaries(param) {
+      const { columns, data } = param;
+      const sums = [];
+
+      columns.forEach((column, index) => {
+        if (index === 0) {
+          sums[index] = '合计';
+          return;
+        }
+
+        const values = data.map(item => Number(item[column.property]));
+
+        if (['trainCampNum', 'sendNum', 'notRegisteredNum', 'registeredNum', 'interruptNum', 'completedNum', 'qwRepeatNum', 'userRepeatNum'].includes(column.property)) {
+          if (!values.every(value => isNaN(value))) {
+            sums[index] = values.reduce((prev, curr) => {
+              const value = Number(curr);
+              if (!isNaN(value)) {
+                return prev + curr;
+              } else {
+                return prev;
+              }
+            }, 0);
+          } else {
+            sums[index] = 'N/A';
+          }
+        }
+        else if (column.property === 'regRate') {
+          const totalRegistered = data.reduce((sum, item) => sum + (Number(item.registeredNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalRegistered / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else if (column.property === 'finishedRate') {
+          const totalCompleted = data.reduce((sum, item) => sum + (Number(item.completedNum) || 0), 0);
+          const totalSendNum = data.reduce((sum, item) => sum + (Number(item.sendNum) || 0), 0);
+
+          if (totalSendNum > 0) {
+            const rate = (totalCompleted / totalSendNum * 100).toFixed(2);
+            sums[index] = `${rate}%`;
+          } else {
+            sums[index] = '0.00%';
+          }
+        }
+        else {
+          sums[index] = '';
+        }
+      });
+
+      return sums;
+    },
+    handleCompanyUserId(val){
+      if(val == null || val === '') {
+        this.queryParams.companyUserId = null;
+        this.queryParams.userIds = [];
+      }
+    },
+    handleMultiChange(e){
+
+    },
+    dateRangeChange(e){
+      getSOPTaskData({
+        startDate: e[0],
+        endDate: e[1]
+      }).then(response => {
+        this.channelList = response.data;
+      });
+    },
+    handleSeller(){
+      if(this.queryParams.companyId != null) {
+        getUserList(this.queryParams.companyId).then(res=>{
+          if(res.code === 200) {
+            this.companyUserList = res.data
+          }
+        })
+      } else {
+        this.queryParams.companyUserId = null;
+      }
+    },
+    /** 查询员工统计列表 */
+    getList() {
+      this.loading = true;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      } else {
+        this.queryParams.userIds = [];
+      }
+
+      listTodayList(this.queryParams).then(response => {
+        this.statsList = response.data.list;
+        this.total = response.data.total;
+        this.loading = false;
+      });
+    },
+    /** 获取员工列表 */
+    getEmployeeList() {
+      getEmployeeList().then(response => {
+        this.employeeList = response.data || [];
+      });
+    },
+    /** 搜索按钮操作 */
+    handleQuery() {
+      if(this.selectedCompanyList == null || this.selectedCompanyList.length <= 0) {
+        this.$message.error("公司和时间为必选!");
+        return;
+      }
+      this.queryParams.pageNum = 1;
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      this.getList();
+    },
+    /** 重置按钮操作 */
+    resetQuery() {
+      this.dateRange = [];
+      this.selectedMultipleTasks = [];
+      this.selectedCompanyList = [];
+      this.resetForm("queryForm");
+      this.handleQuery();
+    },
+    // 多选框选中数据
+    handleSelectionChange(selection) {
+      this.ids = selection.map(item => item.id);
+      this.single = selection.length !== 1;
+      this.multiple = !selection.length;
+    },
+    /** 导出按钮操作 */
+    handleExport() {
+      this.queryParams.periodList = this.selectedMultipleTasks;
+      if (this.dateRange && this.dateRange.length === 2) {
+        this.queryParams.startDate = this.dateRange[0];
+        this.queryParams.endDate = this.dateRange[1];
+      } else {
+        this.queryParams.startDate = null;
+        this.queryParams.endDate = null;
+      }
+      if(this.selectedCompanyList != null && this.selectedCompanyList.length > 0) {
+        this.queryParams.userIds = this.selectedCompanyList;
+      }else {
+        this.queryParams.userIds = [];
+      }
+      const queryParams = this.queryParams;
+
+      this.$confirm('是否确认导出当前查询数据?', "警告", {
+        confirmButtonText: "确定",
+        cancelButtonText: "取消",
+        type: "warning"
+      }).then(function() {
+        return exportTodayData(queryParams);
+      }).then(response => {
+        this.download(response.msg);
+      })
+    }
+  }
+};
+</script>
+
+<style lang="scss" scoped>
+.stats-card {
+  text-align: center;
+  margin-bottom: 16px;
+
+  .stats-number {
+    font-size: 24px;
+    font-weight: bold;
+    color: #409EFF;
+
+    &.online {
+      color: #67C23A;
+    }
+
+    &.offline {
+      color: #F56C6C;
+    }
+
+    &.rate {
+      color: #E6A23C;
+    }
+  }
+}
+
+.mb8 {
+  margin-bottom: 8px;
+}
+
+::v-deep .el-table .el-table__header th {
+  background-color: #f5f7fa;
+}
+
+::v-deep .el-alert {
+  margin-bottom: 16px;
+}
+</style>

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