开发预览 更新于 2026-05-10

Droppable 放置目标

接收来自 CfDraggable 的拖放,按 type 字符串过滤。暴露 isOver / canDrop 给作用域插槽用于视觉反馈。

基础用法

accept 接受单字符串或字符串数组。多个 Droppable 同时覆盖于 pointer 下时,命中的是最上层那一个(用 document.elementsFromPoint 判定)。看板列、收藏区、删除区、上传区都是常见用法。

背景 视口
src/App.vue
<script setup lang="ts">
import { ref } from 'vue';
import { CfDraggable, CfDroppable, type DragPayload } from '@chufix-design/vue';

interface Bucket {
  id: string;
  label: string;
  tone: 'info' | 'warning' | 'error';
  items: string[];
}

const buckets = ref<Bucket[]>([
  { id: 'todo', label: 'To do', tone: 'info', items: ['登录鉴权'] },
  { id: 'doing', label: 'Doing', tone: 'warning', items: ['仪表盘 P99'] },
  { id: 'done', label: 'Done', tone: 'error', items: [] },
]);

function moveTask(payload: DragPayload, targetId: string) {
  const data = payload.data as { from: string; task: string };
  if (data.from === targetId) return;
  buckets.value = buckets.value.map((b) => {
    if (b.id === data.from) return { ...b, items: b.items.filter((t) => t !== data.task) };
    if (b.id === targetId) return { ...b, items: [...b.items, data.task] };
    return b;
  });
}
</script>
<template>
  <div class="dz-demo">
    <CfDroppable
      v-for="b in buckets"
      :key="b.id"
      accept="task"
      @drop="(p) => moveTask(p, b.id)"
    >
      <template #default="{ isOver }">
        <div class="dz-bucket" :class="`dz-bucket--${b.tone}`" :data-over="isOver ? '1' : '0'">
          <header>
            <strong>{{ b.label }}</strong>
            <span>{{ b.items.length }}</span>
          </header>
          <CfDraggable
            v-for="task in b.items"
            :key="task"
            type="task"
            :data="{ from: b.id, task }"
            class="dz-task"
          >
            {{ task }}
          </CfDraggable>
          <p v-if="b.items.length === 0" class="dz-empty">拖到这里</p>
        </div>
      </template>
    </CfDroppable>
  </div>
