实现TabPage标签拖拽功能及样式优化:1. 激活标签背景色改为#F5CC84 2. 激活标签不显示关闭按钮 3. 修复TabPage标签拖拽功能
This commit is contained in:
4
AutoRobot/Windows/Robot/Web/package-lock.json
generated
4
AutoRobot/Windows/Robot/Web/package-lock.json
generated
@@ -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",
|
||||||
|
|||||||
@@ -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 => {
|
||||||
|
|||||||
@@ -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;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -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. 填充满父容器。
|
||||||
|
|||||||
Reference in New Issue
Block a user