|
|
@@ -1,25 +1,72 @@
|
|
|
<template>
|
|
|
<div ref="mapContainer" class="map-container"></div>
|
|
|
+ <div class="map-reset-btn">
|
|
|
+ <Button type="primary" @click="reset"> 重置 </Button>
|
|
|
+ </div>
|
|
|
</template>
|
|
|
|
|
|
-<script setup>
|
|
|
+<script lang="ts" setup>
|
|
|
import { ref, onMounted, onUnmounted } from 'vue';
|
|
|
// 引入 Leaflet
|
|
|
import L from 'leaflet';
|
|
|
import 'leaflet/dist/leaflet.css';
|
|
|
+ import { getGoafAlarmLevel, getMarkers } from '/@/api/sys/map';
|
|
|
+ import { StatusColorEnum } from '/@/enums/jeecgEnum';
|
|
|
+ import { Button } from 'ant-design-vue';
|
|
|
// Ant Design Vue 图标
|
|
|
-
|
|
|
// --- 1. 组件引用和状态定义 ---
|
|
|
const mapContainer = ref(null);
|
|
|
- let map = null; // Leaflet 地图实例
|
|
|
|
|
|
- // --- 2. 瓦片图层配置 ---
|
|
|
- const tileLayers = {
|
|
|
- custom: {
|
|
|
+ const DEFAULT_LAYER_ID = 'default';
|
|
|
+ // 邮政编码,GeoJSON中存有完整编码,下面是去掉了后两位00的编码
|
|
|
+ const layerMap = new Map<string, L.Layer>([
|
|
|
+ [DEFAULT_LAYER_ID, L.featureGroup()],
|
|
|
+ // 西安
|
|
|
+ ['1566101', L.featureGroup()],
|
|
|
+ // 铜川
|
|
|
+ ['1566102', L.featureGroup()],
|
|
|
+ // 宝鸡
|
|
|
+ ['1566103', L.featureGroup()],
|
|
|
+ // 咸阳
|
|
|
+ ['1566104', L.featureGroup()],
|
|
|
+ // 渭南
|
|
|
+ ['1566105', L.featureGroup()],
|
|
|
+ // 延安
|
|
|
+ ['1566106', L.featureGroup()],
|
|
|
+ // 汉中
|
|
|
+ ['1566107', L.featureGroup()],
|
|
|
+ // 榆林
|
|
|
+ ['1566108', L.featureGroup()],
|
|
|
+ // 安康
|
|
|
+ ['1566109', L.featureGroup()],
|
|
|
+ // 商洛
|
|
|
+ ['1566111', L.featureGroup()],
|
|
|
+ ]);
|
|
|
+ const colorMap = new Map<string, string>([
|
|
|
+ ['1566101', '#14baff'],
|
|
|
+ ['1566102', '#14baff'],
|
|
|
+ ['1566103', '#ffff12'],
|
|
|
+ ['1566104', '#ffff12'],
|
|
|
+ ['1566105', '#ffab12'],
|
|
|
+ ['1566106', '#ffab12'],
|
|
|
+ ['1566107', '#fe1414'],
|
|
|
+ ['1566108', '#fe1414'],
|
|
|
+ ['1566109', '#fe1414'],
|
|
|
+ ['1566111', '#fe1414'],
|
|
|
+ ['810321182c6a40fa92d5acccdba34ce3', '#ffff12'],
|
|
|
+ ['63afd272d0e24c7e9c1475c6498f0c80', '#ffab12'],
|
|
|
+ ]);
|
|
|
+
|
|
|
+ let map: typeof L = null; // Leaflet 地图实例
|
|
|
+
|
|
|
+ // --- 2. 配置项声明 ---
|
|
|
+ const tileLayers: any[] = [
|
|
|
+ {
|
|
|
name: '基准瓦片',
|
|
|
layer: null,
|
|
|
url: 'https://shaanxizhxx.chinamine-safety.gov.cn/zh1/8/33/17.png',
|
|
|
// url: 'https://{s}.basemaps.cartocdn.com/light_all/{z}/{x}/{y}{r}.png',
|
|
|
+ addByDefault: false,
|
|
|
options: {
|
|
|
maxZoom: 18,
|
|
|
// attribution: '',
|
|
|
@@ -27,19 +74,21 @@
|
|
|
// crossOrigin: true,
|
|
|
},
|
|
|
},
|
|
|
- satellite: {
|
|
|
+ {
|
|
|
name: '卫星图',
|
|
|
layer: null,
|
|
|
url: 'https://server.arcgisonline.com/ArcGIS/rest/services/World_Imagery/MapServer/tile/{z}/{y}/{x}',
|
|
|
+ addByDefault: false,
|
|
|
options: {
|
|
|
maxZoom: 18,
|
|
|
attribution: '© Esri',
|
|
|
},
|
|
|
},
|
|
|
- gaode: {
|
|
|
+ {
|
|
|
name: '高德地图',
|
|
|
layer: null,
|
|
|
url: 'https://webrd0{s}.is.autonavi.com/appmaptile?lang=zh_cn&size=1&scale=1&style=8&x={x}&y={y}&z={z}',
|
|
|
+ addByDefault: true,
|
|
|
options: {
|
|
|
className: 'light-blue-theme',
|
|
|
maxZoom: 18,
|
|
|
@@ -47,26 +96,13 @@
|
|
|
attribution: '© 高德地图', // 版权声明(必填,遵守使用规范)
|
|
|
},
|
|
|
},
|
|
|
- };
|
|
|
-
|
|
|
- // 添加一个风险点示例(类似第一张图的红色气泡)
|
|
|
+ ];
|
|
|
|
|
|
// --- 3. 地图初始化 ---
|
|
|
onMounted(() => {
|
|
|
- initMap();
|
|
|
- });
|
|
|
-
|
|
|
- onUnmounted(() => {
|
|
|
- if (map) {
|
|
|
- map.remove();
|
|
|
- }
|
|
|
- });
|
|
|
-
|
|
|
- // 初始化地图
|
|
|
- function initMap() {
|
|
|
if (!mapContainer.value) return;
|
|
|
|
|
|
- // 创建地图实例,设置太原市中心和初始缩放级别
|
|
|
+ // 创建地图实例,设置中心和初始缩放级别
|
|
|
map = L.map(mapContainer.value, {
|
|
|
center: [35.841, 108.94], // 西安市中心坐标 [纬度, 经度]
|
|
|
zoom: 7, // 初始缩放级别,适合城市级别查看
|
|
|
@@ -76,94 +112,218 @@
|
|
|
|
|
|
// 初始化所有瓦片图层
|
|
|
initTileLayers();
|
|
|
+ // 请求矿区数据并初始化标记点图层
|
|
|
+ initMarkers();
|
|
|
+ // 初始化陕西省GeoJSON图层
|
|
|
+ initGeoJSON();
|
|
|
|
|
|
- // 默认显示自定义底图
|
|
|
- tileLayers.gaode.layer.addTo(map);
|
|
|
- // tileLayers.custom.layer.addTo(map);
|
|
|
- // tileLayers.satellite.layer.addTo(map);
|
|
|
+ layerMap.get(DEFAULT_LAYER_ID).addTo(map);
|
|
|
+ });
|
|
|
|
|
|
- // initMarkers();
|
|
|
- initGeoJSON();
|
|
|
- }
|
|
|
+ onUnmounted(() => {
|
|
|
+ map && map.remove();
|
|
|
+ });
|
|
|
|
|
|
// 初始化瓦片图层
|
|
|
function initTileLayers() {
|
|
|
- Object.keys(tileLayers).forEach((key) => {
|
|
|
- const config = tileLayers[key];
|
|
|
+ tileLayers.forEach((config) => {
|
|
|
config.layer = L.tileLayer(config.url, config.options);
|
|
|
+
|
|
|
+ if (config.addByDefault) {
|
|
|
+ config.layer.addTo(map);
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
|
|
|
async function initGeoJSON() {
|
|
|
const response = await fetch('/js/shanxi.geo.json');
|
|
|
const ShanXiGeoJSON = await response.json();
|
|
|
- L.geoJSON(ShanXiGeoJSON, {
|
|
|
- style: function () {
|
|
|
- return { color: '#ff9100' };
|
|
|
- },
|
|
|
- }).addTo(map);
|
|
|
+
|
|
|
+ layerMap.forEach((group, code) => {
|
|
|
+ const features = ShanXiGeoJSON.features.filter((feature) => {
|
|
|
+ if (code === DEFAULT_LAYER_ID) return true;
|
|
|
+ return feature.properties.gb.startsWith(code);
|
|
|
+ });
|
|
|
+
|
|
|
+ const GeoJSON = L.geoJSON(features, {
|
|
|
+ style(feature) {
|
|
|
+ const gb = feature.properties.gb.slice(0, -2);
|
|
|
+ return {
|
|
|
+ fillColor: colorMap.get(gb) || '#14baff',
|
|
|
+ fillOpacity: 0.2,
|
|
|
+ weight: 2,
|
|
|
+ opacity: 1,
|
|
|
+ color: '#ff9100',
|
|
|
+ };
|
|
|
+ },
|
|
|
+ onEachFeature(__, layer) {
|
|
|
+ layer.on({
|
|
|
+ mouseover: (e) => {
|
|
|
+ const layer = e.target;
|
|
|
+
|
|
|
+ layer.setStyle({
|
|
|
+ weight: 2,
|
|
|
+ fillOpacity: 0.6,
|
|
|
+ });
|
|
|
+
|
|
|
+ layer.bringToFront();
|
|
|
+ },
|
|
|
+ mouseout: (e) => {
|
|
|
+ GeoJSON.resetStyle(e.target);
|
|
|
+ },
|
|
|
+ // click: zoomToFeature,
|
|
|
+ });
|
|
|
+ },
|
|
|
+ });
|
|
|
+
|
|
|
+ // layerMap.set(code, GeoJSON);
|
|
|
+ GeoJSON.addTo(group);
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ /** 生成一个标准的标记点,通过传入的数据,请求详情信息并生成监测详情框 */
|
|
|
+ async function generateStandardMarker(marker: any, options: any) {
|
|
|
+ const linkArr = ['断线', '正常', '标校'];
|
|
|
+ const statusArr = ['暂无信息', '低风险', '一般风险', '较高风险', '高风险'];
|
|
|
+ const colorArr = [StatusColorEnum.red, StatusColorEnum.blue, StatusColorEnum.gold, StatusColorEnum.purple, StatusColorEnum.red];
|
|
|
+
|
|
|
+ const tbodyInnerHtml = await getGoafAlarmLevel({ mineCode: marker.mineCode }).then(({ goafDataList, alarmLevel, goafNum }) => {
|
|
|
+ marker.alarmLevel = alarmLevel;
|
|
|
+ marker.goafNum = goafNum;
|
|
|
+
|
|
|
+ return goafDataList.reduce((htmlstr, item, index) => {
|
|
|
+ htmlstr += `
|
|
|
+ <tr>
|
|
|
+ <td>${index + 1}</td>
|
|
|
+ <td>${item.devicePos}</td>
|
|
|
+ <td style="color: ${colorArr[item.alarmLevel || 0]}">${statusArr[item.alarmLevel || 0]}</td>
|
|
|
+ <td style="color: ${colorArr[item.linkStatus || 0]}">${linkArr[item.linkStatus || 0]}</td>
|
|
|
+ </tr>
|
|
|
+ `;
|
|
|
+
|
|
|
+ return htmlstr;
|
|
|
+ }, ``);
|
|
|
+ });
|
|
|
+
|
|
|
+ return L.marker([marker.lat, marker.lng], options).bindPopup(
|
|
|
+ `
|
|
|
+ <div class="leaflet-popup-content__title">${marker.mineName}</div>
|
|
|
+ <div class="leaflet-popup-content__divider"></div>
|
|
|
+ <div class="leaflet-popup-content__board mb-5px">
|
|
|
+ <div class="mr-5px">所属执法处</div>
|
|
|
+ <div>${marker.managementName}</div>
|
|
|
+ </div>
|
|
|
+ <div class="leaflet-popup-content__board mb-5px">
|
|
|
+ <div class="mr-5px">采空区密闭数量</div>
|
|
|
+ <div>${marker.goafNum}</div>
|
|
|
+ </div>
|
|
|
+ <div class="leaflet-popup-content__board">
|
|
|
+ <div class="mr-5px">密闭整体风险等级</div>
|
|
|
+ <div style="color: ${colorArr[marker.alarmLevel]}">${statusArr[marker.alarmLevel]}</div>
|
|
|
+ </div>
|
|
|
+ <div class="leaflet-popup-content__divider"></div>
|
|
|
+ <table id="${marker.id}" class="leaflet-popup-content__table w-full mt-10px">
|
|
|
+ <thead>
|
|
|
+ <tr>
|
|
|
+ <th>序号</th>
|
|
|
+ <th>采空区名称</th>
|
|
|
+ <th>风险等级</th>
|
|
|
+ <th>通讯状态</th>
|
|
|
+ </tr>
|
|
|
+ </thead>
|
|
|
+ <tbody>
|
|
|
+ ${tbodyInnerHtml.length ? tbodyInnerHtml : `<tr><td colspan="4">暂无数据</td></tr>`}
|
|
|
+ </tbody>
|
|
|
+ </table>
|
|
|
+ `
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ function generateRootMarker(marker: any, options: any) {
|
|
|
+ const size = 40;
|
|
|
+ const icon = L.divIcon({
|
|
|
+ className: 'numbered-marker',
|
|
|
+ html: `
|
|
|
+ <svg width="${size}" height="${size}" viewBox="0 0 ${size} ${size}" style="overflow: visible;">
|
|
|
+ <!-- 圆形背景 -->
|
|
|
+ <circle
|
|
|
+ cx="${size / 2}"
|
|
|
+ cy="${size / 2}"
|
|
|
+ r="${size / 2 - 2}"
|
|
|
+ fill="${colorMap.get(marker.gb)}"
|
|
|
+ />
|
|
|
+ <!-- 数字文本 -->
|
|
|
+ <text
|
|
|
+ x="${size / 2}"
|
|
|
+ y="${size / 2}"
|
|
|
+ text-anchor="middle"
|
|
|
+ dominant-baseline="middle"
|
|
|
+ fill="gray"
|
|
|
+ font-size="16"
|
|
|
+ font-weight="bold"
|
|
|
+ font-family="Arial, sans-serif"
|
|
|
+ >${marker.children.length}</text>
|
|
|
+ </svg>
|
|
|
+ `,
|
|
|
+ iconSize: [size, size],
|
|
|
+ iconAnchor: [size / 2, size / 2],
|
|
|
+ popupAnchor: [0, -size / 2],
|
|
|
+ });
|
|
|
+
|
|
|
+ return L.marker([marker.lat, marker.lng], {
|
|
|
+ icon,
|
|
|
+ color: colorMap.get(marker.gb),
|
|
|
+ fillColor: colorMap.get(marker.gb),
|
|
|
+ ...options,
|
|
|
+ }).on('click', () => {
|
|
|
+ layerMap.forEach((group, code) => {
|
|
|
+ map.removeLayer(group);
|
|
|
+
|
|
|
+ if (code === marker.gb) {
|
|
|
+ map.addLayer(group);
|
|
|
+ console.log('debug bounds', group, marker);
|
|
|
+ map.flyToBounds(group.getBounds());
|
|
|
+ }
|
|
|
+ });
|
|
|
+ });
|
|
|
}
|
|
|
|
|
|
- // eslint-disable-next-line no-unused-vars, @typescript-eslint/no-unused-vars
|
|
|
+ /** 请求矿区数据并初始化标记点 */
|
|
|
async function initMarkers() {
|
|
|
- const markers = await Promise.resolve([
|
|
|
- {
|
|
|
- name: '太原',
|
|
|
- lat: 37.857014,
|
|
|
- lng: 112.549248,
|
|
|
- value: 35,
|
|
|
- color: 'red',
|
|
|
- radius: 10,
|
|
|
- type: 'marker',
|
|
|
- },
|
|
|
- ]);
|
|
|
- // const circles = await Promise.resolve([
|
|
|
- // {
|
|
|
- // name: '西安',
|
|
|
- // lat: 34.343161,
|
|
|
- // lng: 108.915024,
|
|
|
- // value: 35,
|
|
|
- // color: 'red',
|
|
|
- // radius: 10,
|
|
|
- // type: 'circle',
|
|
|
- // },
|
|
|
- // ]);
|
|
|
- // circles.forEach((circle) => {
|
|
|
- // L.circleMarker([circle.lat, circle.lng], { radius: circle.radius, color: circle.color, fillColor: circle.color, fillOpacity: 0.5 })
|
|
|
- // .addTo(map)
|
|
|
- // .bindPopup(`${circle.name}: ${circle.value}`);
|
|
|
- // });
|
|
|
- markers.forEach((marker) => {
|
|
|
- L.marker([marker.lat, marker.lng], { title: marker.name })
|
|
|
- .addTo(map)
|
|
|
- .bindPopup(
|
|
|
- `
|
|
|
- <div class="leaflet-popup-content__title">${marker.name}: ${marker.value}</div>
|
|
|
- <div class="leaflet-popup-content__divider"></div>
|
|
|
- <div class="leaflet-popup-content__board w-200px">
|
|
|
- <div>测试字体</div>
|
|
|
- <div>测试数值</div>
|
|
|
- </div>
|
|
|
- <table class="leaflet-popup-content__table w-full mt-10px">
|
|
|
- <thead>
|
|
|
- <tr>
|
|
|
- <th>name</th>
|
|
|
- <th>value</th>
|
|
|
- </tr>
|
|
|
- </thead>
|
|
|
- <tbody>
|
|
|
- <tr>
|
|
|
- <td>测试数据1</td>
|
|
|
- <td>100</td>
|
|
|
- </tr>
|
|
|
- <tr>
|
|
|
- <td>测试数据2</td>
|
|
|
- <td>200</td>
|
|
|
- </tr>
|
|
|
- </tbody>
|
|
|
- </table>
|
|
|
- `
|
|
|
+ const records = await getMarkers({ pageNo: 0, paseSize: 9999 });
|
|
|
+ const details: Promise<any>[] = [];
|
|
|
+ records.forEach((item) => {
|
|
|
+ generateRootMarker(item, {
|
|
|
+ riseOnHover: true,
|
|
|
+ }).addTo(layerMap.get(DEFAULT_LAYER_ID));
|
|
|
+
|
|
|
+ if (!item.children.length) return;
|
|
|
+
|
|
|
+ item.children.forEach((child) => {
|
|
|
+ details.push(
|
|
|
+ generateStandardMarker(child, {
|
|
|
+ riseOnHover: false,
|
|
|
+ }).then((marker) => {
|
|
|
+ if (layerMap.has(child.gb)) {
|
|
|
+ marker.addTo(layerMap.get(child.gb));
|
|
|
+ }
|
|
|
+ })
|
|
|
);
|
|
|
+ });
|
|
|
+ });
|
|
|
+
|
|
|
+ await Promise.all(details);
|
|
|
+ }
|
|
|
+
|
|
|
+ function reset() {
|
|
|
+ layerMap.forEach((layer, code) => {
|
|
|
+ map.removeLayer(layer);
|
|
|
+
|
|
|
+ if (code === DEFAULT_LAYER_ID) {
|
|
|
+ map.addLayer(layer);
|
|
|
+
|
|
|
+ map.flyToBounds(layer.getBounds());
|
|
|
+ }
|
|
|
});
|
|
|
}
|
|
|
</script>
|
|
|
@@ -196,6 +356,8 @@
|
|
|
}
|
|
|
|
|
|
.leaflet-popup-content {
|
|
|
+ margin: 13px 20px;
|
|
|
+ min-width: 280px;
|
|
|
// &__popup {
|
|
|
// }
|
|
|
|
|
|
@@ -227,14 +389,25 @@
|
|
|
}
|
|
|
tbody {
|
|
|
tr {
|
|
|
- padding: 5px;
|
|
|
background-color: @map-popup-table-th-bg;
|
|
|
}
|
|
|
tr:nth-child(even) {
|
|
|
background-color: @map-popup-table-theven-bg;
|
|
|
}
|
|
|
+ td {
|
|
|
+ padding: 5px;
|
|
|
+ }
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
}
|
|
|
+
|
|
|
+ .map-reset-btn {
|
|
|
+ position: absolute;
|
|
|
+ right: 30%; /* 距离右边 20px */
|
|
|
+ z-index: 2; /* 确保在地图控件之上 */
|
|
|
+ top: @header-height;
|
|
|
+ // padding: 10px 15px;
|
|
|
+ margin: 10px 0;
|
|
|
+ }
|
|
|
</style>
|