| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244 |
- <template>
- <div class="boxplot-container">
- <div class="chart-container">
- <div class="header">
- <div class="chart-title">灌溉水重金属浓度统计箱线图</div>
- <p>展示各重金属浓度的分布特征(最小值、四分位数、中位数、最大值)</p>
- <p class="sample-subtitle">样本来源:{{ totalPoints }}个数据</p>
- </div>
- <div v-if="isLoading" class="loading-state">
- <span class="spinner"></span> 数据加载中...
- </div>
- <div v-else-if="error" class="error-state">
- ❌ 加载失败:{{ error.message }}
- </div>
- <div v-else>
- <div class="chart-wrapper">
- <v-chart :option="chartOption" autoresize />
- </div>
- </div>
- </div>
- </div>
- </template>
- <script>
- import * as echarts from 'echarts'
- import VChart from 'vue-echarts'
- import { api8000 } from '@/utils/request' // 导入 api8000 实例
- import { ref, onMounted ,computed} from 'vue'
- export default {
- components: { VChart },
- setup() {
- // -------- 基本状态 --------
- const apiUrl = ref('/api/vector/export/all?table_name=water_sampling_data') // 使用相对路径
- const apiTimestamp = ref(null)
- const sampleCount = ref(0)
- const statsData = ref({});
- const chartOption = ref({})
- const isLoading = ref(true)
- const error = ref(null)
- const stats = null;
- const totalPoints = computed(() => {
- const firstMetalKey = heavyMetals[0]?.key;
- return statsData.value[firstMetalKey]?.count || 0;
- })
- // 关键:缓存每个品类的统计量(与 x 轴顺序一致)
- const statsByIndex = ref([])
- // -------- 配置:金属字段(保持你原样) --------
- const heavyMetals = [
- { key: 'cr_concentration', name: '铬 (Cr)', color: '#FF9800' },
- { key: 'as_concentration', name: '砷 (As)', color: '#4CAF50' },
- { key: 'cd_concentration', name: '镉 (Cd)', color: '#9C27B0' },
- { key: 'hg_concentration', name: '汞 (Hg)', color: '#2196F3' },
- { key: 'pb_concentration', name: '铅 (Pb)', color: '#F44336' }
- ]
- // -------- 构建箱线数据(保留你自己的顺序) --------
- const buildBoxplotData = () => {
- const xAxisData = heavyMetals.map(m => m.name); // 保持x轴顺序与配置一致
- // 缓存每个重金属的统计量(用于tooltip)
- statsByIndex.value = heavyMetals.map(metal => {
- const stat = statsData.value[metal.key] || {}; // 从接口数据中取当前金属的统计
- return {
- key: metal.key,
- name: metal.name,
- min: stat.min,
- q1: stat.q1,
- median: stat.median,
- q3: stat.q3,
- max: stat.max,
- color: metal.color
- };
- });
- // 构建ECharts箱线图所需的二维数组格式 [[min, q1, median, q3, max], ...]
- const data = statsByIndex.value.map(s => {
- if (s.min === undefined || s.min === null) {
- return [null, null, null, null, null]; // 无数据时返回空数组
- }
- return [s.min, s.q1, s.median, s.q3, s.max];
- });
- return { xAxisData, data };
- };
- // -------- 初始化图表 --------
- const initChart = () => {
- const { xAxisData, data } = buildBoxplotData(stats)
- chartOption.value = {
- tooltip: {
- trigger: 'item',
- // 关键:不从 params.data 取,直接读我们缓存的原始统计值,彻底避免被内部处理影响
- formatter: (params) => {
- const s = statsByIndex.value[params.dataIndex]
- if (!s || s.min === null) {
- return `<div style="font-weight:bold;color:#f56c6c">${xAxisData[params.dataIndex]}</div><div>无有效数据</div>`
- }
- return `<div style="font-weight:bold">${xAxisData[params.dataIndex]}</div>
- <div style="margin-top:8px">
- <div>最小值:<span style="color:#5a5;">${s.min.toFixed(4)}</span></div>
- <div>下四分位:<span style="color:#d87a80;">${s.q1.toFixed(4)}</span></div>
- <div>中位数:<span style="color:#f56c6c;font-weight:bold;">${s.median.toFixed(4)}</span></div>
- <div>上四分位:<span style="color:#d87a80;">${s.q3.toFixed(4)}</span></div>
- <div>最大值:<span style="color:#5a5;">${s.max.toFixed(4)}</span></div>
- </div>`
- },
- },
- xAxis: {
- type: 'category',
- data: xAxisData,
- name: '重金属类型',
- nameLocation: 'middle',
- nameGap: 30,
- axisLabel: { color: '#555', rotate: 0, fontWeight: 'bold' ,fontSize :11}
- },
- yAxis: {
- type: 'value',
- name: 'ug/L',
- nameTextStyle: { fontSize: 12 },
- nameLocation: 'end',
- nameGap: 8,
- axisLabel: { color: '#555', fontWeight: 'bold',fontSize:11 },
- splitLine: { lineStyle: { color: '#f0f0f0' } }
- },
- series: [{
- name: '重金属浓度分布',
- type: 'boxplot',
- data,
- itemStyle: {
- color: (p) => (heavyMetals[p.dataIndex]?.color || '#1890ff'),
- borderWidth: 2
- },
- emphasis: {
- itemStyle: { shadowBlur: 10, shadowColor: 'rgba(0,0,0,0.2)', borderWidth: 3 }
- }
- }],
- grid: { top: '10%', right: '3%', left: '6%', bottom: '10%' }
- }
- isLoading.value = false
- }
- // -------- 拉取接口并绘图 --------
- onMounted(async () => {
- try {
- //log('发起API请求...')
- const response = await axios.get(apiUrl.value);
- statsData.value = response.data;
- apiTimestamp.value = new Date().toLocaleString()
- initChart()
- } catch (err) {
- error.value = err
- isLoading.value = false
- console.error('接口请求失败:', err)
- }
- })
- return {
- apiUrl,
- apiTimestamp,
- sampleCount,
- chartOption,
- isLoading,
- error,
- totalPoints
- }
- }
- }
- </script>
- <style scoped>
- .boxplot-container {
- width: 100%;
- height: 300px;
- max-width: 1000px;
- margin: 0 auto;
- }
- .header {
- text-align: left;
- margin-bottom: 10px;
- }
- .header h2 {
- font-size: 0.6rem;
- color: #333;
- margin-bottom: 4px;
- }
- .header p {
- font-size: 0.6rem;
- color: #666;
- margin: 0;
- }
- .loading-state {
- text-align: center;
- padding: 40px 0;
- color: #666;
- }
- .loading-state .spinner {
- display: inline-block; width: 24px; height: 24px; margin-right: 8px;
- border: 3px solid #ccc; border-top-color: #1890ff; border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin { to { transform: rotate(360deg); } }
- .error-state { text-align: center; padding: 40px 0; color: #f56c6c; }
- .chart-wrapper { width: 100%; height: 220px; }
- .chart-container {
- background: white;
- border-radius: 12px;
- box-shadow: 0 4px 15px rgba(0, 0, 0, 0.08);
- margin-bottom: 25px;
- height: 100%;
- box-sizing: border-box;
- }
- .chart-header {
- display: flex;
- justify-content: space-between;
- align-items: flex-start; /**与顶部对齐 */
- margin-bottom: 15px;
- }
- .chart-title {
- font-size: 14px;
- color: #2980b9;
- font-weight: 600;
- }
- .title-group {
- display: flex;
- align-items: left;
- }
- .sample-subtitle {
- font-size: 0.85rem;
- color: #888;
- margin-top: 4px;
- }
- </style>
|