Przeglądaj źródła

Merge remote-tracking branch 'origin/master'

yuhongqi 6 dni temu
rodzic
commit
1f27b206c3

+ 1 - 2
src/components/TreeSelect/index.vue

@@ -73,7 +73,7 @@
                   :value="checkedKeysSet.has(item.key)"
                   :disabled="checkboxDisabled(item)"
                   @change="handleCheckboxChange(item, $event)"
-                  @click.native.stop
+                  @click.stop
                   class="node-checkbox"
                 ></el-checkbox>
 
@@ -569,7 +569,6 @@ export default {
       }
     },
     handleCheckboxChange(node, checked) {
-
       if (this.checkboxDisabled(node)) return;
 
       const newCheckedKeys = new Set(this.checkedKeysSet);

+ 1050 - 0
src/components/TreeSelect/indexOld.vue

@@ -0,0 +1,1050 @@
+<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.native.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="isAllCurrentResultsSelected ? 'warning' : 'info'"
+            @click="handleSelectAllToggle"
+          >
+            {{ isAllCurrentResultsSelected ? '取消全选' : '全选' }}
+          </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
+      );
+    },
+    // 计算当前搜索结果中的所有叶子节点
+    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));
+    }
+  },
+  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;
+    },
+    // 新增:处理全选/取消全选功能
+    handleSelectAllToggle() {
+      if (this.disabled) return;
+
+      const leafNodes = this.currentResultLeafNodes;
+      if (leafNodes.length === 0) return;
+
+      const newCheckedKeys = new Set(this.checkedKeysSet);
+
+      if (this.isAllCurrentResultsSelected) {
+        // 取消全选:移除当前搜索结果中的所有叶子节点
+        leafNodes.forEach(node => {
+          if (!this.checkStrictly) {
+            // 如果不是严格模式,需要处理父子节点关系
+            this.handleCheckboxChange(node, false);
+          } else {
+            // 严格模式下直接移除
+            newCheckedKeys.delete(node.key);
+          }
+        });
+
+        if (this.checkStrictly) {
+          this.checkedKeysSet = newCheckedKeys;
+          this.emitChange();
+        }
+      } else {
+        // 全选:选择当前搜索结果中的所有叶子节点
+        if (!this.multiple) {
+          // 单选模式下,只选择第一个叶子节点
+          const firstLeaf = leafNodes[0];
+          if (firstLeaf) {
+            this.handleCheckboxChange(firstLeaf, true);
+          }
+        } else {
+          // 多选模式下,选择所有叶子节点
+          leafNodes.forEach(node => {
+            if (!this.checkStrictly) {
+              // 如果不是严格模式,需要处理父子节点关系
+              if (!this.checkedKeysSet.has(node.key)) {
+                this.handleCheckboxChange(node, true);
+              }
+            } else {
+              // 严格模式下直接添加
+              newCheckedKeys.add(node.key);
+            }
+          });
+
+          if (this.checkStrictly) {
+            this.checkedKeysSet = newCheckedKeys;
+            this.emitChange();
+          }
+        }
+      }
+    },
+    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>

+ 2 - 1
src/views/components/index/statisticsDashboard.vue

@@ -85,7 +85,7 @@
                 <count-to :start-val="0" :end-val="balance" :duration="3600" class="card-panel-num" />
               </div>
             </div>
-            <div class="property-card propertyline">
+            <div class="property-card propertyline" v-if="projectFrom === 'sczy'">
               <div class="property-title">
                 <i class="el-icon-money"></i>
                 润天余额(元)
@@ -593,6 +593,7 @@ const viewCharOption = {
     bottom: '3%',
     containLabel: true
   },
+  projectFrom:process.env.VUE_APP_PROJECT_FROM,
   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']

+ 15 - 0
src/views/fastGpt/fastgptEventLogTotal/index.vue

@@ -55,6 +55,15 @@
     </el-form>
 
     <el-row :gutter="10" class="mb8">
+      <el-col :span="1.5">
+        <el-button
+          type="warning"
+          icon="el-icon-download"
+          size="mini"
+          @click="handleExport"
+          v-hasPermi="['fastGpt:fastgptEventLogTotal:export']"
+        >导出</el-button>
+      </el-col>
       <right-toolbar :showSearch.sync="showSearch" @queryTable="getList"></right-toolbar>
     </el-row>
 
@@ -77,6 +86,12 @@
     </span>
         </template>
       </el-table-column>
+      <!-- 新增金额列 -->
+      <el-table-column label="金额" align="center">
+        <template slot-scope="scope">
+          <span>{{ scope.row.typeCountMap && scope.row.typeCountMap['11'] ? (scope.row.typeCountMap['11'] / 100000).toFixed(4) : '0.0000' }}</span>
+        </template>
+      </el-table-column>
     </el-table>
 
     <pagination