|
@@ -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>
|