修复中心停靠
This commit is contained in:
@@ -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
@@ -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) {
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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的切换和关闭功能
|
||||
|
||||
|
||||
Reference in New Issue
Block a user