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

315 lines
7.0 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">
<!-- 使用panels数组渲染标签栏 -->
<div v-if="showTabs && panels && panels.length > 0" class="tab-header">
<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>
<!-- Tab页内容区域 -->
<div class="tab-content">
<!-- 渲染slot内容Panel组件 -->
<slot></slot>
</div>
</div>
</template>
<script setup>
import { defineProps, defineEmits, ref, onMounted } from 'vue'
const props = defineProps({
id: { type: String, required: true },
title: { type: String, default: '标签页' },
// 从父组件传入的面板数组
panels: {
type: Array,
default: () => []
},
// 是否显示页标签栏
showTabs: {
type: Boolean,
default: true
}
})
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;
flex-direction: column;
width: 100%;
height: 100%;
background: #5D6B99;
border: 0px solid #c7d2ea;
border-radius: 0;
overflow: hidden;
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
}
:root {
--vs-blue-top: #4f72b3;
--vs-blue-bottom: #3c5a99;
}
/* 标签栏样式 - 模仿Windows Forms */
.tab-header {
display: flex;
flex-wrap: nowrap;
height: 28px;
background: transparent;
border-bottom: none;
overflow-x: auto;
overflow-y: hidden;
padding-top: 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;
color: white;
font-size: 12px;
user-select: none;
min-width: 60px;
}
.tab-title {
flex: 1;
text-align: left;
min-width: 0;
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
margin-right: 8px;
}
.tab-item.active {
background: #F5CC84;
border: 1px solid #c7d2ea;
border-bottom-color: #F5CC84;
border-top-width: 2px;
border-top-color: #0078d7;
color: #000000;
font-weight: 500;
}
.tab-item:hover {
background: linear-gradient(to bottom, #5a7db8, #4667a4);
}
.tab-item.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 {
opacity: 0.6;
}
.tab-item.active .button-icon:hover {
opacity: 1;
background: #f3f4f6;
}
/* 确保在非活动标签页中的按钮样式正确 */
.tab-item:not(.active) .button-icon svg line {
stroke: #e6efff;
}
.tab-item:not(.active) .button-icon:hover svg line {
stroke: white;
}
.tab-placeholder {
background: transparent;
border: transparent;
border-bottom-color: #c7d2ea;
}
/* 内容区域样式 */
.tab-content {
flex: 1;
position: relative;
overflow: hidden;
}
.tab-panel {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
padding: 12px;
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::-webkit-scrollbar {
height: 6px;
}
.tab-header::-webkit-scrollbar-track {
background: #f0f0f0;
}
.tab-header::-webkit-scrollbar-thumb {
background: #c7d2ea;
border-radius: 3px;
}
.tab-panel::-webkit-scrollbar {
width: 12px;
height: 12px;
}
.tab-panel::-webkit-scrollbar-track {
background: #f5f7fb;
border-left: 1px solid #c7d2ea;
}
.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>