|
|
@@ -47,7 +47,7 @@
|
|
|
<div class="content-area">
|
|
|
<!-- 地图区域 - 单独一行 -->
|
|
|
<div class="map-section">
|
|
|
- <h3>作物态Cd预测地图</h3>
|
|
|
+ <h3>水稻Cd预测地图</h3>
|
|
|
<div v-if="loadingMap" class="loading-container">
|
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
<span>地图加载中...</span>
|
|
|
@@ -62,12 +62,35 @@
|
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
<span>乡镇地图加载中...</span>
|
|
|
</div>
|
|
|
- <!-- ECharts地图容器 -->
|
|
|
+ <div class="township-map-wrapper">
|
|
|
+ <!-- Leaflet地图容器 -->
|
|
|
+ <div class="town-map-title">水稻Cd含量预测镇域统计分布图</div>
|
|
|
<div
|
|
|
v-show="!loadingTownshipMap"
|
|
|
ref="townshipMapRef"
|
|
|
class="township-map-container"
|
|
|
></div>
|
|
|
+ </div>
|
|
|
+
|
|
|
+ <!-- 颜色图例 -->
|
|
|
+ <div class="legend">
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="color-box" style="background-color: #00FF00;"></div>
|
|
|
+ <span>安全区间 (0.0-0.2 mg/kg)</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="color-box" style="background-color: #FFFF00;"></div>
|
|
|
+ <span>预警区间 (0.2-0.3 mg/kg)</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="color-box" style="background-color: #FF0000;"></div>
|
|
|
+ <span>超标区间 (≥0.3 mg/kg)</span>
|
|
|
+ </div>
|
|
|
+ <div class="legend-item">
|
|
|
+ <div class="color-box" style="background-color: #CCCCCC;"></div>
|
|
|
+ <span>无数据</span>
|
|
|
+ </div>
|
|
|
+ </div>
|
|
|
<div v-if="!loadingTownshipMap && !townshipMapInstance" class="no-data">
|
|
|
<el-icon><Location /></el-icon>
|
|
|
<p>暂无乡镇边界数据</p>
|
|
|
@@ -76,7 +99,7 @@
|
|
|
|
|
|
<!-- 统计图表区域 -->
|
|
|
<div class="stats-area">
|
|
|
- <h3>作物态Cd预测统计信息</h3>
|
|
|
+ <h3>水稻Cd预测统计信息</h3>
|
|
|
<div class="model-info">
|
|
|
<el-tag type="info">{{ modelInfo.modelType || 'Cd预测模型' }}</el-tag>
|
|
|
<span class="update-time">
|
|
|
@@ -133,7 +156,7 @@
|
|
|
|
|
|
<!-- 直方图区域 - 单独一行 -->
|
|
|
<div class="histogram-section">
|
|
|
- <h3>作物态Cd预测直方图</h3>
|
|
|
+ <h3>水稻Cd预测直方图</h3>
|
|
|
<div v-if="loadingHistogram" class="loading-container">
|
|
|
<el-icon class="loading-icon"><Loading /></el-icon>
|
|
|
<span>直方图加载中...</span>
|
|
|
@@ -149,20 +172,30 @@
|
|
|
|
|
|
<script>
|
|
|
import * as XLSX from 'xlsx';
|
|
|
-import * as echarts from 'echarts';
|
|
|
import { saveAs } from 'file-saver';
|
|
|
import { api8000 } from '@/utils/request';
|
|
|
import {
|
|
|
- Location,Loading, Upload, Picture, Histogram, Download, Document, Box, DataAnalysis
|
|
|
+ Location, Loading, Upload, Picture, Histogram, Download, Document, Box, DataAnalysis
|
|
|
} from '@element-plus/icons-vue';
|
|
|
+// 引入Leaflet相关资源
|
|
|
+import L from 'leaflet';
|
|
|
+import 'leaflet/dist/leaflet.css';
|
|
|
+import 'leaflet.markercluster/dist/MarkerCluster.css';
|
|
|
+import 'leaflet.markercluster/dist/MarkerCluster.Default.css';
|
|
|
+import 'leaflet.markercluster';
|
|
|
|
|
|
export default {
|
|
|
name: 'CropCadmiumPrediction',
|
|
|
components: {
|
|
|
+ Location,
|
|
|
Loading, Upload, Picture, Histogram, Download, Document, Box, DataAnalysis
|
|
|
},
|
|
|
data() {
|
|
|
return {
|
|
|
+ mapContainerStyle: {
|
|
|
+ width: '100%',
|
|
|
+ height: '800px'
|
|
|
+ },
|
|
|
isCalculating: false,
|
|
|
isCalculatingFromDB: false,
|
|
|
loadingMap: false,
|
|
|
@@ -183,16 +216,16 @@ export default {
|
|
|
mapBlob: null,
|
|
|
histogramBlob: null,
|
|
|
selectedFile: null,
|
|
|
- // 新增:乡镇地图相关数据
|
|
|
+ // 乡镇地图相关数据
|
|
|
loadingTownshipMap: false, // 乡镇地图加载状态
|
|
|
- townshipMapInstance: null, // ECharts实例
|
|
|
- townshipGeoJson: null, // 本地边界数据(TopoJSON/GeoJSON)
|
|
|
+ townshipMapInstance: null, // Leaflet实例
|
|
|
+ townshipGeoJson: null, // 本地边界数据(GeoJSON)
|
|
|
townshipData: [], // 接口获取的乡镇Cd数据
|
|
|
countyName: '乐昌市', // 默认县市名称
|
|
|
- townshipMapRef :null,
|
|
|
currentTooltipTown: '', // 当前悬浮的乡镇
|
|
|
currentTooltipData: null, // 当前乡镇的详情数据
|
|
|
isTooltipLoading: false, // tooltip 是否在加载中
|
|
|
+ dataMap:{},
|
|
|
};
|
|
|
},
|
|
|
|
|
|
@@ -200,25 +233,41 @@ export default {
|
|
|
// 组件挂载时获取最新数据
|
|
|
this.fetchLatestResults();
|
|
|
this.fetchStatistics();
|
|
|
- this.initTownshipMap();
|
|
|
+
|
|
|
+ // 使用更智能的初始化时机
|
|
|
+ this.$nextTick(() => {
|
|
|
+ // 等待下一个动画帧,确保DOM完全渲染
|
|
|
+ requestAnimationFrame(() => {
|
|
|
+ setTimeout(() => {
|
|
|
+ this.initTownshipMap();
|
|
|
+ }, 300);
|
|
|
+ });
|
|
|
+ });
|
|
|
|
|
|
// 添加窗口调整事件监听
|
|
|
window.addEventListener('resize', this.handleResize);
|
|
|
},
|
|
|
|
|
|
beforeDestroy() {
|
|
|
- if (this.mapImageUrl) URL.revokeObjectURL(this.mapImageUrl);
|
|
|
- if (this.histogramImageUrl) URL.revokeObjectURL(this.histogramImageUrl);
|
|
|
- // 新增:销毁ECharts实例,避免内存泄漏
|
|
|
+ // 清理ResizeObserver
|
|
|
+ if (this.resizeObserver) {
|
|
|
+ this.resizeObserver.disconnect();
|
|
|
+ }
|
|
|
+ // 组件销毁前移除事件监听,避免内存泄漏
|
|
|
+ window.removeEventListener('resize', this.handleResize);
|
|
|
if (this.townshipMapInstance) {
|
|
|
- this.townshipMapInstance.dispose();
|
|
|
+ this.townshipMapInstance.remove();
|
|
|
this.townshipMapInstance = null;
|
|
|
}
|
|
|
-
|
|
|
- // 移除窗口调整事件监听
|
|
|
- window.removeEventListener('resize', this.handleResize);
|
|
|
},
|
|
|
+
|
|
|
+
|
|
|
methods: {
|
|
|
+ handleResize() {
|
|
|
+ if (this.townshipMapInstance) {
|
|
|
+ this.townshipMapInstance.invalidateSize();
|
|
|
+ }
|
|
|
+ },
|
|
|
// 触发文件选择
|
|
|
triggerFileUpload() {
|
|
|
this.$refs.fileInput.click();
|
|
|
@@ -381,14 +430,36 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 新增方法:根据乡镇名请求接口
|
|
|
+ // 根据Cd含量获取对应的颜色
|
|
|
+ getColorByCdValue(cdValue) {
|
|
|
+ if (cdValue === null || cdValue === undefined) {
|
|
|
+ return '#CCCCCC'; // 无数据时显示灰色
|
|
|
+ }
|
|
|
+ const numValue = Number(cdValue);
|
|
|
+ if (isNaN(numValue)) {
|
|
|
+ return '#CCCCCC';
|
|
|
+ }
|
|
|
+ if (numValue >= 0.0 && numValue < 0.2) {
|
|
|
+ return '#00FF00'; // 安全区间 - 绿色
|
|
|
+ } else if (numValue >= 0.2 && numValue < 0.3) {
|
|
|
+ return '#FFFF00'; // 预警区间 - 黄色
|
|
|
+ } else if (numValue >= 0.3) {
|
|
|
+ return '#FF0000'; // 超标区间 - 红色
|
|
|
+ }
|
|
|
+ return '#CCCCCC'; // 默认灰色
|
|
|
+ },
|
|
|
+
|
|
|
+ // 根据乡镇名请求接口
|
|
|
async fetchTownshipDetailByName(townName) {
|
|
|
+ if (this.dataMap[townName]) {
|
|
|
+ return this.dataMap[townName];
|
|
|
+ }
|
|
|
+
|
|
|
try {
|
|
|
- // 调用对应乡镇的接口,假设接口地址为 /api/township-details/{townName}
|
|
|
const encodedTownName = encodeURIComponent(townName);
|
|
|
const response = await api8000.get(`/api/cd-prediction/crop-cd/statistics/town/${encodedTownName}`);
|
|
|
if (response.data && response.data.success) {
|
|
|
- return response.data.data;
|
|
|
+ return response.data;
|
|
|
}
|
|
|
return null;
|
|
|
} catch (error) {
|
|
|
@@ -397,176 +468,331 @@ export default {
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 新增1:初始化乡镇边界地图(核心逻辑,仅加载边界)
|
|
|
- async initTownshipMap() {
|
|
|
- try {
|
|
|
- this.loadingTownshipMap = true;
|
|
|
- // 步骤1:加载本地 GeoJSON 边界文件
|
|
|
- await this.loadLocalGeoJson();
|
|
|
- // 步骤2:等待DOM更新完成
|
|
|
- await this.$nextTick();
|
|
|
- // 步骤3:渲染边界地图(不关联接口数据)
|
|
|
- this.renderTownshipMap();
|
|
|
- } catch (error) {
|
|
|
- console.error('乡镇边界加载失败:', error);
|
|
|
- this.townshipMapInstance = null; // 标记加载失败
|
|
|
- } finally {
|
|
|
- this.loadingTownshipMap = false;
|
|
|
+ // 初始化乡镇边界地图
|
|
|
+async initTownshipMap() {
|
|
|
+ try {
|
|
|
+ this.loadingTownshipMap = true;
|
|
|
+
|
|
|
+ // 先强制设置容器尺寸
|
|
|
+ this.forceMapContainerSize();
|
|
|
+
|
|
|
+ await this.loadLocalGeoJson();
|
|
|
+
|
|
|
+ const mapContainer = this.$refs.townshipMapRef;
|
|
|
+ if (!mapContainer) {
|
|
|
+ throw new Error('地图容器不存在');
|
|
|
}
|
|
|
- },
|
|
|
|
|
|
- // 新增2:加载本地 GeoJSON 文件(关键:路径必须正确)
|
|
|
+ // 使用增强的尺寸等待方法
|
|
|
+ await this.waitForContainerSizeWithFallback(mapContainer);
|
|
|
+
|
|
|
+ const townNames = this.townshipGeoJson.features.map(feature => feature.properties.name);
|
|
|
+
|
|
|
+ const dataPromises = townNames.map(async (townName) => {
|
|
|
+ try {
|
|
|
+ const response = await this.fetchTownshipDetailByName(townName);
|
|
|
+ return { townName, response, success: true };
|
|
|
+ } catch (error) {
|
|
|
+ console.warn(`获取${townName}数据失败:`, error);
|
|
|
+ return { townName, response: null, success: false };
|
|
|
+ }
|
|
|
+ });
|
|
|
+
|
|
|
+ const allResults = await Promise.all(dataPromises);
|
|
|
+
|
|
|
+ this.dataMap = {};
|
|
|
+ allResults.forEach(result => {
|
|
|
+ this.dataMap[result.townName] = result.success ? result.response : null;
|
|
|
+ });
|
|
|
+
|
|
|
+ // 确保DOM更新后渲染地图
|
|
|
+ await this.$nextTick();
|
|
|
+ this.renderTownshipMap();
|
|
|
+
|
|
|
+ } catch (error) {
|
|
|
+ console.error('乡镇地图加载失败:', error);
|
|
|
+ this.townshipMapInstance = null;
|
|
|
+ // 重试机制
|
|
|
+ setTimeout(() => {
|
|
|
+ this.initTownshipMap();
|
|
|
+ }, 500);
|
|
|
+ } finally {
|
|
|
+ this.loadingTownshipMap = false;
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 强制设置地图容器尺寸
|
|
|
+ forceMapContainerSize() {
|
|
|
+ this.mapContainerStyle = {
|
|
|
+ width: '100%',
|
|
|
+ height: '800px',
|
|
|
+ minHeight: '600px',
|
|
|
+ display: 'block',
|
|
|
+ visibility: 'visible'
|
|
|
+ };
|
|
|
+ },
|
|
|
+
|
|
|
+ // 增强的尺寸等待方法
|
|
|
+ waitForContainerSizeWithFallback(container) {
|
|
|
+ return new Promise((resolve, reject) => {
|
|
|
+ let retries = 0;
|
|
|
+ const maxRetries = 30; // 减少重试次数,更快失败
|
|
|
+
|
|
|
+ const checkSize = () => {
|
|
|
+ // 添加更严格的尺寸检查
|
|
|
+ const width = container.offsetWidth || container.clientWidth;
|
|
|
+ const height = container.offsetHeight || container.clientHeight;
|
|
|
+
|
|
|
+ console.log(`容器尺寸检查: ${width}x${height}, 重试: ${retries}`);
|
|
|
+
|
|
|
+ if (width > 100 && height > 100) { // 降低尺寸阈值
|
|
|
+ console.log('容器尺寸有效,继续初始化');
|
|
|
+ resolve();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ if (retries >= maxRetries) {
|
|
|
+ console.warn('容器尺寸等待超时,尝试强制初始化');
|
|
|
+ // 即使尺寸不理想也继续,Leaflet可以处理
|
|
|
+ this.applyEmergencySize(container);
|
|
|
+ resolve();
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ retries++;
|
|
|
+ setTimeout(checkSize, 100); // 增加检查间隔
|
|
|
+ };
|
|
|
+
|
|
|
+ // 立即检查一次
|
|
|
+ setTimeout(checkSize, 0);
|
|
|
+ });
|
|
|
+ },
|
|
|
+
|
|
|
+ // 紧急情况下的尺寸处理
|
|
|
+ applyEmergencySize(container) {
|
|
|
+ console.warn('应用紧急尺寸设置');
|
|
|
+ const parent = container.parentElement;
|
|
|
+ if (parent) {
|
|
|
+ const parentWidth = parent.offsetWidth;
|
|
|
+ if (parentWidth > 0) {
|
|
|
+ container.style.width = parentWidth + 'px';
|
|
|
+ container.style.height = '600px'; // 设置一个合理的最小高度
|
|
|
+ }
|
|
|
+ }
|
|
|
+ },
|
|
|
+
|
|
|
+ // 加载本地 GeoJSON 文件
|
|
|
async loadLocalGeoJson() {
|
|
|
try {
|
|
|
- // 1. 确认文件路径:public/data/韶关市乡镇划分图5.geojson
|
|
|
const geoJsonPath = '/data/韶关市乡镇划分图5.geojson';
|
|
|
-
|
|
|
- // 2. 发送请求加载 GeoJSON
|
|
|
const response = await fetch(geoJsonPath);
|
|
|
- // 3. 检查请求是否成功(状态码 200-299 为成功)
|
|
|
+
|
|
|
if (!response.ok) {
|
|
|
throw new Error(`文件加载失败:状态码 ${response.status}(路径:${geoJsonPath})`);
|
|
|
}
|
|
|
|
|
|
- // 4. 解析 GeoJSON 数据
|
|
|
let originalGeoJson = await response.json();
|
|
|
|
|
|
- // 关键修改:过滤只保留 FXZQMC 为"乐昌市"的特征数据
|
|
|
+ // 过滤只保留 FXZQMC 为"乐昌市"的特征数据
|
|
|
this.townshipGeoJson = {
|
|
|
- ...originalGeoJson, // 保留原有属性(如 type、crs 等)
|
|
|
- features: originalGeoJson.features// 过滤出乐昌市的乡镇
|
|
|
- .filter(feature => feature.properties?.FXZQMC === '乐昌市')
|
|
|
- // 为每个乡镇添加name字段,值为TXZQMC(ECharts默认读取name字段)
|
|
|
- .map(feature => ({
|
|
|
- ...feature,
|
|
|
- properties: {
|
|
|
- ...feature.properties,
|
|
|
- name: feature.properties?.TXZQMC || '未知乡镇' // 核心:映射TXZQMC到name
|
|
|
- }
|
|
|
- }))
|
|
|
+ ...originalGeoJson,
|
|
|
+ features: originalGeoJson.features
|
|
|
+ .filter(feature => feature.properties?.FXZQMC === '乐昌市')
|
|
|
+ .map(feature => ({
|
|
|
+ ...feature,
|
|
|
+ properties: {
|
|
|
+ ...feature.properties,
|
|
|
+ name: feature.properties?.TXZQMC || '未知乡镇'
|
|
|
+ }
|
|
|
+ }))
|
|
|
};
|
|
|
|
|
|
- // 5. 验证 GeoJSON 格式(必须包含 features 数组,否则是无效格式)
|
|
|
if (!this.townshipGeoJson.features || !Array.isArray(this.townshipGeoJson.features)) {
|
|
|
throw new Error('GeoJSON 格式错误:缺少 features 数组');
|
|
|
}
|
|
|
|
|
|
- console.log('GeoJSON 加载成功,包含乡镇数量:', this.townshipGeoJson.features.length);
|
|
|
} catch (error) {
|
|
|
console.error('GeoJSON 加载异常:', error);
|
|
|
- throw error; // 抛出错误,让 initTownshipMap 捕获
|
|
|
+ throw error;
|
|
|
}
|
|
|
},
|
|
|
|
|
|
- // 新增3:渲染乡镇边界(仅显示边界和乡镇名,不关联数据)
|
|
|
- renderTownshipMap() {
|
|
|
- // 1. 获取地图容器 DOM(必须存在)
|
|
|
- const mapContainer = this.$refs.townshipMapRef;
|
|
|
- if (!mapContainer) {
|
|
|
- throw new Error('ECharts 容器不存在:请检查 ref="townshipMapRef" 是否正确');
|
|
|
- }
|
|
|
+ // 使用Leaflet渲染乡镇地图并填充颜色
|
|
|
+ // 修改渲染地图方法,增加错误处理
|
|
|
+// 使用Leaflet渲染乡镇地图并填充颜色
|
|
|
+renderTownshipMap() {
|
|
|
+ try {
|
|
|
+ const mapContainer = this.$refs.townshipMapRef;
|
|
|
+ if (!mapContainer || !this.townshipGeoJson) {
|
|
|
+ console.error('渲染条件不满足');
|
|
|
+ return;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 最终尺寸检查
|
|
|
+ const width = mapContainer.offsetWidth;
|
|
|
+ const height = mapContainer.offsetHeight;
|
|
|
+
|
|
|
+ if (width === 0 || height === 0) {
|
|
|
+ console.error('最终检查: 地图容器尺寸为0,无法渲染');
|
|
|
+ mapContainer.style.width = '100%';
|
|
|
+ mapContainer.style.height = '600px';
|
|
|
+ setTimeout(() => {
|
|
|
+ this.renderTownshipMap();
|
|
|
+ }, 100);
|
|
|
+ return;
|
|
|
+ }
|
|
|
|
|
|
- // 2. 初始化 ECharts 实例
|
|
|
- this.townshipMapInstance = echarts.init(mapContainer);
|
|
|
+ console.log(`开始渲染地图,容器尺寸: ${width}x${height}`);
|
|
|
|
|
|
- // 3. 注册地图:将 GeoJSON 数据注册到 ECharts(名称用 countyName:乐昌市)
|
|
|
- echarts.registerMap(this.countyName, this.townshipGeoJson);
|
|
|
+ // 销毁旧地图实例
|
|
|
+ if (this.townshipMapInstance) {
|
|
|
+ this.townshipMapInstance.remove();
|
|
|
+ }
|
|
|
|
|
|
- // 4. ECharts 配置项(仅显示边界和乡镇名,无数据关联)
|
|
|
- const option = {
|
|
|
- // 标题(可选,显示在地图上方)
|
|
|
- title: {
|
|
|
- text: '乐昌市乡镇边界',
|
|
|
- left: 'center',
|
|
|
- textStyle: { fontSize: 16, fontWeight: 'bold' }
|
|
|
- },
|
|
|
- // 提示框(鼠标悬浮时显示乡镇名)
|
|
|
- tooltip: {
|
|
|
- trigger: 'item', // 按乡镇区域触发
|
|
|
- formatter: () => {
|
|
|
- if (this.isTooltipLoading) {
|
|
|
- return '<div style="padding: 5px;">加载中...</div>';
|
|
|
- } else if (this.currentTooltipData) {
|
|
|
- const detail = this.currentTooltipData;
|
|
|
- let content = `
|
|
|
- <div class="town-tooltip">
|
|
|
- <h3 style="margin: 0 0 5px; color: #0066CC; text-align : center;">${this.currentTooltipTown}</h3>
|
|
|
- <div style="height: 1px; background-color: #0066CC; margin-bottom: 8px;"></div>
|
|
|
- <p>采样点数量: ${detail.基础统计.采样点数量}</p>
|
|
|
- <p>平均值: ${detail.基础统计.平均值.toFixed(4)} mg/kg</p>
|
|
|
- <p>最小值: ${detail.基础统计.最小值.toFixed(4)} mg/kg</p>
|
|
|
- <p>最大值: ${detail.基础统计.最大值.toFixed(4)} mg/kg</p>
|
|
|
- <p>数据更新时间: ${new Date(detail.数据更新时间).toLocaleString()}</p>
|
|
|
- <div style="height: 1px; background-color: #0066CC; margin-bottom: 8px;"></div>
|
|
|
- <p style="color:black; font-size:16px;">分布统计:</p>
|
|
|
- <p>安全区间占比: ${detail.分布统计表格.汇总.安全区间占比}</p>
|
|
|
- <p>预警区间占比: ${detail.分布统计表格.汇总.预警区间占比}</p>
|
|
|
- <p>超标区间占比: ${detail.分布统计表格.汇总.超标区间占比}</p>
|
|
|
- </div>
|
|
|
- `;
|
|
|
- return content;
|
|
|
- }
|
|
|
- },
|
|
|
+ // 创建地图实例
|
|
|
+ this.townshipMapInstance = L.map(mapContainer, {
|
|
|
+ zoomControl: false,
|
|
|
+ attributionControl: false
|
|
|
+ });
|
|
|
|
|
|
- },
|
|
|
- // 地图系列(核心:渲染边界)
|
|
|
- series: [
|
|
|
- {
|
|
|
- type: 'map',
|
|
|
- map: this.countyName, // 对应注册的地图名称(乐昌市)
|
|
|
- roam: true, // 允许鼠标缩放、平移地图(方便查看)
|
|
|
- label: {
|
|
|
- show: true, // 显示乡镇名称标签
|
|
|
- fontSize: 10, // 标签字体大小(避免重叠)
|
|
|
- color: '#333' // 标签颜色
|
|
|
- },
|
|
|
- itemStyle: {
|
|
|
- color: 'transparent', // 乡镇区域填充色(透明,仅显示边界)
|
|
|
- borderColor: '#000000', // 边界颜色(青色,醒目)
|
|
|
- borderWidth: 1.5 // 边界宽度
|
|
|
- },
|
|
|
- emphasis: {
|
|
|
- // 鼠标悬浮时的样式(高亮边界和标签)
|
|
|
- label: { color: '#fff', fontSize: 11 },
|
|
|
- itemStyle: { color: 'rgba(71, 195, 185, 0.3)' } // 悬浮区域填充色
|
|
|
- }
|
|
|
+ // 添加缩放控件
|
|
|
+ L.control.zoom({
|
|
|
+ position: 'bottomright'
|
|
|
+ }).addTo(this.townshipMapInstance);
|
|
|
+
|
|
|
+ // 添加空白底图
|
|
|
+ L.tileLayer('').addTo(this.townshipMapInstance);
|
|
|
+
|
|
|
+ // 处理GeoJSON数据
|
|
|
+ const geoJsonLayer = L.geoJSON(this.townshipGeoJson, {
|
|
|
+ style: (feature) => {
|
|
|
+ const townName = feature.properties.name;
|
|
|
+ const townData = this.dataMap[townName];
|
|
|
+ let avgValue = null;
|
|
|
+
|
|
|
+ if (townData?.data?.基础统计) {
|
|
|
+ avgValue = Number(townData.data.基础统计.平均值);
|
|
|
+ }
|
|
|
+
|
|
|
+ return {
|
|
|
+ fillColor: this.getColorByCdValue(avgValue),
|
|
|
+ weight: 2,
|
|
|
+ opacity: 1,
|
|
|
+ color: '#000',
|
|
|
+ dashArray: '',
|
|
|
+ fillOpacity: 0.7
|
|
|
+ };
|
|
|
+ },
|
|
|
+ onEachFeature: (feature, layer) => {
|
|
|
+ const townName = feature.properties.name;
|
|
|
+
|
|
|
+ // 保存原始样式,用于恢复
|
|
|
+ const originalStyle = {
|
|
|
+ weight: 2,
|
|
|
+ color: '#000',
|
|
|
+ dashArray: '',
|
|
|
+ fillOpacity: 0.7,
|
|
|
+ fillColor: this.getColorByCdValue(this.dataMap[townName]?.data?.基础统计?.平均值)
|
|
|
+ };
|
|
|
+
|
|
|
+ // 鼠标悬停事件 - 修复版本
|
|
|
+ layer.on('mouseover', (e) => {
|
|
|
+ // 高亮当前区域
|
|
|
+ layer.setStyle({
|
|
|
+ weight: 6,
|
|
|
+ color: '#0066CC',
|
|
|
+ dashArray: '',
|
|
|
+ fillOpacity: 0.95
|
|
|
+ });
|
|
|
+
|
|
|
+ // 显示tooltip
|
|
|
+ this.showTooltip(layer, townName, e.latlng);
|
|
|
+ });
|
|
|
+
|
|
|
+ // 鼠标离开事件 - 修复版本
|
|
|
+ layer.on('mouseout', (e) => {
|
|
|
+ // 恢复原始样式 - 使用保存的样式而不是resetStyle
|
|
|
+ layer.setStyle(originalStyle);
|
|
|
+
|
|
|
+ // 正确关闭tooltip
|
|
|
+ if (layer._tooltip) {
|
|
|
+ layer.closeTooltip();
|
|
|
}
|
|
|
- ]
|
|
|
- };
|
|
|
+ });
|
|
|
+
|
|
|
+ // 点击事件
|
|
|
+ layer.on('click', () => {
|
|
|
+ this.$message.info(`${townName} 的Cd含量数据已显示`);
|
|
|
+ });
|
|
|
+ }
|
|
|
+ }).addTo(this.townshipMapInstance);
|
|
|
|
|
|
- this.townshipMapInstance.setOption(option);
|
|
|
- // 监听鼠标悬浮事件
|
|
|
- this.townshipMapInstance.on('mouseover', async (params) => {
|
|
|
- if (params.componentType === 'series' && params.seriesType === 'map') {
|
|
|
- const townName = params.name;
|
|
|
- this.currentTooltipTown = townName;
|
|
|
- this.isTooltipLoading = true;
|
|
|
- this.currentTooltipData = null;
|
|
|
- // 手动触发 tooltip 更新
|
|
|
- this.townshipMapInstance.setOption({ tooltip: {} });
|
|
|
- const detail = await this.fetchTownshipDetailByName(townName);
|
|
|
- this.isTooltipLoading = false;
|
|
|
- this.currentTooltipData = detail;
|
|
|
- // 再次手动触发 tooltip 更新,显示获取到的数据
|
|
|
- this.townshipMapInstance.setOption({ tooltip: {} });
|
|
|
+ // 调整地图视野
|
|
|
+ if (geoJsonLayer.getBounds().isValid()) {
|
|
|
+ // 方式1:完全禁用 fitBounds,直接设置中心和zoom
|
|
|
+ const bounds = geoJsonLayer.getBounds();
|
|
|
+ const center = bounds.getCenter();
|
|
|
+
|
|
|
+ // 直接设置视图,不使用 fitBounds
|
|
|
+ this.townshipMapInstance.setView(center, 10.5); // 直接设置zoom级别为11
|
|
|
+
|
|
|
+ console.log(`直接设置地图中心: ${center.lat}, ${center.lng}, zoom: 11`);
|
|
|
+
|
|
|
+ } else {
|
|
|
+ // 设置默认视图
|
|
|
+ this.townshipMapInstance.setView([25.0, 113.0], 10);
|
|
|
}
|
|
|
- });
|
|
|
- // 监听鼠标移出事件,重置状态
|
|
|
- this.townshipMapInstance.on('mouseout', () => {
|
|
|
- this.currentTooltipTown = '';
|
|
|
- this.currentTooltipData = null;
|
|
|
- this.isTooltipLoading = false;
|
|
|
- });
|
|
|
- // 5. 渲染地图
|
|
|
- this.townshipMapInstance.setOption(option);
|
|
|
+
|
|
|
+ console.log('地图渲染完成');
|
|
|
|
|
|
- // 6. 关键修复:添加延迟resize确保地图正确渲染
|
|
|
- setTimeout(() => {
|
|
|
- if (this.townshipMapInstance) {
|
|
|
- this.townshipMapInstance.resize();
|
|
|
- }
|
|
|
- }, 100);
|
|
|
- },
|
|
|
+ } catch (error) {
|
|
|
+ console.error('渲染地图时发生错误:', error);
|
|
|
+ this.$message.error('地图渲染失败: ' + error.message);
|
|
|
+ }
|
|
|
+},
|
|
|
+
|
|
|
+ // 新增:显示tooltip的方法
|
|
|
+// 修复显示tooltip的方法
|
|
|
+showTooltip(layer, townName, latlng) {
|
|
|
+ const townData = this.dataMap[townName];
|
|
|
+ let tooltipContent = `<div class="town-tooltip"><h3>${townName}</h3>`;
|
|
|
+
|
|
|
+ if (!townData?.data?.基础统计) {
|
|
|
+ tooltipContent += '<p>数据加载中...</p></div>';
|
|
|
+ } else {
|
|
|
+ const stats = townData.data.基础统计;
|
|
|
+ const dist = townData.data.分布统计表格;
|
|
|
+ tooltipContent += `
|
|
|
+ <div style="height:1px;background:#0066CC;margin:5px 0;"></div>
|
|
|
+ <p><strong>样本数量:</strong> ${stats.采样点数量 ?? '无数据'}</p>
|
|
|
+ <p><strong>平均值:</strong> ${stats.平均值?.toFixed(4) ?? '无数据'} mg/kg</p>
|
|
|
+ <p><strong>最小值:</strong> ${stats.最小值?.toFixed(4) ?? '无数据'} mg/kg</p>
|
|
|
+ <p><strong>最大值:</strong> ${stats.最大值?.toFixed(4) ?? '无数据'} mg/kg</p>
|
|
|
+ <div style="height:1px;background:#0066CC;margin:5px 0;"></div>
|
|
|
+ <p><strong>安全区间占比:</strong> ${dist?.汇总?.安全区间占比 ?? '无'}%</p>
|
|
|
+ <p><strong>预警区间占比:</strong> ${dist?.汇总?.预警区间占比 ?? '无'}%</p>
|
|
|
+ <p><strong>超标区间占比:</strong> ${dist?.汇总?.超标区间占比 ?? '无'}%</p>
|
|
|
+ </div>`;
|
|
|
+ }
|
|
|
+
|
|
|
+ // 先解除之前的tooltip绑定
|
|
|
+ if (layer._tooltip) {
|
|
|
+ layer.unbindTooltip();
|
|
|
+ }
|
|
|
+
|
|
|
+ // 使用Leaflet的tooltip
|
|
|
+ layer.bindTooltip(tooltipContent, {
|
|
|
+ className: 'custom-town-tooltip',
|
|
|
+ direction: 'top',
|
|
|
+ permanent: false,
|
|
|
+ sticky: true,
|
|
|
+ offset: [0, -10],
|
|
|
+ interactive: false // 确保tooltip不会拦截鼠标事件
|
|
|
+ });
|
|
|
+
|
|
|
+ // 打开tooltip
|
|
|
+ layer.openTooltip(latlng);
|
|
|
+},
|
|
|
|
|
|
// 上传并计算
|
|
|
async calculate() {
|
|
|
@@ -584,7 +810,7 @@ export default {
|
|
|
const formData = new FormData();
|
|
|
formData.append('area', this.countyName);
|
|
|
formData.append('data_file', this.selectedFile);
|
|
|
- formData.append('use_database', 'false'); // 使用上传的文件
|
|
|
+ formData.append('use_database', 'false');
|
|
|
|
|
|
// 调用作物Cd地图接口
|
|
|
const mapResponse = await api8000.post(
|
|
|
@@ -607,7 +833,7 @@ export default {
|
|
|
await this.fetchStatistics();
|
|
|
|
|
|
this.$message.success('计算完成!');
|
|
|
- await this.initTownshipMap();
|
|
|
+ await this.initTownshipMap();
|
|
|
|
|
|
} catch (error) {
|
|
|
console.error('计算失败:', error);
|
|
|
@@ -641,7 +867,7 @@ export default {
|
|
|
// 创建FormData
|
|
|
const formData = new FormData();
|
|
|
formData.append('area', this.countyName);
|
|
|
- formData.append('use_database', 'true'); // 使用数据库数据
|
|
|
+ formData.append('use_database', 'true');
|
|
|
|
|
|
// 调用作物Cd地图接口
|
|
|
const mapResponse = await api8000.post(
|
|
|
@@ -739,13 +965,6 @@ export default {
|
|
|
this.$message.error('导出数据失败: ' + (error.response?.data?.detail || '请稍后重试'));
|
|
|
}
|
|
|
},
|
|
|
-
|
|
|
- // 处理窗口大小变化
|
|
|
- handleResize() {
|
|
|
- if (this.townshipMapInstance) {
|
|
|
- this.townshipMapInstance.resize();
|
|
|
- }
|
|
|
- },
|
|
|
}
|
|
|
};
|
|
|
</script>
|
|
|
@@ -762,17 +981,91 @@ export default {
|
|
|
box-sizing: border-box;
|
|
|
}
|
|
|
|
|
|
+.township-map-wrapper {
|
|
|
+ position: relative;
|
|
|
+ width: 100%;
|
|
|
+ max-width: 1000px;
|
|
|
+ margin: 15px auto;
|
|
|
+ border: 1px solid #e0e0e0;
|
|
|
+ border-radius: 8px;
|
|
|
+ background: white;
|
|
|
+ box-shadow: 0 2px 8px rgba(0,0,0,0.1);
|
|
|
+ overflow: hidden;
|
|
|
+}
|
|
|
+
|
|
|
+.town-map-title {
|
|
|
+ text-align: center;
|
|
|
+ font-size: 18px;
|
|
|
+ font-weight: bold;
|
|
|
+ background: linear-gradient(135deg, #47C3B9, #36a897);
|
|
|
+ color: white;
|
|
|
+ margin: 0;
|
|
|
+ padding: 12px 0;
|
|
|
+ border-bottom: 1px solid #e0e0e0;
|
|
|
+}
|
|
|
+
|
|
|
+/* 图例样式 */
|
|
|
+.legend {
|
|
|
+ display: flex;
|
|
|
+ flex-wrap: wrap;
|
|
|
+ gap: 15px;
|
|
|
+ margin: 15px 0;
|
|
|
+ padding: 10px;
|
|
|
+ background-color: rgba(255, 255, 255, 0.8);
|
|
|
+ border-radius: 4px;
|
|
|
+}
|
|
|
+
|
|
|
+.legend-item {
|
|
|
+ display: flex;
|
|
|
+ align-items: center;
|
|
|
+ gap: 5px;
|
|
|
+}
|
|
|
+
|
|
|
+.color-box {
|
|
|
+ width: 20px;
|
|
|
+ height: 20px;
|
|
|
+ border: 1px solid #999;
|
|
|
+}
|
|
|
+
|
|
|
+.town-tooltip {
|
|
|
+ min-width: 200px;
|
|
|
+ max-width: 300px;
|
|
|
+}
|
|
|
+
|
|
|
+.town-tooltip h3 {
|
|
|
+ margin: 0 0 5px;
|
|
|
+ color: #0066CC;
|
|
|
+ text-align: center;
|
|
|
+ font-size: 16px;
|
|
|
+}
|
|
|
|
|
|
+.town-tooltip p {
|
|
|
+ margin: 2px 0;
|
|
|
+ font-size: 12px;
|
|
|
+ line-height: 1.4;
|
|
|
+}
|
|
|
+
|
|
|
+/* Leaflet地图容器样式 */
|
|
|
.township-map-container {
|
|
|
- width: 90%; /* 使用百分比宽度 */
|
|
|
- max-width: 1000px; /* 最大宽度限制 */
|
|
|
- height: 500px;
|
|
|
+ width: 100% !important;
|
|
|
+ max-width: 1000px;
|
|
|
+ height: 800px !important;
|
|
|
border-radius: 4px;
|
|
|
- background-color: #fff;
|
|
|
- margin: 15px auto; /* 上下15px,水平自动居中 */
|
|
|
+ background-color: #f5f5f5;
|
|
|
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
|
|
+ z-index: 10;
|
|
|
+ position: relative;
|
|
|
+ margin: 0 auto;
|
|
|
}
|
|
|
|
|
|
+/* Leaflet自定义tooltip样式 */
|
|
|
+::v-deep .custom-tooltip {
|
|
|
+ background-color: rgba(255, 255, 255, 0.95) !important;
|
|
|
+ border: 1px solid #ccc !important;
|
|
|
+ border-radius: 4px !important;
|
|
|
+ padding: 5px !important;
|
|
|
+ box-shadow: 0 1px 5px rgba(0,0,0,0.2) !important;
|
|
|
+}
|
|
|
|
|
|
.toolbar {
|
|
|
display: flex;
|
|
|
@@ -838,8 +1131,8 @@ export default {
|
|
|
padding: 15px;
|
|
|
box-shadow: 0 2px 12px rgba(0, 0, 0, 0.1);
|
|
|
position: relative;
|
|
|
- min-height: 500px;
|
|
|
backdrop-filter: blur(5px);
|
|
|
+ overflow: visible !important;
|
|
|
}
|
|
|
|
|
|
.map-image {
|
|
|
@@ -933,55 +1226,6 @@ export default {
|
|
|
animation: rotate 2s linear infinite;
|
|
|
}
|
|
|
|
|
|
-/* 新增样式 */
|
|
|
-.summary-info {
|
|
|
- margin-top: 20px;
|
|
|
-}
|
|
|
-
|
|
|
-.card-header {
|
|
|
- font-weight: bold;
|
|
|
- color: #409EFF;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-items {
|
|
|
- display: flex;
|
|
|
- flex-direction: column;
|
|
|
- gap: 10px;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item {
|
|
|
- display: flex;
|
|
|
- justify-content: space-between;
|
|
|
- align-items: center;
|
|
|
- padding: 8px 0;
|
|
|
- border-bottom: 1px solid #ebeef5;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item:last-child {
|
|
|
- border-bottom: none;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item .label {
|
|
|
- font-weight: bold;
|
|
|
- color: #606266;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item .value {
|
|
|
- font-weight: bold;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item .value.safe {
|
|
|
- color: #67C23A;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item .value.warning {
|
|
|
- color: #E6A23C;
|
|
|
-}
|
|
|
-
|
|
|
-.summary-item .value.danger {
|
|
|
- color: #F56C6C;
|
|
|
-}
|
|
|
-
|
|
|
@keyframes rotate {
|
|
|
from {
|
|
|
transform: rotate(0deg);
|
|
|
@@ -1014,5 +1258,9 @@ export default {
|
|
|
.file-name {
|
|
|
text-align: center;
|
|
|
}
|
|
|
+
|
|
|
+ .township-map-container {
|
|
|
+ height: 500px !important;
|
|
|
+ }
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|