refluxcdStatictics.vue 8.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341
  1. <template>
  2. <div class="container">
  3. <div class="chart-container">
  4. <div v-if="isLoading" class="loading-overlay">
  5. <div class="spinner"></div>
  6. <span class="loading-text">数据加载中...</span>
  7. </div>
  8. <div v-if="error" class="error-overlay">
  9. <p>{{ errorMessage }}</p>
  10. <button class="retry-btn" @click="loadData">重试</button>
  11. </div>
  12. <div ref="chartRef" class="chart-wrapper"></div>
  13. </div>
  14. </div>
  15. </template>
  16. <script setup>
  17. import { ref, onMounted, onUnmounted, nextTick } from 'vue'
  18. import * as echarts from 'echarts'
  19. import { api5000 } from '@/utils/request'; // 导入 api5000 实例
  20. const indicators = [
  21. { key: 'Delta_pH', name: 'Delta_pH', color: '#f39c12' }
  22. ];
  23. let chart = ref(null) // 存储 ECharts 实例
  24. let chartData = ref([]) // 存储图表数据
  25. const chartRef = ref(null) // 图表容器
  26. const statsByIndex = ref([]); // 存储每个指标的统计量
  27. const isLoading = ref(true);
  28. const error = ref(false);
  29. const errorMessage = ref('');
  30. function initChart() {
  31. // 确保图表容器已渲染
  32. if (!chartRef.value) return;
  33. // 初始化 ECharts 实例
  34. chart.value = echarts.init(chartRef.value);
  35. const option = {
  36. tooltip: {
  37. trigger: 'item',
  38. axisPointer: { type: 'shadow' },
  39. formatter: function(params) {
  40. const stat = statsByIndex.value[params.dataIndex];
  41. if (!stat || stat.min === null) {
  42. return `<div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${params.name}</div>
  43. <div>无有效数据</div>`;
  44. }
  45. return `
  46. <div style="font-weight:bold;margin-bottom:8px;color:#2c3e50;">${stat.name}</div>
  47. <div style="display:grid;grid-template-columns:1fr 1fr;gap:5px;">
  48. <span style="color:#7f8c8d;">最小值:</span> <span style="text-align:right;font-weight:bold;">${stat.min}</span>
  49. <span style="color:#7f8c8d;">下四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q1}</span>
  50. <span style="color:#7f8c8d;">中位数:</span> <span style="text-align:right;font-weight:bold;color:#3498db;">${stat.median}</span>
  51. <span style="color:#7f8c8d;">上四分位:</span> <span style="text-align:right;font-weight:bold;color:#e74c3c;">${stat.q3}</span>
  52. <span style="color:#7f8c8d;">最大值:</span> <span style="text-align:right;font-weight:bold;">${stat.max}</span>
  53. </div>
  54. `;
  55. }
  56. },
  57. title: {
  58. text: '酸化加剧指标箱线图展示',
  59. left: 'center',
  60. textStyle: {
  61. fontSize: 18,
  62. fontWeight: 'normal'
  63. },
  64. top: 10
  65. },
  66. grid: {
  67. left: '0px',
  68. right: '0px',
  69. bottom: '0px',
  70. top: '70px',
  71. containLabel: true
  72. },
  73. xAxis: {
  74. type: 'category',
  75. data: indicators.map(i => i.name),
  76. axisLabel: {
  77. rotate: 30,
  78. fontSize: 12,
  79. margin: 15
  80. },
  81. axisTick: {
  82. alignWithLabel: true
  83. },
  84. },
  85. yAxis: {
  86. type: 'value',
  87. name: '数值',
  88. nameTextStyle: {
  89. fontSize: 14
  90. },
  91. nameGap: 25,
  92. splitLine: {
  93. lineStyle: {
  94. type: 'dashed',
  95. color: '#ddd'
  96. }
  97. }
  98. },
  99. series: [{
  100. name: '指标分布',
  101. type: 'boxplot',
  102. itemStyle: {
  103. color: function(params) {
  104. return indicators[params.dataIndex].color;
  105. },
  106. borderWidth: 2
  107. },
  108. emphasis: {
  109. itemStyle: {
  110. shadowBlur: 10,
  111. shadowColor: 'rgba(0, 0, 0, 0.3)'
  112. }
  113. }
  114. }]
  115. };
  116. chart.value.setOption(option);
  117. }
  118. function calculatePercentile(sortedArray, percentile) {
  119. const n = sortedArray.length;
  120. if (n === 0) return null;
  121. if (percentile <= 0) return sortedArray[0];
  122. if (percentile >= 100) return sortedArray[n - 1];
  123. const index = (n - 1) * (percentile / 100);
  124. const lowerIndex = Math.floor(index);
  125. const upperIndex = lowerIndex + 1;
  126. const fraction = index - lowerIndex;
  127. if (upperIndex >= n) return sortedArray[lowerIndex];
  128. return sortedArray[lowerIndex] + fraction * (sortedArray[upperIndex] - sortedArray[lowerIndex]);
  129. }
  130. function calculateBoxplotStats(data, indicators) {
  131. const boxplotData = [];
  132. const statsArray = [];
  133. indicators.forEach(indicator => {
  134. const values = data
  135. .map(item => Number(item[indicator.key]))
  136. .filter(val => !isNaN(val))
  137. .sort((a, b) => a - b);
  138. if (values.length === 0) {
  139. boxplotData.push([null, null, null, null, null]);
  140. statsArray.push({
  141. min: null, q1: null, median: null, q3: null, max: null,
  142. name: indicator.name,
  143. color: indicator.color
  144. });
  145. } else {
  146. const min = Math.min(...values);
  147. const max = Math.max(...values);
  148. const q1 = calculatePercentile(values, 25);
  149. const median = calculatePercentile(values, 50);
  150. const q3 = calculatePercentile(values, 75);
  151. boxplotData.push([min, q1, median, q3, max]);
  152. statsArray.push({
  153. min, q1, median, q3, max,
  154. name: indicator.name,
  155. color: indicator.color
  156. });
  157. }
  158. });
  159. return { boxplotData, statsArray };
  160. }
  161. async function loadData() {
  162. isLoading.value = true;
  163. error.value = false;
  164. errorMessage.value = '';
  165. try {
  166. // 使用 api5000 实例获取数据
  167. const response = await api5000.get('/api/table-data?table_name=dataset_60');
  168. const result = response.data;
  169. // 检查响应是否成功
  170. if (!result || !result.data) {
  171. throw new Error('接口返回数据格式不正确');
  172. }
  173. chartData.value = result.data;
  174. const { boxplotData, statsArray } = calculateBoxplotStats(chartData.value, indicators);
  175. statsByIndex.value = statsArray;
  176. // 确保图表已初始化
  177. if (!chart.value) {
  178. initChart();
  179. }
  180. chart.value.setOption({
  181. series: [{
  182. data: boxplotData
  183. }]
  184. });
  185. } catch (err) {
  186. error.value = true;
  187. errorMessage.value = `加载失败: ${err.message || '网络错误'}`;
  188. console.error('数据加载失败:', err);
  189. } finally {
  190. isLoading.value = false;
  191. }
  192. }
  193. const handleResize = () => {
  194. if (chart.value) {
  195. chart.value.resize();
  196. }
  197. };
  198. onMounted(() => {
  199. nextTick(() => {
  200. initChart();
  201. loadData();
  202. });
  203. window.addEventListener('resize', handleResize);
  204. });
  205. onUnmounted(() => {
  206. if (chart.value) {
  207. chart.value.dispose(); // 销毁 ECharts 实例
  208. chart.value = null;
  209. }
  210. window.removeEventListener('resize', handleResize);
  211. });
  212. </script>
  213. <style scoped>
  214. * {
  215. margin: 0;
  216. padding: 0;
  217. box-sizing: border-box;
  218. font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
  219. }
  220. .container {
  221. width: 100%;
  222. height: 100%;
  223. margin: 0 auto;
  224. background: white;
  225. border-radius: 12px;
  226. box-shadow: 0 6px 18px rgba(0, 0, 0, 0.08);
  227. overflow: hidden;
  228. }
  229. .chart-container {
  230. width: 100%;
  231. height: 100%;
  232. padding: 20px;
  233. position: relative;
  234. }
  235. .chart-wrapper {
  236. width: 100%;
  237. height: 100%;
  238. min-height: 200px;
  239. }
  240. .loading-overlay {
  241. position: absolute;
  242. top: 0;
  243. left: 0;
  244. right: 0;
  245. bottom: 0;
  246. background: rgba(255, 255, 255, 0.8);
  247. display: flex;
  248. flex-direction: column;
  249. align-items: center;
  250. justify-content: center;
  251. z-index: 10;
  252. }
  253. .spinner {
  254. width: 40px;
  255. height: 40px;
  256. border: 4px solid rgba(0, 0, 0, 0.1);
  257. border-radius: 50%;
  258. border-left-color: #3498db;
  259. animation: spin 1s linear infinite;
  260. }
  261. .loading-text {
  262. margin-top: 15px;
  263. font-size: 16px;
  264. color: #2c3e50;
  265. }
  266. .error-overlay {
  267. position: absolute;
  268. top: 0;
  269. left: 0;
  270. right: 0;
  271. bottom: 0;
  272. background: rgba(255, 255, 255, 0.9);
  273. display: flex;
  274. flex-direction: column;
  275. align-items: center;
  276. justify-content: center;
  277. z-index: 10;
  278. padding: 20px;
  279. text-align: center;
  280. }
  281. .error-overlay p {
  282. color: #e74c3c;
  283. font-size: 16px;
  284. margin-bottom: 20px;
  285. }
  286. .retry-btn {
  287. padding: 8px 16px;
  288. background: #3498db;
  289. color: white;
  290. border: none;
  291. border-radius: 4px;
  292. cursor: pointer;
  293. font-size: 14px;
  294. transition: background 0.3s;
  295. }
  296. .retry-btn:hover {
  297. background: #2980b9;
  298. }
  299. @keyframes spin {
  300. to { transform: rotate(360deg); }
  301. }
  302. </style>