Files
nl-acs3.0/src/views/nl_agv/layout/components/DeviceMarker.vue
2026-01-21 19:39:18 +08:00

399 lines
8.8 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
class="placed-device"
:class="{ selected: selected }"
:style="deviceStyle"
:data-device-id="device.id"
@click.stop="handleClick"
@dblclick.stop="handleDoubleClick"
@mousedown="handleMouseDown"
>
<div class="device-marker">
<div class="device-marker-icon">
<img
v-if="isImageUrl(device.icon)"
:src="device.icon"
:alt="device.name"
class="device-icon-img"
:style="{ transform: `rotate(${device.angle || 0}deg)` }"
/>
<span
v-else
:style="{
fontSize: iconFontSize + 'px',
transform: `rotate(${device.angle || 0}deg)`,
display: 'inline-block'
}"
>
{{ device.icon || '📍' }}
</span>
</div>
<div class="device-marker-label">{{ device.name }}</div>
</div>
<div class="device-delete-btn" @click.stop="handleDelete">×</div>
<div v-if="selected" class="device-controls">
<div
v-for="direction in ['nw', 'ne', 'sw', 'se']"
:key="direction"
class="resize-handle"
:class="`resize-${direction}`"
:data-direction="direction"
@mousedown.stop="handleResizeStart"
></div>
<div class="rotate-handle" title="拖拽旋转" @mousedown.stop="handleRotateStart">🔄</div>
</div>
</div>
</template>
<script setup>
import { computed, ref } from 'vue'
const props = defineProps({
device: {
type: Object,
required: true
},
mapWidth: {
type: Number,
required: true
},
mapHeight: {
type: Number,
required: true
},
selected: {
type: Boolean,
default: false
}
})
const emit = defineEmits(['select', 'update', 'delete'])
const isDragging = ref(false)
const isResizing = ref(false)
const isRotating = ref(false)
const dragStartPos = ref({ x: 0, y: 0 })
const resizeStartSize = ref(0)
const resizeStartPos = ref({ x: 0, y: 0 })
// 计算设备样式
const deviceStyle = computed(() => {
const leftPercent = (props.device.x / props.mapWidth) * 100
const topPercent = (props.device.y / props.mapHeight) * 100
const size = props.device.size || 50
return {
left: leftPercent + '%',
top: topPercent + '%',
width: size + 'px',
height: size + 'px'
}
})
// 计算图标字体大小
const iconFontSize = computed(() => {
return Math.round((props.device.size || 50) * 0.64)
})
// 判断是否为可直接访问的图片URL排除本地文件路径
const isImageUrl = (str) => {
if (!str) return false
// 只接受 http/https 开头的URL
return str.startsWith('http://') || str.startsWith('https://')
}
// 单击选中
const handleClick = () => {
emit('select', props.device.id)
}
// 双击打开属性面板
const handleDoubleClick = () => {
// 可以通过事件总线或其他方式打开属性面板
console.log('双击设备:', props.device.name)
}
// 删除设备
const handleDelete = () => {
emit('delete', props.device.id)
}
// 开始拖动设备
const handleMouseDown = (e) => {
if (e.target.closest('.resize-handle') || e.target.closest('.rotate-handle')) return
if (e.target.closest('.device-delete-btn')) return
isDragging.value = true
dragStartPos.value = { x: e.clientX, y: e.clientY }
document.addEventListener('mousemove', handleDragMove)
document.addEventListener('mouseup', handleDragEnd)
}
// 拖动设备
const handleDragMove = (e) => {
if (!isDragging.value) return
const deltaX = e.clientX - dragStartPos.value.x
const deltaY = e.clientY - dragStartPos.value.y
// 计算新位置
const mapEl = document.querySelector('.map-image')
if (!mapEl) return
const rect = mapEl.getBoundingClientRect()
const newX = props.device.x + Math.round(deltaX / rect.width * props.mapWidth)
const newY = props.device.y + Math.round(deltaY / rect.height * props.mapHeight)
emit('update', props.device.id, {
x: Math.max(0, Math.min(props.mapWidth, newX)),
y: Math.max(0, Math.min(props.mapHeight, newY))
})
dragStartPos.value = { x: e.clientX, y: e.clientY }
}
// 结束拖动
const handleDragEnd = () => {
isDragging.value = false
document.removeEventListener('mousemove', handleDragMove)
document.removeEventListener('mouseup', handleDragEnd)
}
// 开始缩放
const handleResizeStart = (e) => {
isResizing.value = true
resizeStartSize.value = props.device.size || 50
resizeStartPos.value = { x: e.clientX, y: e.clientY }
document.addEventListener('mousemove', handleResizeMove)
document.addEventListener('mouseup', handleResizeEnd)
}
// 缩放设备
const handleResizeMove = (e) => {
if (!isResizing.value) return
const deltaX = e.clientX - resizeStartPos.value.x
const deltaY = e.clientY - resizeStartPos.value.y
const delta = Math.sqrt(deltaX * deltaX + deltaY * deltaY)
const direction = (deltaX + deltaY) > 0 ? 1 : -1
const newSize = Math.max(10, resizeStartSize.value + delta * direction * 0.5)
emit('update', props.device.id, { size: newSize })
}
// 结束缩放
const handleResizeEnd = () => {
isResizing.value = false
document.removeEventListener('mousemove', handleResizeMove)
document.removeEventListener('mouseup', handleResizeEnd)
}
// 开始旋转
const handleRotateStart = (e) => {
isRotating.value = true
document.addEventListener('mousemove', handleRotateMove)
document.addEventListener('mouseup', handleRotateEnd)
}
// 旋转设备
const handleRotateMove = (e) => {
if (!isRotating.value) return
const deviceEl = e.target.closest('.placed-device')
if (!deviceEl) return
const rect = deviceEl.getBoundingClientRect()
const centerX = rect.left + rect.width / 2
const centerY = rect.top + rect.height / 2
const angle = Math.atan2(e.clientY - centerY, e.clientX - centerX) * 180 / Math.PI
const normalizedAngle = Math.round((angle + 360) % 360)
emit('update', props.device.id, { angle: normalizedAngle })
}
// 结束旋转
const handleRotateEnd = () => {
isRotating.value = false
document.removeEventListener('mousemove', handleRotateMove)
document.removeEventListener('mouseup', handleRotateEnd)
}
</script>
<style scoped lang="less">
.placed-device {
position: absolute;
transform: translate(-50%, -50%);
cursor: move;
pointer-events: auto;
will-change: transform;
&:hover {
z-index: 100;
}
&.selected {
z-index: 101;
}
}
.device-marker {
position: relative;
display: flex;
flex-direction: column;
align-items: center;
width: 100%;
height: 100%;
}
.device-marker-icon {
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
font-size: 32px;
background: transparent;
border: none;
border-radius: 10px;
}
.device-icon-img {
width: 100%;
height: 100%;
object-fit: contain;
pointer-events: none;
user-select: none;
}
.device-marker-label {
position: absolute;
bottom: -25px;
left: 50%;
transform: translateX(-50%);
padding: 4px 10px;
background: rgba(0, 0, 0, 0.8);
border: 1px solid #00ffc8;
border-radius: 4px;
font-size: 12px;
color: #00ffc8;
white-space: nowrap;
pointer-events: none;
z-index: 10;
}
.device-delete-btn {
position: absolute;
top: -10px;
right: -10px;
width: 24px;
height: 24px;
background: #ff6b6b;
border: 2px solid #fff;
border-radius: 50%;
color: #fff;
font-size: 14px;
cursor: pointer;
display: none;
align-items: center;
justify-content: center;
transition: all 0.3s;
&:hover {
background: #ff5252;
transform: scale(1.1);
}
}
.placed-device.selected .device-delete-btn {
display: flex;
}
.device-controls {
display: block;
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: calc(100% + 40px);
height: calc(100% + 40px);
pointer-events: none;
}
.resize-handle {
position: absolute;
width: 12px;
height: 12px;
background: #00ffc8;
border: 2px solid #fff;
border-radius: 50%;
cursor: pointer;
pointer-events: auto;
transition: all 0.2s;
box-shadow: 0 0 10px rgba(0, 255, 200, 0.6);
&:hover {
transform: scale(1.3);
background: #00d4ff;
box-shadow: 0 0 20px rgba(0, 255, 200, 1);
}
&.resize-nw {
top: -6px;
left: -6px;
cursor: nw-resize;
}
&.resize-ne {
top: -6px;
right: -6px;
cursor: ne-resize;
}
&.resize-sw {
bottom: -6px;
left: -6px;
cursor: sw-resize;
}
&.resize-se {
bottom: -6px;
right: -6px;
cursor: se-resize;
}
}
.rotate-handle {
position: absolute;
top: -35px;
left: 50%;
transform: translateX(-50%);
width: 28px;
height: 28px;
background: rgba(255, 107, 107, 0.9);
border: 2px solid #fff;
border-radius: 50%;
cursor: grab;
pointer-events: auto;
display: flex;
align-items: center;
justify-content: center;
font-size: 16px;
transition: all 0.2s;
box-shadow: 0 0 15px rgba(255, 107, 107, 0.6);
&:hover {
transform: translateX(-50%) scale(1.2);
background: rgba(255, 107, 107, 1);
box-shadow: 0 0 25px rgba(255, 107, 107, 1);
}
&:active {
cursor: grabbing;
}
}
</style>