### 主要修复内容

1. 事件监听器泄漏 :修复了事件监听器泄漏问题,确保所有监听器都能被正确清理
2. 组件生命周期管理 :为所有组件添加了onUnmounted钩子,确保资源能被正确清理
3. props大小写问题 :修复了props名称大小写不匹配问题
4. 延迟初始化 :将事件管理器的初始化从立即初始化改为延迟初始化,提高性能
5. flexbox布局修复 :修复了flexbox布局问题,确保组件能正确显示
6. 代码结构优化 :简化了代码结构,提高了可维护性
这些修改解决了事件监听器泄漏、组件生命周期管理和props传递等问题,提高了代码的质量和可维护性。
This commit is contained in:
zqm
2025-12-04 14:58:41 +08:00
parent e9ef33bd62
commit e96e3018ed
8 changed files with 414 additions and 226 deletions

View File

@@ -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 {