Files
frontend-app/uni_modules/uview-plus/components/u-tree/u-tree.vue
T
2026-07-23 14:31:06 +08:00

558 lines
14 KiB
Vue

<template>
<view class="u-tree">
<view
v-for="item in visibleNodes"
:key="item.key"
class="u-tree-node"
:class="getNodeClass(item)"
>
<view
class="u-tree-node__content"
:style="getNodeContentStyle(item)"
@tap="handleNodeClick(item)"
>
<view class="u-tree-node__switcher" @tap.stop="handleExpandClick(item)">
<up-icon
v-if="item.hasChildren"
:name="item.expanded ? collapseIcon : expandIcon"
:size="iconSize"
:color="switcherColor"
/>
</view>
<up-checkbox
v-if="showCheckbox"
class="u-tree-node__checkbox"
usedAlone
:size="checkboxSize"
:checked="item.checked"
:disabled="item.disabled"
@change="handleCheckboxChange(item, $event)"
/>
<view class="u-tree-node__label">
<!-- #ifndef MP-WEIXIN -->
<slot
:node="item.node"
:data="item.node"
:level="item.level + 1"
:expanded="item.expanded"
:checked="item.checked"
:indeterminate="item.indeterminate"
:disabled="item.disabled"
>
<text class="u-tree-node__text">{{ getNodeLabel(item.node) }}</text>
</slot>
<!-- #endif -->
<!-- #ifdef MP-WEIXIN -->
<text class="u-tree-node__text">{{ getNodeLabel(item.node) }}</text>
<!-- #endif -->
</view>
</view>
</view>
</view>
</template>
<script>
import { mpMixin } from '../../libs/mixin/mpMixin';
import { mixin } from '../../libs/mixin/mixin';
export default {
name: 'u-tree',
mixins: [mpMixin, mixin],
props: {
data: {
type: Array,
default: () => []
},
props: {
type: Object,
default: () => ({
label: 'label',
children: 'children',
nodeKey: 'id',
disabled: 'disabled'
})
},
nodeKey: {
type: String,
default: ''
},
showCheckbox: {
type: Boolean,
default: false
},
defaultExpandAll: {
type: Boolean,
default: false
},
defaultExpandedKeys: {
type: Array,
default: () => []
},
defaultCheckedKeys: {
type: Array,
default: () => []
},
expandOnClickNode: {
type: Boolean,
default: true
},
checkOnClickNode: {
type: Boolean,
default: false
},
checkStrictly: {
type: Boolean,
default: false
},
accordion: {
type: Boolean,
default: false
},
highlightCurrent: {
type: Boolean,
default: false
},
currentNodeKey: {
type: [String, Number],
default: ''
},
indent: {
type: [String, Number],
default: 32
},
iconSize: {
type: [String, Number],
default: 14
},
checkboxSize: {
type: [String, Number],
default: 16
},
expandIcon: {
type: String,
default: 'play-right-fill'
},
collapseIcon: {
type: String,
default: 'arrow-down-fill'
}
},
data() {
return {
treeData: [],
currentKey: '',
nodeMap: {},
privateKeySeed: 0
};
},
computed: {
treeProps() {
return Object.assign({
label: 'label',
children: 'children',
nodeKey: 'id',
disabled: 'disabled'
}, this.props || {});
},
labelKey() {
return this.treeProps.label;
},
childrenKey() {
return this.treeProps.children;
},
disabledKey() {
return this.treeProps.disabled;
},
keyField() {
return this.nodeKey || this.treeProps.nodeKey || 'id';
},
visibleNodes() {
const result = [];
this.collectVisibleNodes(this.treeData, 0, result);
return result;
},
switcherColor() {
return this.upThemeVar('--up-content-color', '#606266');
}
},
watch: {
data: {
handler() {
this.initTree();
},
deep: true,
immediate: true
},
props: {
handler() {
this.initTree();
},
deep: true
},
nodeKey() {
this.initTree();
},
defaultExpandAll() {
this.initTree();
},
defaultExpandedKeys() {
this.initTree();
},
defaultCheckedKeys() {
this.initTree();
},
checkStrictly() {
this.initTree();
},
currentNodeKey: {
handler(value) {
this.currentKey = value;
},
immediate: true
}
},
emits: [
'node-click',
'check-change',
'check',
'node-expand',
'node-collapse',
'current-change'
],
methods: {
initTree() {
this.privateKeySeed = 0;
this.nodeMap = {};
this.treeData = this.cloneNodes(this.data || [], null, 0);
if (!this.checkStrictly) {
this.syncParentChecked(this.treeData);
}
},
cloneNodes(nodes, parent, level) {
const list = [];
nodes.forEach((node, index) => {
const clone = Object.assign({}, node);
const key = this.resolveNodeKey(clone, parent, index);
const children = Array.isArray(node[this.childrenKey]) ? node[this.childrenKey] : [];
const expanded = this.defaultExpandAll || this.includesKey(this.defaultExpandedKeys, key) || clone.expanded === true;
const checked = this.includesKey(this.defaultCheckedKeys, key) || clone.checked === true;
clone.__uTreeKey = key;
clone.expanded = expanded;
clone.checked = checked;
clone.indeterminate = false;
clone[this.childrenKey] = this.cloneNodes(children, clone, level + 1);
if (checked && !this.checkStrictly) {
this.setChildrenChecked(clone, true);
}
this.nodeMap[key] = {
node: clone,
parent,
level
};
list.push(clone);
});
return list;
},
resolveNodeKey(node, parent, index) {
const rawKey = node[this.keyField];
if (rawKey !== undefined && rawKey !== null && rawKey !== '') {
return rawKey;
}
const parentKey = parent ? parent.__uTreeKey : 'root';
this.privateKeySeed += 1;
return parentKey + '-' + index + '-' + this.privateKeySeed;
},
collectVisibleNodes(nodes, level, result) {
nodes.forEach(node => {
const children = this.getChildren(node);
const key = this.getNodeKey(node);
result.push({
key,
node,
level,
hasChildren: children.length > 0,
expanded: node.expanded === true,
checked: node.checked === true,
indeterminate: node.indeterminate === true,
disabled: this.isNodeDisabled(node)
});
if (children.length > 0 && node.expanded === true) {
this.collectVisibleNodes(children, level + 1, result);
}
});
},
getChildren(node) {
const children = node ? node[this.childrenKey] : [];
return Array.isArray(children) ? children : [];
},
getNodeKey(node) {
return node ? node.__uTreeKey : '';
},
getNodeLabel(node) {
const label = node ? node[this.labelKey] : '';
return label === undefined || label === null ? '' : label;
},
isNodeDisabled(node) {
return !!(node && node[this.disabledKey]);
},
includesKey(keys, key) {
return Array.isArray(keys) && keys.indexOf(key) !== -1;
},
getNodeClass(item) {
const classes = [];
if (this.highlightCurrent && item.key === this.currentKey) {
classes.push('u-tree-node--current');
}
if (item.disabled) {
classes.push('u-tree-node--disabled');
}
return classes.join(' ');
},
getNodeContentStyle(item) {
return {
paddingLeft: this.getIndentValue(item.level)
};
},
getIndentValue(level) {
const indent = String(this.indent);
const matched = indent.match(/^(\d+(?:\.\d+)?)(.*)$/);
if (!matched) {
return indent;
}
const unit = matched[2] || 'rpx';
return (Number(matched[1]) * level) + unit;
},
handleNodeClick(item) {
const node = item.node;
const oldCurrentNode = this.getCurrentNode();
this.currentKey = item.key;
if (this.expandOnClickNode && item.hasChildren) {
this.toggleExpand(item);
}
if (this.checkOnClickNode && this.showCheckbox && !item.disabled) {
this.setNodeChecked(node, !node.checked, true);
this.emitCheck(node);
}
this.$emit('node-click', node);
if (oldCurrentNode !== node) {
this.$emit('current-change', node, oldCurrentNode);
}
},
handleExpandClick(item) {
if (!item.hasChildren) {
return;
}
this.toggleExpand(item);
},
toggleExpand(item) {
const node = item.node;
const nextExpanded = node.expanded !== true;
if (this.accordion && nextExpanded) {
this.collapseSiblingNodes(node);
}
node.expanded = nextExpanded;
this.$emit(nextExpanded ? 'node-expand' : 'node-collapse', node);
},
collapseSiblingNodes(node) {
const parent = this.getParentNode(node);
const siblings = parent ? this.getChildren(parent) : this.treeData;
siblings.forEach(sibling => {
if (sibling !== node) {
sibling.expanded = false;
}
});
},
handleCheckboxChange(item, checked) {
if (item.disabled) {
return;
}
this.setNodeChecked(item.node, checked, true);
this.$emit('check-change', item.node, checked);
this.emitCheck(item.node);
},
setNodeChecked(node, checked, deep) {
node.checked = checked;
node.indeterminate = false;
if (!this.checkStrictly && deep !== false) {
this.setChildrenChecked(node, checked);
}
if (!this.checkStrictly) {
this.updateParentChecked(node);
}
},
setChildrenChecked(node, checked) {
this.getChildren(node).forEach(child => {
if (!this.isNodeDisabled(child)) {
child.checked = checked;
child.indeterminate = false;
this.setChildrenChecked(child, checked);
}
});
},
updateParentChecked(node) {
const parent = this.getParentNode(node);
if (!parent) {
return;
}
const children = this.getChildren(parent).filter(child => !this.isNodeDisabled(child));
const allChecked = children.length > 0 && children.every(child => child.checked === true);
const someChecked = children.some(child => child.checked === true || child.indeterminate === true);
parent.checked = allChecked;
parent.indeterminate = !allChecked && someChecked;
this.updateParentChecked(parent);
},
syncParentChecked(nodes) {
nodes.forEach(node => {
const children = this.getChildren(node);
if (children.length > 0) {
this.syncParentChecked(children);
const enabledChildren = children.filter(child => !this.isNodeDisabled(child));
const allChecked = enabledChildren.length > 0 && enabledChildren.every(child => child.checked === true);
const someChecked = enabledChildren.some(child => child.checked === true || child.indeterminate === true);
node.checked = allChecked;
node.indeterminate = !allChecked && someChecked;
}
});
},
getParentNode(node) {
const mapItem = this.nodeMap[this.getNodeKey(node)];
return mapItem ? mapItem.parent : null;
},
walkNodes(nodes, callback) {
nodes.forEach(node => {
callback(node);
this.walkNodes(this.getChildren(node), callback);
});
},
getNodeByKey(key) {
const mapItem = this.nodeMap[key];
return mapItem ? mapItem.node : null;
},
getCheckedNodes(leafOnly) {
const result = [];
this.walkNodes(this.treeData, node => {
const isLeaf = this.getChildren(node).length === 0;
if (node.checked === true && (!leafOnly || isLeaf)) {
result.push(node);
}
});
return result;
},
getCheckedKeys(leafOnly) {
return this.getCheckedNodes(leafOnly).map(node => this.getNodeKey(node));
},
getHalfCheckedNodes() {
const result = [];
this.walkNodes(this.treeData, node => {
if (node.indeterminate === true) {
result.push(node);
}
});
return result;
},
getHalfCheckedKeys() {
return this.getHalfCheckedNodes().map(node => this.getNodeKey(node));
},
setCheckedKeys(keys, leafOnly) {
const checkedKeys = Array.isArray(keys) ? keys : [];
this.walkNodes(this.treeData, node => {
node.checked = false;
node.indeterminate = false;
});
checkedKeys.forEach(key => {
const node = this.getNodeByKey(key);
if (node) {
const isLeaf = this.getChildren(node).length === 0;
if (!leafOnly || isLeaf) {
this.setNodeChecked(node, true, !this.checkStrictly);
}
}
});
if (!this.checkStrictly) {
this.syncParentChecked(this.treeData);
}
},
setChecked(key, checked, deep) {
const node = typeof key === 'object' ? key : this.getNodeByKey(key);
if (node) {
this.setNodeChecked(node, checked, deep);
}
},
setCurrentKey(key) {
this.currentKey = key;
},
getCurrentKey() {
return this.currentKey;
},
getCurrentNode() {
return this.getNodeByKey(this.currentKey);
},
emitCheck(node) {
this.$emit('check', node, {
checkedNodes: this.getCheckedNodes(false),
checkedKeys: this.getCheckedKeys(false),
halfCheckedNodes: this.getHalfCheckedNodes(),
halfCheckedKeys: this.getHalfCheckedKeys()
});
}
}
};
</script>
<style lang="scss" scoped>
.u-tree {
font-size: 28rpx;
color: var(--up-main-color, #303133);
}
.u-tree-node__content {
display: flex;
flex-direction: row;
align-items: center;
min-height: 72rpx;
box-sizing: border-box;
border-radius: 8rpx;
}
.u-tree-node--current .u-tree-node__content {
background-color: var(--up-primary-light, #ecf5ff);
}
.u-tree-node--disabled {
opacity: 0.55;
}
.u-tree-node__switcher {
width: 36rpx;
height: 36rpx;
display: flex;
flex-direction: row;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.u-tree-node__checkbox {
margin-left: 8rpx;
margin-right: 8rpx;
}
.u-tree-node__label {
flex: 1;
min-width: 0;
display: flex;
flex-direction: row;
align-items: center;
}
.u-tree-node__text {
color: var(--up-main-color, #303133);
font-size: 28rpx;
line-height: 40rpx;
}
</style>