You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

Vue3树形列表组件如何为每个项实现插槽,让使用者自定义内容?

解决Vue递归树形列表组件的插槽自定义问题

我开发了一个树形结构的列表组件,接收对象数组作为数据源,目前固定渲染项的text属性。当前使用方式:

<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a'}]" />

希望升级组件,让使用者通过Vue插槽自由自定义列表项内容,期望使用方式:

<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a', meta: new Date()}]">
  <template v-slot:item="{item}">
     {{ item.text }} <fancy-component v-if="item.meta">{{ item.meta.toISOString()}}</fancy-component>
  </template>
</list-base>

由于组件是递归树形结构,常规插槽方案无法直接生效,以下是针对三个组件的修改方案:


1. 修改list-base.vue

定义插槽并透传给递归子组件:

<script setup>
import ListTree from "./list-tree.vue";

defineProps({
  items: {
    type: Array,
    default: () => [],
  },
  draggable: {
    type: Boolean,
    default: false,
  },
  editable: {
    type: Boolean,
    default: false,
  },
});
defineEmits(["action", "drag", "edit"]);
</script>
<template>
  <div>
    <template v-for="item in items" :key="item.value">
      <list-tree
        :root="item"
        :draggable="draggable"
        :editable="editable"
        @action="$emit('action', $event)"
        @drag="$emit('drag', $event)"
        @edit="$emit('edit', $event)"
        <!-- 透传插槽给递归组件 -->
        v-slots="{ item: $slots.item }"
      />
    </template>
  </div>
</template>

2. 修改list-tree.vue

接收插槽并传递给列表项,递归调用时继续透传插槽:

<script setup>
import { toRef } from "vue";
import { hasElements, ActionsMenu } from "@lib";
import { COLLAPSE_STATUSES, useCollapse } from "./collapse";
import CollapseSwitch from "./collapse-switch.vue";
import ListItem from "./list-item.vue";

const props = defineProps({
  root: {
    type: Object,
    default: () => ({}),
  },
  draggable: {
    type: Boolean,
    default: false,
  },
  editable: {
    type: Boolean,
    default: false,
  },
  // 新增:接收插槽属性
  slots: {
    type: Object,
    default: () => ({})
  }
});
defineEmits(["action", "drag", "edit"]);

const { collapse, handleStart, handleEnd } = useCollapse(toRef(props, "root"));
</script>
<template>
  <div class="list-tree">
    <div class="list-root-wrapper">
      <collapse-switch v-model="collapse" />
      <list-item
        :item="root"
        :draggable="draggable"
        :editable="editable"
        @drag-start="handleStart"
        @drag-end="handleEnd"
        @action="$emit('action', $event)"
        @drag="$emit('drag', $event)"
        @edit="$emit('edit', $event)"
      >
        <!-- 将插槽传递给list-item -->
        <template v-if="slots.item" v-slot:item="{ item }">
          <component :is="slots.item" :item="item" />
        </template>
      </list-item>
      <template v-if="hasElements(root.actions)">
        <actions-menu
          :actions="root.actions"
          @action="$emit('action', { item: root.value, action: $event })"
        />
      </template>
    </div>
    <template v-if="collapse === COLLAPSE_STATUSES.OPEN">
      <div class="list-tree-children">
        <template v-for="child in root.children" :key="child.value">
          <list-tree
            :root="child"
            :draggable="draggable"
            :editable="editable"
            @action="$emit('action', $event)"
            @drag="$emit('drag', $event)"
            @edit="$emit('edit', $event)"
            <!-- 递归时继续透传插槽 -->
            :slots="slots"
          />
        </template>
      </div>
    </template>
  </div>
</template>
<style scoped>
.list-root-wrapper {
  display: flex;
  align-items: center;
}

.list-tree-children {
  padding-inline-start: var(--size-20);
}
</style>

3. 修改list-item.vue

添加插槽支持,保留默认文本作为降级方案:

<script setup>
import { computed } from "vue";
import { TooltipBase } from "../tooltip-base";
import { useDrag } from "./use-drag";

const props = defineProps({
  item: {
    type: Object,
    default: () => ({}),
  },
  draggable: {
    type: Boolean,
    default: false,
  },
  editable: {
    type: Boolean,
    default: false,
  },
});
const emit = defineEmits(["drag", "drag-start", "drag-end", "edit"]);

const { handlers, dragClasses } = useDrag({ value: props.item.value, emit });

const classes = computed(() => ({
  ...dragClasses,
  inactive: props.item.inactive,
}));

const handleBlur = (e) => {
  const text = e.target.textContent;
  if (text === props.item.text) return;
  emit("edit", { item: props.item.value, text });
};
const handleEnter = (e) => e.target.blur();
</script>
<template>
  <div
    v-bind="$attrs"
    :id="item.value"
    class="list-item"
    :class="classes"
    :draggable="draggable"
    :contenteditable="editable ? 'plaintext-only' : 'false'"
    @dragstart="handlers.start"
    @dragenter="handlers.enter"
    @dragleave="handlers.leave"
    @dragover="handlers.over"
    @drop="handlers.drop"
    @dragend="handlers.end"
    @blur="handleBlur"
    @keydown.enter.prevent="handleEnter"
  >
    <!-- 优先渲染自定义插槽,无插槽时显示默认文本 -->
    <slot name="item" :item="item">
      {{ item.text }}
    </slot>
  </div>
  <tooltip-base v-if="item.tooltip" :anchor="item.value" :text="item.tooltip" />
</template>
<style scoped>
.list-item {
  --list-item-drag-border-color: var(--color-neutral-40);
}

.list-item {
  display: flex;
  flex-grow: 1;
  padding-block: var(--size-15);
  padding-inline-start: var(--size-10);
  border: var(--border-size-20) solid transparent;
}

.list-item.inactive {
  color: var(--color-neutral-40);
  text-decoration: line-through;
}

.list-item:hover,
.list-item:focus-within,
.list-item.over {
  background-color: var(--background-color-highlight);
}

.list-item.over.top {
  border-top-color: var(--list-item-drag-border-color);
}

.list-item.over.middle {
  border-color: var(--list-item-drag-border-color);
}

.list-item.over.bottom {
  border-bottom-color: var(--list-item-drag-border-color);
}
</style>

验证使用方式

修改完成后,即可按照预期方式自定义列表项内容:

<list-base :items="[{id: 'a', text: 'a'}, {id: 'b', text: 'b', parentId: 'a', meta: new Date()}]">
  <template v-slot:item="{item}">
     {{ item.text }} <fancy-component v-if="item.meta">{{ item.meta.toISOString()}}</fancy-component>
  </template>
</list-base>

核心原理

递归组件无法直接访问根组件插槽,需通过属性透传插槽的方式,从list-base逐层传递到每一层list-tree和list-item;同时在list-item中设置默认内容,保证组件在未使用自定义插槽时仍能正常工作。

内容的提问来源于stack exchange,提问作者João Melo

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.07.03 04:39:52