3
This commit is contained in:
@@ -26,6 +26,12 @@
|
|||||||
"title": "设计中心",
|
"title": "设计中心",
|
||||||
"content": "新窗口内容",
|
"content": "新窗口内容",
|
||||||
"materialId": "DesignCenter"
|
"materialId": "DesignCenter"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"id": "m9fv3nb",
|
||||||
|
"title": "测试组件A",
|
||||||
|
"content": "新窗口内容",
|
||||||
|
"materialId": "TestWidget1"
|
||||||
}
|
}
|
||||||
],
|
],
|
||||||
"activeTabId": "j70ckww"
|
"activeTabId": "j70ckww"
|
||||||
@@ -141,5 +147,5 @@
|
|||||||
"activeTabId": "mxfx11j"
|
"activeTabId": "mxfx11j"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
"lastUpdated": "2025-12-22T15:06:06.209Z"
|
"lastUpdated": "2025-12-22T15:54:13.388Z"
|
||||||
}
|
}
|
||||||
@@ -1,5 +1,5 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, ref, watch, onMounted, onUnmounted } from 'vue'
|
import { computed, ref, watch } from 'vue'
|
||||||
import { useDragStore } from '../../plugins'
|
import { useDragStore } from '../../plugins'
|
||||||
import type { DropDirection } from '../../plugins'
|
import type { DropDirection } from '../../plugins'
|
||||||
|
|
||||||
@@ -38,23 +38,6 @@ const isRowLayout = computed(() => {
|
|||||||
return dragStore.selectedNode?.type === 'er'
|
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 getDirectionText = (direction: DropDirection): string => {
|
||||||
const texts: Record<DropDirection, string> = {
|
const texts: Record<DropDirection, string> = {
|
||||||
@@ -94,9 +77,6 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
`zone-${direction}`,
|
`zone-${direction}`,
|
||||||
{ 'is-active': dragStore.hoverDirection === 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-icon">{{ getDirectionIcon(direction) }}</span>
|
||||||
<span class="zone-text">{{ getDirectionText(direction) }}</span>
|
<span class="zone-text">{{ getDirectionText(direction) }}</span>
|
||||||
@@ -107,14 +87,10 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
|
|
||||||
<style scoped>
|
<style scoped>
|
||||||
.drop-zone-container {
|
.drop-zone-container {
|
||||||
position: absolute;
|
pointer-events: none; /* 容器和所有子元素都不拦截事件 */
|
||||||
top: 0;
|
|
||||||
left: 0;
|
|
||||||
right: 0;
|
|
||||||
bottom: 0;
|
|
||||||
display: flex;
|
|
||||||
pointer-events: none;
|
|
||||||
z-index: 100;
|
z-index: 100;
|
||||||
|
display: flex;
|
||||||
|
/* 不再设置 position, top, left 等,直接使用 :style 绑定的值 */
|
||||||
}
|
}
|
||||||
|
|
||||||
/* 行布局:上下排列 */
|
/* 行布局:上下排列 */
|
||||||
@@ -137,7 +113,7 @@ const getDirectionIcon = (direction: DropDirection): string => {
|
|||||||
background: rgba(64, 158, 255, 0.1);
|
background: rgba(64, 158, 255, 0.1);
|
||||||
border: 2px dashed rgba(64, 158, 255, 0.5);
|
border: 2px dashed rgba(64, 158, 255, 0.5);
|
||||||
transition: all 0.2s;
|
transition: all 0.2s;
|
||||||
pointer-events: auto;
|
pointer-events: none; /* 不拦截事件,让底层元素可以接收 mouseenter */
|
||||||
cursor: pointer;
|
cursor: pointer;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -50,24 +50,14 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
const handleMouseEnter = (e: MouseEvent) => {
|
const handleMouseEnter = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
|
|
||||||
// 如果正在拖拽,更新层级节点
|
// 更新交互store的悬停状态(无论是否拖拽)
|
||||||
if (dragStore.isDragging) {
|
interactionStore.onHover(target)
|
||||||
dragStore.updateHierarchy(element)
|
|
||||||
} else {
|
if (!dragStore.isDragging) {
|
||||||
interactionStore.onHover(target)
|
|
||||||
element.classList.add('fauto-hover')
|
element.classList.add('fauto-hover')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 鼠标移动(拖拽时持续更新层级)
|
|
||||||
const handleMouseMove = (e: MouseEvent) => {
|
|
||||||
// 只在拖拽状态下处理
|
|
||||||
if (dragStore.isDragging) {
|
|
||||||
e.stopPropagation()
|
|
||||||
dragStore.updateHierarchy(element)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 鼠标离开
|
// 鼠标离开
|
||||||
const handleMouseLeave = (e: MouseEvent) => {
|
const handleMouseLeave = (e: MouseEvent) => {
|
||||||
e.stopPropagation()
|
e.stopPropagation()
|
||||||
@@ -107,7 +97,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
|
|
||||||
// 绑定事件
|
// 绑定事件
|
||||||
element.addEventListener('mouseenter', handleMouseEnter)
|
element.addEventListener('mouseenter', handleMouseEnter)
|
||||||
element.addEventListener('mousemove', handleMouseMove) // 新增:拖拽时持续更新
|
|
||||||
element.addEventListener('mouseleave', handleMouseLeave)
|
element.addEventListener('mouseleave', handleMouseLeave)
|
||||||
element.addEventListener('click', handleClick)
|
element.addEventListener('click', handleClick)
|
||||||
element.addEventListener('mousedown', handleMouseDown)
|
element.addEventListener('mousedown', handleMouseDown)
|
||||||
@@ -117,7 +106,6 @@ const bindElementEvents = (element: HTMLElement, type: ElementType, path: string
|
|||||||
// 返回清理函数
|
// 返回清理函数
|
||||||
return () => {
|
return () => {
|
||||||
element.removeEventListener('mouseenter', handleMouseEnter)
|
element.removeEventListener('mouseenter', handleMouseEnter)
|
||||||
element.removeEventListener('mousemove', handleMouseMove)
|
|
||||||
element.removeEventListener('mouseleave', handleMouseLeave)
|
element.removeEventListener('mouseleave', handleMouseLeave)
|
||||||
element.removeEventListener('click', handleClick)
|
element.removeEventListener('click', handleClick)
|
||||||
element.removeEventListener('mousedown', handleMouseDown)
|
element.removeEventListener('mousedown', handleMouseDown)
|
||||||
@@ -290,6 +278,13 @@ watch(() => props.component, () => {
|
|||||||
}, 100)
|
}, 100)
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// 监听悬停目标变化,拖拽时更新层级
|
||||||
|
watch(() => interactionStore.hoverTarget, (target) => {
|
||||||
|
if (dragStore.isDragging && target?.element) {
|
||||||
|
dragStore.updateHierarchy(target.element)
|
||||||
|
}
|
||||||
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<template>
|
<template>
|
||||||
|
|||||||
@@ -78,6 +78,9 @@ const getNodeIcon = (data: TreeNodeData) => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
onMounted(() => {
|
onMounted(() => {
|
||||||
|
// 初始化Vue文件选择store
|
||||||
|
vueFileStore.initialize()
|
||||||
|
// 扫描文件
|
||||||
scanViewsDirectory()
|
scanViewsDirectory()
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|||||||
@@ -139,8 +139,64 @@ export const useDragStore = defineStore('drag', () => {
|
|||||||
// 按深度从深到浅排序(最深的在前面)
|
// 按深度从深到浅排序(最深的在前面)
|
||||||
nodes.sort((a, b) => b.depth - a.depth)
|
nodes.sort((a, b) => b.depth - a.depth)
|
||||||
|
|
||||||
|
// 检查层级结构是否改变(比较路径列表)
|
||||||
|
const oldPaths = hierarchyNodes.value.map(n => n.path).join(',')
|
||||||
|
const newPaths = nodes.map(n => n.path).join(',')
|
||||||
|
const hierarchyChanged = oldPaths !== newPaths
|
||||||
|
|
||||||
|
// 记录旧的选择深度和路径
|
||||||
|
const oldSelectedNode = hierarchyNodes.value[selectedHierarchyIndex.value]
|
||||||
|
const oldSelectedDepth = oldSelectedNode?.depth
|
||||||
|
const oldSelectedPath = oldSelectedNode?.path
|
||||||
|
|
||||||
|
// 始终更新层级节点列表
|
||||||
hierarchyNodes.value = nodes
|
hierarchyNodes.value = nodes
|
||||||
selectedHierarchyIndex.value = 0 // 默认选中最深层级
|
|
||||||
|
// 如果层级结构改变,尝试保持相同深度或路径的选择
|
||||||
|
if (hierarchyChanged) {
|
||||||
|
// 1. 优先尝试找到相同路径的节点
|
||||||
|
if (oldSelectedPath) {
|
||||||
|
const samePathIndex = nodes.findIndex(n => n.path === oldSelectedPath)
|
||||||
|
if (samePathIndex !== -1) {
|
||||||
|
selectedHierarchyIndex.value = samePathIndex
|
||||||
|
console.log('[DragStore] 保持相同路径选择:', oldSelectedPath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. 尝试找到相同深度的节点
|
||||||
|
if (oldSelectedDepth !== undefined) {
|
||||||
|
const sameDepthIndex = nodes.findIndex(n => n.depth === oldSelectedDepth)
|
||||||
|
if (sameDepthIndex !== -1) {
|
||||||
|
selectedHierarchyIndex.value = sameDepthIndex
|
||||||
|
console.log('[DragStore] 保持相同深度选择:', oldSelectedDepth)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 3. 找到最接近的深度
|
||||||
|
if (oldSelectedDepth !== undefined && nodes.length > 0) {
|
||||||
|
let closestIndex = 0
|
||||||
|
let minDiff = Math.abs(nodes[0].depth - oldSelectedDepth)
|
||||||
|
for (let i = 1; i < nodes.length; i++) {
|
||||||
|
const diff = Math.abs(nodes[i].depth - oldSelectedDepth)
|
||||||
|
if (diff < minDiff) {
|
||||||
|
minDiff = diff
|
||||||
|
closestIndex = i
|
||||||
|
}
|
||||||
|
}
|
||||||
|
selectedHierarchyIndex.value = closestIndex
|
||||||
|
console.log('[DragStore] 选择最接近深度:', nodes[closestIndex].path)
|
||||||
|
} else {
|
||||||
|
// 4. 默认选中最深层级
|
||||||
|
selectedHierarchyIndex.value = 0
|
||||||
|
console.log('[DragStore] 默认选择最深层级')
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// 层级结构未变,保持当前索引(确保不超出范围)
|
||||||
|
selectedHierarchyIndex.value = Math.min(selectedHierarchyIndex.value, nodes.length - 1)
|
||||||
|
console.log('[DragStore] 保持当前选择索引:', selectedHierarchyIndex.value)
|
||||||
|
}
|
||||||
|
|
||||||
console.log('[DragStore] 更新层级:', nodes.map(n => n.path))
|
console.log('[DragStore] 更新层级:', nodes.map(n => n.path))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -10,6 +10,20 @@ export interface VueFileNode {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useVueFileStore = defineStore('vueFile', () => {
|
export const useVueFileStore = defineStore('vueFile', () => {
|
||||||
|
// 从 API 加载上次选中的文件
|
||||||
|
const loadFromApi = async () => {
|
||||||
|
try {
|
||||||
|
const response = await fetch('/api/vue-file-selection')
|
||||||
|
if (response.ok) {
|
||||||
|
const data = await response.json()
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
console.error('加载Vue文件选择失败:', error)
|
||||||
|
}
|
||||||
|
return { path: null, name: null }
|
||||||
|
}
|
||||||
|
|
||||||
// 当前选中的Vue文件路径
|
// 当前选中的Vue文件路径
|
||||||
const selectedFilePath = ref<string | null>(null)
|
const selectedFilePath = ref<string | null>(null)
|
||||||
|
|
||||||
@@ -19,10 +33,26 @@ export const useVueFileStore = defineStore('vueFile', () => {
|
|||||||
// 文件树结构
|
// 文件树结构
|
||||||
const fileTree = ref<VueFileNode[]>([])
|
const fileTree = ref<VueFileNode[]>([])
|
||||||
|
|
||||||
|
// 是否已加载
|
||||||
|
const isLoaded = ref(false)
|
||||||
|
|
||||||
|
// 初始化加载
|
||||||
|
const initialize = async () => {
|
||||||
|
const stored = await loadFromApi()
|
||||||
|
selectedFilePath.value = stored.path
|
||||||
|
selectedFileName.value = stored.name
|
||||||
|
isLoaded.value = true
|
||||||
|
console.log('初始化Vue文件选择:', stored)
|
||||||
|
}
|
||||||
|
|
||||||
// 设置选中的文件
|
// 设置选中的文件
|
||||||
const selectFile = (path: string, name: string) => {
|
const selectFile = (path: string, name: string) => {
|
||||||
selectedFilePath.value = path
|
selectedFilePath.value = path
|
||||||
selectedFileName.value = name
|
selectedFileName.value = name
|
||||||
|
|
||||||
|
// 保存到 API
|
||||||
|
saveToApi({ path, name })
|
||||||
|
|
||||||
console.log('选中文件:', { path, name })
|
console.log('选中文件:', { path, name })
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -30,6 +60,22 @@ export const useVueFileStore = defineStore('vueFile', () => {
|
|||||||
const clearSelection = () => {
|
const clearSelection = () => {
|
||||||
selectedFilePath.value = null
|
selectedFilePath.value = null
|
||||||
selectedFileName.value = null
|
selectedFileName.value = null
|
||||||
|
|
||||||
|
// 清除 API 数据
|
||||||
|
saveToApi({ path: null, name: null })
|
||||||
|
}
|
||||||
|
|
||||||
|
// 保存到 API
|
||||||
|
const saveToApi = async (data: { path: string | null, name: string | null }) => {
|
||||||
|
try {
|
||||||
|
await fetch('/api/vue-file-selection', {
|
||||||
|
method: 'POST',
|
||||||
|
headers: { 'Content-Type': 'application/json' },
|
||||||
|
body: JSON.stringify(data)
|
||||||
|
})
|
||||||
|
} catch (error) {
|
||||||
|
console.error('保存Vue文件选择失败:', error)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 构建文件树(从import.meta.glob结果)
|
// 构建文件树(从import.meta.glob结果)
|
||||||
@@ -103,6 +149,8 @@ export const useVueFileStore = defineStore('vueFile', () => {
|
|||||||
selectedFilePath,
|
selectedFilePath,
|
||||||
selectedFileName,
|
selectedFileName,
|
||||||
fileTree,
|
fileTree,
|
||||||
|
isLoaded,
|
||||||
|
initialize,
|
||||||
selectFile,
|
selectFile,
|
||||||
clearSelection,
|
clearSelection,
|
||||||
buildFileTree
|
buildFileTree
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ import type { Plugin } from 'vite'
|
|||||||
const CONFIG_FILE = path.resolve(__dirname, 'config.json')
|
const CONFIG_FILE = path.resolve(__dirname, 'config.json')
|
||||||
const DESIGN_STATE_FILE = path.resolve(__dirname, 'design-state.json')
|
const DESIGN_STATE_FILE = path.resolve(__dirname, 'design-state.json')
|
||||||
const MATERIAL_STATES_FILE = path.resolve(__dirname, 'material-states.json')
|
const MATERIAL_STATES_FILE = path.resolve(__dirname, 'material-states.json')
|
||||||
|
const VUE_FILE_SELECTION_FILE = path.resolve(__dirname, 'vue-file-selection.json')
|
||||||
const DESIGN_COMPONENTS_DIR = path.resolve(__dirname, 'src/designComponents')
|
const DESIGN_COMPONENTS_DIR = path.resolve(__dirname, 'src/designComponents')
|
||||||
|
|
||||||
// 通用JSON文件读写处理器
|
// 通用JSON文件读写处理器
|
||||||
@@ -52,6 +53,7 @@ function configApiPlugin(): Plugin {
|
|||||||
const configHandler = createJsonHandler(CONFIG_FILE)
|
const configHandler = createJsonHandler(CONFIG_FILE)
|
||||||
const designStateHandler = createJsonHandler(DESIGN_STATE_FILE)
|
const designStateHandler = createJsonHandler(DESIGN_STATE_FILE)
|
||||||
const materialStatesHandler = createJsonHandler(MATERIAL_STATES_FILE)
|
const materialStatesHandler = createJsonHandler(MATERIAL_STATES_FILE)
|
||||||
|
const vueFileSelectionHandler = createJsonHandler(VUE_FILE_SELECTION_FILE)
|
||||||
|
|
||||||
return {
|
return {
|
||||||
name: 'config-api',
|
name: 'config-api',
|
||||||
@@ -158,6 +160,35 @@ function configApiPlugin(): Plugin {
|
|||||||
next()
|
next()
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
|
// Vue文件选择状态 API
|
||||||
|
server.middlewares.use('/api/vue-file-selection', (req, res, next) => {
|
||||||
|
if (req.method === 'GET') {
|
||||||
|
try {
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(vueFileSelectionHandler.read())
|
||||||
|
} catch (error) {
|
||||||
|
res.statusCode = 500
|
||||||
|
res.end(JSON.stringify({ error: 'Failed to read vue file selection' }))
|
||||||
|
}
|
||||||
|
} else if (req.method === 'POST') {
|
||||||
|
let body = ''
|
||||||
|
req.on('data', (chunk) => body += chunk.toString())
|
||||||
|
req.on('end', () => {
|
||||||
|
try {
|
||||||
|
const selection = JSON.parse(body)
|
||||||
|
vueFileSelectionHandler.write(JSON.stringify(selection, null, 2))
|
||||||
|
res.setHeader('Content-Type', 'application/json')
|
||||||
|
res.end(JSON.stringify({ success: true }))
|
||||||
|
} catch (error) {
|
||||||
|
res.statusCode = 500
|
||||||
|
res.end(JSON.stringify({ error: 'Failed to save vue file selection' }))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
next()
|
||||||
|
}
|
||||||
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
4
draggable-panels/vue-file-selection.json
Normal file
4
draggable-panels/vue-file-selection.json
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
{
|
||||||
|
"path": "../../../views/TestPage1.vue",
|
||||||
|
"name": "TestPage1.vue"
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user