Dom-Draggable.page.vue
This commit is contained in:
400
src/pages/Page/Dom-Draggable.page.vue
Normal file
400
src/pages/Page/Dom-Draggable.page.vue
Normal file
@ -0,0 +1,400 @@
|
||||
<script setup lang="ts">
|
||||
interface ComponentItem {
|
||||
id: number | string;
|
||||
name: string;
|
||||
}
|
||||
|
||||
const 组件列表 = ref<ComponentItem[]>([
|
||||
{ id: 1, name: '组件1' },
|
||||
{ id: 2, name: '组件2' },
|
||||
{ id: '3', name: '组件3' },
|
||||
{ id: 4, name: '组件4' },
|
||||
{ id: 5, name: '组件5' },
|
||||
]);
|
||||
|
||||
const 流程起点列表 = ref<ComponentItem[]>([]);
|
||||
const 流程终点列表 = ref<ComponentItem[]>([]);
|
||||
|
||||
// 用于存储当前拖拽的组件 ID (类型与ComponentItem.id保持一致)
|
||||
const draggingItemId = ref<ComponentItem['id'] | null>(null);
|
||||
|
||||
function getItemIndex(item: ComponentItem): number {
|
||||
return 组件列表.value.findIndex((comp) => comp.id === item.id);
|
||||
}
|
||||
|
||||
function getItemById(id: number | string): ComponentItem | undefined {
|
||||
return 组件列表.value.find((comp) => comp.id === id);
|
||||
}
|
||||
|
||||
// ===== 业务逻辑 =====
|
||||
/**
|
||||
* 添加元素到起点列表
|
||||
*/
|
||||
function addToStartList(item: ComponentItem): boolean {
|
||||
const newStartIndex = getItemIndex(item);
|
||||
const endIndex = 流程终点列表.value.length > 0 ? getItemIndex(流程终点列表.value[0]) : -1;
|
||||
|
||||
// 约束检查:如果终点已存在,则新起点必须在终点之前
|
||||
if (endIndex !== -1 && newStartIndex >= endIndex) {
|
||||
console.debug('⚠️ 流程起点必须在流程终点之前');
|
||||
return false; // 表示添加失败
|
||||
}
|
||||
|
||||
// 更新数据模型
|
||||
if (流程起点列表.value.length > 0) {
|
||||
流程起点列表.value.splice(0, 1, item); // 替换
|
||||
} else {
|
||||
流程起点列表.value.push(item); // 添加
|
||||
}
|
||||
return true; // 表示添加成功
|
||||
}
|
||||
|
||||
/**
|
||||
* 添加元素到终点列表
|
||||
*/
|
||||
function addToEndList(item: ComponentItem): boolean {
|
||||
const newEndIndex = getItemIndex(item);
|
||||
const startIndex = 流程起点列表.value.length > 0 ? getItemIndex(流程起点列表.value[0]) : -1;
|
||||
|
||||
// 约束检查:如果起点已存在,则新终点必须在起点之后
|
||||
if (startIndex !== -1 && newEndIndex <= startIndex) {
|
||||
console.debug('⚠️ 流程终点必须在流程起点之后');
|
||||
return false; // 表示添加失败
|
||||
}
|
||||
|
||||
// 更新数据模型
|
||||
if (流程终点列表.value.length > 0) {
|
||||
流程终点列表.value.splice(0, 1, item); // 替换
|
||||
} else {
|
||||
流程终点列表.value.push(item); // 添加
|
||||
}
|
||||
return true; // 表示添加成功
|
||||
}
|
||||
|
||||
// --- 原生拖拽事件处理 ---
|
||||
|
||||
// ===== 拖拽事件处理 =====
|
||||
/**
|
||||
* 处理源列表项开始拖拽
|
||||
*/
|
||||
function handleDragStart(event: DragEvent, item: ComponentItem) {
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('text/plain', String(item.id));
|
||||
event.dataTransfer.effectAllowed = 'copyMove'; // 允许复制或移动
|
||||
draggingItemId.value = item.id as ComponentItem['id']; // 记录拖拽的 ID
|
||||
console.debug(`👆 开始拖拽: ${item.name} (ID: ${item.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理已放置项开始拖拽
|
||||
*/
|
||||
function handlePlacedItemDragStart(event: DragEvent, item: ComponentItem, listType: 'end' | 'start') {
|
||||
// ESLint: 'end' 优先
|
||||
if (event.dataTransfer) {
|
||||
event.dataTransfer.setData('text/plain', String(item.id));
|
||||
event.dataTransfer.setData('sourceList', listType); // 标记来源列表
|
||||
event.dataTransfer.effectAllowed = 'move'; // 只允许移动
|
||||
draggingItemId.value = item.id as ComponentItem['id'];
|
||||
console.debug(`👆 从 ${listType === 'start' ? '起点' : '终点'} 列表开始拖拽: ${item.name} (ID: ${item.id})`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拖拽结束事件
|
||||
*/
|
||||
function handleDragEnd() {
|
||||
const item = draggingItemId.value ? getItemById(draggingItemId.value) : null;
|
||||
if (item) {
|
||||
console.debug(`👇 拖拽结束: ${item.name}`);
|
||||
} else {
|
||||
console.debug('👇 拖拽结束: 未知组件');
|
||||
}
|
||||
draggingItemId.value = null; // 清除拖拽 ID
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拖拽进入目标区域
|
||||
*/
|
||||
function handleDragEnter(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.classList.add('drag-over'); // 添加视觉反馈
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拖拽在目标区域上方移动
|
||||
*/
|
||||
function handleDragOver(event: DragEvent) {
|
||||
event.preventDefault(); // 必须阻止默认行为才能触发 drop
|
||||
if (event.dataTransfer) {
|
||||
// 根据 effectAllowed 判断是复制还是移动
|
||||
event.dataTransfer.dropEffect = event.dataTransfer.effectAllowed === 'copyMove' ? 'copy' : 'move';
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理拖拽离开目标区域
|
||||
*/
|
||||
function handleDragLeave(event: DragEvent) {
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.classList.remove('drag-over'); // 移除视觉反馈
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理在起点列表区域放置
|
||||
*/
|
||||
function handleDropStart(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.classList.remove('drag-over'); // 移除视觉反馈
|
||||
|
||||
if (!draggingItemId.value || !event.dataTransfer) {
|
||||
console.debug('⚠️ 放置失败:无法获取拖拽数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = draggingItemId.value;
|
||||
const sourceList = event.dataTransfer.getData('sourceList'); // 获取来源列表标记
|
||||
const newItem = getItemById(itemId);
|
||||
|
||||
if (!newItem) {
|
||||
console.debug(`❌ 放置失败:找不到 ID 为 ${itemId} 的组件`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`✋ 尝试放置到起点列表: ${newItem.name} (ID: ${itemId}), 来源: ${sourceList || '源列表'}`);
|
||||
|
||||
// 尝试添加到起点列表
|
||||
const added = addToStartList(newItem);
|
||||
|
||||
if (added) {
|
||||
console.debug(`✅ 成功放置到起点: ${newItem.name}`);
|
||||
// 如果是从终点列表拖过来的,需要清空终点列表
|
||||
if (sourceList === 'end') {
|
||||
流程终点列表.value = [];
|
||||
console.debug('🔄 已清空终点列表,因为项已移动到起点');
|
||||
}
|
||||
} else {
|
||||
console.debug(`⚠️ 放置到起点失败 (约束检查未通过): ${newItem.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理在终点列表区域放置
|
||||
*/
|
||||
function handleDropEnd(event: DragEvent) {
|
||||
event.preventDefault();
|
||||
const target = event.currentTarget as HTMLElement;
|
||||
target.classList.remove('drag-over'); // 移除视觉反馈
|
||||
|
||||
if (!draggingItemId.value || !event.dataTransfer) {
|
||||
console.debug('⚠️ 放置失败:无法获取拖拽数据');
|
||||
return;
|
||||
}
|
||||
|
||||
const itemId = draggingItemId.value;
|
||||
const sourceList = event.dataTransfer.getData('sourceList'); // 获取来源列表标记
|
||||
const newItem = getItemById(itemId);
|
||||
|
||||
if (!newItem) {
|
||||
console.debug(`❌ 放置失败:找不到 ID 为 ${itemId} 的组件`);
|
||||
return;
|
||||
}
|
||||
|
||||
console.debug(`✋ 尝试放置到终点列表: ${newItem.name} (ID: ${itemId}), 来源: ${sourceList || '源列表'}`);
|
||||
|
||||
// 尝试添加到终点列表
|
||||
const added = addToEndList(newItem);
|
||||
|
||||
if (added) {
|
||||
console.debug(`✅ 成功放置到终点: ${newItem.name}`);
|
||||
// 如果是从起点列表拖过来的,需要清空起点列表
|
||||
if (sourceList === 'start') {
|
||||
流程起点列表.value = [];
|
||||
console.debug('🔄 已清空起点列表,因为项已移动到终点');
|
||||
}
|
||||
} else {
|
||||
console.debug(`⚠️ 放置到终点失败 (约束检查未通过): ${newItem.name}`);
|
||||
}
|
||||
}
|
||||
|
||||
// ===== 计算属性 =====
|
||||
/**
|
||||
* 计算流程节点(包含起点和终点)
|
||||
*/
|
||||
const 完整流程节点 = computed(() => {
|
||||
const nodes: ComponentItem[] = [];
|
||||
// 添加起点
|
||||
if (流程起点列表.value.length > 0) {
|
||||
nodes.push(流程起点列表.value[0]);
|
||||
}
|
||||
|
||||
// 添加中间节点
|
||||
if (流程起点列表.value.length > 0 && 流程终点列表.value.length > 0) {
|
||||
const startIndex = getItemIndex(流程起点列表.value[0]);
|
||||
const endIndex = getItemIndex(流程终点列表.value[0]);
|
||||
|
||||
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex - 1) {
|
||||
nodes.push(...组件列表.value.slice(startIndex + 1, endIndex));
|
||||
}
|
||||
}
|
||||
|
||||
// 添加终点
|
||||
if (流程终点列表.value.length > 0) {
|
||||
nodes.push(流程终点列表.value[0]);
|
||||
}
|
||||
|
||||
return nodes;
|
||||
});
|
||||
watchEffect(() => {
|
||||
console.debug(`完整流程节点 :>> `, JSON.stringify(完整流程节点.value, null, 2));
|
||||
});
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div class="flex gap-5 p-5 bg-white dark:bg-gray-900 text-gray-800 dark:text-gray-200 font-sans min-h-100">
|
||||
<!-- 组件选择区域 -->
|
||||
<div class="flex-1 p-4 rounded border border-gray-200">
|
||||
<h3 class="text-center mb-5 text-gray-700 font-bold">组件选择</h3>
|
||||
<div class="min-h-150px rounded p-2.5 bg-gray-50 dark:bg-gray-800">
|
||||
<!-- 源列表项:添加 draggable 和 dragstart 事件 -->
|
||||
<div
|
||||
v-for="item in 组件列表"
|
||||
:key="item.id"
|
||||
class="py-2.5 px-4 mb-2 bg-white dark:bg-gray-700 border border-blue-500 dark:border-blue-400 text-gray-800 dark:text-gray-200 rounded cursor-grab text-center drag-item component-item hover:bg-blue-50 dark:hover:bg-gray-600"
|
||||
:data-id="item.id"
|
||||
draggable="true"
|
||||
@dragstart="handleDragStart($event, item)"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 构建流程区域 -->
|
||||
<div class="flex-1 p-4 rounded border border-gray-200">
|
||||
<h3 class="text-center mb-5 text-gray-700 font-bold">构建流程</h3>
|
||||
<div class="flex items-start mb-2.5">
|
||||
<div class="w-20 text-right mr-2.5 pt-2.5 text-[#ccc] dark:text-gray-400">流程起点:</div>
|
||||
<!-- 起点列表区域:添加拖放事件监听器 -->
|
||||
<div
|
||||
class="flex-1 min-h-150px rounded p-2.5 border-2 border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 mt-1.25 mb-5 target-list start-list"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDropStart"
|
||||
>
|
||||
<!-- 占位符 -->
|
||||
<div
|
||||
v-if="流程起点列表.length === 0"
|
||||
class="flex justify-center items-center h-full min-h-130px text-[#888] dark:text-gray-500 text-center placeholder pointer-events-none"
|
||||
>
|
||||
拖入组件作为流程起点
|
||||
</div>
|
||||
<!-- 已放置的起点项:添加 draggable 和 dragstart/dragend 事件 -->
|
||||
<div
|
||||
v-for="item in 流程起点列表"
|
||||
:key="item.id"
|
||||
class="py-2.5 px-4 mb-2 bg-white dark:bg-gray-700 border border-blue-500 dark:border-blue-400 text-gray-800 dark:text-gray-200 rounded cursor-grab text-center drag-item dropped-item hover:bg-blue-50 dark:hover:bg-gray-600"
|
||||
:data-id="item.id"
|
||||
draggable="true"
|
||||
@dragstart="handlePlacedItemDragStart($event, item, 'start')"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="flex items-start mb-2.5">
|
||||
<div class="w-20 text-right mr-2.5 pt-2.5 text-[#ccc] dark:text-gray-400">流程终点:</div>
|
||||
<!-- 终点列表区域:添加拖放事件监听器 -->
|
||||
<div
|
||||
class="flex-1 min-h-150px rounded p-2.5 border-2 border-dashed border-gray-300 dark:border-gray-600 bg-gray-50 dark:bg-gray-800 mt-1.25 target-list end-list"
|
||||
@dragenter="handleDragEnter"
|
||||
@dragover="handleDragOver"
|
||||
@dragleave="handleDragLeave"
|
||||
@drop="handleDropEnd"
|
||||
>
|
||||
<!-- 占位符 -->
|
||||
<div
|
||||
v-if="流程终点列表.length === 0"
|
||||
class="flex justify-center items-center h-full min-h-130px text-[#888] dark:text-gray-500 text-center placeholder pointer-events-none"
|
||||
>
|
||||
拖入组件作为流程终点
|
||||
</div>
|
||||
<!-- 已放置的终点项:添加 draggable 和 dragstart/dragend 事件 -->
|
||||
<div
|
||||
v-for="item in 流程终点列表"
|
||||
:key="item.id"
|
||||
class="py-2.5 px-4 mb-2 bg-white dark:bg-gray-700 border border-blue-500 dark:border-blue-400 text-gray-800 dark:text-gray-200 rounded cursor-grab text-center drag-item dropped-item hover:bg-blue-50 dark:hover:bg-gray-600"
|
||||
:data-id="item.id"
|
||||
draggable="true"
|
||||
@dragstart="handlePlacedItemDragStart($event, item, 'end')"
|
||||
@dragend="handleDragEnd"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 预览区域 -->
|
||||
<div class="flex-1 p-4 rounded border border-gray-200">
|
||||
<h3 class="text-center mb-5 text-gray-700 font-bold">预览</h3>
|
||||
<div
|
||||
class="mt-2.5 p-2.5 bg-gray-50 dark:bg-gray-800 border border-gray-200 dark:border-gray-700 rounded min-h-100px flex flex-col items-stretch justify-start gap-2"
|
||||
>
|
||||
<template v-if="完整流程节点.length > 0">
|
||||
<div
|
||||
v-for="item in 完整流程节点"
|
||||
:key="item.id"
|
||||
class="py-2.5 px-4 bg-white dark:bg-gray-700 text-gray-800 dark:text-gray-200 rounded cursor-default flex-shrink-0 text-center drag-item component-item preview-item border border-gray-300 dark:border-gray-600"
|
||||
>
|
||||
{{ item.name }}
|
||||
</div>
|
||||
</template>
|
||||
<div
|
||||
v-else
|
||||
class="flex justify-center items-center h-20 text-[#888] dark:text-gray-500 text-center w-full preview-placeholder placeholder"
|
||||
>
|
||||
拖入起点和终点组件生成预览
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style scoped>
|
||||
/* 拖拽悬停时的目标区域样式 */
|
||||
.target-list.drag-over {
|
||||
border-color: #3b82f6;
|
||||
background-color: #f0f9ff; /* 拖拽悬停反馈 */
|
||||
}
|
||||
.app-dark .target-list.drag-over {
|
||||
background-color: #1e3a8a;
|
||||
}
|
||||
|
||||
/* 拖拽过程中的源项样式 (可选) */
|
||||
.component-item[draggable='true']:active {
|
||||
opacity: 0.7;
|
||||
cursor: grabbing;
|
||||
box-shadow: 0 0 0 2px #3b82f6;
|
||||
}
|
||||
.app-dark .component-item[draggable='true']:active {
|
||||
box-shadow: 0 0 0 2px #93c5fd;
|
||||
}
|
||||
|
||||
/* 放置后的项样式 */
|
||||
.dropped-item {
|
||||
cursor: grab; /* 允许再次拖动 */
|
||||
}
|
||||
|
||||
/* 占位符样式 */
|
||||
.placeholder {
|
||||
user-select: none; /* 防止文本被选中 */
|
||||
pointer-events: none; /* 确保不干扰拖放事件 */
|
||||
}
|
||||
</style>
|
Reference in New Issue
Block a user