| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250 |
- <template>
- <div class="region-average-chart">
- <!-- 图表容器 -->
- <div ref="chartRef" class="chart-box"></div>
- <!-- 加载/错误提示 -->
- <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 * as echarts from 'echarts';
- import axios from 'axios';
- import { icon } from 'leaflet';
- // ========== 1. 配置项(根据实际需求修改) ==========
- const POLLUTANT_API = 'http://localhost:3000/table/Atmosphere_company_data';
- // 韶关市区县白名单(确保与接口返回的“所属区县”完全一致)
- const SG_REGIONS = [
- '浈江区', '武江区', '曲江区', '乐昌市',
- '南雄市', '始兴县', '仁化县', '翁源县',
- '新丰县', '乳源县'
- ];
- // 排除的非污染物字段(根据接口结构调整)
- const EXCLUDE_FIELDS = [
- '污染源序号', '公司', '类型', '经度', '纬度' // 这些字段不是污染物,需排除
- ];
- // ========== 2. 响应式数据 & 变量 ==========
- const chartRef = ref(null); // ECharts容器引用
- const loading = ref(true); // 加载状态
- const error = ref(''); // 错误信息
- let myChart = null; // ECharts实例
- // ========== 3. 数据处理核心逻辑 ==========
- const processData = (apiData) => {
- // 3.1 空数据保护
- if (!apiData || apiData.length === 0) {
- console.error('接口返回空数据');
- return { regions: [], series: [], totalSamples: 0 };
- }
- // 3.2 提取**污染物字段**(自动过滤非数值字段)
- const pollutantFields = Object.keys(apiData[0])
- .filter(key =>
- !EXCLUDE_FIELDS.includes(key) && // 排除非污染物字段
- !isNaN(parseFloat(apiData[0][key])) // 只保留数值字段
- );
- // 3.3 无有效污染物字段的保护
- if (pollutantFields.length === 0) {
- console.error('未识别到有效污染物字段,请检查 EXCLUDE_FIELDS 配置');
- return { regions: [], series: [], totalSamples: 0 };
- }
- // 3.4 按区县分组统计(总和 + 计数)
- const regionGroups = {}; // 结构:{ 区县: { 污染物: { sum, count } } }
- const globalStats={};//全局统计对象
- let totalSamples = 0; // 总样本数
- pollutantFields.forEach(field=>{
- globalStats[field] = {sum:0,count:0};
- })
- apiData.forEach(item => {
- const region = item['所属区县'] || '未知区县'; // 提取区县
- totalSamples++;
- // 初始化区县分组(每个污染物字段都要初始化)
- if (!regionGroups[region]) {
- regionGroups[region] = {};
- pollutantFields.forEach(field => {
- regionGroups[region][field] = { sum: 0, count: 0 };
- });
- }
- // 累加每个污染物的浓度
- pollutantFields.forEach(field => {
- const value = parseFloat(item[field]);
- if (!isNaN(value)) { // 过滤无效值(如空字符串、null)
- regionGroups[region][field].sum += value;
- regionGroups[region][field].count++;
- globalStats[field].sum+=value;
- globalStats[field].count++;
- }
- });
- });
- // 3.5 筛选**有效区县**(仅韶关市区县白名单内的区域)
- const validRegions = SG_REGIONS.filter(region =>
- regionGroups[region] !== undefined
- );
- validRegions.push('全部样本平均');//全局平均的分类
- // 3.6 构建ECharts所需的series数据
- const series = pollutantFields.map((field, index) => ({
- name: field, // 污染物名称作为系列名
- type: 'bar',
- data: validRegions.map(region => {
- if(region === '全部样本平均'){
- const stats = globalStats[field];
- return stats.count>0
- ?(stats.sum/stats.count).toFixed(2)
- :0;
- }
- const group = regionGroups[region][field];
- return group.count > 0
- ? (group.sum / group.count).toFixed(2) // 计算平均值,保留2位小数
- : 0; // 无数据时显示0
- }),
- itemStyle: { color: (params)=>{
- return params.name ==='全部样本平均'?'#ff0000' : '#1890ff'
- } },
- label: {
- show: true,
- position: 'top',
- fontSize: 18,
- color: '#333'
- }
- }));
- return { regions: validRegions, series, totalSamples };
- };
- // ========== 4. ECharts 初始化 & 更新 ==========
- const initChart = ({ regions, series, totalSamples }) => {
- if (!chartRef.value) return;
- // 销毁旧实例(避免重复初始化)
- if (myChart && !myChart.isDisposed()) {
- myChart.dispose();
- }
- myChart = echarts.init(chartRef.value);
- const option = {
- title: {
- left: 'center',
- subtext: `(基于 ${totalSamples} 个有效样本计算)`,
- subtextStyle: { fontSize: 14, color: '#666' }
- },
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- const region = params[0].name;
- let content = `${region}:<br>`;
- params.forEach(p => {
- content += `${p.seriesName}: ${p.value} t/a<br>`;
- });
- return content;
- },
- textStyle: { fontSize: 16 }
- },
- xAxis: {
- type: 'category',
- data: regions,
- axisLabel: {
- formatter: val => val.replace('韶关市', ''), // 简化显示(可选)
- fontSize: 16,
- interval:0
- }
- },
- yAxis: {
- type: 'value',
- name: '浓度 (t/a)',
- nameTextStyle: { fontSize: 16 },
- axisLabel: { fontSize: 16 }
- },
- legend: {
- data: series.map(s => s.name),
- bottom: 10,
- textStyle: { fontSize: 14 },
- icon:'none'
- },
- series:series,
- grid: { left: '5%', right: '5%', bottom: '15%', containLabel: true }
- };
- myChart.setOption(option);
- };
- // ========== 5. 生命周期 & 响应式 ==========
- onMounted(async () => {
- loading.value = true;
- try {
- // 1. 请求接口数据
- const res = await axios.get(POLLUTANT_API, { timeout: 10000 });
-
- // 2. 处理数据
- const processedData = processData(res.data);
-
- // 3. 渲染图表
- initChart(processedData);
- } catch (err) {
- // 细化错误提示(网络/服务器/超时等)
- if (err.response) {
- error.value = `数据加载失败(${err.response.status})`;
- } else if (err.request) {
- error.value = '网络错误,无法连接服务器';
- } else {
- error.value = `加载失败:${err.message}`;
- }
- console.error('接口请求错误:', err);
- } finally {
- loading.value = false;
- }
- });
- // 窗口resize时自动调整图表
- const resizeHandler = () => {
- if (myChart) myChart.resize();
- };
- onMounted(() => window.addEventListener('resize', resizeHandler));
- onUnmounted(() => window.removeEventListener('resize', resizeHandler));
- </script>
- <style scoped>
- .region-average-chart {
- width: 100%;
- max-width: 1200px;
- margin: 20px auto;
- position: relative;
- }
- .chart-box {
- width: 100%;
- height: 600px;
- min-height: 400px;
- background: white;
- border-radius: 8px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- }
- .status {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 12px 20px;
- background: rgba(255,255,255,0.9);
- border-radius: 6px;
- font-size: 16px;
- }
- .error {
- color: #ff4d4f;
- font-weight: bold;
- }
- </style>
|