Droppable 放置目标
接收来自 CfDraggable 的拖放,按 type 字符串过滤。暴露 isOver / canDrop 给作用域插槽用于视觉反馈。
基础用法
accept 接受单字符串或字符串数组。多个 Droppable 同时覆盖于 pointer 下时,命中的是最上层那一个(用 document.elementsFromPoint 判定)。看板列、收藏区、删除区、上传区都是常见用法。
背景 视口
<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
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
accept | string | string[] | — | 允许的 type;不传则接受所有 |
disabled | boolean | false | 禁用 |
Events
| 事件 | 载荷 | 说明 |
|---|---|---|
drop | (payload, pointer) | 释放时触发,仅 type 匹配时 |
enter | payload | 拖入边界 |
leave | payload | 离开边界或松手 |
默认插槽
{ isOver, canDrop } — isOver 表示 pointer 当前在边界内;canDrop 表示 type 匹配且未禁用,可用于高亮目标。
反馈与讨论
Droppable 放置目标 的讨论