| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483 |
- <template>
- <div class="atmosphere-summary">
- <!-- 图表容器 -->
- <div ref="chartRef" class="chart-box"></div>
-
- <!-- 状态提示 -->
- <div v-if="loading" class="status">
- <div class="spinner"></div>
- <p>数据加载中...</p>
- </div>
-
- <div v-else-if="error" class="status error">
- <i class="fa fa-exclamation-circle"></i> {{ error }}
- <div v-if="errorDetails" class="error-details">
- <p>错误详情:</p>
- <pre>{{ errorDetails }}</pre>
- </div>
- </div>
- </div>
- </template>
- <script setup>
- import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue'
- import * as echarts from 'echarts'
- import { api8000 } from '@/utils/request'; // 导入 api8000 实例
- // 接收计算方式(重量/体积)
- const props = defineProps({
- calculationMethod: {
- type: String,
- required: true,
- default: 'weight'
- }
- })
- // --------------------------
- // 配置区
- // --------------------------
- const API_URL = `/api/vector/export/all?table_name=Atmo_sample_data`; // 使用相对路径
- // 重量指标字段
- const WEIGHT_FIELDS = [
- 'Cr_particulate',
- 'As_particulate',
- 'Cd_particulate',
- 'Hg_particulate',
- 'Pb_particulate'
- ];
- // 体积字段名
- const VOLUME_FIELD = 'volume';
- // 自定义颜色(用户指定)
- const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700', '#52c41a88', '#722ed199'];
- // --------------------------
- // 响应式数据
- const chartRef = ref(null);
- const loading = ref(true);
- const error = ref('');
- const errorDetails = ref(''); // 存储错误详情
- const showLog = ref(false); // 默认隐藏日志
- const fullLog = ref('');
- let myChart = null;
- // 记录日志
- const log = (message) => {
- const time = new Date().toLocaleTimeString();
- fullLog.value += `[${time}] ${message}\n`;
- // console.log(`[日志] ${message}`);
- };
- const fixInvalidJsonValues = (rawData) => {
- if (typeof rawData !== 'string') {
- rawData = JSON.stringify(rawData);
- }
-
- const fixedData = rawData
- .replace(/:\s*NaN\b/g, ': null')
- .replace(/:\s*"N"\b/g, ': null')
- .replace(/:\s*"NaN"\b/g, ': null')
- .replace(/:\s*Infinity\b/g, ': null')
- .replace(/:\s*-\s*Infinity\b/g, ': null')
- .replace(/:\s+/g, ': ')
- .replace(/,\s+/g, ', ');
-
- return fixedData;
- };
- function weightToVolume(weight, volume) {
- if (weight === undefined || weight === null) {
- log(`重量值无效: ${weight}`);
- return 0;
- }
- if (volume === undefined || volume === null || volume === 0 || isNaN(volume)) {
- log(`体积值无效: ${volume}(已自动替换为1)`);
- volume = 1;
- }
-
- const weightNum = parseFloat(weight);
- const volumeNum = parseFloat(volume);
-
- if (isNaN(weightNum)) {
- log(`重量无法转换为数字: ${weight}`);
- return 0;
- }
-
- const ug = weightNum * 1000;
- return parseFloat((ug / volumeNum).toFixed(2));
- }
- function getRegion(location) {
- if (!location || typeof location !== 'string') {
- return '未知区县';
- }
- const regions = [
- '浈江区', '武江区', '曲江区', '乐昌市',
- '南雄市', '始兴县', '仁化县', '翁源县',
- '新丰县', '乳源瑶族自治县'
- ];
- // 精确匹配
- for (const region of regions) {
- if (location.includes(region)) {
- return region;
- }
- }
- // 模糊匹配
- const aliasMap = {
- '浈江': '浈江区', '武江': '武江区', '曲江': '曲江区',
- '乐昌': '乐昌市', '南雄': '南雄市', '始兴': '始兴县',
- '仁化': '仁化县', '翁源': '翁源县', '新丰': '新丰县',
- '乳源': '乳源瑶族自治县'
- };
-
- for (const [alias, region] of Object.entries(aliasMap)) {
- if (location.includes(alias)) {
- return region;
- }
- }
- return '未知区县';
- }
- async function processData() {
- try {
- log('开始数据处理');
-
- // 使用 api8000 实例发起请求
- const response = await api8000.get(API_URL, {
- responseType: 'text', // 确保获取原始文本
- timeout: 15000
- });
-
- const fixedJson = fixInvalidJsonValues(response.data);
- const geoData = JSON.parse(fixedJson);
- if (!geoData || !geoData.features || !Array.isArray(geoData.features)) {
- throw new Error('数据结构错误,缺少features数组');
- }
- log(`解析到${geoData.features.length}条数据`);
- // 处理数据
- const processedItems = geoData.features.map((feature, index) => {
- const props = feature.properties || {};
- return {
- id: index,
- location: props.sampling_location || '',
- region: getRegion(props.sampling_location || ''),
- volume: props[VOLUME_FIELD],
- weights: WEIGHT_FIELDS.reduce((acc, field) => {
- acc[field] = props[field];
- return acc;
- }, {})
- };
- });
- // 统计数据
- const regionStats = {};
- const totalStats = {};
- WEIGHT_FIELDS.forEach(field => {
- totalStats[field] = { sum: 0, count: 0 };
- totalStats[`${field}_volume`] = { sum: 0, count: 0 };
- });
- processedItems.forEach(item => {
- const { region, volume, weights } = item;
- if (!regionStats[region]) {
- regionStats[region] = {};
- WEIGHT_FIELDS.forEach(field => {
- regionStats[region][field] = { sum: 0, count: 0 };
- regionStats[region][`${field}_volume`] = { sum: 0, count: 0 };
- });
- }
- WEIGHT_FIELDS.forEach(field => {
- const weightValue = weights[field];
- const weightNum = parseFloat(weightValue);
- if (!isNaN(weightNum)) {
- regionStats[region][field].sum += weightNum;
- regionStats[region][field].count += 1;
- totalStats[field].sum += weightNum;
- totalStats[field].count += 1;
- }
- const volumeValue = weightToVolume(weightValue, volume);
- regionStats[region][`${field}_volume`].sum += volumeValue;
- regionStats[region][`${field}_volume`].count += 1;
- totalStats[`${field}_volume`].sum += volumeValue;
- totalStats[`${field}_volume`].count += 1;
- });
- });
- // 准备图表数据
- const chartRegions = Object.keys(regionStats).filter(r => r !== '未知区县');
- if (chartRegions.length === 0) chartRegions.push('未知区县');
- chartRegions.push('全市平均');
- // 生成系列数据
- const series = WEIGHT_FIELDS.map((field, index) => {
- const metricType = props.calculationMethod === 'volume'
- ? `${field}_volume`
- : field;
-
- const data = chartRegions.map(region => {
- if (region === '全市平均') {
- return totalStats[metricType].count > 0
- ? (totalStats[metricType].sum / totalStats[metricType].count).toFixed(2)
- : '0.00';
- }
-
- const stats = regionStats[region][metricType];
- return stats.count > 0
- ? (stats.sum / stats.count).toFixed(2)
- : '0.00';
- });
- return {
- name: field.replace('_particulate', ''), // 图例名称(不带后缀)
- type: 'bar',
- data,
- itemStyle: {
- color: COLORS[index % COLORS.length] // 使用用户指定的颜色
- },
- label: {
- show: true,
- position: 'top',
- fontSize: 12
- }
- };
- });
- return { regions: chartRegions, series };
- } catch (err) {
- error.value = '数据处理失败';
- errorDetails.value = err.message;
- return null;
- }
- }
- // /**
- // * 初始化图表(带单位显示)
- // */
- async function initChart() {
- loading.value = true;
- error.value = '';
- errorDetails.value = '';
-
- try {
- await nextTick();
- if (!chartRef.value) {
- throw new Error('图表容器未挂载');
- }
- const chartData = await processData();
- if (!chartData) return;
- // 确定单位(核心修改:添加单位逻辑)
- const { unit, titleText } = props.calculationMethod === 'weight'
- ? {
- unit: 'mg/kg',
- titleText: '各区域重金属含量平均值' ,
- }
- : {
- unit: 'ug/m³', // 体积单位为ug/m³,可根据实际需求修改
- titleText: '各区域重金属含量平均值' ,
- };
- // 销毁旧图表
- if (myChart) myChart.dispose();
- myChart = echarts.init(chartRef.value);
- // 设置图表配置(带单位显示)
- myChart.setOption({
- title: {
- text: titleText,
- subtext: `单位: ${unit}`, // 标题显示单位
- left: 'center',
- textStyle:{fontSize:20},
- subtextStyle:{fontSize:18}
- },
- tooltip: { //提示框
- trigger: 'axis',
- formatter: function(params) {
- // Tooltip显示单位
- let res = `${params[0].name}<br/>`;
- params.forEach(item => {
- res += `${item.marker} ${item.seriesName}: ${item.value} ${unit}<br/>`;
- });
- return res;
- },
- textStyle:{fontSize:15}
- },
- xAxis: {
- type: 'category',
- data: chartData.regions,
- axisLabel: { fontSize:16 }
- },
- yAxis: {
- type: 'value',
- axisLabel: {
- formatter: `{value} ${unit}` // Y轴显示单位
- ,fontSize:15
- }
- },
- series: chartData.series.map(series => ({ // 遍历每个系列,添加 label 配置
- ...series, // 保留原有配置
- label: {
- show: true, // 显示数值标签
- position: 'top', // 标签位置(顶部)
- fontSize: 15, // 这里才是柱状图数值的字体大小!
- color: '#333' // 可选:设置文字颜色
- }
- })),
- legend: { //图例
- data: chartData.series.map(s => s.name),
- top:'10%',
- right:'5%',
- textStyle:{fontSize:18}
- },
- grid: {
- left: '1%', right: '2%', bottom: '2%', top: '20%',
- containLabel: true,
- axisLabel:{fontSize:18}
- }
- }, true);
- // 监听窗口大小
- const handleResize = () => myChart.resize();
- window.addEventListener('resize', handleResize);
- onUnmounted(() => window.removeEventListener('resize', handleResize));
- } catch (err) {
- error.value = '图表加载失败';
- errorDetails.value = err.message;
- } finally {
- loading.value = false;
- }
- }
- // 监听计算方式变化
- watch(() => props.calculationMethod, initChart);
- // 组件挂载后初始化
- onMounted(() => {
- initChart();
- });
- // 组件卸载时清理
- onUnmounted(() => {
- if (myChart) myChart.dispose();
- });
- </script>
- <style scoped>
- .atmosphere-summary {
- width: 100%;
- max-width: 1400px;
- margin: 0 auto;
- box-sizing: border-box;
- position: relative;
- }
- .chart-box {
- width: 100%;
- height: 500px;
- min-height: 400px;
- background: #fff;
- border: 1px solid #e9ecef;
- border-radius: 8px;
- }
- .status {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 20px;
- background: rgba(255, 255, 255, 0.9);
- border-radius: 6px;
- text-align: center;
- box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
- max-width: 80%;
- }
- .error {
- color: #dc3545;
- border: 1px solid #f5c6cb;
- background: #f8d7da;
- }
- .error-details {
- margin-top: 15px;
- text-align: left;
- font-size: 14px;
- }
- .error-details pre {
- background: rgba(255, 255, 255, 0.8);
- padding: 10px;
- border-radius: 4px;
- overflow: auto;
- max-height: 200px;
- white-space: pre-wrap;
- }
- .spinner {
- width: 40px;
- height: 40px;
- margin: 0 auto 15px;
- border: 4px solid #e9ecef;
- border-top: 4px solid #007bff;
- border-radius: 50%;
- animation: spin 1s linear infinite;
- }
- @keyframes spin {
- 0% { transform: rotate(0deg); }
- 100% { transform: rotate(360deg); }
- }
- .debug-panel {
- margin-top: 20px;
- padding: 15px;
- background: #f8f9fa;
- border-radius: 6px;
- font-size: 14px;
- }
- .log-toggle {
- background: #007bff;
- color: white;
- border: none;
- padding: 6px 12px;
- border-radius: 4px;
- cursor: pointer;
- margin-bottom: 10px;
- }
- .log-content {
- max-height: 300px;
- overflow: auto;
- background: #fff;
- padding: 10px;
- border-radius: 4px;
- border: 1px solid #e9ecef;
- }
- .log-content pre {
- margin: 0;
- white-space: pre-wrap;
- font-family: monospace;
- font-size: 12px;
- }
- </style>
|