7. TabPage的页标签可以定义在上、右、下、左四个边缘显示,通过对外提供的属性设置。
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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. 填充满父容器。
|
||||||
@@ -34,3 +35,5 @@
|
|||||||
1. 当一个Panel被拖动时,显示停靠指示器。
|
1. 当一个Panel被拖动时,显示停靠指示器。
|
||||||
2. 当拖动Panel到指示器时,显示停靠区。
|
2. 当拖动Panel到指示器时,显示停靠区。
|
||||||
3. 当主区域内没有其他Area时,隐藏外部边缘指示器、中心区域容器,只显示中心指示器。
|
3. 当主区域内没有其他Area时,隐藏外部边缘指示器、中心区域容器,只显示中心指示器。
|
||||||
|
4. 当将TabPage的页标签拖动到中心指示器时
|
||||||
|
4.1. 如果TabPage只有这一个标签页,则将TabPage停靠到中心区域。
|
||||||
Reference in New Issue
Block a user