|
|
@@ -19,120 +19,133 @@
|
|
|
</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 v-if="showPopup" class="feature-popup fixed-center">
|
|
|
<div class="popup-content">
|
|
|
<div class="popup-header">
|
|
|
- <h4>土壤预测结果</h4>
|
|
|
- <button @click="closePredictionPopup" class="close-btn">×</button>
|
|
|
+ <h4>地块信息</h4>
|
|
|
+ <button @click="closePopup" class="close-btn">×</button>
|
|
|
</div>
|
|
|
<div class="popup-body">
|
|
|
- <div v-if="predictionLoading" class="loading-info">
|
|
|
+ <!-- 坐标信息 -->
|
|
|
+ <div class="coordinate-info">
|
|
|
+ <div class="info-item">
|
|
|
+ <label>经度:</label>
|
|
|
+ <span>{{ currentClickCoords.lng.toFixed(6) }}</span>
|
|
|
+ </div>
|
|
|
+ <div class="info-item">
|
|
|
+ <label>纬度:</label>
|
|
|
+ <span>{{ currentClickCoords.lat.toFixed(6) }}</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 地块信息 -->
|
|
|
+ <div v-if="featureInfo.loading" class="loading-info">
|
|
|
<div class="loading-spinner small"></div>
|
|
|
- <span>预测中...</span>
|
|
|
+ <span>加载地块信息中...</span>
|
|
|
</div>
|
|
|
- <div v-else-if="predictionError" class="error-info">
|
|
|
- {{ predictionError }}
|
|
|
+ <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 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 v-else class="no-data">无地块信息</div>
|
|
|
+
|
|
|
+ <!-- 操作按钮 -->
|
|
|
+ <div class="action-buttons" v-if="!showPredictionResult && !showAcidReductionInput">
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="primary"
|
|
|
+ @click="startAcidReductionPrediction"
|
|
|
+ >
|
|
|
+ 降酸预测
|
|
|
+ </el-button>
|
|
|
+ <el-button
|
|
|
+ size="small"
|
|
|
+ type="success"
|
|
|
+ @click="handleAcidInversionPrediction"
|
|
|
+ >
|
|
|
+ 反酸预测
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 降酸预测参数输入 -->
|
|
|
+ <div v-if="showAcidReductionInput" class="acid-reduction-input">
|
|
|
+ <div class="section-title">降酸预测参数</div>
|
|
|
+ <el-form :model="acidReductionParams" :rules="acidReductionRules" ref="acidReductionFormRef" label-width="80px" size="small">
|
|
|
+ <el-form-item label="目标pH" prop="targetPH">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.targetPH"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ placeholder="0-14"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="NO3" prop="no3">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.no3"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="非负数"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ <el-form-item label="CEC" prop="cec">
|
|
|
+ <el-input
|
|
|
+ v-model.number="acidReductionParams.cec"
|
|
|
+ type="number"
|
|
|
+ step="0.01"
|
|
|
+ min="0"
|
|
|
+ placeholder="非负数"
|
|
|
+ />
|
|
|
+ </el-form-item>
|
|
|
+ </el-form>
|
|
|
+ <div class="input-buttons">
|
|
|
+ <el-button size="small" @click="cancelAcidReduction">取消</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="confirmAcidReduction" :loading="predictionLoading">
|
|
|
+ 开始预测
|
|
|
+ </el-button>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 预测结果展示 -->
|
|
|
+ <div v-if="showPredictionResult" class="prediction-section">
|
|
|
+ <div class="section-title">{{ predictionTitle }}</div>
|
|
|
+ <div v-if="predictionLoading" class="loading-info">
|
|
|
+ <div class="loading-spinner small"></div>
|
|
|
+ <span>预测中...</span>
|
|
|
</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 v-else-if="predictionError" class="error-info">
|
|
|
+ {{ predictionError }}
|
|
|
</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 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-buttons">
|
|
|
+ <el-button size="small" @click="resetPrediction">重新预测</el-button>
|
|
|
+ <el-button size="small" type="primary" @click="closePopup">关闭</el-button>
|
|
|
</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>
|
|
|
|
|
|
@@ -146,23 +159,15 @@ 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 showPopup = ref(false);
|
|
|
+
|
|
|
+
|
|
|
+// 预测状态
|
|
|
const predictionLoading = ref(false);
|
|
|
const predictionError = ref('');
|
|
|
const predictionResult = ref<any>(null);
|
|
|
-
|
|
|
-const disableMouseEvents = ref(false);
|
|
|
-
|
|
|
+const showPredictionResult = ref(false);
|
|
|
+const showAcidReductionInput = ref(false);
|
|
|
|
|
|
let L:any = null;
|
|
|
|
|
|
@@ -173,37 +178,41 @@ const featureInfo = reactive({
|
|
|
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 acidReductionFormRef = ref<FormInstance | null>(null);
|
|
|
const acidReductionParams = reactive({
|
|
|
- targetPH: 0, // 默认值(可调整)
|
|
|
- no3: 0, // 默认值
|
|
|
- cec: 0 // 默认值
|
|
|
+ targetPH: 7.0, // 设置一个合理的默认值
|
|
|
+ no3: 34.05, // 设置一个合理的默认值
|
|
|
+ cec: 7.87 // 设置一个合理的默认值
|
|
|
});
|
|
|
|
|
|
-// 过滤警告信息,排除模型24的错误
|
|
|
+// 过滤警告信息
|
|
|
const filteredWarnings = computed(() => {
|
|
|
if (!predictionResult.value?.warnings) return [];
|
|
|
-
|
|
|
return predictionResult.value.warnings.filter((warning: string) => {
|
|
|
- // 过滤掉模型24相关的错误信息
|
|
|
return !warning.includes('模型24');
|
|
|
});
|
|
|
});
|
|
|
|
|
|
+// 预测标题
|
|
|
+const predictionTitle = computed(() => {
|
|
|
+ if (currentPredictionType.value === 'reduction') return '降酸预测结果';
|
|
|
+ if (currentPredictionType.value === 'inversion') return '反酸预测结果';
|
|
|
+ return '预测结果';
|
|
|
+});
|
|
|
+
|
|
|
// 输入校验规则
|
|
|
const acidReductionRules = reactive({
|
|
|
targetPH: [
|
|
|
- { required: true, message: '请输入目标ph值', trigger: 'blur' },
|
|
|
+ { required: true, message: '请输入目标pH值', trigger: 'blur' },
|
|
|
{ type: 'number', message: '请输入有效数字', trigger: 'blur' },
|
|
|
{
|
|
|
validator: (rule: any, value: number, callback: any) => {
|
|
|
@@ -245,22 +254,6 @@ const acidReductionRules = reactive({
|
|
|
}
|
|
|
]
|
|
|
});
|
|
|
-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 => {
|
|
|
@@ -268,26 +261,22 @@ const formatPredictionValue = (value: any): string => {
|
|
|
|
|
|
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;
|
|
|
|
|
|
@@ -305,7 +294,6 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
|
|
|
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');
|
|
|
@@ -331,11 +319,8 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
|
|
|
|
|
|
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) {
|
|
|
@@ -343,11 +328,11 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
|
|
|
village: properties.QSDWMC,
|
|
|
landType: properties.DLMC,
|
|
|
};
|
|
|
- return true; // 找到有效地块
|
|
|
+ return true;
|
|
|
}
|
|
|
}
|
|
|
|
|
|
- return false; // 没有找到有效地块
|
|
|
+ return false;
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('获取地块信息失败:', error);
|
|
|
@@ -358,30 +343,30 @@ const getFeatureInfo = async (latlng: any, point: any): Promise<boolean> => {
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-// 调用8000后端预测接口
|
|
|
-const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?: {
|
|
|
+// 调用预测接口
|
|
|
+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());
|
|
|
}
|
|
|
|
|
|
+ console.log('调用预测接口,参数:', urlParams.toString());
|
|
|
+
|
|
|
const response = await fetch(
|
|
|
`http://localhost:8000/api/vector/nearest-with-predictions?${urlParams.toString()}`
|
|
|
);
|
|
|
@@ -392,41 +377,40 @@ const callPredictionAPI = async (lng: number, lat: number,acidReductionParams?:
|
|
|
|
|
|
const data = await response.json();
|
|
|
predictionResult.value = data;
|
|
|
+ console.log('预测结果:', data);
|
|
|
|
|
|
- //console.log('完整的预测结果:', data); // 调试用,查看返回的数据结构
|
|
|
- //console.log('模型24预测结果:', data.prediction_model24, '类型:', typeof data.prediction_model24);
|
|
|
-
|
|
|
- // 显示预测弹窗
|
|
|
- showPredictionPopup.value = true;
|
|
|
+ // 显示预测结果
|
|
|
+ showPredictionResult.value = true;
|
|
|
+ showAcidReductionInput.value = false;
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('调用预测接口失败:', error);
|
|
|
predictionError.value = `预测失败: ${error instanceof Error ? error.message : '未知错误'}`;
|
|
|
- ElMessage.error('预测请求失败,请检查后端服务');
|
|
|
-
|
|
|
- disableMouseEvents.value = false;
|
|
|
+ ElMessage.error('预测请求失败,请检查后端服务是否启动');
|
|
|
} finally {
|
|
|
predictionLoading.value = false;
|
|
|
}
|
|
|
};
|
|
|
|
|
|
-
|
|
|
-// 降酸预测按钮点击
|
|
|
-const handleAcidReductionPrediction = (lng: number, lat: number) => {
|
|
|
+// 开始降酸预测(显示参数输入)
|
|
|
+const startAcidReductionPrediction = () => {
|
|
|
currentPredictionType.value = 'reduction';
|
|
|
- currentClickCoords.lng = lng;
|
|
|
- currentClickCoords.lat = lat;
|
|
|
showAcidReductionInput.value = true;
|
|
|
};
|
|
|
|
|
|
-// 新增:确认降酸预测(输入完成后调用)
|
|
|
+// 取消降酸预测
|
|
|
+const cancelAcidReduction = () => {
|
|
|
+ showAcidReductionInput.value = false;
|
|
|
+ currentPredictionType.value = null;
|
|
|
+};
|
|
|
+
|
|
|
+// 确认降酸预测
|
|
|
const confirmAcidReduction = async () => {
|
|
|
-console.log(acidReductionParams);
|
|
|
- // 表单校验
|
|
|
if (!acidReductionFormRef.value) return;
|
|
|
+
|
|
|
try {
|
|
|
- await acidReductionFormRef.value.validate(); // 校验输入是否合法
|
|
|
- // 校验通过,调用接口(传递输入的3个参数)
|
|
|
+ await acidReductionFormRef.value.validate();
|
|
|
+
|
|
|
await callPredictionAPI(
|
|
|
currentClickCoords.lng,
|
|
|
currentClickCoords.lat,
|
|
|
@@ -436,84 +420,54 @@ console.log(acidReductionParams);
|
|
|
cec: acidReductionParams.cec
|
|
|
}
|
|
|
);
|
|
|
- // 关闭输入弹窗
|
|
|
- showAcidReductionInput.value = false;
|
|
|
+
|
|
|
} catch (error) {
|
|
|
- // 校验失败,不调用接口
|
|
|
ElMessage.error('输入参数不合法,请检查后重试');
|
|
|
console.error('表单校验失败:', error);
|
|
|
}
|
|
|
};
|
|
|
|
|
|
// 反酸预测按钮点击
|
|
|
-const handleAcidInversionPrediction = (lng: number, lat: number) => {
|
|
|
+const handleAcidInversionPrediction = () => {
|
|
|
currentPredictionType.value = 'inversion';
|
|
|
- callPredictionAPI(lng, lat);
|
|
|
+ callPredictionAPI(currentClickCoords.lng, currentClickCoords.lat);
|
|
|
};
|
|
|
|
|
|
+// 重置预测
|
|
|
+const resetPrediction = () => {
|
|
|
+ showPredictionResult.value = false;
|
|
|
+ predictionResult.value = null;
|
|
|
+ currentPredictionType.value = null;
|
|
|
+ showAcidReductionInput.value = false;
|
|
|
+};
|
|
|
|
|
|
-// 鼠标移动事件处理
|
|
|
-const handleMouseMove = (e: any) => {
|
|
|
-
|
|
|
- // 如果禁用了鼠标事件,直接返回
|
|
|
- if (disableMouseEvents.value) {
|
|
|
- return;
|
|
|
- }
|
|
|
+// 地图点击事件处理
|
|
|
+const handleMapClick = async (e: any) => {
|
|
|
+ const lng = e.latlng.lng;
|
|
|
+ const lat = e.latlng.lat;
|
|
|
+
|
|
|
+ // 更新当前坐标
|
|
|
+ currentClickCoords.lng = lng;
|
|
|
+ currentClickCoords.lat = lat;
|
|
|
|
|
|
- if (hoverTimer) {
|
|
|
- clearTimeout(hoverTimer);
|
|
|
- }
|
|
|
+ showPopup.value = true;
|
|
|
+
|
|
|
+ // 重置预测状态
|
|
|
+ resetPrediction();
|
|
|
|
|
|
- // 设置防抖,避免频繁请求
|
|
|
- hoverTimer = setTimeout(async () => {
|
|
|
+ // 获取地块信息
|
|
|
+ try {
|
|
|
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);
|
|
|
+ await getFeatureInfo(e.latlng, containerPoint);
|
|
|
+ } catch (error) {
|
|
|
+ console.error('处理地块信息失败:', error);
|
|
|
}
|
|
|
- showPopup.value = false;
|
|
|
};
|
|
|
|
|
|
// 关闭弹窗
|
|
|
const closePopup = () => {
|
|
|
showPopup.value = false;
|
|
|
-};
|
|
|
-
|
|
|
-// 关闭预测弹窗
|
|
|
-const closePredictionPopup = () => {
|
|
|
- showPredictionPopup.value = false;
|
|
|
- predictionResult.value = null;
|
|
|
- currentPredictionType.value = null;
|
|
|
-
|
|
|
- // 重新启用鼠标事件
|
|
|
- disableMouseEvents.value = false;
|
|
|
+ resetPrediction();
|
|
|
};
|
|
|
|
|
|
const initMap = async () => {
|
|
|
@@ -521,7 +475,7 @@ const initMap = async () => {
|
|
|
mapError.value = false;
|
|
|
|
|
|
try {
|
|
|
- // 动态导入Leaflet(保持不变)
|
|
|
+ // 动态导入Leaflet
|
|
|
if (!L) {
|
|
|
L = await import('leaflet');
|
|
|
await import('leaflet/dist/leaflet.css');
|
|
|
@@ -544,8 +498,8 @@ const initMap = async () => {
|
|
|
map.value = L.map('map-container', {
|
|
|
zoomControl: true,
|
|
|
attributionControl: false,
|
|
|
- center: [25.202903, 113.25383], // 设置初始中心点
|
|
|
- zoom: 10 // 设置初始缩放级别
|
|
|
+ center: [25.202903, 113.25383],
|
|
|
+ zoom: 10
|
|
|
});
|
|
|
|
|
|
// WMS配置
|
|
|
@@ -560,48 +514,16 @@ const initMap = async () => {
|
|
|
layers: `${GEOSERVER_CONFIG.workspace}:${GEOSERVER_CONFIG.layerGroup}`,
|
|
|
format: "image/png",
|
|
|
transparent: true,
|
|
|
- version: "1.1.1", // 使用1.1.1版本更稳定
|
|
|
- crs: L.CRS.EPSG4326, // 明确指定坐标系
|
|
|
+ version: "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);
|
|
|
- });
|
|
|
+ // 绑定点击事件
|
|
|
+ map.value.on('click', handleMapClick);
|
|
|
|
|
|
mapLoading.value = false;
|
|
|
|
|
|
@@ -624,41 +546,30 @@ const reloadMap = () => {
|
|
|
|
|
|
// 组件卸载时清理
|
|
|
onUnmounted(() => {
|
|
|
- if (hoverTimer) {
|
|
|
- clearTimeout(hoverTimer);
|
|
|
+ if (map.value) {
|
|
|
+ map.value.remove();
|
|
|
}
|
|
|
-
|
|
|
- // 清理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渲染完成后初始化,避免容器尺寸问题
|
|
|
+ initMap();
|
|
|
});
|
|
|
});
|
|
|
</script>
|
|
|
|
|
|
<style scoped>
|
|
|
-.feature-popup,
|
|
|
-.prediction-popup {
|
|
|
- position: absolute;
|
|
|
+.feature-popup {
|
|
|
+ position: fixed;
|
|
|
+ top: 50%;
|
|
|
+ left: 50%;
|
|
|
+ transform: translate(-50%,-50%);
|
|
|
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;
|
|
|
+ min-width: 320px;
|
|
|
+ max-width: 380px;
|
|
|
}
|
|
|
|
|
|
.popup-content {
|
|
|
@@ -669,7 +580,7 @@ onMounted(() => {
|
|
|
display: flex;
|
|
|
justify-content: space-between;
|
|
|
align-items: center;
|
|
|
- padding: 12px 15px;
|
|
|
+ padding: 10px 15px;
|
|
|
border-bottom: 1px solid #ebeef5;
|
|
|
background: #f5f7fa;
|
|
|
border-radius: 8px 8px 0 0;
|
|
|
@@ -693,7 +604,6 @@ onMounted(() => {
|
|
|
display: flex;
|
|
|
align-items: center;
|
|
|
justify-content: center;
|
|
|
- pointer-events: auto;
|
|
|
}
|
|
|
|
|
|
.close-btn:hover {
|
|
|
@@ -702,12 +612,25 @@ onMounted(() => {
|
|
|
|
|
|
.popup-body {
|
|
|
padding: 15px;
|
|
|
+ max-height: 500px;
|
|
|
+ overflow-y: auto;
|
|
|
+}
|
|
|
+
|
|
|
+.coordinate-info {
|
|
|
+ margin-bottom: 12px;
|
|
|
+ padding-bottom: 12px;
|
|
|
+ border-bottom: 1px solid #f0f0f0;
|
|
|
}
|
|
|
|
|
|
.loading-info, .error-info, .no-data {
|
|
|
text-align: center;
|
|
|
color: #909399;
|
|
|
font-size: 14px;
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ justify-content: center;
|
|
|
+ gap: 8px;
|
|
|
+ margin: 10px 0;
|
|
|
}
|
|
|
|
|
|
.error-info {
|
|
|
@@ -718,6 +641,7 @@ onMounted(() => {
|
|
|
display: flex;
|
|
|
flex-direction: column;
|
|
|
gap: 8px;
|
|
|
+ margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
.info-item {
|
|
|
@@ -729,14 +653,127 @@ onMounted(() => {
|
|
|
.info-item label {
|
|
|
font-weight: 600;
|
|
|
color: #606266;
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 14px;
|
|
|
}
|
|
|
|
|
|
.info-item span {
|
|
|
color: #303133;
|
|
|
- font-size: 12px;
|
|
|
+ font-size: 14px;
|
|
|
text-align: right;
|
|
|
}
|
|
|
+
|
|
|
+.action-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ justify-content: center;
|
|
|
+ margin: 12px 0;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.acid-reduction-input {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.input-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-section {
|
|
|
+ margin-top: 12px;
|
|
|
+ padding-top: 12px;
|
|
|
+ border-top: 1px solid #f0f0f0;
|
|
|
+}
|
|
|
+
|
|
|
+.section-title {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #303133;
|
|
|
+ font-size: 13px;
|
|
|
+ margin-bottom: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-result {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 8px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-item {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ gap: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.result-item label {
|
|
|
+ font-weight: 600;
|
|
|
+ color: #606266;
|
|
|
+ font-size: 14px;
|
|
|
+}
|
|
|
+
|
|
|
+.point-info {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #666;
|
|
|
+ background: #f8f9fa;
|
|
|
+ padding: 6px;
|
|
|
+ border-radius: 4px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-value {
|
|
|
+ font-weight: bold;
|
|
|
+ font-size: 13px;
|
|
|
+}
|
|
|
+
|
|
|
+.prediction-value.reduction {
|
|
|
+ color: #e6a23c;
|
|
|
+}
|
|
|
+
|
|
|
+.warnings {
|
|
|
+ margin-top: 8px;
|
|
|
+ padding: 6px;
|
|
|
+ background: #fff6f6;
|
|
|
+ border: 1px solid #fbc4c4;
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.warning-item {
|
|
|
+ font-size: 11px;
|
|
|
+ color: #f56c6c;
|
|
|
+ line-height: 1.3;
|
|
|
+}
|
|
|
+
|
|
|
+.result-buttons {
|
|
|
+ display: flex;
|
|
|
+ gap: 8px;
|
|
|
+ justify-content: flex-end;
|
|
|
+ margin-top: 12px;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border: 2px solid #f3f3f3;
|
|
|
+ border-top: 2px solid #409eff;
|
|
|
+ border-radius: 50%;
|
|
|
+ animation: spin 1s linear infinite;
|
|
|
+}
|
|
|
+
|
|
|
+.loading-spinner.small {
|
|
|
+ width: 16px;
|
|
|
+ height: 16px;
|
|
|
+ border-width: 1.5px;
|
|
|
+}
|
|
|
+
|
|
|
+@keyframes spin {
|
|
|
+ 0% { transform: rotate(0deg); }
|
|
|
+ 100% { transform: rotate(360deg); }
|
|
|
+}
|
|
|
+
|
|
|
.map-card {
|
|
|
width: 850px;
|
|
|
flex: 1;
|
|
|
@@ -745,7 +782,7 @@ onMounted(() => {
|
|
|
}
|
|
|
|
|
|
.map-container {
|
|
|
- height: 550px; /* 必须设置明确高度 */
|
|
|
+ height: 550px;
|
|
|
width: 100%;
|
|
|
position: relative;
|
|
|
border-radius: 4px;
|
|
|
@@ -767,21 +804,6 @@ onMounted(() => {
|
|
|
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;
|
|
|
@@ -814,76 +836,34 @@ onMounted(() => {
|
|
|
color: #3a9fd3;
|
|
|
}
|
|
|
|
|
|
-.prediction-result {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 12px;
|
|
|
-}
|
|
|
-
|
|
|
-.result-item {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 4px;
|
|
|
+/* 表单样式调整 */
|
|
|
+:deep(.el-form-item) {
|
|
|
+ margin-bottom: 12px;
|
|
|
}
|
|
|
|
|
|
-.result-item label {
|
|
|
- font-weight: 600;
|
|
|
- color: #606266;
|
|
|
+:deep(.el-form-item__label) {
|
|
|
font-size: 12px;
|
|
|
+ line-height: 28px;
|
|
|
}
|
|
|
|
|
|
-.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;
|
|
|
+:deep(.el-input) {
|
|
|
+ font-size: 12px;
|
|
|
}
|
|
|
|
|
|
-.warning-item {
|
|
|
- font-size: 11px;
|
|
|
- color: #f56c6c;
|
|
|
- line-height: 1.3;
|
|
|
+:deep(.el-input__inner) {
|
|
|
+ height: 28px;
|
|
|
+ line-height: 28px;
|
|
|
}
|
|
|
|
|
|
-.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;
|
|
|
+.popup-body {
|
|
|
+ padding: 15px;
|
|
|
+ overflow-y: auto;
|
|
|
+ max-height: calc(80vh - 60px); /* 减去头部高度 */
|
|
|
}
|
|
|
|
|
|
-.prediction-popup {
|
|
|
- max-width: 350px;
|
|
|
- pointer-events: auto;
|
|
|
-}
|
|
|
-::v-deep .acid-reduction-dialog{
|
|
|
- --el-dialog-margin-top:40vh ;
|
|
|
+.popup-content {
|
|
|
+ display: flex;
|
|
|
+ flex-direction: column;
|
|
|
+ height: 100%;
|
|
|
}
|
|
|
</style>
|