Browse Source

完播、频道、每日统计

xdd 1 week ago
parent
commit
3b5bb5a487

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

@@ -65,3 +65,11 @@ export function getSOPTaskData(param){
     method: 'get'
   })
 }
+
+// 获取部门数据
+export function getDeptData(param){
+  return request({
+    url: '/stats/getDeptData',
+    method: 'get'
+  })
+}

+ 118 - 94
src/components/TreeSelect/index.vue

@@ -37,8 +37,8 @@
           >
             <span class="custom-tree-node" slot-scope="{ node, data }">
               <span>{{ node.label }}</span>
-              <span v-if="!data.isParent && showNodeDetail" class="node-detail-text">
-                (ID: {{ data.originalData.qwUserId}} {{ data.originalData.taskExecDate }})
+              <span v-if="isLeafNode(data) && showNodeDetail" class="node-detail-text">
+                (ID: {{ data.originalData.qwUserId || data.originalData.id }} {{ data.originalData.taskExecDate || '' }})
               </span>
             </span>
           </el-tree>
@@ -80,17 +80,17 @@
           </template>
           <span v-else-if="!multiple && currentSelectedNodes.length > 0" class="single-value-display">
             {{ currentSelectedNodes[0][treeProps.label] }}
-            </span>
+          </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>
+          <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>
@@ -167,7 +167,7 @@ export default {
     },
     returnLeafOnly: {
       type: Boolean,
-      default: false
+      default: true
     }
   },
   data() {
@@ -207,23 +207,15 @@ export default {
       immediate: true,
       handler(newData) {
         this.processRawData(newData);
-        // 过滤掉父节点,只保留子节点
-        const leafOnlyKeys = this.value.filter(key => {
-          const node = this.nodeMap[key];
-          return node && !node.isParent;
-        });
+        const leafOnlyKeys = this.getLeafOnlyKeys(this.value);
         this.updateCurrentSelectedNodesFromKeys(leafOnlyKeys, true);
       }
     },
     value: {
       handler(newVal) {
-        // 过滤掉父节点,只保留子节点
-        const leafOnlyKeys = newVal.filter(key => {
-          const node = this.nodeMap[key];
-          return node && !node.isParent;
-        });
-
+        const leafOnlyKeys = this.getLeafOnlyKeys(newVal);
         const validKeysFromNewVal = leafOnlyKeys.filter(key => this.nodeMap[key] !== undefined);
+
         const sortedNewValStr = JSON.stringify([...leafOnlyKeys].sort());
         const sortedValidKeysFromNewValStr = JSON.stringify([...validKeysFromNewVal].sort());
         const currentInternalNodeKeys = this.currentSelectedNodes.map(n => n[this.nodeKey]);
@@ -263,52 +255,68 @@ export default {
     }
   },
   mounted() {
-    // 过滤掉父节点,只保留子节点
-    const leafOnlyKeys = this.value.filter(key => {
-      const node = this.nodeMap[key];
-      return node && !node.isParent;
-    });
+    const leafOnlyKeys = this.getLeafOnlyKeys(this.value);
     this.updateCurrentSelectedNodesFromKeys(leafOnlyKeys, true);
   },
   methods: {
+    /**
+     * 处理原始数据,转换成树形结构
+     */
     processRawData(data) {
       if (!data || !Array.isArray(data)) {
         this.processedTreeData = [];
         this.nodeMap = {};
         return;
       }
-      const newMap = {};
-      const mapNode = (node) => {
-        newMap[node[this.nodeKey]] = node;
-        if (node[this.treeProps.children] && node[this.treeProps.children].length) {
-          node[this.treeProps.children].forEach(mapNode);
-        }
-      };
 
-      this.processedTreeData = data.map(mainTask => {
-        const children = (mainTask.taskDetailList || []).map(detail => ({
-          [this.nodeKey]: detail.taskId,
-          [this.treeProps.label]: `${detail.qwUserId || '未命名子任务'}`,
-          isParent: false,
-          [this.treeProps.isLeaf]: true,
-          disabled: false, // 子节点可以选择
-          originalData: { ...detail, parentTaskId: mainTask.taskId }
-        }));
-
-        const parentNode = {
-          [this.nodeKey]: mainTask.taskId,
-          [this.treeProps.label]: mainTask.taskName,
-          isParent: true,
-          [this.treeProps.isLeaf]: children.length === 0,
-          disabled: true, // 父节点不可选择
-          [this.treeProps.children]: children,
-          originalData: { taskId: mainTask.taskId, taskName: mainTask.taskName }
+      this.nodeMap = {};
+
+      // 递归处理树节点
+      const processNode = (node, level = 0) => {
+        const children = node[this.treeProps.children] || [];
+        const hasChildren = children && children.length > 0;
+
+        const processedNode = {
+          [this.nodeKey]: node[this.nodeKey],
+          [this.treeProps.label]: node[this.treeProps.label] || `节点-${node[this.nodeKey]}`,
+          [this.treeProps.isLeaf]: !hasChildren,
+          level: level,
+          disabled: hasChildren, // 有子节点的节点不可选择(非叶子节点)
+          originalData: { ...node },
+          [this.treeProps.children]: hasChildren ? children.map(child => processNode(child, level + 1)) : undefined
         };
-        mapNode(parentNode); // Add parent and its children to map
-        return parentNode;
+
+        // 添加到节点映射
+        this.nodeMap[node[this.nodeKey]] = processedNode;
+
+        return processedNode;
+      };
+
+      this.processedTreeData = data.map(node => processNode(node, 0));
+    },
+
+    /**
+     * 判断是否为叶子节点
+     */
+    isLeafNode(node) {
+      return !node[this.treeProps.children] || node[this.treeProps.children].length === 0;
+    },
+
+    /**
+     * 过滤出叶子节点的keys
+     */
+    getLeafOnlyKeys(keys) {
+      if (!keys || !Array.isArray(keys)) return [];
+
+      return keys.filter(key => {
+        const node = this.nodeMap[key];
+        return node && this.isLeafNode(node);
       });
-      this.nodeMap = newMap;
     },
+
+    /**
+     * 根据keys更新当前选中的节点
+     */
     updateCurrentSelectedNodesFromKeys(keys, updateTreeVisualState = false) {
       const newSelectedNodes = [];
       const actualFoundKeys = [];
@@ -316,8 +324,8 @@ export default {
       if (keys && Array.isArray(keys)) {
         keys.forEach(key => {
           const node = this.nodeMap[key];
-          // 只添加子节点到选中列表
-          if (node && !node.isParent) {
+          // 只添加子节点到选中列表
+          if (node && this.isLeafNode(node)) {
             newSelectedNodes.push(node);
             actualFoundKeys.push(key);
           }
@@ -330,13 +338,21 @@ export default {
         this.$refs.taskTree.setCheckedKeys([...actualFoundKeys]);
       }
     },
+
+    /**
+     * 过滤节点方法
+     */
     filterNode(value, data) {
       if (!value) return true;
       return data[this.treeProps.label].toLowerCase().indexOf(value.toLowerCase()) !== -1;
     },
+
+    /**
+     * 处理树节点选择
+     */
     handleTreeCheck(nodeData, checkStatus) {
-      // 如果是父节点,不允许选择
-      if (nodeData.isParent) {
+      // 如果是非叶子节点,不允许选择
+      if (!this.isLeafNode(nodeData)) {
         // 恢复之前的选中状态
         this.$refs.taskTree.setCheckedKeys(this.tempCheckedKeys);
         return;
@@ -346,21 +362,25 @@ export default {
         const isNodeChecked = checkStatus.checkedKeys.includes(nodeData[this.nodeKey]);
         if (isNodeChecked) {
           this.tempCheckedKeys = [nodeData[this.nodeKey]];
-          this.$refs.taskTree.setCheckedKeys([nodeData[this.nodeKey]]); // Visually enforce single check
+          this.$refs.taskTree.setCheckedKeys([nodeData[this.nodeKey]]);
         } else {
           this.tempCheckedKeys = [];
         }
       } else {
-        // 只保留子节点的选中状态
+        // 只保留子节点的选中状态
         const leafKeys = checkStatus.checkedKeys.filter(key => {
           const node = this.nodeMap[key];
-          return node && !node.isParent;
+          return node && this.isLeafNode(node);
         });
         this.tempCheckedKeys = leafKeys;
-        // 更新树的选中状态,确保只显示子节点被选中
+        // 更新树的选中状态,确保只显示子节点被选中
         this.$refs.taskTree.setCheckedKeys(leafKeys);
       }
     },
+
+    /**
+     * 确认选择
+     */
     handleConfirm() {
       let finalSelectedKeys = [];
       let finalSelectedNodes = [];
@@ -368,16 +388,13 @@ export default {
       if (this.$refs.taskTree) {
         // 获取所有选中的节点
         const allCheckedNodes = this.$refs.taskTree.getCheckedNodes();
-        // 只保留子节点(叶子节点
-        finalSelectedNodes = allCheckedNodes.filter(node => !node.isParent);
+        // 只保留叶子节点
+        finalSelectedNodes = allCheckedNodes.filter(node => this.isLeafNode(node));
         finalSelectedKeys = finalSelectedNodes.map(node => node[this.nodeKey]);
 
-        if(!this.multiple) {
-          // 单选模式下,确保只选择子节点
-          const leafKeys = this.tempCheckedKeys.filter(key => {
-            const node = this.nodeMap[key];
-            return node && !node.isParent;
-          });
+        if (!this.multiple) {
+          // 单选模式下,确保只选择叶子节点
+          const leafKeys = this.getLeafOnlyKeys(this.tempCheckedKeys);
           finalSelectedKeys = leafKeys;
           finalSelectedNodes = finalSelectedKeys.map(key => this.nodeMap[key]).filter(Boolean);
         }
@@ -388,12 +405,20 @@ export default {
       this.updateCurrentSelectedNodesFromKeys(finalSelectedKeys, true);
       this.popoverVisible = false;
     },
+
+    /**
+     * 弹窗内清空选择
+     */
     handleClearInPopover() {
       this.tempCheckedKeys = [];
       if (this.$refs.taskTree) {
         this.$refs.taskTree.setCheckedKeys([]);
       }
     },
+
+    /**
+     * 触发器清空选择
+     */
     handleClearOnTrigger() {
       if (this.disabled) return;
       this.tempCheckedKeys = [];
@@ -405,6 +430,10 @@ export default {
       }
       this.popoverVisible = false;
     },
+
+    /**
+     * 移除标签
+     */
     removeTag(nodeToRemove) {
       if (this.disabled) return;
       const newKeys = this.value.filter(key => key !== nodeToRemove[this.nodeKey]);
@@ -418,6 +447,10 @@ export default {
         this.$refs.taskTree.setCheckedKeys(newKeys);
       }
     },
+
+    /**
+     * 弹窗显示时的处理
+     */
     onPopoverShow() {
       if (this.$refs.popover && this.$refs.popover.$refs.reference) {
         this.triggerElRect = this.$refs.popover.$refs.reference.getBoundingClientRect();
@@ -450,16 +483,15 @@ export default {
   border: 1px solid #dcdfe6;
   box-sizing: border-box;
   color: #606266;
-  display: flex; /* Use flex for alignment */
-  align-items: center; /* Vertically align items */
-  min-height: 32px; /* Element UI default input height (small) or 40px (default) */
-  /* Adjust line-height or padding if min-height is larger */
-  padding: 0 30px 0 10px; /* Space for text/tags and icons */
+  display: flex;
+  align-items: center;
+  min-height: 32px;
+  padding: 0 30px 0 10px;
   position: relative;
   transition: border-color .2s cubic-bezier(.645,.045,.355,1);
   width: 100%;
   cursor: pointer;
-  overflow: hidden; /* Hide overflow from tags */
+  overflow: hidden;
 }
 .select-trigger-wrapper.is-disabled {
   background-color: #f5f7fa;
@@ -483,19 +515,13 @@ export default {
 }
 
 .tags-or-value-container {
-  flex-grow: 1; /* Allow this container to take available space */
+  flex-grow: 1;
   display: flex;
-  flex-wrap: wrap; /* For multiple tags */
-  gap: 5px; /* Space between tags */
+  flex-wrap: wrap;
+  gap: 5px;
   align-items: center;
-  overflow: hidden; /* Prevent content from pushing icons */
-  padding: 2px 0; /* Small padding for tags not to touch border */
-}
-
-.trigger-tag {
-  /* margin-right: 5px; /* Use gap if supported, otherwise this */
-  /* margin-bottom: 2px;
-  margin-top: 2px; */
+  overflow: hidden;
+  padding: 2px 0;
 }
 
 .single-value-display {
@@ -503,7 +529,7 @@ export default {
   text-overflow: ellipsis;
   white-space: nowrap;
   color: #606266;
-  line-height: 30px; /* Adjust if trigger height changes */
+  line-height: 30px;
 }
 
 .placeholder-text {
@@ -511,7 +537,7 @@ export default {
   overflow: hidden;
   text-overflow: ellipsis;
   white-space: nowrap;
-  line-height: 30px; /* Adjust if trigger height changes */
+  line-height: 30px;
 }
 
 .icons-container {
@@ -530,7 +556,6 @@ export default {
   font-size: 14px;
 }
 .select-trigger-wrapper:hover .clear-icon {
-  /* Only show if showClearIcon is true (handled by v-if) */
   display: inline-block;
 }
 .icons-container .arrow-icon {
@@ -550,15 +575,14 @@ export default {
   margin-bottom: 8px;
 }
 .tree-scrollbar {
-  /* height is set by prop */
   border: 1px solid #ebeef5;
   border-radius: 2px;
 }
-.el-scrollbar__wrap { /* Make sure scrollbar wrap doesn't add extra space */
+.el-scrollbar__wrap {
   overflow-x: hidden;
 }
 .task-tree-in-popover {
-  min-height: 50px; /* Prevent collapse if empty */
+  min-height: 50px;
 }
 .custom-tree-node {
   flex: 1;
@@ -585,6 +609,6 @@ export default {
 <style>
 /* Global style for popper to customize padding */
 .task-select-tree-popper.el-popover {
-  padding: 12px; /* Adjust padding of the popper itself */
+  padding: 12px;
 }
 </style>

+ 13 - 8
src/views/statistics/section/channel.vue

@@ -2,14 +2,17 @@
   <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">
-        <el-select filterable style="width: 220px" v-model="queryParams.companyId" @change="handleSeller" placeholder="请选择公司名" clearable size="small">
-          <el-option
-            v-for="item in companys"
-            :key="item.companyId"
-            :label="item.companyName"
-            :value="item.companyId"
-          />
-        </el-select>
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          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="nickName" v-if="queryParams.companyId">
         <el-select v-model="queryParams.companyUserId" remote
@@ -127,6 +130,8 @@ export default {
       loading: true,
       companys:[],
       selectedMultipleTasks: [],
+      selectedCompanyList: [],
+      deptList: [],
       // 选中数组
       ids: [],
       // 非单个禁用

+ 18 - 27
src/views/statistics/section/index.vue

@@ -1,31 +1,18 @@
 <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">
-        <el-select filterable style="width: 220px" v-model="queryParams.companyId" @change="handleSeller" placeholder="请选择公司名" clearable size="small">
-          <el-option
-            v-for="item in companys"
-            :key="item.companyId"
-            :label="item.companyName"
-            :value="item.companyId"
-          />
-        </el-select>
-      </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 label="公司" prop="companyId">
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          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
@@ -98,7 +85,6 @@
             <span>{{ scope.row.finishedRate }}%</span>
           </template>
         </el-table-column>
-<!--        汇总-->
     </el-table>
 
     <pagination
@@ -112,7 +98,7 @@
 </template>
 
 <script>
-import { listEmployeeStats, getEmployeeList, getChannelList, exportEmployeeStats,getSOPTaskData} from "@/api/system/employeeStats";
+import { listEmployeeStats, getEmployeeList, getChannelList, exportEmployeeStats,getSOPTaskData,getDeptData} from "@/api/system/employeeStats";
 import {getCompanyList} from "@/api/company/company";
 import {getUserList} from "@/api/company/companyUser";
 import SelectTree from "@/components/TreeSelect/index.vue";
@@ -126,6 +112,7 @@ export default {
       loading: true,
       companys:[],
       selectedMultipleTasks: [],
+      selectedCompanyList: [],
       // 选中数组
       ids: [],
       // 非单个禁用
@@ -145,6 +132,7 @@ export default {
       channelList: [],
       // 日期范围
       dateRange: [],
+      deptList: [],
       // 查询参数
       queryParams: {
         pageNum: 1,
@@ -175,6 +163,9 @@ export default {
       this.channelList = response.data;
     });
 
+    getDeptData().then(response => {
+      this.deptList = response.data;
+    })
   },
   methods: {
     handleCompanyUserId(val){

+ 26 - 10
src/views/statistics/section/today.vue

@@ -2,14 +2,17 @@
   <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">
-        <el-select filterable style="width: 220px" v-model="queryParams.companyId" @change="handleSeller" placeholder="请选择公司名" clearable size="small">
-          <el-option
-            v-for="item in companys"
-            :key="item.companyId"
-            :label="item.companyName"
-            :value="item.companyId"
-          />
-        </el-select>
+        <select-tree
+          v-model="selectedCompanyList"
+          :raw-data="deptList"
+          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="nickName" v-if="queryParams.companyId">
         <el-select v-model="queryParams.companyUserId" remote
@@ -40,7 +43,7 @@
       </el-form-item>
       <el-form-item label="SOP任务" prop="channel" label-width="70px">
         <select-tree
-          v-model="selectedMultipleTasks"
+          v-model="selectedCompanyList"
           :raw-data="channelList"
           placeholder="请选择多个任务"
           :multiple="true"
@@ -111,7 +114,14 @@
 </template>
 
 <script>
-import { listTodayList, getEmployeeList, getChannelList, exportEmployeeStats,getSOPTaskData} from "@/api/system/employeeStats";
+import {
+  listTodayList,
+  getEmployeeList,
+  getChannelList,
+  exportEmployeeStats,
+  getSOPTaskData,
+  getDeptData
+} from "@/api/system/employeeStats";
 import {getCompanyList} from "@/api/company/company";
 import {getUserList} from "@/api/company/companyUser";
 import SelectTree from "@/components/TreeSelect/index.vue";
@@ -125,6 +135,8 @@ export default {
       loading: true,
       companys:[],
       selectedMultipleTasks: [],
+      selectedCompanyList: [],
+      deptList: [],
       // 选中数组
       ids: [],
       // 非单个禁用
@@ -174,6 +186,10 @@ export default {
       this.channelList = response.data;
     });
 
+    getDeptData().then(response => {
+      this.deptList = response.data;
+    })
+
   },
   methods: {
     handleCompanyUserId(val){