修复中心停靠
This commit is contained in:
@@ -99,15 +99,19 @@
|
|||||||
|
|
||||||
<!-- 内容区域 -->
|
<!-- 内容区域 -->
|
||||||
<div class="vs-content">
|
<div class="vs-content">
|
||||||
<!-- 这里是内容区域,优先显示slot内容,如果没有slot内容则显示接收到的外部内容 -->
|
<!-- 优先显示receivedContent(停靠的外部内容) -->
|
||||||
<template v-if="$slots.default">
|
<template v-if="receivedContent && receivedContent.length > 0">
|
||||||
<slot></slot>
|
<!-- 调试信息 -->
|
||||||
</template>
|
<div v-if="false" style="position: absolute; top: 0; left: 0; background: red; color: white; padding: 5px; z-index: 9999;">
|
||||||
<!-- 直接显示接收到的外部TabPage内容,不需要额外包装 -->
|
DEBUG: receivedContent长度 = {{ receivedContent.length }}
|
||||||
<template v-else-if="receivedContent.length > 0">
|
<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
|
<TabPage
|
||||||
v-for="(item, index) in receivedContent"
|
v-for="(item, index) in receivedContent"
|
||||||
:key="`received-tab-${index}`"
|
:key="item.tabPage.id"
|
||||||
:id="item.tabPage.id"
|
:id="item.tabPage.id"
|
||||||
:title="item.tabPage.title"
|
:title="item.tabPage.title"
|
||||||
:panels="item.tabPage.panels"
|
:panels="item.tabPage.panels"
|
||||||
@@ -118,6 +122,17 @@
|
|||||||
@maximize="onPanelMaximize"
|
@maximize="onPanelMaximize"
|
||||||
/>
|
/>
|
||||||
</template>
|
</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>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -579,7 +594,8 @@ const mergeAreaContent = (sourceArea) => {
|
|||||||
// 处理源Area的所有tabPages
|
// 处理源Area的所有tabPages
|
||||||
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
|
if (sourceArea.tabPages && sourceArea.tabPages.length > 0) {
|
||||||
sourceArea.tabPages.forEach((tabPage, tabIndex) => {
|
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) => {
|
const newPanels = (tabPage.panels || []).map((panel, panelIndex) => {
|
||||||
// 保持原有Panel ID不变,确保Vue响应式和状态稳定性
|
// 保持原有Panel ID不变,确保Vue响应式和状态稳定性
|
||||||
console.log(`[Area] 添加Panel: ${panel.id}`)
|
console.log(`[Area] 添加Panel: ${panel.id}`)
|
||||||
@@ -590,11 +606,11 @@ const mergeAreaContent = (sourceArea) => {
|
|||||||
})
|
})
|
||||||
|
|
||||||
receivedContent.value.push({
|
receivedContent.value.push({
|
||||||
id: `received-${newTabPageId}`,
|
id: `received-${tabPageId}`,
|
||||||
title: tabPage.title || `标签页${tabIndex + 1}`,
|
title: tabPage.title || `标签页${tabIndex + 1}`,
|
||||||
tabPage: {
|
tabPage: {
|
||||||
...tabPage,
|
...tabPage,
|
||||||
id: newTabPageId,
|
id: tabPageId,
|
||||||
panels: newPanels
|
panels: newPanels
|
||||||
},
|
},
|
||||||
panels: newPanels
|
panels: newPanels
|
||||||
@@ -639,7 +655,7 @@ const mergeAreaContent = (sourceArea) => {
|
|||||||
}
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
// 将新的Panel添加到现有TabPage
|
// 将新的Panel添加到现有TabPage,保持ID连续性
|
||||||
existingTabPage.tabPage.panels.push(...newPanels)
|
existingTabPage.tabPage.panels.push(...newPanels)
|
||||||
// existingTabPage.panels 是旧引用,保持结构一致性但避免重复添加
|
// existingTabPage.panels 是旧引用,保持结构一致性但避免重复添加
|
||||||
|
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -70,8 +70,8 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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 v-if="content" class="panel-content">
|
||||||
<div class="mb-4">
|
<div class="mb-4">
|
||||||
<h3 class="text-lg font-bold mb-2" :style="{ color: content.color }">
|
<h3 class="text-lg font-bold mb-2" :style="{ color: content.color }">
|
||||||
@@ -245,6 +245,38 @@ const onDragEnd = () => {
|
|||||||
shape-rendering: crispEdges;
|
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::before),
|
||||||
:deep(.icon-square::after) {
|
:deep(.icon-square::after) {
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
<template>
|
<template>
|
||||||
<div class="tab-page" :class="[`tab-page-${tabPosition}`]">
|
<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
|
<div
|
||||||
v-for="(panel, index) in panels"
|
v-for="(panel, index) in panels"
|
||||||
:key="panel.id"
|
:key="panel.id"
|
||||||
@@ -29,7 +29,35 @@
|
|||||||
</div>
|
</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
|
<div
|
||||||
v-for="(panel, index) in panels"
|
v-for="(panel, index) in panels"
|
||||||
:key="panel.id"
|
:key="panel.id"
|
||||||
@@ -83,9 +111,9 @@
|
|||||||
@maximize="(event) => { console.log('🔸 TabPage转发最大化事件:', event); $emit('maximize', event); }"
|
@maximize="(event) => { console.log('🔸 TabPage转发最大化事件:', event); $emit('maximize', event); }"
|
||||||
@close="$emit('close', $event)"
|
@close="$emit('close', $event)"
|
||||||
@toggle-toolbar="$emit('toggleToolbar', $event)"
|
@toggle-toolbar="$emit('toggleToolbar', $event)"
|
||||||
@drag-start="$emit('dragStart', $event)"
|
@dragStart="$emit('dragStart', $event)"
|
||||||
@drag-move="$emit('dragMove', $event)"
|
@dragMove="$emit('dragMove', $event)"
|
||||||
@drag-end="$emit('dragEnd', $event)"
|
@dragEnd="$emit('dragEnd', $event)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
@@ -95,36 +123,8 @@
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<!-- 右侧标签栏 -->
|
<!-- 底部标签栏 - 移动到最后确保在底部显示 -->
|
||||||
<div v-if="tabPosition === 'right' && showTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-right">
|
<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"
|
|
||||||
: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
|
<div
|
||||||
v-for="(panel, index) in panels"
|
v-for="(panel, index) in panels"
|
||||||
:key="panel.id"
|
:key="panel.id"
|
||||||
@@ -154,7 +154,7 @@
|
|||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup>
|
<script setup>
|
||||||
import { defineProps, defineEmits, ref, onMounted } from 'vue'
|
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue'
|
||||||
import Panel from './Panel.vue'
|
import Panel from './Panel.vue'
|
||||||
|
|
||||||
const props = defineProps({
|
const props = defineProps({
|
||||||
@@ -187,6 +187,24 @@ const activeTabIndex = ref(-1)
|
|||||||
let isDragging = false
|
let isDragging = false
|
||||||
let dragIndex = -1
|
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) => {
|
const setActiveTab = (index) => {
|
||||||
if (index >= 0 && index < props.panels.length) {
|
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 {
|
.tab-page-bottom .tab-item {
|
||||||
border-radius: 0 0 3px 3px;
|
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 {
|
.tab-title {
|
||||||
flex: 1;
|
flex: 1;
|
||||||
text-align: left;
|
text-align: left;
|
||||||
|
|||||||
@@ -41,4 +41,73 @@
|
|||||||
4.2. 如果目标Area内容区已经包含一个TabPage,则将源Area的TabPage组件的每个标签页添加到目标Area的Tabpage中。这个源Area和源Area的TabPage组件保存到DockLayout的的隐藏列表中。
|
4.2. 如果目标Area内容区已经包含一个TabPage,则将源Area的TabPage组件的每个标签页添加到目标Area的Tabpage中。这个源Area和源Area的TabPage组件保存到DockLayout的的隐藏列表中。
|
||||||
5. 当将源Area拖动到外部边缘指示器时
|
5. 当将源Area拖动到外部边缘指示器时
|
||||||
5.1. 如果主区域内只有一个Area(目标区域),则压缩目标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