259 lines
7.2 KiB
Vue
259 lines
7.2 KiB
Vue
|
|
<template>
|
|||
|
|
<!-- 预置位配置 -->
|
|||
|
|
<DialogModal @onHandleCloseModal="onHandleCloseModal" :modalTitle="modalTitle" :height="`90vh`">
|
|||
|
|
<!-- 平面图操作区域 -->
|
|||
|
|
<div class="plane-map-container" ref="planeMapContainer">
|
|||
|
|
<svg
|
|||
|
|
ref="svgMap"
|
|||
|
|
class="floor-plan"
|
|||
|
|
:viewBox="`${viewBox.x} ${viewBox.y} ${viewBox.width} ${viewBox.height}`"
|
|||
|
|
preserveAspectRatio="xMidYMid meet"
|
|||
|
|
@click="handleMapClick"
|
|||
|
|
>
|
|||
|
|
<!-- 平面图背景 -->
|
|||
|
|
<image
|
|||
|
|
v-if="imgLoaded"
|
|||
|
|
:href="demoImg"
|
|||
|
|
:width="viewBox.width"
|
|||
|
|
:height="viewBox.height"
|
|||
|
|
preserveAspectRatio="xMidYMid slice"
|
|||
|
|
style="object-fit: cover"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 动态渲染标记点 -->
|
|||
|
|
<circle
|
|||
|
|
v-for="(point, index) in points"
|
|||
|
|
:key="index"
|
|||
|
|
:cx="point.x"
|
|||
|
|
:cy="point.y"
|
|||
|
|
r="8"
|
|||
|
|
fill="red"
|
|||
|
|
@click.stop="selectPoint(index)"
|
|||
|
|
@mousedown="startDrag(index, $event)"
|
|||
|
|
/>
|
|||
|
|
|
|||
|
|
<!-- 动态渲染连线 -->
|
|||
|
|
<line
|
|||
|
|
v-for="(line, index) in lines"
|
|||
|
|
:key="'line-' + index"
|
|||
|
|
:x1="line.start.x"
|
|||
|
|
:y1="line.start.y"
|
|||
|
|
:x2="line.end.x"
|
|||
|
|
:y2="line.end.y"
|
|||
|
|
stroke="blue"
|
|||
|
|
stroke-width="2"
|
|||
|
|
/>
|
|||
|
|
</svg>
|
|||
|
|
|
|||
|
|
<div class="map-control-container">
|
|||
|
|
<n-button type="info" @click="handleZoomIn">放大</n-button>
|
|||
|
|
<n-button type="info" @click="handleZoomOut">缩小</n-button>
|
|||
|
|
</div>
|
|||
|
|
</div>
|
|||
|
|
</DialogModal>
|
|||
|
|
</template>
|
|||
|
|
|
|||
|
|
<script setup>
|
|||
|
|
import DialogModal from '@/components/DialogModal/index.vue'
|
|||
|
|
import { refThrottled, useMouseInElement } from '@vueuse/core'
|
|||
|
|
import { ref, onMounted, nextTick, watch } from 'vue'
|
|||
|
|
// import { CashOutline as CashIcon } from '@vicons/ionicons5'
|
|||
|
|
import { NIcon } from 'naive-ui'
|
|||
|
|
import demoImg from '@/assets/demo.png'
|
|||
|
|
|
|||
|
|
const emits = defineEmits(['onHandleCloseModal'])
|
|||
|
|
const modalTitle = ref('预置位配置')
|
|||
|
|
const svgMap = refThrottled(null)
|
|||
|
|
const points = ref([])
|
|||
|
|
const lines = ref([])
|
|||
|
|
const imgLoaded = ref(false)
|
|||
|
|
const imgNaturalSize = ref({ width: 0, height: 0 })
|
|||
|
|
|
|||
|
|
const props = defineProps({
|
|||
|
|
presetSettingVisible: {
|
|||
|
|
type: Boolean,
|
|||
|
|
default: false,
|
|||
|
|
},
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 新增:响应式 viewBox 控制
|
|||
|
|
const viewBox = ref({
|
|||
|
|
x: 0,
|
|||
|
|
y: 0,
|
|||
|
|
width: 0,
|
|||
|
|
height: 0,
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 新增:容器尺寸跟踪
|
|||
|
|
const containerSize = ref({ width: 0, height: 0 })
|
|||
|
|
const planeMapContainer = ref(null)
|
|||
|
|
|
|||
|
|
// 关闭模态框
|
|||
|
|
const onHandleCloseModal = () => {
|
|||
|
|
emits('onHandleCloseModal')
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 拖拽标记点
|
|||
|
|
const startDrag = (index, e) => {
|
|||
|
|
e.preventDefault()
|
|||
|
|
const { x: startX, y: startY } = points.value[index]
|
|||
|
|
const onMove = (moveEvent) => {
|
|||
|
|
const rect = svgMap.value.getBoundingClientRect()
|
|||
|
|
points.value[index] = {
|
|||
|
|
x: moveEvent.clientX - rect.left,
|
|||
|
|
y: moveEvent.clientY - rect.top,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
const onUp = () => {
|
|||
|
|
window.removeEventListener('mousemove', onMove)
|
|||
|
|
window.removeEventListener('mouseup', onUp)
|
|||
|
|
}
|
|||
|
|
window.addEventListener('mousemove', onMove)
|
|||
|
|
window.addEventListener('mouseup', onUp)
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 连线示例(假设连接第一个和第二个点)
|
|||
|
|
const connectPoints = () => {
|
|||
|
|
if (points.value.length >= 2) {
|
|||
|
|
lines.value.push({
|
|||
|
|
start: points.value[0],
|
|||
|
|
end: points.value[1],
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
onMounted(() => {
|
|||
|
|
// 监听容器尺寸变化
|
|||
|
|
nextTick(() => {
|
|||
|
|
const resizeObserver = new ResizeObserver((entries) => {
|
|||
|
|
if (entries[0]) {
|
|||
|
|
containerSize.value = {
|
|||
|
|
width: entries[0].contentRect.width,
|
|||
|
|
height: entries[0].contentRect.height,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
if (planeMapContainer.value) {
|
|||
|
|
resizeObserver.observe(planeMapContainer.value)
|
|||
|
|
}
|
|||
|
|
})
|
|||
|
|
})
|
|||
|
|
|
|||
|
|
// 修改点击事件处理(考虑缩放后的坐标)
|
|||
|
|
const handleMapClick = (e) => {
|
|||
|
|
if (!svgMap.value) return
|
|||
|
|
|
|||
|
|
const pt = svgMap.value.createSVGPoint()
|
|||
|
|
pt.x = e.clientX
|
|||
|
|
pt.y = e.clientY
|
|||
|
|
const svgPt = pt.matrixTransform(svgMap.value.getScreenCTM().inverse())
|
|||
|
|
|
|||
|
|
points.value.push({
|
|||
|
|
x: svgPt.x,
|
|||
|
|
y: svgPt.y,
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
watch(
|
|||
|
|
() => props.presetSettingVisible,
|
|||
|
|
(newVal) => {
|
|||
|
|
if (newVal) {
|
|||
|
|
nextTick(() => {
|
|||
|
|
loadImage()
|
|||
|
|
})
|
|||
|
|
}
|
|||
|
|
},
|
|||
|
|
{
|
|||
|
|
immediate: true,
|
|||
|
|
},
|
|||
|
|
)
|
|||
|
|
|
|||
|
|
const initMapSize = () => {
|
|||
|
|
if (!planeMapContainer.value) return
|
|||
|
|
|
|||
|
|
containerSize.value = {
|
|||
|
|
width: planeMapContainer.value.clientWidth,
|
|||
|
|
height: planeMapContainer.value.clientHeight,
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 初始化 viewBox(根据您的业务逻辑调整)
|
|||
|
|
viewBox.value.width = containerSize.value.width
|
|||
|
|
viewBox.value.height = containerSize.value.height
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 加载图片并获取真实尺寸
|
|||
|
|
const loadImage = () => {
|
|||
|
|
const img = new Image()
|
|||
|
|
img.onload = () => {
|
|||
|
|
imgNaturalSize.value = {
|
|||
|
|
width: img.naturalWidth,
|
|||
|
|
height: img.naturalHeight,
|
|||
|
|
}
|
|||
|
|
imgLoaded.value = true
|
|||
|
|
adjustViewBox()
|
|||
|
|
}
|
|||
|
|
img.onerror = () => {
|
|||
|
|
console.error('图片加载失败,请检查路径:', demoImg)
|
|||
|
|
}
|
|||
|
|
img.src = demoImg // 使用导入的图片路径
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
// 调整viewBox
|
|||
|
|
const adjustViewBox = () => {
|
|||
|
|
if (!planeMapContainer.value || !imgNaturalSize.value.width) return
|
|||
|
|
|
|||
|
|
const container = planeMapContainer.value
|
|||
|
|
const containerRatio = container.clientWidth / container.clientHeight
|
|||
|
|
const imgRatio = imgNaturalSize.value.width / imgNaturalSize.value.height
|
|||
|
|
|
|||
|
|
if (containerRatio > imgRatio) {
|
|||
|
|
// 容器更宽,按高度适配
|
|||
|
|
viewBox.value = {
|
|||
|
|
x: (imgNaturalSize.value.width - imgNaturalSize.value.height * containerRatio) / 2,
|
|||
|
|
y: 0,
|
|||
|
|
width: imgNaturalSize.value.height * containerRatio,
|
|||
|
|
height: imgNaturalSize.value.height,
|
|||
|
|
}
|
|||
|
|
} else {
|
|||
|
|
// 容器更高,按宽度适配
|
|||
|
|
viewBox.value = {
|
|||
|
|
x: 0,
|
|||
|
|
y: (imgNaturalSize.value.height - imgNaturalSize.value.width / containerRatio) / 2,
|
|||
|
|
width: imgNaturalSize.value.width,
|
|||
|
|
height: imgNaturalSize.value.width / containerRatio,
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleZoomIn = () => {
|
|||
|
|
console.log('放大')
|
|||
|
|
viewBox.value.width = viewBox.value.width * 1.1
|
|||
|
|
viewBox.value.height = viewBox.value.height * 1.1
|
|||
|
|
}
|
|||
|
|
|
|||
|
|
const handleZoomOut = () => {
|
|||
|
|
console.log('缩小')
|
|||
|
|
viewBox.value.width = viewBox.value.width * 0.9
|
|||
|
|
viewBox.value.height = viewBox.value.height * 0.9
|
|||
|
|
}
|
|||
|
|
</script>
|
|||
|
|
|
|||
|
|
<style lang="scss" scoped>
|
|||
|
|
.plane-map-container {
|
|||
|
|
width: 100%;
|
|||
|
|
height: 100%;
|
|||
|
|
position: relative;
|
|||
|
|
|
|||
|
|
.map-control-container {
|
|||
|
|
position: absolute;
|
|||
|
|
top: 10px;
|
|||
|
|
right: 10px;
|
|||
|
|
z-index: 1000;
|
|||
|
|
display: flex;
|
|||
|
|
flex-direction: column;
|
|||
|
|
gap: 10px;
|
|||
|
|
}
|
|||
|
|
}
|
|||
|
|
</style>
|