Skip to content

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>
源码
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;
}