实现TabPage标签拖拽功能及样式优化:1. 激活标签背景色改为#F5CC84 2. 激活标签不显示关闭按钮 3. 修复TabPage标签拖拽功能

This commit is contained in:
zqm
2025-11-06 14:57:30 +08:00
parent 6d2de5e6c6
commit 9fc607e18f
4 changed files with 166 additions and 22 deletions

View File

@@ -2321,6 +2321,7 @@
"integrity": "sha512-t+YPtOQHpGW1QWsh1CHQ5cPIr9lbbGZLZnbihP/D/qZj/yuV68m8qarcV17nvkOX81BCrvzAlq2klCQFZghyTg==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"chokidar": "^4.0.0",
"immutable": "^5.0.2",
@@ -2448,6 +2449,7 @@
"integrity": "sha512-jl1vZzPDinLr9eUt3J/t7V6FgNEw9QjvBPdysz9KfQDD41fQrC2Y4vKQdiaUpFT4bXlb1RHhLpp8wtm6M5TgSw==",
"devOptional": true,
"license": "Apache-2.0",
"peer": true,
"bin": {
"tsc": "bin/tsc",
"tsserver": "bin/tsserver"
@@ -2462,6 +2464,7 @@
"integrity": "sha512-o5a9xKjbtuhY6Bi5S3+HvbRERmouabWbyUcpXXUA1u+GNUKoROi9byOJ8M0nHbHYHkYICiMlqxkg1KkYmm25Sw==",
"dev": true,
"license": "MIT",
"peer": true,
"dependencies": {
"esbuild": "^0.21.3",
"postcss": "^8.4.43",
@@ -2521,6 +2524,7 @@
"resolved": "http://47.111.181.23:8081/repository/npm-public/vue/-/vue-3.5.22.tgz",
"integrity": "sha512-toaZjQ3a/G/mYaLSbV+QsQhIdMo9x5rrqIpYRObsJ6T/J+RyCSFwN2LHNVH9v8uIcljDNa3QzPVdv3Y6b9hAJQ==",
"license": "MIT",
"peer": true,
"dependencies": {
"@vue/compiler-dom": "3.5.22",
"@vue/compiler-sfc": "3.5.22",

View File

@@ -36,6 +36,9 @@
:id="tabPage.id"
:title="tabPage.title"
:panels="tabPage.panels"
@tabDragStart="onTabDragStart(area.id, $event)"
@tabDragMove="onTabDragMove(area.id, $event)"
@tabDragEnd="onTabDragEnd"
>
<!-- 在TabPage内渲染其包含的Panels -->
<Panel
@@ -86,6 +89,14 @@ const panelDragState = ref({
startAreaPos: { x: 0, y: 0 }
})
// TabPage拖拽相关状态
const tabDragState = ref({
isDragging: false,
currentAreaId: null,
startClientPos: { x: 0, y: 0 },
startAreaPos: { x: 0, y: 0 }
})
// 添加新的浮动面板
const addFloatingPanel = () => {
// 获取父容器尺寸以计算居中位置
@@ -297,6 +308,64 @@ const onPanelDragEnd = () => {
panelDragState.value.currentAreaId = null
}
// TabPage拖拽开始
const onTabDragStart = (areaId, event) => {
const area = floatingAreas.value.find(a => a.id === areaId)
// 只有当Area中只有一个TabPage时才允许通过TabPage的页标签移动Area
if (area && area.tabPages && area.tabPages.length === 1) {
tabDragState.value.isDragging = true
tabDragState.value.currentAreaId = areaId
tabDragState.value.startClientPos = {
x: event.clientX,
y: event.clientY
}
tabDragState.value.startAreaPos = {
x: area.x,
y: area.y
}
console.log('TabPage拖拽开始移动Area:', areaId)
}
}
// TabPage拖拽移动
const onTabDragMove = (areaId, event) => {
if (tabDragState.value.isDragging && tabDragState.value.currentAreaId === areaId) {
const area = floatingAreas.value.find(a => a.id === areaId)
if (area) {
// 计算移动距离
const deltaX = event.clientX - tabDragState.value.startClientPos.x
const deltaY = event.clientY - tabDragState.value.startClientPos.y
// 计算新位置
let newLeft = tabDragState.value.startAreaPos.x + deltaX
let newTop = tabDragState.value.startAreaPos.y + deltaY
// 确保不超出父容器边界
if (dockLayoutRef.value) {
const parentRect = dockLayoutRef.value.getBoundingClientRect()
// 严格边界检查
newLeft = Math.max(0, Math.min(newLeft, parentRect.width - area.width))
newTop = Math.max(0, Math.min(newTop, parentRect.height - area.height))
}
// 更新位置
area.x = newLeft
area.y = newTop
// 调试信息
console.log('TabPage拖拽移动Area新位置:', { x: newLeft, y: newTop })
}
}
}
// TabPage拖拽结束
const onTabDragEnd = () => {
console.log('TabPage拖拽结束')
tabDragState.value.isDragging = false
tabDragState.value.currentAreaId = null
}
// 监听floatingAreas变化确保当Area最大化时Panel也会自动最大化
watch(floatingAreas, (newAreas) => {
newAreas.forEach(area => {

View File

@@ -7,10 +7,13 @@
: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="关闭"
@@ -33,7 +36,7 @@
</template>
<script setup>
import { defineProps, defineEmits, ref } from 'vue'
import { defineProps, defineEmits, ref, onMounted } from 'vue'
const props = defineProps({
id: { type: String, required: true },
@@ -50,23 +53,88 @@ const props = defineProps({
}
})
const emit = defineEmits(['tabChange', 'tabClose', 'tabAdd'])
const emit = defineEmits(['tabChange', 'tabClose', 'tabAdd', 'tabDragStart', 'tabDragMove', 'tabDragEnd'])
// 当前激活的标签页索引
const activeTabIndex = ref(props.activeIndex)
const activeTabIndex = ref(-1)
// 拖拽相关状态
let isDragging = false
let dragIndex = -1
// 设置激活的标签页
const setActiveTab = (index) => {
if (index >= 0 && index < props.tabs.length) {
if (index >= 0 && index < props.panels.length) {
activeTabIndex.value = index
emit('tabChange', { index, tab: props.tabs[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>
@@ -124,12 +192,12 @@ const closeTab = (tabId) => {
}
.tab-item.active {
background: #ffffff;
background: #F5CC84;
border: 1px solid #c7d2ea;
border-bottom-color: #ffffff;
border-bottom-color: #F5CC84;
border-top-width: 2px;
border-top-color: #0078d7;
color: #333;
color: #000000;
font-weight: 500;
}
@@ -138,8 +206,8 @@ const closeTab = (tabId) => {
}
.tab-item.active:hover {
background: #ffffff;
color: #333;
background: #F5CC84;
color: #000000;
}

View File

@@ -1,19 +1,22 @@
### Area
1. 初始添加时默认宽300px高250px。位置相对父容器水平居中垂直居中。
2. 最大化时,填充满父容器。
3. 还原时,恢复到最大化前的位置和大小。
4. 关闭时从父容器中移除。同时将内容区的Panel移除。
5. 拖拽时,允许在父容器内移动,不允许超出父容器边界。
6. 允许拖动边框改变Area的大小。
7. 当内容区只包含一个Panel时显示Area的标题栏。
8. 当内容区只包含一个Panel时拖动Panel标题栏就相当于拖动Area。
9. 当内容区只包含一个Panel时拖动TabPage的页标签就相当于拖动Area
10. 当内容区只包含一个Panel时单击Panel的最大化按钮就相当于单击Area的最大化按钮同时最大化Panel。
11. 当内容区只包含一个Panel时最大化Area时Panel也会最大化。
### Area
1. 初始添加时默认宽300px高250px。位置相对父容器水平居中垂直居中。
2. 最大化时,填充满父容器。
3. 还原时,恢复到最大化前的位置和大小。
4. 关闭时从父容器中移除。同时将内容区的Panel移除。
5. 拖拽时,允许在父容器内移动,不允许超出父容器边界。
6. 允许拖动边框改变Area的大小。
7. 当内容区只包含一个Panel时显示Area的标题栏。
8. 当内容区只包含一个Panel时拖动Panel标题栏就相当于拖动Area。
9. 当内容区只包含一个Panel时拖动TabPage的页标签就相当于拖动Area的标题栏从而改变Area的位置。已实现
10. 当内容区只包含一个Panel时单击Panel的最大化按钮就相当于单击Area的最大化按钮同时最大化Panel。
11. 当内容区只包含一个Panel时最大化Area时Panel也会最大化。
### TabPage
1. 初始添加时默认宽300px高250px。位置相对父容器水平居中垂直居中。
2. 背景颜色为#5D6B99
3. 当TabPage的页标签被选中时背景颜色为#F5CC84,文字颜色为#000000
4. 当TabPage的页标签未被选中时背景颜色与Area的背景颜色相同文字颜色为#FFFFFF
5. 当TabPage的页标签被选中时激活的页标签不显示关闭按钮。
### Panel
1. 填充满父容器。