123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392 |
- <template>
- <div class="heavy-metal-chart">
- <!-- 错误提示(带原始响应预览) -->
- <div v-if="error" class="status error">
- <i class="fa fa-exclamation-circle"></i> {{ error }}
- <div class="raw-response" v-if="rawResponse">
- <button @click="showRaw = !showRaw" class="raw-btn">
- {{ showRaw ? '收起原始响应' : '查看原始响应(前1000字符)' }}
- </button>
- <pre v-if="showRaw" class="raw-pre">{{ truncatedRawResponse }}</pre>
- </div>
- </div>
- <!-- 加载状态 -->
- <div v-if="loading" class="loading-state">
- <div class="spinner"></div>
- <p>数据加载中...</p>
- </div>
- <!-- 图表容器(使用v-show确保DOM始终存在) -->
- <div
- v-show="!loading && !error"
- ref="chartRef"
- class="chart-box"
- :style="{
- height: '500px',
- border: '2px solid #1890ff',
- position: 'relative'
- }"
- >
- <!-- 容器状态可视化提示(调试用) -->
- <div class="container-status" v-if="debugMode">
- 容器状态: {{ containerStatus }}
- <br>
- 高度: {{ containerHeight }}px
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, computed, onUnmounted, nextTick } from 'vue';
- import * as echarts from 'echarts';
- import axios from 'axios';
- // ========== 核心配置 ==========
- const API_URL = 'http://localhost:8000/api/vector/export/all?table_name=atmo_company';
- const SG_REGIONS = [
- '浈江区', '武江区', '曲江区', '乐昌市',
- '南雄市', '始兴县', '仁化县', '翁源县',
- '新丰县', '乳源县'
- ];
- const EXCLUDE_FIELDS = [
- 'id', 'company_name', 'company_type', 'longitude', 'latitude'
- ];
- const debugMode = true; // 调试模式:显示容器状态
- // ========== 响应式数据 ==========
- const chartRef = ref(null);
- const error = ref('');
- const loading = ref(true);
- const rawResponse = ref('');
- const showRaw = ref(false);
- const containerStatus = ref('未初始化');
- const containerHeight = ref(0);
- let myChart = null;
- // 截断原始响应
- const truncatedRawResponse = computed(() => {
- return rawResponse.value.length > 1000
- ? rawResponse.value.slice(0, 1000) + '...'
- : rawResponse.value;
- });
- // ========== 容器状态检查(实时更新) ==========
- const checkContainer = () => {
- if (!chartRef.value) {
- containerStatus.value = '未找到容器元素';
- containerHeight.value = 0;
- return false;
- }
-
- containerStatus.value = '已找到容器元素';
- containerHeight.value = chartRef.value.offsetHeight;
- return true;
- };
- // ========== 数据处理逻辑 ==========
- const processData = (features) => {
- console.log('🔍 开始处理数据,features数量:', features.length);
-
- // 提取有效properties
- const apiData = features
- .map(feature => feature.properties || {})
- .filter(props => Object.keys(props).length > 0);
- console.log('🔍 有效properties数量:', apiData.length);
- if (apiData.length === 0) {
- throw new Error('无有效数据(properties为空)');
- }
- // 识别污染物字段
- const pollutantFields = Object.keys(apiData[0])
- .filter(key =>
- !EXCLUDE_FIELDS.includes(key) &&
- !isNaN(parseFloat(apiData[0][key]))
- );
- console.log('🔍 识别的污染物字段:', pollutantFields);
- if (pollutantFields.length === 0) {
- throw new Error('未识别到有效污染物字段,请检查EXCLUDE_FIELDS');
- }
- // 按区县统计
- const regionStats = {};
- const globalStats = {};
- let totalSamples = 0;
- pollutantFields.forEach(field => {
- globalStats[field] = { sum: 0, count: 0 };
- });
- apiData.forEach(item => {
- const county = item.county || '未知区县';
- totalSamples++;
- if (!regionStats[county]) {
- regionStats[county] = {};
- pollutantFields.forEach(field => {
- regionStats[county][field] = { sum: 0, count: 0 };
- });
- }
- pollutantFields.forEach(field => {
- const value = parseFloat(item[field]);
- if (!isNaN(value)) {
- regionStats[county][field].sum += value;
- regionStats[county][field].count++;
- globalStats[field].sum += value;
- globalStats[field].count++;
- }
- });
- });
- console.log('🔍 区县统计结果:', regionStats);
- // 构建有效区县
- const validRegions = SG_REGIONS.filter(region => regionStats[region])
- .concat('全市平均');
- console.log('🔍 有效区县列表:', validRegions);
- // 构建图表数据
- const series = pollutantFields.map((field, index) => ({
- name: field,
- type: 'bar',
- data: validRegions.map(region => {
- if (region === '全市平均') {
- return globalStats[field].count
- ? (globalStats[field].sum / globalStats[field].count)
- : 0;
- }
- return regionStats[region][field].count
- ? (regionStats[region][field].sum / regionStats[region][field].count)
- : 0;
- }),
- itemStyle: {
- // 当x轴类目是“全市平均”时,柱子显示红色
- color: (params) => {
- // params.dataIndex 对应 regions 数组的索引,最后一个是“全市平均”
- const isTotal = validRegions[params.dataIndex] === '全市平均';
- return isTotal ? '#ff0000' : '#1890ff'; // 红色可用 #ff0000 或其他红色值
- }
- },
- label: { show: true, position: 'top', fontSize: 15 }
- }));
- return { regions: validRegions, series, totalSamples };
- };
- // ========== ECharts初始化 ==========
- const initChart = (data) => {
- // 检查容器状态
- if (!checkContainer()) {
- error.value = '图表容器未准备好,请刷新页面重试';
- return;
- }
- // 检查容器高度
- if (containerHeight.value < 100) {
- error.value = `容器高度异常(${containerHeight.value}px),请检查样式`;
- return;
- }
- // 销毁旧实例
- if (myChart && !myChart.isDisposed()) {
- myChart.dispose();
- }
- // 空数据检查
- if (data.series.length === 0 || data.regions.length === 0) {
- error.value = '无有效数据用于绘制图表';
- return;
- }
- // 初始化图表
- try {
- myChart = echarts.init(chartRef.value);
- myChart.setOption({
- title: {
- text: '韶关市各区县企业污染物平均值',
- left: 'center',
- subtext: `基于 ${data.totalSamples} 个有效样本`,
- subtextStyle: { fontSize: 15 }
- },
- tooltip: {
- trigger: 'axis',
- formatter: (params) => {
- let content = `${params[0].name}:<br>`;
- params.forEach(p => {
- content += `${p.seriesName}: ${p.value} t/a<br>`;
- });
- return content;
- },
- axisLabel:{fontSize:15}
- },
- xAxis: {
- type: 'category',
- data: data.regions,
- axisLabel: { rotate: 30, fontSize: 15 }
- },
- yAxis: {
- type: 'value',
- name: '浓度 (t/a)',
- nameTextStyle: { fontSize: 15 },
- axisLabel:{fontSize:15},
- },
-
- series: data.series,
- grid: { left: '5%', right: '5%', bottom: '5%', containLabel: true }
- });
- console.log('✅ 图表初始化成功');
- } catch (err) {
- error.value = `图表初始化失败:${err.message}`;
- console.error('图表初始化错误:', err);
- }
- };
- // ========== 数据请求逻辑 ==========
- const fetchData = async () => {
- try {
- loading.value = true;
- error.value = '';
- console.log('🚀 开始请求数据:', API_URL);
- // 发起请求(延长超时)
- const response = await axios.get(API_URL, {
- timeout: 20000, // 20秒超时
- responseType: 'text'
- });
- rawResponse.value = response.data;
- console.log('✅ 数据请求成功,状态码:', response.status);
- // 修复NaN并解析
- const fixedJson = response.data.replace(/:\s*NaN/g, ': null');
- const geoJSONData = JSON.parse(fixedJson);
- // 校验数据结构
- if (!geoJSONData.features || !Array.isArray(geoJSONData.features)) {
- throw new Error('响应数据缺少features数组');
- }
- // 处理数据
- const chartData = processData(geoJSONData.features);
- console.log('✅ 数据处理完成,准备渲染图表');
- // 等待DOM更新(双重保险)
- await nextTick();
- console.log('🔄 DOM更新完成,检查容器:', chartRef.value);
- // 强制延迟确保容器准备好(极端情况处理)
- setTimeout(() => {
- initChart(chartData);
- }, 300);
- } catch (err) {
- error.value = `数据加载失败:${err.message}`;
- console.error('❌ 数据请求错误:', err);
- } finally {
- loading.value = false;
- }
- };
- // ========== 生命周期 ==========
- onMounted(() => {
- // 初始检查容器
- checkContainer();
- // 开始加载数据
- fetchData();
- });
- // ========== 响应式布局 ==========
- const handleResize = () => {
- if (myChart) {
- myChart.resize();
- console.log('🔄 图表已重绘');
- }
- };
- onMounted(() => window.addEventListener('resize', handleResize));
- onUnmounted(() => window.removeEventListener('resize', handleResize));
- </script>
- <style scoped>
- .heavy-metal-chart {
- width: 90%;
- max-width: 1200px;
- margin: 20px auto;
- padding: 20px;
- background: #fff;
- border-radius: 12px;
- box-shadow: 0 2px 8px rgba(0,0,0,0.1);
- position: relative;
- }
- /* 错误提示 */
- .status.error {
- color: #dc2626;
- background: #fee2e2;
- padding: 12px 16px;
- border-radius: 6px;
- margin-bottom: 16px;
- }
- .raw-btn {
- margin: 8px 0;
- padding: 4px 8px;
- background: #ff4d4f;
- color: white;
- border: none;
- border-radius: 4px;
- cursor: pointer;
- font-size: 18px;
- }
- .raw-pre {
- white-space: pre-wrap;
- word-break: break-all;
- background: #f9fafb;
- padding: 8px;
- border-radius: 4px;
- font-size: 18px;
- max-height: 200px;
- overflow: auto;
- }
- /* 加载状态 */
- .loading-state {
- text-align: center;
- padding: 60px 0;
- color: #6b7280;
- }
- .spinner {
- width: 40px;
- height: 40px;
- margin: 0 auto 16px;
- border: 4px solid #e5e7eb;
- border-top: 4px solid #3b82f6;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- /* 图表容器 */
- .chart-box {
- width: 100%;
- min-height: 500px !important; /* 强制最小高度 */
- border-radius: 8px;
- overflow: hidden;
- }
- /* 容器状态提示 */
- .container-status {
- position: absolute;
- top: 10px;
- left: 10px;
- background: rgba(255,255,255,0.8);
- padding: 4px 8px;
- border-radius: 4px;
- font-size: 12px;
- color: #1890ff;
- z-index: 10;
- }
- </style>
|