Preview Updated 2026-05-10

DragLayer 拖拽预览层

Teleport 到 body 的浮层,跟随 pointer。订阅 useDragDrop 全局 store,配合 CfDraggable preview="none" 实现自定义 ghost。

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

基础用法

放一个 <CfDragLayer> 在页面任意位置(推荐根布局或路由顶层)。它 Teleport 到 body,position: fixed,按 pointer 位置实时更新。默认插槽参数:{ payload, over, canDrop }

CfDraggablepreview="none" 打开后,源元素拖动时不变形,所有视觉都跑在 DragLayer 上。

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

interface FileLike {
  id: string;
  name: string;
  size: string;
}

const files: FileLike[] = [
  { id: '1', name: 'design-spec.md', size: '12 KB' },
  { id: '2', name: 'dashboard.png', size: '1.4 MB' },
  { id: '3', name: 'oncall-handoff.pdf', size: '342 KB' },
];

const droppedIds = ref<string[]>([]);

function onDrop(payload: DragPayload) {
  const f = payload.data as FileLike;
  if (!droppedIds.value.includes(f.id)) droppedIds.value.push(f.id);
}
</script>
<template>
  <div class="dl-demo">
    <div class="dl-files">
      <CfDraggable
        v-for="f in files"
        :key="f.id"
        type="file"
        :data="f"
        preview="none"
        class="dl-file"
      >
        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
          <path d="M3 1.5h6L13 5.5V14.5H3z" fill="none" stroke="currentColor" stroke-width="1.2" />
          <path d="M9 1.5V5.5H13" fill="none" stroke="currentColor" stroke-width="1.2" />
        </svg>
        <span>{{ f.name }}</span>
        <em>{{ f.size }}</em>
      </CfDraggable>
    </div>
    <CfDroppable accept="file" @drop="onDrop">
      <template #default="{ isOver }">
        <div class="dl-target" :class="{ 'is-over': isOver }">
          <strong>上传到云端</strong>
          <span v-if="droppedIds.length === 0">拖文件到此处</span>
          <ul v-else>
            <li v-for="id in droppedIds" :key="id">已上传 {{ files.find((x) => x.id === id)?.name }}</li>
          </ul>
        </div>
      </template>
    </CfDroppable>
    <CfDragLayer>
      <template #default="{ payload, canDrop }">
        <div class="dl-preview" :class="{ 'is-accepting': canDrop }">
          <strong>{{ (payload.data as FileLike).name }}</strong>
          <span>{{ canDrop ? '放下以上传' : '拖拽中…' }}</span>
        </div>
      </template>
    </CfDragLayer>
  </div>
