|
|
@@ -46,7 +46,7 @@
|
|
|
<script setup>
|
|
|
import { ref, onMounted, watch, nextTick } from 'vue';
|
|
|
import * as echarts from 'echarts';
|
|
|
-import axios from 'axios';
|
|
|
+import { api8000 } from '@/utils/request'; // 导入 api8000 实例
|
|
|
|
|
|
// 图表实例引用
|
|
|
const cdBarChart = ref(null);
|
|
|
@@ -58,7 +58,6 @@ let chartInstanceCd = null;
|
|
|
let chartInstanceNutrient = null;
|
|
|
let chartInstanceExtra = null;
|
|
|
|
|
|
-
|
|
|
// 响应式状态
|
|
|
const isLoading = ref(true);
|
|
|
const error = ref(null);
|
|
|
@@ -101,14 +100,47 @@ const fieldConfig = {
|
|
|
]
|
|
|
};
|
|
|
|
|
|
-
|
|
|
// 数据请求
|
|
|
const fetchData = async () => {
|
|
|
try {
|
|
|
- // 实际项目中替换为真实API
|
|
|
- const res = await axios.get("http://localhost:8000/api/vector/export/all?table_name=EffCd_input_data&format=json");
|
|
|
- return res.data;
|
|
|
-
|
|
|
+ const apiUrl = '/api/vector/export/all?table_name=EffCd_input_data&format=json';
|
|
|
+ const response = await api8000.get(apiUrl); // 使用 api8000 实例
|
|
|
+
|
|
|
+ let data = response.data;
|
|
|
+
|
|
|
+ // 处理可能的字符串响应
|
|
|
+ if (typeof data === 'string') {
|
|
|
+ try {
|
|
|
+ // 替换 NaN 为 null
|
|
|
+ const cleanedData = data.replace(/\bNaN\b/g, 'null');
|
|
|
+ data = JSON.parse(cleanedData);
|
|
|
+ } catch (parseErr) {
|
|
|
+ throw new Error('接口返回的是字符串,但 JSON 解析失败');
|
|
|
+ }
|
|
|
+ }
|
|
|
+
|
|
|
+ // 处理对象中的 NaN 值
|
|
|
+ if (typeof data === 'object' && data !== null) {
|
|
|
+ const replaceNaN = (obj) => {
|
|
|
+ for (const key in obj) {
|
|
|
+ if (typeof obj[key] === 'object' && obj[key] !== null) {
|
|
|
+ replaceNaN(obj[key]);
|
|
|
+ } else if (typeof obj[key] === 'number' && isNaN(obj[key])) {
|
|
|
+ obj[key] = null;
|
|
|
+ } else if (obj[key] === 'NaN') {
|
|
|
+ obj[key] = null;
|
|
|
+ }
|
|
|
+ }
|
|
|
+ };
|
|
|
+ replaceNaN(data);
|
|
|
+ }
|
|
|
+
|
|
|
+ // 接口返回格式判断(GeoJSON或直接数组)
|
|
|
+ const rawData = data.features
|
|
|
+ ? data.features.map(f => f.properties)
|
|
|
+ : data;
|
|
|
+
|
|
|
+ return rawData;
|
|
|
} catch (err) {
|
|
|
throw new Error('数据加载失败: ' + err.message);
|
|
|
}
|
|
|
@@ -139,21 +171,28 @@ const calculateFieldStats = (data, fieldKey, fieldName) => {
|
|
|
...fieldConfig.nutrient,
|
|
|
...fieldConfig.extra
|
|
|
].find(f => f.key === fieldKey);
|
|
|
+
|
|
|
// 提取并清洗数据
|
|
|
const rawValues = data.map(item => item[fieldKey]);
|
|
|
const values = rawValues
|
|
|
.map((val, idx) => {
|
|
|
let num = Number(val);
|
|
|
- if(fieldConfigItem?.convert && fieldConfigItem.conversionFactor){
|
|
|
- num = num*fieldConfigItem.conversionFactor;
|
|
|
+
|
|
|
+ // 处理无效值
|
|
|
+ if (isNaN(num)) {
|
|
|
+ return null;
|
|
|
}
|
|
|
- //if (isNaN(num)) log(`无效数据: 第${idx+1}条 → ${val}`, fieldName);
|
|
|
- return isNaN(num) ? null : num;
|
|
|
+
|
|
|
+ // 应用单位转换
|
|
|
+ if (fieldConfigItem?.convert && fieldConfigItem.conversionFactor) {
|
|
|
+ num = num * fieldConfigItem.conversionFactor;
|
|
|
+ }
|
|
|
+
|
|
|
+ return num;
|
|
|
})
|
|
|
.filter(v => v !== null);
|
|
|
|
|
|
if (values.length === 0) {
|
|
|
- //log(`无有效数据`, fieldName);
|
|
|
return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
|
|
|
}
|
|
|
|
|
|
@@ -170,10 +209,6 @@ const calculateFieldStats = (data, fieldKey, fieldName) => {
|
|
|
// 强制校正顺序(核心修复)
|
|
|
const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
|
|
|
|
|
|
- //log(`统计量: min=${finalStats.min.toFixed(4)}, q1=${finalStats.q1.toFixed(4)},
|
|
|
- //median=${finalStats.median.toFixed(4)}, q3=${finalStats.q3.toFixed(4)}, max=${finalStats.max.toFixed(4)}`,
|
|
|
- //fieldName);
|
|
|
-
|
|
|
return {
|
|
|
key: fieldKey,
|
|
|
name: fieldName,
|
|
|
@@ -182,7 +217,7 @@ const calculateFieldStats = (data, fieldKey, fieldName) => {
|
|
|
median: sortedStats[2],
|
|
|
q3: sortedStats[3],
|
|
|
max: sortedStats[4],
|
|
|
- mean:mean
|
|
|
+ mean: mean
|
|
|
};
|
|
|
};
|
|
|
|
|
|
@@ -207,8 +242,8 @@ const calculateAllStats = (data) => {
|
|
|
const totalCdStats = pollutionStats.value.find(s => s.key === 'TCd_IDW');
|
|
|
const effCdStats = pollutionStats.value.find(s => s.key === 'Cdsolution');
|
|
|
stats.value = {
|
|
|
- totalCdAvg: totalCdStats ? (totalCdStats.min + totalCdStats.max) / 2 : 0, // 示例:用范围中点模拟平均值
|
|
|
- effCdAvg: effCdStats ? (effCdStats.min + effCdStats.max) / 2 : 0,
|
|
|
+ totalCdAvg: totalCdStats ? totalCdStats.mean : 0,
|
|
|
+ effCdAvg: effCdStats ? effCdStats.mean : 0,
|
|
|
samples: data.length
|
|
|
};
|
|
|
};
|
|
|
@@ -228,7 +263,7 @@ const initPollutionChart = () => {
|
|
|
|
|
|
// 提取x轴标签和数据
|
|
|
const xAxisData = fieldConfig.pollution.map(f => f.name);
|
|
|
- const barData = pollutionStats.value.map(stat =>stat.mean);
|
|
|
+ const barData = pollutionStats.value.map(stat => stat.mean);
|
|
|
|
|
|
chartInstanceCd.setOption({
|
|
|
title: { text: '主要指标含量对比', left: 'center', textStyle: { fontSize: 14 } },
|
|
|
@@ -237,11 +272,25 @@ const initPollutionChart = () => {
|
|
|
formatter: (params) => `${params[0].name}<br/>平均值: ${params[0].value.toFixed(4)} mg/kg`
|
|
|
},
|
|
|
grid: { top: 40, right: 15, bottom: 30, left: 40 },
|
|
|
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12 ,rotate:30 } },
|
|
|
- yAxis: { type: "value", name: '含量 (mg/kg)', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 ,rotate:30} },
|
|
|
+ xAxis: {
|
|
|
+ type: "category",
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ fontSize: 12,
|
|
|
+ rotate: 30,
|
|
|
+ interval: 0, // 强制显示所有标签
|
|
|
+ formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: "value",
|
|
|
+ name: '含量 (mg/kg)',
|
|
|
+ nameTextStyle: { fontSize: 12 },
|
|
|
+ axisLabel: { fontSize: 11 }
|
|
|
+ },
|
|
|
series: [{
|
|
|
name: '平均值', type: "bar",
|
|
|
- itemStyle: {color: '#5470c6' },
|
|
|
+ itemStyle: {color: (params) => fieldConfig.pollution[params.dataIndex].color },
|
|
|
data: barData
|
|
|
}]
|
|
|
});
|
|
|
@@ -262,11 +311,25 @@ const initNutrientChart = () => {
|
|
|
formatter: (params) => formatTooltip(nutrientStats.value[params.dataIndex])
|
|
|
},
|
|
|
grid: { top: 40, right: 15, bottom: 40, left: 40 },
|
|
|
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11, rotate: 30 } },
|
|
|
- yAxis: { type: "value", name: '含量(mg/kg)', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 , rotate: 30 } },
|
|
|
+ xAxis: {
|
|
|
+ type: "category",
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ fontSize: 11,
|
|
|
+ rotate: 30,
|
|
|
+ interval: 0, // 强制显示所有标签
|
|
|
+ formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: "value",
|
|
|
+ name: '含量(mg/kg)',
|
|
|
+ nameTextStyle: { fontSize: 12 },
|
|
|
+ axisLabel: { fontSize: 11 }
|
|
|
+ },
|
|
|
series: [{
|
|
|
name: '养分元素', type: "boxplot",
|
|
|
- itemStyle: { color: '#fac858', borderColor: '#ee6666' },
|
|
|
+ itemStyle: { color: (params) => fieldConfig.nutrient[params.dataIndex].color },
|
|
|
data: boxData
|
|
|
}]
|
|
|
});
|
|
|
@@ -287,18 +350,31 @@ const initExtraChart = () => {
|
|
|
formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
|
|
|
},
|
|
|
grid: { top: 40, right: 15, bottom: 40, left: 40 },
|
|
|
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11, rotate: 30 } },
|
|
|
- yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
|
|
|
+ xAxis: {
|
|
|
+ type: "category",
|
|
|
+ data: xAxisData,
|
|
|
+ axisLabel: {
|
|
|
+ fontSize: 11,
|
|
|
+ rotate: 30,
|
|
|
+ interval: 0, // 强制显示所有标签
|
|
|
+ formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
|
|
|
+ }
|
|
|
+ },
|
|
|
+ yAxis: {
|
|
|
+ type: "value",
|
|
|
+ name: '%',
|
|
|
+ nameTextStyle: { fontSize: 12 },
|
|
|
+ axisLabel: { fontSize: 11 }
|
|
|
+ },
|
|
|
series: [{
|
|
|
name: '理化性质', type: "boxplot",
|
|
|
- itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
|
|
|
+ itemStyle: { color: (params) => fieldConfig.extra[params.dataIndex].color },
|
|
|
data: boxData
|
|
|
}]
|
|
|
});
|
|
|
});
|
|
|
};
|
|
|
|
|
|
-
|
|
|
// 格式化Tooltip(复用缓存的统计数据)
|
|
|
const formatTooltip = (stat) => {
|
|
|
if (!stat || !stat.min) {
|
|
|
@@ -342,7 +418,7 @@ onMounted(() => {
|
|
|
|
|
|
// 窗口 resize 处理
|
|
|
const handleResize = () => {
|
|
|
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
|
|
|
+ [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
|
|
|
.forEach(inst => inst && inst.resize());
|
|
|
};
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
@@ -350,7 +426,7 @@ onMounted(() => {
|
|
|
// 组件卸载清理
|
|
|
return () => {
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra, chartInstancePopup]
|
|
|
+ [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
|
|
|
.forEach(inst => inst && inst.dispose());
|
|
|
};
|
|
|
});
|
|
|
@@ -437,4 +513,4 @@ onMounted(() => {
|
|
|
border-radius: 50%;
|
|
|
margin-right: 5px;
|
|
|
}
|
|
|
-</style>
|
|
|
+</style>
|