Preview Updated 2026-05-10

Draggable 可拖拽容器

把任意内容包成拖拽源,配合 CfDroppable 实现跨区域拖放。payload 自带类型与数据,模块单例 store 协调 Draggable / Droppable / DragLayer 状态。

English translation pending This page hasn't been translated yet — falling back to Chinese. PRs welcome on GitHub.

基础用法

CfDraggable 把 children 包成拖拽源;CfDroppable 是放置区。type 字符串用于匹配 — Droppable accept 不匹配时该组合不接受 drop。整套基于 Pointer Events,触屏 / 笔 / 鼠标统一行为,不依赖 HTML5 DnD API。

背景 视口
src/App.vue
<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

属性类型默认说明
typestring'default'拖拽类型,用于匹配 Droppable.accept
dataunknown任意数据载荷
payload{ type, data }整体 payload(覆盖 type/data)
handlestring限制启动拖拽的子元素选择器
preview'self' | 'ghost' | 'none''ghost'源元素拖拽时的视觉:原样 / 半透明 / 隐藏(搭配 DragLayer)
disabledbooleanfalse禁用

Events

事件载荷说明
drag-start越过 threshold 后
drag-enddropped: boolean是否落在合法 Droppable

反馈与讨论

Draggable 可拖拽容器 · Discussion

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