</template>
<style scoped>
.dl-demo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.dl-files {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.dl-file {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dl-file svg {
  color: var(--accent-1);
  flex-shrink: 0;
}
.dl-file span {
  flex: 1;
}
.dl-file em {
  color: var(--fg-3);
  font-style: normal;
  font-size: var(--t-12);
}
.dl-target {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  min-height: 180px;
  color: var(--fg-2);
  font-size: var(--t-12);
}
.dl-target strong {
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dl-target ul {
  margin: 0;
  padding-left: 18px;
}
.dl-preview {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-3);
  color: var(--fg-1);
  font-size: var(--t-12);
  min-width: 160px;
}
.dl-preview strong {
  font-size: var(--t-13);
}
.dl-preview span {
  color: var(--fg-3);
}
.dl-preview.is-accepting {
  border-color: var(--accent-1);
  background: var(--accent-soft);
}
.dl-preview.is-accepting span {
  color: var(--accent-1);
}
</style>
<script setup>
import { ref } from 'vue';
import {
  CfDraggable,
  CfDroppable,
  CfDragLayer,
} from '@chufix-design/vue';

const files= [
  { id: '1', name: 'design-spec.md', size: '12 KB' },
  { id: '2', name: 'dashboard.png', size: '1.4 MB' },
  { id: '3', name: 'oncall-handoff.pdf', size: '342 KB' },
];

const droppedIds = ref<string[]>([]);

function onDrop(payload) {
  const f = payload.data;
  if (!droppedIds.value.includes(f.id)) droppedIds.value.push(f.id);
}
</script>
<template>
  <div class="dl-demo">
    <div class="dl-files">
      <CfDraggable
        v-for="f in files"
        :key="f.id"
        type="file"
        :data="f"
        preview="none"
        class="dl-file"
      >
        <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
          <path d="M3 1.5h6L13 5.5V14.5H3z" fill="none" stroke="currentColor" stroke-width="1.2" />
          <path d="M9 1.5V5.5H13" fill="none" stroke="currentColor" stroke-width="1.2" />
        </svg>
        <span>{{ f.name }}</span>
        <em>{{ f.size }}</em>
      </CfDraggable>
    </div>
    <CfDroppable accept="file" @drop="onDrop">
      <template #default="{ isOver }">
        <div class="dl-target" :class="{ 'is-over': isOver }">
          <strong>上传到云端</strong>
          <span v-if="droppedIds.length === 0">拖文件到此处</span>
          <ul v-else>
            <li v-for="id in droppedIds" :key="id">已上传 {{ files.find((x) => x.id === id)?.name }}</li>
          </ul>
        </div>
      </template>
    </CfDroppable>
    <CfDragLayer>
      <template #default="{ payload, canDrop }">
        <div class="dl-preview" :class="{ 'is-accepting': canDrop }">
          <strong>{{ (payload.data).name }}</strong>
          <span>{{ canDrop ? '放下以上传' : '拖拽中…' }}</span>
        </div>
      </template>
    </CfDragLayer>
  </div>
</template>
<style scoped>
.dl-demo {
  display: grid;
  grid-template-columns: 1fr 1fr;
  gap: 12px;
}
.dl-files {
  display: flex;
  flex-direction: column;
  gap: 8px;
}
.dl-file {
  display: flex;
  align-items: center;
  gap: 8px;
  padding: 8px 12px;
  background: var(--bg-1);
  border: 1px solid var(--line-1);
  border-radius: var(--r-2);
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dl-file svg {
  color: var(--accent-1);
  flex-shrink: 0;
}
.dl-file span {
  flex: 1;
}
.dl-file em {
  color: var(--fg-3);
  font-style: normal;
  font-size: var(--t-12);
}
.dl-target {
  display: flex;
  flex-direction: column;
  gap: 6px;
  padding: 14px;
  min-height: 180px;
  color: var(--fg-2);
  font-size: var(--t-12);
}
.dl-target strong {
  color: var(--fg-1);
  font-size: var(--t-13);
}
.dl-target ul {
  margin: 0;
  padding-left: 18px;
}
.dl-preview {
  display: flex;
  flex-direction: column;
  gap: 2px;
  padding: 8px 12px;
  background: var(--bg-2);
  border: 1px solid var(--line-2);
  border-radius: var(--r-3);
  box-shadow: var(--shadow-3);
  color: var(--fg-1);
  font-size: var(--t-12);
  min-width: 160px;
}
.dl-preview strong {
  font-size: var(--t-13);
}
.dl-preview span {
  color: var(--fg-3);
}
.dl-preview.is-accepting {
  border-color: var(--accent-1);
  background: var(--accent-soft);
}
.dl-preview.is-accepting span {
  color: var(--accent-1);
}
</style>
import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';

export default function Demo() {
  interface FileLike {
    id: string;
    name: string;
    size: string;
  }

  const files: FileLike[] = [
    { id: '1', name: 'design-spec.md', size: '12 KB' },
    { id: '2', name: 'dashboard.png', size: '1.4 MB' },
    { id: '3', name: 'oncall-handoff.pdf', size: '342 KB' },
  ];

  const [droppedIds, setDroppedIds] = useState<string[]>([]);

  function onDrop(payload: DragPayload) {
    const f = payload.data as FileLike;
    if (!droppedIds.includes(f.id)) droppedIds.push(f.id);
  }
  return (
    <>
      <div className="dl-demo">
          <div className="dl-files">
            <CfDraggable v-for="f in files" key={f.id} type="file" data={f} preview="none" className="dl-file" >
              <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                <path d="M3 1.5h6L13 5.5V14.5H3z" fill="none" stroke="currentColor" stroke-width="1.2" />
                <path d="M9 1.5V5.5H13" fill="none" stroke="currentColor" stroke-width="1.2" />
              </svg>
              <span>{f.name}</span>
              <em>{f.size}</em>
            </CfDraggable>
          </div>
          <CfDroppable accept="file" onDrop={onDrop}>
              <div className="dl-target" className={{ 'is-over': isOver }}>
                <strong>上传到云端</strong>
                <span v-if="droppedIds.length === 0">拖文件到此处</span>
                <ul v-else>
                  <li v-for="id in droppedIds" key={id}>已上传 {files.find((x) => x.id === id)?.name}</li>
                </ul>
              </div>
    </>
  );
}
import { useState } from 'react';
import { CfDraggable, CfDroppable } from '@chufix-design/react';

export default function Demo() {

  const files= [
    { id: '1', name: 'design-spec.md', size: '12 KB' },
    { id: '2', name: 'dashboard.png', size: '1.4 MB' },
    { id: '3', name: 'oncall-handoff.pdf', size: '342 KB' },
  ];

  const [droppedIds, setDroppedIds] = useState<string[]>([]);

  function onDrop(payload) {
    const f = payload.data;
    if (!droppedIds.includes(f.id)) droppedIds.push(f.id);
  }
  return (
    <>
      <div className="dl-demo">
          <div className="dl-files">
            <CfDraggable v-for="f in files" key={f.id} type="file" data={f} preview="none" className="dl-file" >
              <svg viewBox="0 0 16 16" width="14" height="14" aria-hidden="true">
                <path d="M3 1.5h6L13 5.5V14.5H3z" fill="none" stroke="currentColor" stroke-width="1.2" />
                <path d="M9 1.5V5.5H13" fill="none" stroke="currentColor" stroke-width="1.2" />
              </svg>
              <span>{f.name}</span>
              <em>{f.size}</em>
            </CfDraggable>
          </div>
          <CfDroppable accept="file" onDrop={onDrop}>
              <div className="dl-target" className={{ 'is-over': isOver }}>
                <strong>上传到云端</strong>
                <span v-if="droppedIds.length === 0">拖文件到此处</span>
                <ul v-else>
                  <li v-for="id in droppedIds" key={id}>已上传 {files.find((x) => x.id === id)?.name}</li>
                </ul>
              </div>
    </>
  );
}

Teleport 注意事项

CfDragLayer 内部用 <Teleport to="body"> (Vue) / createPortal(document.body) (React)。在 Astro 中嵌入 demo 时必须client:only="vue",不能用 client:load,否则 SSR/hydrate 时机不一致会导致 layer 不出现(同 Tooltip / Modal / Toaster)。

API

Props

属性类型默认说明
offsetXnumber12预览相对 pointer 的水平偏移 (px)
offsetYnumber12预览相对 pointer 的垂直偏移 (px)

默认插槽

{ payload, over, canDrop }

  • payload: { type, data } — 当前拖拽的数据
  • over: HTMLElement \| null — pointer 下当前的 Droppable
  • canDrop: boolean — 该 over 是否接受这个 type

不传插槽时渲染默认小 chip 显示 type。

反馈与讨论

DragLayer 拖拽预览层 · Discussion

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