|
|
@@ -0,0 +1,414 @@
|
|
|
+<!--
|
|
|
+ 标记地图位置弹框(作业票管理用)
|
|
|
+ - 底图来源:地图底图接口 /ehs/mapImage/listEnabled,仅返回启用的底图
|
|
|
+ - 交互:选择底图 -> 在底图上点击放置标记点(再次点击可重新标记)
|
|
|
+ - 每张底图各自独立打点:切换底图不会清除其它底图上的标记
|
|
|
+ - 坐标:标记点按「原图宽高比例」记录(xRate / yRate 取值 0~1),
|
|
|
+ 并附带底图 id、图片原始尺寸,保证底图在不同展示尺寸下标记位置一致
|
|
|
+ - 确认后通过 confirm 事件回传全部标记(数组);一张底图都没有标记时回传 null
|
|
|
+-->
|
|
|
+<template>
|
|
|
+ <ele-modal
|
|
|
+ title="标记地图位置"
|
|
|
+ :visible.sync="visible"
|
|
|
+ width="70vw"
|
|
|
+ append-to-body
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ :maxable="true"
|
|
|
+ @close="handleClose"
|
|
|
+ >
|
|
|
+ <div v-loading="loading" class="map-marker-box">
|
|
|
+ <!-- 底图选择 -->
|
|
|
+ <div class="map-toolbar">
|
|
|
+ <span class="map-toolbar-label">地图底图:</span>
|
|
|
+ <el-select
|
|
|
+ v-model="currentMapId"
|
|
|
+ placeholder="请选择底图"
|
|
|
+ style="width: 260px"
|
|
|
+ :disabled="!mapList.length"
|
|
|
+ @change="onMapChange"
|
|
|
+ >
|
|
|
+ <el-option
|
|
|
+ v-for="item in mapList"
|
|
|
+ :key="item.id"
|
|
|
+ :label="item.name || `底图${item.id}`"
|
|
|
+ :value="item.id"
|
|
|
+ >
|
|
|
+ <span>{{ item.name || `底图${item.id}` }}</span>
|
|
|
+ <span v-if="currentMarkers[item.id]" class="map-option-tag">
|
|
|
+ 已标记
|
|
|
+ </span>
|
|
|
+ </el-option>
|
|
|
+ </el-select>
|
|
|
+ <span class="map-tip">
|
|
|
+ 在底图上点击即可标记作业位置,再次点击可重新标记;每张底图可分别打点
|
|
|
+ </span>
|
|
|
+ <span v-if="markedCount" class="map-count"
|
|
|
+ >已标记 {{ markedCount }} 张底图</span
|
|
|
+ >
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 底图与标记点 -->
|
|
|
+ <div class="map-stage">
|
|
|
+ <div v-if="!loading && !mapList.length" class="map-empty">
|
|
|
+ 暂无启用的地图底图,请先在「地图底图」中配置并启用底图
|
|
|
+ </div>
|
|
|
+ <div
|
|
|
+ v-else
|
|
|
+ class="map-image-wrap"
|
|
|
+ :style="imageBoxStyle"
|
|
|
+ @click="onImageClick"
|
|
|
+ >
|
|
|
+ <img
|
|
|
+ ref="mapImage"
|
|
|
+ :src="currentImageUrl"
|
|
|
+ class="map-image"
|
|
|
+ alt="地图底图"
|
|
|
+ @load="onImageLoad"
|
|
|
+ @error="onImageError"
|
|
|
+ />
|
|
|
+ <div v-if="currentMarker" class="map-pin" :style="pinStyle">
|
|
|
+ <i class="el-icon-location"></i>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div v-if="imageError" class="map-error">
|
|
|
+ 底图图片加载失败,请检查图片地址
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 标记信息 -->
|
|
|
+ <div class="map-info">
|
|
|
+ <template v-if="currentMarker">
|
|
|
+ <span>当前底图:{{ currentMapName || '-' }}</span>
|
|
|
+ <span v-if="pixelText">
|
|
|
+ 原图坐标:X {{ pixelText.x }} / Y {{ pixelText.y }}
|
|
|
+ </span>
|
|
|
+ <span>
|
|
|
+ 比例坐标:X {{ currentMarker.xRate }} / Y {{ currentMarker.yRate }}
|
|
|
+ </span>
|
|
|
+ </template>
|
|
|
+ <span v-else class="map-info-empty">
|
|
|
+ 当前底图尚未标记,请点击底图选择作业位置
|
|
|
+ </span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <template v-slot:footer>
|
|
|
+ <el-button size="small" :disabled="!currentMarker" @click="clearMarker">
|
|
|
+ 清除当前标记
|
|
|
+ </el-button>
|
|
|
+ <el-button size="small" @click="handleClose">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="confirm">确定</el-button>
|
|
|
+ </template>
|
|
|
+ </ele-modal>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script>
|
|
|
+ import { listEnabled } from '@/api/mapImage';
|
|
|
+ import { resolveMapImageUrl } from '../mapMarker';
|
|
|
+
|
|
|
+ export default {
|
|
|
+ name: 'MapMarkerDialog',
|
|
|
+ props: {
|
|
|
+ // 已标记的底图列表,用于回显与修改
|
|
|
+ // 结构:[{ mapImageId, xRate, yRate, mapImageName, imageUrl, imageWidth, imageHeight }]
|
|
|
+ markers: {
|
|
|
+ type: Array,
|
|
|
+ default: () => []
|
|
|
+ }
|
|
|
+ },
|
|
|
+ data() {
|
|
|
+ return {
|
|
|
+ visible: false,
|
|
|
+ loading: false,
|
|
|
+ // 启用的地图底图列表
|
|
|
+ mapList: [],
|
|
|
+ // 当前选中的底图 id
|
|
|
+ currentMapId: '',
|
|
|
+ // 各底图的标记 { [mapImageId]: 标记 },每张底图互不影响
|
|
|
+ currentMarkers: {},
|
|
|
+ // 底图实际渲染尺寸(像素),用于把标记点定位到图片上
|
|
|
+ imageBox: { width: 0, height: 0 },
|
|
|
+ // 底图图片加载失败
|
|
|
+ imageError: false
|
|
|
+ };
|
|
|
+ },
|
|
|
+ computed: {
|
|
|
+ // 当前底图
|
|
|
+ currentMap() {
|
|
|
+ return (
|
|
|
+ this.mapList.find((item) => item.id === this.currentMapId) || null
|
|
|
+ );
|
|
|
+ },
|
|
|
+ // 当前底图上的标记
|
|
|
+ currentMarker() {
|
|
|
+ return this.currentMarkers[this.currentMapId] || null;
|
|
|
+ },
|
|
|
+ // 当前底图名称
|
|
|
+ currentMapName() {
|
|
|
+ return this.currentMap?.name || this.currentMarker?.mapImageName || '';
|
|
|
+ },
|
|
|
+ // 已标记的底图数量
|
|
|
+ markedCount() {
|
|
|
+ return Object.keys(this.currentMarkers).length;
|
|
|
+ },
|
|
|
+ // 当前底图的图片地址
|
|
|
+ currentImageUrl() {
|
|
|
+ const map = this.currentMap;
|
|
|
+ if (!map) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return resolveMapImageUrl(map.imageUrl || map.storePath || '');
|
|
|
+ },
|
|
|
+ // 图片容器的尺寸:与底图渲染尺寸保持一致,标记点才能准确对齐
|
|
|
+ imageBoxStyle() {
|
|
|
+ if (!this.imageBox.width) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ width: this.imageBox.width + 'px',
|
|
|
+ height: this.imageBox.height + 'px'
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // 标记点位置(相对图片容器的百分比)
|
|
|
+ pinStyle() {
|
|
|
+ if (!this.currentMarker) {
|
|
|
+ return {};
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ left: this.currentMarker.xRate * 100 + '%',
|
|
|
+ top: this.currentMarker.yRate * 100 + '%'
|
|
|
+ };
|
|
|
+ },
|
|
|
+ // 原图坐标(像素),底图未提供原始尺寸时不展示
|
|
|
+ pixelText() {
|
|
|
+ const map = this.currentMap || {};
|
|
|
+ const width = Number(map.imageWidth) || 0;
|
|
|
+ const height = Number(map.imageHeight) || 0;
|
|
|
+ if (!this.currentMarker || !width || !height) {
|
|
|
+ return '';
|
|
|
+ }
|
|
|
+ return {
|
|
|
+ x: Math.round(this.currentMarker.xRate * width),
|
|
|
+ y: Math.round(this.currentMarker.yRate * height)
|
|
|
+ };
|
|
|
+ }
|
|
|
+ },
|
|
|
+ mounted() {
|
|
|
+ // 窗口尺寸变化时底图渲染尺寸会变,需要重新测量,保证标记点不错位
|
|
|
+ window.addEventListener('resize', this.measureImage);
|
|
|
+ },
|
|
|
+ beforeDestroy() {
|
|
|
+ window.removeEventListener('resize', this.measureImage);
|
|
|
+ },
|
|
|
+ methods: {
|
|
|
+ // 打开弹框
|
|
|
+ // @param markers 已标记的底图列表(可选),用于回显与修改
|
|
|
+ open(markers) {
|
|
|
+ this.visible = true;
|
|
|
+ this.imageError = false;
|
|
|
+ this.imageBox = { width: 0, height: 0 };
|
|
|
+ this.currentMarkers = this.toMarkerMap(markers);
|
|
|
+ this.loadMapList();
|
|
|
+ },
|
|
|
+ // 标记数组转成按底图 id 索引的对象
|
|
|
+ toMarkerMap(markers) {
|
|
|
+ const map = {};
|
|
|
+ (markers || []).forEach((item) => {
|
|
|
+ if (
|
|
|
+ !item ||
|
|
|
+ item.mapImageId === undefined ||
|
|
|
+ item.mapImageId === null
|
|
|
+ ) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ map[item.mapImageId] = { ...item };
|
|
|
+ });
|
|
|
+ return map;
|
|
|
+ },
|
|
|
+ // 查询启用的地图底图
|
|
|
+ async loadMapList() {
|
|
|
+ this.loading = true;
|
|
|
+ try {
|
|
|
+ const list = await listEnabled();
|
|
|
+ this.mapList = [...(list || [])].sort(
|
|
|
+ (a, b) => Number(a.sortNo || 0) - Number(b.sortNo || 0)
|
|
|
+ );
|
|
|
+ // 优先选中已有标记对应的底图,没有则默认第一张
|
|
|
+ const markedIds = Object.keys(this.currentMarkers);
|
|
|
+ const matched = this.mapList.find((item) =>
|
|
|
+ markedIds.includes(String(item.id))
|
|
|
+ );
|
|
|
+ this.currentMapId = matched
|
|
|
+ ? matched.id
|
|
|
+ : this.mapList.length
|
|
|
+ ? this.mapList[0].id
|
|
|
+ : '';
|
|
|
+ } catch (e) {
|
|
|
+ this.mapList = [];
|
|
|
+ this.currentMapId = '';
|
|
|
+ } finally {
|
|
|
+ this.loading = false;
|
|
|
+ this.$nextTick(this.measureImage);
|
|
|
+ }
|
|
|
+ },
|
|
|
+ // 切换底图:各底图的标记相互独立,切换时不清除已有标记
|
|
|
+ onMapChange() {
|
|
|
+ this.imageError = false;
|
|
|
+ this.imageBox = { width: 0, height: 0 };
|
|
|
+ this.$nextTick(this.measureImage);
|
|
|
+ },
|
|
|
+ // 图片加载完成后测量渲染尺寸
|
|
|
+ onImageLoad() {
|
|
|
+ this.imageError = false;
|
|
|
+ this.$nextTick(this.measureImage);
|
|
|
+ },
|
|
|
+ // 图片加载失败
|
|
|
+ onImageError() {
|
|
|
+ this.imageError = true;
|
|
|
+ },
|
|
|
+ // 测量底图实际渲染尺寸
|
|
|
+ measureImage() {
|
|
|
+ const img = this.$refs.mapImage;
|
|
|
+ if (!img) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const rect = img.getBoundingClientRect();
|
|
|
+ if (!rect.width || !rect.height) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.imageBox = { width: rect.width, height: rect.height };
|
|
|
+ },
|
|
|
+ // 在底图上点击放置标记点(只影响当前底图)
|
|
|
+ onImageClick(event) {
|
|
|
+ const img = this.$refs.mapImage;
|
|
|
+ const map = this.currentMap;
|
|
|
+ if (!img || !map) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const rect = img.getBoundingClientRect();
|
|
|
+ if (!rect.width || !rect.height) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ const xRate = (event.clientX - rect.left) / rect.width;
|
|
|
+ const yRate = (event.clientY - rect.top) / rect.height;
|
|
|
+ if (xRate < 0 || xRate > 1 || yRate < 0 || yRate > 1) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$set(this.currentMarkers, map.id, {
|
|
|
+ mapImageId: map.id,
|
|
|
+ xRate: Number(xRate.toFixed(6)),
|
|
|
+ yRate: Number(yRate.toFixed(6)),
|
|
|
+ mapImageName: map.name || '',
|
|
|
+ imageUrl: map.imageUrl || map.storePath || '',
|
|
|
+ imageWidth: Number(map.imageWidth) || 0,
|
|
|
+ imageHeight: Number(map.imageHeight) || 0
|
|
|
+ });
|
|
|
+ },
|
|
|
+ // 清除当前底图的标记
|
|
|
+ clearMarker() {
|
|
|
+ if (!this.currentMapId) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ this.$delete(this.currentMarkers, this.currentMapId);
|
|
|
+ },
|
|
|
+ // 确认标记:回传全部底图的标记
|
|
|
+ confirm() {
|
|
|
+ const list = Object.values(this.currentMarkers);
|
|
|
+ this.$emit('confirm', list.length ? list : null);
|
|
|
+ this.visible = false;
|
|
|
+ },
|
|
|
+ // 关闭弹框
|
|
|
+ handleClose() {
|
|
|
+ this.visible = false;
|
|
|
+ this.$emit('close');
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+</script>
|
|
|
+
|
|
|
+<style lang="scss" scoped>
|
|
|
+ .map-marker-box {
|
|
|
+ min-height: 320px;
|
|
|
+ }
|
|
|
+ .map-toolbar {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 12px;
|
|
|
+ margin-bottom: 10px;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .map-toolbar-label {
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ }
|
|
|
+ .map-tip {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+ .map-count {
|
|
|
+ font-size: 12px;
|
|
|
+ color: #67c23a;
|
|
|
+ }
|
|
|
+ /* 下拉项上的「已标记」标记 */
|
|
|
+ .map-option-tag {
|
|
|
+ float: right;
|
|
|
+ color: #67c23a;
|
|
|
+ font-size: 12px;
|
|
|
+ }
|
|
|
+ /* 底图展示区:超出时纵向滚动 */
|
|
|
+ .map-stage {
|
|
|
+ position: relative;
|
|
|
+ max-height: 60vh;
|
|
|
+ overflow: auto;
|
|
|
+ padding: 8px;
|
|
|
+ border: 1px solid #ebeef5;
|
|
|
+ border-radius: 4px;
|
|
|
+ background: #f6f7f9;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .map-image-wrap {
|
|
|
+ position: relative;
|
|
|
+ display: inline-block;
|
|
|
+ line-height: 0;
|
|
|
+ cursor: crosshair;
|
|
|
+ }
|
|
|
+ .map-image {
|
|
|
+ display: block;
|
|
|
+ max-width: 100%;
|
|
|
+ max-height: 58vh;
|
|
|
+ }
|
|
|
+ /* 标记点:锚点在标记位置的正下方 */
|
|
|
+ .map-pin {
|
|
|
+ position: absolute;
|
|
|
+ transform: translate(-50%, -100%);
|
|
|
+ color: #f56c6c;
|
|
|
+ font-size: 26px;
|
|
|
+ line-height: 1;
|
|
|
+ pointer-events: none;
|
|
|
+ filter: drop-shadow(0 1px 2px rgba(0, 0, 0, 0.35));
|
|
|
+ }
|
|
|
+ .map-empty,
|
|
|
+ .map-error {
|
|
|
+ padding: 60px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #909399;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+ .map-error {
|
|
|
+ padding: 10px 0;
|
|
|
+ color: #f56c6c;
|
|
|
+ }
|
|
|
+ .map-info {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 20px;
|
|
|
+ margin-top: 10px;
|
|
|
+ font-size: 13px;
|
|
|
+ color: #606266;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+ .map-info-empty {
|
|
|
+ color: #909399;
|
|
|
+ }
|
|
|
+</style>
|