|
|
@@ -0,0 +1,889 @@
|
|
|
+<template>
|
|
|
+ <el-card class="map-card">
|
|
|
+ <div class="title">
|
|
|
+ <div class="section-icon">🗺️</div>
|
|
|
+ <p class="map-title">乐昌市三种用地类型分布地图</p>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <div id="map-container" class="map-container">
|
|
|
+ <div v-if="mapLoading" class="loading">
|
|
|
+ <div class="loading-spinner"></div>
|
|
|
+ <span>地图加载中...</span>
|
|
|
+ </div>
|
|
|
+ <div v-if="mapError" class="error-tip">
|
|
|
+ <el-alert title="地图加载失败" type="error" show-icon>
|
|
|
+ <template #description>
|
|
|
+ <p>请检查GeoServer服务和配置</p>
|
|
|
+ <el-button @click="reloadMap" type="primary">重试加载</el-button>
|
|
|
+ </template>
|
|
|
+ </el-alert>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+
|
|
|
+ <!-- 信息弹窗 -->
|
|
|
+ <div v-if="showPopup" :style="popupStyle" class="feature-popup">
|
|
|
+ <div class="popup-content">
|
|
|
+ <div class="popup-header">
|
|
|
+ <h4>地块信息</h4>
|
|
|
+ <button @click="closePopup" class="close-btn">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="popup-body">
|
|
|
+ <div v-if="featureInfo.loading" class="loading-info">加载中...</div>
|
|
|
+ <div v-else-if="featureInfo.error" class="error-info">获取信息失败</div>
|
|
|
+ <div v-else-if="featureInfo.data" class="feature-info">
|
|
|
+ <div class="info-item">
|
|
|
+ <label>所属村:</label>
|
|
|
+ <span>{{ featureInfo.data.village || '未知' }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <label>用地类型:</label>
|
|
|
+ <span>{{ featureInfo.data.landType || '未知' }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预测结果弹窗 -->
|
|
|
+ <div v-if="showPredictionPopup" :style="predictionPopupStyle" class="prediction-popup">
|
|
|
+ <div class="popup-content">
|
|
|
+ <div class="popup-header">
|
|
|
+ <h4>土壤预测结果</h4>
|
|
|
+ <button @click="closePredictionPopup" class="close-btn">×</button>
|
|
|
+ </div>
|
|
|
+ <div class="popup-body">
|
|
|
+ <div v-if="predictionLoading" class="loading-info">
|
|
|
+ <div class="loading-spinner small"></div>
|
|
|
+ <span>预测中...</span>
|
|
|
+ </div>
|
|
|
+ <div v-else-if="predictionError" class="error-info">
|
|
|
+ {{ predictionError }}
|
|
|
+ </div>
|
|
|
+ <div v-else-if="predictionResult" class="prediction-result">
|
|
|
+ <div class="result-item">
|
|
|
+ <label>最近点位信息:</label>
|
|
|
+ <div class="point-info">
|
|
|
+ <div>经度: {{ predictionResult.nearest_point?.lon }}</div>
|
|
|
+ <div>纬度: {{ predictionResult.nearest_point?.lan }}</div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ <div class="result-item" v-if="currentPredictionType === 'reduction' && predictionResult.prediction_model33 !== undefined">
|
|
|
+ <label>降酸预测结果:</label>
|
|
|
+ <span class="prediction-value reduction">每亩地土壤表层20cm撒{{ formatPredictionValue(predictionResult.prediction_model33)}} 吨</span>
|
|
|
+ </div>
|
|
|
+ <!--<div class="result-item" v-if="currentPredictionType === 'inversion' && predictionResult.prediction_model24 !== undefined">
|
|
|
+ <label>反酸预测结果:</label>
|
|
|
+ <span class="prediction-value inversion">ph值{{ formatPredictionValue(predictionResult.prediction_model24) }} </span>
|
|
|
+ <div v-if="predictionResult.warnings" class="warnings">
|
|
|
+ <div v-for="(warning, index) in filteredWarnings" :key="index" class="warning-item">
|
|
|
+ {{ warning }}
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>-->
|
|
|
+
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 新增:降酸预测参数输入弹窗 -->
|
|
|
+ <el-dialog
|
|
|
+ v-model="showAcidReductionInput"
|
|
|
+ title="降酸预测参数输入"
|
|
|
+ width="350px"
|
|
|
+ :close-on-click-modal="false"
|
|
|
+ class="acid-reduction-dialog"
|
|
|
+ >
|
|
|
+ <el-form :model="acidReductionParams" :rules="acidReductionRules" ref="acidReductionFormRef" label-width="100px">
|
|
|
+ <el-form-item label="targetPH" prop="targetPH">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.targetPH"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="请输入目标pH相关参数(0-14)"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!--平均为34.04685185-->
|
|
|
+ <el-form-item label="NO3" prop="no3">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.no3"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="请输入NO3特征值(非负)"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <!--平均为7.872182305-->
|
|
|
+ <el-form-item label="CEC" prop="cec">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.cec"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="请输入CEC特征值(非负)"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <template #footer>
|
|
|
+ <el-button @click="showAcidReductionInput = false">取消</el-button>
|
|
|
+ <el-button type="primary" @click="confirmAcidReduction">确认预测</el-button>
|
|
|
+ </template>
|
|
|
+ </el-dialog>
|
|
|
+
|
|
|
+
|
|
|
+
|
|
|
+ </el-card>
|
|
|
+</template>
|
|
|
+
|
|
|
+<script setup lang="ts">
|
|
|
+import {reactive, ref, nextTick, onMounted ,onUnmounted, computed} from "vue";
|
|
|
+import { ElMessage } from "element-plus";
|
|
|
+import type { LeafletMouseEvent } from "leaflet";
|
|
|
+import type { FormInstance } from "element-plus";
|
|
|
+
|
|
|
+// 地图状态
|
|
|
+const mapLoading = ref(true);
|
|
|
+const mapError = ref(false);
|
|
|
+const map = ref<any>(null);
|
|
|
+const showPopup =ref(false);
|
|
|
+const popupStyle = reactive({
|
|
|
+ left:'0px',
|
|
|
+ top:'0px'
|
|
|
+})
|
|
|
+// 预测弹窗状态
|
|
|
+const showPredictionPopup = ref(false);
|
|
|
+const predictionPopupStyle = reactive({
|
|
|
+ left: '0px',
|
|
|
+ top: '0px'
|
|
|
+});
|
|
|
+const predictionLoading = ref(false);
|
|
|
+const predictionError = ref('');
|
|
|
+const predictionResult = ref<any>(null);
|
|
|
+
|
|
|
+const disableMouseEvents = ref(false);
|
|
|
+
|
|
|
+
|
|
|
+let L:any = null;
|
|
|
+
|
|
|
+// 地块信息状态
|
|
|
+const featureInfo = reactive({
|
|
|
+ loading: false,
|
|
|
+ error: false,
|
|
|
+ data: null as any
|
|
|
+});
|
|
|
+
|
|
|
+// 当前点击的坐标
|
|
|
+const currentClickCoords = reactive({
|
|
|
+ lng: 0,
|
|
|
+ lat: 0
|
|
|
+});
|
|
|
+
|
|
|
+const currentPredictionType = ref<'reduction' | 'inversion' | null>(null);
|
|
|
+// 新增:降酸预测参数输入相关
|
|
|
+const showAcidReductionInput = ref(false); // 输入弹窗显示状态
|
|
|
+const acidReductionFormRef = ref<FormInstance | null>(null); // 表单引用,用于校验
|
|
|
+// 输入参数(对应后端需要的Q_delete_pH、NO3、CEC)
|
|
|
+const acidReductionParams = reactive({
|
|
|
+ targetPH: 0, // 默认值(可调整)
|
|
|
+ no3: 0, // 默认值
|
|
|
+ cec: 0 // 默认值
|
|
|
+});
|
|
|
+
|
|
|
+// 过滤警告信息,排除模型24的错误
|
|
|
+const filteredWarnings = computed(() => {
|
|
|
+ if (!predictionResult.value?.warnings) return [];
|
|
|
+
|
|
|
+ return predictionResult.value.warnings.filter((warning: string) => {
|
|
|
+ // 过滤掉模型24相关的错误信息
|
|
|
+ return !warning.includes('模型24');
|
|
|
+ });
|
|
|
+});
|
|
|
+
|
|
|
+// 输入校验规则
|
|
|
+const acidReductionRules = reactive({
|
|
|
+ targetPH: [
|
|
|
+ { required: true, message: '请输入目标ph值', trigger: 'blur' },
|
|
|
+ { type: 'number', message: '请输入有效数字', trigger: 'blur' },
|
|
|
+ {
|
|
|
+ validator: (rule: any, value: number, callback: any) => {
|
|
|
+ if (value < 0 || value > 14) {
|
|
|
+ callback(new Error('值范围在0-14之间'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ no3: [
|
|
|
+ { required: true, message: '请输入NO3', trigger: 'blur' },
|
|
|
+ { type: 'number', message: '请输入有效数字', trigger: 'blur' },
|
|
|
+ {
|
|
|
+ validator: (rule: any, value: number, callback: any) => {
|
|
|
+ if (value < 0) {
|
|
|
+ callback(new Error('值不能为负数'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ],
|
|
|
+ cec: [
|
|
|
+ { required: true, message: '请输入CEC', trigger: 'blur' },
|
|
|
+ { type: 'number', message: '请输入有效数字', trigger: 'blur' },
|
|
|
+ {
|
|
|
+ validator: (rule: any, value: number, callback: any) => {
|
|
|
+ if (value < 0) {
|
|
|
+ callback(new Error('值不能为负数'));
|
|
|
+ } else {
|
|
|
+ callback();
|
|
|
+ }
|
|
|
+ },
|
|
|
+ trigger: 'blur'
|
|
|
+ }
|
|
|
+ ]
|
|
|
+});
|
|
|
+const predictionTitle = computed(() => {
|
|
|
+ if (currentPredictionType.value === 'reduction') return '降酸预测结果';
|
|
|
+ if (currentPredictionType.value === 'inversion') return '反酸预测结果';
|
|
|
+ return '土壤预测结果';
|
|
|
+});
|
|
|
+
|
|
|
+
|
|
|
+// 防抖计时器
|
|
|
+let hoverTimer: any = null;
|
|
|
+
|
|
|
+// 格式化数字显示
|
|
|
+const formatNumber = (value: any): string => {
|
|
|
+ if (value === null || value === undefined) return '无数据';
|
|
|
+ const num = Number(value);
|
|
|
+ return isNaN(num) ? '无效数据' : num.toFixed(6);
|
|
|
+};
|
|
|
+
|
|
|
+// 格式化预测值
|
|
|
+const formatPredictionValue = (value: any): string => {
|
|
|
+ console.log('预测值原始数据:', value, '类型:', typeof value);
|
|
|
+
|
|
|
+ if (value === null || value === undefined) return '无数据';
|
|
|
+
|
|
|
+ // 如果是数组,取第一个元素
|
|
|
+ if (Array.isArray(value)) {
|
|
|
+ if (value.length === 0) return '无数据';
|
|
|
+ const num = Number(value[0]);
|
|
|
+ return isNaN(num) ? '无效数据' : num.toFixed(4);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 如果是对象,尝试转换为数字
|
|
|
+ if (typeof value === 'object') {
|
|
|
+ console.warn('预测值是对象类型:', value);
|
|
|
+ return '数据格式错误';
|
|
|
+ }
|
|
|
+
|
|
|
+ // 直接转换为数字
|
|
|
+ const num = Number(value);
|
|
|
+ return isNaN(num) ? '无效数据' : num.toFixed(4);
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 获取地块信息的函数
|
|
|
+const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
|
|
|
+ if (!map.value) return false;
|
|
|
+
|
|
|
+ featureInfo.loading = true;
|
|
|
+ featureInfo.error = false;
|
|
|
+ featureInfo.data = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ const GEOSERVER_CONFIG = {
|
|
|
+ url: "/geoserver/wms",
|
|
|
+ workspace: "acidmap",
|
|
|
+ layerGroup: "mapwithboundary",
|
|
|
+ };
|
|
|
+
|
|
|
+ const bounds = map.value.getBounds();
|
|
|
+ const size = map.value.getSize();
|
|
|
+
|
|
|
+ // 使用URLSearchParams构建查询参数
|
|
|
+ const params = new URLSearchParams();
|
|
|
+ params.append('service', 'WMS');
|
|
|
+ params.append('version', '1.1.1');
|
|
|
+ params.append('request', 'GetFeatureInfo');
|
|
|
+ params.append('layers', `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`);
|
|
|
+ params.append('query_layers', `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`);
|
|
|
+ params.append('info_format', 'application/json');
|
|
|
+ params.append('feature_count', '10');
|
|
|
+ params.append('x', Math.round(point.x).toString());
|
|
|
+ params.append('y', Math.round(point.y).toString());
|
|
|
+ params.append('width', size.x.toString());
|
|
|
+ params.append('height', size.y.toString());
|
|
|
+ params.append('srs', 'EPSG:4326');
|
|
|
+ params.append('bbox', `${bounds.getWest()},${bounds.getSouth()},${bounds.getEast()},${bounds.getNorth()}`);
|
|
|
+
|
|
|
+ const url = `${GEOSERVER_CONFIG.url}?${params.toString()}`;
|
|
|
+
|
|
|
+ const response = await fetch(url);
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+
|
|
|
+ // 检查是否有要素返回
|
|
|
+ if (data.features && data.features.length > 0) {
|
|
|
+ const properties = data.features[0].properties;
|
|
|
+
|
|
|
+ // 检查是否有有效的地块信息(根据您的字段调整)
|
|
|
+ const hasValidData = properties.QSDWMC || properties.DLMC;
|
|
|
+
|
|
|
+ if (hasValidData) {
|
|
|
+ featureInfo.data = {
|
|
|
+ village: properties.QSDWMC,
|
|
|
+ landType: properties.DLMC,
|
|
|
+ };
|
|
|
+ return true; // 找到有效地块
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ return false; // 没有找到有效地块
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('获取地块信息失败:', error);
|
|
|
+ featureInfo.error = true;
|
|
|
+ return false;
|
|
|
+ } finally {
|
|
|
+ featureInfo.loading = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 调用8000后端预测接口
|
|
|
+const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?: {
|
|
|
+ targetPH: number;
|
|
|
+ no3: number;
|
|
|
+ cec: number;
|
|
|
+ } ) => {
|
|
|
+ predictionLoading.value = true;
|
|
|
+ predictionError.value = '';
|
|
|
+ predictionResult.value = null;
|
|
|
+
|
|
|
+ disableMouseEvents.value = true;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 构建URL参数
|
|
|
+ const urlParams = new URLSearchParams();
|
|
|
+ urlParams.append('target_lon', lng.toString());
|
|
|
+ urlParams.append('target_lan', lat.toString());
|
|
|
+ // 如果是降酸预测,添加3个新增参数
|
|
|
+ if (acidReductionParams) {
|
|
|
+ urlParams.append('target_pH', acidReductionParams.targetPH.toString());
|
|
|
+ urlParams.append('NO3', acidReductionParams.no3.toString());
|
|
|
+ urlParams.append('CEC', acidReductionParams.cec.toString());
|
|
|
+ }
|
|
|
+
|
|
|
+ const response = await fetch(
|
|
|
+ `http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
|
|
|
+ );
|
|
|
+
|
|
|
+ if (!response.ok) {
|
|
|
+ throw new Error(`HTTP error! status: ${response.status}`);
|
|
|
+ }
|
|
|
+
|
|
|
+ const data = await response.json();
|
|
|
+ predictionResult.value = data;
|
|
|
+
|
|
|
+ //console.log('完整的预测结果:', data); // 调试用,查看返回的数据结构
|
|
|
+ //console.log('模型24预测结果:', data.prediction_model24, '类型:', typeof data.prediction_model24);
|
|
|
+
|
|
|
+ // 显示预测弹窗
|
|
|
+ showPredictionPopup.value = true;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('调用预测接口失败:', error);
|
|
|
+ predictionError.value = `预测失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
|
+ ElMessage.error('预测请求失败,请检查后端服务');
|
|
|
+
|
|
|
+ disableMouseEvents.value = false;
|
|
|
+ } finally {
|
|
|
+ predictionLoading.value = false;
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 降酸预测按钮点击
|
|
|
+const handleAcidReductionPrediction = (lng: number, lat: number) => {
|
|
|
+ currentPredictionType.value = 'reduction';
|
|
|
+ currentClickCoords.lng = lng;
|
|
|
+ currentClickCoords.lat = lat;
|
|
|
+ showAcidReductionInput.value = true;
|
|
|
+};
|
|
|
+
|
|
|
+// 新增:确认降酸预测(输入完成后调用)
|
|
|
+const confirmAcidReduction = async () => {
|
|
|
+console.log(acidReductionParams);
|
|
|
+ // 表单校验
|
|
|
+ if (!acidReductionFormRef.value) return;
|
|
|
+ try {
|
|
|
+ await acidReductionFormRef.value.validate(); // 校验输入是否合法
|
|
|
+ // 校验通过,调用接口(传递输入的3个参数)
|
|
|
+ await callPredictionAPI(
|
|
|
+ currentClickCoords.lng,
|
|
|
+ currentClickCoords.lat,
|
|
|
+ {
|
|
|
+ targetPH: acidReductionParams.targetPH,
|
|
|
+ no3: acidReductionParams.no3,
|
|
|
+ cec: acidReductionParams.cec
|
|
|
+ }
|
|
|
+ );
|
|
|
+ // 关闭输入弹窗
|
|
|
+ showAcidReductionInput.value = false;
|
|
|
+ } catch (error) {
|
|
|
+ // 校验失败,不调用接口
|
|
|
+ ElMessage.error('输入参数不合法,请检查后重试');
|
|
|
+ console.error('表单校验失败:', error);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+// 反酸预测按钮点击
|
|
|
+const handleAcidInversionPrediction = (lng: number, lat: number) => {
|
|
|
+ currentPredictionType.value = 'inversion';
|
|
|
+ callPredictionAPI(lng, lat);
|
|
|
+};
|
|
|
+
|
|
|
+
|
|
|
+// 鼠标移动事件处理
|
|
|
+const handleMouseMove = (e: any) => {
|
|
|
+
|
|
|
+ // 如果禁用了鼠标事件,直接返回
|
|
|
+ if (disableMouseEvents.value) {
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (hoverTimer) {
|
|
|
+ clearTimeout(hoverTimer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 设置防抖,避免频繁请求
|
|
|
+ hoverTimer = setTimeout(async () => {
|
|
|
+ const containerPoint = map.value.latLngToContainerPoint(e.latlng);
|
|
|
+
|
|
|
+ // 先隐藏弹窗
|
|
|
+ showPopup.value = false;
|
|
|
+ featureInfo.data = null;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 尝试获取地块信息
|
|
|
+ const hasFeature = await getFeatureInfo(e.latlng, containerPoint);
|
|
|
+
|
|
|
+ // 只有在有地块信息时才显示弹窗
|
|
|
+ if (hasFeature) {
|
|
|
+ // 更新弹窗位置
|
|
|
+ popupStyle.left = `${containerPoint.x + 10}px`;
|
|
|
+ popupStyle.top = `${containerPoint.y + 10}px`;
|
|
|
+
|
|
|
+ // 显示弹窗
|
|
|
+ showPopup.value = true;
|
|
|
+ }
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理地块信息失败:', error);
|
|
|
+ // 出错时不显示弹窗
|
|
|
+ showPopup.value = false;
|
|
|
+ }
|
|
|
+ }, 200);
|
|
|
+};
|
|
|
+
|
|
|
+// 鼠标离开地图事件
|
|
|
+const handleMouseOut = () => {
|
|
|
+ if (hoverTimer) {
|
|
|
+ clearTimeout(hoverTimer);
|
|
|
+ }
|
|
|
+ showPopup.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭弹窗
|
|
|
+const closePopup = () => {
|
|
|
+ showPopup.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+// 关闭预测弹窗
|
|
|
+const closePredictionPopup = () => {
|
|
|
+ showPredictionPopup.value = false;
|
|
|
+ predictionResult.value = null;
|
|
|
+ currentPredictionType.value = null;
|
|
|
+
|
|
|
+ // 重新启用鼠标事件
|
|
|
+ disableMouseEvents.value = false;
|
|
|
+};
|
|
|
+
|
|
|
+const initMap = async () => {
|
|
|
+ mapLoading.value = true;
|
|
|
+ mapError.value = false;
|
|
|
+
|
|
|
+ try {
|
|
|
+ // 动态导入Leaflet(保持不变)
|
|
|
+ if (!L) {
|
|
|
+ L = await import('leaflet');
|
|
|
+ await import('leaflet/dist/leaflet.css');
|
|
|
+
|
|
|
+ delete (L.Icon.Default.prototype as any)._getIconUrl;
|
|
|
+ L.Icon.Default.mergeOptions({
|
|
|
+ iconRetinaUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon-2x.png',
|
|
|
+ iconUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-icon.png',
|
|
|
+ shadowUrl: 'https://cdnjs.cloudflare.com/ajax/libs/leaflet/1.7.1/images/marker-shadow.png',
|
|
|
+ });
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清除现有地图
|
|
|
+ if (map.value) {
|
|
|
+ map.value.remove();
|
|
|
+ map.value = null;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 创建地图实例
|
|
|
+ map.value = L.map('map-container', {
|
|
|
+ zoomControl: true,
|
|
|
+ attributionControl: false,
|
|
|
+ center: [25.202903, 113.25383], // 设置初始中心点
|
|
|
+ zoom: 10 // 设置初始缩放级别
|
|
|
+ });
|
|
|
+
|
|
|
+ // WMS配置
|
|
|
+ const GEOSERVER_CONFIG = {
|
|
|
+ url: "/geoserver/wms",
|
|
|
+ workspace: "acidmap",
|
|
|
+ layerGroup: "mapwithboundary",
|
|
|
+ };
|
|
|
+
|
|
|
+ // WMS图层配置
|
|
|
+ const wmsLayer = L.tileLayer.wms(GEOSERVER_CONFIG.url, {
|
|
|
+ layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
|
|
|
+ format: "image/png",
|
|
|
+ transparent: true,
|
|
|
+ version: "1.1.1", // 使用1.1.1版本更稳定
|
|
|
+ crs: L.CRS.EPSG4326, // 明确指定坐标系
|
|
|
+ attribution: "Data from GeoServer"
|
|
|
+ });
|
|
|
+
|
|
|
+ // 添加图层到地图
|
|
|
+ wmsLayer.addTo(map.value);
|
|
|
+
|
|
|
+ // 绑定鼠标事件
|
|
|
+ map.value.on('mousemove', handleMouseMove);
|
|
|
+ map.value.on('mouseout', handleMouseOut);
|
|
|
+
|
|
|
+
|
|
|
+ // 地图点击事件 - 修改后的版本
|
|
|
+ map.value.on('click', function (e: LeafletMouseEvent) {
|
|
|
+ const lng = e.latlng.lng;
|
|
|
+ const lat = e.latlng.lat;
|
|
|
+
|
|
|
+ // 保存当前点击坐标
|
|
|
+ currentClickCoords.lng = lng;
|
|
|
+ currentClickCoords.lat = lat;
|
|
|
+
|
|
|
+ const containerPoint = map.value.latLngToContainerPoint(e.latlng);
|
|
|
+
|
|
|
+ // 设置预测弹窗位置
|
|
|
+ predictionPopupStyle.left = `${containerPoint.x + 10}px`;
|
|
|
+ predictionPopupStyle.top = `${containerPoint.y + 10}px`;
|
|
|
+
|
|
|
+ let popupContent = '<div style="padding: 10px; max-width: 300px;">';
|
|
|
+ popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>经度:</strong> ${lng.toFixed(6)}</p>`;
|
|
|
+ popupContent += `<p style="margin: 3px 0; font-size: 12px;"><strong>纬度:</strong> ${lat.toFixed(6)}</p>`;
|
|
|
+ popupContent += '<div style="margin-top: 8px; display: flex; gap: 8px; justify-content: center;">';
|
|
|
+ popupContent += `<button onclick="window.handleAcidReductionPrediction(${lng}, ${lat})" style="padding: 6px 12px; cursor: pointer; background: #409eff; color: white; border: none; border-radius: 4px;">降酸预测</button>`;
|
|
|
+ popupContent += `<button onclick="window.handleAcidInversionPrediction(${lng}, ${lat})" style="padding: 6px 12px; cursor: pointer; background: #67c23a; color: white; border: none; border-radius: 4px;">反酸预测</button>`;
|
|
|
+ popupContent += '</div>';
|
|
|
+ popupContent += '</div>';
|
|
|
+
|
|
|
+ L.popup()
|
|
|
+ .setLatLng(e.latlng)
|
|
|
+ .setContent(popupContent)
|
|
|
+ .openOn(map.value);
|
|
|
+ });
|
|
|
+
|
|
|
+ mapLoading.value = false;
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('地图初始化失败:', error);
|
|
|
+ mapError.value = true;
|
|
|
+ mapLoading.value = false;
|
|
|
+
|
|
|
+ let errorMessage = '地图初始化失败';
|
|
|
+ if (error instanceof Error) {
|
|
|
+ errorMessage += ': ' + error.message;
|
|
|
+ }
|
|
|
+ ElMessage.error(errorMessage);
|
|
|
+ }
|
|
|
+};
|
|
|
+
|
|
|
+const reloadMap = () => {
|
|
|
+ initMap();
|
|
|
+};
|
|
|
+
|
|
|
+// 组件卸载时清理
|
|
|
+onUnmounted(() => {
|
|
|
+ if (hoverTimer) {
|
|
|
+ clearTimeout(hoverTimer);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 清理window上的方法
|
|
|
+ delete (window as any).handleAcidReductionPrediction;
|
|
|
+ delete (window as any).handleAcidInversionPrediction;
|
|
|
+});
|
|
|
+
|
|
|
+onMounted(() => {
|
|
|
+ // 将预测方法挂载到window对象
|
|
|
+ (window as any).handleAcidReductionPrediction = handleAcidReductionPrediction;
|
|
|
+ (window as any).handleAcidInversionPrediction = handleAcidInversionPrediction;
|
|
|
+ nextTick(() => {
|
|
|
+ initMap(); // DOM渲染完成后初始化,避免容器尺寸问题
|
|
|
+ });
|
|
|
+});
|
|
|
+</script>
|
|
|
+
|
|
|
+<style scoped>
|
|
|
+.feature-popup,
|
|
|
+.prediction-popup {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 1000;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ min-width: 200px;
|
|
|
+ max-width: 300px;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-popup {
|
|
|
+ max-width: 350px;
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-content {
|
|
|
+ padding: 0;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-header {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+ padding: 12px 15px;
|
|
|
+ border-bottom: 1px solid #ebeef5;
|
|
|
+ background: #f5f7fa;
|
|
|
+ border-radius: 8px 8px 0 0;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-header h4 {
|
|
|
+ margin: 0;
|
|
|
+ font-size: 14px;
|
|
|
+ color: #303133;
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn {
|
|
|
+ background: none;
|
|
|
+ border: none;
|
|
|
+ font-size: 18px;
|
|
|
+ cursor: pointer;
|
|
|
+ color: #909399;
|
|
|
+ padding: 0;
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.close-btn:hover {
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.popup-body {
|
|
|
+ padding: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-info, .error-info, .no-data {
|
|
|
+ text-align: center;
|
|
|
+ color: #909399;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.error-info {
|
|
|
+ color: #f56c6c;
|
|
|
+}
|
|
|
+
|
|
|
+.feature-info {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item {
|
|
|
+ display: flex;
|
|
|
+ justify-content: space-between;
|
|
|
+ align-items: center;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.info-item span {
|
|
|
+ color: #303133;
|
|
|
+ font-size: 12px;
|
|
|
+ text-align: right;
|
|
|
+}
|
|
|
+.map-card {
|
|
|
+ width: 850px;
|
|
|
+ flex: 1;
|
|
|
+ min-height: 600px;
|
|
|
+ margin: 0 auto;
|
|
|
+}
|
|
|
+
|
|
|
+.map-container {
|
|
|
+ height: 550px; /* 必须设置明确高度 */
|
|
|
+ width: 100%;
|
|
|
+ position: relative;
|
|
|
+ border-radius: 4px;
|
|
|
+ overflow: hidden;
|
|
|
+ background: #f0f2f5;
|
|
|
+ border: 1px solid #dcdfe6;
|
|
|
+}
|
|
|
+
|
|
|
+.loading {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ text-align: center;
|
|
|
+ z-index: 1000;
|
|
|
+ background: rgba(255, 255, 255, 0.95);
|
|
|
+ padding: 20px;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 30px;
|
|
|
+ height: 30px;
|
|
|
+ border: 3px solid #f3f3f3;
|
|
|
+ border-top: 3px solid #409eff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+ margin: 0 auto 10px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
+.loading span {
|
|
|
+ margin-left: 8px;
|
|
|
+ color: #606266;
|
|
|
+}
|
|
|
+
|
|
|
+.error-tip {
|
|
|
+ position: absolute;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%, -50%);
|
|
|
+ width: 80%;
|
|
|
+ z-index: 1000;
|
|
|
+}
|
|
|
+
|
|
|
+.title {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 10px;
|
|
|
+ margin-bottom: 15px;
|
|
|
+}
|
|
|
+
|
|
|
+.map-title {
|
|
|
+ color: #1a365d;
|
|
|
+ font-size: 1.6rem;
|
|
|
+ font-weight: 600;
|
|
|
+}
|
|
|
+
|
|
|
+.section-icon {
|
|
|
+ font-size: 2.2rem;
|
|
|
+ color: #3a9fd3;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-result {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-item label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-info {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #666;
|
|
|
+ background: #f8f9fa;
|
|
|
+ padding: 8px;
|
|
|
+ border-radius: 4px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-value {
|
|
|
+ font-weight: bold;
|
|
|
+ color: #409eff;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.warnings {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 8px;
|
|
|
+ background: #fff6f6;
|
|
|
+ border: 1px solid #fbc4c4;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-item {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #f56c6c;
|
|
|
+ line-height: 1.3;
|
|
|
+}
|
|
|
+
|
|
|
+.el-form-item {
|
|
|
+ margin-bottom: 16px;
|
|
|
+}
|
|
|
+.el-input {
|
|
|
+ width: 100%;
|
|
|
+}
|
|
|
+.feature-popup,
|
|
|
+.prediction-popup {
|
|
|
+ position: absolute;
|
|
|
+ z-index: 1000;
|
|
|
+ background: white;
|
|
|
+ border-radius: 8px;
|
|
|
+ box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
|
|
+ min-width: 200px;
|
|
|
+ max-width: 300px;
|
|
|
+ pointer-events: none;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-popup {
|
|
|
+ max-width: 350px;
|
|
|
+ pointer-events: auto;
|
|
|
+}
|
|
|
+::v-deep .acid-reduction-dialog{
|
|
|
+ --el-dialog-margin-top:40vh ;
|
|
|
+}
|
|
|
+</style>
|