fluxcdStatictics.vue 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382
  1. <template>
  2. <div class="flux-cd-dashboard p-4 bg-white min-h-screen">
  3. <div class="flex justify-between items-center mb-6">
  4. <h1 class="text-xl font-bold text-gray-800">{{ $t('SoilCdStatistics.fluxCdAnalysis') }}</h1>
  5. <div class="flex items-center">
  6. <div class="stat-card inline-block px-3 py-2">
  7. <div class="stat-value text-lg">{{ $t('SoilCdStatistics.sampleCount') }}: {{ stats.samples }}</div>
  8. </div>
  9. </div>
  10. </div>
  11. <div v-if="error" class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-6">
  12. <p>{{ $t('SoilCdStatistics.dataLoadFailed') }}: {{ error.message }}</p>
  13. <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">{{ $t('SoilCdStatistics.retry') }}</button>
  14. </div>
  15. <!-- 1. 初始 Cd 单独箱线图 -->
  16. <section class="mb-6 chart-container">
  17. <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.initialCdDistribution') }}</h3>
  18. <div ref="initialCdChart" style="width: 100%; height: 400px;"></div>
  19. <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
  20. <div class="spinner"></div>
  21. </div>
  22. <div v-if="error && !chartInstanceInitial" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
  23. <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
  24. <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initInitialCdChart">
  25. {{ $t('SoilCdStatistics.reloadInit') }}
  26. </button>
  27. </div>
  28. </section>
  29. <!-- 2. 其他指标 合并箱线图 -->
  30. <section class="mb-6 chart-container">
  31. <div class="flex flex-wrap justify-between items-center mb-4">
  32. <h3 class="section-title text-base font-semibold">{{ $t('SoilCdStatistics.otherFluxIndicators') }}</h3>
  33. </div>
  34. <div ref="otherIndicatorsChart" style="width: 100%; height: 400px;"></div>
  35. <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
  36. <div class="spinner"></div>
  37. </div>
  38. <div v-if="error && !chartInstanceOther" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
  39. <p class="text-yellow-700">{{ $t('SoilCdStatistics.chartInitFailed') }}: {{ error.message }}</p>
  40. <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initOtherIndicatorsChart">
  41. {{ $t('SoilCdStatistics.reloadInit') }}
  42. </button>
  43. </div>
  44. </section>
  45. </div>
  46. </template>
  47. <script setup>
  48. import { ref, onMounted, nextTick } from 'vue';
  49. import * as echarts from 'echarts';
  50. import { api8000 } from '@/utils/request'; // 导入 api8000 实例
  51. import { useI18n } from 'vue-i18n';
  52. const {locale} = useI18n()
  53. const { t } = useI18n();
  54. // 图表容器 & 实例
  55. const initialCdChart = ref(null);
  56. const otherIndicatorsChart = ref(null);
  57. const chartInstanceInitial = ref(null);
  58. const chartInstanceOther = ref(null);
  59. // 响应式状态
  60. const isLoading = ref(true);
  61. const error = ref(null);
  62. const stats = ref({ samples: 0 });
  63. // 统计数据
  64. const initialCdStats = ref([]);
  65. const otherIndicatorsStats = ref([]);
  66. // 字段配置
  67. const getFieldConfig = () => ({
  68. initialCd: [
  69. { key: 'Initial_Cd', name: t('SoilCdStatistics.initialCadmiumTotal'), color: '#5470c6' }
  70. ],
  71. otherIndicators: [
  72. { key: 'DQCJ_Cd', name: t('SoilCdStatistics.atmosphericDepositionInput'), color: '#91cc75' },
  73. { key: 'GGS_Cd', name: t('SoilCdStatistics.irrigationWaterInput'), color: '#fac858' },
  74. { key: 'NCP_Cd', name: t('SoilCdStatistics.agriculturalInput'), color: '#ee6666' },
  75. { key: 'DX_Cd', name: t('SoilCdStatistics.undergroundLeaching'), color: '#73c0de' },
  76. { key: 'DB_Cd', name: t('SoilCdStatistics.surfaceRunoff'), color: '#38b2ac' },
  77. { key: 'ZL_Cd', name: t('SoilCdStatistics.grainRemoval'), color: '#4169e1' },
  78. ]
  79. });
  80. const fetchData = async () => {
  81. try {
  82. isLoading.value = true;
  83. const apiUrl = '/api/vector/stats/FluxCd_input_data';
  84. // console.log('正在请求数据:', apiUrl);
  85. const response = await api8000.get(apiUrl);
  86. // console.log('API响应:', response);
  87. // 调试:输出响应结构
  88. // console.log('响应数据:', response.data);
  89. // 处理不同的响应格式
  90. let processedData;
  91. processedData = response.data.data;
  92. if (!processedData) {
  93. throw new Error('无法解析API返回的数据结构');
  94. }
  95. // console.log('处理后的数据:', processedData);
  96. return processedData;
  97. } catch (err) {
  98. console.error('数据请求失败:', err);
  99. throw new Error(`数据加载失败: ${err.message || '网络或服务器错误'}`);
  100. }
  101. };
  102. // 计算单个字段的统计量
  103. const calculateFieldStats = (statsData, fieldKey, fieldName) => {
  104. // 从接口数据中获取当前字段的统计结果
  105. const fieldStats = statsData[fieldKey];
  106. if (!fieldStats) {
  107. return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
  108. }
  109. // 提取预统计值
  110. let min = fieldStats.min;
  111. let q1 = fieldStats.q1;
  112. let median = fieldStats.median;
  113. let q3 = fieldStats.q3;
  114. let max = fieldStats.max;
  115. // 强制校正统计量顺序(确保 min ≤ q1 ≤ median ≤ q3 ≤ max)
  116. const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
  117. return {
  118. key: fieldKey,
  119. name: fieldName,
  120. min: sortedStats[0],
  121. q1: sortedStats[1],
  122. median: sortedStats[2],
  123. q3: sortedStats[3],
  124. max: sortedStats[4]
  125. };
  126. };
  127. // 计算所有统计数据
  128. const calculateAllStats = (data) => {
  129. const currentFieldConfig = getFieldConfig();
  130. // 初始 Cd 统计
  131. initialCdStats.value = currentFieldConfig.initialCd.map(indicator =>
  132. calculateFieldStats(data, indicator.key, indicator.name)
  133. );
  134. // 其他指标统计
  135. otherIndicatorsStats.value = currentFieldConfig.otherIndicators.map(indicator =>
  136. calculateFieldStats(data, indicator.key, indicator.name)
  137. );
  138. // 更新样本数
  139. const firstStat = initialCdStats.value[0] || otherIndicatorsStats.value[0];
  140. stats.value.samples = firstStat?.count || 0;
  141. };
  142. // 构建箱线图数据
  143. const buildBoxplotData = (statsArray) => {
  144. return statsArray.map(stat => {
  145. if (stat.min === null || stat.min === undefined) {
  146. return [null, null, null, null, null];
  147. }
  148. return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
  149. });
  150. };
  151. // 初始化【初始 Cd】图表
  152. const initInitialCdChart = () => {
  153. if (!initialCdChart.value) {
  154. error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
  155. return;
  156. }
  157. try {
  158. if (chartInstanceInitial.value) {
  159. chartInstanceInitial.value.dispose();
  160. }
  161. chartInstanceInitial.value = echarts.init(initialCdChart.value);
  162. const currentFieldConfig = getFieldConfig();
  163. const xAxisData = currentFieldConfig.initialCd.map(ind => ind.name);
  164. const boxData = buildBoxplotData(initialCdStats.value);
  165. const option = {
  166. title: { text: t('SoilCdStatistics.initialCdDistribution'), left: 'center' },
  167. tooltip: {
  168. trigger: "item",
  169. formatter: (params) => {
  170. const stat = initialCdStats.value[params.dataIndex];
  171. return formatTooltip(stat);
  172. }
  173. },
  174. grid: { top: 60, right: 30, bottom: 25, left: 60 },
  175. xAxis: {
  176. type: "category",
  177. data: xAxisData
  178. },
  179. yAxis: {
  180. type: "value",
  181. name: 'g/ha'
  182. },
  183. series: [{
  184. type: "boxplot",
  185. itemStyle: {
  186. color: currentFieldConfig.initialCd[0].color,
  187. borderWidth: 2
  188. },
  189. data: boxData
  190. }]
  191. };
  192. chartInstanceInitial.value.setOption(option);
  193. } catch (err) {
  194. console.error(t('SoilCdStatistics.chartInitFailed'), err);
  195. error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
  196. }
  197. };
  198. // 初始化【其他指标】图表
  199. const initOtherIndicatorsChart = () => {
  200. if (!otherIndicatorsChart.value) {
  201. error.value = new Error(t('SoilCdStatistics.chartInitFailed'));
  202. return;
  203. }
  204. try {
  205. if (chartInstanceOther.value) {
  206. chartInstanceOther.value.dispose();
  207. }
  208. chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
  209. const currentFieldConfig = getFieldConfig();
  210. const xAxisData = currentFieldConfig.otherIndicators.map(ind => ind.name);
  211. const boxData = buildBoxplotData(otherIndicatorsStats.value);
  212. const option = {
  213. title: { text: t('SoilCdStatistics.otherFluxIndicators'), left: 'center' },
  214. tooltip: {
  215. trigger: "item",
  216. formatter: (params) => {
  217. const stat = otherIndicatorsStats.value[params.dataIndex];
  218. return formatTooltip(stat);
  219. }
  220. },
  221. grid: { top: 60, right: 30, bottom: 70, left: 60 },
  222. xAxis: {
  223. type: "category",
  224. data: xAxisData,
  225. axisLabel: {
  226. rotate: 45,
  227. formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
  228. }
  229. },
  230. yAxis: {
  231. type: "value",
  232. name: 'g/ha/a'
  233. },
  234. series: [{
  235. type: "boxplot",
  236. itemStyle: {
  237. color: (params) => currentFieldConfig.otherIndicators[params.dataIndex].color,
  238. borderWidth: 2
  239. },
  240. data: boxData
  241. }]
  242. };
  243. chartInstanceOther.value.setOption(option);
  244. } catch (err) {
  245. console.error(t('SoilCdStatistics.chartInitFailed'), err);
  246. error.value = new Error(`${t('SoilCdStatistics.chartInitFailed')}: ${err.message}`);
  247. }
  248. };
  249. // Tooltip格式化
  250. const formatTooltip = (stat) => {
  251. if (!stat || stat.min === null) {
  252. return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
  253. }
  254. return `<div style="font-weight:bold">${stat.name}</div>
  255. <div style="margin-top:8px">
  256. <div>${t('DetectionStatistics.minValue')}:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
  257. <div>${t('DetectionStatistics.q1Value')}:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
  258. <div>${t('DetectionStatistics.medianValue')}:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
  259. <div>${t('DetectionStatistics.q3Value')}:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
  260. <div>${t('DetectionStatistics.maxValue')}:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
  261. </div>`;
  262. };
  263. // 主初始化函数
  264. const initCharts = async () => {
  265. try {
  266. isLoading.value = true;
  267. error.value = null;
  268. // console.log('开始初始化图表...');
  269. // 获取数据
  270. const data = await fetchData();
  271. // console.log('获取到的数据:', data);
  272. if (!data || (Array.isArray(data) && data.length === 0)) {
  273. throw new Error('未获取到有效数据');
  274. }
  275. // 计算统计数据
  276. calculateAllStats(data);
  277. // 等待DOM更新
  278. await nextTick();
  279. // 初始化图表
  280. initInitialCdChart();
  281. initOtherIndicatorsChart();
  282. isLoading.value = false;
  283. // console.log('图表初始化完成');
  284. } catch (err) {
  285. isLoading.value = false;
  286. error.value = err;
  287. console.error('初始化失败:', err);
  288. }
  289. };
  290. // 组件生命周期
  291. onMounted(() => {
  292. initCharts();
  293. const handleResize = () => {
  294. chartInstanceInitial.value?.resize();
  295. chartInstanceOther.value?.resize();
  296. };
  297. window.addEventListener('resize', handleResize);
  298. });
  299. // 监听语言变化
  300. watch(locale, () => {
  301. // 语言切换后重新初始化所有图表
  302. initCharts();
  303. });
  304. </script>
  305. <style scoped>
  306. /* 样式保持不变 */
  307. .chart-container {
  308. background: white;
  309. border-radius: 6px;
  310. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  311. padding: 16px;
  312. margin-bottom: 16px;
  313. min-height: 400px;
  314. position: relative;
  315. }
  316. .spinner {
  317. width: 30px;
  318. height: 30px;
  319. border: 3px solid rgba(0, 0, 0, 0.1);
  320. border-radius: 50%;
  321. border-left-color: #3498db;
  322. animation: spin 1s linear infinite;
  323. }
  324. @keyframes spin {
  325. to { transform: rotate(360deg); }
  326. }
  327. </style>