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>
|
@ -1,429 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import Sortable from 'sortablejs';
|
|
||||||
|
|
||||||
interface ComponentItem {
|
|
||||||
id: number;
|
|
||||||
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[]>([]);
|
|
||||||
|
|
||||||
const 源列表引用 = useTemplateRef<HTMLDivElement | null>('源列表引用');
|
|
||||||
const 起点列表引用 = useTemplateRef<HTMLDivElement | null>('起点列表引用');
|
|
||||||
const 终点列表引用 = useTemplateRef<HTMLDivElement | null>('终点列表引用');
|
|
||||||
|
|
||||||
function getItemIndex(item: ComponentItem): number {
|
|
||||||
return 组件列表.value.findIndex((comp) => comp.id === item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
function getItemById(id: number): ComponentItem | undefined {
|
|
||||||
return 组件列表.value.find((comp) => comp.id === id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加元素到起点列表,维护相应的 DOM 和数据模型
|
|
||||||
function addToStartList(item: ComponentItem, domItem: HTMLElement) {
|
|
||||||
const newStartIndex = getItemIndex(item);
|
|
||||||
const endIndex = 流程终点列表.value.length > 0 ? getItemIndex(流程终点列表.value[0]) : -1;
|
|
||||||
|
|
||||||
// 约束检查:如果终点已存在,则新起点必须在终点之前
|
|
||||||
if (endIndex !== -1 && newStartIndex >= endIndex) {
|
|
||||||
consola.warn('流程起点必须在流程终点之前');
|
|
||||||
domItem.remove(); // 移除不符合约束的 DOM 项
|
|
||||||
return false; // 表示添加失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据模型
|
|
||||||
if (流程起点列表.value.length > 0) {
|
|
||||||
流程起点列表.value.splice(0, 1, item); // 替换
|
|
||||||
} else {
|
|
||||||
流程起点列表.value.push(item); // 添加
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // 表示添加成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 添加元素到终点列表,维护相应的 DOM 和数据模型
|
|
||||||
function addToEndList(item: ComponentItem, domItem: HTMLElement) {
|
|
||||||
const newEndIndex = getItemIndex(item);
|
|
||||||
const startIndex = 流程起点列表.value.length > 0 ? getItemIndex(流程起点列表.value[0]) : -1;
|
|
||||||
|
|
||||||
// 约束检查:如果起点已存在,则新终点必须在起点之后
|
|
||||||
if (startIndex !== -1 && newEndIndex <= startIndex) {
|
|
||||||
consola.warn('流程终点必须在流程起点之后');
|
|
||||||
domItem.remove();
|
|
||||||
return false; // 表示添加失败
|
|
||||||
}
|
|
||||||
|
|
||||||
// 更新数据模型
|
|
||||||
if (流程终点列表.value.length > 0) {
|
|
||||||
流程终点列表.value.splice(0, 1, item); // 替换
|
|
||||||
} else {
|
|
||||||
流程终点列表.value.push(item); // 添加
|
|
||||||
}
|
|
||||||
|
|
||||||
return true; // 表示添加成功
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理起点列表的 DOM,确保与数据模型一致
|
|
||||||
function cleanupStartListDOM(keepItemId?: number) {
|
|
||||||
if (!起点列表引用.value) return;
|
|
||||||
|
|
||||||
consola.info(`清理起点列表 DOM,保留的项 ID:${keepItemId ?? '无'}`);
|
|
||||||
|
|
||||||
const children = [...起点列表引用.value.children];
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (child instanceof HTMLElement && child.classList.contains('drag-item') && child.dataset.id) {
|
|
||||||
const childId = Number.parseInt(child.dataset.id, 10);
|
|
||||||
|
|
||||||
// 如果指定了要保留的项
|
|
||||||
if (keepItemId !== undefined && childId === keepItemId) {
|
|
||||||
if (found) {
|
|
||||||
consola.info(`移除重复的起点项,ID: ${childId}`);
|
|
||||||
child.remove(); // 如果已经找到过一个相同ID的项,则这个是重复的,移除
|
|
||||||
} else {
|
|
||||||
consola.info(`保留起点项,ID: ${childId}`);
|
|
||||||
found = true; // 标记找到,保留这个
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 对于其他项
|
|
||||||
else if (keepItemId === undefined || childId !== keepItemId) {
|
|
||||||
if (流程起点列表.value.length === 1 && 流程起点列表.value[0].id === childId) {
|
|
||||||
// 如果这个项与数据模型匹配,保留
|
|
||||||
if (found) {
|
|
||||||
consola.info(`移除重复的起点项,ID: ${childId}`);
|
|
||||||
child.remove(); // 但如果已找到一个相同ID的,则这个是重复的
|
|
||||||
} else {
|
|
||||||
consola.info(`保留与数据模型匹配的起点项,ID: ${childId}`);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// 不在数据模型中的项,移除
|
|
||||||
consola.info(`移除不在数据模型中的起点项,ID: ${childId}`);
|
|
||||||
child.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (child.classList.contains('placeholder') && 流程起点列表.value.length > 0) {
|
|
||||||
// 数据模型有项时,移除占位符
|
|
||||||
consola.info('移除起点列表中的占位符,因为数据模型中已有项');
|
|
||||||
child.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果数据模型为空,且DOM中没有占位符,添加占位符
|
|
||||||
if (流程起点列表.value.length === 0 && !起点列表引用.value.querySelector('.placeholder')) {
|
|
||||||
consola.info('添加起点列表占位符,因为数据模型为空');
|
|
||||||
const placeholder = document.createElement('div');
|
|
||||||
placeholder.className = 'placeholder';
|
|
||||||
placeholder.textContent = '请拖入组件构成流程起点';
|
|
||||||
起点列表引用.value.append(placeholder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 清理终点列表的 DOM,确保与数据模型一致
|
|
||||||
function cleanupEndListDOM(keepItemId?: number) {
|
|
||||||
if (!终点列表引用.value) return;
|
|
||||||
|
|
||||||
consola.info(`清理终点列表 DOM,保留的项 ID:${keepItemId ?? '无'}`);
|
|
||||||
|
|
||||||
const children = [...终点列表引用.value.children];
|
|
||||||
let found = false;
|
|
||||||
|
|
||||||
for (const child of children) {
|
|
||||||
if (child instanceof HTMLElement && child.classList.contains('drag-item') && child.dataset.id) {
|
|
||||||
const childId = Number.parseInt(child.dataset.id, 10);
|
|
||||||
|
|
||||||
// 如果指定了要保留的项
|
|
||||||
if (keepItemId !== undefined && childId === keepItemId) {
|
|
||||||
if (found) {
|
|
||||||
consola.info(`移除重复的终点项,ID: ${childId}`);
|
|
||||||
child.remove(); // 如果已经找到过一个相同ID的项,则这个是重复的,移除
|
|
||||||
} else {
|
|
||||||
consola.info(`保留终点项,ID: ${childId}`);
|
|
||||||
found = true; // 标记找到,保留这个
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// 对于其他项
|
|
||||||
else if (keepItemId === undefined || childId !== keepItemId) {
|
|
||||||
if (流程终点列表.value.length === 1 && 流程终点列表.value[0].id === childId) {
|
|
||||||
// 如果这个项与数据模型匹配,保留
|
|
||||||
if (found) {
|
|
||||||
consola.info(`移除重复的终点项,ID: ${childId}`);
|
|
||||||
child.remove(); // 但如果已找到一个相同ID的,则这个是重复的
|
|
||||||
} else {
|
|
||||||
consola.info(`保留与数据模型匹配的终点项,ID: ${childId}`);
|
|
||||||
found = true;
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
consola.info(`移除不在数据模型中的终点项,ID: ${childId}`);
|
|
||||||
// 不在数据模型中的项,移除
|
|
||||||
child.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (child.classList.contains('placeholder') && 流程终点列表.value.length > 0) {
|
|
||||||
// 数据模型有项时,移除占位符
|
|
||||||
consola.info('移除终点列表中的占位符,因为数据模型中已有项');
|
|
||||||
child.remove();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 如果数据模型为空,且DOM中没有占位符,添加占位符
|
|
||||||
if (流程终点列表.value.length === 0 && !终点列表引用.value.querySelector('.placeholder')) {
|
|
||||||
consola.info('添加终点列表占位符,因为数据模型为空');
|
|
||||||
const placeholder = document.createElement('div');
|
|
||||||
placeholder.className = 'placeholder';
|
|
||||||
placeholder.textContent = '请拖入组件构成流程终点';
|
|
||||||
终点列表引用.value.append(placeholder);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
onMounted(() => {
|
|
||||||
if (!源列表引用.value || !起点列表引用.value || !终点列表引用.value) {
|
|
||||||
consola.error('未能获取到 SortableJS 容器元素');
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 初始化源列表
|
|
||||||
Sortable.create(源列表引用.value, {
|
|
||||||
group: {
|
|
||||||
name: 'components',
|
|
||||||
pull: 'clone', // 允许克隆
|
|
||||||
put: false, // 不允许拖入
|
|
||||||
},
|
|
||||||
sort: false, // 源列表内不允许排序
|
|
||||||
animation: 150,
|
|
||||||
// 将 data-id 附加到拖拽数据中,供目标列表识别
|
|
||||||
setData: (dataTransfer, dragEl) => {
|
|
||||||
// 确保 dragEl 是有效的 HTMLElement 且包含 data-id
|
|
||||||
if (dragEl instanceof HTMLElement && dragEl.dataset.id) {
|
|
||||||
dataTransfer.setData('text/plain', dragEl.dataset.id);
|
|
||||||
}
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化流程起点列表
|
|
||||||
Sortable.create(起点列表引用.value, {
|
|
||||||
group: {
|
|
||||||
name: 'flowPoints', // 修改为新的组名,用于区分"流程点"和普通组件
|
|
||||||
pull: true, // 允许拖出
|
|
||||||
put: ['components', 'flowPoints'], // 允许接收"组件"和"流程点"
|
|
||||||
},
|
|
||||||
animation: 150,
|
|
||||||
draggable: '.drag-item', // 只有带 .drag-item 的元素才能被拖动
|
|
||||||
onAdd: (evt) => {
|
|
||||||
const itemId = Number.parseInt(evt.item.dataset.id || '0', 10);
|
|
||||||
const newItem = getItemById(itemId);
|
|
||||||
|
|
||||||
if (!newItem) {
|
|
||||||
consola.error('找不到拖拽的组件项数据 (起点)');
|
|
||||||
evt.item.remove(); // 从 DOM 中移除无效项
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查来源,如果是从终点列表拖来的
|
|
||||||
if (evt.from === 终点列表引用.value) {
|
|
||||||
// 从终点列表移除
|
|
||||||
流程终点列表.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试添加到起点列表
|
|
||||||
const added = addToStartList(newItem, evt.item);
|
|
||||||
|
|
||||||
// 在下一个 tick 清理 DOM
|
|
||||||
nextTick(() => {
|
|
||||||
cleanupStartListDOM(added ? itemId : undefined);
|
|
||||||
|
|
||||||
// 如果来源是终点列表,需同时清理终点列表的 DOM
|
|
||||||
if (evt.from === 终点列表引用.value) {
|
|
||||||
cleanupEndListDOM();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
|
|
||||||
// 初始化流程终点列表
|
|
||||||
Sortable.create(终点列表引用.value, {
|
|
||||||
group: {
|
|
||||||
name: 'flowPoints', // 与起点列表使用相同的组名
|
|
||||||
pull: true, // 允许拖出
|
|
||||||
put: ['components', 'flowPoints'], // 允许接收"组件"和"流程点"
|
|
||||||
},
|
|
||||||
draggable: '.drag-item',
|
|
||||||
animation: 150,
|
|
||||||
onAdd: (evt) => {
|
|
||||||
const itemId = Number.parseInt(evt.item.dataset.id || '0', 10);
|
|
||||||
const newItem = getItemById(itemId);
|
|
||||||
|
|
||||||
if (!newItem) {
|
|
||||||
consola.error('找不到拖拽的组件项数据 (终点)');
|
|
||||||
evt.item.remove();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 检查来源,如果是从起点列表拖来的
|
|
||||||
if (evt.from === 起点列表引用.value) {
|
|
||||||
// 从起点列表移除
|
|
||||||
流程起点列表.value = [];
|
|
||||||
}
|
|
||||||
|
|
||||||
// 尝试添加到终点列表
|
|
||||||
const added = addToEndList(newItem, evt.item);
|
|
||||||
|
|
||||||
// 在下一个 tick 清理 DOM
|
|
||||||
nextTick(() => {
|
|
||||||
cleanupEndListDOM(added ? itemId : undefined);
|
|
||||||
|
|
||||||
// 如果来源是起点列表,需同时清理起点列表的 DOM
|
|
||||||
if (evt.from === 起点列表引用.value) {
|
|
||||||
cleanupStartListDOM();
|
|
||||||
}
|
|
||||||
});
|
|
||||||
},
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
// 计算流程中的中间节点 (基于起点和终点在原始列表中的位置)
|
|
||||||
const intermediateNodes = computed(() => {
|
|
||||||
if (流程起点列表.value.length === 0 || 流程终点列表.value.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const startIndex = getItemIndex(流程起点列表.value[0]);
|
|
||||||
const endIndex = getItemIndex(流程终点列表.value[0]);
|
|
||||||
|
|
||||||
// 确保起点和终点都有效,且它们之间至少有一个组件
|
|
||||||
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex - 1) {
|
|
||||||
// 返回原始列表中位于起点和终点之间的组件
|
|
||||||
return 组件列表.value.slice(startIndex + 1, endIndex);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="flex gap-5 p-5 bg-[#1a1a1a] text-[#e0e0e0] font-sans min-h-100">
|
|
||||||
<!-- 组件选择区域 -->
|
|
||||||
<div class="flex-1 p-4 rounded">
|
|
||||||
<h3 class="text-center mb-5 text-white font-bold">组件选择</h3>
|
|
||||||
<div ref="源列表引用" class="min-h-150px rounded p-2.5 transition-bg duration-200">
|
|
||||||
<div
|
|
||||||
v-for="item in 组件列表"
|
|
||||||
:key="item.id"
|
|
||||||
class="py-2.5 px-4 mb-2 bg-[#333] border border-[#007bff] text-[#e0e0e0] rounded cursor-grab text-center drag-item component-item"
|
|
||||||
:data-id="item.id"
|
|
||||||
>
|
|
||||||
<!-- data-id 用于 SortableJS 识别拖拽项 -->
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 构建流程区域 -->
|
|
||||||
<div class="flex-1 p-4 rounded">
|
|
||||||
<h3 class="text-center mb-5 text-white 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]">流程起点:</div>
|
|
||||||
<div
|
|
||||||
ref="起点列表引用"
|
|
||||||
class="flex-1 min-h-150px rounded p-2.5 border-2 border-dashed border-[#555] bg-[#2a2a2a] mt-1.25 mb-5 target-list start-list"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="流程起点列表.length === 0"
|
|
||||||
class="flex justify-center items-center h-full min-h-130px text-[#888] text-center placeholder"
|
|
||||||
>
|
|
||||||
请拖入组件构成流程起点
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="item in 流程起点列表"
|
|
||||||
:key="item.id"
|
|
||||||
class="py-2.5 px-4 mb-2 bg-[#444] border border-[#17a2b8] text-[#e0e0e0] rounded cursor-default text-center drag-item dropped-item"
|
|
||||||
:data-id="item.id"
|
|
||||||
>
|
|
||||||
{{ 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]">流程终点:</div>
|
|
||||||
<div
|
|
||||||
ref="终点列表引用"
|
|
||||||
class="flex-1 min-h-150px rounded p-2.5 border-2 border-dashed border-[#555] bg-[#2a2a2a] mt-1.25 target-list end-list"
|
|
||||||
>
|
|
||||||
<div
|
|
||||||
v-if="流程终点列表.length === 0"
|
|
||||||
class="flex justify-center items-center h-full min-h-130px text-[#888] text-center placeholder"
|
|
||||||
>
|
|
||||||
请拖入组件构成流程终点
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="item in 流程终点列表"
|
|
||||||
:key="item.id"
|
|
||||||
class="py-2.5 px-4 mb-2 bg-[#444] border border-[#17a2b8] text-[#e0e0e0] rounded cursor-default text-center drag-item dropped-item"
|
|
||||||
:data-id="item.id"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 预览区域 -->
|
|
||||||
<div class="flex-1 p-4 rounded">
|
|
||||||
<h3 class="text-center mb-5 text-white font-bold">预览</h3>
|
|
||||||
<div
|
|
||||||
class="mt-2.5 p-2.5 bg-[#2a2a2a] border border-[#444] rounded min-h-100px flex flex-col items-stretch justify-start gap-2"
|
|
||||||
>
|
|
||||||
<template v-if="流程起点列表.length > 0 || 流程终点列表.length > 0">
|
|
||||||
<div
|
|
||||||
v-if="流程起点列表.length > 0"
|
|
||||||
:key="`start-${流程起点列表[0].id}`"
|
|
||||||
class="py-2.5 px-4 bg-[#333] border border-[#007bff] text-[#e0e0e0] rounded cursor-default flex-shrink-0 text-center drag-item component-item preview-item"
|
|
||||||
>
|
|
||||||
{{ 流程起点列表[0].name }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-for="item in intermediateNodes"
|
|
||||||
:key="`middle-${item.id}`"
|
|
||||||
class="py-2.5 px-4 bg-[#333] border border-[#6c757d] text-[#e0e0e0] rounded cursor-default flex-shrink-0 text-center drag-item component-item preview-item preview-intermediate"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
<div
|
|
||||||
v-if="流程终点列表.length > 0"
|
|
||||||
:key="`end-${流程终点列表[0].id}`"
|
|
||||||
class="py-2.5 px-4 bg-[#333] border border-[#007bff] text-[#e0e0e0] rounded cursor-default flex-shrink-0 text-center drag-item component-item preview-item"
|
|
||||||
>
|
|
||||||
{{ 流程终点列表[0].name }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<div
|
|
||||||
v-else
|
|
||||||
class="flex justify-center items-center h-20 text-[#888] text-center w-full preview-placeholder placeholder"
|
|
||||||
>
|
|
||||||
请拖入起点和终点组件以生成预览
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
/* SortableJS 拖拽过程中的辅助样式 */
|
|
||||||
.sortable-ghost {
|
|
||||||
opacity: 0.5;
|
|
||||||
background: #4a90e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* .sortable-chosen {} */ /* 被选中的元素 */
|
|
||||||
/* .sortable-drag {} */ /* 拖拽中的元素 */
|
|
||||||
</style>
|
|
@ -1,18 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
const state = reactive({
|
|
||||||
组件列表: [
|
|
||||||
{ id: 1, name: '组件1' },
|
|
||||||
{ id: 2, name: '组件2' },
|
|
||||||
{ id: 3, name: '组件3' },
|
|
||||||
],
|
|
||||||
已选流程ID: [],
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div>
|
|
||||||
<SortableComponentSelector :state="state" />
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped></style>
|
|
@ -1,330 +0,0 @@
|
|||||||
<script setup lang="ts">
|
|
||||||
import { computed, ref } from 'vue'; // 导入 computed
|
|
||||||
import { VueDraggable } from 'vue-draggable-plus';
|
|
||||||
|
|
||||||
// 定义组件项的类型
|
|
||||||
interface ComponentItem {
|
|
||||||
id: number;
|
|
||||||
name: string;
|
|
||||||
}
|
|
||||||
|
|
||||||
// 左侧可选组件列表
|
|
||||||
const componentsList = ref<ComponentItem[]>([
|
|
||||||
{ id: 1, name: '目标信号分析组件' },
|
|
||||||
{ id: 2, name: '邻星选择组件' },
|
|
||||||
{ id: 3, name: '参数估计与运算组件' },
|
|
||||||
{ id: 4, name: '参考信号发射组件' },
|
|
||||||
{ id: 5, name: '定位点生成组件' },
|
|
||||||
]);
|
|
||||||
|
|
||||||
// 中间流程起点列表
|
|
||||||
const processStartList = ref<ComponentItem[]>([]);
|
|
||||||
// 中间流程终点列表
|
|
||||||
const processEndList = ref<ComponentItem[]>([]);
|
|
||||||
|
|
||||||
// 获取组件在原始列表中的索引
|
|
||||||
function getItemIndex(item: ComponentItem): number {
|
|
||||||
return componentsList.value.findIndex((comp) => comp.id === item.id);
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理添加到“流程起点”列表的函数
|
|
||||||
function onAddStart(evt: { newIndex?: number; item?: HTMLElement }) {
|
|
||||||
const newItem = processStartList.value[evt.newIndex!]; // 获取新拖入的项
|
|
||||||
const endIndex = processEndList.value.length > 0 ? getItemIndex(processEndList.value[0]) : -1;
|
|
||||||
const newStartIndex = getItemIndex(newItem);
|
|
||||||
|
|
||||||
// 检查约束:新起点不能在终点之后(如果终点已存在)
|
|
||||||
if (endIndex !== -1 && newStartIndex >= endIndex) {
|
|
||||||
consola.warn('流程起点必须在流程终点之前');
|
|
||||||
// 异步移除,因为 v-model 可能尚未完全同步
|
|
||||||
nextTick(() => {
|
|
||||||
processStartList.value.splice(evt.newIndex!, 1);
|
|
||||||
});
|
|
||||||
return; // 阻止后续替换逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理替换逻辑
|
|
||||||
if (processStartList.value.length > 1) {
|
|
||||||
const oldIndex = evt.newIndex === 0 ? 1 : 0;
|
|
||||||
processStartList.value.splice(oldIndex, 1); // 移除旧项
|
|
||||||
consola.info('流程起点组件已替换');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理添加到“流程终点”列表的函数
|
|
||||||
function onAddEnd(evt: { newIndex?: number; item?: HTMLElement }) {
|
|
||||||
const newItem = processEndList.value[evt.newIndex!]; // 获取新拖入的项
|
|
||||||
const startIndex = processStartList.value.length > 0 ? getItemIndex(processStartList.value[0]) : -1;
|
|
||||||
const newEndIndex = getItemIndex(newItem);
|
|
||||||
|
|
||||||
// 检查约束:新终点不能在起点之前(如果起点已存在)
|
|
||||||
if (startIndex !== -1 && newEndIndex <= startIndex) {
|
|
||||||
consola.warn('流程终点必须在流程起点之后');
|
|
||||||
// 异步移除
|
|
||||||
nextTick(() => {
|
|
||||||
processEndList.value.splice(evt.newIndex!, 1);
|
|
||||||
});
|
|
||||||
return; // 阻止后续替换逻辑
|
|
||||||
}
|
|
||||||
|
|
||||||
// 处理替换逻辑
|
|
||||||
if (processEndList.value.length > 1) {
|
|
||||||
const oldIndex = evt.newIndex === 0 ? 1 : 0;
|
|
||||||
processEndList.value.splice(oldIndex, 1); // 移除旧项
|
|
||||||
consola.info('流程终点组件已替换');
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 计算中间节点
|
|
||||||
const intermediateNodes = computed(() => {
|
|
||||||
if (processStartList.value.length === 0 || processEndList.value.length === 0) {
|
|
||||||
return [];
|
|
||||||
}
|
|
||||||
const startIndex = getItemIndex(processStartList.value[0]);
|
|
||||||
const endIndex = getItemIndex(processEndList.value[0]);
|
|
||||||
|
|
||||||
if (startIndex !== -1 && endIndex !== -1 && startIndex < endIndex - 1) {
|
|
||||||
// 提取起点和终点之间的组件
|
|
||||||
return componentsList.value.slice(startIndex + 1, endIndex);
|
|
||||||
}
|
|
||||||
return [];
|
|
||||||
});
|
|
||||||
</script>
|
|
||||||
|
|
||||||
<template>
|
|
||||||
<div class="drag-container">
|
|
||||||
<!-- 组件选择区域 -->
|
|
||||||
<div class="component-selection">
|
|
||||||
<h3>组件选择</h3>
|
|
||||||
<VueDraggable
|
|
||||||
v-model="componentsList"
|
|
||||||
class="drag-list source-list"
|
|
||||||
:group="{ name: 'components', pull: 'clone', put: false }"
|
|
||||||
:sort="false"
|
|
||||||
>
|
|
||||||
<div v-for="item in componentsList" :key="item.id" class="drag-item component-item">
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</VueDraggable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 构建流程区域 -->
|
|
||||||
<div class="build-process">
|
|
||||||
<h3>构建流程</h3>
|
|
||||||
<div class="process-area">
|
|
||||||
<div class="process-label">流程起点:</div>
|
|
||||||
<VueDraggable
|
|
||||||
v-model="processStartList"
|
|
||||||
class="drag-list target-list start-list"
|
|
||||||
group="components"
|
|
||||||
@add="onAddStart"
|
|
||||||
>
|
|
||||||
<div v-if="processStartList.length === 0" class="placeholder">请拖入组件构成流程起点</div>
|
|
||||||
<div v-for="item in processStartList" :key="item.id" class="drag-item dropped-item">
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</VueDraggable>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<div class="process-area">
|
|
||||||
<div class="process-label">流程终点:</div>
|
|
||||||
<VueDraggable
|
|
||||||
v-model="processEndList"
|
|
||||||
class="drag-list target-list end-list"
|
|
||||||
group="components"
|
|
||||||
@add="onAddEnd"
|
|
||||||
draggable=".drag-item"
|
|
||||||
>
|
|
||||||
<div v-if="processEndList.length === 0" class="placeholder">请拖入组件构成流程终点</div>
|
|
||||||
<div v-for="item in processEndList" :key="item.id" class="drag-item dropped-item">
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
</VueDraggable>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<!-- 预览区域 -->
|
|
||||||
<div class="preview-area">
|
|
||||||
<h3>预览</h3>
|
|
||||||
<div class="preview-content">
|
|
||||||
<template v-if="processStartList.length > 0 || processEndList.length > 0">
|
|
||||||
<!-- 显示起点组件 -->
|
|
||||||
<div
|
|
||||||
v-if="processStartList.length > 0"
|
|
||||||
:key="`start-${processStartList[0].id}`"
|
|
||||||
class="drag-item component-item preview-item"
|
|
||||||
>
|
|
||||||
{{ processStartList[0].name }}
|
|
||||||
</div>
|
|
||||||
<!-- 显示计算出的中间节点 -->
|
|
||||||
<div
|
|
||||||
v-for="item in intermediateNodes"
|
|
||||||
:key="`middle-${item.id}`"
|
|
||||||
class="drag-item component-item preview-item preview-intermediate"
|
|
||||||
>
|
|
||||||
{{ item.name }}
|
|
||||||
</div>
|
|
||||||
<!-- 显示终点组件 -->
|
|
||||||
<div
|
|
||||||
v-if="processEndList.length > 0"
|
|
||||||
:key="`end-${processEndList[0].id}`"
|
|
||||||
class="drag-item component-item preview-item"
|
|
||||||
>
|
|
||||||
{{ processEndList[0].name }}
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
<!-- 如果都没有选择,显示占位符 -->
|
|
||||||
<div v-else class="placeholder preview-placeholder">请拖入起点和终点组件以生成预览</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</template>
|
|
||||||
|
|
||||||
<style scoped>
|
|
||||||
.drag-container {
|
|
||||||
display: flex;
|
|
||||||
gap: 20px;
|
|
||||||
padding: 20px;
|
|
||||||
background-color: #1a1a1a; /* 深色背景模拟图片 */
|
|
||||||
color: #e0e0e0;
|
|
||||||
font-family: sans-serif;
|
|
||||||
min-height: 400px; /* 保证容器有足够高度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-selection,
|
|
||||||
.build-process,
|
|
||||||
.preview-area {
|
|
||||||
flex: 1;
|
|
||||||
padding: 15px;
|
|
||||||
border-radius: 4px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-selection h3,
|
|
||||||
.build-process h3,
|
|
||||||
.preview-area h3 {
|
|
||||||
text-align: center;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
color: #ffffff;
|
|
||||||
font-weight: bold;
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-list {
|
|
||||||
min-height: 150px; /* 列表最小高度 */
|
|
||||||
border-radius: 4px;
|
|
||||||
padding: 10px;
|
|
||||||
transition: background-color 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.source-list {
|
|
||||||
/* 源列表特定样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-list {
|
|
||||||
border: 2px dashed #555; /* 虚线边框 */
|
|
||||||
background-color: #2a2a2a; /* 目标区域背景 */
|
|
||||||
margin-top: 5px; /* 与标签的间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-list.start-list {
|
|
||||||
margin-bottom: 20px; /* 起点和终点区域间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.drag-item {
|
|
||||||
padding: 10px 15px;
|
|
||||||
margin-bottom: 8px;
|
|
||||||
background-color: #333;
|
|
||||||
border: 1px solid #007bff; /* 蓝色边框 */
|
|
||||||
color: #e0e0e0;
|
|
||||||
border-radius: 4px;
|
|
||||||
cursor: grab;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.component-item {
|
|
||||||
/* 源列表项特定样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.dropped-item {
|
|
||||||
background-color: #444; /* 放入目标区域后的样式 */
|
|
||||||
border-color: #17a2b8; /* 不同颜色边框 */
|
|
||||||
cursor: default;
|
|
||||||
}
|
|
||||||
|
|
||||||
.placeholder {
|
|
||||||
display: flex;
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
height: 100%;
|
|
||||||
min-height: 130px; /* 确保占位符区域可见 */
|
|
||||||
color: #888;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.process-area {
|
|
||||||
display: flex;
|
|
||||||
align-items: flex-start; /* 标签和列表顶部对齐 */
|
|
||||||
margin-bottom: 10px; /* 增加区域间距 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.process-label {
|
|
||||||
width: 80px; /* 固定标签宽度 */
|
|
||||||
text-align: right;
|
|
||||||
margin-right: 10px;
|
|
||||||
padding-top: 10px; /* 调整标签垂直位置 */
|
|
||||||
color: #ccc;
|
|
||||||
}
|
|
||||||
|
|
||||||
.target-list {
|
|
||||||
flex: 1; /* 列表占据剩余空间 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-content {
|
|
||||||
margin-top: 10px;
|
|
||||||
padding: 10px; /* 调整内边距以适应项目 */
|
|
||||||
background-color: #2a2a2a;
|
|
||||||
border: 1px solid #444;
|
|
||||||
border-radius: 4px;
|
|
||||||
min-height: 100px; /* 增加最小高度 */
|
|
||||||
display: flex;
|
|
||||||
flex-direction: column; /* 设置为垂直方向排列 */
|
|
||||||
align-items: stretch; /* 使项目宽度撑满容器 */
|
|
||||||
justify-content: flex-start; /* 从顶部开始排列 */
|
|
||||||
gap: 8px; /* 调整垂直间距 */
|
|
||||||
/* 移除 flex-wrap,因为单列不需要换行 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-item {
|
|
||||||
cursor: default; /* 预览项不可拖动 */
|
|
||||||
flex-shrink: 0; /* 防止项目被压缩 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.preview-intermediate {
|
|
||||||
/* 可以为中间节点添加特定样式,例如不同的边框颜色 */
|
|
||||||
border-color: #6c757d; /* 灰色边框 */
|
|
||||||
}
|
|
||||||
/* 预览区域内的占位符特定样式 */
|
|
||||||
.preview-placeholder {
|
|
||||||
display: flex; /* 确保 flex 布局生效 */
|
|
||||||
justify-content: center;
|
|
||||||
align-items: center;
|
|
||||||
min-height: auto; /* 移除继承的最小高度 */
|
|
||||||
height: 80px; /* 给一个合适的高度 */
|
|
||||||
color: #888;
|
|
||||||
text-align: center;
|
|
||||||
width: 100%; /* 占满容器宽度 */
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 拖拽过程中的样式 (可选) */
|
|
||||||
.sortable-ghost {
|
|
||||||
opacity: 0.5;
|
|
||||||
background: #4a90e2;
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-chosen {
|
|
||||||
/* 被选中的元素样式 */
|
|
||||||
}
|
|
||||||
|
|
||||||
.sortable-drag {
|
|
||||||
/* 拖拽中的元素样式 */
|
|
||||||
}
|
|
||||||
</style>
|
|
4
typed-router.d.ts
vendored
4
typed-router.d.ts
vendored
@ -26,6 +26,7 @@ declare module 'vue-router/auto-routes' {
|
|||||||
'FlowbiteSidebar': RouteRecordInfo<'FlowbiteSidebar', '/FlowbiteSidebar', Record<never, never>, Record<never, never>>,
|
'FlowbiteSidebar': RouteRecordInfo<'FlowbiteSidebar', '/FlowbiteSidebar', Record<never, never>, Record<never, never>>,
|
||||||
'Home': RouteRecordInfo<'Home', '/Home', Record<never, never>, Record<never, never>>,
|
'Home': RouteRecordInfo<'Home', '/Home', Record<never, never>, Record<never, never>>,
|
||||||
'PageAPI': RouteRecordInfo<'PageAPI', '/Page/API', Record<never, never>, Record<never, never>>,
|
'PageAPI': RouteRecordInfo<'PageAPI', '/Page/API', Record<never, never>, Record<never, never>>,
|
||||||
|
'PageDomDraggable': RouteRecordInfo<'PageDomDraggable', '/Page/Dom-Draggable', Record<never, never>, Record<never, never>>,
|
||||||
'PageFonts': RouteRecordInfo<'PageFonts', '/Page/fonts', Record<never, never>, Record<never, never>>,
|
'PageFonts': RouteRecordInfo<'PageFonts', '/Page/fonts', Record<never, never>, Record<never, never>>,
|
||||||
'PageIcons': RouteRecordInfo<'PageIcons', '/Page/Icons', Record<never, never>, Record<never, never>>,
|
'PageIcons': RouteRecordInfo<'PageIcons', '/Page/Icons', Record<never, never>, Record<never, never>>,
|
||||||
'PageIframePageIframeConstellationDiagram': RouteRecordInfo<'PageIframePageIframeConstellationDiagram', '/Page/iframe-page/IframeConstellationDiagram', Record<never, never>, Record<never, never>>,
|
'PageIframePageIframeConstellationDiagram': RouteRecordInfo<'PageIframePageIframeConstellationDiagram', '/Page/iframe-page/IframeConstellationDiagram', Record<never, never>, Record<never, never>>,
|
||||||
@ -35,10 +36,7 @@ declare module 'vue-router/auto-routes' {
|
|||||||
'PageP5Js': RouteRecordInfo<'PageP5Js', '/Page/p5_js', Record<never, never>, Record<never, never>>,
|
'PageP5Js': RouteRecordInfo<'PageP5Js', '/Page/p5_js', Record<never, never>, Record<never, never>>,
|
||||||
'PageStyle': RouteRecordInfo<'PageStyle', '/Page/Style', Record<never, never>, Record<never, never>>,
|
'PageStyle': RouteRecordInfo<'PageStyle', '/Page/Style', Record<never, never>, Record<never, never>>,
|
||||||
'PkgsUsageI18n': RouteRecordInfo<'PkgsUsageI18n', '/PkgsUsage/I18n', Record<never, never>, Record<never, never>>,
|
'PkgsUsageI18n': RouteRecordInfo<'PkgsUsageI18n', '/PkgsUsage/I18n', Record<never, never>, Record<never, never>>,
|
||||||
'PkgsUsageSortableJS': RouteRecordInfo<'PkgsUsageSortableJS', '/PkgsUsage/SortableJS', Record<never, never>, Record<never, never>>,
|
|
||||||
'PkgsUsageSortableJSComps': RouteRecordInfo<'PkgsUsageSortableJSComps', '/PkgsUsage/SortableJSComps', Record<never, never>, Record<never, never>>,
|
|
||||||
'PkgsUsageTsEnumUtil': RouteRecordInfo<'PkgsUsageTsEnumUtil', '/PkgsUsage/ts-enum-util', Record<never, never>, Record<never, never>>,
|
'PkgsUsageTsEnumUtil': RouteRecordInfo<'PkgsUsageTsEnumUtil', '/PkgsUsage/ts-enum-util', Record<never, never>, Record<never, never>>,
|
||||||
'PkgsUsageVueDraggablePlus': RouteRecordInfo<'PkgsUsageVueDraggablePlus', '/PkgsUsage/VueDraggablePlus', Record<never, never>, Record<never, never>>,
|
|
||||||
'UIComponentsAntdV': RouteRecordInfo<'UIComponentsAntdV', '/UI-components/AntdV', Record<never, never>, Record<never, never>>,
|
'UIComponentsAntdV': RouteRecordInfo<'UIComponentsAntdV', '/UI-components/AntdV', Record<never, never>, Record<never, never>>,
|
||||||
'UIComponentsComponents': RouteRecordInfo<'UIComponentsComponents', '/UI-components/Components', Record<never, never>, Record<never, never>>,
|
'UIComponentsComponents': RouteRecordInfo<'UIComponentsComponents', '/UI-components/Components', Record<never, never>, Record<never, never>>,
|
||||||
'UIComponentsInfiniteLoading': RouteRecordInfo<'UIComponentsInfiniteLoading', '/UI-components/infinite-loading', Record<never, never>, Record<never, never>>,
|
'UIComponentsInfiniteLoading': RouteRecordInfo<'UIComponentsInfiniteLoading', '/UI-components/infinite-loading', Record<never, never>, Record<never, never>>,
|
||||||
|
Reference in New Issue
Block a user