2
This commit is contained in:
@@ -141,5 +141,5 @@
|
|||||||
"activeTabId": "mxfx11j"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-21T13:24:40.794Z"
|
"lastUpdated": "2025-12-22T14:24:28.202Z"
|
||||||
}
|
}
|
||||||
@@ -67,5 +67,5 @@
|
|||||||
}
|
}
|
||||||
],
|
],
|
||||||
"selectedId": "jy87mdv",
|
"selectedId": "jy87mdv",
|
||||||
"lastUpdated": "2025-12-21T13:23:32.873Z"
|
"lastUpdated": "2025-12-22T14:24:28.783Z"
|
||||||
}
|
}
|
||||||
@@ -1,8 +1,9 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||||
import { useInteractionStore } from '../plugins'
|
import { useInteractionStore, useDragStore } from '../plugins'
|
||||||
|
|
||||||
const interactionStore = useInteractionStore()
|
const interactionStore = useInteractionStore()
|
||||||
|
const dragStore = useDragStore()
|
||||||
|
|
||||||
const currentTime = ref('')
|
const currentTime = ref('')
|
||||||
let timer: number | null = null
|
let timer: number | null = null
|
||||||
@@ -49,6 +50,34 @@ const selectedInfo = computed(() => {
|
|||||||
return `选中: ${typeName} [${target.path}]`
|
return `选中: ${typeName} [${target.path}]`
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 拖拽状态信息
|
||||||
|
const dragInfo = computed(() => {
|
||||||
|
if (!dragStore.isDragging) return null
|
||||||
|
|
||||||
|
const source = dragStore.dragSource
|
||||||
|
if (!source) return null
|
||||||
|
|
||||||
|
if (source.type === 'design-component') {
|
||||||
|
return `拖拽: ${source.componentName}`
|
||||||
|
}
|
||||||
|
return `拖抽: ${source.path}`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 当前拖放目标信息
|
||||||
|
const dropTargetInfo = computed(() => {
|
||||||
|
if (!dragStore.isDragging) return null
|
||||||
|
if (!dragStore.selectedNode) return null
|
||||||
|
|
||||||
|
const node = dragStore.selectedNode
|
||||||
|
const typeText = node.type === 'er' ? 'Row' : 'Col'
|
||||||
|
return `目标: ${typeText} [${node.path}]`
|
||||||
|
})
|
||||||
|
|
||||||
|
// 最后一次拖放记录
|
||||||
|
const lastDropInfo = computed(() => {
|
||||||
|
return dragStore.formatDropRecord(dragStore.lastDropRecord)
|
||||||
|
})
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
updateTime()
|
updateTime()
|
||||||
timer = window.setInterval(updateTime, 1000)
|
timer = window.setInterval(updateTime, 1000)
|
||||||
@@ -62,23 +91,45 @@ onUnmounted(() => {
|
|||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
<footer class="app-footer">
|
<footer class="app-footer" :class="{ 'is-dragging': dragStore.isDragging }">
|
||||||
<div class="footer-left">
|
<div class="footer-left">
|
||||||
<span v-if="hoverInfo" class="info-item hover-info">
|
<!-- 拖拽状态显示 -->
|
||||||
👁️ {{ hoverInfo }}
|
<template v-if="dragStore.isDragging">
|
||||||
</span>
|
<span class="info-item drag-info">
|
||||||
<span v-if="selectedInfo" class="info-item selected-info">
|
🎯 {{ dragInfo }}
|
||||||
✅ {{ selectedInfo }}
|
</span>
|
||||||
</span>
|
<span v-if="dropTargetInfo" class="info-item target-info">
|
||||||
<span v-if="!hoverInfo && !selectedInfo" class="info-item idle-info">
|
➡️ {{ dropTargetInfo }}
|
||||||
🎯 就绪 | 悬停或点击元素查看信息
|
</span>
|
||||||
</span>
|
<span v-if="dragStore.hoverDirection" class="info-item direction-info">
|
||||||
|
📍 {{ dragStore.hoverDirection === 'top' ? '上方' : dragStore.hoverDirection === 'bottom' ? '下方' : dragStore.hoverDirection === 'left' ? '左侧' : '右侧' }}
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<!-- 普通状态显示 -->
|
||||||
|
<template v-else>
|
||||||
|
<span v-if="hoverInfo" class="info-item hover-info">
|
||||||
|
👁️ {{ hoverInfo }}
|
||||||
|
</span>
|
||||||
|
<span v-if="selectedInfo" class="info-item selected-info">
|
||||||
|
✅ {{ selectedInfo }}
|
||||||
|
</span>
|
||||||
|
<span v-if="!hoverInfo && !selectedInfo" class="info-item idle-info">
|
||||||
|
🎯 就绪 | 悬停或点击元素查看信息
|
||||||
|
</span>
|
||||||
|
</template>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-center">
|
<div class="footer-center">
|
||||||
<span v-if="interactionStore.lastInteraction" class="last-action">
|
<!-- 拖放记录 -->
|
||||||
|
<span v-if="lastDropInfo" class="drop-record">
|
||||||
|
📋 拖放: {{ lastDropInfo }}
|
||||||
|
</span>
|
||||||
|
<span v-else-if="interactionStore.lastInteraction" class="last-action">
|
||||||
最近: {{ interactionText }}
|
最近: {{ interactionText }}
|
||||||
</span>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div class="footer-right">
|
<div class="footer-right">
|
||||||
<span class="time">{{ currentTime }}</span>
|
<span class="time">{{ currentTime }}</span>
|
||||||
</div>
|
</div>
|
||||||
@@ -142,6 +193,34 @@ onUnmounted(() => {
|
|||||||
border-radius: 3px;
|
border-radius: 3px;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.drop-record {
|
||||||
|
color: #90ee90;
|
||||||
|
font-size: 11px;
|
||||||
|
background: rgba(0, 0, 0, 0.3);
|
||||||
|
padding: 2px 8px;
|
||||||
|
border-radius: 3px;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 拖拽状态样式 */
|
||||||
|
.app-footer.is-dragging {
|
||||||
|
background: #e6a23c;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drag-info {
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.target-info {
|
||||||
|
color: #ffffc0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.direction-info {
|
||||||
|
color: #90ee90;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
.time {
|
.time {
|
||||||
color: white;
|
color: white;
|
||||||
font-size: 12px;
|
font-size: 12px;
|
||||||
|
|||||||
@@ -0,0 +1,135 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { ref, watch, onMounted, onUnmounted, computed } from 'vue'
|
||||||
|
import { useDragStore } from '../../plugins'
|
||||||
|
|
||||||
|
const dragStore = useDragStore()
|
||||||
|
|
||||||
|
// 鼠标位置
|
||||||
|
const mouseX = ref(0)
|
||||||
|
const mouseY = ref(0)
|
||||||
|
|
||||||
|
// 预览样式
|
||||||
|
const previewStyle = computed(() => ({
|
||||||
|
position: 'fixed',
|
||||||
|
left: `${mouseX.value + 15}px`,
|
||||||
|
top: `${mouseY.value + 15}px`,
|
||||||
|
pointerEvents: 'none',
|
||||||
|
zIndex: 9999
|
||||||
|
}))
|
||||||
|
|
||||||
|
// 拖拽源显示文本
|
||||||
|
const sourceText = computed(() => {
|
||||||
|
const source = dragStore.dragSource
|
||||||
|
if (!source) return ''
|
||||||
|
|
||||||
|
if (source.type === 'design-component') {
|
||||||
|
return source.componentName || source.componentId || '组件'
|
||||||
|
}
|
||||||
|
return source.path || '元素'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 拖拽源类型图标
|
||||||
|
const sourceIcon = computed(() => {
|
||||||
|
const source = dragStore.dragSource
|
||||||
|
if (!source) return '📦'
|
||||||
|
|
||||||
|
if (source.type === 'design-component') {
|
||||||
|
return '📦'
|
||||||
|
}
|
||||||
|
return source.elementType === 'er' ? '⬛' : '◻️'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 鼠标移动处理
|
||||||
|
const handleMouseMove = (e: MouseEvent) => {
|
||||||
|
mouseX.value = e.clientX
|
||||||
|
mouseY.value = e.clientY
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听拖拽状态变化,控制body class
|
||||||
|
watch(() => dragStore.isDragging, (isDragging) => {
|
||||||
|
if (isDragging) {
|
||||||
|
document.body.classList.add('is-dragging')
|
||||||
|
} else {
|
||||||
|
document.body.classList.remove('is-dragging')
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
document.addEventListener('mousemove', handleMouseMove)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mousemove', handleMouseMove)
|
||||||
|
document.body.classList.remove('is-dragging')
|
||||||
|
})
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="dragStore.isDragging && dragStore.dragSource"
|
||||||
|
class="drag-preview"
|
||||||
|
:style="previewStyle"
|
||||||
|
>
|
||||||
|
<div class="preview-icon">{{ sourceIcon }}</div>
|
||||||
|
<div class="preview-content">
|
||||||
|
<div class="preview-label">
|
||||||
|
{{ dragStore.dragSource.type === 'design-component' ? '设计组件' : '画布元素' }}
|
||||||
|
</div>
|
||||||
|
<div class="preview-name">{{ sourceText }}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drag-preview {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 10px;
|
||||||
|
background: rgba(0, 0, 0, 0.9);
|
||||||
|
border: 2px solid #409eff;
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 10px 14px;
|
||||||
|
box-shadow: 0 8px 24px rgba(0, 0, 0, 0.4);
|
||||||
|
animation: dragPreviewAppear 0.15s ease-out;
|
||||||
|
max-width: 200px;
|
||||||
|
}
|
||||||
|
|
||||||
|
@keyframes dragPreviewAppear {
|
||||||
|
from {
|
||||||
|
opacity: 0;
|
||||||
|
transform: scale(0.8);
|
||||||
|
}
|
||||||
|
to {
|
||||||
|
opacity: 1;
|
||||||
|
transform: scale(1);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
flex-shrink: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-content {
|
||||||
|
flex: 1;
|
||||||
|
min-width: 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-label {
|
||||||
|
font-size: 10px;
|
||||||
|
color: #888;
|
||||||
|
text-transform: uppercase;
|
||||||
|
letter-spacing: 0.5px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.preview-name {
|
||||||
|
font-size: 13px;
|
||||||
|
color: #fff;
|
||||||
|
font-weight: 500;
|
||||||
|
white-space: nowrap;
|
||||||
|
overflow: hidden;
|
||||||
|
text-overflow: ellipsis;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
187
draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue
Normal file
187
draggable-panels/src/fauto/materials/DesignCenter/DropZone.vue
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
<script setup lang="ts">
|
||||||
|
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
||||||
|
import { useDragStore } from '../../plugins'
|
||||||
|
import type { DropDirection } from '../../plugins'
|
||||||
|
|
||||||
|
const dragStore = useDragStore()
|
||||||
|
|
||||||
|
// 拖放区域位置
|
||||||
|
const zoneStyle = ref<Record<string, string>>({})
|
||||||
|
|
||||||
|
// 计算拖放区域位置
|
||||||
|
const updateZonePosition = () => {
|
||||||
|
if (!dragStore.selectedNode?.element) {
|
||||||
|
zoneStyle.value = {}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
const rect = dragStore.selectedNode.element.getBoundingClientRect()
|
||||||
|
zoneStyle.value = {
|
||||||
|
position: 'fixed',
|
||||||
|
top: `${rect.top}px`,
|
||||||
|
left: `${rect.left}px`,
|
||||||
|
width: `${rect.width}px`,
|
||||||
|
height: `${rect.height}px`
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 监听选中节点变化
|
||||||
|
watch(() => dragStore.selectedNode, () => {
|
||||||
|
updateZonePosition()
|
||||||
|
}, { immediate: true })
|
||||||
|
|
||||||
|
// 当前选中节点的拖放方向选项
|
||||||
|
const directions = computed(() => dragStore.getDropDirections)
|
||||||
|
|
||||||
|
// 是否是行(上下布局)
|
||||||
|
const isRowLayout = computed(() => {
|
||||||
|
return dragStore.selectedNode?.type === 'er'
|
||||||
|
})
|
||||||
|
|
||||||
|
// 处理区域悬停
|
||||||
|
const handleZoneEnter = (direction: DropDirection) => {
|
||||||
|
dragStore.setHoverDirection(direction)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理区域离开
|
||||||
|
const handleZoneLeave = () => {
|
||||||
|
dragStore.setHoverDirection(null)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 处理区域点击(确认放置)
|
||||||
|
const handleZoneClick = (direction: DropDirection) => {
|
||||||
|
dragStore.setHoverDirection(direction)
|
||||||
|
dragStore.confirmDrop()
|
||||||
|
dragStore.endDrag()
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取方向文本
|
||||||
|
const getDirectionText = (direction: DropDirection): string => {
|
||||||
|
const texts: Record<DropDirection, string> = {
|
||||||
|
'top': '移动至上方',
|
||||||
|
'bottom': '移动至下方',
|
||||||
|
'left': '移动至左侧',
|
||||||
|
'right': '移动至右侧'
|
||||||
|
}
|
||||||
|
return texts[direction]
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取方向图标
|
||||||
|
const getDirectionIcon = (direction: DropDirection): string => {
|
||||||
|
const icons: Record<DropDirection, string> = {
|
||||||
|
'top': '⬆',
|
||||||
|
'bottom': '⬇',
|
||||||
|
'left': '⬅',
|
||||||
|
'right': '➡'
|
||||||
|
}
|
||||||
|
return icons[direction]
|
||||||
|
}
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<template>
|
||||||
|
<Teleport to="body">
|
||||||
|
<div
|
||||||
|
v-if="dragStore.isDragging && dragStore.selectedNode"
|
||||||
|
class="drop-zone-container"
|
||||||
|
:class="{ 'is-row': isRowLayout, 'is-col': !isRowLayout }"
|
||||||
|
:style="zoneStyle"
|
||||||
|
>
|
||||||
|
<div
|
||||||
|
v-for="direction in directions"
|
||||||
|
:key="direction"
|
||||||
|
class="drop-zone"
|
||||||
|
:class="[
|
||||||
|
`zone-${direction}`,
|
||||||
|
{ 'is-active': dragStore.hoverDirection === direction }
|
||||||
|
]"
|
||||||
|
@mouseenter="handleZoneEnter(direction)"
|
||||||
|
@mouseleave="handleZoneLeave"
|
||||||
|
@click.stop="handleZoneClick(direction)"
|
||||||
|
>
|
||||||
|
<span class="zone-icon">{{ getDirectionIcon(direction) }}</span>
|
||||||
|
<span class="zone-text">{{ getDirectionText(direction) }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Teleport>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<style scoped>
|
||||||
|
.drop-zone-container {
|
||||||
|
position: absolute;
|
||||||
|
top: 0;
|
||||||
|
left: 0;
|
||||||
|
right: 0;
|
||||||
|
bottom: 0;
|
||||||
|
display: flex;
|
||||||
|
pointer-events: none;
|
||||||
|
z-index: 100;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 行布局:上下排列 */
|
||||||
|
.drop-zone-container.is-row {
|
||||||
|
flex-direction: column;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 列布局:左右排列 */
|
||||||
|
.drop-zone-container.is-col {
|
||||||
|
flex-direction: row;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone {
|
||||||
|
flex: 1;
|
||||||
|
display: flex;
|
||||||
|
flex-direction: column;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;
|
||||||
|
gap: 4px;
|
||||||
|
background: rgba(64, 158, 255, 0.1);
|
||||||
|
border: 2px dashed rgba(64, 158, 255, 0.5);
|
||||||
|
transition: all 0.2s;
|
||||||
|
pointer-events: auto;
|
||||||
|
cursor: pointer;
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 上方区域 */
|
||||||
|
.zone-top {
|
||||||
|
border-bottom: 2px solid rgba(64, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 下方区域 */
|
||||||
|
.zone-bottom {
|
||||||
|
border-top: 2px solid rgba(64, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 左侧区域 */
|
||||||
|
.zone-left {
|
||||||
|
border-right: 2px solid rgba(64, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
/* 右侧区域 */
|
||||||
|
.zone-right {
|
||||||
|
border-left: 2px solid rgba(64, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover,
|
||||||
|
.drop-zone.is-active {
|
||||||
|
background: rgba(64, 158, 255, 0.25);
|
||||||
|
border-color: rgba(64, 158, 255, 0.8);
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-icon {
|
||||||
|
font-size: 24px;
|
||||||
|
color: #409eff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.zone-text {
|
||||||
|
font-size: 12px;
|
||||||
|
color: #409eff;
|
||||||
|
font-weight: 500;
|
||||||
|
}
|
||||||
|
|
||||||
|
.drop-zone:hover .zone-icon,
|
||||||
|
.drop-zone:hover .zone-text,
|
||||||
|
.drop-zone.is-active .zone-icon,
|
||||||
|
.drop-zone.is-active .zone-text {
|
||||||
|
color: #66b1ff;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -1,13 +1,16 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||||
import { useInteractionStore, generateElementPath } from '../../plugins'
|
import { useInteractionStore, useDragStore, generateElementPath } from '../../plugins'
|
||||||
import type { ElementType, InteractionTarget } from '../../plugins'
|
import type { ElementType, InteractionTarget } from '../../plugins'
|
||||||
|
import DropZone from './DropZone.vue'
|
||||||
|
import DragPreview from './DragPreview.vue'
|
||||||
|
|
||||||
const props = defineProps<{
|
const props = defineProps<{
|
||||||
component: any
|
component: any
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const interactionStore = useInteractionStore()
|
const interactionStore = useInteractionStore()
|
||||||
|
const dragStore = useDragStore()
|
||||||
const containerRef = ref<HTMLElement | null>(null)
|
const containerRef = ref<HTMLElement | null>(null)
|
||||||
|
|
||||||
// 存储所有绑定的事件清理函数
|
// 存储所有绑定的事件清理函数
|
||||||
@@ -16,6 +19,12 @@ const cleanupFunctions: (() => void)[] = []
|
|||||||
// MutationObserver 用于监听DOM变化
|
// MutationObserver 用于监听DOM变化
|
||||||
let observer: MutationObserver | null = null
|
let observer: MutationObserver | null = null
|
||||||
|
|
||||||
|
// 当前选中的节点(用于显示DropZone的位置)
|
||||||
|
const selectedNodeRect = computed(() => {
|
||||||
|
if (!dragStore.selectedNode?.element) return null
|
||||||
|
return dragStore.selectedNode.element.getBoundingClientRect()
|
||||||
|
})
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* 为元素绑定交互事件
|
* 为元素绑定交互事件
|
||||||
*/
|
*/
|
||||||
@@ -40,20 +49,33 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
// 鼠标悬停
|
// 鼠标悬停
|
||||||
const handleMouseEnter = (e: MouseEvent) => {
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
interactionStore.onHover(target)
|
|
||||||
element.classList.add('fauto-hover')
|
// 如果正在拖拽,更新层级节点
|
||||||
|
if (dragStore.isDragging) {
|
||||||
|
dragStore.updateHierarchy(element)
|
||||||
|
} else {
|
||||||
|
interactionStore.onHover(target)
|
||||||
|
element.classList.add('fauto-hover')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标离开
|
// 鼠标离开
|
||||||
const handleMouseLeave = (e: MouseEvent) => {
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
interactionStore.onLeave()
|
|
||||||
element.classList.remove('fauto-hover')
|
if (!dragStore.isDragging) {
|
||||||
|
interactionStore.onLeave()
|
||||||
|
element.classList.remove('fauto-hover')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标点击
|
// 鼠标点击
|
||||||
const handleClick = (e: MouseEvent) => {
|
const handleClick = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
|
// 如果正在拖拽,不处理点击
|
||||||
|
if (dragStore.isDragging) return
|
||||||
|
|
||||||
interactionStore.onClick(target)
|
interactionStore.onClick(target)
|
||||||
|
|
||||||
// 移除其他元素的选中状态
|
// 移除其他元素的选中状态
|
||||||
@@ -63,23 +85,15 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
element.classList.add('fauto-selected')
|
element.classList.add('fauto-selected')
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标按下(长按检测)
|
// 鼠标按下(画布内元素拖拽)
|
||||||
const handleMouseDown = (e: MouseEvent) => {
|
const handleMouseDown = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
interactionStore.onMouseDown(target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标移动(拖拽检测)
|
// 如果已经有拖拽源,不处理
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
if (dragStore.isDragging) return
|
||||||
if (interactionStore.isLongPressing) {
|
|
||||||
e.stopPropagation()
|
|
||||||
interactionStore.onMouseMove(target)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标松开
|
// 开始画布内元素拖拽
|
||||||
const handleMouseUp = (e: MouseEvent) => {
|
dragStore.startDragFromCanvas(path, type)
|
||||||
interactionStore.onMouseUp()
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
@@ -87,8 +101,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
element.addEventListener('mouseleave', handleMouseLeave)
|
element.addEventListener('mouseleave', handleMouseLeave)
|
||||||
element.addEventListener('click', handleClick)
|
element.addEventListener('click', handleClick)
|
||||||
element.addEventListener('mousedown', handleMouseDown)
|
element.addEventListener('mousedown', handleMouseDown)
|
||||||
element.addEventListener('mousemove', handleMouseMove)
|
|
||||||
element.addEventListener('mouseup', handleMouseUp)
|
|
||||||
|
|
||||||
console.log(`[注入] ${type} 路径: ${path}`)
|
console.log(`[注入] ${type} 路径: ${path}`)
|
||||||
|
|
||||||
@@ -98,8 +110,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
element.removeEventListener('mouseleave', handleMouseLeave)
|
element.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
element.removeEventListener('click', handleClick)
|
element.removeEventListener('click', handleClick)
|
||||||
element.removeEventListener('mousedown', handleMouseDown)
|
element.removeEventListener('mousedown', handleMouseDown)
|
||||||
element.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
element.removeEventListener('mouseup', handleMouseUp)
|
|
||||||
element.classList.remove('fauto-interactive', 'fauto-hover', 'fauto-selected')
|
element.classList.remove('fauto-interactive', 'fauto-hover', 'fauto-selected')
|
||||||
element.removeAttribute('data-fauto-bindend')
|
element.removeAttribute('data-fauto-bindend')
|
||||||
}
|
}
|
||||||
@@ -167,6 +177,65 @@ const startObserver = () => {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 键盘事件处理
|
||||||
|
*/
|
||||||
|
const handleKeyDown = (e: KeyboardEvent) => {
|
||||||
|
// 只在拖拽时响应键盘事件
|
||||||
|
if (!dragStore.isDragging) return
|
||||||
|
|
||||||
|
if (e.key === 'ArrowUp') {
|
||||||
|
e.preventDefault()
|
||||||
|
dragStore.selectParentLevel()
|
||||||
|
updateSelectedHighlight()
|
||||||
|
} else if (e.key === 'ArrowDown') {
|
||||||
|
e.preventDefault()
|
||||||
|
dragStore.selectChildLevel()
|
||||||
|
updateSelectedHighlight()
|
||||||
|
} else if (e.key === 'Escape') {
|
||||||
|
e.preventDefault()
|
||||||
|
dragStore.cancelDrag()
|
||||||
|
clearDragHighlight()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新选中层级的高亮显示
|
||||||
|
*/
|
||||||
|
const updateSelectedHighlight = () => {
|
||||||
|
// 清除所有拖拽高亮
|
||||||
|
document.querySelectorAll('.fauto-drop-target').forEach(el => {
|
||||||
|
el.classList.remove('fauto-drop-target')
|
||||||
|
})
|
||||||
|
|
||||||
|
// 高亮当前选中的节点
|
||||||
|
if (dragStore.selectedNode?.element) {
|
||||||
|
dragStore.selectedNode.element.classList.add('fauto-drop-target')
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清除拖拽高亮
|
||||||
|
*/
|
||||||
|
const clearDragHighlight = () => {
|
||||||
|
document.querySelectorAll('.fauto-drop-target').forEach(el => {
|
||||||
|
el.classList.remove('fauto-drop-target')
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 全局鼠标松开处理
|
||||||
|
*/
|
||||||
|
const handleGlobalMouseUp = () => {
|
||||||
|
if (dragStore.isDragging && dragStore.selectedNode && dragStore.hoverDirection) {
|
||||||
|
// 确认拖放
|
||||||
|
dragStore.confirmDrop()
|
||||||
|
}
|
||||||
|
|
||||||
|
dragStore.endDrag()
|
||||||
|
clearDragHighlight()
|
||||||
|
}
|
||||||
|
|
||||||
// 组件挂载后注入事件
|
// 组件挂载后注入事件
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 等待异步组件加载完成,使用多次nextTick和延时
|
// 等待异步组件加载完成,使用多次nextTick和延时
|
||||||
@@ -176,6 +245,11 @@ onMounted(() => {
|
|||||||
startObserver()
|
startObserver()
|
||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 添加键盘事件监听
|
||||||
|
document.addEventListener('keydown', handleKeyDown)
|
||||||
|
// 添加全局鼠标松开事件
|
||||||
|
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 组件卸载时清理事件
|
// 组件卸载时清理事件
|
||||||
@@ -187,6 +261,10 @@ onUnmounted(() => {
|
|||||||
observer.disconnect()
|
observer.disconnect()
|
||||||
observer = null
|
observer = null
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 移除键盘事件监听
|
||||||
|
document.removeEventListener('keydown', handleKeyDown)
|
||||||
|
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||||
})
|
})
|
||||||
|
|
||||||
// 监听组件变化,重新注入事件
|
// 监听组件变化,重新注入事件
|
||||||
@@ -206,6 +284,26 @@ watch(() => props.component, () => {
|
|||||||
<template>
|
<template>
|
||||||
<div ref="containerRef" class="interactive-wrapper">
|
<div ref="containerRef" class="interactive-wrapper">
|
||||||
<component :is="component" />
|
<component :is="component" />
|
||||||
|
|
||||||
|
<!-- 拖放区域指示器 -->
|
||||||
|
<DropZone />
|
||||||
|
|
||||||
|
<!-- 拖拽预览(跟随鼠标) -->
|
||||||
|
<DragPreview />
|
||||||
|
|
||||||
|
<!-- 拖拽时的层级选择提示 -->
|
||||||
|
<div v-if="dragStore.isDragging && dragStore.hierarchyNodes.length > 1" class="hierarchy-hint">
|
||||||
|
<div class="hint-title">⌨️ 用上下键切换层级</div>
|
||||||
|
<div
|
||||||
|
v-for="(node, index) in dragStore.hierarchyNodes"
|
||||||
|
:key="node.path"
|
||||||
|
class="hint-item"
|
||||||
|
:class="{ 'is-selected': index === dragStore.selectedHierarchyIndex }"
|
||||||
|
>
|
||||||
|
<span class="hint-type">{{ node.type === 'er' ? 'Row' : 'Col' }}</span>
|
||||||
|
<span class="hint-path">{{ node.path }}</span>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
@@ -214,6 +312,7 @@ watch(() => props.component, () => {
|
|||||||
.interactive-wrapper {
|
.interactive-wrapper {
|
||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.fauto-interactive {
|
.fauto-interactive {
|
||||||
@@ -234,6 +333,14 @@ watch(() => props.component, () => {
|
|||||||
box-shadow: 0 0 8px rgba(103, 194, 58, 0.4);
|
box-shadow: 0 0 8px rgba(103, 194, 58, 0.4);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 拖放目标样式 */
|
||||||
|
.fauto-interactive.fauto-drop-target {
|
||||||
|
outline: 3px solid #e6a23c;
|
||||||
|
outline-offset: -3px;
|
||||||
|
z-index: 30;
|
||||||
|
background-color: rgba(230, 162, 60, 0.1) !important;
|
||||||
|
}
|
||||||
|
|
||||||
/* el-row 悬停时的特殊样式 */
|
/* el-row 悬停时的特殊样式 */
|
||||||
.el-row.fauto-hover {
|
.el-row.fauto-hover {
|
||||||
background-color: rgba(64, 158, 255, 0.05);
|
background-color: rgba(64, 158, 255, 0.05);
|
||||||
@@ -251,4 +358,57 @@ watch(() => props.component, () => {
|
|||||||
.el-col.fauto-selected {
|
.el-col.fauto-selected {
|
||||||
background-color: rgba(103, 194, 58, 0.1);
|
background-color: rgba(103, 194, 58, 0.1);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 层级选择提示 */
|
||||||
|
.hierarchy-hint {
|
||||||
|
position: fixed;
|
||||||
|
top: 80px;
|
||||||
|
right: 20px;
|
||||||
|
background: rgba(0, 0, 0, 0.85);
|
||||||
|
border-radius: 8px;
|
||||||
|
padding: 12px;
|
||||||
|
z-index: 1000;
|
||||||
|
min-width: 180px;
|
||||||
|
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.3);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-title {
|
||||||
|
color: #fff;
|
||||||
|
font-size: 12px;
|
||||||
|
margin-bottom: 8px;
|
||||||
|
padding-bottom: 8px;
|
||||||
|
border-bottom: 1px solid rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item {
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
gap: 8px;
|
||||||
|
padding: 6px 8px;
|
||||||
|
border-radius: 4px;
|
||||||
|
margin-bottom: 4px;
|
||||||
|
color: #999;
|
||||||
|
font-size: 12px;
|
||||||
|
transition: all 0.2s;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item.is-selected {
|
||||||
|
background: #409eff;
|
||||||
|
color: #fff;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-type {
|
||||||
|
padding: 2px 6px;
|
||||||
|
background: rgba(255, 255, 255, 0.1);
|
||||||
|
border-radius: 3px;
|
||||||
|
font-size: 11px;
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-item.is-selected .hint-type {
|
||||||
|
background: rgba(255, 255, 255, 0.2);
|
||||||
|
}
|
||||||
|
|
||||||
|
.hint-path {
|
||||||
|
font-family: monospace;
|
||||||
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +1,33 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { onMounted } from 'vue'
|
import { onMounted, onUnmounted, ref } from 'vue'
|
||||||
import { useDesignStore } from '../../stores/designStore'
|
import { useDesignStore } from '../../stores/designStore'
|
||||||
import { useInteractionStore } from '../../plugins'
|
import { useInteractionStore, useDragStore } from '../../plugins'
|
||||||
import type { InteractionTarget } from '../../plugins'
|
import type { InteractionTarget } from '../../plugins'
|
||||||
import config from './index.json'
|
import config from './index.json'
|
||||||
|
|
||||||
const designStore = useDesignStore()
|
const designStore = useDesignStore()
|
||||||
const interactionStore = useInteractionStore()
|
const interactionStore = useInteractionStore()
|
||||||
|
const dragStore = useDragStore()
|
||||||
|
|
||||||
|
// 拖拽状态
|
||||||
|
const draggingId = ref<string | null>(null)
|
||||||
|
let dragStartTimer: number | null = null
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
// 确保设计组件元数据已加载
|
// 确保设计组件元数据已加载
|
||||||
if (designStore.componentMetas.length === 0) {
|
if (designStore.componentMetas.length === 0) {
|
||||||
designStore.loadComponentMetas()
|
designStore.loadComponentMetas()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 添加全局鼠标松开事件
|
||||||
|
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||||
|
})
|
||||||
|
|
||||||
|
onUnmounted(() => {
|
||||||
|
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||||
|
if (dragStartTimer) {
|
||||||
|
clearTimeout(dragStartTimer)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const handleAddComponent = (componentId: string) => {
|
const handleAddComponent = (componentId: string) => {
|
||||||
@@ -36,6 +51,9 @@ const handleMouseLeave = () => {
|
|||||||
|
|
||||||
// 鼠标点击设计组件
|
// 鼠标点击设计组件
|
||||||
const handleClick = (componentId: string, componentName: string) => {
|
const handleClick = (componentId: string, componentName: string) => {
|
||||||
|
// 如果正在拖拽,不触发点击
|
||||||
|
if (dragStore.isDragging) return
|
||||||
|
|
||||||
const target: InteractionTarget = {
|
const target: InteractionTarget = {
|
||||||
type: 'dc',
|
type: 'dc',
|
||||||
path: componentId,
|
path: componentId,
|
||||||
@@ -45,6 +63,24 @@ const handleClick = (componentId: string, componentName: string) => {
|
|||||||
// 添加组件
|
// 添加组件
|
||||||
designStore.addComponent(componentId)
|
designStore.addComponent(componentId)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 鼠标按下 - 开始检测拖拽
|
||||||
|
const handleMouseDown = (e: MouseEvent, componentId: string, componentName: string) => {
|
||||||
|
e.preventDefault()
|
||||||
|
|
||||||
|
// 立即开始拖拽(不需要长按)
|
||||||
|
draggingId.value = componentId
|
||||||
|
dragStore.startDragFromComponentList(componentId, componentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
// 全局鼠标松开
|
||||||
|
const handleGlobalMouseUp = () => {
|
||||||
|
if (dragStartTimer) {
|
||||||
|
clearTimeout(dragStartTimer)
|
||||||
|
dragStartTimer = null
|
||||||
|
}
|
||||||
|
draggingId.value = null
|
||||||
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
@@ -58,16 +94,18 @@ const handleClick = (componentId: string, componentName: string) => {
|
|||||||
v-for="meta in designStore.componentMetas"
|
v-for="meta in designStore.componentMetas"
|
||||||
:key="meta.id"
|
:key="meta.id"
|
||||||
class="component-item"
|
class="component-item"
|
||||||
|
:class="{ 'is-dragging': draggingId === meta.id }"
|
||||||
@click="handleClick(meta.id, meta.name)"
|
@click="handleClick(meta.id, meta.name)"
|
||||||
@mouseenter="handleMouseEnter(meta.id, meta.name)"
|
@mouseenter="handleMouseEnter(meta.id, meta.name)"
|
||||||
@mouseleave="handleMouseLeave"
|
@mouseleave="handleMouseLeave"
|
||||||
|
@mousedown="handleMouseDown($event, meta.id, meta.name)"
|
||||||
>
|
>
|
||||||
<div class="component-icon">📦</div>
|
<div class="component-icon">📦</div>
|
||||||
<div class="component-info">
|
<div class="component-info">
|
||||||
<div class="component-name">{{ meta.name }}</div>
|
<div class="component-name">{{ meta.name }}</div>
|
||||||
<div class="component-desc">{{ meta.description }}</div>
|
<div class="component-desc">{{ meta.description }}</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="add-btn">+</div>
|
<div class="drag-handle">✥</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<div v-if="designStore.componentMetas.length === 0" class="empty-tip">
|
<div v-if="designStore.componentMetas.length === 0" class="empty-tip">
|
||||||
@@ -83,6 +121,7 @@ const handleClick = (componentId: string, componentName: string) => {
|
|||||||
flex-direction: column;
|
flex-direction: column;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
background: #1e1e1e;
|
background: #1e1e1e;
|
||||||
|
position: relative;
|
||||||
}
|
}
|
||||||
|
|
||||||
.list-header {
|
.list-header {
|
||||||
@@ -119,15 +158,21 @@ const handleClick = (componentId: string, componentName: string) => {
|
|||||||
background: #252526;
|
background: #252526;
|
||||||
border-radius: 6px;
|
border-radius: 6px;
|
||||||
margin-bottom: 8px;
|
margin-bottom: 8px;
|
||||||
cursor: pointer;
|
cursor: grab;
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
|
user-select: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.component-item.is-dragging {
|
||||||
|
opacity: 0.6;
|
||||||
|
cursor: grabbing;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-item:hover {
|
.component-item:hover {
|
||||||
background: #2a2d2e;
|
background: #2a2d2e;
|
||||||
}
|
}
|
||||||
|
|
||||||
.component-item:hover .add-btn {
|
.component-item:hover .drag-handle {
|
||||||
opacity: 1;
|
opacity: 1;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -151,23 +196,20 @@ const handleClick = (componentId: string, componentName: string) => {
|
|||||||
font-size: 11px;
|
font-size: 11px;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn {
|
.drag-handle {
|
||||||
width: 24px;
|
width: 24px;
|
||||||
height: 24px;
|
height: 24px;
|
||||||
display: flex;
|
display: flex;
|
||||||
align-items: center;
|
align-items: center;
|
||||||
justify-content: center;
|
justify-content: center;
|
||||||
background: #007acc;
|
color: #888888;
|
||||||
color: white;
|
font-size: 14px;
|
||||||
border-radius: 4px;
|
|
||||||
font-size: 16px;
|
|
||||||
font-weight: bold;
|
|
||||||
opacity: 0;
|
opacity: 0;
|
||||||
transition: opacity 0.2s;
|
transition: opacity 0.2s;
|
||||||
}
|
}
|
||||||
|
|
||||||
.add-btn:hover {
|
.drag-handle:hover {
|
||||||
background: #0088e0;
|
color: #cccccc;
|
||||||
}
|
}
|
||||||
|
|
||||||
.empty-tip {
|
.empty-tip {
|
||||||
|
|||||||
294
draggable-panels/src/fauto/plugins/dragStore.ts
Normal file
294
draggable-panels/src/fauto/plugins/dragStore.ts
Normal file
@@ -0,0 +1,294 @@
|
|||||||
|
import { defineStore } from 'pinia'
|
||||||
|
import { ref, computed } from 'vue'
|
||||||
|
import type { InteractionTarget, ElementType } from './interactionStore'
|
||||||
|
import { parsePath } from './pathUtils'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖放方向
|
||||||
|
*/
|
||||||
|
export type DropDirection = 'top' | 'bottom' | 'left' | 'right'
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽源信息
|
||||||
|
*/
|
||||||
|
export interface DragSource {
|
||||||
|
type: 'design-component' | 'canvas-element' // 来源类型
|
||||||
|
componentId?: string // 设计组件ID(设计组件列表)
|
||||||
|
componentName?: string // 设计组件名称
|
||||||
|
path?: string // 元素路径(画布内元素)
|
||||||
|
elementType?: ElementType // 元素类型
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 层级节点信息
|
||||||
|
*/
|
||||||
|
export interface HierarchyNode {
|
||||||
|
path: string // 结构化路径
|
||||||
|
type: ElementType // 类型 er/ec
|
||||||
|
element: HTMLElement // DOM元素
|
||||||
|
depth: number // 深度
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖放记录
|
||||||
|
*/
|
||||||
|
export interface DropRecord {
|
||||||
|
source: DragSource // 拖拽源
|
||||||
|
targetPath: string // 目标路径
|
||||||
|
targetType: ElementType // 目标类型
|
||||||
|
direction: DropDirection // 拖放方向
|
||||||
|
timestamp: number // 时间戳
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 拖拽管理Store
|
||||||
|
*/
|
||||||
|
export const useDragStore = defineStore('drag', () => {
|
||||||
|
// ========== 拖拽状态 ==========
|
||||||
|
|
||||||
|
// 是否正在拖拽
|
||||||
|
const isDragging = ref(false)
|
||||||
|
|
||||||
|
// 拖拽源
|
||||||
|
const dragSource = ref<DragSource | null>(null)
|
||||||
|
|
||||||
|
// ========== 层级选择状态 ==========
|
||||||
|
|
||||||
|
// 当前悬停位置的所有层级节点(从深到浅排序)
|
||||||
|
const hierarchyNodes = ref<HierarchyNode[]>([])
|
||||||
|
|
||||||
|
// 当前选中的层级索引(0表示最深层级)
|
||||||
|
const selectedHierarchyIndex = ref(0)
|
||||||
|
|
||||||
|
// 当前选中的层级节点
|
||||||
|
const selectedNode = computed(() => {
|
||||||
|
if (hierarchyNodes.value.length === 0) return null
|
||||||
|
const index = Math.min(selectedHierarchyIndex.value, hierarchyNodes.value.length - 1)
|
||||||
|
return hierarchyNodes.value[index]
|
||||||
|
})
|
||||||
|
|
||||||
|
// ========== 拖放区域状态 ==========
|
||||||
|
|
||||||
|
// 当前悬停的拖放方向
|
||||||
|
const hoverDirection = ref<DropDirection | null>(null)
|
||||||
|
|
||||||
|
// ========== 拖放记录 ==========
|
||||||
|
|
||||||
|
// 最后一条拖放记录
|
||||||
|
const lastDropRecord = ref<DropRecord | null>(null)
|
||||||
|
|
||||||
|
// 所有拖放记录
|
||||||
|
const dropRecords = ref<DropRecord[]>([])
|
||||||
|
|
||||||
|
// ========== 方法 ==========
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始拖拽(从设计组件列表)
|
||||||
|
*/
|
||||||
|
const startDragFromComponentList = (componentId: string, componentName: string) => {
|
||||||
|
isDragging.value = true
|
||||||
|
dragSource.value = {
|
||||||
|
type: 'design-component',
|
||||||
|
componentId,
|
||||||
|
componentName
|
||||||
|
}
|
||||||
|
console.log('[DragStore] 开始拖拽设计组件:', componentName)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 开始拖拽(从画布内元素)
|
||||||
|
*/
|
||||||
|
const startDragFromCanvas = (path: string, elementType: ElementType) => {
|
||||||
|
isDragging.value = true
|
||||||
|
dragSource.value = {
|
||||||
|
type: 'canvas-element',
|
||||||
|
path,
|
||||||
|
elementType
|
||||||
|
}
|
||||||
|
console.log('[DragStore] 开始拖拽画布元素:', path)
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 更新层级节点列表
|
||||||
|
* @param element 当前悬停的元素
|
||||||
|
*/
|
||||||
|
const updateHierarchy = (element: HTMLElement) => {
|
||||||
|
const nodes: HierarchyNode[] = []
|
||||||
|
let current: HTMLElement | null = element
|
||||||
|
|
||||||
|
while (current) {
|
||||||
|
const isRow = current.classList.contains('el-row')
|
||||||
|
const isCol = current.classList.contains('el-col')
|
||||||
|
|
||||||
|
if (isRow || isCol) {
|
||||||
|
const path = current.getAttribute('data-path') || ''
|
||||||
|
const type: ElementType = isRow ? 'er' : 'ec'
|
||||||
|
const depth = parsePath(path).length
|
||||||
|
|
||||||
|
nodes.push({
|
||||||
|
path,
|
||||||
|
type,
|
||||||
|
element: current,
|
||||||
|
depth
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
current = current.parentElement
|
||||||
|
}
|
||||||
|
|
||||||
|
// 按深度从深到浅排序(最深的在前面)
|
||||||
|
nodes.sort((a, b) => b.depth - a.depth)
|
||||||
|
|
||||||
|
hierarchyNodes.value = nodes
|
||||||
|
selectedHierarchyIndex.value = 0 // 默认选中最深层级
|
||||||
|
|
||||||
|
console.log('[DragStore] 更新层级:', nodes.map(n => n.path))
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 清空层级节点
|
||||||
|
*/
|
||||||
|
const clearHierarchy = () => {
|
||||||
|
hierarchyNodes.value = []
|
||||||
|
selectedHierarchyIndex.value = 0
|
||||||
|
hoverDirection.value = null
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择上一层级(键盘↑)
|
||||||
|
*/
|
||||||
|
const selectParentLevel = () => {
|
||||||
|
if (hierarchyNodes.value.length === 0) return
|
||||||
|
|
||||||
|
if (selectedHierarchyIndex.value < hierarchyNodes.value.length - 1) {
|
||||||
|
selectedHierarchyIndex.value++
|
||||||
|
console.log('[DragStore] 选择上层:', selectedNode.value?.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 选择下一层级(键盘↓)
|
||||||
|
*/
|
||||||
|
const selectChildLevel = () => {
|
||||||
|
if (hierarchyNodes.value.length === 0) return
|
||||||
|
|
||||||
|
if (selectedHierarchyIndex.value > 0) {
|
||||||
|
selectedHierarchyIndex.value--
|
||||||
|
console.log('[DragStore] 选择下层:', selectedNode.value?.path)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 设置悬停方向
|
||||||
|
*/
|
||||||
|
const setHoverDirection = (direction: DropDirection | null) => {
|
||||||
|
hoverDirection.value = direction
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 确认拖放
|
||||||
|
*/
|
||||||
|
const confirmDrop = () => {
|
||||||
|
if (!isDragging.value || !dragSource.value || !selectedNode.value || !hoverDirection.value) {
|
||||||
|
console.log('[DragStore] 拖放条件不满足')
|
||||||
|
return null
|
||||||
|
}
|
||||||
|
|
||||||
|
const record: DropRecord = {
|
||||||
|
source: { ...dragSource.value },
|
||||||
|
targetPath: selectedNode.value.path,
|
||||||
|
targetType: selectedNode.value.type,
|
||||||
|
direction: hoverDirection.value,
|
||||||
|
timestamp: Date.now()
|
||||||
|
}
|
||||||
|
|
||||||
|
dropRecords.value.push(record)
|
||||||
|
lastDropRecord.value = record
|
||||||
|
|
||||||
|
console.log('[DragStore] 确认拖放:', record)
|
||||||
|
|
||||||
|
return record
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 取消拖拽
|
||||||
|
*/
|
||||||
|
const cancelDrag = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
dragSource.value = null
|
||||||
|
clearHierarchy()
|
||||||
|
console.log('[DragStore] 取消拖拽')
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 结束拖拽
|
||||||
|
*/
|
||||||
|
const endDrag = () => {
|
||||||
|
isDragging.value = false
|
||||||
|
dragSource.value = null
|
||||||
|
clearHierarchy()
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 格式化拖放记录为显示文本
|
||||||
|
*/
|
||||||
|
const formatDropRecord = (record: DropRecord | null): string => {
|
||||||
|
if (!record) return ''
|
||||||
|
|
||||||
|
const directionNames: Record<DropDirection, string> = {
|
||||||
|
'top': '上方',
|
||||||
|
'bottom': '下方',
|
||||||
|
'left': '左侧',
|
||||||
|
'right': '右侧'
|
||||||
|
}
|
||||||
|
|
||||||
|
let sourceName = ''
|
||||||
|
if (record.source.type === 'design-component') {
|
||||||
|
sourceName = record.source.componentName || record.source.componentId || '设计组件'
|
||||||
|
} else {
|
||||||
|
sourceName = record.source.path || '画布元素'
|
||||||
|
}
|
||||||
|
|
||||||
|
return `${sourceName} → ${record.targetPath} ${directionNames[record.direction]}`
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 获取当前选中节点应该显示的拖放方向选项
|
||||||
|
*/
|
||||||
|
const getDropDirections = computed((): DropDirection[] => {
|
||||||
|
if (!selectedNode.value) return []
|
||||||
|
|
||||||
|
// el-row 显示上下
|
||||||
|
if (selectedNode.value.type === 'er') {
|
||||||
|
return ['top', 'bottom']
|
||||||
|
}
|
||||||
|
// el-col 显示左右
|
||||||
|
return ['left', 'right']
|
||||||
|
})
|
||||||
|
|
||||||
|
return {
|
||||||
|
// 状态
|
||||||
|
isDragging,
|
||||||
|
dragSource,
|
||||||
|
hierarchyNodes,
|
||||||
|
selectedHierarchyIndex,
|
||||||
|
selectedNode,
|
||||||
|
hoverDirection,
|
||||||
|
lastDropRecord,
|
||||||
|
dropRecords,
|
||||||
|
getDropDirections,
|
||||||
|
|
||||||
|
// 方法
|
||||||
|
startDragFromComponentList,
|
||||||
|
startDragFromCanvas,
|
||||||
|
updateHierarchy,
|
||||||
|
clearHierarchy,
|
||||||
|
selectParentLevel,
|
||||||
|
selectChildLevel,
|
||||||
|
setHoverDirection,
|
||||||
|
confirmDrop,
|
||||||
|
cancelDrag,
|
||||||
|
endDrag,
|
||||||
|
formatDropRecord
|
||||||
|
}
|
||||||
|
})
|
||||||
@@ -14,6 +14,15 @@ export type {
|
|||||||
HookCallback
|
HookCallback
|
||||||
} from './interactionStore'
|
} from './interactionStore'
|
||||||
|
|
||||||
|
// 导出拖拽Store
|
||||||
|
export { useDragStore } from './dragStore'
|
||||||
|
export type {
|
||||||
|
DropDirection,
|
||||||
|
DragSource,
|
||||||
|
HierarchyNode,
|
||||||
|
DropRecord
|
||||||
|
} from './dragStore'
|
||||||
|
|
||||||
// 导出路径工具
|
// 导出路径工具
|
||||||
export {
|
export {
|
||||||
parsePath,
|
parsePath,
|
||||||
|
|||||||
@@ -29,3 +29,18 @@ html, body {
|
|||||||
width: 100%;
|
width: 100%;
|
||||||
height: 100%;
|
height: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/* 拖拽时禁用文本选择 */
|
||||||
|
body.is-dragging {
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
-moz-user-select: none !important;
|
||||||
|
-ms-user-select: none !important;
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.is-dragging * {
|
||||||
|
user-select: none !important;
|
||||||
|
-webkit-user-select: none !important;
|
||||||
|
cursor: grabbing !important;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user