修复中心停靠

This commit is contained in:
zqm
2025-11-18 15:39:46 +08:00
parent 0e163e0c32
commit 0d3b81df7f
5 changed files with 1184 additions and 112 deletions

View File

@@ -99,15 +99,19 @@
<!-- 内容区域 -->
<div class="vs-content">
<!-- 这里是内容区域优先显示slot内容如果没有slot内容则显示接收到的外部内容 -->
<template v-if="$slots.default">
<slot></slot>
</template>
<!-- 直接显示接收到的外部TabPage内容不需要额外包装 -->
<template v-else-if="receivedContent.length > 0">
<!-- 优先显示receivedContent停靠的外部内容 -->
<template v-if="receivedContent && receivedContent.length > 0">
<!-- 调试信息 -->
<div v-if="false" style="position: absolute; top: 0; left: 0; background: red; color: white; padding: 5px; z-index: 9999;">
DEBUG: receivedContent长度 = {{ receivedContent.length }}
<div v-for="(item, index) in receivedContent" :key="`debug-${index}`">
TabPage {{ index }}: {{ item.tabPage.id }} - {{ item.tabPage.title }} ({{ item.tabPage.panels.length }} panels)
</div>
</div>
<TabPage
v-for="(item, index) in receivedContent"
:key="`received-tab-${index}`"
:key="item.tabPage.id"
:id="item.tabPage.id"
:title="item.tabPage.title"
:panels="item.tabPage.panels"
@@ -118,6 +122,17 @@
@maximize="onPanelMaximize"
/>
</template>
<!-- 如果没有receivedContent但有slot内容显示slot内容 -->
<template v-else-if="$slots.default">
<slot></slot>
</template>
<!-- 当既没有slot内容也没有receivedContent时显示空状态 -->
<template v-else
style="display: flex; align-items: center; justify-content: center; height: 100%; color: #999;">
<span>此处可以放置内容</span>
</template>
</div>
</div>
</template>
@@ -579,7 +594,8 @@ const mergeAreaContent = (sourceArea) => {
// 处理源Area的所有tabPages
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
sourceArea.tabPages.forEach((tabPage, tabIndex) => {
const newTabPageId = `merged-tabpage-${Date.now()}-${tabIndex}`
// 保持原有的tabPage ID确保Vue组件状态连续性
const tabPageId = `merged-tabpage-${tabPage.id}`
const newPanels = (tabPage.panels || []).map((panel, panelIndex) => {
// 保持原有Panel ID不变确保Vue响应式和状态稳定性
console.log(`[Area] 添加Panel: ${panel.id}`)
@@ -590,11 +606,11 @@ const mergeAreaContent = (sourceArea) => {
})
receivedContent.value.push({
id: `received-${newTabPageId}`,
id: `received-${tabPageId}`,
title: tabPage.title || `标签页${tabIndex + 1}`,
tabPage: {
...tabPage,
id: newTabPageId,
id: tabPageId,
panels: newPanels
},
panels: newPanels
@@ -639,7 +655,7 @@ const mergeAreaContent = (sourceArea) => {
}
})
// 将新的Panel添加到现有TabPage
// 将新的Panel添加到现有TabPage保持ID连续性
existingTabPage.tabPage.panels.push(...newPanels)
// existingTabPage.panels 是旧引用,保持结构一致性但避免重复添加

File diff suppressed because it is too large Load Diff

View File

@@ -70,8 +70,8 @@
</button>
</div>
<!-- 内容区可折叠 -->
<div class="content-area bg-[#f5f7fb] flex-1 p-4" v-show="!collapsed">
<!-- 内容区可折叠添加滚动条 -->
<div class="content-area bg-[#f5f7fb] flex-1 p-4 overflow-auto min-h-0" v-show="!collapsed">
<div v-if="content" class="panel-content">
<div class="mb-4">
<h3 class="text-lg font-bold mb-2" :style="{ color: content.color }">
@@ -245,6 +245,38 @@ const onDragEnd = () => {
shape-rendering: crispEdges;
}
/* 内容区域滚动条样式 */
.content-area {
/* 确保滚动条正确显示 */
scrollbar-width: thin;
scrollbar-color: #c7d2ea #f5f7fb;
}
/* Webkit浏览器滚动条样式 */
.content-area::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.content-area::-webkit-scrollbar-track {
background: #f5f7fb;
border-radius: 6px;
}
.content-area::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #d0d6ea, #c0c7e0);
border: 1px solid #b0b6d6;
border-radius: 6px;
}
.content-area::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #c1c7e2, #b2b8d9);
}
.content-area::-webkit-scrollbar-corner {
background: #f5f7fb;
}
/* 禁用可能存在的旧伪元素样式 */
:deep(.icon-square::before),
:deep(.icon-square::after) {

View File

@@ -1,7 +1,7 @@
<template>
<div class="tab-page" :class="[`tab-page-${tabPosition}`]">
<!-- 顶部标签栏 -->
<div v-if="tabPosition === 'top' && showTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal">
<div v-if="tabPosition === 'top' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal">
<div
v-for="(panel, index) in panels"
:key="panel.id"
@@ -29,7 +29,35 @@
</div>
<!-- 左侧标签栏 -->
<div v-if="tabPosition === 'left' && showTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-left">
<div v-if="tabPosition === 'left' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-left">
<div
v-for="(panel, index) in panels"
:key="panel.id"
:class="['tab-item-vertical', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex flex-col items-center justify-center w-full h-full py-2">
<span class="tab-title-vertical">{{ panel.title }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80 mt-1"
@click.stop="closeTab(panel.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<line x1="2" y1="2" x2="9" y2="9" stroke="#e6efff" stroke-width="1" />
<line x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1" />
</svg>
</button>
</div>
</div>
<div class="tab-placeholder"></div>
</div>
<!-- 右侧标签栏 -->
<div v-if="tabPosition === 'right' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-right">
<div
v-for="(panel, index) in panels"
:key="panel.id"
@@ -83,9 +111,9 @@
@maximize="(event) => { console.log('🔸 TabPage转发最大化事件:', event); $emit('maximize', event); }"
@close="$emit('close', $event)"
@toggle-toolbar="$emit('toggleToolbar', $event)"
@drag-start="$emit('dragStart', $event)"
@drag-move="$emit('dragMove', $event)"
@drag-end="$emit('dragEnd', $event)"
@dragStart="$emit('dragStart', $event)"
@dragMove="$emit('dragMove', $event)"
@dragEnd="$emit('dragEnd', $event)"
/>
</div>
</template>
@@ -95,36 +123,8 @@
</div>
</div>
<!-- 右侧标签栏 -->
<div v-if="tabPosition === 'right' && showTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-right">
<div
v-for="(panel, index) in panels"
:key="panel.id"
:class="['tab-item-vertical', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex flex-col items-center justify-center w-full h-full py-2">
<span class="tab-title-vertical">{{ panel.title }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80 mt-1"
@click.stop="closeTab(panel.id)"
aria-label="关闭"
>
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
<line x1="2" y1="2" x2="9" y2="9" stroke="#e6efff" stroke-width="1" />
<line x1="2" y1="9" x2="9" y2="2" stroke="#e6efff" stroke-width="1" />
</svg>
</button>
</div>
</div>
<div class="tab-placeholder"></div>
</div>
<!-- 底部标签栏 -->
<div v-if="tabPosition === 'bottom' && showTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal tab-header-bottom">
<!-- 底部标签栏 - 移动到最后确保在底部显示 -->
<div v-if="tabPosition === 'bottom' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal tab-header-bottom">
<div
v-for="(panel, index) in panels"
:key="panel.id"
@@ -154,7 +154,7 @@
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted } from 'vue'
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue'
import Panel from './Panel.vue'
const props = defineProps({
@@ -187,6 +187,24 @@ const activeTabIndex = ref(-1)
let isDragging = false
let dragIndex = -1
// 计算属性:控制标签栏的显示
const shouldShowTabs = computed(() => {
// 暂时保持标签栏始终显示确保用户能清楚看到TabPage结构
// 未来可以优化当只有一个Panel且不是浮动窗口时隐藏标签栏
const result = props.showTabs && props.panels && props.panels.length > 0
// 调试信息输出TabPage的显示状态
console.log(`[TabPage ${props.id}] shouldShowTabs:`, {
showTabs: props.showTabs,
panelsLength: props.panels?.length || 0,
tabPosition: props.tabPosition,
shouldShow: result,
panels: props.panels
})
return result
})
// 设置激活的标签页
const setActiveTab = (index) => {
if (index >= 0 && index < props.panels.length) {
@@ -344,10 +362,34 @@ const onTabDragEnd = () => {
}
/* 底部位置的特殊样式 */
.tab-page-bottom {
flex-direction: column;
display: flex;
height: 100%;
}
.tab-page-bottom .tab-item {
border-radius: 0 0 3px 3px;
}
/* Tab内容区域 - 占用剩余空间 */
.tab-page-bottom .tab-content {
flex: 1;
overflow: auto;
min-height: 0; /* 重要允许flex子项收缩 */
}
/* 底部标签栏 - 固定高度,始终在底部 */
.tab-page-bottom .tab-header-bottom {
flex-shrink: 0;
height: 28px;
margin-top: auto;
border-top: none;
border-bottom: none;
padding-top: 0;
padding-bottom: 2px;
}
.tab-title {
flex: 1;
text-align: left;

View File

@@ -41,4 +41,73 @@
4.2. 如果目标Area内容区已经包含一个TabPage则将源Area的TabPage组件的每个标签页添加到目标Area的Tabpage中。这个源Area和源Area的TabPage组件保存到DockLayout的的隐藏列表中。
5. 当将源Area拖动到外部边缘指示器时
5.1. 如果主区域内只有一个Area(目标区域)则压缩目标Area的空间源区域和目标区域并排放置在主区域内。
- 实现压缩目标Area空间的逻辑已实现
- 设置源Area和目标Area的并排布局已实现
- 在并排的Area之间添加ResizeBar组件以支持拖动调整大小
- ResizeBar的方向根据停靠方向动态设置
* 左右停靠使用水平ResizeBar垂直分割线
* 上下停靠使用垂直ResizeBar水平分割线
- 确保ResizeBar支持最小/最大尺寸限制
5.2. 如果主区域内没有Area则从隐藏列表中取出Area如果隐藏列表中没有Area则创建一个Area将主区域内的组件添加到Area中作为目标Area。 压缩目标Area的空间源区域和目标区域并排放置在主区域内。
- 检查隐藏列表是否有可用Area
- 如果没有隐藏Area创建新的Area
- 将主区域内的组件移动到新创建的Area中作为目标Area
- 执行并排放置逻辑复用5.1的实现)
- 添加ResizeBar支持拖动调整
5.3. 并排停靠后的ResizeBar集成
- 在并排的Area之间动态插入ResizeBar组件
- ResizeBar的事件处理
* resize事件实时调整并排Area的尺寸比例
* resizeStart/resizeEnd事件处理调整开始/结束状态
- 保持Area的最小尺寸限制避免调整过小无法使用
- 调整后更新Area的ratio属性以保持布局持久化
- 支持双向调整拖动ResizeBar可以同时影响两个Area的大小
5.4. ResizeBar的样式和交互优化
- ResizeBar的显示条件仅在并排布局中存在时显示
- 鼠标悬停效果高亮显示ResizeBar指示器
- 拖动时的视觉反馈:实时显示调整效果
- 确保ResizeBar不会影响Area的拖拽和停靠功能
### ResizeBar集成技术要求
1. ResizeBar组件依赖
- 确保ResizeBar.vue组件已正确导入和使用
- ResizeBar支持水平和垂直两种方向
- 提供resize、resizeStart、resizeEnd事件接口
2. 布局管理
- 并排布局时自动在两个Area之间插入ResizeBar
- ResizeBar的位置计算居中放置在两个Area的分界线上
- ResizeBar的层级管理确保在Area之上但在停靠指示器之下
3. 事件处理
- ResizeBar的resize事件触发时重新计算并排Area的尺寸比例
- 保持源Area和目标Area的比例总和为1
- 调整过程中实时更新Area的width、height或ratio属性
4. 用户体验优化
- ResizeBar的视觉反馈悬停时高亮拖动时变色
- 键盘支持:方向键微调尺寸(可选功能)
- 触摸设备支持:移动端拖动调整(可选功能)
### 测试验证点
1. 基本并排功能
- 拖动源Area到边缘指示器时目标Area被正确压缩
- 两个Area能够并排显示在主区域内
- 比例分配符合预期默认50%:50%
2. ResizeBar功能
- 并排布局时正确显示ResizeBar
- 拖动ResizeBar能够调整两个Area的尺寸
- 调整后比例保持正确,无重叠或间隙
- 拖动过程中有适当的视觉反馈
3. 边界情况处理
- 调整到最小尺寸时的处理
- 快速连续拖动的稳定性
- 窗口大小变化时的布局保持
4. 与现有功能的兼容性
- 不影响现有的拖拽停靠功能
- 不影响Panel的最大化/还原功能
- 不影响TabPage的切换和关闭功能