|
@@ -1,1045 +0,0 @@
|
|
|
-<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 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,
|
|
|
- shoeWaterSystem: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='http://localhost: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.55,114.2),//前大往下,后大往左
|
|
|
- zoom: 9,
|
|
|
- minZoom:8.5,
|
|
|
- maxZoom:12,
|
|
|
- renderOptions: {
|
|
|
- preserveDrawingBuffer: true, // 必须开启以支持截图
|
|
|
- antialias: true
|
|
|
- },
|
|
|
- restrictBounds: new TMap.value.LatLngBounds(
|
|
|
- new TMap.value.LatLng(24.8, 113.7), // 西南角(最南最西)
|
|
|
- new TMap.value.LatLng(25.2, 114.0) // 东北角(最北最东)
|
|
|
- )
|
|
|
- })
|
|
|
- //console.log('地图实例创建成功,开始创建markersLayer');
|
|
|
-
|
|
|
- if (markersLayer) {
|
|
|
- markersLayer.setMap(null);
|
|
|
- markersLayer = null;
|
|
|
- }
|
|
|
- // 创建标记点向量图层
|
|
|
- markersLayer = new TMap.value.MultiMarker({
|
|
|
- map: map,
|
|
|
- zIndex:1000,
|
|
|
- styles: {
|
|
|
- default: new TMap.value.MarkerStyle({
|
|
|
- width: 30, // 图标宽度
|
|
|
- height: 30, // 图标高度
|
|
|
- anchor: { x: 12.5, y: 12.5 }, // 居中定位
|
|
|
- src: 'data:image/svg+xml;base64,PHN2ZyB4bWxucz0iaHR0cDovL3d3dy53My5vcmcvMjAwMC9zdmciIHdpZHRoPSIyNCIgaGVpZ2h0PSIyNCIgdmlld0JveD0iMCAwIDI0IDI0Ij48cGF0aCBkPSJNMTIgMTcuMjdsNi4xOCAzLjYzLTEuNjQtNy4wMyA1LjM0LTQuNjMtNy4xOS0uNjFMMTIgM2wtMy4xOSA2LjYzLTcuMTkuNjFMMTAuNDYgMTMuODkgOC44MiAyMC45IDE4IDE3LjI3eiIgZmlsbD0iI0ZGMDAwMCIvPjwvc3ZnPg=='
|
|
|
- })
|
|
|
- }
|
|
|
- });
|
|
|
- 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. 绑定事件
|
|
|
- map.on('click', handleMapClick);
|
|
|
- //console.log('地图实例创建完成,开始加载水系图');
|
|
|
- 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 {
|
|
|
- // 1. 请求 GeoJSON 文件(路径根据实际存放位置修改,如 public 目录下的 water_system.geojson)
|
|
|
- const response = await fetch('/data/韶关市河流水系图.geojson');
|
|
|
- const geojson = await response.json();
|
|
|
- //console.log('水系 GeoJSON 加载成功,要素数量:', geojson.features.length);
|
|
|
-
|
|
|
- // 2. 销毁旧图层(避免重复加载)
|
|
|
- if (waterSystemLayer) {
|
|
|
- waterSystemLayer.setMap(null);
|
|
|
- waterSystemLayer = null;
|
|
|
- }
|
|
|
-
|
|
|
- // 3. 根据 GeoJSON 类型创建图层(水系通常是 LineString,用 MultiPolyline)
|
|
|
- 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 =>{
|
|
|
- const type = feature.geometry.type;
|
|
|
- //console.log('要素类型:', type); // 调试:打印每个要素的类型
|
|
|
- return type === 'LineString' || type === 'MultiLineString';
|
|
|
- }) // 筛选线要素
|
|
|
- .map(feature => {
|
|
|
- let paths = [];
|
|
|
- if (feature.geometry.type === 'LineString') {
|
|
|
- paths = feature.geometry.coordinates.map(coord => {
|
|
|
- const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]); // WGS84 → GCJ02
|
|
|
- return new TMap.value.LatLng(gcjLat, gcjLng);
|
|
|
- });
|
|
|
- } else if (feature.geometry.type === 'MultiLineString') {
|
|
|
- paths = feature.geometry.coordinates.map(line =>
|
|
|
- line.map(coord => {
|
|
|
- const [gcjLng, gcjLat] = wgs84togcj02(coord[0], coord[1]);
|
|
|
- return new TMap.value.LatLng(gcjLat, gcjLng);
|
|
|
- })
|
|
|
- );
|
|
|
-}
|
|
|
- //console.log('转换后的路径长度:', paths.length); // 调试:确保有坐标
|
|
|
- return {
|
|
|
- id: feature.id || `water_${Date.now()}`,
|
|
|
- styleId: 'default',
|
|
|
- paths: paths,
|
|
|
- properties: feature.properties
|
|
|
- };
|
|
|
- })
|
|
|
- });
|
|
|
-
|
|
|
- // console.log('水系图层加载完成');
|
|
|
-
|
|
|
- // 4. 修正:遍历几何要素,合并边界
|
|
|
- if (waterSystemLayer) {
|
|
|
- const geometries = waterSystemLayer.getGeometries(); // 获取所有几何要素
|
|
|
- if (geometries.length === 0) {
|
|
|
- console.warn('水系图层无有效几何要素');
|
|
|
- return;
|
|
|
- }
|
|
|
-
|
|
|
- // 初始化边界为第一个要素的边界
|
|
|
- let bounds = geometries[0].getBounds();
|
|
|
- // 合并剩余要素的边界
|
|
|
- for (let i = 1; i < geometries.length; i++) {
|
|
|
- bounds.extend(geometries[i].getBounds());
|
|
|
- }
|
|
|
-
|
|
|
- // 适配地图视野
|
|
|
- map.fitBounds(bounds, { padding: [50, 50] });
|
|
|
- }
|
|
|
-
|
|
|
- } catch (err) {
|
|
|
- console.error('水系 GeoJSON 加载失败:', err);
|
|
|
- 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 = 'http://localhost: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">重金属含量 (mg/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);
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
- const manageTempMarker = {
|
|
|
- add: (lat, lng, phValue) => {
|
|
|
- if (activeTempMarker.value) {
|
|
|
- markersLayer.remove("-999")
|
|
|
- }
|
|
|
-
|
|
|
- const tempMarker = markersLayer.add({
|
|
|
- id: "-999",
|
|
|
- position: new TMap.value.LatLng(lat, lng),
|
|
|
- styleId: 'temp',
|
|
|
- properties: {
|
|
|
- title: '克里金插值',
|
|
|
- phValue: parseFloat(phValue).toFixed(2),
|
|
|
- isTemp: true
|
|
|
- }
|
|
|
- })
|
|
|
- activeTempMarker.value = tempMarker
|
|
|
- },
|
|
|
- remove: () => {
|
|
|
- if (activeTempMarker.value) {
|
|
|
- markersLayer.remove("-999")
|
|
|
- activeTempMarker.value = null
|
|
|
- }
|
|
|
- }
|
|
|
-}
|
|
|
-
|
|
|
- const handleMapClick = async (e) => {
|
|
|
- if (selectedPolygon.value) {
|
|
|
- resetPolygonStyle();
|
|
|
- infoWindow.value?.close();
|
|
|
- }
|
|
|
- const now = Date.now()
|
|
|
-
|
|
|
- if (now - state.lastTapTime < 1000) return
|
|
|
- state.lastTapTime = now
|
|
|
-
|
|
|
- try {
|
|
|
- const latLng = e?.latLng
|
|
|
- if (!latLng) throw new Error("地图点击事件缺少坐标信息")
|
|
|
-
|
|
|
- const lat = Number(latLng.lat)
|
|
|
- const lng = Number(latLng.lng)
|
|
|
-
|
|
|
- if (!isValidCoordinate(lat, lng)) throw new Error(`非法坐标值 (${lat}, ${lng})`)
|
|
|
-
|
|
|
- //console.log('有效坐标:', lat, lng)
|
|
|
-
|
|
|
- const result = await reverseGeocode(lat, lng)
|
|
|
- if (!validateLocation(result)) throw new Error('非有效陆地区域')
|
|
|
- const phValue = await getPhValue(lng, lat)
|
|
|
-
|
|
|
- // 使用封装方法添加临时标记
|
|
|
- manageTempMarker.add(lat, lng, phValue)
|
|
|
-
|
|
|
- if (infoWindow.value) {
|
|
|
- infoWindow.value.close()
|
|
|
- }
|
|
|
- infoWindow.value = new TMap.value.InfoWindow({
|
|
|
- map: map,
|
|
|
- position: new TMap.value.LatLng(lat,lng),
|
|
|
- content: `
|
|
|
- <div style="padding:12px">
|
|
|
- <h3>临时采样点</h3>
|
|
|
- <p>位置:${result.address}</p>
|
|
|
- <p>PH值:${phValue}</p>
|
|
|
- </div>
|
|
|
- `
|
|
|
- })
|
|
|
- infoWindow.value.open()
|
|
|
- } catch (error) {
|
|
|
- console.error('操作失败详情:', error)
|
|
|
- error.value = error.message.includes('非法坐标')
|
|
|
- ? '请点击有效地图区域'
|
|
|
- : '服务暂时不可用,请稍后重试'
|
|
|
- setTimeout(() => error.value = null, 3000)
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-// // 验证坐标有效性
|
|
|
- const isValidCoordinate = (lat, lng) => {
|
|
|
- return !isNaN(lat) && !isNaN(lng) &&
|
|
|
- lat >= -90 && lat <= 90 &&
|
|
|
- lng >= -180 && lng <= 180
|
|
|
- }
|
|
|
-
|
|
|
-// // 逆地理编码
|
|
|
- const reverseGeocode = (lat, lng) => {
|
|
|
- return new Promise((resolve, reject) => {
|
|
|
- const callbackName = `tmap_callback_${Date.now()}`
|
|
|
- window[callbackName] = (response) => {
|
|
|
- delete window[callbackName]
|
|
|
- document.body.removeChild(script)
|
|
|
- if (response.status !== 0) reject(response.message)
|
|
|
- else resolve(response.result)
|
|
|
- }
|
|
|
-
|
|
|
- const script = document.createElement('script')
|
|
|
- script.src = `https://apis.map.qq.com/ws/geocoder/v1/?location=${lat},${lng}&key=${tMapConfig.key}&output=jsonp&callback=${callbackName}`
|
|
|
- script.onerror = reject
|
|
|
- document.body.appendChild(script)
|
|
|
- })
|
|
|
- }
|
|
|
-
|
|
|
-// // 验证地理位置
|
|
|
- const validateLocation = (result) => {
|
|
|
- if (!result || !result.address_component) {
|
|
|
- return false;
|
|
|
- }
|
|
|
- return result.address_component.nation === '中国' &&
|
|
|
- !['香港特别行政区', '澳门特别行政区', '台湾省'].includes(
|
|
|
- result.address_component.province
|
|
|
- )
|
|
|
- }
|
|
|
-
|
|
|
-// // 获取PH值
|
|
|
- const getPhValue = async (lng, lat) => {
|
|
|
- try {
|
|
|
- const { data } = await axios.post('https://soilgd.com:5000/kriging_interpolation', {
|
|
|
- file_name: 'emissions.xlsx',
|
|
|
- emission_column: 'dust_emissions',
|
|
|
- points: [[lng, lat]]
|
|
|
- })
|
|
|
- return parseFloat(data.interpolated_concentrations[0]).toFixed(2)
|
|
|
- } catch (error) {
|
|
|
- console.error('获取PH值失败:', error)
|
|
|
- throw error
|
|
|
- }
|
|
|
- }
|
|
|
-
|
|
|
-
|
|
|
-
|
|
|
-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>
|
|
|
-.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; }
|
|
|
-}
|
|
|
-
|
|
|
-</style>
|