waterassaydata3.vue 5.7 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197
  1. <template>
  2. <div class="heavy-metal-radar">
  3. <h2 class="chart-title">重金属指标雷达图分析</h2>
  4. <canvas ref="chartRef" class="chart-box"></canvas>
  5. <div v-if="loading" class="status">数据加载中...</div>
  6. <div v-else-if="error" class="status error">{{ error }}</div>
  7. </div>
  8. </template>
  9. <script setup>
  10. import { ref, onMounted, onUnmounted } from 'vue';
  11. import Chart from 'chart.js/auto';
  12. import axios from 'axios';
  13. // ========== 接口配置(和柱状图对齐) ==========
  14. const ASSAY_API = 'http://localhost:3000/table/Water_assay_data'; // 复用柱状图的接口
  15. // ========== 配置项(模仿柱状图) ==========
  16. const EXCLUDE_FIELDS = [
  17. 'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time',
  18. 'assay_instrument_model', 'water_sample_ID', 'pH'
  19. ];
  20. const COLORS = ['#165DFF', '#36CFC9', '#722ED1']; // 雷达图三色
  21. // ========== 响应式数据 ==========
  22. const chartRef = ref(null);
  23. const loading = ref(true);
  24. const error = ref('');
  25. let radarChart = null;
  26. // ========== 数据处理:提取重金属指标 + 统计计算 ==========
  27. const processRadarData = (assayData) => {
  28. // 1. 提取重金属字段(排除指定字段,且为数值类型)
  29. const metals = Object.keys(assayData[0] || {})
  30. .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(assayData[0][key])));
  31. // 2. 计算每个重金属的统计值(均值、中位数、标准差)
  32. const stats = metals.map(metal => {
  33. const values = assayData.map(item => parseFloat(item[metal])).filter(v => !isNaN(v));
  34. return {
  35. mean: calculateMean(values),
  36. median: calculateMedian(values),
  37. std: calculateStdDev(values)
  38. };
  39. });
  40. return { metals, stats };
  41. };
  42. // ========== 统计工具函数 ==========
  43. const calculateMean = (values) => {
  44. if (values.length === 0) return 0;
  45. return values.reduce((sum, val) => sum + val, 0) / values.length;
  46. };
  47. const calculateMedian = (values) => {
  48. if (values.length === 0) return 0;
  49. const sorted = [...values].sort((a, b) => a - b);
  50. const mid = Math.floor(sorted.length / 2);
  51. return sorted.length % 2 === 0
  52. ? (sorted[mid - 1] + sorted[mid]) / 2
  53. : sorted[mid];
  54. };
  55. const calculateStdDev = (values) => {
  56. if (values.length <= 1) return 0;
  57. const mean = calculateMean(values);
  58. const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
  59. return Math.sqrt(variance);
  60. };
  61. // ========== 初始化雷达图(Chart.js) ==========
  62. const initRadarChart = ({ metals, stats }) => {
  63. if (!chartRef.value) return;
  64. if (radarChart) radarChart.destroy();
  65. const ctx = chartRef.value.getContext('2d');
  66. radarChart = new Chart(ctx, {
  67. type: 'radar',
  68. data: {
  69. labels: metals,
  70. datasets: [
  71. {
  72. label: '均值',
  73. data: stats.map(s => s.mean.toFixed(2)),
  74. borderColor: COLORS[0],
  75. backgroundColor: 'rgba(22, 93, 255, 0.1)',
  76. pointRadius: 4,
  77. borderWidth: 2
  78. },
  79. {
  80. label: '中位数',
  81. data: stats.map(s => s.median.toFixed(2)),
  82. borderColor: COLORS[1],
  83. backgroundColor: 'rgba(54, 207, 201, 0.1)',
  84. pointRadius: 4,
  85. borderWidth: 2
  86. },
  87. {
  88. label: '标准差',
  89. data: stats.map(s => s.std.toFixed(2)),
  90. borderColor: COLORS[2],
  91. backgroundColor: 'rgba(114, 46, 209, 0.1)',
  92. pointRadius: 4,
  93. borderWidth: 2
  94. }
  95. ]
  96. },
  97. options: {
  98. responsive: true,
  99. maintainAspectRatio: false,
  100. scales: {
  101. r: {
  102. beginAtZero: true,
  103. ticks: { display: false },
  104. pointLabels: { font: { size: 12, weight: 'bold' } },
  105. grid: { color: 'rgba(0,0,0,0.05)' },
  106. angleLines: { color: 'rgba(0,0,0,0.1)' }
  107. }
  108. },
  109. plugins: {
  110. legend: { position: 'bottom' },
  111. tooltip: {
  112. callbacks: {
  113. label: (ctx) => `${ctx.dataset.label}: ${ctx.raw} mg/L`
  114. }
  115. }
  116. }
  117. }
  118. });
  119. };
  120. // ========== 生命周期钩子(和柱状图对齐) ==========
  121. onMounted(async () => {
  122. try {
  123. // 【关键】和柱状图一样,axios 请求 **不携带凭证**(withCredentials: false,默认就是false)
  124. const assayRes = await axios.get(ASSAY_API, { timeout: 10000, withCredentials:false });
  125. const processed = processRadarData(assayRes.data);
  126. if (processed.metals.length === 0) {
  127. throw new Error('未检测到有效重金属指标');
  128. }
  129. initRadarChart(processed);
  130. } catch (err) {
  131. error.value = '数据加载失败: ' + (err.message || '未知错误');
  132. console.error('接口错误:', err);
  133. } finally {
  134. loading.value = false;
  135. }
  136. });
  137. // 响应式resize(模仿柱状图)
  138. const resizeHandler = () => radarChart && radarChart.resize();
  139. onMounted(() => window.addEventListener('resize', resizeHandler));
  140. onUnmounted(() => window.removeEventListener('resize', resizeHandler));
  141. </script>
  142. <style scoped>
  143. .heavy-metal-radar {
  144. width: 100%;
  145. max-width: 800px;
  146. margin: 20px auto;
  147. position: relative;
  148. padding-top: 0;
  149. background-color: white;
  150. border-radius: 8px;
  151. }
  152. .chart-box {
  153. width: 100%;
  154. min-height: 350px;
  155. max-height: 600px;
  156. height: auto;
  157. box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
  158. }
  159. .chart-title {
  160. text-align: center; /* 水平居中 */
  161. font-size: 18px; /* 字体大小 */
  162. font-weight: 600; /* 加粗 */
  163. color: #333; /* 字体颜色 */
  164. margin: 10px 0; /* 底部间距,避免和图表贴紧 */
  165. }
  166. .status {
  167. position: absolute;
  168. top: 50%;
  169. left: 50%;
  170. transform: translate(-50%, -50%);
  171. padding: 15px;
  172. background: rgba(255,255,255,0.8);
  173. border-radius: 4px;
  174. z-index: 10;
  175. }
  176. .error {
  177. color: #ff4d4f;
  178. font-weight: bold;
  179. }
  180. </style>