Draggable 可拖拽容器
把任意内容包成拖拽源,配合 CfDroppable 实现跨区域拖放。payload 自带类型与数据,模块单例 store 协调 Draggable / Droppable / DragLayer 状态。
基础用法
CfDraggable 把 children 包成拖拽源;CfDroppable 是放置区。type 字符串用于匹配 — Droppable accept 不匹配时该组合不接受 drop。整套基于 Pointer Events,触屏 / 笔 / 鼠标统一行为,不依赖 HTML5 DnD API。
背景 视口
<script setup lang="ts">
import { ref } from 'vue';
import { CfDraggable, CfDroppable, type DragPayload } from '@chufix-design/vue';
const bin = ref<string[]>([]);
const log = ref('拖动卡片到放置区试试。');
const cards = [
{ id: 'cpu', label: 'CPU 使用率' },
{ id: 'mem', label: '内存占用' },
{ id: 'net', label: '出向流量' },
{ id: 'lat', label: 'P99 延迟' },
];
function onDrop(payload: DragPayload) {
const data = payload.data as { id: string; label: string };
if (bin.value.includes(data.id)) return;
bin.value.push(data.id);
log.value = `drop: ${data.label} (id=${data.id})`;
}
</script>
<template>
<div class="dd-demo">
<div class="dd-demo__pool">
<CfDraggable
v-for="c in cards"
:key="c.id"
type="metric"
:data="c"
class="dd-card"
>
{{ c.label }}
</CfDraggable>
</div>
<CfDroppable accept="metric" @drop="onDrop">
<template #default="{ isOver, canDrop }">
<div class="dd-zone" :class="{ 'is-over': isOver, 'is-accept': canDrop }">
<strong>仪表盘</strong>
<span v-if="bin.length === 0">拖卡片进来</span>
<ul v-else>
<li v-for="id in bin" :key="id">{{ id }}</li>
</ul>
</div>
</template>
</CfDroppable>
<code class="dd-demo__log">{{ log }}</code>
</div>
</template>
<style scoped>
.dd-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
width: 100%;
}
.dd-demo__pool {
display: flex;
flex-direction: column;
gap: 8px;
}
.dd-card {
padding: 10px 12px;
background: var(--bg-2);
border: 1px solid var(--line-1);
border-radius: var(--r-3);
color: var(--fg-1);
font-size: var(--t-13);
}
.dd-zone {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px;
height: 100%;
min-height: 160px;
color: var(--fg-2);
font-size: var(--t-12);
}
.dd-zone strong {
color: var(--fg-1);
font-size: var(--t-13);
}
.dd-zone ul {
margin: 0;
padding-left: 18px;
}
.dd-demo__log {
grid-column: 1 / -1;
padding: 6px 10px;
background: var(--bg-inset);
border-radius: var(--r-2);
font-size: var(--t-12);
}
</style> <script setup>
import { ref } from 'vue';
import { CfDraggable, CfDroppable } from '@chufix-design/vue';
const bin = ref<string[]>([]);
const log = ref('拖动卡片到放置区试试。');
const cards = [
{ id: 'cpu', label: 'CPU 使用率' },
{ id: 'mem', label: '内存占用' },
{ id: 'net', label: '出向流量' },
{ id: 'lat', label: 'P99 延迟' },
];
function onDrop(payload) {
const data = payload.data as { id: string; label: string };
if (bin.value.includes(data.id)) return;
bin.value.push(data.id);
log.value = `drop: ${data.label} (id=${data.id})`;
}
</script>
<template>
<div class="dd-demo">
<div class="dd-demo__pool">
<CfDraggable
v-for="c in cards"
:key="c.id"
type="metric"
:data="c"
class="dd-card"
>
{{ c.label }}
</CfDraggable>
</div>
<CfDroppable accept="metric" @drop="onDrop">
<template #default="{ isOver, canDrop }">
<div class="dd-zone" :class="{ 'is-over': isOver, 'is-accept': canDrop }">
<strong>仪表盘</strong>
<span v-if="bin.length === 0">拖卡片进来</span>
<ul v-else>
<li v-for="id in bin" :key="id">{{ id }}</li>
</ul>
</div>
</template>
</CfDroppable>
<code class="dd-demo__log">{{ log }}</code>
</div>
</template>
<style scoped>
.dd-demo {
display: grid;
grid-template-columns: 1fr 1fr;
gap: 12px;
width: 100%;
}
.dd-demo__pool {
display: flex;
flex-direction: column;
gap: 8px;
}
.dd-card {
padding: 10px 12px;
background: var(--bg-2);
border: 1px solid var(--line-1);
border-radius: var(--r-3);
color: var(--fg-1);
font-size: var(--t-13);
}
.dd-zone {
display: flex;
flex-direction: column;
gap: 6px;
padding: 14px;
height: 100%;
min-height: 160px;
color: var(--fg-2);
font-size: var(--t-12);
}
.dd-zone strong {
color: var(--fg-1);
font-size: var(--t-13);
}
.dd-zone ul {
margin: 0;
padding-left: 18px;
}
.dd-demo__log {
grid-column: 1 / -1;
padding: 6px 10px;
background: var(--bg-inset);
border-radius: var(--r-2);
font-size: var(--t-12);
}
</style> import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';
export default function Demo() {
const [bin, setBin] = useState<string[]>([]);
const [log, setLog] = useState('拖动卡片到放置区试试。');
const cards = [
{ id: 'cpu', label: 'CPU 使用率' },
{ id: 'mem', label: '内存占用' },
{ id: 'net', label: '出向流量' },
{ id: 'lat', label: 'P99 延迟' },
];
function onDrop(payload: DragPayload) {
const data = payload.data as { id: string; label: string };
if (bin.includes(data.id)) return;
bin.push(data.id);
setLog(`drop: ${data.label} (id=${data.id})`);
}
return (
<>
<div className="dd-demo">
<div className="dd-demo__pool">
<CfDraggable v-for="c in cards" key={c.id} type="metric" data={c} className="dd-card" >
{c.label}
</CfDraggable>
</div>
<CfDroppable accept="metric" onDrop={onDrop}>
<div className="dd-zone" className={{ 'is-over': isOver, 'is-accept': canDrop }}>
<strong>仪表盘</strong>
<span v-if="bin.length === 0">拖卡片进来</span>
<ul v-else>
<li v-for="id in bin" key={id}>{id}</li>
</ul>
</div>
</>
);
} import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';
export default function Demo() {
const [bin, setBin] = useState<string[]>([]);
const [log, setLog] = useState('拖动卡片到放置区试试。');
const cards = [
{ id: 'cpu', label: 'CPU 使用率' },
{ id: 'mem', label: '内存占用' },
{ id: 'net', label: '出向流量' },
{ id: 'lat', label: 'P99 延迟' },
];
function onDrop(payload) {
const data = payload.data as { id: string; label: string };
if (bin.includes(data.id)) return;
bin.push(data.id);
setLog(`drop: ${data.label} (id=${data.id})`);
}
return (
<>
<div className="dd-demo">
<div className="dd-demo__pool">
<CfDraggable v-for="c in cards" key={c.id} type="metric" data={c} className="dd-card" >
{c.label}
</CfDraggable>
</div>
<CfDroppable accept="metric" onDrop={onDrop}>
<div className="dd-zone" className={{ 'is-over': isOver, 'is-accept': canDrop }}>
<strong>仪表盘</strong>
<span v-if="bin.length === 0">拖卡片进来</span>
<ul v-else>
<li v-for="id in bin" key={id}>{id}</li>
</ul>
</div>
</>
);
} 模块单例 store · ⚠ 必须 barrel import
CfDraggable / CfDroppable / CfDragLayer 共享一个 module-level store(dndStore)。如果用 deep import(from '@chufix-design/vue/src/...')会拿到第二份 store 实例,导致 drop 静默失败。总是用 barrel:
// ✅ 正确
import { CfDraggable, CfDroppable, CfDragLayer } from '@chufix-design/vue';
// ❌ 错误 — 第二份 store
import CfDraggable from '@chufix-design/vue/src/draggable/Draggable.vue';
API
Props
| 属性 | 类型 | 默认 | 说明 |
|---|---|---|---|
type | string | 'default' | 拖拽类型,用于匹配 Droppable.accept |
data | unknown | — | 任意数据载荷 |
payload | { type, data } | — | 整体 payload(覆盖 type/data) |
handle | string | — | 限制启动拖拽的子元素选择器 |
preview | 'self' | 'ghost' | 'none' | 'ghost' | 源元素拖拽时的视觉:原样 / 半透明 / 隐藏(搭配 DragLayer) |
disabled | boolean | false | 禁用 |
Events
| 事件 | 载荷 | 说明 |
|---|---|---|
drag-start | — | 越过 threshold 后 |
drag-end | dropped: boolean | 是否落在合法 Droppable |
反馈与讨论
Draggable 可拖拽容器 的讨论