VirtTree
Basic
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
expandOnClickNode
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Selectable
selectable 模式下只能点击图标进行展开/折叠
ts
type Props = {
selectable: boolean; // 开启 展开/折叠
selectMultiple: boolean; // 开启多选
};
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
disableSelect?: boolean;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
list.value[1].disableSelect = true;
});
const virtTreeRef = ref<typeof VirtTree>();
// 可传可不传
const selectedKeys = ref<(number | string)[]>(['0']);
function onSelect(keys: number[]) {
console.log('keys', keys);
}
let selectAll = false;
function onToggleSelectAll() {
selectAll = !selectAll;
virtTreeRef.value?.selectAll(selectAll);
}
let selectKey1 = true;
function onToggleSelectNode() {
selectKey1 = !selectKey1;
virtTreeRef.value?.selectNode('0', selectKey1);
}
// setTimeout(() => {
// selectedKeys.value = [];
// }, 2000);
// setTimeout(() => {
// selectedKeys.value = [];
// }, 4000);
</script>
<template>
<div class="demo-tree">
<div style="height: 40px; display: flex">
<div>选中keys:</div>
<div style="flex: 1; overflow: auto">[{{ selectedKeys.join(', ') }}]</div>
</div>
<div style="margin-bottom: 4px">
<span class="demo-btn" @click="onToggleSelectAll">ToggleSelectAll</span>
<span class="demo-btn" @click="onToggleSelectNode"
>ToggleSelectNode(key: 0)</span
>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
defaultExpandAll
v-model:selectedKeys="selectedKeys"
selectable
selectMultiple
@select="onSelect"
>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Focus
Focus 状态切换完全交由外部处理,内部仅给Node节点加上.is-focused
类名
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef, triggerRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
disableSelect?: boolean;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const focusedKeys = ref<(number | string)[]>(['0']);
const selectedKeys = ref<(number | string)[]>(['1']);
function onSelect(keys: number[]) {
console.log('keys', keys);
}
function onChangeFocus(node: any) {
focusedKeys.value = [node.data.id];
// focusedKeys.value.splice(0, 1, node.key);
console.log('onChangeFocus', focusedKeys.value);
virtTreeRef.value?.forceUpdate();
}
// setTimeout(() => {
// focusedKeys.value = [];
// }, 2000);
</script>
<template>
<div class="demo-tree">
<div>
<span>选中keys:</span>
<span>[{{ focusedKeys.join(', ') }}]</span>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
selectable
defaultExpandAll
:list="list"
:fieldNames="customFieldNames"
:indent="20"
:focusedKeys="focusedKeys"
:selectedKeys="selectedKeys"
@select="onSelect"
>
<template #content="{ node }">
<div class="content">
<div>
<span>level: {{ node.level }}; </span>
<span>title: {{ node.data.name }}</span>
</div>
<div class="more" @click.stop="onChangeFocus(node)">
<svg
t="1720683384262"
class="icon"
viewBox="0 0 1024 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="5388"
xmlns:xlink="http://www.w3.org/1999/xlink"
width="100%"
height="100%"
>
<path
d="M288 456.864A63.264 63.264 0 0 0 256 448a64 64 0 1 0 0 128c11.712 0 22.56-3.392 32-8.896 19.04-11.072 32-31.488 32-55.104 0-23.648-12.96-44.064-32-55.136M544 456.864A63.264 63.264 0 0 0 512 448c-11.712 0-22.56 3.36-32 8.864-19.04 11.072-32 31.488-32 55.136 0 23.616 12.96 44.032 32 55.104 9.44 5.504 20.288 8.896 32 8.896s22.56-3.392 32-8.896c19.04-11.072 32-31.488 32-55.104 0-23.648-12.96-44.064-32-55.136M768 448c-11.712 0-22.56 3.392-32 8.864-19.04 11.104-32 31.52-32 55.136 0 23.616 12.96 44.032 32 55.136 9.44 5.472 20.288 8.864 32 8.864a64 64 0 1 0 0-128"
fill="#757575"
p-id="5389"
></path>
</svg>
</div>
</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
.content {
width: 100%;
display: flex;
justify-content: space-between;
&:hover {
.more {
display: block;
}
}
.more {
display: none;
width: 22px;
height: 22px;
border-radius: 4px;
color: #757575;
background-color: red;
}
}
}
</style>
<style lang="scss">
.demo-tree {
.virt-tree-node.is-focused:not(.is-selected) {
background-color: #4c88ff26;
}
.virt-tree-node.is-focused {
.more {
display: block;
}
}
}
</style>
Expand
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
disableCheckbox: indexChild % 2 === 0,
children:
indexChild % 2 !== 0
? []
: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}-${indexChild}`,
})),
})),
})),
}));
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<string>('0-0');
const onExpandAll = () => {
virtTreeRef.value?.expandAll(true);
};
const onCollapseAll = () => {
virtTreeRef.value?.expandAll(false);
};
const expandNode = () => {
virtTreeRef.value?.expandNode(key.value, true);
};
const collapseNode = () => {
virtTreeRef.value?.expandNode(key.value, false);
};
const expandedKeys = ref<(number | string)[]>(['0-0']);
// setTimeout(() => {
// expandedKeys.value = [];
// }, 2000);
const onExpand = (data: Data, expandedInfo: any) => {
console.warn('onExpand', data, expandedInfo);
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div class="input-container">
<div class="input-label">操作指定节点:</div>
<input v-model="key" />
<div class="btn-item" @click="expandNode">展开</div>
<div class="btn-item" @click="collapseNode">折叠</div>
</div>
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="onCollapseAll">折叠所有</div>
<div class="btn-item" @click="onExpandAll">展开所有</div>
</div>
</div>
<div style="height: 40px; display: flex">
<div>expandedKeys:</div>
<div style="flex: 1; overflow: auto">{{ expandedKeys }}</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
v-model:expandedKeys="expandedKeys"
@expand="onExpand"
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Checkbox
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
disableCheckbox: indexChild % 2 === 0,
children:
indexChild % 2 !== 0
? []
: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}-${indexChild}`,
})),
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<string>('0');
const onExpandAll = () => {
virtTreeRef.value?.expandAll(true);
};
const onCollapseAll = () => {
virtTreeRef.value?.expandAll(false);
};
const expandNode = () => {
virtTreeRef.value?.expandNode(key.value, true);
};
const collapseNode = () => {
virtTreeRef.value?.expandNode(key.value, false);
};
const checkedKeys = ref<(number | string)[]>(['0']);
const onCheck = (data: Data, checkedInfo: any) => {
console.warn('data', data, checkedInfo);
};
const clearCheck = (check: boolean) => {
virtTreeRef.value?.checkAll(check);
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="onCollapseAll">折叠所有</div>
<div class="btn-item" @click="onExpandAll">展开所有</div>
<div class="btn-item" @click="clearCheck(false)">清空 check</div>
<div class="btn-item" @click="clearCheck(true)">check所有</div>
</div>
<div class="input-container">
<div class="input-label">操作指定节点:</div>
<input v-model="key" />
<div class="btn-item" @click="expandNode">展开</div>
<div class="btn-item" @click="collapseNode">折叠</div>
</div>
</div>
<div>{{ checkedKeys }}</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
checkable
checkOnClickNode
v-model:checkedKeys="checkedKeys"
@check="onCheck"
defaultExpandAll
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Filter
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<string>('Node-0');
const filterMethod = (query: string, node: any) => {
return node.title.includes(query);
};
const onFilter = () => {
virtTreeRef.value?.filter(key.value);
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div class="input-container">
<input v-model="key" />
<div class="btn-item" @click="onFilter">filter</div>
</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
:filter-method="filterMethod"
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
ICON Slot
提供展开状态下的图标
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const filterMethod = (query: string, node: any) => {
return node.title.includes(query);
};
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
>
<template #icon>
<div style="height: 16px; width: 16px">
<svg
viewBox="0 0 48 48"
fill="none"
xmlns="http://www.w3.org/2000/svg"
stroke="currentColor"
class="arco-icon arco-icon-down"
stroke-width="4"
stroke-linecap="butt"
stroke-linejoin="miter"
>
<path d="M39.6 17.443 24.043 33 8.487 17.443"></path>
</svg>
</div>
</template>
<!-- 或者使用作用域插槽,注意:折叠状态下面是被旋转的 -->
<!-- <template #icon="{ node }">
<div v-if="node.isExpanded">1</div>
<div v-else>2</div>
</template> -->
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Content Slot
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const filterMethod = (query: string, node: any) => {
return node.title.includes(query);
};
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
:filter-method="filterMethod"
>
<template #content="{ node }">
<div>
<span>level: {{ node.level }}; </span>
<span>title: {{ node.data.name }}</span>
</div>
</template>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Default Slot
自定义整个node节点
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const onExpandAll = () => {
virtTreeRef.value?.expandAll(true);
};
const onCollapseAll = () => {
virtTreeRef.value?.expandAll(false);
};
const expandNode = () => {
virtTreeRef.value?.expandNode(key.value, true);
};
const collapseNode = () => {
virtTreeRef.value?.expandNode(key.value, false);
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="onCollapseAll">折叠所有</div>
<div class="btn-item" @click="onExpandAll">展开所有</div>
</div>
<div class="input-container">
<div class="input-label">操作指定节点:</div>
<input v-model="key" />
<div class="btn-item" @click="expandNode">展开</div>
<div class="btn-item" @click="collapseNode">折叠</div>
</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:expandedKeys="['4']"
:indent="20"
selectable
defaultExpandAll
stickyHeaderStyle="text-align: center; height: 40px; background: #42b983;"
headerStyle="text-align: center; height: 40px; background: cyan"
footerStyle="text-align: center; height: 40px; background: cyan"
stickyFooterStyle="text-align: center; height: 40px; background: #42b983;"
>
<template #default="{ node }">
<div
style="
height: 40px;
display: flex;
align-items: center;
border-bottom: 1px solid red;
"
>
<div>level: {{ node.level }};</div>
<div>--</div>
<div>title: {{ node.data.name }}</div>
</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
showLine
展示节点连接线
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
// list.value[0].children[0].title =
// '所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入';
// list.value[0].children[0].title =
// '1Sm4srxpVaGczlsAPRv-F - Synagoga quae eligendi est arx alveus pauper ager. Canonicus verbera auditor utrum vociferor taceo. Paens volo peior.';
list.value[0].children[0].title =
'abvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagacabvfgzgagagac';
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const filterMethod = (query: string, node: any) => {
return node.title.includes(query);
};
const showLine = ref(true);
const changeLine = () => {
showLine.value = !showLine.value;
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div class="btn-item" @click="changeLine">
{{ showLine ? '隐藏' : '显示' }}连接线
</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="28"
:iconSize="14"
:filter-method="filterMethod"
:showLine="showLine"
defaultExpandAll
:itemGap="4"
fixed
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
Operations
对 tree 的各种操作方式
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const onExpandAll = () => {
virtTreeRef.value?.expandAll(true);
};
const onCollapseAll = () => {
virtTreeRef.value?.expandAll(false);
};
const expandNode = () => {
virtTreeRef.value?.expandNode(key.value, true);
};
const collapseNode = () => {
virtTreeRef.value?.expandNode(key.value, false);
};
const targetOffset = ref(0);
const targetKey = ref(0);
const scrollToOffset = () => {
if (targetOffset.value >= 0)
virtTreeRef.value?.scrollTo({
offset: targetOffset.value,
});
};
const scrollToTarget = (isTop: boolean) => {
if (isTop) {
virtTreeRef.value?.scrollTo({
key: targetKey.value,
align: 'top',
});
} else {
virtTreeRef.value?.scrollTo({
key: targetKey.value,
});
}
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="onCollapseAll">折叠所有</div>
<div class="btn-item" @click="onExpandAll">展开所有</div>
</div>
<div class="input-container">
<div class="input-label">操作指定节点:</div>
<input v-model="key" />
<div class="btn-item" @click="expandNode">展开</div>
<div class="btn-item" @click="collapseNode">折叠</div>
</div>
</div>
<div class="tree-btn-container">
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="scrollToOffset">滚动到指定位置</div>
<div class="btn-item" @click="scrollToTarget(true)">
滚动到指定节点(顶部)
</div>
<div class="btn-item" @click="scrollToTarget(false)">
滚动到指定节点(可视区)
</div>
</div>
<div class="input-container">
<div class="input-label">目标 key:</div>
<input v-model="targetKey" />
<div class="input-label">目标距离:</div>
<input v-model="targetOffset" />
</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
width: 120px;
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
header&footer Slot
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree } from 'vue-virt-list';
import 'vue-virt-list/lib/assets/tree.css';
type Data = {
id: string | number;
title: string;
children?: Data;
}[];
const customFieldNames = {
key: 'id',
};
const list = shallowRef<Data>([]);
onMounted(() => {
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: String(i),
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: `${i}-${index}`,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: `${i}-${index}-${indexChild}`,
title: `Node-${i}-${index}-${indexChild}`,
})),
})),
}));
});
const virtTreeRef = ref<typeof VirtTree>();
const key = ref<number>(0);
const onExpandAll = () => {
virtTreeRef.value?.expandAll(true);
};
const onCollapseAll = () => {
virtTreeRef.value?.expandAll(false);
};
const expandNode = () => {
virtTreeRef.value?.expandNode(key.value, true);
};
const collapseNode = () => {
virtTreeRef.value?.expandNode(key.value, false);
};
</script>
<template>
<div class="demo-tree">
<div class="tree-btn-container">
<div style="display: flex; gap: 8px">
<div class="btn-item" @click="onCollapseAll">折叠所有</div>
<div class="btn-item" @click="onExpandAll">展开所有</div>
</div>
<div class="input-container">
<div class="input-label">操作指定节点:</div>
<input v-model="key" />
<div class="btn-item" @click="expandNode">展开</div>
<div class="btn-item" @click="collapseNode">折叠</div>
</div>
</div>
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
:list="list"
:fieldNames="customFieldNames"
:expandedKeys="['4']"
:indent="20"
selectable
defaultExpandAll
stickyHeaderStyle="text-align: center; height: 40px; background: #42b983;"
headerStyle="text-align: center; height: 40px; background: cyan"
footerStyle="text-align: center; height: 40px; background: cyan"
stickyFooterStyle="text-align: center; height: 40px; background: #42b983;"
>
<template #stickyHeader>
<div>悬浮header</div>
</template>
<template #header>
<div>header</div>
</template>
<template #footer>
<div>footer</div>
</template>
<template #stickyFooter>
<div>悬浮footer</div>
</template>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
draggable
拖拽后不直接修改数据,而是提供 dragend 事件,由业务自行判定并修改数据,数据更改后,通过响应式更新树。
位置判定说明:每个元素会被切割为3份,上面一份判定为拖入该元素上方,下面一份判定为拖入该元素下方。如果该元素被禁止拖入,则会把该元素一分为二,去掉中间的区域。
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree, type TreeNode } from 'vue-virt-list';
type ItemData = {
id: string | number;
title: string;
children?: ItemData[];
// 禁止拖入
disableDragIn?: boolean;
// 禁止托出
disableDragOut?: boolean;
};
const customFieldNames = {
key: 'id',
};
const list = ref<ItemData[]>([]);
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: i + 1,
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: (i + 1) * 100 + index,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: (i + 1) * 1000 + (index + 1) * 10 + indexChild,
title: `Node-${i}-${index}-${indexChild}`,
children: Array.from({ length: 2 }).map((_, indexChildChild) => ({
id:
(i + 1) * 10000 +
(index + 1) * 100 +
(indexChild + 1) * 10 +
indexChildChild,
title: `Node-${i}-${index}-${indexChild}-${indexChildChild} (禁止拖入-disableDragIn)`,
// 所有叶子节点禁用拖入
disableDragIn: true,
})),
})),
})),
}));
// setTimeout(() => {
// console.log(list.value.length);
// list.value.splice(0, 1);
// console.log(list.value.length);
// }, 1000);
// TODO 模拟数据
// list.value[0].title =
// '所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入所有叶子节点禁用拖入';
list.value[1].disableDragOut = true;
list.value[1].title = `${list.value[1].title} (禁止拖出-disableDragOut)`;
const virtTreeRef = ref<typeof VirtTree>();
// const key = ref<number>(0);
const filterMethod = (query: string, node: any) => {
return node.title.includes(query);
};
function onDragstart() {
console.log('onDragstart');
}
function onDragEnd(data: any) {
if (data) {
console.log('drag success', data);
// const { node, prevNode, parentNode } = data;
// console.log('drag node', node);
// console.log('target prevNode', prevNode);
// console.log('target parentNode', parentNode);
} else {
console.warn('drag fail: Invalid');
}
}
const draggable = ref(true);
// setTimeout(() => {
// draggable.value = true;
// }, 1000);
// setTimeout(() => {
// draggable.value = false;
// }, 6000);
const expandedKeys = ref<number[]>([1, 100, 102]);
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<!--
:dragLineWidth="28"
:dragLineLeading="14"
dragSourceClass="drag-class"
dragGhostClass="drag-ghost-class" -->
<VirtTree
ref="virtTreeRef"
v-model:expandedKeys="expandedKeys"
:list="list"
:fieldNames="customFieldNames"
:indent="20"
:iconSize="14"
:filterMethod="filterMethod"
:itemGap="4"
:draggable="draggable"
@dragstart="onDragstart"
@dragend="onDragEnd"
dragOnly
dragGhostClass="drag-ghost-class"
dragClass="drag-class"
expandOnClickNode
default-expand-all
>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
<template #content="{ node }">
<div
v-if="!node.isLeaf"
style="width: 16px; height: 16px; margin-right: 4px"
>
<svg
style="width: 100%; height: 100%"
t="1735113670562"
class="icon"
viewBox="0 0 1208 1024"
version="1.1"
xmlns="http://www.w3.org/2000/svg"
p-id="11451"
width="256"
height="256"
>
<path
d="M132.51584 120.4736h879.4368c33.26976 0 60.2368 26.96704 60.2368 60.23168v409.6c0 33.26976-26.96704 60.2368-60.2368 60.2368H132.51584c-33.26464 0-60.23168-26.96704-60.23168-60.2368v-409.6c0-33.26464 26.96704-60.2368 60.23168-60.2368z"
fill="#F9B552"
p-id="11452"
></path>
<path
d="M469.8368 0c73.18528 0 132.51584 59.33056 132.51584 132.51584v84.3264h469.8368c73.18528 0 132.51584 59.33568 132.51584 132.52096v542.12096c0 73.18528-59.33056 132.51584-132.51584 132.51584H132.51584A132.51584 132.51584 0 0 1 0 891.48416V349.3632c0-4.03456 0.1792-8.06912 0.54272-12.04736A134.25664 134.25664 0 0 1 0 325.2736V132.51584C0 59.33056 59.33056 0 132.51584 0h337.32096z"
fill="#FFCF5C"
p-id="11453"
></path>
</svg>
</div>
<div>{{ node.title }}</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
drag handler
自定义拖拽生效图标,而不是整个节点
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree, type TreeNode } from 'vue-virt-list';
type ItemData = {
id: string | number;
title: string;
children?: ItemData[];
// 禁止拖入
disableDragIn?: boolean;
// 禁止托出
disableDragOut?: boolean;
};
const customFieldNames = {
key: 'id',
};
const list = ref<ItemData[]>([]);
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: i + 1,
title: `Node-${i}`,
children: Array.from({ length: 3 }).map((_, index) => ({
id: (i + 1) * 100 + index,
title: `Node-${i}-${index}`,
children: Array.from({ length: 2 }).map((_, indexChild) => ({
id: (i + 1) * 1000 + (index + 1) * 10 + indexChild,
title: `Node-${i}-${index}-${indexChild}`,
// 所有叶子节点禁用拖入
disableDragIn: true,
})),
})),
}));
const virtTreeRef = ref<typeof VirtTree>();
const filterMethod = (query: string, node: any) => {
return node.name.includes(query);
};
function onDragstart() {
console.log('onDragstart');
}
function onDragEnd(data: any) {
if (data) {
console.log('drag success', data);
} else {
console.warn('drag fail: Invalid');
}
}
const draggable = ref(true);
const expandedKeys = ref<number[]>([1, 100, 102]);
const toggleExpand = (key: number) => {
virtTreeRef.value?.expandNode(key, !expandedKeys.value.includes(key));
};
function onDragStart(e: MouseEvent | Event) {
if (virtTreeRef.value?.onDragstart) virtTreeRef.value?.onDragstart(e);
}
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
v-model:expandedKeys="expandedKeys"
:list="list"
:fieldNames="customFieldNames"
:indent="16"
:iconSize="14"
:filter-method="filterMethod"
:draggable="draggable"
@dragstart="onDragstart"
@dragend="onDragEnd"
dragOnly
dragGhostClass="drag-ghost-class"
dragClass="drag-class"
expandOnClickNode
default-expand-all
>
<template #default="{ node }">
<div
:style="{
display: 'flex',
alignItems: 'center',
height: '32px',
}"
@click="toggleExpand(node.data.id)"
>
<!-- drag handler -->
<div style="cursor: move" draggable="true" @dragstart="onDragStart">
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 3C5 2.44772 5.44772 2 6 2C6.55228 2 7 2.44772 7 3C7 3.55228 6.55228 4 6 4C5.44772 4 5 3.55228 5 3ZM9 3C9 2.44772 9.44772 2 10 2C10.5523 2 11 2.44772 11 3C11 3.55228 10.5523 4 10 4C9.44772 4 9 3.55228 9 3ZM5 8C5 7.44772 5.44772 7 6 7C6.55228 7 7 7.44772 7 8C7 8.55228 6.55228 9 6 9C5.44772 9 5 8.55228 5 8ZM9 8C9 7.44772 9.44772 7 10 7C10.5523 7 11 7.44772 11 8C11 8.55228 10.5523 9 10 9C9.44772 9 9 8.55228 9 8ZM5 13C5 12.4477 5.44772 12 6 12C6.55228 12 7 12.4477 7 13C7 13.5523 6.55228 14 6 14C5.44772 14 5 13.5523 5 13ZM9 13C9 12.4477 9.44772 12 10 12C10.5523 12 11 12.4477 11 13C11 13.5523 10.5523 14 10 14C9.44772 14 9 13.5523 9 13Z"
fill="var(--virt-tree-color-icon)"
/>
</svg>
</div>
<!-- indent -->
<div
:style="{
width: `${(node.level - 1) * 16}px`,
}"
></div>
<!-- switcher icon -->
<div
v-if="node.data?.children?.length > 0"
style="height: 16px; width: 16px; cursor: pointer"
:style="{
rotate: expandedKeys.includes(node.data.id) ? '0deg' : '-90deg',
}"
>
<svg
width="100%"
height="100%"
viewBox="0 0 20 20"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
d="M14.5632 7.72544L10.539 13.2587C10.2728 13.6247 9.72696 13.6247 9.46073 13.2587L5.43658 7.72544C5.11611 7.28479 5.43088 6.66666 5.97573 6.66666L14.024 6.66666C14.5689 6.66666 14.8837 7.28479 14.5632 7.72544Z"
fill="var(--virt-tree-color-icon)"
></path>
</svg>
</div>
<!-- content -->
<div>
{{ node.title }}
</div>
</div>
</template>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
通常这种形式用在替代 VueDraggable 组件
源码
vue
<script lang="ts" setup>
import { onMounted, ref, shallowRef } from 'vue';
import { VirtTree, type TreeNode } from 'vue-virt-list';
type ItemData = {
id: string | number;
title: string;
children?: ItemData[];
// 禁止拖入
disableDragIn?: boolean;
// 禁止托出
disableDragOut?: boolean;
};
const customFieldNames = {
key: 'id',
};
const list = ref<ItemData[]>([]);
list.value = Array.from({ length: 40 }).map((_, i) => ({
id: i + 1,
title: `Node-${i}`,
disableDragIn: true,
}));
const virtTreeRef = ref<typeof VirtTree>();
const filterMethod = (query: string, node: any) => {
return node.name.includes(query);
};
function onDragstart() {
console.log('onDragstart');
}
function onDragEnd(data: any) {
if (data) {
console.log('drag success', data);
} else {
console.warn('drag fail: Invalid');
}
}
const draggable = ref(true);
const expandedKeys = ref<number[]>([1, 100, 102]);
const toggleExpand = (key: number) => {
virtTreeRef.value?.expandNode(key, !expandedKeys.value.includes(key));
};
function onDragStart(e: MouseEvent | Event) {
if (virtTreeRef.value?.onDragstart) virtTreeRef.value?.onDragstart(e);
}
</script>
<template>
<div class="demo-tree">
<div class="virt-tree-wrapper">
<VirtTree
ref="virtTreeRef"
v-model:expandedKeys="expandedKeys"
:list="list"
:fieldNames="customFieldNames"
:indent="0"
:iconSize="14"
:filterMethod="filterMethod"
:itemGap="4"
@dragstart="onDragstart"
@dragend="onDragEnd"
dragOnly
dragGhostClass="drag-ghost-class"
dragClass="drag-class"
expandOnClickNode
default-expand-all
>
<template #default="{ node }">
<div
:style="{
display: 'flex',
alignItems: 'center',
height: '32px',
}"
@click="toggleExpand(node.data.id)"
>
<!-- drag handler -->
<div
style="cursor: move; margin-right: 8px"
draggable="true"
@dragstart="onDragStart"
>
<svg
width="16"
height="16"
viewBox="0 0 16 16"
fill="none"
xmlns="http://www.w3.org/2000/svg"
>
<path
fill-rule="evenodd"
clip-rule="evenodd"
d="M5 3C5 2.44772 5.44772 2 6 2C6.55228 2 7 2.44772 7 3C7 3.55228 6.55228 4 6 4C5.44772 4 5 3.55228 5 3ZM9 3C9 2.44772 9.44772 2 10 2C10.5523 2 11 2.44772 11 3C11 3.55228 10.5523 4 10 4C9.44772 4 9 3.55228 9 3ZM5 8C5 7.44772 5.44772 7 6 7C6.55228 7 7 7.44772 7 8C7 8.55228 6.55228 9 6 9C5.44772 9 5 8.55228 5 8ZM9 8C9 7.44772 9.44772 7 10 7C10.5523 7 11 7.44772 11 8C11 8.55228 10.5523 9 10 9C9.44772 9 9 8.55228 9 8ZM5 13C5 12.4477 5.44772 12 6 12C6.55228 12 7 12.4477 7 13C7 13.5523 6.55228 14 6 14C5.44772 14 5 13.5523 5 13ZM9 13C9 12.4477 9.44772 12 10 12C10.5523 12 11 12.4477 11 13C11 13.5523 10.5523 14 10 14C9.44772 14 9 13.5523 9 13Z"
fill="var(--virt-tree-color-icon)"
/>
</svg>
</div>
<!-- content -->
<div>
{{ node.title }}
</div>
</div>
</template>
<template #empty>
<div style="padding: 16px">暂无数据</div>
</template>
</VirtTree>
</div>
</div>
</template>
<style scoped lang="scss">
.demo-tree {
width: 100%;
display: flex;
flex-direction: column;
overflow: hidden;
.tree-btn-container {
display: flex;
flex: 1;
flex-direction: row-reverse;
justify-content: space-between;
padding: 12px 8px;
gap: 8px;
.input-label {
font-size: 14px;
}
.btn-item {
padding: 4px 12px;
cursor: pointer;
border: 1px solid #ececec;
border-radius: 4px;
font-size: 14px;
}
.input-container {
display: flex;
gap: 8px;
align-items: center;
input {
height: 100%;
border: 1px solid #ececec;
border-radius: 4px;
padding: 0 8px;
}
}
}
}
</style>
css variable
css
.virt-tree-item {
/* drag line */
--virt-tree-color-drag-line: #4c88ff;
--virt-tree-color-drag-box: rgb(76, 136, 255, 0.1);
--virt-tree-color-drag-line-disabled: rgb(76, 136, 255, 0.4);
/* text */
--virt-tree-color-text: #1f2329;
--virt-tree-color-text-disabled: #a8abb2;
--virt-tree-color-text-selected: #1456f0;
/* node */
--virt-tree-color-node-bg: #fff;
--virt-tree-color-node-bg-hover: #1f232914;
--virt-tree-color-node-bg-disabled: transparent;
--virt-tree-color-node-bg-selected: #f0f4ff;
/* icon */
--virt-tree-color-icon: #2b2f36;
--virt-tree-color-icon-bg-hover: #1f23291a;
/* line */
--virt-tree-line-color: #cacdd1;
/* checkbox */
--virt-tree-color-checkbox-bg: #fff;
--virt-tree-color-checkbox-bg-indeterminate: #1890ff;
--virt-tree-color-checkbox-bg-checked: #1890ff;
--virt-tree-color-checkbox-bg-disabled: rgba(255, 255, 255, 0.3);
--virt-tree-color-checkbox-border: rgb(190, 192, 198);
--virt-tree-color-checkbox-border-checked: #1890ff;
--virt-tree-color-checkbox-border-indeterminate: #1890ff;
/* 生效于图标的margin和拖拽线的左边距离 */
--virt-tree-switcher-icon-margin-right: 4px;
--virt-tree-drag-line-gap: 4px;
}
html.dark .virt-tree-item {
/* drag line */
--virt-tree-color-drag-line: #4c88ff;
--virt-tree-color-drag-box: rgb(76, 136, 255, 0.1);
--virt-tree-color-drag-line-disabled: rgb(76, 136, 255, 0.4);
/* text */
--virt-tree-color-text: #f9f9f9;
--virt-tree-color-text-disabled: rgba(255, 255, 255, 0.3);
--virt-tree-color-text-selected: #4c88ff;
/* node */
--virt-tree-color-node-bg: #1b1b1f;
--virt-tree-color-node-bg-hover: #2e3238;
--virt-tree-color-node-bg-disabled: transparent;
--virt-tree-color-node-bg-selected: #152340;
/* icon */
--virt-tree-color-icon: #f9f9f9;
--virt-tree-color-icon-bg-hover: #ebebeb1a;
/* line */
--virt-tree-line-color: #35393f;
/* checkbox */
--virt-tree-color-checkbox-bg: #fff;
--virt-tree-color-checkbox-bg-indeterminate: #1890ff;
--virt-tree-color-checkbox-bg-checked: #1890ff;
--virt-tree-color-checkbox-bg-disabled: rgba(255, 255, 255, 0.3);
--virt-tree-color-checkbox-border: rgb(190, 192, 198);
--virt-tree-color-checkbox-border-checked: #1890ff;
--virt-tree-color-checkbox-border-indeterminate: #1890ff;
/* 生效于图标的margin和拖拽线的左边距离 */
--virt-tree-switcher-icon-margin-right: 4px;
--virt-tree-drag-line-gap: 4px;
}