This commit is contained in:
wfz
2025-12-22 22:24:39 +08:00
parent 3c38f1bee9
commit ff8a6a28f8
10 changed files with 973 additions and 52 deletions

View File

@@ -141,5 +141,5 @@
"activeTabId": "mxfx11j"
}
},
"lastUpdated": "2025-12-21T13:24:40.794Z"
"lastUpdated": "2025-12-22T14:24:28.202Z"
}

View File

@@ -67,5 +67,5 @@
}
],
"selectedId": "jy87mdv",
"lastUpdated": "2025-12-21T13:23:32.873Z"
"lastUpdated": "2025-12-22T14:24:28.783Z"
}

View File

@@ -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;

View File

@@ -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>

View 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>

View File

@@ -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)
}
}
// 鼠标松开
const handleMouseUp = (e: MouseEvent) => {
interactionStore.onMouseUp()
// 如果已经有拖拽源,不处理
if (dragStore.isDragging) return
// 开始画布内元素拖拽
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>

View File

@@ -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 {

View 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
}
})

View File

@@ -14,6 +14,15 @@ export type {
HookCallback
} from './interactionStore'
// 导出拖拽Store
export { useDragStore } from './dragStore'
export type {
DropDirection,
DragSource,
HierarchyNode,
DropRecord
} from './dragStore'
// 导出路径工具
export {
parsePath,

View File

@@ -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;
}