| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540 |
- <template>
- <div class="crop-cd-dashboard p-4 bg-white min-h-screen">
- <div class="flex justify-between items-center mb-6">
- <h1 class="text-xl font-bold text-gray-800">{{ $t('SoilCdStatistics.cropCdAnalysis') }}</h1>
- <div class="flex items-center">
- <div class="stat-card inline-block px-3 py-2">
- <div class="stat-value text-lg">{{ $t('SoilCdStatistics.sampleCount') }}: {{ stats.samples }}</div>
- </div>
- </div>
- </div>
-
- <!-- 加载状态 -->
- <div v-if="isLoading" class="loading-overlay">
- <span class="spinner"></span>
- <span class="ml-3 text-gray-700">{{ $t('DetectionStatistics.dataLoading') }}</span>
- </div>
-
- <!-- 错误提示 -->
- <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
- <p>{{ $t('SoilCdStatistics.dataLoadFailed') }}: {{ error.message }}</p>
- <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">{{ $t('SoilCdStatistics.retry') }}</button>
- </div>
-
- <!-- 1️⃣ 作物态 Cd 指标 -->
- <section class="mb-4 chart-container">
- <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.cropCdMainIndicators') }}</h3>
- <div ref="cdBarChart" style="width: 100%; height: 415px;"></div>
- </section>
- <!-- 2️⃣ 养分元素 -->
- <section class="mb-4 chart-container">
- <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.mainNutrients') }}</h3>
- <div ref="nutrientBoxChart" style="width: 100%; height: 400px;"></div>
- </section>
- <!-- 3️⃣ 其他理化性质 -->
- <section class="chart-container">
- <div class="flex justify-between items-center mb-3">
- <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherProperties') }}</h3>
- </div>
- <div ref="extraBoxChart" style="width: 100%; height: 400px;"></div>
- </section>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, nextTick } from 'vue';
- import * as echarts from 'echarts';
- import { api8000 } from '@/utils/request'; // 导入 api8000 实例import {api8000} from '@/utils/request'
- import { useI18n } from 'vue-i18n';
- const { t } = useI18n();
- const {locale} = useI18n()
- // 图表实例引用
- const cdBarChart = ref(null);
- const nutrientBoxChart = ref(null);
- const extraBoxChart = ref(null);
- // 图表实例变量
- let chartInstanceCd = null;
- let chartInstanceNutrient = null;
- let chartInstanceExtra = null;
- // 响应式状态
- const isLoading = ref(true);
- const error = ref(null);
- const stats = ref({
- cd002Avg: 0,
- cd02Avg: 0,
- cd2Avg: 0,
- samples: 0
- });
- // 按类别缓存统计数据(与x轴顺序对应)
- const pollutionStats = ref([]);
- const nutrientStats = ref([]);
- const extraStats = ref([]);
- // 字段配置 - 改为函数形式
- const getFieldConfig = () => ({
- pollution: [
- {
- key: '002_0002IDW',
- name: t('SoilCdStatistics.siltContent'),
- color: '#5470c6',
- unit: t('SoilCdStatistics.unitPercent'),
- convert: false
- },
- {
- key: '02_002IDW',
- name: t('SoilCdStatistics.sandContent'),
- color: '#91cc75',
- unit: t('SoilCdStatistics.unitPercent'),
- convert: false
- },
- {
- key: '2_02IDW',
- name: t('SoilCdStatistics.gravelContent'),
- color: '#fac858',
- unit: t('SoilCdStatistics.unitPercent'),
- convert: false
- }
- ],
- nutrient: [
- { key: 'AvaK_IDW', name: t('SoilCdStatistics.availablePotassium'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
- { key: 'SAvaK_IDW', name: t('SoilCdStatistics.exchangeablePotassium'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
- { key: 'AvaP_IDW', name: t('SoilCdStatistics.availablePhosphorus'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
- { key: 'TMn_IDW', name: t('SoilCdStatistics.totalManganese'), color: '#73c0de', unit: t('SoilCdStatistics.unitMgKg'), convert: false },
- { key: 'TN_IDW', name: t('SoilCdStatistics.totalNitrogen'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 },
- { key: 'TS_IDW', name: t('SoilCdStatistics.totalSulfur'), color: '#ee6666', unit: t('SoilCdStatistics.unitMgKg'), convert: true, conversionFactor: 1000 }
- ],
- extra: [
- { key: 'TAl_IDW', name: t('SoilCdStatistics.totalAluminum'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
- { key: 'TCa_IDW', name: t('SoilCdStatistics.totalCalcium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
- { key: 'TFe_IDW', name: t('SoilCdStatistics.totalIron'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
- { key: 'TMg_IDW', name: t('SoilCdStatistics.totalMagnesium'), color: '#73c0de', unit: t('SoilCdStatistics.unitPercent'), convert: false },
- ]
- });
- // 数据请求 - 增强错误处理和调试
- const fetchData = async () => {
- try {
- isLoading.value = true;
- const apiUrl = '/api/vector/stats/CropCd_input_data';
- // console.log('正在请求数据:', apiUrl);
-
- const response = await api8000.get(apiUrl);
- // console.log('API响应:', response);
-
- // 调试:输出响应结构
- // console.log('响应数据:', response.data);
-
- // 处理不同的响应格式
- let processedData;
- processedData = response.data.data;
-
- if (!processedData) {
- throw new Error('无法解析API返回的数据结构');
- }
-
- // console.log('处理后的数据:', processedData);
- return processedData;
- } catch (err) {
- console.error('数据请求失败:', err);
- throw new Error(`数据加载失败: ${err.message || '网络或服务器错误'}`);
- }
- };
- // 构建箱线图数据
- const buildBoxplotData = (statsArray) => {
- return statsArray.map(stat => {
- if (!stat.min) return [null, null, null, null, null];
- return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
- });
- };
- // 初始化作物态 Cd 指标图表
- const initPollutionChart = () => {
- nextTick(() => {
- if (chartInstanceCd) chartInstanceCd.dispose();
- if (!cdBarChart.value) return;
- chartInstanceCd = echarts.init(cdBarChart.value);
-
- const currentFieldConfig = getFieldConfig();
- const xAxisData = currentFieldConfig.pollution.map(f => f.name);
- const barData = pollutionStats.value.map(stat => stat.avg || 0);
-
- chartInstanceCd.setOption({
- title: {},
- tooltip: {
- trigger: 'axis',
- formatter: (params) => `${params[0].name}<br/>${t('SoilCdStatistics.averageValue')}: ${params[0].value.toFixed(4)}`
- },
- grid: { top: 40, right: 15, bottom: '18%', left: '10%' },
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 12, rotate: 30 } },
- yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
- series: [{
- name: t('SoilCdStatistics.averageValue'), type: "bar",
- itemStyle: { color: (p) => currentFieldConfig.pollution[p.dataIndex].color },
- data: barData
- }]
- });
- });
- };
- // 初始化养分元素图表
- const initNutrientChart = () => {
- nextTick(() => {
- if (chartInstanceNutrient) chartInstanceNutrient.dispose();
- if (!nutrientBoxChart.value) return;
- chartInstanceNutrient = echarts.init(nutrientBoxChart.value);
-
- const currentFieldConfig = getFieldConfig();
- const xAxisData = currentFieldConfig.nutrient.map(f => f.name);
- const boxData = buildBoxplotData(nutrientStats.value);
-
- chartInstanceNutrient.setOption({
- title: { text: t('SoilCdStatistics.nutrientDistribution'), left: 'center', textStyle: { fontSize: 14 } },
- tooltip: {
- trigger: "item",
- formatter: (params) => {
- const stat = nutrientStats.value[params.dataIndex];
- const fieldConfigItem = currentFieldConfig.nutrient.find(f => f.key === stat.key);
- return formatTooltip(stat, fieldConfigItem?.unit);
- }
- },
- grid: { top: 40, right: 15, bottom: 45, left: 40 },
- xAxis: {
- type: "category",
- data: xAxisData,
- axisLabel: { fontSize: 11, rotate: 30 }
- },
- yAxis: {
- type: "value",
- name: 'mg/kg',
- nameTextStyle: { fontSize: 12 },
- axisLabel: { fontSize: 11 }
- },
- series: [{
- name: t('SoilCdStatistics.mainNutrients'),
- type: "boxplot",
- itemStyle: { color: '#ee6666', borderColor: '#fac858' },
- data: boxData
- }]
- });
- });
- };
- // 初始化其他理化性质图表
- const initExtraChart = () => {
- const currentFieldConfig = getFieldConfig();
- const xAxisData = currentFieldConfig.extra.map(f => f.name);
- const boxData = buildBoxplotData(extraStats.value);
-
- nextTick(() => {
- if (chartInstanceExtra) chartInstanceExtra.dispose();
- chartInstanceExtra = echarts.init(extraBoxChart.value);
- chartInstanceExtra.setOption({
- title: { text: t('SoilCdStatistics.propertiesDistribution'), left: 'center', textStyle: { fontSize: 14 } },
- tooltip: {
- trigger: "item",
- formatter: (params) => formatTooltip(extraStats.value[params.dataIndex])
- },
- grid: { top: 40, right: 15, bottom: 40, left: 40 },
- xAxis: { type: "category", data: xAxisData, axisLabel: { fontSize: 11 } },
- yAxis: { type: "value", name: '%', nameTextStyle: { fontSize: 12 }, axisLabel: { fontSize: 11 } },
- series: [{
- name: t('SoilCdStatistics.otherProperties'), type: "boxplot",
- itemStyle: { color: '#73c0de', borderColor: '#5470c6' },
- data: boxData
- }]
- });
- });
- };
- // 格式化 Tooltip
- const formatTooltip = (stat, unit = '') => {
- if (!stat || !stat.min) {
- return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || t('DetectionStatistics.noValidData')}</div><div>${t('DetectionStatistics.noValidData')}</div>`;
- }
- return `<div style="font-weight:bold">${stat.name}</div>
- <div style="margin-top:8px">
- <div>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)} ${unit}</span></div>
- <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)} ${unit}</span></div>
- <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)} ${unit}</span></div>
- <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)} ${unit}</span></div>
- <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)} ${unit}</span></div>
- </div>`;
- };
- // 初始化图表主流程
- const initCharts = async () => {
- try {
- isLoading.value = true;
- error.value = null;
-
- const statsData = await fetchData();
- const currentFieldConfig = getFieldConfig();
-
- // -------- 1. 处理「作物态 Cd 指标」统计 --------
- pollutionStats.value = currentFieldConfig.pollution.map(field => {
- const fieldStats = statsData[field.key];
- if (!fieldStats) {
- return {
- key: field.key,
- name: field.name,
- min: null,
- q1: null,
- median: null,
- q3: null,
- max: null,
- avg: null
- };
- }
- let min = fieldStats.min;
- let q1 = fieldStats.q1;
- let median = fieldStats.median;
- let q3 = fieldStats.q3;
- let max = fieldStats.max;
- let avg = fieldStats.mean;
- if (field.convert && field.conversionFactor) {
- min *= field.conversionFactor;
- q1 *= field.conversionFactor;
- median *= field.conversionFactor;
- q3 *= field.conversionFactor;
- max *= field.conversionFactor;
- avg *= field.conversionFactor;
- }
- return {
- key: field.key,
- name: field.name,
- min,
- q1,
- median,
- q3,
- max,
- avg
- };
- });
- // -------- 2. 处理「主要养分元素」统计 --------
- nutrientStats.value = currentFieldConfig.nutrient.map(field => {
- const fieldStats = statsData[field.key];
- if (!fieldStats) {
- return {
- key: field.key,
- name: field.name,
- min: null,
- q1: null,
- median: null,
- q3: null,
- max: null,
- avg: null
- };
- }
- let min = fieldStats.min;
- let q1 = fieldStats.q1;
- let median = fieldStats.median;
- let q3 = fieldStats.q3;
- let max = fieldStats.max;
- let avg = fieldStats.mean;
- if (field.convert && field.conversionFactor) {
- min *= field.conversionFactor;
- q1 *= field.conversionFactor;
- median *= field.conversionFactor;
- q3 *= field.conversionFactor;
- max *= field.conversionFactor;
- avg *= field.conversionFactor;
- }
- return {
- key: field.key,
- name: field.name,
- min,
- q1,
- median,
- q3,
- max,
- avg
- };
- });
- // -------- 3. 处理「其他理化性质」统计 --------
- extraStats.value = currentFieldConfig.extra.map(field => {
- const fieldStats = statsData[field.key];
- if (!fieldStats) {
- return {
- key: field.key,
- name: field.name,
- min: null,
- q1: null,
- median: null,
- q3: null,
- max: null,
- avg: null
- };
- }
- let min = fieldStats.min;
- let q1 = fieldStats.q1;
- let median = fieldStats.median;
- let q3 = fieldStats.q3;
- let max = fieldStats.max;
- let avg = fieldStats.mean;
- if (field.convert && field.conversionFactor) {
- min *= field.conversionFactor;
- q1 *= field.conversionFactor;
- median *= field.conversionFactor;
- q3 *= field.conversionFactor;
- max *= field.conversionFactor;
- avg *= field.conversionFactor;
- }
- return {
- key: field.key,
- name: field.name,
- min,
- q1,
- median,
- q3,
- max,
- avg
- };
- });
- // -------- 更新汇总统计 --------
- const firstFieldKey = currentFieldConfig.pollution[0]?.key;
- stats.value = {
- cd002Avg: pollutionStats.value.find(s => s.key === '002_0002IDW')?.avg || 0,
- cd02Avg: pollutionStats.value.find(s => s.key === '02_002IDW')?.avg || 0,
- cd2Avg: pollutionStats.value.find(s => s.key === '2_02IDW')?.avg || 0,
- samples: statsData[firstFieldKey]?.count || 0
- };
- // 初始化图表
- initPollutionChart();
- initNutrientChart();
- initExtraChart();
-
- isLoading.value = false;
- } catch (err) {
- isLoading.value = false;
- error.value = err;
- console.error('初始化失败:', err);
- }
- };
- // 组件挂载
- onMounted(() => {
- initCharts();
-
- // 窗口resize处理
- const handleResize = () => {
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
- .forEach(inst => inst && inst.resize());
- };
- window.addEventListener('resize', handleResize);
-
- // 组件卸载清理
- return () => {
- window.removeEventListener('resize', handleResize);
- [chartInstanceCd, chartInstanceNutrient, chartInstanceExtra]
- .forEach(inst => inst && inst.dispose());
- };
- });
- // 监听语言变化
- watch(locale, () => {
- console.log('语言切换,重新初始化图表');
- initCharts();
- });
- </script>
- <style>
- .crop-cd-dashboard {
- font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
- max-width: 1200px;
- margin: 0 auto;
- font-size: 14px;
- }
- .chart-container {
- background: white;
- border-radius: 6px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- padding: 12px;
- margin-bottom: 16px;
- }
- .section-title {
- color: #2c3e50;
- border-left: 3px solid #3498db;
- padding-left: 10px;
- margin-bottom: 12px;
- }
- .toggle-btn {
- background: #f8f9fa;
- border: 1px solid #e9ecef;
- padding: 6px 12px;
- border-radius: 3px;
- cursor: pointer;
- display: inline-flex;
- align-items: center;
- transition: all 0.3s;
- }
- .toggle-btn:hover {
- background: #e9ecef;
- }
- .loading-overlay {
- position: absolute;
- top: 0;
- left: 0;
- right: 0;
- bottom: 0;
- background: rgba(255, 255, 255, 0.8);
- display: flex;
- align-items: center;
- justify-content: center;
- z-index: 10;
- }
- .spinner {
- width: 30px;
- height: 30px;
- border: 3px solid rgba(0, 0, 0, 0.1);
- border-radius: 50%;
- border-left-color: #3498db;
- animation: spin 1s linear infinite;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
- .stat-card {
- background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
- border-radius: 6px;
- padding: 8px 12px;
- box-shadow: 0 1px 3px rgba(0,0,0,0.05);
- }
- .stat-value {
- font-size: 16px;
- font-weight: bold;
- color: #2c3e50;
- }
- .stat-label {
- font-size: 12px;
- color: #7f8c8d;
- }
- .legend-item {
- display: flex;
- align-items: center;
- margin-right: 12px;
- font-size: 13px;
- margin-bottom: 8px;
- }
- .legend-color {
- width: 10px;
- height: 10px;
- border-radius: 50%;
- margin-right: 5px;
- }
- </style>
|