Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/TabPage.vue

566 lines
15 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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-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 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">
<!-- 渲染当前激活的Panel内容 -->
<template v-if="activeTabIndex >= 0 && activeTabIndex < panels.length">
<div
v-for="(panel, index) in panels"
:key="panel.id"
v-show="index === activeTabIndex"
class="tab-panel"
:class="{ active: index === activeTabIndex }"
>
<!-- 使用Panel组件渲染面板内容 -->
<Panel
:id="panel.id"
:title="panel.title"
:x="panel.x || 0"
:y="panel.y || 0"
:width="panel.width || 300"
:height="panel.height || 200"
:collapsed="panel.collapsed || false"
:toolbar-expanded="panel.toolbarExpanded || false"
:maximized="panel.maximized || false"
:content="panel.content"
@toggle-collapse="$emit('toggleCollapse', $event)"
@maximize="$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)"
/>
</div>
</template>
<!-- 空状态提示 -->
<div v-else class="tab-empty">
<span>没有可显示的内容</span>
</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>
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted } from 'vue'
import Panel from './Panel.vue'
const props = defineProps({
id: { type: String, required: true },
title: { type: String, default: '标签页' },
// 从父组件传入的面板数组
panels: {
type: Array,
default: () => []
},
// 是否显示页标签栏
showTabs: {
type: Boolean,
default: true
},
// 标签页位置top(顶部), right(右侧), bottom(底部), left(左侧)
tabPosition: {
type: String,
default: 'top',
validator: (value) => ['top', 'right', 'bottom', 'left'].includes(value)
}
})
const emit = defineEmits(['tabChange', 'tabClose', 'tabAdd', 'tabDragStart', 'tabDragMove', 'tabDragEnd'])
// 当前激活的标签页索引
const activeTabIndex = ref(-1)
// 拖拽相关状态
let isDragging = false
let dragIndex = -1
// 设置激活的标签页
const setActiveTab = (index) => {
if (index >= 0 && index < props.panels.length) {
activeTabIndex.value = index
emit('tabChange', { index, tab: props.panels[index] })
}
}
// 组件挂载后,如果有面板且没有激活的标签,默认激活第一个
onMounted(() => {
if (props.panels && props.panels.length > 0 && activeTabIndex.value === -1) {
setActiveTab(0)
}
})
// 关闭标签页
const closeTab = (tabId) => {
emit('tabClose', { id: tabId })
}
// 标签拖拽开始
const onTabDragStart = (index, event) => {
// 只有当点击的是标题文本区域(不是关闭按钮)时才触发拖拽
if (!event.target.closest('.button-icon') && !event.target.closest('button')) {
isDragging = true
dragIndex = index
// 传递标签页索引和鼠标位置
emit('tabDragStart', {
clientX: event.clientX,
clientY: event.clientY,
tabIndex: index,
tabId: props.panels[index].id
})
// 防止文本选择和默认行为
event.preventDefault()
event.stopPropagation()
// 将鼠标移动和释放事件绑定到document确保拖拽的连续性
document.addEventListener('mousemove', onTabDragMove)
document.addEventListener('mouseup', onTabDragEnd)
document.addEventListener('mouseleave', onTabDragEnd)
}
}
// 标签拖拽移动
const onTabDragMove = (event) => {
if (isDragging) {
// 防止文本选择和默认行为
event.preventDefault()
event.stopPropagation()
emit('tabDragMove', {
clientX: event.clientX,
clientY: event.clientY,
tabIndex: dragIndex
})
}
}
// 标签拖拽结束
const onTabDragEnd = () => {
if (isDragging) {
isDragging = false
emit('tabDragEnd', { tabIndex: dragIndex })
dragIndex = -1
// 拖拽结束后移除事件监听器
document.removeEventListener('mousemove', onTabDragMove)
document.removeEventListener('mouseup', onTabDragEnd)
document.removeEventListener('mouseleave', onTabDragEnd)
}
}
</script>
<style scoped>
/* 主容器样式 */
.tab-page {
display: flex;
width: 100%;
height: 100%;
background: #5D6B99;
border: 0px solid #c7d2ea;
border-radius: 0;
overflow: hidden;
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;
}
/* 水平标签栏样式 */
.tab-header-horizontal {
display: flex;
flex-wrap: nowrap;
height: 28px;
background: transparent;
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-radius: 3px 3px 0 0;
cursor: pointer;
white-space: nowrap;
color: white;
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 {
flex: 1;
text-align: left;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
/* 垂直标签项样式 */
.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;
color: #000000;
font-weight: 500;
cursor: move;
}
/* 顶部位置活动标签 */
.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-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;
cursor: pointer;
display: flex;
align-items: center;
justify-content: center;
}
/* 确保在活动标签页中的按钮样式正确 */
.tab-item.active .button-icon,
.tab-item-vertical.active .button-icon {
opacity: 0.6;
}
.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-vertical:not(.active) .button-icon svg line {
stroke: #e6efff;
}
.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;
}
/* 内容区域样式 */
.tab-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-panel {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
overflow: auto;
background: #ffffff;
display: none;
}
.tab-panel.active {
display: block;
}
.tab-empty {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 100%;
color: #999;
font-size: 14px;
}
/* 滚动条样式 - 水平标签栏 */
.tab-header-horizontal::-webkit-scrollbar {
height: 6px;
}
.tab-header-horizontal::-webkit-scrollbar-track {
background: #f0f0f0;
}
.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;
}
.tab-panel::-webkit-scrollbar-track {
background: #f5f7fb;
}
.tab-panel::-webkit-scrollbar-thumb {
background: linear-gradient(to bottom, #d0d6ea, #c0c7e0);
border: 1px solid #b0b6d6;
border-radius: 6px;
}
.tab-panel::-webkit-scrollbar-thumb:hover {
background: linear-gradient(to bottom, #c1c7e2, #b2b8d9);
}
</style>