# 树
支持单多选,可拖拽改变结构和顺序的树组件
# 1、主组件
XTree.vue
<script >
import { defineComponent, h, ref, watch, getCurrentInstance, onMounted, reactive, toRaw, nextTick } from 'vue';
import { trave, trave2, checkIsLeaf, deepClone, findDraggableNode } from './utils';
import CollapseTransition from './CollapseTransition.js';
import XLoading from './XLoading.vue';
import XCheckbox from '@/components/XCheckbox/XCheckbox.vue';
const renderEmptyContent = (h, props, context) => {
return h('div', {}, props.emptyText);
};
export default defineComponent({
name: 'XTree',
props: {
treeData: {
type: Array,
default: []
},
defaultProps: {
type: Object,
default: () => ({
children: 'children',
title: 'title'
})
},
treeItemHeight: {
type: Number,
default: 26
},
/* 重复点击某节点,是否总是选中当前文本节点 单选时有效(multiple=false) 注意:重复点击已选择的节点,也会触发on-select事件 */
alwaysSelect: {
type: Boolean,
default: false
},
// 文本节点是否可选中 注意:设置false, 则点击文本节点,其事件功能会转向checkbox选中
selectable: {
type: Boolean,
default: true
},
nodeKey: {
type: [String, Number],
default: ''
},
defaultExpandedKeys: {
type: Array,
default: []
},
// 对于同一级的节点,每次只能展开一个
accordion: {
type: Boolean,
default: false
},
lazy: {
type: Boolean,
default: false
},
load: {
type: Function,
default: () => {}
},
showCheckbox: {
type: Boolean,
default: false
},
//在显示复选框的情况下,是否严格的遵循父子不互相关联的做法,默认为 false
checkStrictly: {
type: Boolean,
default: false
},
defaultCheckedKeys: {
type: Array,
default: []
},
emptyText: {
type: String,
default: '暂无数据'
},
// TODO 是否在第一次展开某个树节点后才渲染其子节点
renderAfterExpand: {
type: Boolean,
default: false
},
// render-content 树节点的内容区的渲染 Function Function(h, { node, data, store }
renderContent: {
type: Function,
default: undefined
},
// 是否默认展开所有节点
defaultExpandAll: {
type: Boolean,
default: false
},
// 是否在点击节点的时候展开或者收缩节点, 默认值为 true,如果为 false,则只有点箭头图标的时候才会展开或者收缩节点。
expandOnClickNode: {
type: Boolean,
default: false
},
// 是否在点击节点的时候选中节点,默认值为 false,即只有在点击复选框时才会选中节点。
checkOnClickNode: {
type: Boolean,
default: false
},
// 当前选中的节点
currentNodeKey: {
type: [String, Number],
default: ''
},
// 相邻级节点间的水平缩进,单位为像素
indent: {
type: Number,
default: 16
},
draggable: {
type: Boolean,
default: false
},
// 判断节点能否被拖拽
allowDrag: {
type: Function,
default: undefined
}
},
// emits: {},
// components: {},
setup(props, context) {
const nodeKey = props.nodeKey || 'id';
let treeData = [];
const root = {
title: '根节点',
[nodeKey]: 'x_tree_root',
level: 0,
children: treeData
};
if (props.lazy) {
props.load(root, (newNodeList) => {
root.children = newNodeList;
});
} else {
treeData = deepClone(props.treeData);
root.children = treeData;
}
// 渲染空节点
if (!root.children || root.children.length === 0) {
if (context.slots.empty && typeof context.slots.empty === 'function') {
return () => context.slots.empty();
}
return () => renderEmptyContent(h, props, context);
}
const nodeKeyMap = new Map();
nodeKeyMap.set(root[nodeKey], root);
const rootProxy = reactive(root);
const initData = () => {
trave2(
rootProxy.children,
{
nodeKey,
defaultExpandedKeys: props.defaultExpandedKeys,
defaultCheckedKeys: props.defaultCheckedKeys,
checkStrictly: props.checkStrictly
},
null,
nodeKeyMap
);
};
initData();
if (props.defaultExpandAll) {
for (let node of nodeKeyMap.values()) {
node.expand = true;
}
} else {
// 处理默认展开功能
for (let _nodeKey of props.defaultExpandedKeys) {
const node = nodeKeyMap.get(_nodeKey);
if (node) {
node.expand = true;
let parentNodeKey = node[nodeKey];
while (parentNodeKey) {
const parentNode = nodeKeyMap.get(parentNodeKey);
if (parentNode) {
parentNode.expand = true;
parentNodeKey = parentNode.parentNodeKey;
} else {
parentNodeKey = null;
}
}
}
}
}
let _currentCheckBoxChangeNode = null;
const currentSelectNode = ref({});
watch(currentSelectNode, (newVal, oldVal) => {
// console.log('watch currentSelectNode', newVal);
});
const getCurrentNode = () => {
if (currentSelectNode && currentSelectNode.value && currentSelectNode.value[nodeKey]) {
return toRaw(currentSelectNode.value);
}
return null;
};
// 设定默认选中的行
if (props.selectable && props.currentNodeKey) {
const node = nodeKeyMap.get(props.currentNodeKey);
if (node) {
currentSelectNode.value = node;
}
}
const getCheckedNodes = () => {
const checkedNodes = [];
for (let node of nodeKeyMap.values()) {
if (node.check && node.indeterminate === false) {
checkedNodes.push(toRaw(node));
}
}
return checkedNodes;
};
const onCheckBoxChangeForChild = (node, checkVal) => {
if (node.children && node.children.length > 0) {
for (let i = 0, len = node.children.length; i < len; i++) {
node.children[i].check = checkVal;
node.children[i].indeterminate = false;
onCheckBoxChangeForChild(node.children[i], checkVal);
}
}
};
const onCheckBoxChangeForParent = (node, checkVal) => {
const parentNodeKey = node.parentNodeKey;
const parentNode = nodeKeyMap.get(parentNodeKey);
// 判断一下父元素的状态
let childCheckCount = 0;
let childIndeterminateCount = 0;
if (parentNode && parentNode.children && parentNode.children.length > 0) {
parentNode.children.forEach((child) => {
if (child.check) {
childCheckCount++;
}
if (child.indeterminate) {
childIndeterminateCount++;
}
});
if (childCheckCount === 0) {
parentNode.indeterminate = false;
parentNode.check = false;
} else if (childCheckCount === parentNode.children.length) {
parentNode.indeterminate = false;
parentNode.check = true;
}
if (
(0 < childCheckCount && childCheckCount < parentNode.children.length) ||
(0 < childIndeterminateCount && childIndeterminateCount < parentNode.children.length)
) {
parentNode.indeterminate = true;
parentNode.check = false;
}
}
if (parentNode) {
onCheckBoxChangeForParent(parentNode, checkVal);
}
};
let _timer = null;
const handleCheckboxChange = (node, val) => {
context.emit('current-check-change', toRaw(node), val);
if (_timer) {
clearTimeout(_timer);
_timer = null;
}
_timer = setTimeout(() => {
_currentCheckBoxChangeNode = null;
const checkedNodes = getCheckedNodes();
context.emit('check-change', checkedNodes);
});
if (props.checkStrictly) {
// 父子不联动
return;
}
// 下面是父子联动的逻辑 考虑懒加载的时候,可能check的值需要读取自父节点
onCheckBoxChangeForChild(node, val);
onCheckBoxChangeForParent(node, val);
};
const handleExpandIconClcik = (node) => {
if (checkIsLeaf(node)) {
return;
}
if (node.loading) {
return;
}
let _expand = false;
if (props.accordion && !node.expand) {
if (node.parentNodeKey) {
let parentNode = nodeKeyMap.get(node.parentNodeKey);
if (parentNode) {
parentNode.children.forEach((node) => {
node.expand = false;
});
} else {
}
_expand = true;
}
} else {
_expand = !node.expand;
}
node.expand = props.lazy && _expand ? false : _expand;
if (props.lazy && _expand) {
// 懒加载并且展开
if (node.children && node.children.length > 0) {
// 说明已经加载过了, 不必再加载
node.expand = _expand;
} else {
// 需要加载
node.loading = true;
props.load(node, (newNodeList = []) => {
node.loading = false;
node.children = newNodeList;
if (newNodeList.length === 0) {
node.isleaf = true;
}
initData();
nextTick(() => {
node.expand = _expand;
});
});
}
}
};
const expand = ref(false);
const treeEl = ref(null);
let dropType = ref(''); // prev inner next
const activeNodeId = ref('');
function renderTree(h, parentNode, chidlren, options) {
return h(
CollapseTransition,
{},
{
default: () => {
const parentKey = parentNode ? parentNode[nodeKey] || parentNode.index : '';
return h(
'ul',
{
role: 'tree',
class: [options.outside ? 'x-tree' : 'x-tree-node__children'],
key: !parentNode || parentNode.expand ? parentKey + 'block' : parentKey + 'none',
style: {
display: !parentNode || parentNode.expand ? 'block' : 'none'
},
ref: options.outside ? treeEl : null
},
chidlren.map((node) => {
let children = null;
if (node[options.defaultProps.children] && node[options.defaultProps.children].length > 0) {
children = renderTree(h, node, node[options.defaultProps.children], { ...options, outside: false });
}
const rowVNode = h(
'li',
{
role: 'treeitem',
class: ['x-tree-node', activeNodeId.value === node[nodeKey] && dropType.value ? 'x-tree-node--' + dropType.value : ''],
'data-index': node.index,
'data-id': node[nodeKey],
'data-leaf': node.isleaf !== undefined ? node.isleaf.toString() : '',
'data-parentNodeKey': node.parentNodeKey,
key: node.index,
'^draggable': props.allowDrag ? props.allowDrag(node) : props.draggable
},
[renderNodeContent(h, node), children]
);
return rowVNode;
})
);
}
}
);
}
function renderNodeContent(h, node) {
return h(
'div',
{
class: [
'x-tree-node__content',
currentSelectNode.value[nodeKey] && currentSelectNode.value[nodeKey] === node[nodeKey] ? 'x-tree-node__content--selected' : ''
],
style: {
paddingLeft: (node.level - 1) * props.indent + 'px',
height: props.treeItemHeight + 'px'
},
onClick: (e) => {
e.stopPropagation();
if (props.selectable) {
if (props.alwaysSelect) {
currentSelectNode.value = node;
} else {
if (currentSelectNode.value[nodeKey] === node[nodeKey]) {
currentSelectNode.value = {};
} else {
currentSelectNode.value = node;
}
}
}
context.emit('node-click', toRaw(node));
}
},
[
h('span', {
class: {
'x-tree-node__expand-icon': true,
expanded: node.expand,
'x-tree-node__expand-icon--hidden': checkIsLeaf(node)
},
onClickCapture: (e) => {
e.stopPropagation();
handleExpandIconClcik(node);
}
}),
// 复选框
props.showCheckbox
? h(
XCheckbox,
{
showLabel: false,
disabled: node.disabled,
modelValue: node.check,
indeterminate: node.indeterminate,
'onUpdate:modelValue': (val) => {
node.check = val;
node.indeterminate = false;
},
onChange: (val) => {
if (!_currentCheckBoxChangeNode) {
_currentCheckBoxChangeNode = node;
}
if (node[nodeKey] === _currentCheckBoxChangeNode[nodeKey]) {
handleCheckboxChange(node, val);
}
},
'data-check': node.check.toString()
},
{
default: () => ''
}
)
: null,
// loading 状态
node.loading ? h(XLoading, { style: { marginRight: '4px' } }) : null,
// 文本内容
h(
'div',
{
class: 'x-tree-node__label-content',
style: {
lineHeight: props.treeItemHeight + 'px'
},
onClick: (e) => {
// e.stopPropagation();
if (props.expandOnClickNode) {
handleExpandIconClcik(node);
}
if (props.checkOnClickNode) {
const oldVal = node.check;
node.check = !oldVal;
node.indeterminate = false;
if (!_currentCheckBoxChangeNode) {
_currentCheckBoxChangeNode = node;
}
if (node[nodeKey] === _currentCheckBoxChangeNode[nodeKey]) {
handleCheckboxChange(node, !oldVal);
}
}
}
},
props.renderContent ? props.renderContent(h, node) : node.title
)
]
);
}
/*
为 Tree 中的一个节点追加一个子节点
(data, parentNode) 接收两个参数,1. 要追加的子节点的 data 2. 子节点的 parent 的 data、key 或者 node
*/
const append = (newNode, parentNode) => {
if (parentNode) {
const parentNodeKey = parentNode[nodeKey] || parentNode;
if (parentNodeKey) {
const parentNode = nodeKeyMap.get(parentNodeKey);
if (!parentNode.children) {
parentNode.children = [];
}
parentNode.children.push(newNode);
initData();
}
} else {
rootProxy.children.push(newNode);
initData();
}
};
context.expose({
getCurrentNode,
getCheckedNodes,
append
});
let draggingNode = null;
let targetNode = null;
const handleDrag = () => {
if (props.draggable) {
const toNodeIsChildOfFromNode = (fromNode, toNode) => {
let flag = false;
if (fromNode[nodeKey] === toNode[nodeKey]) {
return true;
}
let node = toNode;
while (node) {
if (node[nodeKey] === fromNode[nodeKey]) {
flag = true;
break;
}
node = nodeKeyMap.get(node.parentNodeKey);
}
return flag;
};
const resetStatus = () => {
draggingNode = null;
targetNode = null;
dropType.value = '';
activeNodeId.value = '';
};
const treeOuterEl = treeEl.value;
const overHandler = (e) => {
const target = e.target;
const overTarget = findDraggableNode(target);
if (overTarget && draggingNode) {
targetNode = overTarget;
activeNodeId.value = targetNode.dataset.id;
const clientY = e.clientY;
const { top } = overTarget.getBoundingClientRect();
const height = props.treeItemHeight;
const step = height / 3;
if (top <= clientY && clientY < top + step) {
dropType.value = 'prev';
} else if (top + step <= clientY && clientY < top + step * 2) {
dropType.value = 'inner';
} else if (top + step * 2 <= clientY && clientY < top + height) {
dropType.value = 'next';
}
}
};
const dragEndHandler = (e) => {
if (draggingNode && targetNode && dropType.value) {
const draggingNodeId = draggingNode.dataset.id;
const fromNode = nodeKeyMap.get(draggingNodeId);
const targetNodeId = targetNode.dataset.id;
const toNode = nodeKeyMap.get(targetNodeId);
if (fromNode[nodeKey] === toNode[nodeKey]) {
resetStatus();
return;
}
// 如果父节点移动到子节点,不被允许
if (toNodeIsChildOfFromNode(fromNode, toNode)) {
resetStatus();
return;
}
// 移除源节点
const fromParentNode = nodeKeyMap.get(fromNode.parentNodeKey);
for (let i = 0, len = fromParentNode.children.length; i < len; i++) {
if (fromParentNode.children[i][nodeKey] === fromNode[nodeKey]) {
fromParentNode.children.splice(i, 1);
break;
}
}
// 插入到目标节点
if (dropType.value === 'inner') {
if (!toNode.children) {
toNode.children = [];
}
fromNode.parentNodeKey = toNode[nodeKey];
console.log({ ...fromNode }, toNode[nodeKey], '====');
toNode.children.push(fromNode);
toNode.expand = true;
}
const toNodeParentNodeKey = toNode.parentNodeKey;
fromNode.parentNodeKey = toNodeParentNodeKey;
const parentNode = nodeKeyMap.get(toNodeParentNodeKey);
for (let i = 0, len = parentNode.children.length; i < len; i++) {
if (parentNode.children[i][nodeKey] === toNode[nodeKey]) {
if (dropType.value === 'prev') {
parentNode.children.splice(i, 0, fromNode);
}
if (dropType.value === 'next') {
parentNode.children.splice(i + 1, 0, fromNode);
}
break;
}
}
toNode.expand = true;
initData();
resetStatus();
}
};
treeOuterEl.addEventListener('dragstart', (e) => {
const target = findDraggableNode(e.target);
if (target) {
draggingNode = target;
}
});
treeOuterEl.addEventListener('dragend', (e) => {
dragEndHandler(e);
});
treeOuterEl.addEventListener('dragover', (e) => {
e.preventDefault();
overHandler(e);
});
treeOuterEl.addEventListener('dragenter', (e) => {
e.preventDefault();
e.stopPropagation();
});
treeOuterEl.addEventListener('dragleave', (e) => {
e.preventDefault();
e.stopPropagation();
});
treeOuterEl.addEventListener('drop', (e) => {
// resetStatus();
});
}
};
onMounted(() => {
const vm = getCurrentInstance();
// console.log(vm.ctx.$options.name, 'vm', treeEl.value);
handleDrag();
});
return () =>
renderTree(h, null, rootProxy.children, {
defaultProps: props.defaultProps,
outside: true
});
}
});
</script>
<style>
@import url('./XTree.css');
</style>
# 1.1样式
XTree.css
.x-tree,
.x-tree-node__children {
list-style: none;
}
.x-tree {
position: relative;
cursor: default;
background: #fff;
color: #606266;
}
.red {
color: red;
}
.x-tree-node__content {
display: flex;
align-items: center;
height: 26px;
cursor: pointer;
white-space: nowrap;
position: relative;
}
.x-tree-node.x-tree-node--prev > .x-tree-node__content::before {
content: ' ';
position: absolute;
left: 0;
top: 0;
right: 0;
height: 1px;
background-color: #409eff;;
}
.x-tree-node.x-tree-node--next > .x-tree-node__content::after {
content: ' ';
position: absolute;
left: 0;
bottom: 0;
right: 0;
height: 1px;
background-color: #409eff;;
}
.x-tree-node.x-tree-node--inner > .x-tree-node__content {
background-color: #409eff;;
}
.x-tree-node__content:hover {
background-color: #f5f7fa;
}
.x-tree-node__content.x-tree-node__content--selected {
background-color: #f5f7fa;
}
.x-tree-node__content > label.x-checkbox {
margin-right: 8px;
}
.x-tree-node__label {
font-size: 14px;
display: inline-block;
height: 100%;
line-height: 26px;
}
.x-tree-node__label-content {
flex: 1;
overflow: hidden;
height: 100%;
font-size: 14px;
}
.x-tree-node__expand-icon {
position: relative;
flex: 0 0 24px;
height: 24px;
padding: 8px;
transform: rotate(-90deg);
transition: transform 0.4s ease;
}
.x-tree-node__expand-icon.expanded {
transform: rotate(0deg);
}
.x-tree-node__expand-icon::before {
content: ' ';
position: absolute;
left: 8px;
top: 10px;
width: 0px;
height: 0px;
border-style: solid;
border-width: 4px 4px;
border-color: #c0c4cc transparent transparent transparent;
}
.x-tree-node__expand-icon.x-tree-node__expand-icon--hidden::before {
display: none;
}
# 2、复选框组件
XCheckbox.vue
<template>
<label :class="{
'x-checkbox': true,
'is-checked': isChecked,
'is-disabled': disabled,
'is-indeterminate': isIndeterminate,
'is-focus': isFocus
}"
@click.stop="noop">
<span :class="{
'x-checkbox__input': true,
'is-checked': isChecked,
'is-disabled': disabled,
'is-indeterminate': isIndeterminate,
'is-focus': isFocus
}">
<input type="checkbox"
ref="CheckBoxInputRef"
class="x-checkbox__original"
:disabled="disabled"
@blur="onInputBlur"
@focus="onInputFocus">
<span class="x-checkbox__inner"
@click.stop="handleTrigger"></span>
</span>
<span v-if="showLabel"
class="x-checkbox__label"
@click.stop="handleTrigger">
<slot></slot>
</span>
</label>
</template>
<script lang="ts">
import { defineComponent, ref, toRef, computed, watch } from 'vue';
export default defineComponent({
name: 'XCheckBox',
props: {
modelValue: {
type: [String, Number, Boolean],
default: ''
},
trueLabel: {
type: [String, Number],
default: undefined
},
falseLabel: {
type: [String, Number],
default: undefined
},
disabled: {
type: Boolean,
default: false
},
indeterminate: {
type: Boolean,
default: false
},
showLabel: {
type: Boolean,
default: true
}
},
computed: {},
emits: ['update:modelValue', 'change'],
// components: {},
setup(props, context) {
const modelValue = ref(props.modelValue);
const isBooleanMode = props.trueLabel === undefined;
const CheckBoxInputRef = ref(null);
const isFocus = ref(false);
const focusInput = () => {
if (props.disabled) {
return;
}
if (CheckBoxInputRef && CheckBoxInputRef.value) {
CheckBoxInputRef.value.focus();
isFocus.value = true;
}
};
watch(
() => props.modelValue,
(newVal, oldVal) => {
modelValue.value = newVal;
context.emit('change', modelValue.value);
focusInput();
}
);
const isIndeterminate = computed(() => {
if (props.disabled) {
return false;
}
return props.indeterminate;
});
const isChecked = computed(() => {
if (isIndeterminate.value) {
return false;
}
if (isBooleanMode) {
return modelValue.value;
}
return modelValue.value === props.trueLabel;
});
const handleTrigger = () => {
if (props.disabled) return;
const oldValue = modelValue.value;
if (isBooleanMode) {
modelValue.value = isIndeterminate.value ? true : !modelValue.value;
} else {
if (isIndeterminate.value) {
modelValue.value = props.trueLabel;
} else {
modelValue.value = modelValue.value === props.trueLabel ? props.falseLabel : props.trueLabel;
}
}
if (oldValue !== modelValue.value) {
context.emit('update:modelValue', modelValue.value);
context.emit('change', modelValue.value);
}
focusInput();
};
const onInputBlur = () => {
isFocus.value = false;
};
const onInputFocus = () => {};
const noop = (e) => {};
return {
modelValue,
handleTrigger,
isChecked,
isIndeterminate,
CheckBoxInputRef,
onInputBlur,
onInputFocus,
isFocus,
noop
};
}
});
</script>
<style >
.x-checkbox {
color: #606266;
font-weight: 500;
font-size: 14px;
position: relative;
cursor: pointer;
display: inline-block;
white-space: nowrap;
user-select: none;
margin-right: 30px;
}
.x-checkbox.is-disabled {
cursor: not-allowed;
}
.x-checkbox:last-of-type {
margin-right: 0;
}
.x-checkbox__input {
position: relative;
display: inline-block;
vertical-align: middle;
outline: none;
line-height: 1;
}
.x-checkbox__inner {
width: 14px;
height: 14px;
display: inline-block;
border: 1px solid #dcdfe6;
position: relative;
z-index: 1;
box-sizing: border-box;
border-radius: 2px;
transition: border-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46), background-color 0.25s cubic-bezier(0.71, -0.46, 0.29, 1.46);
}
.x-checkbox__inner::after {
box-sizing: content-box;
content: '';
border: 1px solid #fff;
border-left: 0;
border-top: 0;
height: 7px;
left: 4px;
position: absolute;
top: 1px;
transform: rotate(45deg) scaleY(0);
width: 3px;
transition: transform 0.15s ease-in 0.05s;
transform-origin: center;
}
.x-checkbox__input.is-checked .x-checkbox__inner:after {
transform: rotate(45deg) scaleY(1);
}
.x-checkbox__input.is-checked .x-checkbox__inner,
.x-checkbox__input.is-indeterminate .x-checkbox__inner {
border-color: #409eff;
background-color: #409eff;
}
.x-checkbox__input.is-disabled .x-checkbox__inner {
background-color: #edf2fc;
border-color: #dcdfe6;
cursor: not-allowed;
}
.x-checkbox__input.is-focus .x-checkbox__inner {
border-color: #409eff;
}
.x-checkbox__input.is-indeterminate .x-checkbox__inner::after {
display: none;
}
.x-checkbox__input.is-indeterminate .x-checkbox__inner::before {
content: '';
position: absolute;
display: block;
background-color: #fff;
height: 2px;
transform: scale(0.5);
left: 0;
right: 0;
top: 5px;
}
.x-checkbox__original {
opacity: 0;
outline: none;
position: absolute;
margin: 0;
width: 0;
height: 0;
z-index: -1;
}
.x-checkbox__label {
display: inline-block;
padding-left: 10px;
font-size: 14px;
font-weight: 500;
}
.x-checkbox.is-checked .x-checkbox__label {
color: #409eff;
}
.x-checkbox.is-disabled .x-checkbox__label {
color: #c0c4cc;
cursor: not-allowed;
}
</style>
# 3、Loading组件
XLoading.vue
<template>
<svg class="xtree-loading"
style="width: 1em;height: 1em;vertical-align: middle;fill: currentColor;overflow: hidden;"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="1473">
<path d="M511.882596 287.998081h-0.361244a31.998984 31.998984 0 0 1-31.659415-31.977309v-0.361244c0-0.104761 0.115598-11.722364 0.115598-63.658399V96.000564a31.998984 31.998984 0 1 1 64.001581 0V192.001129c0 52.586273-0.111986 63.88237-0.119211 64.337537a32.002596 32.002596 0 0 1-31.977309 31.659415zM511.998194 959.99842a31.998984 31.998984 0 0 1-31.998984-31.998984v-96.379871c0-51.610915-0.111986-63.174332-0.115598-63.286318s0-0.242033 0-0.361243a31.998984 31.998984 0 0 1 63.997968-0.314283c0 0.455167 0.11921 11.711527 0.11921 64.034093v96.307622a31.998984 31.998984 0 0 1-32.002596 31.998984zM330.899406 363.021212a31.897836 31.897836 0 0 1-22.866739-9.612699c-0.075861-0.075861-8.207461-8.370021-44.931515-45.094076L195.198137 240.429485a31.998984 31.998984 0 0 1 45.256635-45.253022L308.336112 263.057803c37.182834 37.182834 45.090463 45.253022 45.41197 45.578141A31.998984 31.998984 0 0 1 330.899406 363.021212zM806.137421 838.11473a31.901448 31.901448 0 0 1-22.628318-9.374279L715.624151 760.859111c-36.724054-36.724054-45.018214-44.859267-45.097687-44.93874a31.998984 31.998984 0 0 1 44.77618-45.729864c0.32512 0.317895 8.395308 8.229136 45.578142 45.411969l67.88134 67.88134a31.998984 31.998984 0 0 1-22.624705 54.630914zM224.000113 838.11473a31.901448 31.901448 0 0 0 22.628317-9.374279l67.88134-67.88134c36.724054-36.724054 45.021826-44.859267 45.097688-44.93874a31.998984 31.998984 0 0 0-44.776181-45.729864c-0.32512 0.317895-8.395308 8.229136-45.578142 45.411969l-67.88134 67.884953a31.998984 31.998984 0 0 0 22.628318 54.627301zM255.948523 544.058589h-0.361244c-0.104761 0-11.722364-0.115598-63.658399-0.115598H95.942765a31.998984 31.998984 0 1 1 0-64.00158h95.996952c52.586273 0 63.88237 0.111986 64.337538 0.11921a31.998984 31.998984 0 0 1 31.659414 31.97731v0.361244a32.002596 32.002596 0 0 1-31.988146 31.659414zM767.939492 544.058589a32.002596 32.002596 0 0 1-31.995372-31.666639v-0.361244a31.998984 31.998984 0 0 1 31.659415-31.970085c0.455167 0 11.754876-0.11921 64.34115-0.11921h96.000564a31.998984 31.998984 0 0 1 0 64.00158H831.944685c-51.936034 0-63.553638 0.111986-63.665624 0.115598h-0.335957zM692.999446 363.0176a31.998984 31.998984 0 0 1-22.863126-54.381656c0.317895-0.32512 8.229136-8.395308 45.41197-45.578141l67.88134-67.884953A31.998984 31.998984 0 1 1 828.693489 240.429485l-67.892177 67.88134c-31.020013 31.023625-41.644196 41.759794-44.241539 44.393262l-0.697201 0.722488a31.908673 31.908673 0 0 1-22.863126 9.591025z"
fill=""
p-id="1474"></path>
</svg>
</template>
<script lang="ts">
import { defineComponent } from 'vue';
export default defineComponent({
name: 'XLoading',
// props: {},
// emits: {},
// components: {},
setup(props, context) {
return {};
}
});
</script>
<style scoped>
.xtree-loading {
animation: rotating 2s linear infinite;
}
@keyframes rotating {
0% {
transform: rotate(0);
}
50% {
transform: rotate(180deg);
}
100% {
transform: rotate(360deg);
}
}
</style>
# 4、动画组件
CollapseTransition.js
import { h, Transition } from 'vue';
import './CollapseTransition.css';
function addClass(el, className) {
el.classList.add(className);
// Element.prototype.classList.remove
}
function removeClass(el, className) {
el.classList.remove(className);
}
export default function CollapseTransition(props, context) {
return h(
Transition,
{
name: 'collapse-transition',
'on-before-enter': el => {
addClass(el, 'collapse-transition');
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.style.height = '0';
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
},
'on-enter': el => {
el.dataset.oldOverflow = el.style.overflow;
if (el.scrollHeight !== 0) {
el.style.height = el.scrollHeight + 'px';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
} else {
el.style.height = '';
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
el.style.overflow = 'hidden';
},
'on-after-enter': el => {
// for safari: remove class then reset height is necessary
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
},
'on-before-leave': el => {
if (!el.dataset) el.dataset = {};
el.dataset.oldPaddingTop = el.style.paddingTop;
el.dataset.oldPaddingBottom = el.style.paddingBottom;
el.dataset.oldOverflow = el.style.overflow;
el.style.height = el.scrollHeight + 'px';
el.style.overflow = 'hidden';
},
'on-leave': el => {
if (el.scrollHeight !== 0) {
// for safari: add class after set height, or it will jump to zero height suddenly, weired
addClass(el, 'collapse-transition');
el.style.height = 0;
el.style.paddingTop = 0;
el.style.paddingBottom = 0;
}
},
'on-after-leave': el => {
removeClass(el, 'collapse-transition');
el.style.height = '';
el.style.overflow = el.dataset.oldOverflow;
el.style.paddingTop = el.dataset.oldPaddingTop;
el.style.paddingBottom = el.dataset.oldPaddingBottom;
}
},
() => context.slots.default()
);
}
动画组件样式 CollapseTransition.css
.collapse-transition {
transition: 0.3s height ease-in-out, 0.3s padding-top ease-in-out, 0.3s padding-bottom ease-in-out;
}
c
# 5、辅助函数
utils.js
export function trave(root, options = {}) {
if (!root) return root;
const nodeIndexMap = new Map();
const { nodeKey, defaultExpandedKeys, checkStrictly } = options;
const defaultExpandedKeysMap = new Map();
if (nodeKey && defaultExpandedKeys.length > 0) {
defaultExpandedKeys.forEach(key => {
defaultExpandedKeysMap.set(key, true);
});
}
const queue = [];
queue.push(...root);
let level = 1;
while (queue.length > 0) {
// 关键循环
const len = queue.length;
for (let i = 0; i < len; i++) {
const node = queue.shift();
node.level = level;
node.index = `${level}_${i}`;
node[nodeKey] = node[nodeKey] || node.index;
nodeIndexMap.set(node.index, node);
// node.isleaf = node.children && node.children.length > 0 ? false : true;
node.check = false;
node.disabled = node.disabled || false;
let expand = node.expand || false;
node.expand = expand;
// node.parentId =
if (node.children && node.children.length > 0) {
queue.push(...node.children);
}
}
level++;
}
trave2(root, options);
for (let [_nodeKey, _nodeIndex] of defaultExpandedKeysMap) {
const node = nodeIndexMap.get(_nodeIndex);
if (node) {
node.expand = true;
let parentIndex = node.parentIndex;
while (parentIndex && parentIndex !== '-1') {
const parentNode = nodeIndexMap.get(parentIndex);
parentNode.expand = true;
parentIndex = parentNode.parentIndex;
}
}
}
return root;
}
export function trave2(root, options = { nodeKey: 'id' }, parentNode = null, nodeKeyMap) {
const { nodeKey, defaultCheckedKeys, checkStrictly } = options;
for (let i = 0, len = root.length; i < len; i++) {
const node = root[i];
let index = parentNode ? `${parentNode.index}_${i}` : i.toString();
node.index = index;
let level = parentNode ? parentNode.level + 1 : 1;
node.level = level;
node[nodeKey] = node[nodeKey] || node.index;
nodeKeyMap.set(node[nodeKey], node);
// node.isleaf = node.children && node.children.length > 0 ? false : true;
const defaultCheck = defaultCheckedKeys.some(value => {
return value === node[nodeKey];
});
node.check = node.check || defaultCheck || false;
node.indeterminate = node.indeterminate || false;
if (!checkStrictly) {
if (parentNode && parentNode.check) {
node.check = true;
}
}
node.disabled = node.disabled || false;
let expand = node.expand || false;
node.expand = expand;
node.loading = false;
if (parentNode) {
node.parentNodeKey = parentNode[nodeKey];
} else {
node.parentNodeKey = 'x_tree_root';
}
if (node && node.children && node.children.length > 0) {
trave2(node.children, options, node, nodeKeyMap);
}
}
}
export function checkIsLeaf(node) {
if (!node) {
return false;
}
if (node.isleaf === true) {
return true;
}
if (node.isleaf === false) {
return false;
}
return !(node.children && node.children.length > 0);
}
export function deepClone(target, hash = new WeakMap()) {
// 已经遍历过了就返回
if (hash.has(target)) return target;
let cobj;
// 处理为null的特殊情况
if (target === null) {
return target;
}
// 如果是基本数据类型 则直接返回了
const t = typeof target;
switch (t) {
case 'string':
case 'number':
case 'boolean':
case 'undefined':
case 'symbol':
return target;
}
if (Array.isArray(target)) {
hash.set(target, true);
cobj = [];
target.forEach(o => {
cobj.push(deepClone(o, hash));
});
} else {
cobj = {};
if (Object.prototype.toString.call(target) === '[object Object]') {
hash.set(target, true);
// 同时考虑string key 和 symbol key
const keys = [...Object.getOwnPropertyNames(target), ...Object.getOwnPropertySymbols(target)];
for (let i = 0, len = keys.length; i < len; i++) {
const key = keys[i];
cobj[key] = deepClone(target[key], hash);
}
} else {
// 考虑到可能是函数等的情况
cobj = target;
}
}
return cobj;
}
export function findDraggableNode(
el,
predicate = el => {
try {
return el.getAttribute('role') === 'treeitem';
} catch (error) {
return false;
}
}
) {
if (!el) return null;
let target = el;
while (target) {
if (predicate(target)) {
break;
} else {
target = target.parentNode;
}
}
return target;
}
# 6、用法
<template>
<div>
<div :style=" {
width: '400px',
padding: '20px',
border: '1px solid #ccc',
margin: '20px'
}">
<XTree ref="xTreeRef"
:treeData="treeData"
:selectable="true"
:alwaysSelect="false"
nodeKey="id"
:accordion="false"
:defaultExpandedKeys="defaultExpandedKeys"
:defaultExpandAll="false"
:defaultCheckedKeys="defaultCheckedKeys"
:showCheckbox="true"
:checkStrictly="false"
:renderContent="renderContent"
@node-click="handleNodeClick"
:expandOnClickNode="false"
:checkOnClickNode="false"
currentNodeKey="A-1"
draggable
:treeItemHeight="32"
:allowDrag="allowDrag"
@check-change="onCheckChange"></XTree>
</div>
<div :style=" {
width: '400px',
padding: '20px',
border: '1px solid #ccc',
margin: '20px'
}">
<XTree ref="xTreeRef2"
nodeKey="id"
:accordion="true"
lazy
:load="loadMore"></XTree>
</div>
<div :style=" {
width: '400px',
padding: '20px',
border: '1px solid #ccc',
margin: '20px'
}">
<XTree :treeData="[]"
:selectable="true"
:alwaysSelect="true"
nodeKey="id"
:accordion="true"
:defaultExpandedKeys="defaultExpandedKeys"
:defaultCheckedKeys="defaultCheckedKeys"
showCheckbox
:checkStrictly="false"
@node-click="handleNodeClick">
<template v-slot:empty>
<div>
暂无数据啊啊啊
</div>
</template>
</XTree>
</div>
<div>
<button @click="triggerVisible">triggerVisible</button>
<CollapseTransition>
<div v-show="visible"
class="">adasdasd</div>
</CollapseTransition>
</div>
<div>
{{treeData[0].expand}}
</div>
<div>
<!-- <x-check-box v-model="checked">选项1</x-check-box> -->
<x-checkbox v-model="allCheck"
:true-label="1"
:false-label="0"
:indeterminate="indeterminate"
@change="onChange1">选项1</x-checkbox>
<x-checkbox v-model="checked"
true-label="1"
false-label="0"
@change="onChange">选项2</x-checkbox>
<button @click="triggerChecked">triggerChecked --- {{checked}}--{{allCheck}}</button>
<button @click="makeindeterminate">make indeterminate</button>
<button @click="getCurrentNode">getCurrentNode</button>
<br>
<button @click="deleteNode">delete node</button>
<br>
<button @click="handleClickAdd">add</button>
<p>
<button @click="getCheckedNodes">getCheckedNodes</button>
</p>
</div>
</div>
</template>
<script lang="ts">
import { defineComponent, h, reactive, toRaw, ref } from 'vue';
import XTree from './XTree.vue';
import CollapseTransition from './CollapseTransition';
import XCheckbox from '@/components/XCheckbox/XCheckbox.vue';
import { trave2 } from './utils';
import XLoading from './XLoading.vue';
export default defineComponent({
name: 'Tree',
// props: {},
// emits: {},
components: {
XTree,
XLoading,
CollapseTransition,
XCheckbox
},
setup(props, context) {
const treeData = [
{
title: 'node-A',
id: 'A',
children: [
{ title: 'node-A-1', id: 'A-1' },
{
title: 'node-A-2',
id: 'A-2',
disabled: true,
children: [{ title: 'node-A-2-1', id: 'A-2-1' }]
},
{
title: 'node-A-3',
id: 'A-3',
children: [
{ title: 'node-A-3-1', id: 'A-3-1' },
{ title: 'node-A-3-2', id: 'A-3-2' },
{ title: 'node-A-3-3', id: 'A-3-3' }
]
}
]
},
{
title: 'node-B',
id: 'B',
children: [
{
title: 'node-B-1',
id: 'B-1',
children: [
{
title: 'node-B-1-1',
id: 'B-1-1',
children: [
{
title: 'node-B-1-1-1',
id: 'B-1-1-1'
}
]
},
{
title: 'node-B-1-2',
id: 'B-1-2'
}
]
},
{
title: 'node-B-2',
id: 'B-2'
},
{
title: 'node-B-3',
id: 'B-3'
},
{
title: 'node-B-4',
id: 'B-4'
}
]
}
];
// trave2(treeData);
const handleNodeClick = (node) => {
// console.log(node.title);
// console.log(toRaw(node));
};
const visible = ref(true);
const triggerVisible = () => {
visible.value = !visible.value;
};
const checked = ref('1');
const triggerChecked = () => {
if (checked.value === '1') {
checked.value = '0';
} else {
checked.value = '1';
}
// checked.value = !checked.value;
};
const onChange = (val) => {
console.log(val);
};
const allCheck = ref(false);
const indeterminate = ref(true);
const onChange1 = (val) => {
allCheck.value = val;
indeterminate.value = false;
};
const makeindeterminate = () => {
indeterminate.value = true;
};
const xTreeRef = ref(null);
const getCurrentNode = () => {
const node = xTreeRef.value.getCurrentNode();
console.log('getCurrentNode', node);
};
const getCheckedNodes = () => {
const node = xTreeRef.value.getCheckedNodes();
console.log('getCheckedNodes', node);
};
let _id = 1000;
const handleClickAdd = () => {
xTreeRef.value.append({
id: 'new' + _id,
title: 'node-c',
children: [
{
id: _id + '_1',
title: 'node-' + _id
}
]
});
_id++;
};
const deleteNode = () => {
treeData[0].children.splice(1, 1);
};
const defaultExpandedKeys = ref(['A']);
const defaultCheckedKeys = ref(['A-2']);
const loadMore = (node, resolve) => {
if (node.level === 0) {
return resolve([
{
title: 'A',
id: 'A',
isleaf: false
},
{ title: 'B', id: 'B' }
]);
}
setTimeout(() => {
const children = [];
for (let i = 0; i < 4; i++) {
children.push({
id: node.id + i.toString(),
title: node.title + '-' + i,
isleaf: i === 0
});
}
resolve(children);
}, 100);
};
const onCheckChange = (node, val) => {
console.log(node, val);
};
const renderContent = (h, node) => {
return h('span', { style: { color: '#ccc' } }, node.title.replace('node-', ''));
};
const allowDrag = (node) => {
if (node.id === 'B-2') {
return false;
}
return true;
};
return {
treeData,
handleClickAdd,
handleNodeClick,
visible,
triggerVisible,
checked,
triggerChecked,
onChange,
allCheck,
onChange1,
indeterminate,
makeindeterminate,
xTreeRef,
getCurrentNode,
deleteNode,
defaultExpandedKeys,
loadMore,
onCheckChange,
defaultCheckedKeys,
renderContent,
getCheckedNodes,
allowDrag
};
}
});
</script>