on-site-robots-screen/src/views/home/components/modal-content/preset-setting.vue

670 lines
20 KiB
Vue
Raw Normal View History

2025-06-24 18:12:02 +08:00
<template>
<!-- 预置位配置 -->
2025-06-25 18:22:07 +08:00
<DialogModal
@onHandleCloseModal="onHandleCloseModal"
:modalTitle="modalTitle"
2025-06-27 11:23:33 +08:00
:width="`90vw`"
2025-06-25 18:22:07 +08:00
style="position: relative"
>
2025-06-24 18:12:02 +08:00
<!-- 平面图操作区域 -->
2025-06-25 18:22:07 +08:00
<div
class="plane-map-container"
ref="planeMapContainer"
:class="{ 'can-drag': canDrag() }"
2025-06-27 11:23:33 +08:00
:style="{ height: INITIAL_HEIGHT + 'px' }"
2025-06-25 18:22:07 +08:00
@mousemove="handleMouseMove"
>
2025-06-24 18:12:02 +08:00
<svg
2025-06-25 18:22:07 +08:00
ref="svgMapRef"
class="svg-map-container"
:width="svgWidth"
:height="svgHeight"
2025-06-27 11:23:33 +08:00
@wheel.passive="handleWheel"
2025-06-24 18:12:02 +08:00
@click="handleMapClick"
2025-06-25 18:22:07 +08:00
:style="{ transform: `translate(${offsetX}px, ${offsetY}px)` }"
2025-06-24 18:12:02 +08:00
>
2025-06-25 18:22:07 +08:00
<!-- 图片宽高 100% 跟随 SVG -->
2025-06-24 18:12:02 +08:00
<image
2025-06-25 18:22:07 +08:00
width="100%"
height="100%"
2025-06-27 11:23:33 +08:00
:href="mapInfo.mapBase64"
2025-06-25 18:22:07 +08:00
preserveAspectRatio="none"
2025-06-24 18:12:02 +08:00
/>
2025-06-25 18:22:07 +08:00
<!-- 标记的点位 -->
2025-06-24 18:12:02 +08:00
<circle
2025-06-25 18:22:07 +08:00
v-for="(point, index) in devicePoints"
2025-06-24 18:12:02 +08:00
:key="index"
2025-06-27 11:23:33 +08:00
:cx="point.markerX * scale + offsetX / scale"
:cy="point.markerY1 * scale + offsetY / scale"
2025-06-24 18:12:02 +08:00
r="8"
fill="red"
2025-06-25 18:22:07 +08:00
@click="handlePointClick(point, index)"
:ref="(el) => setItemRef(el, index)"
@contextmenu.prevent="handlePointRightClick($event, point, index)"
2025-06-24 18:12:02 +08:00
/>
</svg>
2025-06-25 18:22:07 +08:00
<div v-if="showTooltip && mousePosition" class="coord-tooltip" :style="tooltipStyle">
<!-- 数据向下取整 -->
2025-06-27 11:23:33 +08:00
X: {{ Math.floor(mousePosition.x) * 2 }}, Y: {{ Math.floor(mousePosition.y) * 2 }}
2025-06-24 18:12:02 +08:00
</div>
2025-06-25 18:22:07 +08:00
<div v-if="showContextMenu" class="context-menu" :style="contextMenuStyle" @click.stop>
<div class="menu-item" @click="handleModifyPoint">修改</div>
<div class="menu-item" @click="handleDeletePoint">删除</div>
</div>
<!-- 操作按钮 -->
<n-flex vertical class="operation-container">
<n-button strong size="small" color="#5c96fa" @click="zoomIn($event)">
<template #icon>
<NIcon color="#fff">
<AddCircleOutline />
</NIcon>
</template>
</n-button>
<n-button strong size="small" color="#5c96fa" @click="zoomOut($event)">
<template #icon>
<NIcon color="#fff">
<RemoveCircleOutline />
</NIcon>
</template>
</n-button>
</n-flex>
2025-06-24 18:12:02 +08:00
</div>
</DialogModal>
</template>
<script setup>
import DialogModal from '@/components/DialogModal/index.vue'
2025-06-27 11:23:33 +08:00
import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
import { NIcon } from 'naive-ui'
2025-06-25 18:22:07 +08:00
import { throttle } from 'lodash'
2025-06-27 11:23:33 +08:00
import { AddCircleOutline, RemoveCircleOutline } from '@vicons/ionicons5'
import { getRobotDeviceListFn, getRobotMapInfoFn } from '@/utils/getRobotInfo'
import { getMarkerListAllApi } from '@/api/home'
2025-06-28 17:09:43 +08:00
import { useMessage } from 'naive-ui'
2025-06-24 18:12:02 +08:00
2025-06-27 11:23:33 +08:00
const modalTitle = ref('预置位配置') // 模态框标题
const mousePosition = ref(null) // 鼠标位置
const devicePoints = ref([]) // 设备点位
const isPointClickStatus = ref(true) // 是否点击点位
const mapInfo = ref({}) // 地图信息
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
// SVG 初始尺寸
2025-06-27 11:23:33 +08:00
const INITIAL_WIDTH = ref(0)
const INITIAL_HEIGHT = ref(0)
2025-06-25 18:22:07 +08:00
// SVG 动态宽高
2025-06-27 11:23:33 +08:00
const svgWidth = ref(INITIAL_WIDTH.value)
const svgHeight = ref(INITIAL_HEIGHT.value)
// 拖拽相关状态
const isDragging = ref(false) // 是否拖拽
const offsetX = ref(0) // 偏移量X
const offsetY = ref(0) // 偏移量Y
const startX = ref(0) // 起始X
const startY = ref(0) // 起始Y
const maxOffsetX1 = ref(0) // 最大偏移量X
const maxOffsetY1 = ref(0) // 最大偏移量Y
2025-06-25 18:22:07 +08:00
// 缩放比例
const scale = ref(1.0)
// 缩放配置
const MIN_SCALE = 0.5 // 最小缩放比例
const MAX_SCALE = 3 // 最大缩放比例
const SCALE_STEP = 0.1 // 每次滚轮的缩放步长
2025-06-27 11:23:33 +08:00
// 右键菜单相关状态
const showContextMenu = ref(false) // 是否显示右键菜单
const contextMenuStyle = ref({}) // 右键菜单样式
const selectedPointIndex = ref(-1) // 选中的点位索引
// 容器尺寸跟踪
const planeMapContainer = ref(null) // 容器引用
const svgMapRef = ref(null) // svg引用
const tooltipStyle = ref({}) // 提示框样式
const itemRefs = ref([]) // 标记点位引用
const emits = defineEmits(['onHandleCloseModal', 'onHandleAddMarker']) // 定义 emits
2025-06-28 17:09:43 +08:00
const message = useMessage()
2025-06-27 11:23:33 +08:00
// 定义props
const props = defineProps({
markerInfoNew: {
type: Object,
default: () => {},
},
2025-06-28 17:09:43 +08:00
formType: {
type: String,
default: '',
},
2025-06-27 11:23:33 +08:00
})
// 设置标记点位引用
const setItemRef = (el, index) => {
if (el) {
itemRefs.value[index] = el
}
}
2025-06-25 18:22:07 +08:00
// 缩放时调整偏移量(以鼠标位置为中心)
const adjustOffsetOnZoom = (e, newScale) => {
const container = planeMapContainer.value
if (!container) return
const containerRect = container.getBoundingClientRect()
const mouseX = e ? e.clientX - containerRect.left : containerRect.width / 2
const mouseY = e ? e.clientY - containerRect.top : containerRect.height / 2
// 计算鼠标在内容中的相对位置(基于当前缩放和偏移)
const contentX = (mouseX - offsetX.value) / scale.value
const contentY = (mouseY - offsetY.value) / scale.value
// 更新偏移量(保持鼠标指向的内容位置不变)
offsetX.value = mouseX - contentX * newScale
offsetY.value = mouseY - contentY * newScale
enforceBoundaries() // 确保修正后不越界
}
2025-06-27 11:23:33 +08:00
// 缩放
2025-06-25 18:22:07 +08:00
const zoom = (direction, e = { clientX: 0, clientY: 0 }) => {
const delta = direction === 'in' ? SCALE_STEP : -SCALE_STEP
const newScale = Math.min(MAX_SCALE, Math.max(MIN_SCALE, scale.value + delta))
if (newScale !== scale.value) {
const oldScale = scale.value
scale.value = newScale
updateSvgSize() // 内部已按比例修正偏移量
adjustOffsetOnZoom(e, newScale) // 进一步以鼠标为中心修正
}
2025-06-24 18:12:02 +08:00
}
2025-06-27 11:23:33 +08:00
// 放大
2025-06-25 18:22:07 +08:00
const zoomIn = (e) => zoom('in', e)
2025-06-27 11:23:33 +08:00
// 缩小
2025-06-25 18:22:07 +08:00
const zoomOut = (e) => zoom('out', e)
2025-06-27 11:23:33 +08:00
// 滚轮缩放
2025-06-25 18:22:07 +08:00
const handleWheel = (e) => {
2025-06-27 11:23:33 +08:00
requestAnimationFrame(() => {
e.preventDefault() // 延迟阻止默认行为
zoom(e.deltaY > 0 ? 'out' : 'in', e)
})
2025-06-25 18:22:07 +08:00
}
// 更新 SVG 尺寸
const updateSvgSize = () => {
const oldScale = scale.value
2025-06-27 11:23:33 +08:00
svgWidth.value = INITIAL_WIDTH.value * scale.value
svgHeight.value = INITIAL_HEIGHT.value * scale.value
2025-06-25 18:22:07 +08:00
// 按比例修正偏移量(保持视觉连续性)
if (oldScale !== 0) {
offsetX.value = (offsetX.value / oldScale) * scale.value
offsetY.value = (offsetY.value / oldScale) * scale.value
}
// 确保修正后不越界
enforceBoundaries()
}
2025-06-27 11:23:33 +08:00
// 确保修正后不越界
2025-06-25 18:22:07 +08:00
const enforceBoundaries = () => {
const { maxOffsetX, maxOffsetY } = getMaxOffset()
// 限制偏移量
offsetX.value = Math.min(0, Math.max(-maxOffsetX, offsetX.value))
offsetY.value = Math.min(0, Math.max(-maxOffsetY, offsetY.value))
// 确保鼠标坐标不会越界(二次保护)
if (mousePosition.value) {
mousePosition.value = {
x: Math.max(0, Math.min(svgWidth.value / scale.value, mousePosition.value.x)),
y: Math.max(0, Math.min(svgHeight.value / scale.value, mousePosition.value.y)),
2025-06-24 18:12:02 +08:00
}
}
2025-06-25 18:22:07 +08:00
}
// 开始拖拽
const startDrag = (e) => {
if (!canDrag()) return // 如果不需要拖拽,直接返回
isDragging.value = true
startX.value = e.clientX - offsetX.value
startY.value = e.clientY - offsetY.value
document.addEventListener('mousemove', handleDrag)
document.addEventListener('mouseup', stopDrag)
}
// 处理拖拽
const handleDrag = (e) => {
if (!isDragging.value) return
const { maxOffsetX, maxOffsetY } = getMaxOffset()
// 计算新偏移量,并限制范围
let newOffsetX = e.clientX - startX.value
let newOffsetY = e.clientY - startY.value
maxOffsetX1.value = maxOffsetX
maxOffsetY1.value = maxOffsetY
// 限制 X 轴范围(左边界和右边界)
newOffsetX = Math.min(0, newOffsetX) // 不能向右移动(左边界)
newOffsetX = Math.max(-maxOffsetX, newOffsetX) // 不能向左超出(右边界)
// 限制 Y 轴范围(上边界和下边界)
newOffsetY = Math.min(0, newOffsetY) // 不能向下移动(上边界)
newOffsetY = Math.max(-maxOffsetY, newOffsetY) // 不能向上超出(下边界)
offsetX.value = newOffsetX
offsetY.value = newOffsetY
// 强制更新坐标显示
if (showTooltip.value) {
mousePosition.value = getLogicalPosition(e.clientX, e.clientY)
2025-06-24 18:12:02 +08:00
}
}
2025-06-25 18:22:07 +08:00
// 停止拖拽
const stopDrag = () => {
isDragging.value = false
isPointClickStatus.value = true
document.removeEventListener('mousemove', handleDrag)
document.removeEventListener('mouseup', stopDrag)
}
2025-06-27 11:23:33 +08:00
// 是否可以拖拽
2025-06-25 18:22:07 +08:00
const canDrag = () => {
const parentWidth = planeMapContainer.value?.clientWidth || 0
const parentHeight = planeMapContainer.value?.clientHeight || 0
return svgWidth.value > parentWidth || svgHeight.value > parentHeight
}
// 计算最大可移动偏移量(限制拖拽边界)
const getMaxOffset = () => {
const parentWidth = planeMapContainer.value?.clientWidth || 0
const parentHeight = planeMapContainer.value?.clientHeight || 0
return {
maxOffsetX: Math.max(0, svgWidth.value - parentWidth), // 右边界
maxOffsetY: Math.max(0, svgHeight.value - parentHeight), // 下边界
2025-06-24 18:12:02 +08:00
}
}
2025-06-27 11:23:33 +08:00
// 获取逻辑坐标
2025-06-25 18:22:07 +08:00
const getLogicalPosition = (clientX, clientY) => {
const svgRect = svgMapRef.value.getBoundingClientRect()
// 计算相对于SVG内容的逻辑坐标考虑缩放和偏移
const logicalX = (clientX - svgRect.left - offsetX.value) / scale.value
// Y坐标需要翻转因为原点在左下角
const logicalY =
svgHeight.value / scale.value - (clientY - svgRect.top - offsetY.value) / scale.value
const logicalY1 = (clientY - svgRect.top - offsetY.value) / scale.value
return {
2025-06-27 11:23:33 +08:00
x: Math.max(0, Math.min(INITIAL_WIDTH.value, logicalX)),
y: Math.max(0, Math.min(INITIAL_HEIGHT.value, logicalY)),
y1: Math.max(0, Math.min(INITIAL_HEIGHT.value, logicalY1)),
2025-06-25 18:22:07 +08:00
}
}
// 鼠标移动事件
const showTooltip = ref(false)
// 鼠标移动事件
const handleMouseMove = throttle((e) => {
if (!svgMapRef.value) return
// 检查鼠标是否在SVG内
const svgRect = svgMapRef.value.getBoundingClientRect()
const isInside =
e.clientX >= svgRect.left &&
e.clientX <= svgRect.right &&
e.clientY >= svgRect.top &&
e.clientY <= svgRect.bottom
showTooltip.value = isInside
if (isInside) {
mousePosition.value = getLogicalPosition(e.clientX, e.clientY)
tooltipStyle.value = {
left: `${e.clientX}px`,
top: `${e.clientY}px`,
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
}
}, 30)
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
// 左键点击标点
2025-06-24 18:12:02 +08:00
const handleMapClick = (e) => {
2025-06-25 18:22:07 +08:00
isPointClickStatus.value = false
if (!svgMapRef.value || isDragging.value) return // 拖拽过程中不标点
// 判断是否点击了标记的点位
const isPointClick = devicePoints.value.some((point, index) => {
const pointRect = itemRefs.value[index].getBoundingClientRect()
return (
e.clientX >= pointRect.left &&
e.clientX <= pointRect.right &&
e.clientY >= pointRect.top &&
e.clientY <= pointRect.bottom
)
})
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
if (isPointClick) {
return
}
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
const svgRect = svgMapRef.value.getBoundingClientRect()
const isInside =
e.clientX >= svgRect.left &&
e.clientX <= svgRect.right &&
e.clientY >= svgRect.top &&
e.clientY <= svgRect.bottom
if (isInside) {
const pos = getLogicalPosition(e.clientX, e.clientY)
addDevicePoint(pos.x, pos.y, pos.y1)
}
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
// 处理右键点击
const handlePointRightClick = (e, point, index) => {
e.preventDefault()
selectedPointIndex.value = index
// 计算菜单位置
const containerRect = planeMapContainer.value.getBoundingClientRect()
const x = e.clientX - containerRect.left
const y = e.clientY - containerRect.top
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
// 确保菜单不会超出容器边界
const menuWidth = 100
const menuHeight = 80
const adjustedX = x + menuWidth > containerRect.width ? x - menuWidth : x
const adjustedY = y + menuHeight > containerRect.height ? y - menuHeight : y
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
contextMenuStyle.value = {
left: `${adjustedX + 50}px`,
top: `${adjustedY + 20}px`,
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
showContextMenu.value = true
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
// 关闭菜单
const closeContextMenu = () => {
showContextMenu.value = false
}
// 点击菜单外部关闭菜单
const handleClickOutsideMenu = (e) => {
if (showContextMenu.value && !e.target.closest('.context-menu')) {
closeContextMenu()
}
}
// 修改点位
const handleModifyPoint = () => {
if (selectedPointIndex.value >= 0) {
const point = devicePoints.value[selectedPointIndex.value]
// 这里可以添加修改逻辑,比如弹出对话框
const markerInfo = {
type: '修改',
2025-06-27 11:23:33 +08:00
xCount: point.xCount,
yCount: point.yCount,
2025-06-25 18:22:07 +08:00
markerIndex: selectedPointIndex.value,
2025-06-27 11:23:33 +08:00
markerX: point.markerX,
markerY: point.markerY,
markerY1: point.markerY1,
markerName: point.markerName,
markerAngle: point.markerAngle,
markerPreset: point.markerPreset,
2025-07-01 10:34:22 +08:00
id: point.id,
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
emits('onHandleAddMarker', markerInfo)
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
closeContextMenu()
}
// 删除点位
const handleDeletePoint = () => {
if (selectedPointIndex.value >= 0) {
devicePoints.value.splice(selectedPointIndex.value, 1)
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
closeContextMenu()
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
// 添加设备点位
const addDevicePoint = (x, y, y1) => {
if (isPointClickStatus.value) return
devicePoints.value.push({
2025-06-27 11:23:33 +08:00
xCount: x * 2, // 像素坐标 真实数据
yCount: y * 2, // 像素坐标 真实数据
markerX: x, // 直接存储逻辑坐标
markerY: y, // 直接存储逻辑坐标
markerY1: y1, // 直接存储逻辑坐标
markerName: '',
markerAngle: '', // 角度
markerPreset: '', // 摄像头预置位
2025-06-25 18:22:07 +08:00
})
}
// 点击标记的点位
const handlePointClick = (point, index) => {
2025-06-28 17:09:43 +08:00
if (point.id) {
message.warning('点位已存在,无需新增,可右击修改或删除')
return
}
2025-06-25 18:22:07 +08:00
const markerInfo = {
type: '新增',
2025-06-27 11:23:33 +08:00
xCount: point.xCount, // 像素坐标 真实数据
yCount: point.yCount, // 像素坐标 真实数据
2025-06-25 18:22:07 +08:00
markerIndex: index,
2025-06-27 11:23:33 +08:00
markerX: point.markerX,
markerY: point.markerY,
markerY1: point.markerY1,
markerName: point.markerName,
markerAngle: point.markerAngle,
markerPreset: point.markerPreset,
2025-06-24 18:12:02 +08:00
}
2025-06-25 18:22:07 +08:00
emits('onHandleAddMarker', markerInfo)
2025-06-24 18:12:02 +08:00
}
// 获取全部已经添加的点位
const getMarkerListAll = async () => {
const { data: res } = await getMarkerListAllApi()
2025-06-28 17:09:43 +08:00
// console.log(res, 'res全部点位--')
if (res.code == 200) {
2025-06-28 17:09:43 +08:00
devicePoints.value = []
if (res.data.length > 0) {
const svgRect = svgMapRef.value.getBoundingClientRect()
res.data.forEach((item) => {
// addDevicePoint(item.positionX, item.positionY, item.positionY1)
const clientY =
svgHeight.value -
scale.value * (item.positionY / 2) +
svgRect.top +
offsetY.value
const logicalY1 = (clientY - svgRect.top - offsetY.value) / scale.value
devicePoints.value.push({
...item,
xCount: item.positionX, // 像素坐标 真实数据
yCount: item.positionY, // 像素坐标 真实数据
markerX: item.positionX / 2,
markerY: item.positionY / 2,
markerY1: logicalY1,
markerName: item.pointName,
markerAngle: item.theta,
})
})
}
}
}
getMarkerListAll()
2025-06-25 18:22:07 +08:00
// 关闭模态框
const onHandleCloseModal = () => {
emits('onHandleCloseModal')
2025-06-24 18:12:02 +08:00
}
2025-06-27 11:23:33 +08:00
// 监听点击事件以关闭菜单
onMounted(async () => {
document.addEventListener('click', handleClickOutsideMenu)
const svg = svgMapRef.value
svg.addEventListener('wheel', handleWheel, { passive: false }) // 明确声明
const deviceInfo = await getRobotDeviceListFn()
// 获取地图信息
mapInfo.value = await getRobotMapInfoFn(deviceInfo?.puId)
})
onBeforeUnmount(() => {
document.removeEventListener('click', handleClickOutsideMenu)
})
watch(
() => props.markerInfoNew,
(newVal) => {
// 判断是否为空对象
if (Object.keys(newVal).length > 0) {
2025-06-28 17:09:43 +08:00
// if (props.formType == '新增') {
// getMarkerListAll()
// }
// const svgRect = svgMapRef.value.getBoundingClientRect()
// const clientY =
// svgHeight.value - scale.value * (newVal.yCount / 2) + svgRect.top + offsetY.value
// const logicalY1 = (clientY - svgRect.top - offsetY.value) / scale.value
// devicePoints.value[newVal.markerIndex].markerX = newVal.xCount / 2
// devicePoints.value[newVal.markerIndex].markerY = newVal.markerY
// devicePoints.value[newVal.markerIndex].markerY1 = logicalY1
// devicePoints.value[newVal.markerIndex].markerName = newVal.markerName
// devicePoints.value[newVal.markerIndex].markerAngle = newVal.markerAngle
// devicePoints.value[newVal.markerIndex].markerPreset = newVal.markerPreset
// devicePoints.value[newVal.markerIndex].xCount = newVal.xCount
// devicePoints.value[newVal.markerIndex].yCount = newVal.yCount
2025-06-27 11:23:33 +08:00
}
},
{
immediate: true,
},
)
2025-06-28 17:09:43 +08:00
watch(
() => props.formType,
(newVal) => {
if (newVal == '新增') {
getMarkerListAll()
}
},
{
immediate: true,
},
)
2025-06-27 11:23:33 +08:00
watch(
// 监听地图信息
() => mapInfo.value,
(newVal) => {
if (newVal.mapWidth && newVal.mapHeight) {
INITIAL_WIDTH.value = Math.ceil(newVal.mapWidth / 2)
INITIAL_HEIGHT.value = Math.ceil(newVal.mapHeight / 2)
svgWidth.value = INITIAL_WIDTH.value
svgHeight.value = INITIAL_HEIGHT.value
}
},
)
2025-06-24 18:12:02 +08:00
</script>
<style lang="scss" scoped>
.plane-map-container {
width: 100%;
2025-06-27 11:23:33 +08:00
// height: 80vh;
max-height: 85vh;
2025-06-25 18:22:07 +08:00
overflow: auto;
// 优化横向和竖向滚动条样式
&::-webkit-scrollbar {
width: 6px;
height: 8px;
}
&::-webkit-scrollbar-thumb {
background: #888;
border-radius: 4px;
}
&::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
&.can-drag {
cursor: grab; /* 可拖拽时显示手型 */
&:active {
cursor: grabbing;
}
}
.svg-map-container {
position: relative;
cursor: default; /* 默认箭头 */
}
circle {
transition: fill 0.2s;
cursor: pointer;
&:hover {
fill: #ff5252;
stroke: white;
stroke-width: 2px;
}
}
2025-06-24 18:12:02 +08:00
2025-06-25 18:22:07 +08:00
// 操作按钮
.operation-container {
2025-06-24 18:12:02 +08:00
position: absolute;
2025-06-25 18:22:07 +08:00
top: 80px;
right: 60px;
2025-06-24 18:12:02 +08:00
z-index: 1000;
gap: 10px;
}
}
2025-06-25 18:22:07 +08:00
.coord-tooltip {
position: fixed; /* 改为fixed定位 */
background: rgba(0, 0, 0, 0.8);
color: white;
padding: 4px 8px;
border-radius: 4px;
pointer-events: none;
z-index: 1001;
transform: translate(10px, 10px);
}
.context-menu {
position: absolute;
background: white;
border: 1px solid #ddd;
border-radius: 4px;
box-shadow: 0 2px 10px rgba(0, 0, 0, 0.2);
z-index: 1002;
min-width: 100px;
.menu-item {
padding: 8px 12px;
cursor: pointer;
color: #333;
&:hover {
background-color: #f5f5f5;
}
}
}
2025-06-24 18:12:02 +08:00
</style>