2
This commit is contained in:
@@ -141,5 +141,5 @@
|
||||
"activeTabId": "mxfx11j"
|
||||
}
|
||||
},
|
||||
"lastUpdated": "2025-12-21T13:24:40.794Z"
|
||||
"lastUpdated": "2025-12-22T14:24:28.202Z"
|
||||
}
|
||||
@@ -67,5 +67,5 @@
|
||||
}
|
||||
],
|
||||
"selectedId": "jy87mdv",
|
||||
"lastUpdated": "2025-12-21T13:23:32.873Z"
|
||||
"lastUpdated": "2025-12-22T14:24:28.783Z"
|
||||
}
|
||||
@@ -1,8 +1,9 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useInteractionStore } from '../plugins'
|
||||
import { useInteractionStore, useDragStore } from '../plugins'
|
||||
|
||||
const interactionStore = useInteractionStore()
|
||||
const dragStore = useDragStore()
|
||||
|
||||
const currentTime = ref('')
|
||||
let timer: number | null = null
|
||||
@@ -49,6 +50,34 @@ const selectedInfo = computed(() => {
|
||||
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(() => {
|
||||
updateTime()
|
||||
timer = window.setInterval(updateTime, 1000)
|
||||
@@ -62,23 +91,45 @@ onUnmounted(() => {
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<footer class="app-footer" :class="{ 'is-dragging': dragStore.isDragging }">
|
||||
<div class="footer-left">
|
||||
<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 v-if="dragStore.isDragging">
|
||||
<span class="info-item drag-info">
|
||||
🎯 {{ dragInfo }}
|
||||
</span>
|
||||
<span v-if="dropTargetInfo" class="info-item target-info">
|
||||
➡️ {{ dropTargetInfo }}
|
||||
</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 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 }}
|
||||
</span>
|
||||
</div>
|
||||
|
||||
<div class="footer-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</div>
|
||||
@@ -142,6 +193,34 @@ onUnmounted(() => {
|
||||
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 {
|
||||
color: white;
|
||||
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">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useInteractionStore, generateElementPath } from '../../plugins'
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick, computed } from 'vue'
|
||||
import { useInteractionStore, useDragStore, generateElementPath } from '../../plugins'
|
||||
import type { ElementType, InteractionTarget } from '../../plugins'
|
||||
import DropZone from './DropZone.vue'
|
||||
import DragPreview from './DragPreview.vue'
|
||||
|
||||
const props = defineProps<{
|
||||
component: any
|
||||
}>()
|
||||
|
||||
const interactionStore = useInteractionStore()
|
||||
const dragStore = useDragStore()
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 存储所有绑定的事件清理函数
|
||||
@@ -16,6 +19,12 @@ const cleanupFunctions: (() => void)[] = []
|
||||
// MutationObserver 用于监听DOM变化
|
||||
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) => {
|
||||
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) => {
|
||||
e.stopPropagation()
|
||||
interactionStore.onLeave()
|
||||
element.classList.remove('fauto-hover')
|
||||
|
||||
if (!dragStore.isDragging) {
|
||||
interactionStore.onLeave()
|
||||
element.classList.remove('fauto-hover')
|
||||
}
|
||||
}
|
||||
|
||||
// 鼠标点击
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
|
||||
// 如果正在拖拽,不处理点击
|
||||
if (dragStore.isDragging) return
|
||||
|
||||
interactionStore.onClick(target)
|
||||
|
||||
// 移除其他元素的选中状态
|
||||
@@ -63,23 +85,15 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
||||
element.classList.add('fauto-selected')
|
||||
}
|
||||
|
||||
// 鼠标按下(长按检测)
|
||||
// 鼠标按下(画布内元素拖拽)
|
||||
const handleMouseDown = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
interactionStore.onMouseDown(target)
|
||||
}
|
||||
|
||||
// 鼠标移动(拖拽检测)
|
||||
const handleMouseMove = (e: MouseEvent) => {
|
||||
if (interactionStore.isLongPressing) {
|
||||
e.stopPropagation()
|
||||
interactionStore.onMouseMove(target)
|
||||
}
|
||||
}
|
||||
// 如果已经有拖拽源,不处理
|
||||
if (dragStore.isDragging) return
|
||||
|
||||
// 鼠标松开
|
||||
const handleMouseUp = (e: MouseEvent) => {
|
||||
interactionStore.onMouseUp()
|
||||
// 开始画布内元素拖拽
|
||||
dragStore.startDragFromCanvas(path, type)
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
@@ -87,8 +101,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
||||
element.addEventListener('mouseleave', handleMouseLeave)
|
||||
element.addEventListener('click', handleClick)
|
||||
element.addEventListener('mousedown', handleMouseDown)
|
||||
element.addEventListener('mousemove', handleMouseMove)
|
||||
element.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
console.log(`[注入] ${type} 路径: ${path}`)
|
||||
|
||||
@@ -98,8 +110,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
||||
element.removeEventListener('mouseleave', handleMouseLeave)
|
||||
element.removeEventListener('click', handleClick)
|
||||
element.removeEventListener('mousedown', handleMouseDown)
|
||||
element.removeEventListener('mousemove', handleMouseMove)
|
||||
element.removeEventListener('mouseup', handleMouseUp)
|
||||
element.classList.remove('fauto-interactive', 'fauto-hover', 'fauto-selected')
|
||||
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(() => {
|
||||
// 等待异步组件加载完成,使用多次nextTick和延时
|
||||
@@ -176,6 +245,11 @@ onMounted(() => {
|
||||
startObserver()
|
||||
}, 100)
|
||||
})
|
||||
|
||||
// 添加键盘事件监听
|
||||
document.addEventListener('keydown', handleKeyDown)
|
||||
// 添加全局鼠标松开事件
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||
})
|
||||
|
||||
// 组件卸载时清理事件
|
||||
@@ -187,6 +261,10 @@ onUnmounted(() => {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
|
||||
// 移除键盘事件监听
|
||||
document.removeEventListener('keydown', handleKeyDown)
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||
})
|
||||
|
||||
// 监听组件变化,重新注入事件
|
||||
@@ -206,6 +284,26 @@ watch(() => props.component, () => {
|
||||
<template>
|
||||
<div ref="containerRef" class="interactive-wrapper">
|
||||
<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>
|
||||
</template>
|
||||
|
||||
@@ -214,6 +312,7 @@ watch(() => props.component, () => {
|
||||
.interactive-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.fauto-interactive {
|
||||
@@ -234,6 +333,14 @@ watch(() => props.component, () => {
|
||||
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.fauto-hover {
|
||||
background-color: rgba(64, 158, 255, 0.05);
|
||||
@@ -251,4 +358,57 @@ watch(() => props.component, () => {
|
||||
.el-col.fauto-selected {
|
||||
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>
|
||||
|
||||
@@ -1,18 +1,33 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { onMounted, onUnmounted, ref } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import { useInteractionStore } from '../../plugins'
|
||||
import { useInteractionStore, useDragStore } from '../../plugins'
|
||||
import type { InteractionTarget } from '../../plugins'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
const interactionStore = useInteractionStore()
|
||||
const dragStore = useDragStore()
|
||||
|
||||
// 拖拽状态
|
||||
const draggingId = ref<string | null>(null)
|
||||
let dragStartTimer: number | null = null
|
||||
|
||||
onMounted(() => {
|
||||
// 确保设计组件元数据已加载
|
||||
if (designStore.componentMetas.length === 0) {
|
||||
designStore.loadComponentMetas()
|
||||
}
|
||||
|
||||
// 添加全局鼠标松开事件
|
||||
document.addEventListener('mouseup', handleGlobalMouseUp)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
document.removeEventListener('mouseup', handleGlobalMouseUp)
|
||||
if (dragStartTimer) {
|
||||
clearTimeout(dragStartTimer)
|
||||
}
|
||||
})
|
||||
|
||||
const handleAddComponent = (componentId: string) => {
|
||||
@@ -36,6 +51,9 @@ const handleMouseLeave = () => {
|
||||
|
||||
// 鼠标点击设计组件
|
||||
const handleClick = (componentId: string, componentName: string) => {
|
||||
// 如果正在拖拽,不触发点击
|
||||
if (dragStore.isDragging) return
|
||||
|
||||
const target: InteractionTarget = {
|
||||
type: 'dc',
|
||||
path: componentId,
|
||||
@@ -45,6 +63,24 @@ const handleClick = (componentId: string, componentName: string) => {
|
||||
// 添加组件
|
||||
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>
|
||||
|
||||
<template>
|
||||
@@ -58,16 +94,18 @@ const handleClick = (componentId: string, componentName: string) => {
|
||||
v-for="meta in designStore.componentMetas"
|
||||
:key="meta.id"
|
||||
class="component-item"
|
||||
:class="{ 'is-dragging': draggingId === meta.id }"
|
||||
@click="handleClick(meta.id, meta.name)"
|
||||
@mouseenter="handleMouseEnter(meta.id, meta.name)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
@mousedown="handleMouseDown($event, meta.id, meta.name)"
|
||||
>
|
||||
<div class="component-icon">📦</div>
|
||||
<div class="component-info">
|
||||
<div class="component-name">{{ meta.name }}</div>
|
||||
<div class="component-desc">{{ meta.description }}</div>
|
||||
</div>
|
||||
<div class="add-btn">+</div>
|
||||
<div class="drag-handle">✥</div>
|
||||
</div>
|
||||
|
||||
<div v-if="designStore.componentMetas.length === 0" class="empty-tip">
|
||||
@@ -83,6 +121,7 @@ const handleClick = (componentId: string, componentName: string) => {
|
||||
flex-direction: column;
|
||||
height: 100%;
|
||||
background: #1e1e1e;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.list-header {
|
||||
@@ -119,15 +158,21 @@ const handleClick = (componentId: string, componentName: string) => {
|
||||
background: #252526;
|
||||
border-radius: 6px;
|
||||
margin-bottom: 8px;
|
||||
cursor: pointer;
|
||||
cursor: grab;
|
||||
transition: all 0.2s;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.component-item.is-dragging {
|
||||
opacity: 0.6;
|
||||
cursor: grabbing;
|
||||
}
|
||||
|
||||
.component-item:hover {
|
||||
background: #2a2d2e;
|
||||
}
|
||||
|
||||
.component-item:hover .add-btn {
|
||||
.component-item:hover .drag-handle {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -151,23 +196,20 @@ const handleClick = (componentId: string, componentName: string) => {
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.add-btn {
|
||||
.drag-handle {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: #007acc;
|
||||
color: white;
|
||||
border-radius: 4px;
|
||||
font-size: 16px;
|
||||
font-weight: bold;
|
||||
color: #888888;
|
||||
font-size: 14px;
|
||||
opacity: 0;
|
||||
transition: opacity 0.2s;
|
||||
}
|
||||
|
||||
.add-btn:hover {
|
||||
background: #0088e0;
|
||||
.drag-handle:hover {
|
||||
color: #cccccc;
|
||||
}
|
||||
|
||||
.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
|
||||
} from './interactionStore'
|
||||
|
||||
// 导出拖拽Store
|
||||
export { useDragStore } from './dragStore'
|
||||
export type {
|
||||
DropDirection,
|
||||
DragSource,
|
||||
HierarchyNode,
|
||||
DropRecord
|
||||
} from './dragStore'
|
||||
|
||||
// 导出路径工具
|
||||
export {
|
||||
parsePath,
|
||||
|
||||
@@ -29,3 +29,18 @@ html, body {
|
||||
width: 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