| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572573574575576577578579580581582583584585586587588589590591592593594595596597598599600601602603604605606607608609610611612613614615616617618619620621622623624625626627628629630631632633634635636637638639640641642643644645646647648649650651652653654655656657658659660661662663664665666667668669670671672673674675676677678679680681682683684685686687688689690691692693694695696697698699700701702703704705706707708709710711712713714715716717718719720721722723724725726727728729730731732733734735736737738739740741742743744745746747748749750751752753754755756757758759760761762763764765766767768769770771772773774775776777778779780781782783784785786787788789790791792793794795796797798799800801802803804805806807808809810811812813814815816817818819820821822823824825826827828829830831832833834835836837838839840841842843844845846847848849850851852853854855856857858859860861862863864865866867868869870871872873874875876877878879880881882883884885886887888889890891892893894895896897898899900901902903904905906907908909910911912913914915916917918919920921922923924925926927928929930931932933934935936937938939940941942943944945946947948949950951952953954955956957958959960961962963964965966967968969970971972973974975976977978979980981982983984985986987988989990991992993994995996997998999100010011002100310041005100610071008100910101011101210131014101510161017101810191020102110221023102410251026102710281029103010311032103310341035103610371038103910401041104210431044104510461047104810491050105110521053105410551056105710581059106010611062106310641065106610671068106910701071107210731074107510761077107810791080108110821083108410851086108710881089109010911092109310941095109610971098109911001101110211031104110511061107110811091110111111121113111411151116111711181119112011211122112311241125112611271128112911301131113211331134113511361137113811391140114111421143114411451146114711481149115011511152115311541155115611571158115911601161116211631164116511661167116811691170117111721173117411751176117711781179118011811182118311841185118611871188118911901191119211931194119511961197119811991200120112021203120412051206120712081209121012111212121312141215121612171218121912201221122212231224122512261227122812291230123112321233123412351236123712381239124012411242124312441245124612471248124912501251125212531254125512561257125812591260126112621263126412651266126712681269127012711272127312741275127612771278127912801281128212831284128512861287128812891290129112921293129412951296129712981299130013011302130313041305130613071308130913101311131213131314131513161317131813191320132113221323132413251326132713281329133013311332133313341335133613371338133913401341134213431344134513461347134813491350135113521353135413551356135713581359136013611362136313641365136613671368136913701371137213731374137513761377137813791380138113821383138413851386138713881389139013911392139313941395139613971398139914001401140214031404140514061407140814091410141114121413141414151416141714181419142014211422142314241425142614271428142914301431143214331434143514361437143814391440144114421443144414451446144714481449145014511452145314541455145614571458145914601461146214631464146514661467146814691470147114721473147414751476147714781479148014811482148314841485148614871488148914901491149214931494149514961497 |
- <script setup>
- // 安全区间 (0.0-0.2 mg/kg)
- // 预警区间 (0.2-0.3 mg/kg)
- // 超标区间 (≥0.3 mg/kg)
- import { ref, watch, onMounted, onBeforeUnmount, onUnmounted ,computed,nextTick} from 'vue'
- import L from 'leaflet'
- import 'leaflet/dist/leaflet.css'
- import { api8000 } from '@/utils/request'
- const samplePointsData = ref([])
- const cdSampleData = ref([]) // 第二个地图的 CD 数据
- const mapRef = ref(null)
- const mapRef2 = ref(null) // 第二个地图
- let map = null
- let map2 = null // 第二个地图实例
- let wmsLayer = null
- let wmsLayer2 = null // 第二个地图的 WMS 图层
- let geoJsonLayer = null
- // 统计信息
- const statistics = ref({
- totalBlocks: 0,
- avgPH: 0,
- strongAcidCount: 0,
- mildAcidCount: 0,
- normalCount: 0,
- maxPH: 0,
- minPH: 0,
- strongAcidArea: 0,
- mildAcidArea: 0,
- normalArea: 0,
- totalArea: 0,
- strongAcidPercent: 0,
- mildAcidPercent: 0,
- normalPercent: 0
- })
- // pH 分布个数计算
- const phDistribution = ref({
- range1: 0,
- range2: 0,
- range3: 0,
- })
- // 第二个地图的 CD 含量统计
- const cdStatistics = ref({
- totalBlocks: 0,
- avgCD: 0,
- safeCount: 0, // 安全区间 (0.0-0.2 mg/kg)
- warningCount: 0, // 预警区间 (0.2-0.3 mg/kg)
- exceedCount: 0, // 超标区间 (≥0.3 mg/kg)
- maxCD: 0,
- minCD: 0,
- safeArea: 0,
- warningArea: 0,
- exceedArea: 0,
- totalArea: 0,
- safePercent: 0,
- warningPercent: 0,
- exceedPercent: 0
- })
- // CD 含量分布个数计算
- const cdDistribution = ref({
- safe: 0, // 安全
- warning: 0, // 预警
- exceed: 0 // 超标
- })
- const selectedPoint = ref(null)
- // 地图配置
- const CONFIG = {
- center:[25.222903, 113.25383],
- zoom:10, // 调小缩放级别,显示更大范围
- // 获取所有点的 API 地址
- getPoint:'/api/vector/export/all?table_name=le_origin_ph_map&format=geojson',
- geoserver:{
- url:'/geoserver',
- workspace:'acidmap',
- layerGroup:'leshujukanbanmap',
- dataLayer:'le_origin_ph_map',
- wmsUrl:'/geoserver/acidmap/wms',
- phField: 'ph_mean'
- },
- reboundGeoserver:{
- url:'/geoserver',
- workspace:'acidmap',
- layerGroup:'le_reflux_map',
- dataLayer:'le_data_reflux_result',
- wmsUrl:'/geoserver/acidmap/wms',
- phField: 'le_data__4'
- },
- cdGeoserver:{
- url:'/geoserver',
- workspace:'acidmap',
- layerGroup:'CropCd_block_map_with_boundary',
- dataLayer:'Crop_cd_block_map',
- wmsUrl:'/geoserver/acidmap/wms',
- cdField: 'CropCd_mea'
- },
- reboundCdGeoserver:{
- url:'/geoserver',
- workspace:'acidmap',
- layerGroup:'PrediCropCd_block_with_boundary',
- dataLayer:'Predi_Cropcd_block',
- wmsUrl:'/geoserver/acidmap/wms',
- cdField: 'sceCropCd_'
- }
- }
- // 图层配置已验证:正常地图使用 le_data_block_map,反酸地图使用 le_data_reflux_result
- const currentMapType = ref('normal')
- // 获取 pH 等级对应的 CSS 类
- function getPHLevelClass(ph) {
- // ✅ 先转换为数字
- const numericPh = typeof ph === 'string' ? parseFloat(ph) : ph
- if (!numericPh || numericPh === 0) return ''
- if (numericPh <= 5.2) return 'danger'
- if (numericPh < 6.0) return 'warning'
- return 'success'
- }
- // 获取综合评语
- function getPHComment(avgPH) {
- if (avgPH <= 4.5) return '🔴 土壤严重酸化,需立即治理!'
- if (avgPH <= 5.2) return '🟠 土壤酸化明显,建议尽快改良'
- if (avgPH < 6.0) return '🟡 土壤微酸性,注意保持'
- return '🟢 土壤酸碱度适宜,状态良好'
- }
-
- function getPHRangeLabel(range) {
- const labels = {
- range1: '< 5.2',
- range2: '5.2 - 6.0',
- range3: ' >6.0 ',
- }
- return labels[range] || range
- }
- // 添加分页加载支持
- const batchSize = 1000;
- let allFeatures = [];
- async function initMap(){
- await nextTick()
- if(!mapRef.value) return
- map = L.map(mapRef.value).setView(CONFIG.center,CONFIG.zoom)
- wmsLayer = L.tileLayer.wms(CONFIG.geoserver.wmsUrl, {
- layers: `${CONFIG.geoserver.workspace}:${CONFIG.geoserver.layerGroup}`,
- format: 'image/png',
- transparent: true,
- version: '1.1.0',
- srs:'EPSG:4326',
- attribution: '© GeoServer - Acidmap'
- }).addTo(map)
- }
- async function initMap2(){
- await nextTick()
- if(!mapRef2.value) return
- map2 = L.map(mapRef2.value).setView(CONFIG.center,CONFIG.zoom)
- // 第二个地图展示 CD 图层
- wmsLayer2 = L.tileLayer.wms(CONFIG.cdGeoserver.wmsUrl, {
- layers: `${CONFIG.cdGeoserver.workspace}:${CONFIG.cdGeoserver.layerGroup}`,
- format: 'image/png',
- transparent: true,
- version: '1.1.0',
- srs:'EPSG:4326',
- attribution: '© GeoServer - Crop CD'
- }).addTo(map2)
- }
- async function switchMap(mapType) {
- if (!map || !wmsLayer) return
- if (!map2 || !wmsLayer2) return
-
- console.log('🔄 开始切换地图,目标类型:', mapType)
- console.log('️ map2 存在:', !!map2, 'wmsLayer2 存在:', !!wmsLayer2)
-
- currentMapType.value = mapType
-
- // 切换 pH 地图
- const config = mapType === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
- console.log('📊 pH 配置:', config.layerGroup, config.dataLayer)
-
- map.removeLayer(wmsLayer)
-
- wmsLayer = L.tileLayer.wms(config.wmsUrl, {
- layers: `${config.workspace}:${config.layerGroup}`,
- format: 'image/png',
- transparent: true,
- version: '1.1.0',
- srs:'EPSG:4326',
- attribution: '© GeoServer - Acidmap'
- }).addTo(map)
-
- console.log('✅ pH 地图图层已切换')
-
- // 切换 CD 地图
- const cdConfig = mapType === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
- console.log(' CD 配置:', cdConfig.layerGroup, cdConfig.dataLayer, cdConfig.cdField)
- console.log('🔗 CD WMS 完整图层名:', `${cdConfig.workspace}:${cdConfig.layerGroup}`)
-
- // 移除旧图层(和 pH 地图使用相同的方式)
- map2.removeLayer(wmsLayer2)
-
- // 创建新图层并添加到地图
- wmsLayer2 = L.tileLayer.wms(cdConfig.wmsUrl, {
- layers: `${cdConfig.workspace}:${cdConfig.layerGroup}`,
- format: 'image/png',
- transparent: true,
- version: '1.1.0',
- srs:'EPSG:4326',
- attribution: '© GeoServer - Crop CD'
- }).addTo(map2)
-
- // 监听瓦片加载事件
- wmsLayer2.on('tileload', function(e) {
- console.log(' CD 瓦片加载成功:', e.tile.src)
- })
- wmsLayer2.on('tileerror', function(e) {
- console.error(' CD 瓦片加载失败! 完整URL:', e.tile.src, '错误信息:', e.error)
- })
- console.log('✅ CD 地图图层已切换,新图层:', wmsLayer2.options.layers)
-
- // 刷新地图渲染
- await nextTick()
- map2.invalidateSize()
- wmsLayer2.redraw()
-
- // 重新加载 pH 地图的数据和统计
- await loadMapData()
-
- // 重新加载 CD 地图的数据和统计
- await loadCDData()
- console.log('🎉 地图切换完成')
- }
-
- // 加载当前地图类型的数据
- async function loadMapData() {
- try {
- const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
-
- const url = `${config.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${config.workspace}:${config.dataLayer}&outputFormat=application/json`
-
- // console.log('🔍 请求 WFS 数据:', url);
-
- const response = await fetch(url);
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error('❌ WFS 响应错误:', errorText);
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const text = await response.text();
- // console.log('📦 原始响应:', text.substring(0, 200));
-
- // 尝试解析 JSON
- let geoJsonData;
- try {
- geoJsonData = JSON.parse(text);
- } catch (parseError) {
- console.error('❌ JSON 解析失败,响应内容:', text);
- throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
- }
-
- // console.log('✅ 获取到地图数据:', geoJsonData.features?.length || 0, '条记录');
- // 保存数据用于统计和交互
- allFeatures = geoJsonData.features;
- samplePointsData.value = geoJsonData.features;
-
- // 计算统计信息
- calculatePHDistribution(allFeatures);
- loadStatistics();
-
- } catch (err) {
- console.error('❌ 加载数据失败:', err);
- }
- }
- function createPointIcon(feature) {
- const ph = feature.properties.ph || feature.properties.value
- let color = '#22c55e'
- if (ph <= 5.2) color = '#ef4444'
- else if (ph < 6.0) color = '#f59e0b'
- return L.divIcon({
- className: 'custom-marker',
- html: `<div style="
- background-color: ${color};
- width: 12px;
- height: 12px;
- border-radius: 50%;
- border: 2px solid white;
- box-shadow: 0 2px 4px rgba(0,0,0,0.3);
- "></div>`,
- iconSize: [12, 12],
- iconAnchor: [6, 6]
- })
- }
- function parsePHValue(phValue) {
- if (!phValue && phValue !== 0) return null
- const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
- return !isNaN(numericPh) && numericPh > 0 ? numericPh : null
- }
- async function loadStatistics() {
- try {
- // 等待数据加载完成
- if (samplePointsData.value.length === 0) return;
-
- // 根据当前地图类型选择 pH 字段
- const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
- let phCount = 0
- let avgPH = 0
- let strongAcidCount = 0
- let mildAcidCount = 0
- let normalCount = 0
- let maxPH = -Infinity
- let minPH = Infinity
- let strongAcidArea = 0
- let mildAcidArea = 0
- let normalArea = 0
- let totalArea = 0
- samplePointsData.value.forEach(feature => {
- const numericPh = parsePHValue(feature.properties[config.phField])
- const area = parseFloat(feature.properties.area) || 0
-
- // 累加总面积(所有地块)
- if (area > 0) {
- totalArea += area
- }
-
- // 过滤无效 pH 值,只统计有数据的地块
- if (numericPh && numericPh > 0 && !isNaN(numericPh)) {
- phCount++
- const delta = numericPh - avgPH;
- avgPH = avgPH + delta / phCount;
- maxPH = Math.max(maxPH, numericPh)
- minPH = Math.min(minPH, numericPh)
-
- if (numericPh <= 5.2) {
- strongAcidCount++
- strongAcidArea += area
- }
- else if (numericPh < 6.0) {
- mildAcidCount++
- mildAcidArea += area
- }
- else {
- normalCount++
- normalArea += area
- }
- }
- });
- // 计算有 pH 数据的地块总面积(用于占比计算)
- const totalPHArea = strongAcidArea + mildAcidArea + normalArea;
-
- // 计算面积占比(基于有 pH 数据的地块)
- const strongAcidPercent = totalPHArea > 0 ? parseFloat(((strongAcidArea / totalPHArea) * 100).toFixed(2)) : 0
- const mildAcidPercent = totalPHArea > 0 ? parseFloat(((mildAcidArea / totalPHArea) * 100).toFixed(2)) : 0
- const normalPercent = totalPHArea > 0 ? parseFloat(((normalArea / totalPHArea) * 100).toFixed(2)) : 0
- statistics.value = {
- totalBlocks: samplePointsData.value.length,
- avgPH: phCount > 0 ? parseFloat(avgPH.toFixed(2)) : 0, // ✅ 确保是数字
- strongAcidCount,
- mildAcidCount,
- normalCount,
- maxPH: maxPH === -Infinity ? 0 : parseFloat(maxPH.toFixed(2)),
- minPH: minPH === Infinity ? 0 : parseFloat(minPH.toFixed(2)),
- strongAcidArea: parseFloat(strongAcidArea.toFixed(2)),
- mildAcidArea: parseFloat(mildAcidArea.toFixed(2)),
- normalArea: parseFloat(normalArea.toFixed(2)),
- totalArea: parseFloat(totalArea.toFixed(2)),
- strongAcidPercent,
- mildAcidPercent,
- normalPercent
- };
- // console.log('✅ 统计数据加载完成:', statistics.value);
- } catch (err) {
- console.error('加载统计数据失败:', err);
- }
- }
- // 加载第二个地图的 CD 含量统计数据
- async function loadCDStatistics() {
- try {
- // 等待数据加载完成
- if (cdSampleData.value.length === 0) return;
-
- const cdConfig = currentMapType.value === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
-
- let cdCount = 0
- let avgCD = 0
- let safeCount = 0 // 0.0-0.2 mg/kg
- let warningCount = 0 // 0.2-0.3 mg/kg
- let exceedCount = 0 // ≥0.3 mg/kg
- let maxCD = -Infinity
- let minCD = Infinity
- let safeArea = 0
- let warningArea = 0
- let exceedArea = 0
- let totalArea = 0
- cdSampleData.value.forEach(feature => {
- // 获取 CD 含量和面积
- const cdValue = feature.properties[cdConfig.cdField]
- const numericCd = typeof cdValue === 'string' ? parseFloat(cdValue) : cdValue
- const area = parseFloat(feature.properties.area) || 0
-
- // 累加总面积
- if (area > 0) {
- totalArea += area
- }
-
- if (numericCd !== null && numericCd !== undefined && !isNaN(numericCd) && numericCd >= 0) {
- cdCount++
- const delta = numericCd - avgCD;
- avgCD = avgCD + delta / cdCount;
- maxCD = Math.max(maxCD, numericCd)
- minCD = Math.min(minCD, numericCd)
-
- // 根据规则分类并累加面积
- if (numericCd < 0.2) {
- safeCount++
- safeArea += area
- }
- else if (numericCd < 0.3) {
- warningCount++
- warningArea += area
- }
- else {
- exceedCount++
- exceedArea += area
- }
- }
- });
- // 计算有 CD 数据的地块总面积(用于占比计算)
- const totalCDArea = safeArea + warningArea + exceedArea;
-
- // 计算面积占比(基于有 CD 数据的地块)
- const safePercent = totalCDArea > 0 ? parseFloat(((safeArea / totalCDArea) * 100).toFixed(2)) : 0
- const warningPercent = totalCDArea > 0 ? parseFloat(((warningArea / totalCDArea) * 100).toFixed(2)) : 0
- const exceedPercent = totalCDArea > 0 ? parseFloat(((exceedArea / totalCDArea) * 100).toFixed(2)) : 0
- cdStatistics.value = {
- totalBlocks: cdSampleData.value.length,
- avgCD: cdCount > 0 ? parseFloat(avgCD.toFixed(3)) : 0,
- safeCount,
- warningCount,
- exceedCount,
- maxCD: maxCD === -Infinity ? 0 : parseFloat(maxCD.toFixed(3)),
- minCD: minCD === Infinity ? 0 : parseFloat(minCD.toFixed(3)),
- safeArea: parseFloat(safeArea.toFixed(2)),
- warningArea: parseFloat(warningArea.toFixed(2)),
- exceedArea: parseFloat(exceedArea.toFixed(2)),
- totalArea: parseFloat(totalArea.toFixed(2)),
- safePercent,
- warningPercent,
- exceedPercent
- };
-
- // 计算 CD 含量分布
- cdDistribution.value = {
- safe: safeCount,
- warning: warningCount,
- exceed: exceedCount
- };
- // console.log('✅ CD 统计数据加载完成:', cdStatistics.value);
- } catch (err) {
- console.error('加载 CD 统计数据失败:', err);
- }
- }
- // 加载第二个地图的 CD 数据
- async function loadCDData() {
- try {
- const cdConfig = currentMapType.value === 'rebound' ? CONFIG.reboundCdGeoserver : CONFIG.cdGeoserver
-
- const url = `${cdConfig.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${cdConfig.workspace}:${cdConfig.dataLayer}&outputFormat=application/json`
-
- const response = await fetch(url);
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error('❌ WFS 响应错误:', errorText);
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const text = await response.text();
-
- // 尝试解析 JSON
- let geoJsonData;
- try {
- geoJsonData = JSON.parse(text);
- } catch (parseError) {
- console.error('❌ JSON 解析失败,响应内容:', text);
- throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
- }
-
- // 保存 CD 数据
- cdSampleData.value = geoJsonData.features;
-
- // 计算 CD 统计信息
- loadCDStatistics();
-
- } catch (err) {
- console.error('❌ 加载 CD 数据失败:', err);
- }
- }
- // 修改为只获取数据用于统计,不渲染到地图
- async function fetchDataForStatistics() {
- try {
- const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
-
- const url = `${config.url}/ows?service=WFS&version=1.0.0&request=GetFeature&typeName=${config.workspace}:${config.dataLayer}&outputFormat=application/json`
-
- // console.log('🔍 请求 WFS 数据:', url);
-
- const response = await fetch(url);
-
- if (!response.ok) {
- const errorText = await response.text();
- console.error('❌ WFS 响应错误:', errorText);
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const text = await response.text();
- // console.log('📦 原始响应:', text.substring(0, 200));
-
- // 尝试解析 JSON
- let geoJsonData;
- try {
- geoJsonData = JSON.parse(text);
- } catch (parseError) {
- console.error('❌ JSON 解析失败,响应内容:', text);
- throw new Error('GeoServer 返回的数据格式不正确,不是有效的 JSON');
- }
-
- // console.log('✅ 获取到数据:', geoJsonData.features?.length || 0, '条记录');
-
- // 调试:打印第一个要素的 properties
- // if (geoJsonData.features && geoJsonData.features.length > 0) {
- // console.log('📋 第一个要素的 properties:', geoJsonData.features[0].properties);
- // console.log('📋 pH 字段值 (ph_mean):', geoJsonData.features[0].properties.ph_mean);
- // console.log('📋 pH 字段值 (le_data__4):', geoJsonData.features[0].properties.le_data__4);
- // }
- // 保存数据用于统计和交互
- allFeatures = geoJsonData.features;
- samplePointsData.value = geoJsonData.features;
-
- // 计算统计信息
- calculatePHDistribution(allFeatures);
- loadStatistics();
-
- // 添加点击事件监听
- map.on('click', handleMapClick);
-
- } catch (err) {
- console.error('❌ 加载统计数据失败:', err);
- }
- }
- // 计算 pH 分布
- function calculatePHDistribution(features) {
- const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
- const distribution = {
- range1: 0,
- range2: 0,
- range3: 0
- }
- features.forEach(feature => {
- const phValue = feature.properties[config.phField]
- const numericPh = typeof phValue === 'string' ? parseFloat(phValue) : phValue
- if (numericPh && numericPh > 0) {
- if (numericPh <= 5.2) distribution.range1++
- else if (numericPh <= 6) distribution.range2++
- else distribution.range3++
- }
- })
- phDistribution.value = distribution
- }
- // 处理地图点击,查询地块属性
- async function handleMapClick(e) {
- const latlng = e.latlng;
- // console.log('🔍 地图点击:', latlng);
-
- try {
- // 根据当前地图类型选择配置
- const config = currentMapType.value === 'rebound' ? CONFIG.reboundGeoserver : CONFIG.geoserver
-
- // console.log('📋 当前地图类型:', currentMapType.value);
- // console.log('📋 查询的数据层:', config.dataLayer);
-
- // 构建 WMS GetFeatureInfo 请求
- const size = map.getSize();
- const point = map.latLngToContainerPoint(latlng);
-
- const params = new URLSearchParams({
- SERVICE: 'WMS',
- VERSION: '1.1.1',
- REQUEST: 'GetFeatureInfo',
- LAYERS: `${config.workspace}:${config.dataLayer}`,
- QUERY_LAYERS: `${config.workspace}:${config.dataLayer}`,
- STYLES: '',
- SRS: 'EPSG:4326',
- BBOX: map.getBounds().toBBoxString(),
- WIDTH: size.x,
- HEIGHT: size.y,
- X: Math.round(point.x),
- Y: Math.round(point.y),
- INFO_FORMAT: 'application/json',
- FEATURE_COUNT: 1
- });
-
- const url = `${config.wmsUrl}?${params.toString()}`;
- // console.log('🔍 查询 URL:', url);
-
- const response = await fetch(url);
-
- if (!response.ok) {
- throw new Error(`HTTP error! status: ${response.status}`);
- }
-
- const data = await response.json();
-
- // console.log('� 查询结果:', data);
-
- if (data.features && data.features.length > 0) {
- const feature = data.features[0];
- const ph = parsePHValue(feature.properties[config.phField]);
-
- // console.log('📋 pH 字段:', config.phField);
- // console.log('📋 pH 值:', ph);
- // console.log('📋 完整属性:', feature.properties);
-
- selectedPoint.value = {
- ph: ph,
- properties: feature.properties
- };
-
- // console.log('✅ 选中地块:', selectedPoint.value);
- } else {
- // console.log('⚠️ 该位置没有地块数据');
- selectedPoint.value = null;
- }
-
- } catch (err) {
- console.error('❌ 查询地块信息失败:', err);
- alert('查询地块信息失败,请重试');
- }
- }
- onUnmounted(()=>{
- if(map) {
- map.off('click', handleMapClick) // 移除点击事件监听
- map.remove()
- map = null
- }
- if(wmsLayer) {
- wmsLayer = null
- }
- if(markersLayer) {
- markersLayer = null
- }
- if(geoJsonLayer) { // 原 markersLayer 改为 geoJsonLayer(代码中实际定义的是 geoJsonLayer)
- geoJsonLayer = null
- }
- samplePointsData.value = []
- selectedPoint.value = null
- })
- onMounted(async()=>{
- await initMap()
- await initMap2() // 初始化第二个地图
- await fetchDataForStatistics()
- await loadStatistics()
- await loadCDData() // 加载 CD 数据
- // loadCDStatistics() 会在 loadCDData 完成后自动调用
- })
- onUnmounted(()=>{
- if(map) {
- map.remove()
- map = null
- }
- if(map2) {
- map2.remove()
- map2 = null
- }
- })
- </script>
- <template>
- <div class="map-container">
- <div class="ph-map" ref="mapRef"></div>
- <div class="ph-map" ref="mapRef2"></div>
- <!-- 计算刷新按钮 -->
- <div class="compute">
- <button class="combtn" @click="switchMap('normal')">实施降酸措施一周期后</button>
- <button class="combtn" @click="switchMap('rebound')">反酸一周期后</button>
- </div>
- <!-- pH 统计 -->
- <div class="statistics-panel">
- <h4>📊 韶关土壤 pH 统计</h4>
-
- <div class="stat-row">
- <span class="stat-label">地块总数:</span>
- <span class="stat-value">{{ statistics.totalBlocks }} 个</span>
- </div>
-
- <div class="stat-row highlight">
- <span class="stat-label">平均 pH 值:</span>
- <span class="stat-value" :class="getPHLevelClass(parseFloat(statistics.avgPH))">
- {{ statistics.avgPH || '-' }}
- </span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">强酸性 (pH≤5.2):</span>
- <span class="stat-value danger">{{ statistics.strongAcidCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">强酸性面积占比:</span>
- <span class="stat-value danger">{{ statistics.strongAcidPercent }}%</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">弱酸性 (pH 5.2~6.0): </span>
- <span class="stat-value warning">{{ statistics.mildAcidCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">弱酸性面积占比:</span>
- <span class="stat-value warning">{{ statistics.mildAcidPercent }}%</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">正常 (pH≥6.0):</span>
- <span class="stat-value success">{{ statistics.normalCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">正常面积占比:</span>
- <span class="stat-value success">{{ statistics.normalPercent }}%</span>
- </div>
-
- <div class="stat-divider"></div>
-
- <div class="stat-row small">
- <span class="stat-label">总面积:</span>
- <span class="stat-value">{{ statistics.totalArea }} 亩</span>
- </div>
-
- <div class="stat-row small">
- <span class="stat-label">最高 pH:</span>
- <span class="stat-value success">{{ statistics.maxPH || '-' }}</span>
- </div>
-
- <div class="stat-row small">
- <span class="stat-label">最低 pH:</span>
- <span class="stat-value danger">{{ statistics.minPH || '-' }}</span>
- </div>
- </div>
- <!-- CD 含量统计 -->
- <div class="statistics-panel" style="top: 550px;">
- <h4>🌾 作物 Cd 含量统计</h4>
-
- <div class="stat-row">
- <span class="stat-label">地块总数:</span>
- <span class="stat-value">{{ cdStatistics.totalBlocks }} 个</span>
- </div>
-
- <div class="stat-row highlight">
- <span class="stat-label">平均 Cd 含量:</span>
- <span class="stat-value" :class="cdStatistics.avgCD > 0 ? 'success' : 'danger'">
- {{ cdStatistics.avgCD > 0 ? cdStatistics.avgCD + ' mg/kg' : '-' }}
- </span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">安全 (<0.2 mg/kg):</span>
- <span class="stat-value success">{{ cdStatistics.safeCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">安全面积占比:</span>
- <span class="stat-value success">{{ cdStatistics.safePercent }}%</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">预警 (0.2-0.3 mg/kg):</span>
- <span class="stat-value warning">{{ cdStatistics.warningCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">预警面积占比:</span>
- <span class="stat-value warning">{{ cdStatistics.warningPercent }}%</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">超标 (≥0.3 mg/kg):</span>
- <span class="stat-value danger">{{ cdStatistics.exceedCount }} 个</span>
- </div>
-
- <div class="stat-row">
- <span class="stat-label">超标面积占比:</span>
- <span class="stat-value danger">{{ cdStatistics.exceedPercent }}%</span>
- </div>
-
- <div class="stat-divider"></div>
-
- <div class="stat-row small">
- <span class="stat-label">总面积:</span>
- <span class="stat-value">{{ cdStatistics.totalArea }} 亩</span>
- </div>
-
- <div class="stat-row small">
- <span class="stat-label">最高 Cd:</span>
- <span class="stat-value danger">{{ cdStatistics.maxCD > 0 ? cdStatistics.maxCD + ' mg/kg' : '-' }}</span>
- </div>
-
- <div class="stat-row small">
- <span class="stat-label">最低 Cd:</span>
- <span class="stat-value success">{{ cdStatistics.minCD > 0 ? cdStatistics.minCD + ' mg/kg' : '-' }}</span>
- </div>
- </div>
- <!-- ph 分布 -->
- <div class="distribution-chart">
- <h4>📈 pH 分布</h4>
- <div class="bar-chart">
- <div
- v-for="(value, key) in phDistribution"
- :key="key"
- class="bar-item"
- >
- <div class="bar-label">{{ getPHRangeLabel(key) }}</div>
- <div class="bar-container">
- <div
- class="bar-fill"
- :style="{ width: `${(value / statistics.totalBlocks) * 100}%` }"
- :class="
- {
- danger: key === 'range1',
- warning: key === 'range2',
- success: key === 'range3'
- }
- "
- ></div>
- <span class="bar-value">{{ value }}</span>
- </div>
- </div>
- </div>
- </div>
- <!-- CD 含量分布 -->
- <div class="distribution-chart" style="top: 550px;">
- <h4>🌾 作物 Cd 含量分布</h4>
- <div class="bar-chart">
- <div class="bar-item">
- <div class="bar-label">安全 (<0.2)</div>
- <div class="bar-container">
- <div
- class="bar-fill success"
- :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.safe / cdStatistics.totalBlocks) * 100 : 0}%` }"
- ></div>
- <span class="bar-value">{{ cdDistribution.safe }}</span>
- </div>
- </div>
- <div class="bar-item">
- <div class="bar-label">预警 (0.2-0.3)</div>
- <div class="bar-container">
- <div
- class="bar-fill warning"
- :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.warning / cdStatistics.totalBlocks) * 100 : 0}%` }"
- ></div>
- <span class="bar-value">{{ cdDistribution.warning }}</span>
- </div>
- </div>
- <div class="bar-item">
- <div class="bar-label">超标 (≥0.3)</div>
- <div class="bar-container">
- <div
- class="bar-fill danger"
- :style="{ width: `${cdStatistics.totalBlocks > 0 ? (cdDistribution.exceed / cdStatistics.totalBlocks) * 100 : 0}%` }"
- ></div>
- <span class="bar-value">{{ cdDistribution.exceed }}</span>
- </div>
- </div>
- </div>
- </div>
- <div class="stat-comment" v-if="statistics.avgPH > 0">
- <div class="comment-title">📝 综合评估:</div>
- <div class="comment-text" :class="getPHLevelClass(statistics.avgPH)">
- {{ getPHComment(statistics.avgPH) }}
- </div>
- </div>
-
- <!-- 图例 -->
- <div class="legend">
- <h4>pH 值图例</h4>
- <div class="legend-item">
- <span class="legend-color" style="background: #ef4444;"></span>
- <span>≤ 5.2 (强酸性)</span>
- </div>
- <div class="legend-item">
- <span class="legend-color" style="background: #eab308;"></span>
- <span>5.2 - 6.0 (弱酸性)</span>
- </div>
- <div class="legend-item">
- <span class="legend-color" style="background: #22c55e;"></span>
- <span>≥ 6.0 (中性/碱性)</span>
- </div>
- <div class="legend-item">
- <span class="legend-color" style="background: #cccccc;"></span>
- <span>无数据</span>
- </div>
- </div>
- <!-- CD 含量图例 -->
- <div class="legend" style="top: 580px;">
- <h4>🌾 Cd 含量图例</h4>
- <div class="legend-item">
- <span class="legend-color" style="background: #22c55e;"></span>
- <span>0.0 - 0.2 mg/kg (安全)</span>
- </div>
- <div class="legend-item">
- <span class="legend-color" style="background: #eab308;"></span>
- <span>0.2-0.3 mg/kg (预警)</span>
- </div>
- <div class="legend-item">
- <span class="legend-color" style="background: #ef4444;"></span>
- <span>≥ 0.3 mg/kg (超标)</span>
- </div>
- </div>
- <!-- 采样点详情 -->
- <div class="point-detail-modal" v-if="selectedPoint">
- <div class="detail-content">
- <div class="detail-header">
- <h4>📍 采样点详情</h4>
- <button @click="selectedPoint = null" class="close-btn">×</button>
- </div>
- <div class="detail-row">
- <span class="detail-label">pH 值:</span>
- <span class="detail-value" :class="getPHLevelClass(selectedPoint.ph)">
- {{ selectedPoint.ph?.toFixed(2) || '-' }}
- </span>
- </div>
- <div class="detail-row">
- <span class="detail-label">酸化程度:</span>
- <span :class="['detail-value', getPHLevelClass(selectedPoint.ph)]">
- {{ !selectedPoint.ph || selectedPoint.ph <= 0 ? '无数据' : selectedPoint.ph <= 5.2 ? '强酸性' : selectedPoint.ph < 6.0 ? '弱酸性' : '正常' }}
- </span>
- </div>
- <div class="detail-row">
- <span class="detail-label">建议:</span>
- <span class="detail-suggestion">
- {{ !selectedPoint.ph || selectedPoint.ph <= 0 ? '无数据' :selectedPoint.ph <= 5.2 ? '立即治理,施用石灰改良' : selectedPoint.ph < 6.0 ? '注意保持,适量施用有机肥' : '继续保持当前管理措施' }}
- </span>
- </div>
- </div>
- </div>
- </div>
- </template>
- <style scoped>
- .map-container{
- width: 100%;
- height: 100vh;
- position: absolute;
- left:0;
- top:0;
- z-index: 1000;
- }
- .ph-map{
- width: 600px;
- height: 500px; /* 固定高度 */
- border-radius: 16px;
- border: 3px solid #1092d8;
- overflow: hidden;
- margin-bottom: 20px; /* 两个地图之间的间距 */
- }
- .ph-map:first-child {
- margin-bottom: 50px; /* 第一个地图下方留空 */
- }
- /* ✅ 统计面板样式 */
- .statistics-panel {
- position: absolute;
- top: 0px;
- left: 650px;
- background: rgba(255, 255, 255, 0.95);
- padding: 20px;
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- min-width: 350px;
- height: 450px;
- backdrop-filter: blur(10px);
- border: 2px solid rgba(16, 146, 216, 0.2);
- }
- .compute {
- position: absolute;
- display: flex;
- gap: 15px;
- top:5px;
- right: 20px;
- padding: 20px;
- z-index: 1000;
- }
- .combtn {
- padding: 15px 20px;
- font-size: 14px;
- font-weight: 600;
- color: #fff;
- background: linear-gradient(135deg, #1092d8 0%, #0d7bb8 100%);
- border: none;
- border-radius: 8px;
- cursor: pointer;
- box-shadow: 0 4px 12px rgba(16, 146, 216, 0.3);
- transition: all 0.3s ease;
- white-space: nowrap;
- }
- .combtn:hover {
- background: linear-gradient(135deg, #0d7bb8 0%, #0a6598 100%);
- box-shadow: 0 6px 16px rgba(16, 146, 216, 0.4);
- transform: translateY(-2px);
- }
- .combtn:active {
- transform: translateY(0);
- box-shadow: 0 2px 8px rgba(16, 146, 216, 0.3);
- }
- .statistics-panel h4 {
- margin: 0 0 15px 0;
- font-size: 18px;
- color: #1092d8;
- border-bottom: 2px solid #1092d8;
- padding-bottom: 10px;
- font-weight: bold;
- }
- .stat-row {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 12px;
- font-size: 14px;
- line-height: 1.4; /* 新增行高,提升可读性 */
- }
- .stat-row.highlight {
- background: linear-gradient(to right, rgba(16, 146, 216, 0.1), transparent);
- padding: 8px 12px;
- border-radius: 6px;
- margin-bottom: 15px;
- }
- .stat-row.small {
- font-size: 12px;
- margin-bottom: 6px;
- }
- .stat-label {
- color: #666;
- font-weight: 500;
- }
- .stat-value {
- font-weight: 600;
- font-size: 16px;
- color: #333;
- letter-spacing: 0.5px; /* 新增字间距 */
- }
- .stat-value.danger {
- color: #ef4444;
- }
- .stat-value.warning {
- color: #f59e0b;
- }
- .stat-value.success {
- color: #22c55e;
- }
- .stat-divider {
- height: 1px;
- background: linear-gradient(to right, transparent, #ddd, transparent);
- margin: 15px 0;
- }
- /* 综合评语 */
- .stat-comment {
- position: absolute;
- top: 300px; /* 放在 pH 分布统计下面 */
- right: 10px;
- padding: 15px;
- width: 350px; /* 和 pH 分布统计差不多的宽度 */
- background: rgba(255, 255, 255, 0.95);
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- backdrop-filter: blur(10px);
- border-left: 4px solid #1092d8;
- }
- .comment-title {
- font-size: 15px;
- color: #666;
- margin-bottom: 6px;
- font-weight: bold;
- }
- .comment-text {
- font-size: 14px;
- font-weight: bold;
- color: #333;
- }
- .comment-text.danger {
- color: #ef4444;
- }
- .comment-text.warning {
- color: #f59e0b;
- }
- .comment-text.success {
- color: #22c55e;
- }
- .distribution-chart {
- position: absolute;
- top:100px;
- right: 10px;
- background: rgba(255, 255, 255, 0.95);
- padding: 15px;
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- min-width: 350px;
- backdrop-filter: blur(10px);
- }
- .distribution-chart h4 {
- margin: 0 0 12px 0;
- font-size: 16px;
- color: #1092d8;
- border-bottom: 2px solid #1092d8;
- padding-bottom: 8px;
- }
- .bar-chart {
- display: flex;
- flex-direction: column;
- gap: 8px;
- }
- .bar-item {
- display: flex;
- align-items: center;
- gap: 8px;
- }
- .bar-label {
- font-size: 11px;
- color: #666;
- min-width: 60px;
- }
- .bar-container {
- flex: 1;
- position: relative;
- height: 20px;
- background: #f0f0f0;
- border-radius: 4px;
- overflow: hidden;
- border: 1px solid #e5e7eb;
- }
- .bar-fill {
- height: 100%;
- transition: width 0.3s ease-in-out; /* 缓动动画更丝滑 */
- border-radius: 3px; /* 圆角匹配容器 */
- }
- .bar-fill.danger {
- background: #ef4444;
- }
- .bar-fill.warning {
- background: #f59e0b;
- }
- .bar-fill.success {
- background: #22c55e;
- }
- .bar-value {
- position: absolute;
- right: 8px;
- top: 50%;
- transform: translateY(-50%);
- font-size: 11px;
- font-weight: bold;
- color: black;
- text-shadow: 0 1px 2px rgba(0,0,0,0.2); /* 新增文字阴影 */
- }
- .alert-panel {
- position: absolute;
- top: 20px;
- left: 10px;
- background: rgba(255, 255, 255, 0.95);
- padding: 15px;
- border-radius: 12px;
- box-shadow: 0 4px 20px rgba(0, 0, 0, 0.15);
- z-index: 1000;
- min-width: 250px;
- max-height: 300px;
- overflow-y: auto;
- backdrop-filter: blur(10px);
- }
- .alert-panel h4 {
- margin: 0 0 12px 0;
- font-size: 16px;
- color: #ef4444;
- border-bottom: 2px solid #ef4444;
- padding-bottom: 8px;
- }
- .alert-list {
- display: flex;
- flex-direction: column;
- gap: 6px;
- }
- .alert-item {
- display: flex;
- align-items: center;
- gap: 8px;
- padding: 8px;
- background: rgba(239, 68, 68, 0.05);
- border-radius: 6px;
- cursor: pointer;
- transition: all 0.3s;
- }
- .alert-item:hover {
- background: rgba(239, 68, 68, 0.15);
- transform: translateX(4px);
- }
- .alert-rank {
- background: #ef4444;
- color: white;
- width: 20px;
- height: 20px;
- border-radius: 50%;
- display: flex;
- align-items: center;
- justify-content: center;
- font-size: 12px;
- font-weight: bold;
- flex-shrink: 0;
- }
- .alert-name {
- flex: 1;
- font-size: 13px;
- color: #333;
- }
- .alert-ph {
- font-size: 12px;
- font-weight: bold;
- }
- .alert-ph.danger {
- color: #ef4444;
- }
- /* 图例样式 */
- .legend {
- position: absolute;
- top: 30px;
- left: 30px;
- background: rgba(255, 255, 255, 0.95);
- padding: 15px;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- min-width: 10px;
- backdrop-filter: blur(4px);
- z-index: 0;
- }
- .legend h4 {
- margin: 0 0 10px 0;
- font-size: 10px;
- color: #333;
- border-bottom: 2px solid #1092d8;
- padding-bottom: 6px;
- }
- .legend-item {
- display: flex;
- align-items: center;
- gap: 8px;
- margin-bottom: 6px;
- font-size: 8px;
- color: #555;
- }
- .legend-item:last-child {
- margin-bottom: 0;
- }
- .legend-color {
- width: 10px;
- height: 10px;
- border-radius: 4px;
- border: 1px solid #ddd;
- flex-shrink: 0;
- }
- .point-detail-modal {
- position: fixed;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- background: white;
- padding: 20px;
- border-radius: 12px;
- box-shadow: 0 8px 32px rgba(0, 0, 0, 0.3);
- z-index: 2000;
- min-width: 300px;
- border: 1px solid rgba(16, 146, 216, 0.2); /* 新增边框 */
- animation: fadeIn 0.3s ease; /* 新增淡入动画 */
- }
- /* 新增弹窗动画 */
- @keyframes fadeIn {
- from {
- opacity: 0;
- transform: translate(-50%, -45%);
- }
- to {
- opacity: 1;
- transform: translate(-50%, -50%);
- }
- }
- .detail-content {
- position: relative;
- }
- .detail-content h4 {
- margin: 0 0 15px 0;
- color: #1092d8;
- border-bottom: 2px solid #1092d8;
- padding-bottom: 10px;
- }
- .detail-row {
- display: flex;
- justify-content: space-between;
- margin-bottom: 12px;
- padding: 8px;
- background: #f8f9fa;
- border-radius: 6px;
- }
- .detail-label {
- font-weight: 500;
- color: #666;
- }
- .detail-value {
- font-weight: bold;
- color: #333;
- }
- .detail-value.danger {
- color: #ef4444;
- }
- .detail-value.warning {
- color: #f59e0b;
- }
- .detail-value.success {
- color: #22c55e;
- }
- .detail-suggestion {
- color: #1092d8;
- font-size: 13px;
- line-height: 1.5;
- }
- /* 自定义 Tooltip 样式 */
- :deep(.custom-tooltip) {
- background: rgba(255, 255, 255, 0.95);
- border: 2px solid #1092d8;
- border-radius: 8px;
- padding: 8px 12px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
- font-family: 'Microsoft YaHei', sans-serif;
- backdrop-filter: blur(4px);
- }
- .custom-marker {
- background: transparent !important;
- border: none !important;
- }
- .detail-content {
- position: relative;
- }
- .detail-header {
- display: flex;
- justify-content: space-between;
- align-items: center;
- margin-bottom: 15px;
- }
- .close-btn {
- background: none;
- border: none;
- font-size: 20px;
- cursor: pointer;
- color: #909399;
- padding: 0;
- width: 20px;
- height: 20px;
- display: flex;
- align-items: center;
- justify-content: center;
- border-radius: 50%; /* 新增圆形背景 */
- transition: all 0.2s; /* 新增过渡 */
- }
- .close-btn:hover {
- color: #ef4444; /* 改为红色更醒目 */
- background: #fef2f2; /* 新增背景色 */
- }
- </style>
|