### 主要修复内容
1. 事件监听器泄漏 :修复了事件监听器泄漏问题,确保所有监听器都能被正确清理 2. 组件生命周期管理 :为所有组件添加了onUnmounted钩子,确保资源能被正确清理 3. props大小写问题 :修复了props名称大小写不匹配问题 4. 延迟初始化 :将事件管理器的初始化从立即初始化改为延迟初始化,提高性能 5. flexbox布局修复 :修复了flexbox布局问题,确保组件能正确显示 6. 代码结构优化 :简化了代码结构,提高了可维护性 这些修改解决了事件监听器泄漏、组件生命周期管理和props传递等问题,提高了代码的质量和可维护性。
This commit is contained in:
@@ -1,21 +1,21 @@
|
||||
<template>
|
||||
<div class="tab-page" :class="[`tab-page-${tabPosition}`]" style="width: 100%; height: 100%;">
|
||||
<!-- 顶部标签栏 -->
|
||||
<div v-if="tabPosition === 'top' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal">
|
||||
<div v-if="tabPosition === 'top' && shouldShowTabs" class="tab-header tab-header-horizontal">
|
||||
<div
|
||||
v-for="(panel, index) in panels"
|
||||
:key="panel.id"
|
||||
v-for="(item, index) in slotItems"
|
||||
:key="index"
|
||||
: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>
|
||||
<span class="tab-title">{{ item?.title || '未命名' }}</span>
|
||||
<!-- 当标签页未被激活时显示关闭按钮 -->
|
||||
<button
|
||||
v-if="activeTabIndex !== index"
|
||||
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
|
||||
@click.stop="closeTab(panel.id)"
|
||||
@click.stop="closeTab(item?.id)"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
||||
@@ -29,21 +29,21 @@
|
||||
</div>
|
||||
|
||||
<!-- 左侧标签栏 -->
|
||||
<div v-if="tabPosition === 'left' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-left">
|
||||
<div v-if="tabPosition === 'left' && shouldShowTabs" class="tab-header tab-header-vertical tab-header-left">
|
||||
<div
|
||||
v-for="(panel, index) in panels"
|
||||
:key="panel.id"
|
||||
v-for="(item, index) in slotItems"
|
||||
:key="index"
|
||||
: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>
|
||||
<span class="tab-title-vertical">{{ item?.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)"
|
||||
@click.stop="closeTab(item?.id)"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
||||
@@ -57,21 +57,21 @@
|
||||
</div>
|
||||
|
||||
<!-- 右侧标签栏 -->
|
||||
<div v-if="tabPosition === 'right' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-vertical tab-header-right">
|
||||
<div v-if="tabPosition === 'right' && shouldShowTabs" class="tab-header tab-header-vertical tab-header-right">
|
||||
<div
|
||||
v-for="(panel, index) in panels"
|
||||
:key="panel.id"
|
||||
v-for="(item, index) in slotItems"
|
||||
:key="index"
|
||||
: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>
|
||||
<span class="tab-title-vertical">{{ item?.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)"
|
||||
@click.stop="closeTab(item?.id)"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
||||
@@ -87,59 +87,30 @@
|
||||
|
||||
<!-- 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="(event) => { /* console.log('🔸 TabPage转发最大化事件:', event); */ $emit('maximize', event); }"
|
||||
@close="$emit('close', $event)"
|
||||
@toggle-toolbar="$emit('toggleToolbar', $event)"
|
||||
@dragStart="$emit('dragStart', $event)"
|
||||
@dragMove="$emit('dragMove', $event)"
|
||||
@dragEnd="$emit('dragEnd', $event)"
|
||||
/>
|
||||
</div>
|
||||
</template>
|
||||
<!-- 直接渲染插槽内容 -->
|
||||
<slot></slot>
|
||||
<!-- 空状态提示 -->
|
||||
<div v-else class="tab-empty">
|
||||
<div v-if="slotItems.length === 0" class="tab-empty">
|
||||
<span>没有可显示的内容</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<!-- 底部标签栏 - 移动到最后确保在底部显示 -->
|
||||
<div v-if="tabPosition === 'bottom' && shouldShowTabs && panels && panels.length > 0" class="tab-header tab-header-horizontal tab-header-bottom">
|
||||
<div v-if="tabPosition === 'bottom' && shouldShowTabs" class="tab-header tab-header-horizontal tab-header-bottom">
|
||||
<div
|
||||
v-for="(panel, index) in panels"
|
||||
:key="panel.id"
|
||||
v-for="(item, index) in slotItems"
|
||||
:key="index"
|
||||
: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>
|
||||
<span class="tab-title">{{ item?.title || '未命名' }}</span>
|
||||
<!-- 当标签页未被激活时显示关闭按钮 -->
|
||||
<button
|
||||
v-if="activeTabIndex !== index"
|
||||
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
|
||||
@click.stop="closeTab(panel.id)"
|
||||
@click.stop="closeTab(item?.id)"
|
||||
aria-label="关闭"
|
||||
>
|
||||
<svg width="11" height="11" viewBox="0 0 11 11" aria-hidden="true">
|
||||
@@ -155,17 +126,13 @@
|
||||
</template>
|
||||
|
||||
<script setup>
|
||||
import { defineProps, defineEmits, ref, onMounted, computed } from 'vue'
|
||||
import Panel from './Panel.vue'
|
||||
import { defineProps, defineEmits, ref, onMounted, onUnmounted, computed, useSlots } from 'vue'
|
||||
|
||||
const slots = useSlots()
|
||||
|
||||
const props = defineProps({
|
||||
id: { type: String, required: true },
|
||||
title: { type: String, default: '标签页' },
|
||||
// 从父组件传入的面板数组
|
||||
panels: {
|
||||
type: Array,
|
||||
default: () => []
|
||||
},
|
||||
// 是否显示页标签栏
|
||||
showTabs: {
|
||||
type: Boolean,
|
||||
@@ -188,31 +155,32 @@ const activeTabIndex = ref(-1)
|
||||
let isDragging = false
|
||||
let dragIndex = -1
|
||||
|
||||
// 计算属性:获取插槽项的props
|
||||
const slotItems = computed(() => {
|
||||
if (!slots.default) return []
|
||||
const slotChildren = slots.default()
|
||||
return slotChildren.map(child => child?.props || {})
|
||||
})
|
||||
|
||||
// 计算属性:控制标签栏的显示
|
||||
const shouldShowTabs = computed(() => {
|
||||
// 未来可以优化:当只有一个Panel且不是浮动窗口时隐藏标签栏
|
||||
const result = props.showTabs && props.panels && props.panels.length > 0
|
||||
// console.log(`[TabPage ${props.id}] shouldShowTabs:`, {
|
||||
// showTabs: result,
|
||||
// panelsLength: props.panels?.length || 0,
|
||||
// tabPosition: props.tabPosition,
|
||||
// shouldShow: result,
|
||||
// panels: props.panels
|
||||
// })
|
||||
return result
|
||||
// 显示标签栏的条件:showTabs为true,且有子组件
|
||||
return props.showTabs && slots.default && slots.default().length > 0
|
||||
})
|
||||
|
||||
// 设置激活的标签页
|
||||
const setActiveTab = (index) => {
|
||||
if (index >= 0 && index < props.panels.length) {
|
||||
const slotChildren = slots.default ? slots.default() : []
|
||||
if (index >= 0 && index < slotChildren.length) {
|
||||
activeTabIndex.value = index
|
||||
emit('tabChange', { index, tab: props.panels[index] })
|
||||
emit('tabChange', { index, tab: slotChildren[index] })
|
||||
}
|
||||
}
|
||||
|
||||
// 组件挂载后,如果有面板且没有激活的标签,默认激活第一个
|
||||
// 组件挂载后,如果有子组件且没有激活的标签,默认激活第一个
|
||||
onMounted(() => {
|
||||
if (props.panels && props.panels.length > 0 && activeTabIndex.value === -1) {
|
||||
const slotChildren = slots.default ? slots.default() : []
|
||||
if (slotChildren.length > 0 && activeTabIndex.value === -1) {
|
||||
setActiveTab(0)
|
||||
}
|
||||
})
|
||||
@@ -234,7 +202,7 @@ const onTabDragStart = (index, event) => {
|
||||
clientX: event.clientX,
|
||||
clientY: event.clientY,
|
||||
tabIndex: index,
|
||||
tabId: props.panels[index].id
|
||||
tabId: $slots.default()[index]?.props?.id
|
||||
})
|
||||
|
||||
// 防止文本选择和默认行为
|
||||
@@ -275,6 +243,14 @@ const onTabDragEnd = () => {
|
||||
document.removeEventListener('mouseleave', onTabDragEnd)
|
||||
}
|
||||
}
|
||||
|
||||
// 组件卸载时清理全局事件监听器
|
||||
onUnmounted(() => {
|
||||
// 确保清理所有可能存在的全局事件监听器
|
||||
document.removeEventListener('mousemove', onTabDragMove)
|
||||
document.removeEventListener('mouseup', onTabDragEnd)
|
||||
document.removeEventListener('mouseleave', onTabDragEnd)
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped>
|
||||
@@ -295,6 +271,12 @@ const onTabDragEnd = () => {
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.tab-page-top .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
/* 底部位置:内容区 -> 工具栏 -> 标签栏 */
|
||||
.tab-page-bottom {
|
||||
flex-direction: column-reverse;
|
||||
@@ -305,11 +287,23 @@ const onTabDragEnd = () => {
|
||||
flex-direction: row;
|
||||
}
|
||||
|
||||
.tab-page-left .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
/* 右侧位置:标签栏 -> 工具栏 -> 内容区(垂直排列) */
|
||||
.tab-page-right {
|
||||
flex-direction: row-reverse;
|
||||
}
|
||||
|
||||
.tab-page-right .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
:root {
|
||||
--vs-blue-top: #4f72b3;
|
||||
--vs-blue-bottom: #3c5a99;
|
||||
@@ -536,6 +530,30 @@ const onTabDragEnd = () => {
|
||||
flex: 1;
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
min-height: 0; /* 重要:允许flex子项收缩 */
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
}
|
||||
|
||||
/* 顶部位置:标签栏 -> 内容区 */
|
||||
.tab-page-top .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0; /* 重要:允许flex子项收缩 */
|
||||
}
|
||||
|
||||
/* 左侧位置:标签栏 -> 内容区 */
|
||||
.tab-page-left .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0; /* 重要:允许flex子项收缩 */
|
||||
}
|
||||
|
||||
/* 右侧位置:标签栏 -> 内容区 */
|
||||
.tab-page-right .tab-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
min-height: 0; /* 重要:允许flex子项收缩 */
|
||||
}
|
||||
|
||||
.tab-panel {
|
||||
|
||||
Reference in New Issue
Block a user