Files
JoyD/AutoRobot/Windows/Robot/Web/src/DockLayout/DockIndicator.vue
2025-11-13 16:12:23 +08:00

761 lines
22 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<template>
<div v-if="visible" class="dock-indicator" :style="indicatorStyle">
<!-- 停靠区 半透明区域框根据活动区域显示 -->
<div
v-if="activeDockZone"
class="dock-preview-area"
:style="previewAreaStyle"
></div>
<!-- 1. 定义可复用组件symbol封装所有渐变和路径只写一次 -->
<svg width="0" height="0" viewBox="0 0 40 40" aria-hidden="true">
<defs>
<!-- 渐变定义共用只写一次 -->
<linearGradient
id="lightGradient"
x1="0%" y1="0%" x2="100%" y2="100%">
<stop offset="0%" stop-color="#DCE3F5" />
<stop offset="100%" stop-color="#B7BED1" />
</linearGradient>
<linearGradient
id="Area"
x1="50%" y1="0%" x2="50%" y2="100%">
<stop offset="0%" stop-color="#F0E2BC" />
<stop offset="100%" stop-color="#B09556" />
</linearGradient>
</defs>
<!-- 封装所有图形为 symbolid 用于后续调用 -->
<symbol id="shared-border" viewBox="0 0 40 40">
<path
fill="#61697E"
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 0 L40 0 L40 40 L0 40 Z M1 1 L39 1 L39 39 L1 39 Z" />
<path
fill="#A1A9C4"
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 1 L39 1 L39 39 L1 39 Z M4 5 L5 4 L35 4 L36 5 L36 35 L35 36 L5 36 L4 35 Z" />
<path
fill="#7E869C"
fill-rule="evenodd"
clip-rule="evenodd"
d="M4 5 L5 4 L35 4 L36 5 L36 35 L35 36 L5 36 L4 35 Z M6 7 L7 6 L33 6 L34 7 L34 33 L33 34 L7 34 L6 33 Z" />
<path
fill="url(#lightGradient)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M6 7 L7 6 L33 6 L34 7 L34 33 L33 34 L7 34 L6 33 Z" />
</symbol>
<symbol id="shared-icon" viewBox="0 0 40 40">
<use xlink:href="#shared-border" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M16 30 L20 26 L23 30 Z" />
</symbol>
<symbol id="shared-area" viewBox="0 0 40 40">
<use xlink:href="#shared-border" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L32 8 L32 31 L31 32 L9 32 L8 31 Z" />
</symbol>
</svg>
<!-- 上区指示 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="false"
>
<use xlink:href="#shared-area" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 13 L30 13 L30 20 L10 20 Z" />
<path
d="M10 20 L30 20"
stroke="#4C5E83"
stroke-dasharray="2,2"
stroke-width="1"
fill="none" />
<path
fill="#F0F2F6"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 21 L30 21 L30 30 L10 30 Z" />
</svg>
<!-- 右区指示 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="false"
>
<use xlink:href="#shared-area" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 14 L30 14 L30 30 L20 30 Z" />
<path
d="M20 14 L20 30"
stroke="#4C5E83"
stroke-dasharray="2,2"
stroke-width="1"
fill="none" />
<path
fill="#F0F2F6"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 14 L19 14 L19 30 L10 30 Z" />
</svg>
<!-- 下区指示 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="false"
>
<use xlink:href="#shared-area" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 21 L30 21 L30 30 L10 30 Z" />
<path
d="M10 21 L30 21"
stroke="#4C5E83"
stroke-dasharray="2,2"
stroke-width="1"
fill="none" />
<path
fill="#F0F2F6"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 13 L30 13 L30 20 L10 20 Z" />
</svg>
<!-- 左区指示 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="false"
>
<use xlink:href="#shared-area" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 14 L19 14 L19 30 L10 30 Z" />
<path
d="M20 14 L20 30"
stroke="#4C5E83"
stroke-dasharray="2,2"
stroke-width="1"
fill="none" />
<path
fill="#F0F2F6"
fill-rule="evenodd"
clip-rule="evenodd"
d="M20 14 L30 14 L30 30 L20 30 Z" />
</svg>
<!-- 上指示根据activeDockZone状态显示和高亮 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-top"
:class="{ 'active': activeDockZone === 'top' }"
@mouseenter="handleMouseEnter('top')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L32 8 L32 20 L31 21 L9 21 L8 20 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 13 L30 13 L30 19 L10 19 Z" />
</svg>
<!-- 右指示根据activeDockZone状态显示和高亮 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-right"
:class="{ 'active': activeDockZone === 'right' }"
@mouseenter="handleMouseEnter('right')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(90 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M19 8 L32 8 L32 31 L31 32 L20 32 L19 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M21 14 L30 14 L30 30 L21 30 Z" />
</svg>
<!-- 下指示根据activeDockZone状态显示和高亮 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-bottom"
:class="{ 'active': activeDockZone === 'bottom' }"
@mouseenter="handleMouseEnter('bottom')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(180 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 19 L32 19 L32 31 L31 32 L9 32 L8 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 24 L30 24 L30 30 L10 30 Z" />
</svg>
<!-- 左指示根据activeDockZone状态显示和高亮 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-left"
:class="{ 'active': activeDockZone === 'left' }"
@mouseenter="handleMouseEnter('left')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(270 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L21 8 L21 31 L19 32 L9 32 L8 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 14 L19 14 L19 30 L10 30 Z" />
</svg>
<!-- 中心区域容器包装中心指示区和中心指示器 -->
<div class="center-dock-container">
<!-- 中心指示区一直可见 -->
<svg
width="185"
height="185"
viewBox="0 0 185 185"
aria-hidden="true"
class="indicator-center-area"
:class="{ 'active': activeDockZone === 'center' }"
>
<path
fill="#636873"
fill-rule="evenodd"
clip-rule="evenodd"
d="M0 72 L63 72 L72 63 L72 0 L113 0 L113 63 L123 72 L185 72 L185 113 L123 113 L113 123 L113 185 L72 185 L72 123 L63 113 L0 113 Z" />
<path
fill="#D5DCF0"
fill-rule="evenodd"
clip-rule="evenodd"
d="M1 73 L64 73 L73 64 L73 1 L112 1 L112 64 L122 73 L184 73 L184 112 L122 112 L112 122 L112 184 L73 184 L73 122 L64 112 L1 112 Z" />
</svg>
<!-- 中心指示区上方指示器位于中心指示区上边缘距离上边框5像素 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-center-top"
:class="{ 'active': activeDockZone === 'center-top' }"
@mouseenter="handleMouseEnter('center-top')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L32 8 L32 20 L31 21 L9 21 L8 20 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 13 L30 13 L30 19 L10 19 Z" />
</svg>
<!-- 中心指示区右侧指示器位于中心指示区右边缘距离右边框5像素 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-center-right"
:class="{ 'active': activeDockZone === 'center-right' }"
@mouseenter="handleMouseEnter('center-right')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(90 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M19 8 L32 8 L32 31 L31 32 L20 32 L19 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M21 14 L30 14 L30 30 L21 30 Z" />
</svg>
<!-- 中心指示区下方指示器位于中心指示区下边缘距离下边框5像素 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-center-bottom"
:class="{ 'active': activeDockZone === 'center-bottom' }"
@mouseenter="handleMouseEnter('center-bottom')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(180 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 19 L32 19 L32 31 L31 32 L9 32 L8 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 24 L30 24 L30 30 L10 30 Z" />
</svg>
<!-- 中心指示区左侧指示器位于中心指示区左边缘距离左边框5像素 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-center-left"
:class="{ 'active': activeDockZone === 'center-left' }"
@mouseenter="handleMouseEnter('center-left')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-icon" transform="rotate(270 20 20)" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L21 8 L21 31 L19 32 L9 32 L8 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 14 L19 14 L19 30 L10 30 Z" />
</svg>
<!-- 中心指示器根据activeDockZone状态显示和高亮添加鼠标事件处理 -->
<svg
width="41"
height="41"
viewBox="0 0 40 40"
aria-hidden="true"
class="indicator-center"
:class="{ 'active': activeDockZone === 'center' }"
@mouseenter="handleMouseEnter('center')"
@mouseleave="handleMouseLeave"
>
<use xlink:href="#shared-border" />
<path
fill="#4C5E83"
fill-rule="evenodd"
clip-rule="evenodd"
d="M8 8 L32 8 L32 31 L31 32 L9 32 L8 31 Z" />
<path
fill="url(#Area)"
fill-rule="evenodd"
clip-rule="evenodd"
d="M10 14 L30 14 L30 30 L10 30 Z" />
</svg>
</div>
</div>
</template>
<script setup>
import { computed, watch, ref, onUnmounted } from 'vue'
// Props定义
const props = defineProps({
// 是否可见
visible: {
type: Boolean,
default: false
},
// 目标区域的位置和大小信息
targetRect: {
type: Object,
default: () => ({
left: 0,
top: 0,
width: 0,
height: 0
})
},
// 鼠标位置
mousePosition: {
type: Object,
default: () => ({
x: 0,
y: 0
})
}
})
// 鼠标悬停在哪个指示器上
const hoveredZone = ref(null)
// 鼠标是否悬停在中心指示器上(用于控制中心指示区的显示)
const isCenterIndicatorHovered = ref(false)
// 延迟定时器
let mouseLeaveTimer = null
let centerLeaveTimer = null
// 处理鼠标进入指示器
const handleMouseEnter = (zone) => {
// 清除可能存在的离开定时器
if (mouseLeaveTimer) {
clearTimeout(mouseLeaveTimer)
mouseLeaveTimer = null
}
hoveredZone.value = zone
// 如果是中心指示器,设置专门的状态
if (zone === 'center') {
isCenterIndicatorHovered.value = true
}
}
// 处理鼠标离开指示器
const handleMouseLeave = () => {
// 添加短暂延迟,避免快速进出导致的闪烁
mouseLeaveTimer = setTimeout(() => {
hoveredZone.value = null
mouseLeaveTimer = null
}, 100)
// 单独处理中心指示器的离开事件,设置延迟
centerLeaveTimer = setTimeout(() => {
isCenterIndicatorHovered.value = false
centerLeaveTimer = null
}, 100)
}
// 清理定时器
onUnmounted(() => {
if (mouseLeaveTimer) {
clearTimeout(mouseLeaveTimer)
}
if (centerLeaveTimer) {
clearTimeout(centerLeaveTimer)
}
})
// 计算指示器的样式
const indicatorStyle = computed(() => {
return {
position: 'absolute',
left: `${props.targetRect.left}px`,
top: `${props.targetRect.top}px`,
width: `${props.targetRect.width}px`,
height: `${props.targetRect.height}px`,
pointerEvents: props.visible ? 'auto' : 'none',
zIndex: 9999
}
})
// 计算活动的停靠区域 - 只有当鼠标悬停在指示器上时才返回对应的区域
const activeDockZone = computed(() => {
if (!props.visible) return null
// 只有当鼠标悬停在某个指示器上时才返回对应的区域
return hoveredZone.value
})
// 计算半透明区域框的样式
const previewAreaStyle = computed(() => {
if (!activeDockZone.value) return {};
const { left, top, width, height } = props.targetRect;
const threshold = 0.25;
// 根据不同的活动区域计算预览区域的样式
switch (activeDockZone.value) {
case 'top':
case 'center-top': // 中心指示区上方指示器使用与top相同的预览区域
return {
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
width: `${width}px`,
height: `${height * threshold}px`
};
case 'bottom':
case 'center-bottom': // 中心指示区下方指示器使用与bottom相同的预览区域
return {
position: 'absolute',
left: `${left}px`,
top: `${top + height * (1 - threshold)}px`,
width: `${width}px`,
height: `${height * threshold}px`
};
case 'left':
case 'center-left': // 中心指示区左侧指示器使用与left相同的预览区域
return {
position: 'absolute',
left: `${left}px`,
top: `${top}px`,
width: `${width * threshold}px`,
height: `${height}px`
};
case 'right':
case 'center-right': // 中心指示区右侧指示器使用与right相同的预览区域
return {
position: 'absolute',
left: `${left + width * (1 - threshold)}px`,
top: `${top}px`,
width: `${width * threshold}px`,
height: `${height}px`
};
case 'center':
return {
position: 'absolute',
left: `${left + width * threshold}px`,
top: `${top + height * threshold}px`,
width: `${width * (1 - 2 * threshold)}px`,
height: `${height * (1 - 2 * threshold)}px`
};
default:
return {};
}
})
// 定义事件
const emit = defineEmits(['zone-active'])
// 监听activeDockZone变化触发事件
watch(activeDockZone, (newZone) => {
emit('zone-active', newZone)
})
</script>
<style scoped>
.dock-indicator {
position: absolute;
box-sizing: border-box;
}
/* 半透明区域框样式 */
.dock-preview-area {
background-color: rgba(161, 169, 196, 0.3); /* 半透明背景 */
border: 2px dashed #61697E; /* 虚线边框 */
box-sizing: border-box;
z-index: 9998; /* 确保在指示器下方 */
transition: all 0.2s ease; /* 平滑过渡效果 */
pointer-events: none; /* 确保区域框不干扰鼠标事件 */
}
/* 上指示器定位在目标区域的顶端中间上边缘距dock-layout上边缘5像素 */
.indicator-top {
position: absolute;
top: 5px;
left: 50%;
transform: translateX(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 9999; /* 确保在预览区域上方 */
}
/* 右指示器定位在目标区域的右侧中间右边缘距dock-layout右边缘5像素 */
.indicator-right {
position: absolute;
top: 50%;
right: 5px;
transform: translateY(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 9999; /* 确保在预览区域上方 */
}
/* 下指示器定位在目标区域的底部中间下边缘距dock-layout下边缘5像素 */
.indicator-bottom {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 9999; /* 确保在预览区域上方 */
}
/* 左指示器定位在目标区域的左侧中间左边缘距dock-layout左边缘5像素 */
.indicator-left {
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 9999; /* 确保在预览区域上方 */
}
/* 中心区域容器:包装中心指示区和中心指示器 */
.center-dock-container {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
z-index: 9999; /* 确保在预览区域上方 */
width: 185px;
height: 185px;
display: flex;
justify-content: center;
align-items: center;
}
/* 中心指示区:较大的背景区域 */
.indicator-center-area {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 1; /* 中心指示区在下层 */
}
/* 中心指示区上方指示器位于中心指示区上边缘距离上边框5像素 */
.indicator-center-top {
position: absolute;
top: 5px;
left: 50%;
transform: translateX(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 2; /* 确保在中心指示区上方 */
}
/* 中心指示区右侧指示器位于中心指示区右边缘距离右边框5像素 */
.indicator-center-right {
position: absolute;
top: 50%;
right: 5px;
transform: translateY(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 2; /* 确保在中心指示区上方 */
}
/* 中心指示区下方指示器位于中心指示区下边缘距离下边框5像素 */
.indicator-center-bottom {
position: absolute;
bottom: 5px;
left: 50%;
transform: translateX(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 2; /* 确保在中心指示区上方 */
}
/* 中心指示区左侧指示器位于中心指示区左边缘距离左边框5像素 */
.indicator-center-left {
position: absolute;
top: 50%;
left: 5px;
transform: translateY(-50%);
opacity: 0.7; /* 默认半透明 */
transition: opacity 0.2s;
z-index: 2; /* 确保在中心指示区上方 */
}
/* 中心指示器:较小的图标,显示在中心指示区上层 */
.indicator-center {
position: relative;
z-index: 2; /* 确保在中心指示区上方 */
opacity: 0.7; /* 默认半透明 */
transition: all 0.2s; /* 应用到所有属性的过渡 */
}
/* 活动状态样式 */
.indicator-top.active {
opacity: 1; /* 完全不透明 */
transform: translateX(-50%) scale(1.1); /* 保持水平居中的同时放大 */
}
.indicator-right.active {
opacity: 1; /* 完全不透明 */
transform: translateY(-50%) scale(1.1); /* 保持垂直居中的同时放大 */
}
.indicator-bottom.active {
opacity: 1; /* 完全不透明 */
transform: translateX(-50%) scale(1.1); /* 保持水平居中的同时放大 */
}
.indicator-left.active {
opacity: 1; /* 完全不透明 */
transform: translateY(-50%) scale(1.1); /* 保持垂直居中的同时放大 */
}
.indicator-center.active {
opacity: 1; /* 完全不透明 */
transform: scale(1.1); /* 只放大,不改变位置 */
}
/* 中心指示区内部指示器的活动状态样式 */
.indicator-center-top.active {
opacity: 1; /* 完全不透明 */
transform: translateX(-50%) scale(1.1); /* 保持水平居中的同时放大 */
}
.indicator-center-right.active {
opacity: 1; /* 完全不透明 */
transform: translateY(-50%) scale(1.1); /* 保持垂直居中的同时放大 */
}
.indicator-center-bottom.active {
opacity: 1; /* 完全不透明 */
transform: translateX(-50%) scale(1.1); /* 保持水平居中的同时放大 */
}
.indicator-center-left.active {
opacity: 1; /* 完全不透明 */
transform: translateY(-50%) scale(1.1); /* 保持垂直居中的同时放大 */
}
.indicator-center-area.active {
opacity: 1; /* 完全不透明 */
transition: all 0.2s;
}
</style>