|
|
@@ -0,0 +1,1015 @@
|
|
|
+<template>
|
|
|
+ <div class="map-page">
|
|
|
+ <div ref="mapContainer"
|
|
|
+ class="map-container"
|
|
|
+ ></div>
|
|
|
+ </div>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup>
|
|
|
+import { ref, reactive, onMounted, onBeforeUnmount } from 'vue'
|
|
|
+import axios from 'axios'
|
|
|
+import {wgs84togcj02} from 'coordtransform';
|
|
|
+const farmlandLayer = ref(null);
|
|
|
+const isMapReady = ref(false)
|
|
|
+const mapContainer = ref(null)
|
|
|
+const error = ref(null)
|
|
|
+const TMap = ref(null);
|
|
|
+let districtBoundaryLayer = null;
|
|
|
+let activeTempMarker = ref(null)
|
|
|
+let infoWindow = ref(null)
|
|
|
+let map = null
|
|
|
+let markersLayer = null
|
|
|
+let soilTypeVectorLayer=null;//土壤类型多边形图层
|
|
|
+let waterSystemLayer = null;
|
|
|
+const state = reactive({
|
|
|
+ showOverlay: false,
|
|
|
+ showSoilTypes: true,
|
|
|
+ showSurveyData: true,
|
|
|
+ showWaterSystem:true,
|
|
|
+ excelData: [],//标记点数据
|
|
|
+ lastTapTime: 0
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+const tMapConfig = reactive({
|
|
|
+ key: import.meta.env.VITE_TMAP_KEY, // 请替换为你的开发者密钥
|
|
|
+ geocoderURL: 'https://apis.map.qq.com/ws/geocoder/v1/'
|
|
|
+})
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+let sdkLoaded = false; // 新增:标记 SDK 是否已加载
|
|
|
+const loadSDK = () => {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ if (sdkLoaded) { // 已加载则直接返回
|
|
|
+ resolve(window.TMap);
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ // 移除旧脚本(避免重复加载)
|
|
|
+ const oldScript = document.querySelector('script[src*="map.qq.com"]');
|
|
|
+ if (oldScript) oldScript.remove();
|
|
|
+
|
|
|
+ const script = document.createElement('script');
|
|
|
+ script.src = `https://map.qq.com/api/gljs?v=2.exp&libraries=basic,service,vector&key=${tMapConfig.key}&callback=initTMap`;
|
|
|
+
|
|
|
+ window.initTMap = () => {
|
|
|
+ sdkLoaded = true; // 标记为已加载
|
|
|
+ if (!window.TMap?.service?.Geocoder) {
|
|
|
+ reject(new Error('地图SDK加载失败'));
|
|
|
+ return;
|
|
|
+ }
|
|
|
+ TMap.value = window.TMap;
|
|
|
+ resolve(window.TMap);
|
|
|
+ };
|
|
|
+
|
|
|
+ script.onerror = (err) => {
|
|
|
+ reject(`地图资源加载失败: ${err.message}`);
|
|
|
+ document.head.removeChild(script);
|
|
|
+ };
|
|
|
+ document.head.appendChild(script);
|
|
|
+ });
|
|
|
+};
|
|
|
+
|
|
|
+const WATER_SAMPLING_API='https://www.soilgd.com:3000/table/Water_sampling_data';
|
|
|
+const fetchWaterSamplingData = async ()=>{
|
|
|
+ try{
|
|
|
+ const response = await axios.get(WATER_SAMPLING_API);
|
|
|
+ return response.data.data || response.data;
|
|
|
+ }catch(err){
|
|
|
+ console.error("接口请求失败:",err);
|
|
|
+ throw new Error(`获取水样数据失败:${err.message || '网络错误'}`)
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const initData =async ()=>{
|
|
|
+ try{
|
|
|
+ const rawData = await fetchWaterSamplingData();
|
|
|
+ if(!Array.isArray(rawData)){
|
|
|
+ throw new Error('接口返回数据格式错误');
|
|
|
+ }
|
|
|
+
|
|
|
+ state.excelData = rawData.map(item=>{
|
|
|
+ const lat=Number(item.latitude);
|
|
|
+ const lng=Number(item.longitude);
|
|
|
+
|
|
|
+ if(isNaN(lat)||isNaN(lng)){
|
|
|
+ console.error('无效经纬度数据',item);
|
|
|
+ return null;
|
|
|
+ }
|
|
|
+
|
|
|
+ return{
|
|
|
+ ...item,
|
|
|
+ latitude:lat,
|
|
|
+ longitude:lng,
|
|
|
+ };
|
|
|
+ }).filter(item=>item !==null)
|
|
|
+ console.log(`成功加载${state.excelData.length}条有效数据`);
|
|
|
+ }catch(err){
|
|
|
+ console.error('数据初始化失败:',err);
|
|
|
+ error.value = err.message;
|
|
|
+ state.excelData=[];
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 初始化地图
|
|
|
+const initMap = async () => {
|
|
|
+ try {
|
|
|
+ if (map) {
|
|
|
+ map.destroy();
|
|
|
+ map = null;
|
|
|
+ }
|
|
|
+ await loadSDK()
|
|
|
+ //console.log('开始创建地图实例');
|
|
|
+
|
|
|
+ map = new TMap.value.Map(mapContainer.value, {
|
|
|
+ center: new TMap.value.LatLng(24.25,114.5),//前大往下,后大往左
|
|
|
+ zoom:9,
|
|
|
+ zoomControl:true,
|
|
|
+ renderOptions: {
|
|
|
+ preserveDrawingBuffer: true, // 必须开启以支持截图
|
|
|
+ antialias: true
|
|
|
+ },
|
|
|
+ })
|
|
|
+ //console.log('地图实例创建成功,开始创建markersLayer');
|
|
|
+ //console.log('当前地图样式ID:', map.getMapStyleId());
|
|
|
+ if (markersLayer) {
|
|
|
+ markersLayer.setMap(null);
|
|
|
+ markersLayer = null;
|
|
|
+ }
|
|
|
+ // 创建标记点向量图层
|
|
|
+ markersLayer = new TMap.value.MultiMarker({
|
|
|
+ map: map,
|
|
|
+ zIndex:1000,
|
|
|
+ styles: {
|
|
|
+ default: new TMap.value.MarkerStyle({
|
|
|
+ width: 15, // 图标宽度
|
|
|
+ height: 15, // 图标高度
|
|
|
+ anchor: { x: 12.5, y: 12.5 }, // 居中定位
|
|
|
+ src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIzMCIgaGVpZ2h0PSIzMCI+PGNpcmNsZSBjeD0iMTUiIGN5PSIxNSIgcj0iMTAiIGZpbGw9InJlZCIvPjwvc3ZnPg=='
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+ console.log('markersLayer是否绑定地图:',markersLayer.getMap() === map);
|
|
|
+
|
|
|
+
|
|
|
+ // 创建土壤类型多边形图层
|
|
|
+ soilTypeVectorLayer = new TMap.value.MultiPolygon({
|
|
|
+ map: map,
|
|
|
+ styles: {
|
|
|
+ default: new TMap.value.PolygonStyle({
|
|
|
+ fillColor: '#cccccc',
|
|
|
+ fillOpacity: 0.4,
|
|
|
+ strokeColor: '#333',
|
|
|
+ strokeWidth: 1
|
|
|
+ })
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ if (typeof handleMarkerClick === 'function' && markersLayer) {
|
|
|
+ markersLayer.on('click', handleMarkerClick);
|
|
|
+ console.log('[地图] 标记点点击事件绑定成功');
|
|
|
+ }
|
|
|
+ await initData()
|
|
|
+ updateMarkers()
|
|
|
+ // 在updateMarkers()后执行
|
|
|
+ // console.log(markersLayer.getStyles());
|
|
|
+ //console.log(document.querySelector('.tmap-marker img'));
|
|
|
+
|
|
|
+
|
|
|
+ // 6. 绑定事件
|
|
|
+
|
|
|
+ //console.log('地图实例创建完成,开始加载水系图');
|
|
|
+ //加载区县边界
|
|
|
+ await loadDistrictBoundaries();
|
|
|
+ await loadWaterSystemGeoJSON(); // 等待水系图加载完成
|
|
|
+
|
|
|
+ // 标记地图就绪
|
|
|
+ isMapReady.value = true;
|
|
|
+ //console.log('地图初始化完成(含水系图)');
|
|
|
+
|
|
|
+
|
|
|
+ // 新增地图就绪状态监听
|
|
|
+ map.on('idle', () => {
|
|
|
+ isMapReady.value = true;
|
|
|
+ //console.log('地图初始化完成');
|
|
|
+ //console.log('标记点图层初始化:',markersLayer.value);
|
|
|
+ })
|
|
|
+ } catch (err) {
|
|
|
+ isMapReady.value = true;
|
|
|
+ console.error('initMap执行异常:',err);
|
|
|
+ error.value = err.message
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+// 加载水系 GeoJSON 并添加到地图
|
|
|
+const loadWaterSystemGeoJSON = async () => {
|
|
|
+ try {
|
|
|
+ const url = `${window.location.origin}/data/韶关市河流水系图.geojson`;
|
|
|
+ console.log('加载水系:', url);
|
|
|
+
|
|
|
+ const response = await fetch(url);
|
|
|
+
|
|
|
+ // 检查响应状态
|
|
|
+ if (!response.ok) {
|
|
|
+ const errorText = await response.text();
|
|
|
+ throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const geojson = await response.json();
|
|
|
+ console.log('成功加载水系GeoJSON:', geojson.features.length, '个要素');
|
|
|
+
|
|
|
+ // 创建腾讯地图可用的坐标转换函数
|
|
|
+ const processCoordinates = (coords) => {
|
|
|
+ const [gcjLng, gcjLat] = wgs84togcj02(coords[0], coords[1]);
|
|
|
+ return new TMap.value.LatLng(gcjLat, gcjLng);
|
|
|
+ };
|
|
|
+
|
|
|
+ // 销毁旧图层
|
|
|
+ if (waterSystemLayer) {
|
|
|
+ waterSystemLayer.setMap(null);
|
|
|
+ waterSystemLayer = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建水系图层
|
|
|
+ waterSystemLayer = new TMap.value.MultiPolyline({
|
|
|
+ map: map,
|
|
|
+ styles: {
|
|
|
+ default: new TMap.value.PolylineStyle({
|
|
|
+ color: '#0066cc',
|
|
|
+ width: 2,
|
|
|
+ opacity: 0.8,
|
|
|
+ lineCap: 'round',
|
|
|
+ lineJoin: 'round'
|
|
|
+ })
|
|
|
+ },
|
|
|
+ geometries: geojson.features
|
|
|
+ .filter(feature => {
|
|
|
+ return feature.geometry.type === 'LineString' ||
|
|
|
+ feature.geometry.type === 'MultiLineString';
|
|
|
+ })
|
|
|
+ .map(feature => {
|
|
|
+ let paths = [];
|
|
|
+
|
|
|
+ if (feature.geometry.type === 'LineString') {
|
|
|
+ paths = feature.geometry.coordinates.map(processCoordinates);
|
|
|
+ } else {
|
|
|
+ paths = feature.geometry.coordinates.map(line =>
|
|
|
+ line.map(processCoordinates)
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: feature.id || `water_${Date.now()}`,
|
|
|
+ styleId: 'default',
|
|
|
+ paths: paths,
|
|
|
+ properties: feature.properties
|
|
|
+ };
|
|
|
+ })
|
|
|
+ });
|
|
|
+ console.log('水系图层加载完成');
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('水系加载失败:', err.message);
|
|
|
+ error.value = `水系图加载失败: ${err.message}`;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 加载区县边界数据
|
|
|
+const loadDistrictBoundaries = async () => {
|
|
|
+ try {
|
|
|
+ const url = '/data/韶关市各区县边界图.geojson';
|
|
|
+ console.log('加载区县边界:', url);
|
|
|
+
|
|
|
+ const response = await fetch(url);
|
|
|
+
|
|
|
+ // 打印响应状态和头信息
|
|
|
+ console.log('HTTP状态码:', response.status);
|
|
|
+ console.log('内容类型:', response.headers.get('content-type'));
|
|
|
+ console.log('内容长度:', response.headers.get('content-length'));
|
|
|
+
|
|
|
+ // 检查响应状态
|
|
|
+ if (!response.ok) {
|
|
|
+ const errorText = await response.text();
|
|
|
+ throw new Error(`HTTP错误 ${response.status}: ${errorText.substring(0, 100)}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const geojson = await response.json();
|
|
|
+ console.log('成功加载区县GeoJSON:', geojson.features.length, '个要素');
|
|
|
+
|
|
|
+ // 1. 定义颜色数组(顺序与 geojson.features 中的区县顺序一致)
|
|
|
+ const districtColorMap = {
|
|
|
+ '武江区': '#FF6B6B',
|
|
|
+ '浈江区': '#4ECDC4',
|
|
|
+ '曲江区': '#FFD166',
|
|
|
+ '始兴县': '#A0DAA9',
|
|
|
+ '仁化县': '#6A0572',
|
|
|
+ '翁源县': '#1A535C',
|
|
|
+ '乳源瑶族自治县': '#FF9F1C', // 修正:匹配 GeoJSON 的“乳源瑶族自治县”
|
|
|
+ '新丰县': '#87CEEB', // 新增:为“新丰县”配置颜色(可自定义)
|
|
|
+ '乐昌市': '#118AB2',
|
|
|
+ '南雄市': '#06D6A0'
|
|
|
+ };
|
|
|
+
|
|
|
+ // 2. 处理几何数据:为每个区县分配 styleId(用索引,与颜色数组对应)
|
|
|
+ const geometries = geojson.features.map(feature => {
|
|
|
+ const districtName = feature.properties.name;
|
|
|
+ console.log('GEOJSON中的区县名称',districtName);
|
|
|
+
|
|
|
+ const color = districtColorMap[districtName] ||'#ccc';
|
|
|
+ // 坐标转换(WGS84 → GCJ02,确保边界在正确位置)
|
|
|
+ let paths = [];
|
|
|
+ if (feature.geometry.type === 'Polygon') {
|
|
|
+ paths = feature.geometry.coordinates.map(ring =>
|
|
|
+ ring.map(coord => {
|
|
|
+ const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
|
|
|
+ return new TMap.value.LatLng(gcjLat, gcjLng);
|
|
|
+ })
|
|
|
+ );
|
|
|
+ } else if (feature.geometry.type === 'MultiPolygon') {
|
|
|
+ paths = feature.geometry.coordinates.map(polygon =>
|
|
|
+ polygon.map(ring =>
|
|
|
+ ring.map(coord => {
|
|
|
+ const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
|
|
|
+ return new TMap.value.LatLng(gcjLat, gcjLng);
|
|
|
+ })
|
|
|
+ )
|
|
|
+ );
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关键:styleId 设为索引(与颜色数组索引对应)
|
|
|
+ return {
|
|
|
+ id: `district-${districtName}`,
|
|
|
+ styleId: `style-${districtName}`,
|
|
|
+ paths:paths
|
|
|
+ };
|
|
|
+ });
|
|
|
+
|
|
|
+ districtColorMap['武江区'] = '#FF0000'; // 强制武江区为红色
|
|
|
+ // 3. 构建样式对象(key 与 styleId 一致)
|
|
|
+ const styles = {};
|
|
|
+ for(const name in districtColorMap){
|
|
|
+ styles[`style-${name}`]=new TMap.value.PolygonStyle({
|
|
|
+ fillColor:districtColorMap[name],
|
|
|
+ fillOpacity:0.7,
|
|
|
+ strokeColor:'#333',
|
|
|
+ strokeWidth:2
|
|
|
+ })
|
|
|
+ console.log(`区县${name}的样式颜色:`, styles[`style-${name}`].getFillColor());
|
|
|
+ }
|
|
|
+
|
|
|
+ // 4. 创建图层并应用样式
|
|
|
+ districtBoundaryLayer = new TMap.value.MultiPolygon({
|
|
|
+ map: map,
|
|
|
+ geometries: geometries,
|
|
|
+ styles:styles
|
|
|
+ });
|
|
|
+ console.log('区县样式对象:', styles);
|
|
|
+ districtBoundaryLayer.setStyles(styles); // 样式生效
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('加载区县边界失败:', err.message);
|
|
|
+ error.value = `区县边界加载失败: ${err.message}`;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 更新标记点,添加Label显示
|
|
|
+const updateMarkers = () => {
|
|
|
+ // 正确的标记点创建方式
|
|
|
+ const geometries = state.excelData.map(item => {
|
|
|
+ // console.log(`'原始ID:'"${item.water_sample_ID}"`);
|
|
|
+ //console.log(`坐标验证:lat=${item.latitude},lng=${item.longitude}`);
|
|
|
+
|
|
|
+ return {
|
|
|
+ id: item.water_sample_ID,
|
|
|
+ styleId: 'default',
|
|
|
+ position: new TMap.value.LatLng( item.latitude,item.longitude),
|
|
|
+ properties: {
|
|
|
+ title: item.sampling_location,
|
|
|
+ sampler_id:item.water_sample_ID,
|
|
|
+ }
|
|
|
+ };
|
|
|
+ })
|
|
|
+
|
|
|
+ // 一次性设置所有标记
|
|
|
+ markersLayer.setGeometries(geometries);
|
|
|
+};
|
|
|
+
|
|
|
+const API_BASE_URL = 'https://www.soilgd.com:3000/table/Water_assay_data';
|
|
|
+
|
|
|
+// 新增Marker点击事件处理
|
|
|
+const handleMarkerClick = async(e) => {
|
|
|
+ //console.log('点击事件已发生');
|
|
|
+
|
|
|
+ const marker = e.geometry;
|
|
|
+ const markerId=marker.id.trim();
|
|
|
+
|
|
|
+ if (!marker) {
|
|
|
+ //console.error('未获取到标记点对象');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 关闭之前的信息窗口
|
|
|
+ if (infoWindow.value) {
|
|
|
+ infoWindow.value.close();
|
|
|
+ infoWindow.value=null;
|
|
|
+ }
|
|
|
+ // 显示加载中的信息窗口
|
|
|
+ infoWindow.value = new TMap.value.InfoWindow({
|
|
|
+ map: map,
|
|
|
+ position: marker.position,
|
|
|
+ content: '<div style="padding:12px;text-align:center">加载数据中...</div>',
|
|
|
+ // offset: { x: 0, y: -32 }
|
|
|
+ });
|
|
|
+ infoWindow.value.open();
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 调试信息:显示当前点击的标记点ID
|
|
|
+ //console.log('点击标记点ID:', markerId);
|
|
|
+ //console.log('请求URL:', `${API_BASE_URL}?water_sample_ID=eq.${markerId}`);
|
|
|
+
|
|
|
+ // 调用API获取水质数据 - 使用 markerId 而不是 marker.id
|
|
|
+ const response = await axios.get(API_BASE_URL, {
|
|
|
+ params: {
|
|
|
+ water_sample_ID: `eq.${markerId}`
|
|
|
+ },
|
|
|
+ timeout: 5000
|
|
|
+ });
|
|
|
+
|
|
|
+ //console.log('API响应数据:', response.data);
|
|
|
+
|
|
|
+ // 关键:手动筛选出 water_sample_ID 匹配的第一条数据
|
|
|
+ const matchedData = response.data.find(item =>
|
|
|
+ item.water_sample_ID.trim() === markerId
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!matchedData) {
|
|
|
+ throw new Error(`未找到采样点 ${markerId} 的监测数据`);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 获取第一条数据
|
|
|
+ const apiData = matchedData;
|
|
|
+
|
|
|
+ // 调试信息:显示获取到的数据ID
|
|
|
+ //console.log('获取到的水质数据ID:', apiData.water_sample_ID);
|
|
|
+
|
|
|
+ // 创建信息窗口内容 - 使用 marker.properties.title 确保显示正确位置
|
|
|
+ const content = `
|
|
|
+ <div class="water-info-window">
|
|
|
+ <!-- 标题区 -->
|
|
|
+ <h3 class="info-title">${marker.properties.title}</h3>
|
|
|
+
|
|
|
+ <!-- 基础信息区 -->
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">采样点ID:</span>
|
|
|
+ <span class="info-value">${apiData.water_sample_ID}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">样本编号:</span>
|
|
|
+ <span class="info-value">${apiData.sample_code || '无'}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-row">
|
|
|
+ <span class="info-label">pH值:</span>
|
|
|
+ <span class="info-value">${apiData.pH}</span>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 分隔线 -->
|
|
|
+ <div class="divider"></div>
|
|
|
+
|
|
|
+ <!-- 重金属区 -->
|
|
|
+ <h4 class="contaminant-title">重金属含量 (ug/L)</h4>
|
|
|
+ <div class="contaminant-grid">
|
|
|
+ <div class="contaminant-item">
|
|
|
+ <span class="contaminant-name">Cr:</span>
|
|
|
+ <span class="contaminant-value">${apiData.Cr}</span>
|
|
|
+ </div>
|
|
|
+ <div class="contaminant-item">
|
|
|
+ <span class="contaminant-name">As:</span>
|
|
|
+ <span class="contaminant-value">${apiData.As}</span>
|
|
|
+ </div>
|
|
|
+ <div class="contaminant-item">
|
|
|
+ <span class="contaminant-name">Cd:</span>
|
|
|
+ <span class="contaminant-value">${apiData.Cd}</span>
|
|
|
+ </div>
|
|
|
+ <div class="contaminant-item">
|
|
|
+ <span class="contaminant-name">Hg:</span>
|
|
|
+ <span class="contaminant-value">${apiData.Hg}</span>
|
|
|
+ </div>
|
|
|
+ <div class="contaminant-item">
|
|
|
+ <span class="contaminant-name">Pb:</span>
|
|
|
+ <span class="contaminant-value">${apiData.Pb}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+`;
|
|
|
+
|
|
|
+ // 更新信息窗口
|
|
|
+ infoWindow.value.setContent(content);
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('API请求失败:', error);
|
|
|
+
|
|
|
+ // 显示错误信息
|
|
|
+ const errorContent = `
|
|
|
+ <div style="padding:12px;color:red">
|
|
|
+ <h3>${marker.properties.title}</h3>
|
|
|
+ <p>获取数据失败: ${error.message}</p>
|
|
|
+ <p>尝试获取的ID: ${markerId}</p>
|
|
|
+ </div>
|
|
|
+ `;
|
|
|
+
|
|
|
+ infoWindow.value.setContent(errorContent);
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+onMounted(async () => {
|
|
|
+ //console.log('开始执行 onMounted');
|
|
|
+
|
|
|
+ try {
|
|
|
+ await loadSDK();
|
|
|
+ //console.log('SDK加载完成,开始initData');
|
|
|
+ await initMap()
|
|
|
+ //console.log('initMap执行完毕');
|
|
|
+
|
|
|
+ } catch (err) {
|
|
|
+ console.error('onMounted执行异常',err);
|
|
|
+ error.value = err.message
|
|
|
+ }
|
|
|
+})
|
|
|
+
|
|
|
+onBeforeUnmount(() => {
|
|
|
+ // 1. 销毁地图实例(先销毁,再置空)
|
|
|
+ if (map) {
|
|
|
+ try {
|
|
|
+ map.destroy(); // 腾讯地图销毁方法
|
|
|
+ // console.log('[地图] 地图实例已销毁');
|
|
|
+ } catch (e) {
|
|
|
+ console.error('[地图] 销毁失败:', e);
|
|
|
+ }
|
|
|
+ map = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 2. 销毁图层(逐个检查)
|
|
|
+ const layers = [markersLayer, soilTypeVectorLayer, waterSystemLayer];
|
|
|
+ layers.forEach(layer => {
|
|
|
+ if (layer) {
|
|
|
+ try {
|
|
|
+ layer.setMap(null); // 从地图移除
|
|
|
+ if (layer.destroy) layer.destroy(); // 调用图层销毁方法
|
|
|
+ } catch (e) {
|
|
|
+ console.error('[地图] 图层销毁失败:', e);
|
|
|
+ }
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ // 3. 清理全局变量
|
|
|
+ if (window.initTMap) {
|
|
|
+ delete window.initTMap; // 移除全局回调
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+onUpdated(() => {
|
|
|
+ try {
|
|
|
+ if (map.value && farmlandLayer.value) {
|
|
|
+ // 更新地图视图
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error("地图更新错误:", error);
|
|
|
+ }
|
|
|
+});
|
|
|
+
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.map-page {
|
|
|
+ position: relative;
|
|
|
+ width: 100vw;
|
|
|
+ height: 100vh;
|
|
|
+}
|
|
|
+
|
|
|
+.map-container {
|
|
|
+ width: 100%;
|
|
|
+ height: 100vh ;
|
|
|
+ min-height: 600px;
|
|
|
+ pointer-events: all;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel {
|
|
|
+ position: fixed;
|
|
|
+ top: 24px;
|
|
|
+ right: 24px;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ padding: 16px;
|
|
|
+ border-radius: 12px;
|
|
|
+ box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1);
|
|
|
+ backdrop-filter: blur(8px);
|
|
|
+ border: 1px solid rgba(255, 255, 255, 0.2);
|
|
|
+ z-index: 1000;
|
|
|
+ min-width: 240px;
|
|
|
+ transition: all 0.3s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel:hover {
|
|
|
+ box-shadow: 0 12px 40px rgba(0, 0, 0, 0.15);
|
|
|
+ transform: translateY(-2px);
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel label {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ border-radius: 8px;
|
|
|
+ transition: background 0.2s ease;
|
|
|
+ cursor: pointer;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel label:hover {
|
|
|
+ background: rgba(56, 118, 255, 0.05);
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel input[type="checkbox"] {
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ border: 2px solid #3876ff;
|
|
|
+ border-radius: 4px;
|
|
|
+ appearance: none;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+}
|
|
|
+
|
|
|
+.control-panel input[type="checkbox"]:checked {
|
|
|
+ background: #3876ff url("data:image/svg+xml,%3Csvg xmlns='http://www.w3.org/2000/svg' width='12' height='12' viewBox='0 0 24 24'%3E%3Cpath fill='%23fff' d='M9 16.17L4.83 12l-1.42 1.41L9 19 21 7l-1.41-1.41z'/%3E%3C/svg%3E") no-repeat center;
|
|
|
+ background-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.export-controls {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+ margin-top: 16px;
|
|
|
+}
|
|
|
+
|
|
|
+.export-controls button {
|
|
|
+ padding: 10px 16px;
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: 500;
|
|
|
+ border: none;
|
|
|
+ border-radius: 8px;
|
|
|
+ cursor: pointer;
|
|
|
+ transition: all 0.2s ease;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 8px;
|
|
|
+ background: #3876ff;
|
|
|
+ color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.export-controls button:disabled {
|
|
|
+ background: #e0e0e0;
|
|
|
+ color: #9e9e9e;
|
|
|
+ cursor: not-allowed;
|
|
|
+ opacity: 0.8;
|
|
|
+}
|
|
|
+
|
|
|
+.export-controls button:not(:disabled):hover {
|
|
|
+ background: #2b5dc5;
|
|
|
+ box-shadow: 0 4px 12px rgba(56, 118, 255, 0.3);
|
|
|
+}
|
|
|
+
|
|
|
+/* 新增加载动画 */
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 18px;
|
|
|
+ height: 18px;
|
|
|
+ border: 2px solid rgba(255, 255, 255, 0.3);
|
|
|
+ border-top-color: white;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 0.8s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+/* 响应式调整 */
|
|
|
+@media (max-width: 768px) {
|
|
|
+ .control-panel {
|
|
|
+ top: 16px;
|
|
|
+ right: 16px;
|
|
|
+ left: 16px;
|
|
|
+ width: auto;
|
|
|
+ min-width: auto;
|
|
|
+ }
|
|
|
+
|
|
|
+ .export-controls {
|
|
|
+ flex-direction: row;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ }
|
|
|
+
|
|
|
+ .export-controls button {
|
|
|
+ flex: 1;
|
|
|
+ justify-content: center;
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+.polygon-info {
|
|
|
+ padding: 12px;
|
|
|
+ max-width: 300px;
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ color: #333;
|
|
|
+ font-size: 16px;
|
|
|
+ }
|
|
|
+
|
|
|
+ table {
|
|
|
+ width: 100%;
|
|
|
+ border-collapse: collapse;
|
|
|
+
|
|
|
+ tr {
|
|
|
+ border-bottom: 1px solid #eee;
|
|
|
+ }
|
|
|
+
|
|
|
+ th, td {
|
|
|
+ padding: 6px 4px;
|
|
|
+ text-align: left;
|
|
|
+ font-size: 14px;
|
|
|
+ }
|
|
|
+
|
|
|
+ th {
|
|
|
+ color: #666;
|
|
|
+ white-space: nowrap;
|
|
|
+ padding-right: 8px;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+.point-info {
|
|
|
+ padding: 12px;
|
|
|
+ min-width: 200px;
|
|
|
+
|
|
|
+ h3 {
|
|
|
+ margin: 0 0 8px;
|
|
|
+ font-size: 14px;
|
|
|
+ color: white;
|
|
|
+ padding: 4px 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ display: inline-block;
|
|
|
+ background: var(--category-color);
|
|
|
+ }
|
|
|
+
|
|
|
+ p {
|
|
|
+ margin: 6px 0;
|
|
|
+ font-size: 13px;
|
|
|
+ line-height: 1.4;
|
|
|
+
|
|
|
+ &:last-child {
|
|
|
+ margin-bottom: 0;
|
|
|
+ }
|
|
|
+ }
|
|
|
+}
|
|
|
+
|
|
|
+
|
|
|
+.tooltip {
|
|
|
+ position: absolute;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background: rgba(255, 255, 255, 0.9);
|
|
|
+ border-radius: 6px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0, 0, 0, 0.2);
|
|
|
+ z-index: 1001;
|
|
|
+ font-size: 14px;
|
|
|
+ white-space: nowrap;
|
|
|
+ opacity: 0;
|
|
|
+ transform: translateY(10px);
|
|
|
+ visibility: hidden;
|
|
|
+ transition: opacity 0.2s, transform 0.2s, visibility 0.2s;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+.tooltip.visible {
|
|
|
+ opacity: 1;
|
|
|
+ transform: translateY(0);
|
|
|
+ visibility: visible;
|
|
|
+}
|
|
|
+
|
|
|
+.tooltip::after {
|
|
|
+ content: "";
|
|
|
+ position: absolute;
|
|
|
+ width: 0;
|
|
|
+ height: 0;
|
|
|
+ border-left: 6px solid transparent;
|
|
|
+ border-right: 6px solid transparent;
|
|
|
+ top: 100%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translateX(-50%);
|
|
|
+ border-top: 6px solid rgba(255, 255, 255, 0.9);
|
|
|
+ border-top-color: inherit;
|
|
|
+}
|
|
|
+
|
|
|
+:deep(.tmap-vector-label) {
|
|
|
+ white-space: nowrap;
|
|
|
+ pointer-events: none; /* 允许点击穿透,不影响地图交互 */
|
|
|
+}
|
|
|
+
|
|
|
+/* 在style标签中添加以下样式 */
|
|
|
+:deep(.tmap-infowindow) {
|
|
|
+ padding: 12px;
|
|
|
+ min-width: 300px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.15);
|
|
|
+ background-color: white;
|
|
|
+}
|
|
|
+
|
|
|
+.db-info {
|
|
|
+ margin-top: 10px;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: #f8f9fa;
|
|
|
+ border-left: 3px solid #4285f4;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.db-info h4 {
|
|
|
+ margin-top: 0;
|
|
|
+ color: #4285f4;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.db-info pre {
|
|
|
+ margin: 5px 0 0;
|
|
|
+ font-size: 12px;
|
|
|
+ white-space: pre-wrap;
|
|
|
+ word-break: break-word;
|
|
|
+}
|
|
|
+
|
|
|
+.water-info-window {
|
|
|
+ font-family: 'Segoe UI', Tahoma, sans-serif;
|
|
|
+ background: #fff;
|
|
|
+ border-radius: 4px;
|
|
|
+ padding: 4px;
|
|
|
+ width: 200px;
|
|
|
+ height:auto;
|
|
|
+ border: 1px solid #e2e8f0;
|
|
|
+ box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05);
|
|
|
+ font-size: 0.7rem; /* 基础字体大小调整为0.7rem(约11px) */
|
|
|
+}
|
|
|
+
|
|
|
+.info-title {
|
|
|
+ color: #1e40af;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ margin: 0 0 3px 0;
|
|
|
+ padding-bottom: 2px;
|
|
|
+ border-bottom: 1px solid #e0f2fe;
|
|
|
+ font-weight: 600;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+
|
|
|
+.info-content {
|
|
|
+ padding: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-row {
|
|
|
+ display: flex;
|
|
|
+ margin-bottom: 2px;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.info-label {
|
|
|
+ flex: 0 0 60px; /* 标签宽度调整为60px */
|
|
|
+ color: #475569;
|
|
|
+ font-weight: 500;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+}
|
|
|
+
|
|
|
+.info-value {
|
|
|
+ flex: 1;
|
|
|
+ color: #0f172a;
|
|
|
+ padding: 1px 3px;
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 2px;
|
|
|
+ overflow: hidden;
|
|
|
+ text-overflow: ellipsis;
|
|
|
+ white-space: nowrap;
|
|
|
+ font-size: 0.7rem;
|
|
|
+}
|
|
|
+
|
|
|
+.contaminant-section {
|
|
|
+ margin-top: 3px;
|
|
|
+ padding-top: 3px;
|
|
|
+ border-top: 1px dotted #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.contaminant-title {
|
|
|
+ color: #1e40af;
|
|
|
+ margin: 0 0 2px 0;
|
|
|
+ font-size: 0.7rem;
|
|
|
+ font-weight: 500;
|
|
|
+ padding-left: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+/* 污染物改为网格布局,每行3个 */
|
|
|
+.contaminants {
|
|
|
+ display: grid;
|
|
|
+ grid-template-columns: repeat(3, 1fr);
|
|
|
+ gap: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.contaminant-item {
|
|
|
+ background: #f8fafc;
|
|
|
+ border-radius: 2px;
|
|
|
+ padding: 2px;
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ align-items: center;
|
|
|
+ border: 0.5px solid #e2e8f0;
|
|
|
+}
|
|
|
+
|
|
|
+.contaminant-name {
|
|
|
+ color: #3b82f6;
|
|
|
+ font-weight: 500;
|
|
|
+ font-size: 0.7rem;
|
|
|
+ white-space: nowrap;
|
|
|
+ max-width: 100%;
|
|
|
+}
|
|
|
+
|
|
|
+.contaminant-value {
|
|
|
+ color: #0f172a;
|
|
|
+ font-size: 0.8rem;
|
|
|
+ background: #e2e8f0;
|
|
|
+ padding: 1px 2px;
|
|
|
+ border-radius: 2px;
|
|
|
+ margin-top: 1px;
|
|
|
+ min-width: 25px;
|
|
|
+ text-align: center;
|
|
|
+}
|
|
|
+ .assay-info {
|
|
|
+ margin-top: 16px;
|
|
|
+ padding: 8px 12px;
|
|
|
+ background-color: #f5f5f5;
|
|
|
+ border-radius: 6px;
|
|
|
+ font-size: 0.85rem;
|
|
|
+ color: #666;
|
|
|
+ text-align: center;
|
|
|
+ }
|
|
|
+
|
|
|
+ /* 在style标签中添加 */
|
|
|
+.crystal-bubble .bubble {
|
|
|
+ width: 24px;
|
|
|
+ height: 24px;
|
|
|
+ border-radius: 50%;
|
|
|
+ background: radial-gradient(circle at 30% 30%, #00b4ff, #0077cc);
|
|
|
+ box-shadow:
|
|
|
+ 0 0 10px rgba(0, 183, 255, 0.7),
|
|
|
+ inset 0 0 15px rgba(0, 100, 200, 0.5);
|
|
|
+ position: relative;
|
|
|
+ animation: pulse 1.5s infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.crystal-bubble .water-drop {
|
|
|
+ position: absolute;
|
|
|
+ width: 10px;
|
|
|
+ height: 10px;
|
|
|
+ background: rgba(255, 255, 255, 0.85);
|
|
|
+ border-radius: 50%;
|
|
|
+ top: 25%;
|
|
|
+ left: 25%;
|
|
|
+ box-shadow:
|
|
|
+ 0 0 5px #fff,
|
|
|
+ inset 0 0 3px rgba(0, 0, 0, 0.2);
|
|
|
+ transform: rotate(-20deg);
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes pulse {
|
|
|
+ 0% { transform: scale(1); opacity: 0.8; }
|
|
|
+ 50% { transform: scale(1.1); opacity: 1; }
|
|
|
+ 100% { transform: scale(1); opacity: 0.8; }
|
|
|
+}
|
|
|
+
|
|
|
+/* 区县边界样式 */
|
|
|
+.district-boundary {
|
|
|
+ stroke: #333;
|
|
|
+ stroke-width: 1px;
|
|
|
+ fill-opacity: 0.6;
|
|
|
+ transition: fill-opacity 0.3s;
|
|
|
+}
|
|
|
+
|
|
|
+.district-boundary:hover {
|
|
|
+ fill-opacity: 0.8;
|
|
|
+ stroke-width: 2px;
|
|
|
+}
|
|
|
+
|
|
|
+.district-label {
|
|
|
+ font-size: 14px;
|
|
|
+ font-weight: bold;
|
|
|
+ text-anchor: middle;
|
|
|
+ pointer-events: none;
|
|
|
+ fill: #333;
|
|
|
+ text-shadow: 0 0 3px white, 0 0 3px white, 0 0 3px white;
|
|
|
+}
|
|
|
+</style>
|