2
This commit is contained in:
@@ -16,7 +16,7 @@
|
||||
"materialId": "DesignComponentList"
|
||||
}
|
||||
],
|
||||
"activeTabId": "6hfm9ux"
|
||||
"activeTabId": "up60643"
|
||||
},
|
||||
"centerPanel": {
|
||||
"id": "center",
|
||||
@@ -26,12 +26,6 @@
|
||||
"title": "设计中心",
|
||||
"content": "新窗口内容",
|
||||
"materialId": "DesignCenter"
|
||||
},
|
||||
{
|
||||
"id": "rdp9iuv",
|
||||
"title": "测试组件A",
|
||||
"content": "新窗口内容",
|
||||
"materialId": "TestWidget1"
|
||||
}
|
||||
],
|
||||
"activeTabId": "j70ckww"
|
||||
@@ -147,5 +141,5 @@
|
||||
"activeTabId": "mxfx11j"
|
||||
}
|
||||
},
|
||||
"lastUpdated": "2025-12-21T12:24:13.100Z"
|
||||
"lastUpdated": "2025-12-21T13:24:40.794Z"
|
||||
}
|
||||
@@ -52,8 +52,20 @@
|
||||
"列3"
|
||||
]
|
||||
}
|
||||
},
|
||||
{
|
||||
"id": "jy87mdv",
|
||||
"componentId": "RadioSelect",
|
||||
"name": "单选器 2",
|
||||
"props": {
|
||||
"options": [
|
||||
"选项1",
|
||||
"选项2",
|
||||
"选项3"
|
||||
]
|
||||
}
|
||||
}
|
||||
],
|
||||
"selectedId": "xazr6j9",
|
||||
"lastUpdated": "2025-12-21T12:22:52.464Z"
|
||||
"selectedId": "jy87mdv",
|
||||
"lastUpdated": "2025-12-21T13:23:32.873Z"
|
||||
}
|
||||
@@ -1,5 +1,8 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted } from 'vue'
|
||||
import { ref, computed, onMounted, onUnmounted } from 'vue'
|
||||
import { useInteractionStore } from '../plugins'
|
||||
|
||||
const interactionStore = useInteractionStore()
|
||||
|
||||
const currentTime = ref('')
|
||||
let timer: number | null = null
|
||||
@@ -17,6 +20,35 @@ const updateTime = () => {
|
||||
})
|
||||
}
|
||||
|
||||
// 交互状态显示文本
|
||||
const interactionText = computed(() => {
|
||||
return interactionStore.formatInteraction(interactionStore.lastInteraction)
|
||||
})
|
||||
|
||||
// 当前悬停的元素信息
|
||||
const hoverInfo = computed(() => {
|
||||
const target = interactionStore.hoverTarget
|
||||
if (!target) return null
|
||||
|
||||
const typeName = interactionStore.getTypeName(target.type)
|
||||
if (target.type === 'dc') {
|
||||
return `悬停: ${typeName} [${target.componentId}]`
|
||||
}
|
||||
return `悬停: ${typeName} [${target.path}]`
|
||||
})
|
||||
|
||||
// 当前选中的元素信息
|
||||
const selectedInfo = computed(() => {
|
||||
const target = interactionStore.selectedTarget
|
||||
if (!target) return null
|
||||
|
||||
const typeName = interactionStore.getTypeName(target.type)
|
||||
if (target.type === 'dc') {
|
||||
return `选中: ${typeName} [${target.componentId}]`
|
||||
}
|
||||
return `选中: ${typeName} [${target.path}]`
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
updateTime()
|
||||
timer = window.setInterval(updateTime, 1000)
|
||||
@@ -31,7 +63,22 @@ onUnmounted(() => {
|
||||
|
||||
<template>
|
||||
<footer class="app-footer">
|
||||
<div class="footer-left"></div>
|
||||
<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>
|
||||
</div>
|
||||
<div class="footer-center">
|
||||
<span v-if="interactionStore.lastInteraction" class="last-action">
|
||||
最近: {{ interactionText }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="footer-right">
|
||||
<span class="time">{{ currentTime }}</span>
|
||||
</div>
|
||||
@@ -47,10 +94,19 @@ onUnmounted(() => {
|
||||
justify-content: space-between;
|
||||
padding: 0 12px;
|
||||
flex-shrink: 0;
|
||||
gap: 20px;
|
||||
}
|
||||
|
||||
.footer-left {
|
||||
flex: 1;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 16px;
|
||||
}
|
||||
|
||||
.footer-center {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.footer-right {
|
||||
@@ -58,6 +114,34 @@ onUnmounted(() => {
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.hover-info {
|
||||
color: #ffffc0;
|
||||
}
|
||||
|
||||
.selected-info {
|
||||
color: #90ee90;
|
||||
}
|
||||
|
||||
.idle-info {
|
||||
color: rgba(255, 255, 255, 0.7);
|
||||
}
|
||||
|
||||
.last-action {
|
||||
color: rgba(255, 255, 255, 0.8);
|
||||
font-size: 11px;
|
||||
background: rgba(0, 0, 0, 0.2);
|
||||
padding: 2px 8px;
|
||||
border-radius: 3px;
|
||||
}
|
||||
|
||||
.time {
|
||||
color: white;
|
||||
font-size: 12px;
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
<script setup lang="ts">
|
||||
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
|
||||
import { useInteractionStore, generateElementPath } from '../../plugins'
|
||||
import type { ElementType, InteractionTarget } from '../../plugins'
|
||||
|
||||
const props = defineProps<{
|
||||
component: any
|
||||
}>()
|
||||
|
||||
const interactionStore = useInteractionStore()
|
||||
const containerRef = ref<HTMLElement | null>(null)
|
||||
|
||||
// 存储所有绑定的事件清理函数
|
||||
const cleanupFunctions: (() => void)[] = []
|
||||
|
||||
// MutationObserver 用于监听DOM变化
|
||||
let observer: MutationObserver | null = null
|
||||
|
||||
/**
|
||||
* 为元素绑定交互事件
|
||||
*/
|
||||
const bindElementEvents = (element: HTMLElement, type: ElementType, path: string) => {
|
||||
// 跳过已经绑定过的元素
|
||||
if (element.hasAttribute('data-fauto-bindend')) return null
|
||||
|
||||
const target: InteractionTarget = {
|
||||
type,
|
||||
path,
|
||||
element
|
||||
}
|
||||
|
||||
// 设置数据属性
|
||||
element.setAttribute('data-path', path)
|
||||
element.setAttribute('data-type', type)
|
||||
element.setAttribute('data-fauto-bindend', 'true')
|
||||
|
||||
// 添加可交互样式类
|
||||
element.classList.add('fauto-interactive')
|
||||
|
||||
// 鼠标悬停
|
||||
const handleMouseEnter = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
interactionStore.onHover(target)
|
||||
element.classList.add('fauto-hover')
|
||||
}
|
||||
|
||||
// 鼠标离开
|
||||
const handleMouseLeave = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
interactionStore.onLeave()
|
||||
element.classList.remove('fauto-hover')
|
||||
}
|
||||
|
||||
// 鼠标点击
|
||||
const handleClick = (e: MouseEvent) => {
|
||||
e.stopPropagation()
|
||||
interactionStore.onClick(target)
|
||||
|
||||
// 移除其他元素的选中状态
|
||||
document.querySelectorAll('.fauto-selected').forEach(el => {
|
||||
el.classList.remove('fauto-selected')
|
||||
})
|
||||
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()
|
||||
}
|
||||
|
||||
// 绑定事件
|
||||
element.addEventListener('mouseenter', handleMouseEnter)
|
||||
element.addEventListener('mouseleave', handleMouseLeave)
|
||||
element.addEventListener('click', handleClick)
|
||||
element.addEventListener('mousedown', handleMouseDown)
|
||||
element.addEventListener('mousemove', handleMouseMove)
|
||||
element.addEventListener('mouseup', handleMouseUp)
|
||||
|
||||
console.log(`[注入] ${type} 路径: ${path}`)
|
||||
|
||||
// 返回清理函数
|
||||
return () => {
|
||||
element.removeEventListener('mouseenter', handleMouseEnter)
|
||||
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')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 扫描并注入事件到所有el-row和el-col元素
|
||||
*/
|
||||
const injectInteractionEvents = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
// 查找所有el-row和el-col元素
|
||||
const rows = containerRef.value.querySelectorAll('.el-row')
|
||||
const cols = containerRef.value.querySelectorAll('.el-col')
|
||||
|
||||
console.log(`[InteractiveWrapper] 发现 ${rows.length} 个 el-row, ${cols.length} 个 el-col`)
|
||||
|
||||
// 为el-row绑定事件
|
||||
rows.forEach((row) => {
|
||||
const path = generateElementPath(row)
|
||||
const cleanup = bindElementEvents(row as HTMLElement, 'er', path)
|
||||
if (cleanup) cleanupFunctions.push(cleanup)
|
||||
})
|
||||
|
||||
// 为el-col绑定事件
|
||||
cols.forEach((col) => {
|
||||
const path = generateElementPath(col)
|
||||
const cleanup = bindElementEvents(col as HTMLElement, 'ec', path)
|
||||
if (cleanup) cleanupFunctions.push(cleanup)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 启动DOM观察器,监听子元素变化
|
||||
*/
|
||||
const startObserver = () => {
|
||||
if (!containerRef.value) return
|
||||
|
||||
observer = new MutationObserver((mutations) => {
|
||||
// 检查是否有新的el-row或el-col添加
|
||||
let hasNewElements = false
|
||||
mutations.forEach(mutation => {
|
||||
mutation.addedNodes.forEach(node => {
|
||||
if (node instanceof HTMLElement) {
|
||||
if (node.classList.contains('el-row') || node.classList.contains('el-col')) {
|
||||
hasNewElements = true
|
||||
}
|
||||
// 检查子元素
|
||||
if (node.querySelector('.el-row, .el-col')) {
|
||||
hasNewElements = true
|
||||
}
|
||||
}
|
||||
})
|
||||
})
|
||||
|
||||
if (hasNewElements) {
|
||||
console.log('[MutationObserver] 检测到新元素,重新注入事件')
|
||||
injectInteractionEvents()
|
||||
}
|
||||
})
|
||||
|
||||
observer.observe(containerRef.value, {
|
||||
childList: true,
|
||||
subtree: true
|
||||
})
|
||||
}
|
||||
|
||||
// 组件挂载后注入事件
|
||||
onMounted(() => {
|
||||
// 等待异步组件加载完成,使用多次nextTick和延时
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
injectInteractionEvents()
|
||||
startObserver()
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
|
||||
// 组件卸载时清理事件
|
||||
onUnmounted(() => {
|
||||
cleanupFunctions.forEach(fn => fn())
|
||||
cleanupFunctions.length = 0
|
||||
|
||||
if (observer) {
|
||||
observer.disconnect()
|
||||
observer = null
|
||||
}
|
||||
})
|
||||
|
||||
// 监听组件变化,重新注入事件
|
||||
watch(() => props.component, () => {
|
||||
// 清理之前的绑定
|
||||
cleanupFunctions.forEach(fn => fn())
|
||||
cleanupFunctions.length = 0
|
||||
|
||||
nextTick(() => {
|
||||
setTimeout(() => {
|
||||
injectInteractionEvents()
|
||||
}, 100)
|
||||
})
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div ref="containerRef" class="interactive-wrapper">
|
||||
<component :is="component" />
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style>
|
||||
/* 全局样式,用于交互元素 */
|
||||
.interactive-wrapper {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
.fauto-interactive {
|
||||
position: relative;
|
||||
transition: outline 0.15s ease, box-shadow 0.15s ease;
|
||||
}
|
||||
|
||||
.fauto-interactive.fauto-hover {
|
||||
outline: 2px dashed #409eff;
|
||||
outline-offset: -2px;
|
||||
z-index: 10;
|
||||
}
|
||||
|
||||
.fauto-interactive.fauto-selected {
|
||||
outline: 2px solid #67c23a;
|
||||
outline-offset: -2px;
|
||||
z-index: 20;
|
||||
box-shadow: 0 0 8px rgba(103, 194, 58, 0.4);
|
||||
}
|
||||
|
||||
/* el-row 悬停时的特殊样式 */
|
||||
.el-row.fauto-hover {
|
||||
background-color: rgba(64, 158, 255, 0.05);
|
||||
}
|
||||
|
||||
.el-row.fauto-selected {
|
||||
background-color: rgba(103, 194, 58, 0.05);
|
||||
}
|
||||
|
||||
/* el-col 悬停时的特殊样式 */
|
||||
.el-col.fauto-hover {
|
||||
background-color: rgba(64, 158, 255, 0.1);
|
||||
}
|
||||
|
||||
.el-col.fauto-selected {
|
||||
background-color: rgba(103, 194, 58, 0.1);
|
||||
}
|
||||
</style>
|
||||
@@ -2,6 +2,7 @@
|
||||
import { defineAsyncComponent, markRaw, computed, watch } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import { useVueFileStore } from '../../stores/vueFileStore'
|
||||
import InteractiveWrapper from './InteractiveWrapper.vue'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
@@ -72,9 +73,9 @@ watch(() => vueFileStore.selectedFilePath, (newPath) => {
|
||||
</span>
|
||||
</div>
|
||||
<div class="center-body">
|
||||
<!-- 动态渲染选中的Vue页面 -->
|
||||
<!-- 动态渲染选中的Vue页面(使用InteractiveWrapper注入交互事件) -->
|
||||
<div v-if="selectedPageComponent" class="page-preview">
|
||||
<component :is="selectedPageComponent" />
|
||||
<InteractiveWrapper :component="selectedPageComponent" />
|
||||
</div>
|
||||
|
||||
<!-- 原有的设计组件实例列表 -->
|
||||
|
||||
@@ -1,9 +1,12 @@
|
||||
<script setup lang="ts">
|
||||
import { onMounted } from 'vue'
|
||||
import { useDesignStore } from '../../stores/designStore'
|
||||
import { useInteractionStore } from '../../plugins'
|
||||
import type { InteractionTarget } from '../../plugins'
|
||||
import config from './index.json'
|
||||
|
||||
const designStore = useDesignStore()
|
||||
const interactionStore = useInteractionStore()
|
||||
|
||||
onMounted(() => {
|
||||
// 确保设计组件元数据已加载
|
||||
@@ -15,6 +18,33 @@ onMounted(() => {
|
||||
const handleAddComponent = (componentId: string) => {
|
||||
designStore.addComponent(componentId)
|
||||
}
|
||||
|
||||
// 鼠标悬停设计组件
|
||||
const handleMouseEnter = (componentId: string, componentName: string) => {
|
||||
const target: InteractionTarget = {
|
||||
type: 'dc',
|
||||
path: componentId,
|
||||
componentId: componentName
|
||||
}
|
||||
interactionStore.onHover(target)
|
||||
}
|
||||
|
||||
// 鼠标离开设计组件
|
||||
const handleMouseLeave = () => {
|
||||
interactionStore.onLeave()
|
||||
}
|
||||
|
||||
// 鼠标点击设计组件
|
||||
const handleClick = (componentId: string, componentName: string) => {
|
||||
const target: InteractionTarget = {
|
||||
type: 'dc',
|
||||
path: componentId,
|
||||
componentId: componentName
|
||||
}
|
||||
interactionStore.onClick(target)
|
||||
// 添加组件
|
||||
designStore.addComponent(componentId)
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
@@ -28,7 +58,9 @@ const handleAddComponent = (componentId: string) => {
|
||||
v-for="meta in designStore.componentMetas"
|
||||
:key="meta.id"
|
||||
class="component-item"
|
||||
@click="handleAddComponent(meta.id)"
|
||||
@click="handleClick(meta.id, meta.name)"
|
||||
@mouseenter="handleMouseEnter(meta.id, meta.name)"
|
||||
@mouseleave="handleMouseLeave"
|
||||
>
|
||||
<div class="component-icon">📦</div>
|
||||
<div class="component-info">
|
||||
|
||||
27
draggable-panels/src/fauto/plugins/index.ts
Normal file
27
draggable-panels/src/fauto/plugins/index.ts
Normal file
@@ -0,0 +1,27 @@
|
||||
/**
|
||||
* Fauto 插件系统入口
|
||||
*
|
||||
* 提供全局交互钩子和工具函数
|
||||
*/
|
||||
|
||||
// 导出交互Store
|
||||
export { useInteractionStore } from './interactionStore'
|
||||
export type {
|
||||
ElementType,
|
||||
InteractionEvent,
|
||||
InteractionTarget,
|
||||
InteractionState,
|
||||
HookCallback
|
||||
} from './interactionStore'
|
||||
|
||||
// 导出路径工具
|
||||
export {
|
||||
parsePath,
|
||||
buildPath,
|
||||
calculateSiblingIndex,
|
||||
generateElementPath,
|
||||
getParentPath,
|
||||
getPathDepth,
|
||||
isAncestorPath
|
||||
} from './pathUtils'
|
||||
export type { PathNode } from './pathUtils'
|
||||
257
draggable-panels/src/fauto/plugins/interactionStore.ts
Normal file
257
draggable-panels/src/fauto/plugins/interactionStore.ts
Normal file
@@ -0,0 +1,257 @@
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, shallowRef } from 'vue'
|
||||
|
||||
/**
|
||||
* 交互元素类型
|
||||
* - er: el-row
|
||||
* - ec: el-col
|
||||
* - dc: design-component (设计组件)
|
||||
*/
|
||||
export type ElementType = 'er' | 'ec' | 'dc'
|
||||
|
||||
/**
|
||||
* 交互事件类型
|
||||
*/
|
||||
export type InteractionEvent =
|
||||
| 'hover' // 鼠标悬停
|
||||
| 'click' // 鼠标点击
|
||||
| 'longpress' // 鼠标长按
|
||||
| 'drag' // 长按并移动
|
||||
| 'release' // 鼠标松开
|
||||
|
||||
/**
|
||||
* 交互目标信息
|
||||
*/
|
||||
export interface InteractionTarget {
|
||||
type: ElementType // 元素类型
|
||||
path: string // 结构化路径ID (如 r1c2r1c1)
|
||||
componentId?: string // 设计组件ID (仅dc类型)
|
||||
element?: HTMLElement // DOM元素引用
|
||||
}
|
||||
|
||||
/**
|
||||
* 交互状态信息
|
||||
*/
|
||||
export interface InteractionState {
|
||||
event: InteractionEvent // 当前事件类型
|
||||
target: InteractionTarget // 目标元素信息
|
||||
timestamp: number // 时间戳
|
||||
}
|
||||
|
||||
/**
|
||||
* 钩子函数回调类型
|
||||
*/
|
||||
export type HookCallback = (state: InteractionState) => void
|
||||
|
||||
/**
|
||||
* 全局交互钩子插件
|
||||
* 管理所有的鼠标交互事件和状态
|
||||
*/
|
||||
export const useInteractionStore = defineStore('interaction', () => {
|
||||
// 当前悬停的元素
|
||||
const hoverTarget = ref<InteractionTarget | null>(null)
|
||||
|
||||
// 当前点击/选中的元素
|
||||
const selectedTarget = ref<InteractionTarget | null>(null)
|
||||
|
||||
// 当前拖拽的元素
|
||||
const dragTarget = ref<InteractionTarget | null>(null)
|
||||
|
||||
// 拖拽放置的目标元素
|
||||
const dropTarget = ref<InteractionTarget | null>(null)
|
||||
|
||||
// 最后一次交互状态(用于Footer显示)
|
||||
const lastInteraction = ref<InteractionState | null>(null)
|
||||
|
||||
// 是否正在长按
|
||||
const isLongPressing = ref(false)
|
||||
|
||||
// 是否正在拖拽
|
||||
const isDragging = ref(false)
|
||||
|
||||
// 长按计时器
|
||||
let longPressTimer: number | null = null
|
||||
|
||||
// 钩子函数注册表
|
||||
const hooks = shallowRef<Map<InteractionEvent, Set<HookCallback>>>(new Map())
|
||||
|
||||
/**
|
||||
* 注册钩子函数
|
||||
*/
|
||||
const registerHook = (event: InteractionEvent, callback: HookCallback) => {
|
||||
if (!hooks.value.has(event)) {
|
||||
hooks.value.set(event, new Set())
|
||||
}
|
||||
hooks.value.get(event)!.add(callback)
|
||||
|
||||
// 返回取消注册函数
|
||||
return () => {
|
||||
hooks.value.get(event)?.delete(callback)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 触发钩子函数
|
||||
*/
|
||||
const triggerHooks = (event: InteractionEvent, target: InteractionTarget) => {
|
||||
const state: InteractionState = {
|
||||
event,
|
||||
target,
|
||||
timestamp: Date.now()
|
||||
}
|
||||
|
||||
// 更新最后交互状态
|
||||
lastInteraction.value = state
|
||||
|
||||
// 触发所有注册的回调
|
||||
const callbacks = hooks.value.get(event)
|
||||
if (callbacks) {
|
||||
callbacks.forEach(cb => cb(state))
|
||||
}
|
||||
|
||||
console.log(`[Interaction] ${event}:`, target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标悬停事件
|
||||
*/
|
||||
const onHover = (target: InteractionTarget) => {
|
||||
hoverTarget.value = target
|
||||
triggerHooks('hover', target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标离开事件
|
||||
*/
|
||||
const onLeave = () => {
|
||||
hoverTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标点击事件
|
||||
*/
|
||||
const onClick = (target: InteractionTarget) => {
|
||||
selectedTarget.value = target
|
||||
triggerHooks('click', target)
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标按下事件(开始检测长按)
|
||||
*/
|
||||
const onMouseDown = (target: InteractionTarget) => {
|
||||
// 清除之前的计时器
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer)
|
||||
}
|
||||
|
||||
// 设置长按检测(500ms后触发)
|
||||
longPressTimer = window.setTimeout(() => {
|
||||
isLongPressing.value = true
|
||||
dragTarget.value = target
|
||||
triggerHooks('longpress', target)
|
||||
}, 500)
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标移动事件(长按状态下触发拖拽)
|
||||
*/
|
||||
const onMouseMove = (target: InteractionTarget) => {
|
||||
if (isLongPressing.value && dragTarget.value) {
|
||||
if (!isDragging.value) {
|
||||
isDragging.value = true
|
||||
}
|
||||
dropTarget.value = target
|
||||
triggerHooks('drag', target)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 鼠标松开事件
|
||||
*/
|
||||
const onMouseUp = () => {
|
||||
// 清除长按计时器
|
||||
if (longPressTimer) {
|
||||
clearTimeout(longPressTimer)
|
||||
longPressTimer = null
|
||||
}
|
||||
|
||||
// 如果正在拖拽,触发释放事件
|
||||
if (isDragging.value && dragTarget.value && dropTarget.value) {
|
||||
triggerHooks('release', dropTarget.value)
|
||||
}
|
||||
|
||||
// 重置状态
|
||||
isLongPressing.value = false
|
||||
isDragging.value = false
|
||||
dragTarget.value = null
|
||||
dropTarget.value = null
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取元素类型的中文名称
|
||||
*/
|
||||
const getTypeName = (type: ElementType): string => {
|
||||
const names: Record<ElementType, string> = {
|
||||
'er': 'el-row',
|
||||
'ec': 'el-col',
|
||||
'dc': '设计组件'
|
||||
}
|
||||
return names[type]
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取事件类型的中文名称
|
||||
*/
|
||||
const getEventName = (event: InteractionEvent): string => {
|
||||
const names: Record<InteractionEvent, string> = {
|
||||
'hover': '悬停',
|
||||
'click': '点击',
|
||||
'longpress': '长按',
|
||||
'drag': '拖拽',
|
||||
'release': '释放'
|
||||
}
|
||||
return names[event]
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化交互状态为显示文本
|
||||
*/
|
||||
const formatInteraction = (state: InteractionState | null): string => {
|
||||
if (!state) return '无交互'
|
||||
|
||||
const eventName = getEventName(state.event)
|
||||
const typeName = getTypeName(state.target.type)
|
||||
const path = state.target.path
|
||||
const componentId = state.target.componentId
|
||||
|
||||
if (state.target.type === 'dc') {
|
||||
return `${eventName} → ${typeName} [${componentId}]`
|
||||
}
|
||||
return `${eventName} → ${typeName} [${path}]`
|
||||
}
|
||||
|
||||
return {
|
||||
// 状态
|
||||
hoverTarget,
|
||||
selectedTarget,
|
||||
dragTarget,
|
||||
dropTarget,
|
||||
lastInteraction,
|
||||
isLongPressing,
|
||||
isDragging,
|
||||
|
||||
// 方法
|
||||
registerHook,
|
||||
onHover,
|
||||
onLeave,
|
||||
onClick,
|
||||
onMouseDown,
|
||||
onMouseMove,
|
||||
onMouseUp,
|
||||
|
||||
// 辅助方法
|
||||
getTypeName,
|
||||
getEventName,
|
||||
formatInteraction
|
||||
}
|
||||
})
|
||||
125
draggable-panels/src/fauto/plugins/pathUtils.ts
Normal file
125
draggable-panels/src/fauto/plugins/pathUtils.ts
Normal file
@@ -0,0 +1,125 @@
|
||||
/**
|
||||
* 结构化路径ID生成器
|
||||
* 用于为el-row和el-col生成唯一的结构化路径
|
||||
*
|
||||
* 路径格式: r{n}c{m}r{x}c{y}...
|
||||
* - r: el-row
|
||||
* - c: el-col
|
||||
* - 数字: 在同级元素中的索引(从1开始)
|
||||
*/
|
||||
|
||||
import type { ElementType } from './interactionStore'
|
||||
|
||||
/**
|
||||
* 路径节点信息
|
||||
*/
|
||||
export interface PathNode {
|
||||
type: ElementType
|
||||
index: number
|
||||
}
|
||||
|
||||
/**
|
||||
* 解析结构化路径字符串
|
||||
* @param path 路径字符串,如 "r1c2r1c1"
|
||||
* @returns 路径节点数组
|
||||
*/
|
||||
export const parsePath = (path: string): PathNode[] => {
|
||||
const nodes: PathNode[] = []
|
||||
const regex = /(r|c)(\d+)/g
|
||||
let match
|
||||
|
||||
while ((match = regex.exec(path)) !== null) {
|
||||
nodes.push({
|
||||
type: match[1] === 'r' ? 'er' : 'ec',
|
||||
index: parseInt(match[2])
|
||||
})
|
||||
}
|
||||
|
||||
return nodes
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成结构化路径字符串
|
||||
* @param nodes 路径节点数组
|
||||
* @returns 路径字符串
|
||||
*/
|
||||
export const buildPath = (nodes: PathNode[]): string => {
|
||||
return nodes.map(node => {
|
||||
const prefix = node.type === 'er' ? 'r' : 'c'
|
||||
return `${prefix}${node.index}`
|
||||
}).join('')
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算元素在其父级中的索引
|
||||
* @param element 目标元素
|
||||
* @param className 要匹配的class名(el-row 或 el-col)
|
||||
* @returns 索引(从1开始)
|
||||
*/
|
||||
export const calculateSiblingIndex = (element: Element, className: string): number => {
|
||||
const parent = element.parentElement
|
||||
if (!parent) return 1
|
||||
|
||||
// 通过class名称筛选同级元素(Element Plus组件渲染后是div带class)
|
||||
const siblings = Array.from(parent.children).filter(
|
||||
child => child.classList.contains(className)
|
||||
)
|
||||
|
||||
const index = siblings.indexOf(element)
|
||||
return index >= 0 ? index + 1 : 1
|
||||
}
|
||||
|
||||
/**
|
||||
* 递归生成元素的完整结构化路径
|
||||
* @param element 目标元素
|
||||
* @returns 结构化路径字符串
|
||||
*/
|
||||
export const generateElementPath = (element: Element): string => {
|
||||
const pathNodes: PathNode[] = []
|
||||
let current: Element | null = element
|
||||
|
||||
while (current) {
|
||||
// Element Plus的el-row/el-col渲染后是div标签,通过class来识别
|
||||
if (current.classList.contains('el-row')) {
|
||||
const index = calculateSiblingIndex(current, 'el-row')
|
||||
pathNodes.unshift({ type: 'er', index })
|
||||
} else if (current.classList.contains('el-col')) {
|
||||
const index = calculateSiblingIndex(current, 'el-col')
|
||||
pathNodes.unshift({ type: 'ec', index })
|
||||
}
|
||||
|
||||
current = current.parentElement
|
||||
}
|
||||
|
||||
return buildPath(pathNodes)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径的父路径
|
||||
* @param path 当前路径
|
||||
* @returns 父路径,如果已是根则返回空字符串
|
||||
*/
|
||||
export const getParentPath = (path: string): string => {
|
||||
const nodes = parsePath(path)
|
||||
if (nodes.length <= 1) return ''
|
||||
return buildPath(nodes.slice(0, -1))
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取路径的深度
|
||||
* @param path 路径字符串
|
||||
* @returns 深度(节点数量)
|
||||
*/
|
||||
export const getPathDepth = (path: string): number => {
|
||||
return parsePath(path).length
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查路径A是否是路径B的祖先
|
||||
* @param ancestorPath 祖先路径
|
||||
* @param descendantPath 后代路径
|
||||
* @returns 是否为祖先关系
|
||||
*/
|
||||
export const isAncestorPath = (ancestorPath: string, descendantPath: string): boolean => {
|
||||
return descendantPath.startsWith(ancestorPath) && descendantPath.length > ancestorPath.length
|
||||
}
|
||||
@@ -1,52 +1,43 @@
|
||||
<template>
|
||||
<div class="test-page">
|
||||
<h2>测试页面 1</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-row class="page-container" :gutter="20">
|
||||
<el-col :span="12">
|
||||
<div class="grid-content">左侧内容区域</div>
|
||||
<div class="design-component">左侧内容区域</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="grid-content">右侧内容区域</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="24">
|
||||
<div class="design-component">右侧标题</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">列 1</div>
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<div class="design-component">右侧列1</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">列 2</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">列 3</div>
|
||||
<el-col :span="12">
|
||||
<div class="design-component">右侧列2</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const message = ref('这是测试页面1')
|
||||
// 测试页面1 - 基础布局
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.design-component {
|
||||
background: #409eff;
|
||||
color: white;
|
||||
padding: 30px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,64 +1,53 @@
|
||||
<template>
|
||||
<div class="test-page">
|
||||
<h2>测试页面 2 - 表单布局</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-row class="page-container" :gutter="20">
|
||||
<el-col :span="24">
|
||||
<div class="grid-content">表单标题区域</div>
|
||||
<div class="design-component form-title">表单标题区域</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="6">
|
||||
<div class="grid-content">标签</div>
|
||||
<div class="design-component">用户名</div>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<div class="grid-content">输入框</div>
|
||||
<div class="design-component">输入框占位</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="6">
|
||||
<div class="grid-content">标签</div>
|
||||
<div class="design-component">密码</div>
|
||||
</el-col>
|
||||
<el-col :span="18">
|
||||
<div class="grid-content">输入框</div>
|
||||
<div class="design-component">密码框占位</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="24">
|
||||
<div class="grid-content">提交按钮</div>
|
||||
<div class="design-component submit-btn">提交按钮</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const formData = ref({
|
||||
name: '',
|
||||
email: ''
|
||||
})
|
||||
// 测试页面2 - 表单布局
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.test-page {
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background: #fafafa;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.design-component {
|
||||
background: #67c23a;
|
||||
color: white;
|
||||
padding: 20px;
|
||||
text-align: center;
|
||||
border-radius: 4px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.form-title {
|
||||
background: #409eff;
|
||||
font-size: 18px;
|
||||
font-weight: bold;
|
||||
}
|
||||
|
||||
.submit-btn {
|
||||
background: #e6a23c;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,72 +1,55 @@
|
||||
<template>
|
||||
<div class="overview-page">
|
||||
<h2>仪表板概览</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-row class="page-container" :gutter="20">
|
||||
<el-col :span="6">
|
||||
<div class="grid-content stat-card">
|
||||
<div class="design-component stat-card">
|
||||
<div class="stat-title">总用户</div>
|
||||
<div class="stat-value">1,234</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="grid-content stat-card">
|
||||
<div class="design-component stat-card">
|
||||
<div class="stat-title">活跃用户</div>
|
||||
<div class="stat-value">856</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="grid-content stat-card">
|
||||
<div class="design-component stat-card">
|
||||
<div class="stat-title">订单数</div>
|
||||
<div class="stat-value">432</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="6">
|
||||
<div class="grid-content stat-card">
|
||||
<div class="design-component stat-card">
|
||||
<div class="stat-title">收入</div>
|
||||
<div class="stat-value">¥12,345</div>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
|
||||
<el-row :gutter="20" style="margin-top: 20px;">
|
||||
<el-col :span="16">
|
||||
<div class="grid-content chart-area">图表区域</div>
|
||||
<div class="design-component chart-area">图表区域占位</div>
|
||||
</el-col>
|
||||
<el-col :span="8">
|
||||
<div class="grid-content">侧边栏信息</div>
|
||||
<div class="design-component sidebar">侧边栏信息</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const stats = ref({
|
||||
users: 1234,
|
||||
active: 856,
|
||||
orders: 432,
|
||||
revenue: 12345
|
||||
})
|
||||
// 仪表板概览页面
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.overview-page {
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background: #f0f2f5;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.design-component {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.stat-card {
|
||||
@@ -91,5 +74,11 @@ h2 {
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #999;
|
||||
background: #e8f4ff;
|
||||
}
|
||||
|
||||
.sidebar {
|
||||
height: 300px;
|
||||
background: #f0f9ff;
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -1,64 +1,46 @@
|
||||
<template>
|
||||
<div class="profile-page">
|
||||
<h2>用户资料</h2>
|
||||
<el-row :gutter="20">
|
||||
<el-row class="page-container" :gutter="20">
|
||||
<el-col :span="8">
|
||||
<div class="grid-content avatar-section">
|
||||
<div class="design-component avatar-section">
|
||||
<div class="avatar">头像</div>
|
||||
</div>
|
||||
</el-col>
|
||||
<el-col :span="16">
|
||||
<div class="grid-content info-section">
|
||||
<el-row :gutter="10">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">姓名: 张三</div>
|
||||
<div class="design-component info-item">姓名: 张三</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">邮箱: zhang@example.com</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
<el-row :gutter="10" style="margin-top: 10px;">
|
||||
<el-col :span="12">
|
||||
<div class="info-item">电话: 138****8888</div>
|
||||
<div class="design-component info-item">邮箱: zhang@example.com</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="info-item">部门: 技术部</div>
|
||||
<div class="design-component info-item">电话: 138****8888</div>
|
||||
</el-col>
|
||||
<el-col :span="12">
|
||||
<div class="design-component info-item">部门: 技术部</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</el-col>
|
||||
</el-row>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<script setup lang="ts">
|
||||
import { ref } from 'vue'
|
||||
|
||||
const userInfo = ref({
|
||||
name: '张三',
|
||||
email: 'zhang@example.com',
|
||||
phone: '138****8888',
|
||||
department: '技术部'
|
||||
})
|
||||
// 用户资料页面
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
.profile-page {
|
||||
.page-container {
|
||||
padding: 20px;
|
||||
background: #f5f5f5;
|
||||
min-height: 100vh;
|
||||
min-height: 100%;
|
||||
}
|
||||
|
||||
h2 {
|
||||
color: #333;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.grid-content {
|
||||
.design-component {
|
||||
background: white;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.avatar-section {
|
||||
@@ -80,14 +62,9 @@ h2 {
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.info-section {
|
||||
height: 200px;
|
||||
}
|
||||
|
||||
.info-item {
|
||||
padding: 10px;
|
||||
padding: 15px;
|
||||
background: #f9f9f9;
|
||||
border-radius: 4px;
|
||||
color: #666;
|
||||
}
|
||||
</style>
|
||||
|
||||
Reference in New Issue
Block a user