123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197 |
- <template>
- <div class="heavy-metal-radar">
- <h2 class="chart-title">重金属指标雷达图分析</h2>
- <canvas ref="chartRef" class="chart-box"></canvas>
- <div v-if="loading" class="status">数据加载中...</div>
- <div v-else-if="error" class="status error">{{ error }}</div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted } from 'vue';
- import Chart from 'chart.js/auto';
- import axios from 'axios';
- // ========== 接口配置(和柱状图对齐) ==========
- const ASSAY_API = 'http://localhost:3000/table/Water_assay_data'; // 复用柱状图的接口
- // ========== 配置项(模仿柱状图) ==========
- const EXCLUDE_FIELDS = [
- 'water_assay_ID', 'sample_code', 'assayer_ID', 'assay_time',
- 'assay_instrument_model', 'water_sample_ID', 'pH'
- ];
- const COLORS = ['#165DFF', '#36CFC9', '#722ED1']; // 雷达图三色
- // ========== 响应式数据 ==========
- const chartRef = ref(null);
- const loading = ref(true);
- const error = ref('');
- let radarChart = null;
- // ========== 数据处理:提取重金属指标 + 统计计算 ==========
- const processRadarData = (assayData) => {
- // 1. 提取重金属字段(排除指定字段,且为数值类型)
- const metals = Object.keys(assayData[0] || {})
- .filter(key => !EXCLUDE_FIELDS.includes(key) && !isNaN(parseFloat(assayData[0][key])));
- // 2. 计算每个重金属的统计值(均值、中位数、标准差)
- const stats = metals.map(metal => {
- const values = assayData.map(item => parseFloat(item[metal])).filter(v => !isNaN(v));
- return {
- mean: calculateMean(values),
- median: calculateMedian(values),
- std: calculateStdDev(values)
- };
- });
- return { metals, stats };
- };
- // ========== 统计工具函数 ==========
- const calculateMean = (values) => {
- if (values.length === 0) return 0;
- return values.reduce((sum, val) => sum + val, 0) / values.length;
- };
- const calculateMedian = (values) => {
- if (values.length === 0) return 0;
- const sorted = [...values].sort((a, b) => a - b);
- const mid = Math.floor(sorted.length / 2);
- return sorted.length % 2 === 0
- ? (sorted[mid - 1] + sorted[mid]) / 2
- : sorted[mid];
- };
- const calculateStdDev = (values) => {
- if (values.length <= 1) return 0;
- const mean = calculateMean(values);
- const variance = values.reduce((sum, val) => sum + Math.pow(val - mean, 2), 0) / values.length;
- return Math.sqrt(variance);
- };
- // ========== 初始化雷达图(Chart.js) ==========
- const initRadarChart = ({ metals, stats }) => {
- if (!chartRef.value) return;
- if (radarChart) radarChart.destroy();
- const ctx = chartRef.value.getContext('2d');
- radarChart = new Chart(ctx, {
- type: 'radar',
- data: {
- labels: metals,
- datasets: [
- {
- label: '均值',
- data: stats.map(s => s.mean.toFixed(2)),
- borderColor: COLORS[0],
- backgroundColor: 'rgba(22, 93, 255, 0.1)',
- pointRadius: 4,
- borderWidth: 2
- },
- {
- label: '中位数',
- data: stats.map(s => s.median.toFixed(2)),
- borderColor: COLORS[1],
- backgroundColor: 'rgba(54, 207, 201, 0.1)',
- pointRadius: 4,
- borderWidth: 2
- },
- {
- label: '标准差',
- data: stats.map(s => s.std.toFixed(2)),
- borderColor: COLORS[2],
- backgroundColor: 'rgba(114, 46, 209, 0.1)',
- pointRadius: 4,
- borderWidth: 2
- }
- ]
- },
- options: {
- responsive: true,
- maintainAspectRatio: false,
- scales: {
- r: {
- beginAtZero: true,
- ticks: { display: false },
- pointLabels: { font: { size: 12, weight: 'bold' } },
- grid: { color: 'rgba(0,0,0,0.05)' },
- angleLines: { color: 'rgba(0,0,0,0.1)' }
- }
- },
- plugins: {
- legend: { position: 'bottom' },
- tooltip: {
- callbacks: {
- label: (ctx) => `${ctx.dataset.label}: ${ctx.raw} mg/L`
- }
- }
- }
- }
- });
- };
- // ========== 生命周期钩子(和柱状图对齐) ==========
- onMounted(async () => {
- try {
- // 【关键】和柱状图一样,axios 请求 **不携带凭证**(withCredentials: false,默认就是false)
- const assayRes = await axios.get(ASSAY_API, { timeout: 10000, withCredentials:false });
- const processed = processRadarData(assayRes.data);
-
- if (processed.metals.length === 0) {
- throw new Error('未检测到有效重金属指标');
- }
-
- initRadarChart(processed);
- } catch (err) {
- error.value = '数据加载失败: ' + (err.message || '未知错误');
- console.error('接口错误:', err);
- } finally {
- loading.value = false;
- }
- });
- // 响应式resize(模仿柱状图)
- const resizeHandler = () => radarChart && radarChart.resize();
- onMounted(() => window.addEventListener('resize', resizeHandler));
- onUnmounted(() => window.removeEventListener('resize', resizeHandler));
- </script>
- <style scoped>
- .heavy-metal-radar {
- width: 100%;
- max-width: 800px;
- margin: 20px auto;
- position: relative;
- padding-top: 0;
- background-color: white;
- border-radius: 8px;
- }
- .chart-box {
- width: 100%;
- min-height: 350px;
- max-height: 600px;
- height: auto;
- box-shadow: 0 3px 10px rgba(0, 0, 0, 0.1);
- }
- .chart-title {
- text-align: center; /* 水平居中 */
- font-size: 18px; /* 字体大小 */
- font-weight: 600; /* 加粗 */
- color: #333; /* 字体颜色 */
- margin: 10px 0; /* 底部间距,避免和图表贴紧 */
- }
- .status {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 15px;
- background: rgba(255,255,255,0.8);
- border-radius: 4px;
- z-index: 10;
- }
- .error {
- color: #ff4d4f;
- font-weight: bold;
- }
- </style>
|