开篇之前特别想吐槽iview tree是真的难用,tui~~
问题
针对子节点禁用状态下,父节点勾选依旧可以操作子节点。翻阅了源码,发现实现逻辑有问题。
tree的勾选更新逻辑:勾选一个节点后向上节点和下节点传递更新的,此次修改涉及的源代码如下:
kotlin
// 向下更新
updateTreeDown(node, changes = {}) {
if (this.checkStrictly) return;
for (let key in changes) {
this.$set(node, key, changes[key]);
}
if (node[this.childrenKey]) {
node[this.childrenKey].forEach(child => {
this.updateTreeDown(child, changes);
});
}
},
// 向上更新
updateTreeUp(nodeKey){
const parentKey = this.flatState[nodeKey].parent;
if (typeof parentKey == 'undefined' || this.checkStrictly) return;
const node = this.flatState[nodeKey].node;
const parent = this.flatState[parentKey].node;
if (node.checked == parent.checked && node.indeterminate == parent.indeterminate) return; // no need to update upwards
if (node.checked == true) {
this.$set(parent, 'checked', parent[this.childrenKey].every(node => node.checked));
this.$set(parent, 'indeterminate', !parent.checked);
} else {
this.$set(parent, 'checked', false);
this.$set(parent, 'indeterminate', parent[this.childrenKey].some(node => node.checked || node.indeterminate));
}
this.updateTreeUp(parentKey);
},
// 勾选逻辑
handleCheck({ checked, nodeKey }) {
// 获取节点
const node = this.flatState[nodeKey].node;
this.$set(node, "checked", checked);
this.$set(node, "indeterminate", false);
this.updateTreeUp(nodeKey); // propagate up
this.updateTreeDown(node, { checked, indeterminate: false }); // reset `indeterminate` when going down
this.$emit("on-check-change", this.getCheckedNodes(), node);
},
可以发现 向下更新的时候压根没判断是否是disabled状态,直接就改变勾选了
vbnet
for (let key in changes) {
this.$set(node, key, changes[key]);
}
解决方案:在main.js里面直接修改源码,判断操作是checked 同时 是禁用状态 checked状态不变
kotlin
import ViewUI, { Tree } from "view-design";
// 向下更新数据
Tree.methods.updateTreeDown = function (node, changes = {}) {
// 是否父子关联
if (this.checkStrictly) return;
// ### 调整范围
// 循环操作的key
for (let key in changes) {
// after #6121
// 操作是checked 同时 是禁用状态 checked状态不变
if (key === "checked" && node.disabled) {
this.$set(node, key, node.checked);
} else {
// 否则 赋值传递的值
this.$set(node, key, changes[key]);
}
}
// ###
// 判断是否有子节点 继续上续操作
if (node[this.childrenKey]) {
node[this.childrenKey].forEach((child) => {
this.updateTreeDown(child, changes);
});
}
};
但是随后会引发两个问题:
1、iview tree 勾选逻辑是,勾选 -> 不勾选,半勾选 -> 不勾选
其他组件库 tree 勾选逻辑是,勾选 -> 不勾选,半勾选 -> 全选
需要调整组件库勾选逻辑,按照iview tree勾选逻辑,一旦有一个子节点 checked:true,disabled:true父节点完全无法交互了,因为半勾选 -> 不勾选,但是代码逻辑里面父节点勾选是不操作disabled:true的子节点
解决方案: 勾选逻辑改为,勾选 -> 不勾选,半勾选 -> 全选
kotlin
import ViewUI, { Tree } from "view-design";
// 勾选逻辑
Tree.methods.updateTreeDown =function ({ nodeKey }) {
// 获取节点
const node = this.flatState[nodeKey].node;
// ### 调整范围
let checked = false;
if (node.checked) {
checked = false;
} else if (node.indeterminate) {
checked = true;
} else {
checked = true;
}
// ###
this.$set(node, "checked", checked);
this.$set(node, "indeterminate", false);
this.updateTreeUp(nodeKey);
this.updateTreeDown(node, { checked, indeterminate: false });
this.$emit("on-check-change", this.getCheckedNodes(), node);
};
2、向上更新时出现问题:当有一个子节点checked:false,disabled:true,其他子节点全部都checked:true,disabled:false。这个时候事实上应该父节点是勾选状态,毕竟子节点禁用了就不应该参与勾选了。否则的话,如果父节点是半勾选状态,再次点击应该是 半勾选 -> 全选,但是由于上面的调整导致向下传递失败,无法到达勾选状态(在查看源码中发现,有些版本的源码是解决了这个问题的,根据实际情况调整)
解决方案: 将checked:true,disabled:false。状态的子节点不纳入父节点交互
kotlin
import ViewUI, { Tree } from "view-design";
// 向上更新
Tree.methods.updateTreeUp = function (nodeKey) {
// 获取父级key
const parentKey = this.flatState[nodeKey].parent;
// 判断是否继续向上更新
if (typeof parentKey == "undefined" || this.checkStrictly) return;
// 获取当前节点 和 父级节点
const node = this.flatState[nodeKey].node;
const parent = this.flatState[parentKey].node;
// 判断当前节点 勾选状态和 显示状态是否完全一致
if (
node.checked == parent.checked &&
node.indeterminate == parent.indeterminate
)
return; // ##无需向上更新
// 节点为勾选状态
// ### 调整范围
if (node.checked == true) {
// #6121
// 操作了一个子节点后判断所有子节点的状态后反向更新父级
this.$set(
parent,
"checked",
// 所有子节点都勾选 或者 子节点disabled都存在
parent[this.childrenKey].every(
(node) => node.checked || node.disabled !== undefined
)
);
// 因为子节点必然勾选一个 所以样式根据父节点勾选状态展示
this.$set(parent, "indeterminate", !parent.checked);
} else {
// 以为子节点必然有一个没勾选,父级必然不勾选
this.$set(parent, "checked", false);
this.$set(
parent,
"indeterminate",
// 所有子节点但凡有一个勾选或者半勾选就向上更新 半勾选状态
parent[this.childrenKey].some(
(node) => node.checked || node.indeterminate
)
);
}
// ###
// 继续向上更新
this.updateTreeUp(parentKey);
};
到此全局性的解决了这个问题
但是如果在已经稳定运行的项目中想解决这个问题,不一定能直接全局性修改,毕竟不知道其他模块的同事是否有其他调整
局部解决方案:
在局部解决的思路是一样的, this.$refs.tree.updateTreeUp 直接修改,但是在 handleCheck 方法中失效了。可恶 再次查看源码发现,代码中子组件向父组件传递是封装一个混入方法 this.dispatch
ini
// 向指定祖先节点传递
dispatch(componentName, eventName, params) {
let parent = this.$parent || this.$root; // 从父组件或根开始搜索
let name = parent.$options.name; // 获取当前父组件的名称
// 遍历父组件链
while (parent && (!name || name !== componentName)) {
parent = parent.$parent; // 向上移动到下一个父组件
if (parent) {
name = parent.$options.name; // 更新当前父组件的名称
}
}
// 找到目标组件后触发事件
if (parent) {
}
},
tree handleCheck 调用方式
kotlin
mounted () {
this.$on('on-check', this.handleCheck);
}
结合上面的代码 发现 子组件调用 dispatch ,拿到当前组件去实例找父组件,然后循环往上找,一直找到组件name是目标name的组件,然后在目标组件(也就是父组件)调用 emit,然后使用this.on()捕获 。
由于是 this.$on() 的方式截取自身的方法,所以需要改变一下策略(在看源码才知道 , emit,on 不只是父子组件可以用,本组件内也可以使用)
kotlin
// 先删除再装填方法
this.$refs.tree.$off("on-check", this.handleCheck);
const handleCheck = function ({ nodeKey }) {
// 获取节点
const node = this.flatState[nodeKey].node;
let checked = false;
if (node.checked) {
checked = false;
} else if (node.indeterminate) {
checked = true;
} else {
checked = true;
}
this.$set(node, "checked", checked);
this.$set(node, "indeterminate", false);
this.updateTreeUp(nodeKey);
this.updateTreeDown(node, { checked, indeterminate: false });
this.$emit("on-check-change", this.getCheckedNodes(), node);
};
this.$refs.tree.$on("on-check", handleCheck);
完整局部解决代码:
kotlin
this.$refs.tree.$off("on-check", this.handleCheck);
const handleCheck = function ({ nodeKey }) {
// 获取节点
const node = this.flatState[nodeKey].node;
let checked = false;
if (node.checked) {
checked = false;
} else if (node.indeterminate) {
checked = true;
} else {
checked = true;
}
this.$set(node, "checked", checked);
this.$set(node, "indeterminate", false);
this.updateTreeUp(nodeKey);
this.updateTreeDown(node, { checked, indeterminate: false });
this.$emit("on-check-change", this.getCheckedNodes(), node);
};
this.$refs.tree.$on("on-check", handleCheck);
// 向上更新树
this.$refs.tree.updateTreeUp = function (nodeKey) {
// 获取父级key
const parentKey = this.flatState[nodeKey].parent;
// 判断是否继续向上更新
if (typeof parentKey == "undefined" || this.checkStrictly) return;
// 获取当前节点 和 父级节点
const node = this.flatState[nodeKey].node;
const parent = this.flatState[parentKey].node;
// 判断当前节点 勾选状态和 显示状态是否完全一致
if (
node.checked == parent.checked &&
node.indeterminate == parent.indeterminate
)
return; // ##无需向上更新
// 节点为勾选状态
if (node.checked == true) {
// #6121
// 操作了一个子节点后判断所有子节点的状态后反向更新父级
this.$set(
parent,
"checked",
// 所有子节点都勾选 或者 子节点disabled都存在
parent[this.childrenKey].every(
(node) => node.checked || node.disabled !== undefined
)
);
// 因为子节点必然勾选一个 所以样式根据父节点勾选状态展示
this.$set(parent, "indeterminate", !parent.checked);
} else {
// 以为子节点必然有一个没勾选,父级必然不勾选
this.$set(parent, "checked", false);
this.$set(
parent,
"indeterminate",
// 所有子节点但凡有一个勾选或者半勾选就向上更新 半勾选状态
parent[this.childrenKey].some(
(node) => node.checked || node.indeterminate
)
);
}
// 继续向上更新
this.updateTreeUp(parentKey);
};
// 向下更新数据
this.$refs.tree.updateTreeDown = function (node, changes = {}) {
// 是否父子关联
if (this.checkStrictly) return;
// 循环操作的key
for (let key in changes) {
// after #6121
// 操作是checked 同时 是禁用状态 checked状态不变
if (key === "checked" && node.disabled) {
this.$set(node, key, node.checked);
} else {
// 否则 赋值传递的值
this.$set(node, key, changes[key]);
}
}
// 判断是否有子节点 继续上续操作
if (node[this.childrenKey]) {
node[this.childrenKey].forEach((child) => {
this.updateTreeDown(child, changes);
});
}
};
完结撒花~~~~