685 lines
21 KiB
Vue
685 lines
21 KiB
Vue
<template>
|
||
<!-- 预置位配置 -->
|
||
<DialogModal
|
||
@onHandleCloseModal="onHandleCloseModal"
|
||
:modalTitle="modalTitle"
|
||
:width="`90vw`"
|
||
style="position: relative"
|
||
>
|
||
<!-- 平面图操作区域 -->
|
||
<div
|
||
class="plane-map-container"
|
||
ref="planeMapContainer"
|
||
:class="{ 'can-drag': canDrag() }"
|
||
:style="{ height: INITIAL_HEIGHT + 'px' }"
|
||
@mousemove="handleMouseMove"
|
||
>
|
||
<svg
|
||
ref="svgMapRef"
|
||
class="svg-map-container"
|
||
:width="svgWidth"
|
||
:height="svgHeight"
|
||
@wheel.passive="handleWheel"
|
||
@click="handleMapClick"
|
||
:style="{ transform: `translate(${offsetX}px, ${offsetY}px)` }"
|
||
>
|
||
<!-- 图片,宽高 100% 跟随 SVG -->
|
||
<image
|
||
width="100%"
|
||
height="100%"
|
||
:href="mapInfo.mapBase64"
|
||
preserveAspectRatio="none"
|
||
/>
|
||
|
||
<!-- 标记的点位 -->
|
||
<circle
|
||
v-for="(point, index) in devicePoints"
|
||
:key="index"
|
||
:cx="point.markerX * scale + offsetX / scale"
|
||
:cy="point.markerY1 * scale + offsetY / scale"
|
||
r="8"
|
||
fill="red"
|
||
@click="handlePointClick(point, index)"
|
||
:ref="(el) => setItemRef(el, index)"
|
||
@contextmenu.prevent="handlePointRightClick($event, point, index)"
|
||
/>
|
||
</svg>
|
||
|
||
<div v-if="showTooltip && mousePosition" class="coord-tooltip" :style="tooltipStyle">
|
||
<!-- 数据向下取整 -->
|
||
X: {{ Math.floor(mousePosition.x) * 2 }}, Y: {{ Math.floor(mousePosition.y) * 2 }}
|
||
</div>
|
||
|
||
<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>
|
||
</div>
|
||
</DialogModal>
|
||
</template>
|
||
|
||
<script setup>
|
||
import DialogModal from '@/components/DialogModal/index.vue'
|
||
import { ref, onMounted, watch, onBeforeUnmount } from 'vue'
|
||
import { NIcon } from 'naive-ui'
|
||
import { throttle } from 'lodash'
|
||
import { AddCircleOutline, RemoveCircleOutline } from '@vicons/ionicons5'
|
||
import { getRobotDeviceListFn, getRobotMapInfoFn } from '@/utils/getRobotInfo'
|
||
import { getMarkerListAllApi, deleteMarkerApi } from '@/api/home'
|
||
import { useMessage } from 'naive-ui'
|
||
|
||
const modalTitle = ref('预置位配置') // 模态框标题
|
||
const mousePosition = ref(null) // 鼠标位置
|
||
const devicePoints = ref([]) // 设备点位
|
||
const isPointClickStatus = ref(true) // 是否点击点位
|
||
const mapInfo = ref({}) // 地图信息
|
||
|
||
// SVG 初始尺寸
|
||
const INITIAL_WIDTH = ref(0)
|
||
const INITIAL_HEIGHT = ref(0)
|
||
|
||
// SVG 动态宽高
|
||
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
|
||
|
||
// 缩放比例
|
||
const scale = ref(1.0)
|
||
|
||
// 缩放配置
|
||
const MIN_SCALE = 0.5 // 最小缩放比例
|
||
const MAX_SCALE = 3 // 最大缩放比例
|
||
const SCALE_STEP = 0.1 // 每次滚轮的缩放步长
|
||
|
||
// 右键菜单相关状态
|
||
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
|
||
|
||
const message = useMessage()
|
||
|
||
// 定义props
|
||
const props = defineProps({
|
||
markerInfoNew: {
|
||
type: Object,
|
||
default: () => {},
|
||
},
|
||
formType: {
|
||
type: String,
|
||
default: '',
|
||
},
|
||
})
|
||
|
||
// 设置标记点位引用
|
||
const setItemRef = (el, index) => {
|
||
if (el) {
|
||
itemRefs.value[index] = el
|
||
}
|
||
}
|
||
|
||
// 缩放时调整偏移量(以鼠标位置为中心)
|
||
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() // 确保修正后不越界
|
||
}
|
||
|
||
// 缩放
|
||
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) // 进一步以鼠标为中心修正
|
||
}
|
||
}
|
||
|
||
// 放大
|
||
const zoomIn = (e) => zoom('in', e)
|
||
|
||
// 缩小
|
||
const zoomOut = (e) => zoom('out', e)
|
||
|
||
// 滚轮缩放
|
||
const handleWheel = (e) => {
|
||
requestAnimationFrame(() => {
|
||
e.preventDefault() // 延迟阻止默认行为
|
||
zoom(e.deltaY > 0 ? 'out' : 'in', e)
|
||
})
|
||
}
|
||
|
||
// 更新 SVG 尺寸
|
||
const updateSvgSize = () => {
|
||
const oldScale = scale.value
|
||
svgWidth.value = INITIAL_WIDTH.value * scale.value
|
||
svgHeight.value = INITIAL_HEIGHT.value * scale.value
|
||
|
||
// 按比例修正偏移量(保持视觉连续性)
|
||
if (oldScale !== 0) {
|
||
offsetX.value = (offsetX.value / oldScale) * scale.value
|
||
offsetY.value = (offsetY.value / oldScale) * scale.value
|
||
}
|
||
|
||
// 确保修正后不越界
|
||
enforceBoundaries()
|
||
}
|
||
|
||
// 确保修正后不越界
|
||
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)),
|
||
}
|
||
}
|
||
}
|
||
|
||
// 开始拖拽
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 停止拖拽
|
||
const stopDrag = () => {
|
||
isDragging.value = false
|
||
isPointClickStatus.value = true
|
||
document.removeEventListener('mousemove', handleDrag)
|
||
document.removeEventListener('mouseup', stopDrag)
|
||
}
|
||
|
||
// 是否可以拖拽
|
||
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), // 下边界
|
||
}
|
||
}
|
||
|
||
// 获取逻辑坐标
|
||
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 {
|
||
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)),
|
||
}
|
||
}
|
||
|
||
// 鼠标移动事件
|
||
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`,
|
||
}
|
||
}
|
||
}, 30)
|
||
|
||
// 左键点击标点
|
||
const handleMapClick = (e) => {
|
||
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
|
||
)
|
||
})
|
||
|
||
if (isPointClick) {
|
||
return
|
||
}
|
||
|
||
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)
|
||
}
|
||
}
|
||
|
||
// 处理右键点击
|
||
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
|
||
|
||
// 确保菜单不会超出容器边界
|
||
const menuWidth = 100
|
||
const menuHeight = 80
|
||
const adjustedX = x + menuWidth > containerRect.width ? x - menuWidth : x
|
||
const adjustedY = y + menuHeight > containerRect.height ? y - menuHeight : y
|
||
|
||
contextMenuStyle.value = {
|
||
left: `${adjustedX + 50}px`,
|
||
top: `${adjustedY + 20}px`,
|
||
}
|
||
|
||
showContextMenu.value = true
|
||
}
|
||
|
||
// 关闭菜单
|
||
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: '修改',
|
||
xCount: point.xCount,
|
||
yCount: point.yCount,
|
||
markerIndex: selectedPointIndex.value,
|
||
markerX: point.markerX,
|
||
markerY: point.markerY,
|
||
markerY1: point.markerY1,
|
||
markerName: point.markerName,
|
||
markerAngle: point.markerAngle,
|
||
markerPreset: point.markerPreset,
|
||
id: point.id,
|
||
isAdd: point.isAdd,
|
||
}
|
||
emits('onHandleAddMarker', markerInfo)
|
||
}
|
||
closeContextMenu()
|
||
}
|
||
|
||
// 删除点位
|
||
const handleDeletePoint = async () => {
|
||
if (selectedPointIndex.value >= 0) {
|
||
if (devicePoints.value[selectedPointIndex.value].isAdd) {
|
||
devicePoints.value.splice(selectedPointIndex.value, 1)
|
||
} else {
|
||
// 调后台接口删除
|
||
const { data: res } = await deleteMarkerApi({
|
||
id: devicePoints.value[selectedPointIndex.value].id,
|
||
})
|
||
if (res.code == 200) {
|
||
// 重新获取所有点位
|
||
devicePoints.value.splice(selectedPointIndex.value, 1)
|
||
}
|
||
}
|
||
}
|
||
closeContextMenu()
|
||
}
|
||
|
||
// 添加设备点位
|
||
const addDevicePoint = (x, y, y1) => {
|
||
if (isPointClickStatus.value) return
|
||
devicePoints.value.push({
|
||
xCount: x * 2, // 像素坐标 真实数据
|
||
yCount: y * 2, // 像素坐标 真实数据
|
||
markerX: x, // 直接存储逻辑坐标
|
||
markerY: y, // 直接存储逻辑坐标
|
||
markerY1: y1, // 直接存储逻辑坐标
|
||
markerName: '',
|
||
markerAngle: '', // 角度
|
||
markerPreset: '', // 摄像头预置位
|
||
isAdd: true,
|
||
})
|
||
}
|
||
|
||
// 点击标记的点位
|
||
const handlePointClick = (point, index) => {
|
||
if (point.id) {
|
||
message.warning('点位已存在,无需新增,可右击修改或删除')
|
||
return
|
||
}
|
||
const markerInfo = {
|
||
type: '新增',
|
||
xCount: point.xCount, // 像素坐标 真实数据
|
||
yCount: point.yCount, // 像素坐标 真实数据
|
||
markerIndex: index,
|
||
markerX: point.markerX,
|
||
markerY: point.markerY,
|
||
markerY1: point.markerY1,
|
||
markerName: point.markerName,
|
||
markerAngle: point.markerAngle,
|
||
markerPreset: point.markerPreset,
|
||
isAdd: point.isAdd,
|
||
}
|
||
emits('onHandleAddMarker', markerInfo)
|
||
}
|
||
|
||
// 获取全部已经添加的点位
|
||
const getMarkerListAll = async () => {
|
||
const { data: res } = await getMarkerListAllApi()
|
||
|
||
// console.log(res, 'res全部点位--')
|
||
if (res.code == 200) {
|
||
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,
|
||
isAdd: false,
|
||
})
|
||
})
|
||
}
|
||
}
|
||
}
|
||
|
||
getMarkerListAll()
|
||
|
||
// 关闭模态框
|
||
const onHandleCloseModal = () => {
|
||
emits('onHandleCloseModal')
|
||
}
|
||
|
||
// 监听点击事件以关闭菜单
|
||
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) {
|
||
// 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
|
||
}
|
||
},
|
||
|
||
{
|
||
immediate: true,
|
||
},
|
||
)
|
||
|
||
watch(
|
||
() => props.formType,
|
||
(newVal) => {
|
||
if (newVal == '新增') {
|
||
getMarkerListAll()
|
||
}
|
||
},
|
||
{
|
||
immediate: true,
|
||
},
|
||
)
|
||
|
||
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
|
||
}
|
||
},
|
||
)
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.plane-map-container {
|
||
width: 100%;
|
||
// height: 80vh;
|
||
max-height: 85vh;
|
||
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;
|
||
}
|
||
}
|
||
|
||
// 操作按钮
|
||
.operation-container {
|
||
position: absolute;
|
||
top: 80px;
|
||
right: 60px;
|
||
z-index: 1000;
|
||
gap: 10px;
|
||
}
|
||
}
|
||
|
||
.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;
|
||
}
|
||
}
|
||
}
|
||
</style>
|