</template>
<style scoped>
.dz-demo {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  width: 100%;
}
.dz-bucket {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  min-height: 200px;
  background: var(--bg-1);
  border: 1px dashed var(--line-2);
  border-radius: var(--r-3);
}
.dz-bucket header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--fg-1);
  font-size: var(--t-12);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.dz-bucket header span {
  color: var(--fg-3);
}
.dz-task {
  padding: 8px 10px;
  background: var(--bg-2);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dz-empty {
  margin: 0;
  color: var(--fg-3);
  font-size: var(--t-12);
}
</style>
<script setup>
import { ref } from 'vue';
import { CfDraggable, CfDroppable } from '@chufix-design/vue';

const buckets = ref<Bucket[]>([
  { id: 'todo', label: 'To do', tone: 'info', items: ['登录鉴权'] },
  { id: 'doing', label: 'Doing', tone: 'warning', items: ['仪表盘 P99'] },
  { id: 'done', label: 'Done', tone: 'error', items: [] },
]);

function moveTask(payload, targetId) {
  const data = payload.data as { from: string; task: string };
  if (data.from === targetId) return;
  buckets.value = buckets.value.map((b) => {
    if (b.id === data.from) return { ...b, items: b.items.filter((t) => t !== data.task) };
    if (b.id === targetId) return { ...b, items: [...b.items, data.task] };
    return b;
  });
}
</script>
<template>
  <div class="dz-demo">
    <CfDroppable
      v-for="b in buckets"
      :key="b.id"
      accept="task"
      @drop="(p) => moveTask(p, b.id)"
    >
      <template #default="{ isOver }">
        <div class="dz-bucket" :class="`dz-bucket--${b.tone}`" :data-over="isOver ? '1' : '0'">
          <header>
            <strong>{{ b.label }}</strong>
            <span>{{ b.items.length }}</span>
          </header>
          <CfDraggable
            v-for="task in b.items"
            :key="task"
            type="task"
            :data="{ from: b.id, task }"
            class="dz-task"
          >
            {{ task }}
          </CfDraggable>
          <p v-if="b.items.length === 0" class="dz-empty">拖到这里</p>
        </div>
      </template>
    </CfDroppable>
  </div>
</template>
<style scoped>
.dz-demo {
  display: grid;
  grid-template-columns: repeat(3, 1fr);
  gap: 12px;
  width: 100%;
}
.dz-bucket {
  display: flex;
  flex-direction: column;
  gap: 8px;
  padding: 12px;
  min-height: 200px;
  background: var(--bg-1);
  border: 1px dashed var(--line-2);
  border-radius: var(--r-3);
}
.dz-bucket header {
  display: flex;
  justify-content: space-between;
  align-items: center;
  color: var(--fg-1);
  font-size: var(--t-12);
  text-transform: uppercase;
  letter-spacing: 0.04em;
}
.dz-bucket header span {
  color: var(--fg-3);
}
.dz-task {
  padding: 8px 10px;
  background: var(--bg-2);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dz-empty {
  margin: 0;
  color: var(--fg-3);
  font-size: var(--t-12);
}
</style>
import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';

export default function Demo() {
  interface Bucket {
    id: string;
    label: string;
    tone: 'info' | 'warning' | 'error';
    items: string[];
  }

  const [buckets, setBuckets] = useState<Bucket[]>([
    { id: 'todo', label: 'To do', tone: 'info', items: ['登录鉴权'] },
    { id: 'doing', label: 'Doing', tone: 'warning', items: ['仪表盘 P99'] },
    { id: 'done', label: 'Done', tone: 'error', items: [] },
  ]);

  function moveTask(payload: DragPayload, targetId: string) {
    const data = payload.data as { from: string; task: string };
    if (data.from === targetId) return;
    setBuckets(buckets.map((b) => {
      if (b.id === data.from) return { ...b, items: b.items.filter((t) => t !== data.task) });
      if (b.id === targetId) return { ...b, items: [...b.items, data.task] };
      return b;
    });
  }
  return (
    <>
      <div className="dz-demo">
          <CfDroppable v-for="b in buckets" key={b.id} accept="task" onDrop={(p) => moveTask(p, b.id)}
          >
              <div className="dz-bucket" className={`dz-bucket--${b.tone}`} dataOver={isOver ? '1' : '0'}>
                <header>
                  <strong>{b.label}</strong>
                  <span>{b.items.length}</span>
                </header>
                <CfDraggable v-for="task in b.items" key={task} type="task" data={{ from: b.id, task }} className="dz-task" >
                  {task}
                </CfDraggable>
                <p v-if="b.items.length === 0" className="dz-empty">拖到这里</p>
              </div>
    </>
  );
}
import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';

export default function Demo() {

  const [buckets, setBuckets] = useState<Bucket[]>([
    { id: 'todo', label: 'To do', tone: 'info', items: ['登录鉴权'] },
    { id: 'doing', label: 'Doing', tone: 'warning', items: ['仪表盘 P99'] },
    { id: 'done', label: 'Done', tone: 'error', items: [] },
  ]);

  function moveTask(payload, targetId) {
    const data = payload.data as { from: string; task: string };
    if (data.from === targetId) return;
    setBuckets(buckets.map((b) => {
      if (b.id === data.from) return { ...b, items: b.items.filter((t) => t !== data.task) });
      if (b.id === targetId) return { ...b, items: [...b.items, data.task] };
      return b;
    });
  }
  return (
    <>
      <div className="dz-demo">
          <CfDroppable v-for="b in buckets" key={b.id} accept="task" onDrop={(p) => moveTask(p, b.id)}
          >
              <div className="dz-bucket" className={`dz-bucket--${b.tone}`} dataOver={isOver ? '1' : '0'}>
                <header>
                  <strong>{b.label}</strong>
                  <span>{b.items.length}</span>
                </header>
                <CfDraggable v-for="task in b.items" key={task} type="task" data={{ from: b.id, task }} className="dz-task" >
                  {task}
                </CfDraggable>
                <p v-if="b.items.length === 0" className="dz-empty">拖到这里</p>
              </div>
    </>
  );
}

API

Props

属性类型默认说明
acceptstring | string[]允许的 type;不传则接受所有
disabledbooleanfalse禁用

Events

事件载荷说明
drop(payload, pointer)释放时触发,仅 type 匹配时
enterpayload拖入边界
leavepayload离开边界或松手

默认插槽

{ isOver, canDrop }isOver 表示 pointer 当前在边界内;canDrop 表示 type 匹配且未禁用,可用于高亮目标。

反馈与讨论

Droppable 放置目标 的讨论

0
0 / 600
正在加载评论...