fluxcdStatictics.vue 15 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459
  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">通量Cd数据统计分析</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">样本数量:{{ 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>数据加载失败: {{ error.message }}</p>
  13. <button class="mt-2 px-3 py-1 bg-red-500 text-white rounded" @click="initCharts">重试</button>
  14. </div>
  15. <!-- 1. 初始Cd 单独箱线图 -->
  16. <section class="mb-6 chart-container">
  17. <h3 class="section-title text-base font-semibold">初始Cd(Initial_Cd)分布箱线图</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">图表初始化失败: {{ error.message }}</p>
  24. <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initInitialCdChart">
  25. 重新尝试初始化
  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">其他通量Cd指标分布箱线图</h3>
  33. </div>
  34. <div ref="otherIndicatorsChart" style="width: 100%; height: 400px;"></div>
  35. <!-- 容器内的加载遮罩 -->
  36. <div v-if="isLoading" class="absolute inset-0 bg-white bg-opacity-80 flex items-center justify-center">
  37. <div class="spinner"></div>
  38. </div>
  39. <!-- 错误提示(保留重试按钮) -->
  40. <div v-if="error && !chartInstanceOther" class="bg-yellow-50 border border-yellow-200 p-4 rounded mt-4">
  41. <p class="text-yellow-700">图表初始化失败: {{ error.message }}</p>
  42. <button class="mt-2 px-3 py-1 bg-yellow-500 text-white rounded" @click="initOtherIndicatorsChart">
  43. 重新尝试初始化
  44. </button>
  45. </div>
  46. </section>
  47. </div>
  48. </template>
  49. <script setup>
  50. import { ref, onMounted, nextTick } from 'vue';
  51. import * as echarts from 'echarts';
  52. import { api8000 } from '@/utils/request'; // 导入 api8000 实例
  53. // 图表容器 & 实例
  54. const initialCdChart = ref(null); // 初始Cd图表
  55. const otherIndicatorsChart = ref(null); // 其他指标图表
  56. const chartInstanceInitial = ref(null); // 初始Cd实例
  57. const chartInstanceOther = ref(null); // 其他指标实例
  58. const chartInstancePopup = ref(null); // 弹窗实例
  59. // 响应式状态
  60. const isLoading = ref(true);
  61. const error = ref(null);
  62. const stats = ref({ samples: 0 });
  63. // 统计数据(拆分两组)
  64. const initialCdStats = ref([]); // 初始Cd统计
  65. const otherIndicatorsStats = ref([]); // 其他指标统计
  66. // 字段配置(拆分初始Cd和其他指标)
  67. const fieldConfig = {
  68. initialCd: [
  69. { key: 'Initial_Cd', name: '土壤初始Cd总量', color: '#5470c6' }
  70. ],
  71. otherIndicators: [
  72. { key: 'DQCJ_Cd', name: '大气沉降输入Cd', color: '#91cc75' },
  73. { key: 'GGS_Cd', name: '灌溉水输入Cd', color: '#fac858' },
  74. { key: 'NCP_Cd', name: '农业投入输入Cd', color: '#ee6666' },
  75. { key: 'DX_Cd', name: '地下渗漏Cd', color: '#73c0de' },
  76. { key: 'DB_Cd', name: '地表径流Cd', color: '#38b2ac' },
  77. { key: 'ZL_Cd', name: '籽粒移除Cd', color: '#4169e1' },
  78. ]
  79. };
  80. // 数据请求
  81. const fetchData = async () => {
  82. try {
  83. const apiUrl = 'http://localhost:8000/api/vector/stats/FluxCd_input_data';
  84. const response = await axios.get(apiUrl);
  85. const rawData = response.data.features
  86. ? response.data.features.map(f => f.properties)
  87. : response.data;
  88. return rawData;
  89. } catch (err) {
  90. throw new Error('数据加载失败: ' + err.message);
  91. }
  92. };
  93. // 分位数计算(QUARTILE.INC)
  94. const calculatePercentile = (sortedArray, percentile) => {
  95. const n = sortedArray.length;
  96. if (n === 0) return null;
  97. if (percentile <= 0) return sortedArray[0];
  98. if (percentile >= 100) return sortedArray[n - 1];
  99. const index = (n - 1) * (percentile / 100);
  100. const lowerIndex = Math.floor(index);
  101. const upperIndex = lowerIndex + 1;
  102. const fraction = index - lowerIndex;
  103. if (upperIndex >= n) return sortedArray[lowerIndex];
  104. return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex]);
  105. };
  106. // 计算单个字段的统计量
  107. const calculateFieldStats = (statsData, fieldKey, fieldName) => {
  108. // 从接口数据中获取当前字段的统计结果
  109. const fieldStats = statsData[fieldKey];
  110. if (!fieldStats) {
  111. return { key: fieldKey, name: fieldName, min: null, q1: null, median: null, q3: null, max: null };
  112. }
  113. // 提取预统计值
  114. let min = fieldStats.min;
  115. let q1 = fieldStats.q1;
  116. let median = fieldStats.median;
  117. let q3 = fieldStats.q3;
  118. let max = fieldStats.max;
  119. // 强制校正统计量顺序(确保 min ≤ q1 ≤ median ≤ q3 ≤ max)
  120. const sortedStats = [min, q1, median, q3, max].sort((a, b) => a - b);
  121. return {
  122. key: fieldKey,
  123. name: fieldName,
  124. min: sortedStats[0],
  125. q1: sortedStats[1],
  126. median: sortedStats[2],
  127. q3: sortedStats[3],
  128. max: sortedStats[4]
  129. };
  130. };
  131. // 计算所有统计数据(拆分两组)
  132. const calculateAllStats = (statsData) => {
  133. // 1. 初始Cd统计(与配置顺序一致)
  134. initialCdStats.value = fieldConfig.initialCd.map(indicator =>
  135. calculateFieldStats(statsData, indicator.key, indicator.name)
  136. );
  137. // 2. 其他指标统计(与配置顺序一致)
  138. otherIndicatorsStats.value = fieldConfig.otherIndicators.map(indicator =>
  139. calculateFieldStats(statsData, indicator.key, indicator.name)
  140. );
  141. // 3. 更新样本数(从预统计数据中取第一个字段的count)
  142. const firstFieldKey = fieldConfig.initialCd[0]?.key || fieldConfig.otherIndicators[0]?.key;
  143. stats.value.samples = statsData[firstFieldKey]?.count || 0;
  144. };
  145. // 构建箱线图数据(通用函数)
  146. const buildBoxplotData = (statsArray) => {
  147. return statsArray.map(stat => {
  148. if (!stat.min) return [null, null, null, null, null];
  149. return [stat.min, stat.q1, stat.median, stat.q3, stat.max];
  150. });
  151. };
  152. // 初始化【初始Cd】图表(独立箱线图)
  153. const initInitialCdChart = () => {
  154. // 容器存在性检查
  155. if (!initialCdChart.value) {
  156. console.error('initialCdChart容器未找到');
  157. error.value = new Error('初始Cd图表容器未找到,请刷新页面重试');
  158. return;
  159. }
  160. // 容器尺寸检查
  161. const { offsetWidth, offsetHeight } = initialCdChart.value;
  162. if (offsetWidth === 0 || offsetHeight === 0) {
  163. console.error('initialCdChart容器尺寸异常', { offsetWidth, offsetHeight });
  164. error.value = new Error('初始Cd图表容器尺寸异常,请检查页面样式');
  165. return;
  166. }
  167. // 销毁旧实例
  168. if (chartInstanceInitial.value) {
  169. chartInstanceInitial.value.dispose();
  170. }
  171. // 初始化图表
  172. try {
  173. chartInstanceInitial.value = echarts.init(initialCdChart.value);
  174. const xAxisData = fieldConfig.initialCd.map(ind => ind.name);
  175. const boxData = buildBoxplotData(initialCdStats.value);
  176. chartInstanceInitial.value.setOption({
  177. title: { text: '初始Cd分布箱线图', left: 'center', textStyle: { fontSize: 14 } },
  178. tooltip: {
  179. trigger: "item",
  180. formatter: (params) => formatTooltip(initialCdStats.value[params.dataIndex])
  181. },
  182. grid: { top: 60, right: 30, bottom: 25, left: 60 },
  183. xAxis: {
  184. type: "category",
  185. data: xAxisData,
  186. axisLabel: { fontSize: 12 }
  187. },
  188. yAxis: {
  189. type: "value",
  190. name: 'g/ha',
  191. nameTextStyle: { fontSize: 12 },
  192. axisLabel: { fontSize: 11 },
  193. scale: true
  194. },
  195. series: [{
  196. name: '初始Cd',
  197. type: "boxplot",
  198. itemStyle: {
  199. color: (p) => fieldConfig.initialCd[p.dataIndex].color,
  200. borderWidth: 2
  201. },
  202. data: boxData
  203. }]
  204. });
  205. } catch (err) {
  206. console.error('初始Cd图表初始化失败', err);
  207. error.value = new Error(`初始Cd图表初始化失败: ${err.message}`);
  208. }
  209. };
  210. // 初始化【其他指标】合并图表
  211. const initOtherIndicatorsChart = () => {
  212. // 容器存在性检查
  213. if (!otherIndicatorsChart.value) {
  214. console.error('otherIndicatorsChart容器未找到');
  215. error.value = new Error('其他指标图表容器未找到,请刷新页面重试');
  216. return;
  217. }
  218. // 容器尺寸检查
  219. const { offsetWidth, offsetHeight } = otherIndicatorsChart.value;
  220. if (offsetWidth === 0 || offsetHeight === 0) {
  221. console.error('otherIndicatorsChart容器尺寸异常', { offsetWidth, offsetHeight });
  222. error.value = new Error('其他指标图表容器尺寸异常,请检查页面样式');
  223. return;
  224. }
  225. // 销毁旧实例
  226. if (chartInstanceOther.value) {
  227. chartInstanceOther.value.dispose();
  228. }
  229. // 初始化图表
  230. try {
  231. chartInstanceOther.value = echarts.init(otherIndicatorsChart.value);
  232. const xAxisData = fieldConfig.otherIndicators.map(ind => ind.name);
  233. const boxData = buildBoxplotData(otherIndicatorsStats.value);
  234. chartInstanceOther.value.setOption({
  235. title: { text: '其他通量Cd指标分布对比', left: 'center', textStyle: { fontSize: 14 } },
  236. tooltip: {
  237. trigger: "item",
  238. formatter: (params) => formatTooltip(otherIndicatorsStats.value[params.dataIndex])
  239. },
  240. grid: { top: 60, right: 30, bottom: 70, left: 60 },
  241. xAxis: {
  242. type: "category",
  243. data: xAxisData,
  244. axisLabel: {
  245. fontSize: 11,
  246. rotate: 45,
  247. interval: 0, // 强制显示所有标签
  248. formatter: (value) => value.length > 8 ? value.substring(0, 8) + '...' : value
  249. }
  250. },
  251. yAxis: {
  252. type: "value",
  253. name: 'g/ha/a',
  254. nameTextStyle: { fontSize: 12 },
  255. axisLabel: { fontSize: 11 }
  256. },
  257. series: [{
  258. name: '其他指标',
  259. type: "boxplot",
  260. itemStyle: {
  261. color: (p) => fieldConfig.otherIndicators[p.dataIndex].color,
  262. borderWidth: 2
  263. },
  264. data: boxData
  265. }]
  266. });
  267. } catch (err) {
  268. console.error('其他指标图表初始化失败', err);
  269. error.value = new Error(`其他指标图表初始化失败: ${err.message}`);
  270. }
  271. };
  272. // Tooltip格式化(通用逻辑)
  273. const formatTooltip = (stat) => {
  274. if (!stat || !stat.min) {
  275. return `<div style="font-weight:bold;color:#f56c6c">${stat?.name || '未知'}</div><div>无有效数据</div>`;
  276. }
  277. return `<div style="font-weight:bold">${stat.name}</div>
  278. <div style="margin-top:8px">
  279. <div>最小值:<span style="color:#5a5;">${stat.min.toFixed(4)}</span></div>
  280. <div>下四分位:<span style="color:#d87a80;">${stat.q1.toFixed(4)}</span></div>
  281. <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${stat.median.toFixed(4)}</span></div>
  282. <div>上四分位:<span style="color:#d87a80;">${stat.q3.toFixed(4)}</span></div>
  283. <div>最大值:<span style="color:#5a5;">${stat.max.toFixed(4)}</span></div>
  284. </div>`;
  285. };
  286. // 初始化图表主流程
  287. const initCharts = async () => {
  288. try {
  289. isLoading.value = true;
  290. error.value = null;
  291. chartInstanceInitial.value = null;
  292. chartInstanceOther.value = null;
  293. // 1. 获取数据
  294. const data = await fetchData();
  295. if (!data || data.length === 0) {
  296. throw new Error('未获取到有效数据');
  297. }
  298. // 2. 计算统计数据
  299. calculateAllStats(data);
  300. // 3. 等待DOM更新
  301. await nextTick();
  302. // 4. 轮询检查容器尺寸(最多等待3秒)
  303. const checkContainers = () => {
  304. return new Promise((resolve, reject) => {
  305. let checkCount = 0;
  306. const interval = setInterval(() => {
  307. // 检查两个容器的宽度是否有效
  308. const initialWidth = initialCdChart.value?.offsetWidth || 0;
  309. const otherWidth = otherIndicatorsChart.value?.offsetWidth || 0;
  310. if (initialWidth > 0 && otherWidth > 0) {
  311. clearInterval(interval);
  312. resolve();
  313. } else if (checkCount >= 30) { // 30 * 100ms = 3秒
  314. clearInterval(interval);
  315. reject(new Error('图表容器尺寸异常,准备超时,请检查样式'));
  316. }
  317. checkCount++;
  318. }, 100);
  319. });
  320. };
  321. await checkContainers();
  322. // 5. 初始化图表
  323. initInitialCdChart();
  324. initOtherIndicatorsChart();
  325. isLoading.value = false;
  326. } catch (err) {
  327. isLoading.value = false;
  328. error.value = err;
  329. console.error('初始化失败:', err);
  330. }
  331. };
  332. // 组件挂载 & 销毁
  333. onMounted(() => {
  334. initCharts();
  335. // 窗口resize响应
  336. const handleResize = () => {
  337. if (chartInstanceInitial.value) chartInstanceInitial.value.resize();
  338. if (chartInstanceOther.value) chartInstanceOther.value.resize();
  339. if (chartInstancePopup.value) chartInstancePopup.value.resize();
  340. };
  341. window.addEventListener('resize', handleResize);
  342. return () => {
  343. window.removeEventListener('resize', handleResize);
  344. if (chartInstanceInitial.value) chartInstanceInitial.value.dispose();
  345. if (chartInstanceOther.value) chartInstanceOther.value.dispose();
  346. if (chartInstancePopup.value) chartInstancePopup.value.dispose();
  347. };
  348. });
  349. </script>
  350. <style scoped>
  351. .flux-cd-dashboard {
  352. font-family: 'PingFang SC', 'Microsoft YaHei', sans-serif;
  353. max-width: 1200px;
  354. margin: 0 auto;
  355. font-size: 14px;
  356. }
  357. .chart-container {
  358. background: white;
  359. border-radius: 6px;
  360. box-shadow: 0 2px 4px rgba(0, 0, 0, 0.1);
  361. padding: 16px;
  362. margin-bottom: 16px;
  363. min-height: 400px;
  364. position: relative;
  365. }
  366. [ref="initialCdChart"],
  367. [ref="otherIndicatorsChart"] {
  368. min-height: 400px;
  369. background-color: #f9f9f9;
  370. border: 1px dashed #eee;
  371. }
  372. .section-title {
  373. color: #2c3e50;
  374. border-left: 3px solid #3498db;
  375. padding-left: 10px;
  376. margin-bottom: 12px;
  377. }
  378. .legend-item {
  379. display: flex;
  380. align-items: center;
  381. margin-right: 12px;
  382. margin-bottom: 6px;
  383. font-size: 12px;
  384. }
  385. .legend-color {
  386. width: 10px;
  387. height: 10px;
  388. border-radius: 50%;
  389. margin-right: 5px;
  390. }
  391. .spinner {
  392. width: 30px;
  393. height: 30px;
  394. border: 3px solid rgba(0, 0, 0, 0.1);
  395. border-radius: 50%;
  396. border-left-color: #3498db;
  397. animation: spin 1s linear infinite;
  398. }
  399. @keyframes spin { to { transform: rotate(360deg); } }
  400. .stat-card {
  401. background: linear-gradient(135deg, #f5f7fa 0%, #e4edf5 100%);
  402. border-radius: 6px;
  403. padding: 8px 12px;
  404. box-shadow: 0 1px 3px rgba(0,0,0,0.05);
  405. }
  406. .stat-value {
  407. font-size: 16px;
  408. font-weight: bold;
  409. color: #2c3e50;
  410. }
  411. .stat-label {
  412. font-size: 12px;
  413. color: #7f8c8d;
  414. }
  415. </style>