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" :id="tabPage.id"
:title="tabPage.title" :title="tabPage.title"
:panels="tabPage.panels" :panels="tabPage.panels"
:tabPosition="'bottom'"
@tabDragStart="onTabDragStart(area.id, $event)" @tabDragStart="onTabDragStart(area.id, $event)"
@tabDragMove="onTabDragMove(area.id, $event)" @tabDragMove="onTabDragMove(area.id, $event)"
@tabDragEnd="onTabDragEnd" @tabDragEnd="onTabDragEnd"

View File

@@ -1,7 +1,7 @@
<template> <template>
<div class="tab-page"> <div class="tab-page" :class="[`tab-page-${tabPosition}`]">
<!-- 使用panels数组渲染标签栏 --> <!-- 顶部标签栏 -->
<div v-if="showTabs && panels && panels.length > 0" class="tab-header"> <div v-if="tabPosition === 'top' && showTabs && 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"
@@ -27,11 +27,96 @@
</div> </div>
<div class="tab-placeholder"></div> <div class="tab-placeholder"></div>
</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页内容区域 --> <!-- Tab页内容区域 -->
<div class="tab-content"> <div class="tab-content">
<!-- 渲染slot内容Panel组件 --> <!-- 渲染slot内容Panel组件 -->
<slot></slot> <slot></slot>
</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-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> </div>
</template> </template>
@@ -50,6 +135,12 @@ const props = defineProps({
showTabs: { showTabs: {
type: Boolean, type: Boolean,
default: true 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> </script>
<style scoped> <style scoped>
/* 主容器样式 */
.tab-page { .tab-page {
display: flex; display: flex;
flex-direction: column;
width: 100%; width: 100%;
height: 100%; height: 100%;
background: #5D6B99; background: #5D6B99;
@@ -150,28 +241,63 @@ const onTabDragEnd = () => {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif; 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 { :root {
--vs-blue-top: #4f72b3; --vs-blue-top: #4f72b3;
--vs-blue-bottom: #3c5a99; --vs-blue-bottom: #3c5a99;
} }
/* 标签栏样式 - 模仿Windows Forms */ /* 水平标签栏样式 */
.tab-header { .tab-header-horizontal {
display: flex; display: flex;
flex-wrap: nowrap; flex-wrap: nowrap;
height: 28px; height: 28px;
background: transparent; background: transparent;
border-bottom: none;
overflow-x: auto; overflow-x: auto;
overflow-y: hidden; overflow-y: hidden;
padding-top: 2px; 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 { .tab-item {
height: 26px; height: 26px;
margin-left: 1px; margin-left: 1px;
background: linear-gradient(to bottom, var(--vs-blue-top), var(--vs-blue-bottom)); background: linear-gradient(to bottom, var(--vs-blue-top), var(--vs-blue-bottom));
border-bottom-color: #c7d2ea;
border-radius: 3px 3px 0 0; border-radius: 3px 3px 0 0;
cursor: pointer; cursor: pointer;
white-space: nowrap; white-space: nowrap;
@@ -179,6 +305,13 @@ const onTabDragEnd = () => {
font-size: 12px; font-size: 12px;
user-select: none; user-select: none;
min-width: 60px; min-width: 60px;
display: flex;
align-items: center;
}
/* 底部位置的特殊样式 */
.tab-page-bottom .tab-item {
border-radius: 0 0 3px 3px;
} }
.tab-title { .tab-title {
@@ -191,28 +324,95 @@ const onTabDragEnd = () => {
margin-right: 8px; 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; background: #F5CC84;
border: 1px solid #c7d2ea; border: 1px solid #c7d2ea;
border-bottom-color: #F5CC84;
border-top-width: 2px;
border-top-color: #0078d7;
color: #000000; color: #000000;
font-weight: 500; font-weight: 500;
cursor: move; 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); 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; background: #F5CC84;
color: #000000; color: #000000;
} }
/* 按钮样式 */
.button-icon { .button-icon {
background: transparent; background: transparent;
border: none; 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; opacity: 0.6;
} }
.tab-item.active .button-icon:hover { .tab-item.active .button-icon:hover,
.tab-item-vertical.active .button-icon:hover {
opacity: 1; opacity: 1;
background: #f3f4f6; 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; 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; stroke: white;
} }
.tab-placeholder { .tab-placeholder {
background: transparent; background: transparent;
border: transparent; border: transparent;
border-bottom-color: #c7d2ea;
} }
/* 内容区域样式 */ /* 内容区域样式 */
@@ -280,20 +483,35 @@ const onTabDragEnd = () => {
font-size: 14px; font-size: 14px;
} }
/* 滚动条样式 */ /* 滚动条样式 - 水平标签栏 */
.tab-header::-webkit-scrollbar { .tab-header-horizontal::-webkit-scrollbar {
height: 6px; height: 6px;
} }
.tab-header::-webkit-scrollbar-track { .tab-header-horizontal::-webkit-scrollbar-track {
background: #f0f0f0; background: #f0f0f0;
} }
.tab-header::-webkit-scrollbar-thumb { .tab-header-horizontal::-webkit-scrollbar-thumb {
background: #c7d2ea; background: #c7d2ea;
border-radius: 3px; 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 { .tab-panel::-webkit-scrollbar {
width: 12px; width: 12px;
height: 12px; height: 12px;
@@ -301,7 +519,6 @@ const onTabDragEnd = () => {
.tab-panel::-webkit-scrollbar-track { .tab-panel::-webkit-scrollbar-track {
background: #f5f7fb; background: #f5f7fb;
border-left: 1px solid #c7d2ea;
} }
.tab-panel::-webkit-scrollbar-thumb { .tab-panel::-webkit-scrollbar-thumb {

View File

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