Преглед на файлове

修改树形节点样式优化

吴朋磊 преди 1 месец
родител
ревизия
c69f07d3de
променени са 2 файла, в които са добавени 206 реда и са изтрити 24 реда
  1. 154 23
      src/views/targetImprove/components/CommonTree.vue
  2. 52 1
      src/views/targetImprove/consolidate/index.vue

+ 154 - 23
src/views/targetImprove/components/CommonTree.vue

@@ -24,6 +24,7 @@
       :default-expanded-keys="defaultExpandedKeys"
       :current-node-key="currentNodeKey"
       :expand-on-click-node="expandOnClickNode"
+      :check-strictly="true"
       @check="handleNodeCheck"
     >
       <!--
@@ -180,7 +181,12 @@ export default {
   inject: ['openAiRobot'],//注入Ai机器人组件调用方法
   // ==================== 数据状态 ====================
   data() {
-    return {};
+    return {
+      // 用户直接勾选的节点 key 列表(不含级联自动勾选的祖先/后代)
+      manuallyCheckedKeys: [],
+      // 防止程序化设置勾选状态时递归触发 @check 事件
+      _isUpdating: false,
+    };
   },
   computed:{
     targetCommon()
@@ -210,10 +216,20 @@ export default {
     treeData: {
       handler(newVal) {
         if (!Array.isArray(newVal) || newVal.length === 0) return;
-        // 用 setTimeout 确保 el-tree 内部节点已完全渲染(比 $nextTick 更可靠)
+        // 用 setTimeout 确保 el-tree 内部节点已完全渲染
         clearTimeout(this._defaultSelectTimer);
         this._defaultSelectTimer = setTimeout(() => {
-          this.applyDefaultFirstNode(newVal);
+          // 如果已有手动勾选的节点,在新数据上重新应用状态
+          if (this.manuallyCheckedKeys.length > 0 && this.showCheckbox) {
+            this._isUpdating = true;
+            this.reapplyCheckStates();
+            this.$nextTick(() => {
+              this._isUpdating = false;
+            });
+          } else {
+            // 没有手动勾选节点时,执行默认选中第一个节点
+            this.applyDefaultFirstNode(newVal);
+          }
         }, 100);
       },
     },
@@ -222,14 +238,113 @@ export default {
   // ==================== 方法 ====================
   methods: {
     /**
-     * 树节点勾选事件(多选,showCheckbox=true 时生效)
-     * 将所有勾选节点的 key(即 nodeKey 字段值,如 knowledgeId)数组发给父组件
-     * 勾选父节点时,el-tree 默认级联勾选其全部子节点,故数组中包含父节点及全部子节点 key
+     * 树节点勾选事件(自定义级联逻辑)
+     *
+     * 核心规则:
+     * - 同级节点可多选,只传递用户直接勾选的节点 knowledgeId
+     * - 勾选某节点时:其所有祖先和后代自动勾选并置灰(禁止操作),只传递当前节点
+     * - 取消勾选时:其所有祖先和后代恢复可操作状态
+     * - 已被置灰(因上下级被勾选)的节点禁止勾选/取消
+     *
      * @param {Object} data        - 当前勾选/取消的节点数据
-     * @param {Object} checkedInfo - 勾选状态详情 { checkedNodes, checkedKeys, halfCheckedKeys, halfCheckedNodes }
+     * @param {Object} checkedInfo - 勾选状态详情
      */
     handleNodeCheck(data, checkedInfo) {
-      this.$emit("nodeCheck", checkedInfo.checkedKeys);
+      // 程序化设置状态时跳过,防止递归
+      if (this._isUpdating) return;
+
+      const treeRef = this.$refs.commonTree;
+      if (!treeRef) return;
+
+      const key = data[this.nodeKey];
+      const isChecked = checkedInfo.checkedKeys.includes(key);
+
+      if (isChecked) {
+        // 用户勾选了该节点 → 加入手动勾选列表
+        if (!this.manuallyCheckedKeys.includes(key)) {
+          this.manuallyCheckedKeys.push(key);
+        }
+      } else {
+        // 用户取消勾选 → 从手动勾选列表移除
+        const index = this.manuallyCheckedKeys.indexOf(key);
+        if (index > -1) {
+          this.manuallyCheckedKeys.splice(index, 1);
+        }
+      }
+
+      // 重新应用所有勾选/置灰状态
+      this._isUpdating = true;
+      this.reapplyCheckStates();
+      this.$nextTick(() => {
+        this._isUpdating = false;
+      });
+
+      // 只传递用户直接勾选的节点 key
+      this.$emit("nodeCheck", [...this.manuallyCheckedKeys]);
+    },
+
+    /**
+     * 重新应用所有勾选和置灰状态
+     * 策略:先全部清空,再根据 manuallyCheckedKeys 逐个应用
+     * 注意:node.disabled 是只读计算属性,需通过 this.$set(node.data, 'disabled', ...) 设置
+     */
+    reapplyCheckStates() {
+      const treeRef = this.$refs.commonTree;
+      if (!treeRef) return;
+
+      // 1. 清空所有节点的 checked 和 disabled 状态
+      this.clearAllCheckAndDisabled();
+
+      // 2. 遍历每个手动勾选的节点,设置其祖先和后代的勾选+置灰
+      this.manuallyCheckedKeys.forEach(key => {
+        const node = treeRef.getNode(key);
+        if (!node) return;
+
+        // 当前节点:勾选,不置灰(用户可取消勾选)
+        node.checked = true;
+        this.$set(node.data, 'disabled', false);
+
+        // 祖先节点:勾选 + 置灰,并展开(确保勾选节点可见)
+        let parent = node.parent;
+        while (parent && parent.level > 0) {
+          parent.checked = true;
+          this.$set(parent.data, 'disabled', true);
+          parent.expanded = true;
+          parent = parent.parent;
+        }
+
+        // 后代节点:勾选 + 置灰
+        const checkDescendants = (n) => {
+          if (n.childNodes && n.childNodes.length > 0) {
+            n.childNodes.forEach(child => {
+              child.checked = true;
+              this.$set(child.data, 'disabled', true);
+              checkDescendants(child);
+            });
+          }
+        };
+        checkDescendants(node);
+      });
+    },
+
+    /**
+     * 清空整棵树的 checked 和 disabled 状态
+     * 注意:node.disabled 是只读计算属性,需通过 this.$set(node.data, 'disabled', false) 清除
+     */
+    clearAllCheckAndDisabled() {
+      const treeRef = this.$refs.commonTree;
+      if (!treeRef || !treeRef.store || !treeRef.store.root) return;
+
+      const clearNode = (node) => {
+        node.checked = false;
+        if (node.data) {
+          this.$set(node.data, 'disabled', false);
+        }
+        if (node.childNodes && node.childNodes.length > 0) {
+          node.childNodes.forEach(child => clearNode(child));
+        }
+      };
+      clearNode(treeRef.store.root);
     },
 
     /**
@@ -260,22 +375,28 @@ export default {
 
     /**
      * 对外暴露方法:获取所有勾选节点的 key 数组
-     * @param {Boolean} leafOnly
-     * @returns {Array} 选中的节点 key 数组
+     * 新逻辑:只返回用户直接勾选的节点 key
+     * @param {Boolean} leafOnly - 保留兼容参数
+     * @returns {Array} 手动勾选的节点 key 数组
      */
     getCheckedKeys(leafOnly = false) {
-      if (!this.$refs.commonTree) return [];
-      return this.$refs.commonTree.getCheckedKeys(leafOnly);
+      return [...this.manuallyCheckedKeys];
     },
 
     /**
      * 对外暴露方法:通过 keys 设置哪些节点被选中
+     * 新逻辑:keys 即手动勾选的节点列表
      * @param {Array} keys       - 要选中的节点 key 数组
-     * @param {Boolean} leafOnly - 是否只设置叶子节点
+     * @param {Boolean} leafOnly - 是否只设置叶子节点(保留兼容,新逻辑中忽略)
      */
     setCheckedKeys(keys, leafOnly = false) {
       if (!this.$refs.commonTree) return;
-      this.$refs.commonTree.setCheckedKeys(keys, leafOnly);
+      this.manuallyCheckedKeys = Array.isArray(keys) ? [...keys] : [];
+      this._isUpdating = true;
+      this.reapplyCheckStates();
+      this.$nextTick(() => {
+        this._isUpdating = false;
+      });
     },
 
     /**
@@ -283,7 +404,12 @@ export default {
      */
     clearChecked() {
       if (!this.$refs.commonTree) return;
-      this.$refs.commonTree.setCheckedKeys([]);
+      this.manuallyCheckedKeys = [];
+      this._isUpdating = true;
+      this.clearAllCheckAndDisabled();
+      this.$nextTick(() => {
+        this._isUpdating = false;
+      });
     },
 
     /**
@@ -297,7 +423,7 @@ export default {
 
     /**
      * 默认勾选并展开第一个一级节点
-     * 直接通过 el-tree 内部 store 获取节点实例,避免 node-key 与数据字段不匹配导致的 key 查找失败
+     * 新逻辑:只勾选第一个节点本身(手动勾选),其祖先和后代自动勾选+置灰
      * @param {Array} data - 树形数据
      */
     applyDefaultFirstNode(data) {
@@ -305,7 +431,7 @@ export default {
       const treeRef = this.$refs.commonTree;
       if (!treeRef || !treeRef.store || !treeRef.store.root) return;
 
-      // 1. 获取第一个顶层节点实例(绕过 key 匹配)
+      // 1. 获取第一个顶层节点实例
       const rootNode = treeRef.store.root;
       const childNodes = rootNode.childNodes;
       if (!childNodes || childNodes.length === 0) return;
@@ -313,9 +439,14 @@ export default {
       const firstNodeInstance = childNodes[0];
       if (!firstNodeInstance) return;
 
-      // 2. 默认勾选第一个节点及其全部子节点(deep=true 级联勾选所有子节点)
+      // 2. 默认勾选第一个节点(手动勾选,只传该节点 key)
       if (this.showCheckbox) {
-        firstNodeInstance.setChecked(true, true);
+        this.manuallyCheckedKeys = [firstNodeInstance.key];
+        this._isUpdating = true;
+        this.reapplyCheckStates();
+        this.$nextTick(() => {
+          this._isUpdating = false;
+        });
       }
 
       // 3. 默认高亮选中第一个节点
@@ -332,11 +463,9 @@ export default {
       };
       expandAllChildren(firstNodeInstance);
 
-      // 5. 触发 nodeCheck 事件,将所有勾选节点的 key 数组发给父组件
-      //    仅发送一个数组参数(包含第一个节点及其所有子节点的 key)
+      // 5. 只传递手动勾选的节点 key(即第一个节点本身)
       if (this.showCheckbox) {
-        const checkedKeys = treeRef.getCheckedKeys();
-        this.$emit("nodeCheck", checkedKeys);
+        this.$emit("nodeCheck", [...this.manuallyCheckedKeys]);
       }
     },
   },
@@ -375,6 +504,8 @@ $btn_color: #4f3eff; // AI讲解按钮颜色  (用户要求)
   height: 100%;
   box-sizing: border-box;
   padding: 8px 0;
+  overflow-x: auto;
+  overflow-y: auto;
 
   /* ===================== 基础 ===================== */
   .tree_container,

+ 52 - 1
src/views/targetImprove/consolidate/index.vue

@@ -276,16 +276,67 @@ export default {
   width: 100%;
   display: flex;
   justify-content: space-between;
+  align-items: flex-start;
   height: calc(100% - 50px);
 
+  // 左栏:与右栏外框保持同一高度 calc(100% - 10px),内部再用 flex 分配
   .left_filter
   {
     width: 320px;
-    height: 100%;
+    height: calc(100% - 10px);
+    display: flex;
+    flex-direction: column;
+    overflow: hidden;
+  }
+
+  // 顶部筛选条
+  .filter_list
+  {
+    flex-shrink: 0;
+    background: #fff;
+    border-radius: 10px;
+    padding: 12px;
+    box-sizing: border-box;
+  }
+
+  // 顶部筛选条与知识点树之间的 10px 间距(不被 flex 压缩)
+  .page_jg_10
+  {
+    height: 10px;
+    flex-shrink: 0;
+  }
 
+  // 知识点树容器:flex:1 填满左栏剩余高度
+  // min-height:0 + overflow:hidden 保证内部 CommonTree 的 height:100% 能拿到真实高度,并在内部滚动
+  .filter_tree
+  {
+    flex: 1;
+    min-height: 0;
+    display: flex;
+    flex-direction: column;
+    background: #fff;
+    border-radius: 10px;
+    padding: 10px;
+    box-sizing: border-box;
+    overflow: hidden;
 
+    .tree_title
+    {
+      flex-shrink: 0;
+      margin-bottom: 10px;
+      font-weight: 500;
+      color: #333;
+    }
 
+    // 让 CommonTree 承接真正的剩余高度,超出时由它自己的 overflow 出现滚动条
+    ::v-deep .common_tree_wrapper
+    {
+      flex: 1;
+      min-height: 0;
+    }
   }
+
+  // 右栏:保持原有高度表达式(左栏已和它对齐)
   .right_list
   {
     width: calc(100% - 330px);