| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382 |
- <template>
- <div class="flux-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.fluxCdAnalysis') }}</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="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-6 chart-container">
- <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.initialCdDistribution') }}</h3>
- <div ref="initialCdChart" style="width: 100%; height: 400px;"></div>
- <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
- <div class="spinner"></div>
- </div>
- <div v-if="error && !chartInstanceInitial" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
- <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
- <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initInitialCdChart">
- {{ $t('SoilCdStatistics.reloadInit') }}
- </button>
- </div>
- </section>
- <!-- 2. 其他指标 合并箱线图 -->
- <section class="mb-6 chart-container">
- <div class="flex flex-wrap justify-between items-center mb-4">
- <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherFluxIndicators') }}</h3>
- </div>
- <div ref="otherIndicatorsChart" style="width: 100%; height: 400px;"></div>
- <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
- <div class="spinner"></div>
- </div>
- <div v-if="error && !chartInstanceOther" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
- <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
- <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initOtherIndicatorsChart">
- {{ $t('SoilCdStatistics.reloadInit') }}
- </button>
- </div>
- </section>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, nextTick } from 'vue';
- import * as echarts from 'echarts';
- import { api8000 } from '@/utils/request'; // 导入 api8000 实例
- import { useI18n } from 'vue-i18n';
- const {locale} = useI18n()
- const { t } = useI18n();
- // 图表容器 & 实例
- const initialCdChart = ref(null);
- const otherIndicatorsChart = ref(null);
- const chartInstanceInitial = ref(null);
- const chartInstanceOther = ref(null);
- // 响应式状态
- const isLoading = ref(true);
- const error = ref(null);
- const stats = ref({ samples: 0 });
- // 统计数据
- const initialCdStats = ref([]);
- const otherIndicatorsStats = ref([]);
- // 字段配置
- const getFieldConfig = () => ({
- initialCd: [
- { key: 'Initial_Cd', name: t('SoilCdStatistics.initialCadmiumTotal'), color: '#5470c6' }
- ],
- otherIndicators: [
- { key: 'DQCJ_Cd', name: t('SoilCdStatistics.atmosphericDepositionInput'), color: '#91cc75' },
- { key: 'GGS_Cd', name: t('SoilCdStatistics.irrigationWaterInput'), color: '#fac858' },
- { key: 'NCP_Cd', name: t('SoilCdStatistics.agriculturalInput'), color: '#ee6666' },
- { key: 'DX_Cd', name: t('SoilCdStatistics.undergroundLeaching'), color: '#73c0de' },
- { key: 'DB_Cd', name: t('SoilCdStatistics.surfaceRunoff'), color: '#38b2ac' },
- { key: 'ZL_Cd', name: t('SoilCdStatistics.grainRemoval'), color: '#4169e1' },
- ]
- });
- const fetchData = async () => {
- try {
- isLoading.value = true;
- const apiUrl = '/api/vector/stats/FluxCd_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 calculateFieldStats = (statsData, fieldKey, fieldName) => {
- // 从接口数据中获取当前字段的统计结果
- const fieldStats = statsData[fieldKey];
- if (!fieldStats) {
- return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
- }
- // 提取预统计值
- let min = fieldStats.min;
- let q1 = fieldStats.q1;
- let median = fieldStats.median;
- let q3 = fieldStats.q3;
- let max = fieldStats.max;
- // 强制校正统计量顺序(确保 min ≤ q1 ≤ median ≤ q3 ≤ max)
- const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
- return {
- key: fieldKey,
- name: fieldName,
- min: sortedStats[0],
- q1: sortedStats[1],
- median: sortedStats[2],
- q3: sortedStats[3],
- max: sortedStats[4]
- };
- };
- // 计算所有统计数据
- const calculateAllStats = (data) => {
- const currentFieldConfig = getFieldConfig();
-
- // 初始 Cd 统计
- initialCdStats.value = currentFieldConfig.initialCd.map(indicator =>
- calculateFieldStats(data, indicator.key, indicator.name)
- );
-
- // 其他指标统计
- otherIndicatorsStats.value = currentFieldConfig.otherIndicators.map(indicator =>
- calculateFieldStats(data, indicator.key, indicator.name)
- );
-
- // 更新样本数
- const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
- stats.value.samples = firstStat?.count || 0;
- };
- // 构建箱线图数据
- const buildBoxplotData = (statsArray) => {
- return statsArray.map(stat => {
- if (stat.min === null || stat.min === undefined) {
- return [null, null, null, null, null];
- }
- return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
- });
- };
- // 初始化【初始 Cd】图表
- const initInitialCdChart = () => {
- if (!initialCdChart.value) {
- error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
- return;
- }
-
- try {
- if (chartInstanceInitial.value) {
- chartInstanceInitial.value.dispose();
- }
-
- chartInstanceInitial.value = echarts.init(initialCdChart.value);
- const currentFieldConfig = getFieldConfig();
- const xAxisData = currentFieldConfig.initialCd.map(ind => ind.name);
- const boxData = buildBoxplotData(initialCdStats.value);
-
- const option = {
- title: { text: t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
- tooltip: {
- trigger: "item",
- formatter: (params) => {
- const stat = initialCdStats.value[params.dataIndex];
- return formatTooltip(stat);
- }
- },
- grid: { top: 60, right: 30, bottom: 25, left: 60 },
- xAxis: {
- type: "category",
- data: xAxisData
- },
- yAxis: {
- type: "value",
- name: 'g/ha'
- },
- series: [{
- type: "boxplot",
- itemStyle: {
- color: currentFieldConfig.initialCd[0].color,
- borderWidth: 2
- },
- data: boxData
- }]
- };
-
- chartInstanceInitial.value.setOption(option);
- } catch (err) {
- console.error(t('SoilCdStatistics.chartInitFailed'), err);
- error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
- }
- };
- // 初始化【其他指标】图表
- const initOtherIndicatorsChart = () => {
- if (!otherIndicatorsChart.value) {
- error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
- return;
- }
-
- try {
- if (chartInstanceOther.value) {
- chartInstanceOther.value.dispose();
- }
-
- chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
- const currentFieldConfig = getFieldConfig();
- const xAxisData = currentFieldConfig.otherIndicators.map(ind => ind.name);
- const boxData = buildBoxplotData(otherIndicatorsStats.value);
-
- const option = {
- title: { text: t('SoilCdStatistics.otherFluxIndicators'), left: 'center' },
- tooltip: {
- trigger: "item",
- formatter: (params) => {
- const stat = otherIndicatorsStats.value[params.dataIndex];
- return formatTooltip(stat);
- }
- },
- grid: { top: 60, right: 30, bottom: 70, left: 60 },
- xAxis: {
- type: "category",
- data: xAxisData,
- axisLabel: {
- rotate: 45,
- formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
- }
- },
- yAxis: {
- type: "value",
- name: 'g/ha/a'
- },
- series: [{
- type: "boxplot",
- itemStyle: {
- color: (params) => currentFieldConfig.otherIndicators[params.dataIndex].color,
- borderWidth: 2
- },
- data: boxData
- }]
- };
-
- chartInstanceOther.value.setOption(option);
- } catch (err) {
- console.error(t('SoilCdStatistics.chartInitFailed'), err);
- error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
- }
- };
- // Tooltip格式化
- const formatTooltip = (stat) => {
- if (!stat || stat.min === null) {
- return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</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)}</span></div>
- <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
- <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
- <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
- <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
- </div>`;
- };
- // 主初始化函数
- const initCharts = async () => {
- try {
- isLoading.value = true;
- error.value = null;
-
- // console.log('开始初始化图表...');
-
- // 获取数据
- const data = await fetchData();
- // console.log('获取到的数据:', data);
-
- if (!data || (Array.isArray(data) && data.length === 0)) {
- throw new Error('未获取到有效数据');
- }
-
- // 计算统计数据
- calculateAllStats(data);
-
- // 等待DOM更新
- await nextTick();
-
- // 初始化图表
- initInitialCdChart();
- initOtherIndicatorsChart();
-
- isLoading.value = false;
- // console.log('图表初始化完成');
-
- } catch (err) {
- isLoading.value = false;
- error.value = err;
- console.error('初始化失败:', err);
- }
- };
- // 组件生命周期
- onMounted(() => {
- initCharts();
-
- const handleResize = () => {
- chartInstanceInitial.value?.resize();
- chartInstanceOther.value?.resize();
- };
-
- window.addEventListener('resize', handleResize);
- });
- // 监听语言变化
- watch(locale, () => {
- // 语言切换后重新初始化所有图表
- initCharts();
- });
- </script>
- <style scoped>
- /* 样式保持不变 */
- .chart-container {
- background: white;
- border-radius: 6px;
- box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
- padding: 16px;
- margin-bottom: 16px;
- min-height: 400px;
- position: relative;
- }
- .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); }
- }
- </style>
|