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

View File

@@ -36,6 +36,9 @@
:id="tabPage.id" :id="tabPage.id"
:title="tabPage.title" :title="tabPage.title"
:panels="tabPage.panels" :panels="tabPage.panels"
@tabDragStart="onTabDragStart(area.id, $event)"
@tabDragMove="onTabDragMove(area.id, $event)"
@tabDragEnd="onTabDragEnd"
> >
<!-- 在TabPage内渲染其包含的Panels --> <!-- 在TabPage内渲染其包含的Panels -->
<Panel <Panel
@@ -86,6 +89,14 @@ const panelDragState = ref({
startAreaPos: { x: 0, y: 0 } 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 = () => { const addFloatingPanel = () => {
// 获取父容器尺寸以计算居中位置 // 获取父容器尺寸以计算居中位置
@@ -297,6 +308,64 @@ const onPanelDragEnd = () => {
panelDragState.value.currentAreaId = null 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也会自动最大化 // 监听floatingAreas变化确保当Area最大化时Panel也会自动最大化
watch(floatingAreas, (newAreas) => { watch(floatingAreas, (newAreas) => {
newAreas.forEach(area => { newAreas.forEach(area => {

View File

@@ -7,10 +7,13 @@
:key="panel.id" :key="panel.id"
:class="['tab-item', { 'active': activeTabIndex === index }]" :class="['tab-item', { 'active': activeTabIndex === index }]"
@click="setActiveTab(index)" @click="setActiveTab(index)"
@mousedown="onTabDragStart(index, $event)"
> >
<div class="flex items-center justify-between h-full px-3"> <div class="flex items-center justify-between h-full px-3">
<span class="tab-title">{{ panel.title }}</span> <span class="tab-title">{{ panel.title }}</span>
<!-- 当标签页未被激活时显示关闭按钮 -->
<button <button
v-if="activeTabIndex !== index"
class="button-icon p-[2px] rounded hover:opacity-100 opacity-80" class="button-icon p-[2px] rounded hover:opacity-100 opacity-80"
@click.stop="closeTab(panel.id)" @click.stop="closeTab(panel.id)"
aria-label="关闭" aria-label="关闭"
@@ -33,7 +36,7 @@
</template> </template>
<script setup> <script setup>
import { defineProps, defineEmits, ref } from 'vue' import { defineProps, defineEmits, ref, onMounted } from 'vue'
const props = defineProps({ const props = defineProps({
id: { type: String, required: true }, 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) => { const setActiveTab = (index) => {
if (index >= 0 && index < props.tabs.length) { if (index >= 0 && index < props.panels.length) {
activeTabIndex.value = index 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) => { const closeTab = (tabId) => {
emit('tabClose', { id: 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> </script>
<style scoped> <style scoped>
@@ -124,12 +192,12 @@ const closeTab = (tabId) => {
} }
.tab-item.active { .tab-item.active {
background: #ffffff; background: #F5CC84;
border: 1px solid #c7d2ea; border: 1px solid #c7d2ea;
border-bottom-color: #ffffff; border-bottom-color: #F5CC84;
border-top-width: 2px; border-top-width: 2px;
border-top-color: #0078d7; border-top-color: #0078d7;
color: #333; color: #000000;
font-weight: 500; font-weight: 500;
} }
@@ -138,8 +206,8 @@ const closeTab = (tabId) => {
} }
.tab-item.active:hover { .tab-item.active:hover {
background: #ffffff; background: #F5CC84;
color: #333; color: #000000;
} }

View File

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