7. TabPage的页标签可以定义在上、右、下、左四个边缘显示,通过对外提供的属性设置。

This commit is contained in:
zqm
2025-11-17 09:10:29 +08:00
parent fd4c694b46
commit 5502db6d42
3 changed files with 248 additions and 27 deletions

View File

@@ -62,6 +62,7 @@
:id="tabPage.id"
:title="tabPage.title"
:panels="tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="onTabDragStart(area.id, $event)"
@tabDragMove="onTabDragMove(area.id, $event)"
@tabDragEnd="onTabDragEnd"

View File

@@ -1,7 +1,7 @@
<template>
<div class="tab-page">
<!-- 使用panels数组渲染标签栏 -->
<div v-if="showTabs && panels && panels.length > 0" class="tab-header">
<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-for="(panel, index) in panels"
:key="panel.id"
@@ -27,11 +27,96 @@
</div>
<div class="tab-placeholder"></div>
</div>
<!-- 左侧标签栏 -->
<div v-if="tabPosition === 'left' && showTabs && 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>
<!-- Tab页内容区域 -->
<div class="tab-content">
<!-- 渲染slot内容Panel组件 -->
<slot></slot>
</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-for="(panel, index) in panels"
:key="panel.id"
:class="['tab-item', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
>
<div class="flex items-center justify-between h-full px-3">
<span class="tab-title">{{ panel.title }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@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>
</template>
@@ -50,6 +135,12 @@ const props = defineProps({
showTabs: {
type: Boolean,
default: true
},
// 标签页位置top(顶部), right(右侧), bottom(底部), left(左侧)
tabPosition: {
type: String,
default: 'top',
validator: (value) => ['top', 'right', 'bottom', 'left'].includes(value)
}
})
@@ -138,9 +229,9 @@ const onTabDragEnd = () => {
</script>
<style scoped>
/* 主容器样式 */
.tab-page {
display: flex;
flex-direction: column;
width: 100%;
height: 100%;
background: #5D6B99;
@@ -150,28 +241,63 @@ const onTabDragEnd = () => {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
/* 顶部和底部位置:水平布局 */
.tab-page-top,
.tab-page-bottom {
flex-direction: column;
}
/* 左侧和右侧位置:水平布局但标签栏垂直 */
.tab-page-left,
.tab-page-right {
flex-direction: row;
}
:root {
--vs-blue-top: #4f72b3;
--vs-blue-bottom: #3c5a99;
}
/* 标签栏样式 - 模仿Windows Forms */
.tab-header {
/* 水平标签栏样式 */
.tab-header-horizontal {
display: flex;
flex-wrap: nowrap;
height: 28px;
background: transparent;
border-bottom: none;
overflow-x: auto;
overflow-y: hidden;
padding-top: 2px;
}
.tab-header-bottom {
border-top: none;
border-bottom: none;
padding-top: 0;
padding-bottom: 2px;
}
/* 垂直标签栏样式 */
.tab-header-vertical {
display: flex;
flex-wrap: nowrap;
width: 28px;
background: transparent;
overflow-x: hidden;
overflow-y: auto;
padding-left: 2px;
}
.tab-header-right {
flex-direction: column;
padding-left: 0;
padding-right: 2px;
}
/* 水平标签项样式 */
.tab-item {
height: 26px;
margin-left: 1px;
background: linear-gradient(to bottom, var(--vs-blue-top), var(--vs-blue-bottom));
border-bottom-color: #c7d2ea;
border-radius: 3px 3px 0 0;
cursor: pointer;
white-space: nowrap;
@@ -179,6 +305,13 @@ const onTabDragEnd = () => {
font-size: 12px;
user-select: none;
min-width: 60px;
display: flex;
align-items: center;
}
/* 底部位置的特殊样式 */
.tab-page-bottom .tab-item {
border-radius: 0 0 3px 3px;
}
.tab-title {
@@ -191,28 +324,95 @@ const onTabDragEnd = () => {
margin-right: 8px;
}
.tab-item.active {
/* 垂直标签项样式 */
.tab-item-vertical {
width: 26px;
height: 60px;
margin-top: 1px;
background: linear-gradient(to right, var(--vs-blue-top), var(--vs-blue-bottom));
border-radius: 3px 0 0 3px;
cursor: pointer;
white-space: nowrap;
color: white;
font-size: 12px;
user-select: none;
display: flex;
align-items: center;
justify-content: center;
}
/* 右侧位置的特殊样式 */
.tab-page-right .tab-item-vertical {
border-radius: 0 3px 3px 0;
background: linear-gradient(to left, var(--vs-blue-top), var(--vs-blue-bottom));
}
.tab-title-vertical {
text-align: center;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin: 0 4px;
writing-mode: vertical-rl;
text-orientation: mixed;
}
/* 活动标签样式 */
.tab-item.active,
.tab-item-vertical.active {
background: #F5CC84;
border: 1px solid #c7d2ea;
border-bottom-color: #F5CC84;
border-top-width: 2px;
border-top-color: #0078d7;
color: #000000;
font-weight: 500;
cursor: move;
}
.tab-item:hover {
/* 顶部位置活动标签 */
.tab-page-top .tab-item.active {
border-bottom-color: #F5CC84;
border-top-width: 2px;
border-top-color: #0078d7;
}
/* 底部位置活动标签 */
.tab-page-bottom .tab-item.active {
border-top-color: #F5CC84;
border-bottom-width: 2px;
border-bottom-color: #0078d7;
}
/* 左侧位置活动标签 */
.tab-page-left .tab-item-vertical.active {
border-right-color: #F5CC84;
border-left-width: 2px;
border-left-color: #0078d7;
}
/* 右侧位置活动标签 */
.tab-page-right .tab-item-vertical.active {
border-left-color: #F5CC84;
border-right-width: 2px;
border-right-color: #0078d7;
}
/* 悬停样式 */
.tab-item:hover,
.tab-item-vertical:hover {
background: linear-gradient(to bottom, #5a7db8, #4667a4);
}
.tab-item.active:hover {
.tab-page-right .tab-item-vertical:hover {
background: linear-gradient(to left, #5a7db8, #4667a4);
}
.tab-item.active:hover,
.tab-item-vertical.active:hover {
background: #F5CC84;
color: #000000;
}
/* 按钮样式 */
.button-icon {
background: transparent;
border: none;
@@ -223,28 +423,31 @@ const onTabDragEnd = () => {
}
/* 确保在活动标签页中的按钮样式正确 */
.tab-item.active .button-icon {
.tab-item.active .button-icon,
.tab-item-vertical.active .button-icon {
opacity: 0.6;
}
.tab-item.active .button-icon:hover {
.tab-item.active .button-icon:hover,
.tab-item-vertical.active .button-icon:hover {
opacity: 1;
background: #f3f4f6;
}
/* 确保在非活动标签页中的按钮样式正确 */
.tab-item:not(.active) .button-icon svg line {
.tab-item:not(.active) .button-icon svg line,
.tab-item-vertical:not(.active) .button-icon svg line {
stroke: #e6efff;
}
.tab-item:not(.active) .button-icon:hover svg line {
.tab-item:not(.active) .button-icon:hover svg line,
.tab-item-vertical:not(.active) .button-icon:hover svg line {
stroke: white;
}
.tab-placeholder {
background: transparent;
border: transparent;
border-bottom-color: #c7d2ea;
}
/* 内容区域样式 */
@@ -280,20 +483,35 @@ const onTabDragEnd = () => {
font-size: 14px;
}
/* 滚动条样式 */
.tab-header::-webkit-scrollbar {
/* 滚动条样式 - 水平标签栏 */
.tab-header-horizontal::-webkit-scrollbar {
height: 6px;
}
.tab-header::-webkit-scrollbar-track {
.tab-header-horizontal::-webkit-scrollbar-track {
background: #f0f0f0;
}
.tab-header::-webkit-scrollbar-thumb {
.tab-header-horizontal::-webkit-scrollbar-thumb {
background: #c7d2ea;
border-radius: 3px;
}
/* 滚动条样式 - 垂直标签栏 */
.tab-header-vertical::-webkit-scrollbar {
width: 6px;
}
.tab-header-vertical::-webkit-scrollbar-track {
background: #f0f0f0;
}
.tab-header-vertical::-webkit-scrollbar-thumb {
background: #c7d2ea;
border-radius: 3px;
}
/* 内容区域滚动条 */
.tab-panel::-webkit-scrollbar {
width: 12px;
height: 12px;
@@ -301,7 +519,6 @@ const onTabDragEnd = () => {
.tab-panel::-webkit-scrollbar-track {
background: #f5f7fb;
border-left: 1px solid #c7d2ea;
}
.tab-panel::-webkit-scrollbar-thumb {

View File

@@ -18,6 +18,7 @@
4. 当TabPage的页标签未被选中时背景颜色与Area的背景颜色相同文字颜色为#FFFFFF
5. 当TabPage的页标签被选中时激活的页标签不显示关闭按钮。
6. 当TabPage的页标签被选中时鼠标显示为移动否则显示为手型。
7. TabPage的页标签可以定义在上、右、下、左四个边缘显示通过对外提供的属性设置。
### Panel
1. 填充满父容器。
@@ -34,3 +35,5 @@
1. 当一个Panel被拖动时显示停靠指示器。
2. 当拖动Panel到指示器时显示停靠区。
3. 当主区域内没有其他Area时隐藏外部边缘指示器、中心区域容器只显示中心指示器。
4. 当将TabPage的页标签拖动到中心指示器时
4.1. 如果TabPage只有这一个标签页则将TabPage停靠到中心区域。