拖拽Area停靠中心区域

This commit is contained in:
zqm
2025-11-17 10:59:46 +08:00
parent b4b2e75fef
commit 8e472d4497
4 changed files with 525 additions and 26 deletions

View File

@@ -99,14 +99,52 @@
<!-- 内容区域 -->
<div class="vs-content">
<!-- 这里是内容区域使用slot接收子组件并监听Panel的maximize事件 -->
<!-- 这里是内容区域优先显示slot内容如果没有slot内容则显示接收到的外部内容 -->
<template v-if="$slots.default">
<slot @maximize="onPanelMaximize"></slot>
</template>
<!-- 直接显示接收到的外部TabPage内容不需要额外包装 -->
<template v-else-if="receivedContent.length > 0">
<TabPage
v-for="(item, index) in receivedContent"
:key="`received-tab-${index}`"
:id="item.tabPage.id"
:title="item.tabPage.title"
:panels="item.tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="() => {}"
@tabDragMove="() => {}"
@tabDragEnd="() => {}"
>
<!-- 在TabPage内渲染其包含的Panels -->
<Panel
v-for="panel in item.tabPage.panels"
:key="`received-panel-${index}-${panel.id}`"
:id="panel.id"
:title="panel.title"
:collapsed="panel.collapsed"
:toolbarExpanded="panel.toolbarExpanded"
:maximized="panel.maximized"
@toggleCollapse="() => {}"
@maximize="onPanelMaximize"
@close="() => {}"
@toggleToolbar="() => {}"
@dragStart="() => {}"
@dragMove="() => {}"
@dragEnd="() => {}"
@dragover="handleDragOver"
@dragleave="handleDragLeave"
/>
</TabPage>
</template>
</div>
</div>
</template>
<script setup>
import { defineProps, computed, defineEmits, ref, onMounted, watch } from 'vue'
import { defineProps, computed, defineEmits, ref, onMounted, watch, defineExpose } from 'vue'
import TabPage from './TabPage.vue'
import Panel from './Panel.vue'
const props = defineProps({
id: { type: String, required: true },
@@ -137,6 +175,9 @@ const originalPosition = ref({
// 保存最大化前的位置和大小,用于还原
const maximizedFromPosition = ref(null)
// 存储接收到的外部Area内容
const receivedContent = ref([])
// 拖拽相关状态
const isDragging = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
@@ -527,6 +568,61 @@ onMounted(() => {
})
}
})
// 添加Area内容的方法用于主区域的Area接收外部Area内容
const appendAreaContent = (sourceArea) => {
console.log(`[Area] ${props.id} 接收到内容移动请求:`, sourceArea)
if (!sourceArea) {
console.warn('[Area] 源Area为空无法接收内容')
return false
}
try {
// 清空之前的内容确保只接收一个新的TabPage
receivedContent.value = []
// 处理源Area的tabPages只接收第一个TabPage用户要求只能接收一个TabPage
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
const tabPage = sourceArea.tabPages[0] // 只接收第一个TabPage
if (tabPage) {
// 确保所有Panel都处于最大化状态显示还原按钮
const maximizedPanels = (tabPage.panels || []).map(panel => ({
...panel,
maximized: true // 设置为最大化状态
}))
receivedContent.value.push({
id: `received-${tabPage.id || Date.now() + Math.random()}`,
title: tabPage.title || '标签页',
tabPage: {
...tabPage,
panels: maximizedPanels // 使用最大化状态的Panel
},
panels: maximizedPanels
})
console.log(`[Area] 成功接收tabPage: ${tabPage.title}并设置Panel为最大化状态`)
}
} else {
console.warn('[Area] 源Area中没有tabPages数据')
}
console.log(`[Area] ${props.id} 当前接收内容数量: ${receivedContent.value.length}`)
return true
} catch (error) {
console.error('[Area] 接收外部内容时出错:', error)
return false
}
}
// 暴露方法给父组件调用
defineExpose({
appendAreaContent,
id: props.id,
title: props.title,
isMaximized: isMaximized.value
})
</script>
<style scoped>
@@ -784,4 +880,55 @@ onMounted(() => {
align-items: stretch;
justify-content: stretch;
}
/* 接收到的外部内容样式 */
.received-content {
width: 100%;
height: 100%;
overflow: auto;
background: #f8f9ff;
border: 1px solid #e0e6f0;
border-radius: 4px;
}
.received-item {
background: white;
border: 1px solid #d0d7e2;
border-radius: 6px;
margin: 8px;
padding: 12px;
box-shadow: 0 2px 4px rgba(0,0,0,0.1);
transition: box-shadow 0.2s ease;
}
.received-item:hover {
box-shadow: 0 4px 8px rgba(0,0,0,0.15);
}
.received-title {
font-size: 13px;
font-weight: 600;
color: #2c3e7a;
margin-bottom: 8px;
padding-bottom: 6px;
border-bottom: 1px solid #e8edf7;
}
.received-body {
min-height: 60px;
}
/* TabPage和Panel容器样式 */
.tab-page-container,
.area-container {
width: 100%;
height: 100%;
border: 1px dashed #c7d2ea;
border-radius: 4px;
padding: 12px;
background: #fafbff;
color: #6b7aa9;
text-align: center;
font-style: italic;
}
</style>

View File

@@ -412,7 +412,6 @@
<!-- 中心指示器独立于中心区域容器位于容器正中央 -->
<svg
v-if="props.hideEdgeIndicators"
width="41"
height="41"
viewBox="0 0 40 40"

View File

@@ -104,6 +104,9 @@ const windowState = ref('最大化')
// 浮动区域列表 - 每个area包含panels数组
const floatingAreas = ref([])
// 隐藏区域列表 - 存储被隐藏的Area
const hiddenAreas = ref([])
// 容器引用
const dockLayoutRef = ref(null)
// 主区域引用
@@ -329,6 +332,28 @@ const onToggleToolbar = (id) => {
}
}
// 隐藏Area管理方法 - 第1步实现
// 将Area添加到隐藏列表
const addAreaToHiddenList = (area) => {
// 确保area有唯一标识符
if (!area.id) {
area.id = `hidden-area-${Date.now()}`
}
// 检查是否已经存在于隐藏列表中
const existingIndex = hiddenAreas.value.findIndex(h => h.id === area.id)
if (existingIndex === -1) {
// 添加到隐藏列表
hiddenAreas.value.push({
...area,
hiddenAt: new Date().toISOString()
})
console.log('Area已添加到隐藏列表:', area.id)
} else {
console.warn('Area已在隐藏列表中:', area.id)
}
}
// Panel拖拽开始
const onPanelDragStart = (areaId, event) => {
const area = floatingAreas.value.find(a => a.id === areaId)
@@ -436,17 +461,23 @@ const onPanelDragMove = (areaId, event) => {
const onPanelDragEnd = () => {
panelDragState.value.isDragging = false
const currentAreaId = panelDragState.value.currentAreaId
panelDragState.value.currentAreaId = null
// 3.1 在onPanelDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center' && currentAreaId) {
// 处理中心停靠
const result = handleCenterDocking(currentAreaId)
if (!result.success) {
console.warn('中心停靠失败:', result.message)
} else {
console.log('中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
// 如果有活动的停靠区域,可以在这里处理停靠逻辑
if (activeDockZone.value) {
// 这里可以实现具体的停靠逻辑
}
}
// Area拖拽开始
@@ -454,6 +485,24 @@ const onAreaDragStart = (areaId, event) => {
const area = floatingAreas.value.find(a => a.id === areaId)
if (!area) return
// 设置拖拽状态类似TabPage拖拽
panelDragState.value.isDragging = true
panelDragState.value.currentAreaId = areaId
panelDragState.value.startClientPos = {
x: event.clientX,
y: event.clientY
}
panelDragState.value.startAreaPos = {
x: area.x,
y: area.y
}
// 初始化鼠标位置跟踪
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
// 拖拽开始时显示指示器
showDockIndicator.value = true
@@ -477,12 +526,33 @@ const onAreaDragStart = (areaId, event) => {
// Area拖拽移动
const onAreaDragMove = (areaId, event) => {
// 更新鼠标位置
currentMousePosition.value = {
x: event.clientX,
y: event.clientY
}
// 根据鼠标位置动态更新停靠区域
updateDockZoneByMousePosition(event.clientX, event.clientY)
}
// Area拖拽结束
const onAreaDragEnd = (areaId, event) => {
// 清理拖拽状态
panelDragState.value.isDragging = false
panelDragState.value.currentAreaId = null
// 3.1 在onAreaDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center') {
// 处理中心停靠
const result = handleCenterDocking(areaId)
if (!result.success) {
console.warn('Area中心停靠失败:', result.message)
} else {
console.log('Area中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
@@ -595,8 +665,20 @@ const onTabDragMove = (areaId, event) => {
const onTabDragEnd = () => {
tabDragState.value.isDragging = false
const currentAreaId = tabDragState.value.currentAreaId
tabDragState.value.currentAreaId = null
// 3.2 在onTabDragEnd方法中添加中心停靠检测
if (activeDockZone.value === 'center' && currentAreaId) {
// 处理中心停靠
const result = handleCenterDocking(currentAreaId)
if (!result.success) {
console.warn('中心停靠失败:', result.message)
} else {
console.log('中心停靠成功:', result.message)
}
}
// 隐藏停靠指示器
showDockIndicator.value = false
activeDockZone.value = null
@@ -642,7 +724,22 @@ watch(floatingAreas, () => {
// 当主区域内没有其他Area时隐藏外部边缘指示器只显示中心指示器
// 根据鼠标位置动态更新停靠区域
// 第5步优化UI指示器显示逻辑当主区域为空时
/**
* 检查主区域是否为空
* @returns {boolean} 主区域是否为空
*/
const isMainAreaEmpty = () => {
checkMainContentForAreas()
return !hasAreasInMainContent.value
}
/**
* 根据鼠标位置动态更新停靠区域(优化版)
* 当主区域为空时,只显示中心指示器
* @param {number} mouseX - 鼠标X坐标
* @param {number} mouseY - 鼠标Y坐标
*/
const updateDockZoneByMousePosition = (mouseX, mouseY) => {
if (!dockLayoutRef.value || !targetAreaRect.value) return
@@ -658,8 +755,19 @@ const updateDockZoneByMousePosition = (mouseX, mouseY) => {
let newActiveZone = null
// 检查主区域是否为空
const mainAreaEmpty = isMainAreaEmpty()
if (relativeX >= 0 && relativeX <= 1 && relativeY >= 0 && relativeY <= 1) {
// 鼠标在目标区域内
if (mainAreaEmpty) {
// 主区域为空时:只允许停靠到中心区域
if (relativeX >= 0.3 && relativeX <= 0.7 && relativeY >= 0.3 && relativeY <= 0.7) {
// 扩大的中心区域 (30%-70% 的中心区域)
newActiveZone = 'center'
}
} else {
// 主区域不为空时:显示所有指示器选项
if (relativeY <= threshold) {
newActiveZone = 'top'
} else if (relativeY >= 1 - threshold) {
@@ -673,11 +781,17 @@ const updateDockZoneByMousePosition = (mouseX, mouseY) => {
newActiveZone = 'center'
}
}
}
// 只有当停靠区域改变时才更新,减少不必要的重新渲染
if (activeDockZone.value !== newActiveZone) {
activeDockZone.value = newActiveZone
// 如果激活的区域不为空,显示指示器;如果为空,隐藏指示器
if (newActiveZone) {
showDockIndicator.value = true
} else {
showDockIndicator.value = false
}
}
}
@@ -790,9 +904,243 @@ const onPanelMaximizeSync = ({ areaId, maximized }) => {
}
}
// 第2步Area验证逻辑 - 目标Area内容区为空的情况
/**
* 检查Area是否可以停靠到中心区域
* @param {Object} sourceArea - 源Area对象
* @param {Object} targetArea - 目标Area对象主区域
* @returns {Object} 验证结果 {canDock: boolean, reason: string}
*/
const canDockToCenter = (sourceArea, targetArea) => {
// 验证1检查源Area是否包含TabPage
if (!sourceArea) {
return {
canDock: false,
reason: '源Area不存在'
}
}
if (!sourceArea.tabPages || sourceArea.tabPages.length === 0) {
return {
canDock: false,
reason: '源Area不包含TabPage无法停靠'
}
}
// 验证2检查目标Area主区域- 支持Area级别停靠
if (targetArea === mainAreaRef.value) {
// Area级别停靠支持将源Area的内容移动到主区域
// 不再要求主区域完全为空允许Area停靠
console.log('Area级别停靠源Area可停靠到主区域')
} else {
// 对于其他目标Area的验证逻辑可以后续扩展
return {
canDock: false,
reason: '暂不支持停靠到非主区域'
}
}
// 验证3确保包含TabPage的Area才能停靠到中心
const sourceTabPage = sourceArea.tabPages[0]
if (!sourceTabPage.panels || sourceTabPage.panels.length === 0) {
return {
canDock: false,
reason: '源Area的TabPage不包含Panel无法停靠'
}
}
// 所有验证通过
return {
canDock: true,
reason: '验证通过,可以停靠到中心区域'
}
}
// 第3步中心停靠逻辑
/**
* 处理中心停靠的核心函数
* @param {string} areaId - 源Area的ID
* @returns {Object} 处理结果 {success: boolean, message: string}
*/
const handleCenterDocking = (areaId) => {
try {
// 3.4 查找源Area
const sourceArea = floatingAreas.value.find(a => a.id === areaId)
if (!sourceArea) {
return {
success: false,
message: '找不到指定的Area'
}
}
// 2.1-2.5 验证是否可以停靠到中心
const validation = canDockToCenter(sourceArea, mainAreaRef.value)
if (!validation.canDock) {
return {
success: false,
message: `验证失败:${validation.reason}`
}
}
// 3.4 实现Area子组件移动到主区域的逻辑
// 将Area的内容移动到主区域通过主区域Ref处理
if (mainAreaRef.value && mainAreaRef.value.appendAreaContent) {
mainAreaRef.value.appendAreaContent(sourceArea)
} else {
console.warn('主区域Ref不可用无法移动内容')
}
// 3.5 实现Area添加到隐藏列表的逻辑
addAreaToHiddenList(sourceArea)
// 3.6 实现Area从浮动区域移除的逻辑
const index = floatingAreas.value.findIndex(a => a.id === areaId)
if (index !== -1) {
floatingAreas.value.splice(index, 1)
}
// 3.7 更新主区域和浮动区域的响应式状态
checkMainContentForAreas()
return {
success: true,
message: '成功停靠到中心区域'
}
} catch (error) {
console.error('中心停靠处理错误:', error)
return {
success: false,
message: `停靠处理出错:${error.message}`
}
}
}
// 第4步在defineExpose中暴露隐藏列表管理方法
/**
* 获取隐藏Area列表
* @returns {Array} 隐藏Area列表
*/
const getHiddenAreas = () => {
return [...hiddenAreas.value]
}
/**
* 从隐藏列表恢复Area到浮动区域
* @param {string} areaId - Area的ID
* @returns {Object} 恢复结果 {success: boolean, message: string}
*/
const restoreAreaFromHidden = (areaId) => {
try {
const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId)
if (hiddenIndex === -1) {
return {
success: false,
message: '找不到指定的隐藏Area'
}
}
const areaToRestore = hiddenAreas.value[hiddenIndex]
// 检查是否已存在相同的Area避免重复
const existingIndex = floatingAreas.value.findIndex(area => area.id === areaId)
if (existingIndex !== -1) {
return {
success: false,
message: 'Area已存在于浮动区域中'
}
}
// 恢复Area到浮动区域
floatingAreas.value.push(areaToRestore)
// 从隐藏列表移除
hiddenAreas.value.splice(hiddenIndex, 1)
// 更新状态
checkMainContentForAreas()
return {
success: true,
message: 'Area已成功恢复到浮动区域'
}
} catch (error) {
console.error('恢复Area时出错:', error)
return {
success: false,
message: `恢复失败:${error.message}`
}
}
}
/**
* 从隐藏列表移除Area不恢复
* @param {string} areaId - Area的ID
* @returns {Object} 移除结果 {success: boolean, message: string}
*/
const removeFromHiddenList = (areaId) => {
try {
const hiddenIndex = hiddenAreas.value.findIndex(area => area.id === areaId)
if (hiddenIndex === -1) {
return {
success: false,
message: '找不到指定的隐藏Area'
}
}
// 从隐藏列表移除
hiddenAreas.value.splice(hiddenIndex, 1)
return {
success: true,
message: 'Area已从隐藏列表中移除'
}
} catch (error) {
console.error('移除Area时出错:', error)
return {
success: false,
message: `移除失败:${error.message}`
}
}
}
/**
* 清空隐藏列表
* @returns {Object} 清空结果 {success: boolean, message: string, removedCount: number}
*/
const clearHiddenList = () => {
try {
const count = hiddenAreas.value.length
hiddenAreas.value.splice(0, hiddenAreas.value.length)
return {
success: true,
message: `已清空隐藏列表,移除${count}个Area`,
removedCount: count
}
} catch (error) {
console.error('清空隐藏列表时出错:', error)
return {
success: false,
message: `清空失败:${error.message}`,
removedCount: 0
}
}
}
// 暴露方法给父组件
defineExpose({
addFloatingPanel
// 原有方法
addFloatingPanel,
// 第4步隐藏列表管理方法
getHiddenAreas,
restoreAreaFromHidden,
removeFromHiddenList,
clearHiddenList
})
</script>

View File

@@ -35,5 +35,10 @@
1. 当一个Panel被拖动时显示停靠指示器。
2. 当拖动Panel到指示器时显示停靠区。
3. 当主区域内没有其他Area时隐藏外部边缘指示器、中心区域容器只显示中心指示器。
4. 当将Area拖动到中心指示器时
4.1. 如果Area只有一个直接子组件TabPage则将Area的子组件停靠到中心区域。这个Area保存到DockLayout的的隐藏列表中。
4. 当将源Panel拖动到中心指示器时
4.1. 如果Area只有一个直接子组件TabPage且目标Area内容区为空则将Area的子组件停靠到中心区域。这个Area保存到DockLayout的的隐藏列表中。
5. 当将源Area拖动到中心指示器时
5.1. 如果源Area只有一个直接子组件TabPage且目标Area内容区为空则将源Area的子组件停靠到中心区域。这个源Area保存到DockLayout的的隐藏列表中。
5.2. 如果源Area只有一个直接子组件TabPage且目标Area内容区已经包含一个TabPage则将源Area的TabPage组件的每个标签页移动并添加到目标Area的Tabpage中。这个源Area保存到DockLayout的的隐藏列表中。