| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- <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 { api8000 } from '@/utils/request'; // 导入 api8000 实例
- // ========== 接口配置(使用 api8000 实例) ==========
- const TABLE_NAME = 'water_sampling_data';
- // ========== 配置项(调整字段适配新接口) ==========
- // 排除的非重金属字段(适配新接口字段名)
- const EXCLUDE_FIELDS = [
- 'id', 'sample_code', 'assayer_id', 'assay_time',
- 'assay_instrument_model', 'sample_number', 'ph_value',
- 'latitude', 'longitude', 'sampling_location', 'sampling_time' // 新增采样相关非检测字段
- ];
- const COLORS = ['#ff4d4f99', '#1890ff', '#ffd700', '#52c41a88', '#722ed199' ];
- // 韶关市下属行政区划白名单(保持不变)
- const SG_REGIONS = [
- '浈江区', '武江区', '曲江区', '乐昌市',
- '南雄市', '始兴县', '仁化县', '翁源县',
- '新丰县', '乳源瑶族自治县'
- ];
- // ========== 响应式数据 ==========
- const chartRef = ref(null);
- const loading = ref(true);
- const error = ref('');
- let myChart = null;
- // ========== 地区提取函数(保持不变) ==========
- const extractRegion = (location) => {
- if (!location || typeof location !== 'string') return null;
- // 1. 精确匹配官方区县名称
- const officialMatch = SG_REGIONS.find(region =>
- location.includes(region)
- );
- if (officialMatch) return officialMatch;
- // 2. 处理嵌套格式(如"韶关市-浈江区")
- const nestedMatch = location.match(/(韶关市)([^市]+?[区市县])/);
- if (nestedMatch && nestedMatch[2]) {
- const region = nestedMatch[2].replace("韶关市", "").trim();
- const validRegion = SG_REGIONS.find(r => r.includes(region));
- if (validRegion) return validRegion;
- }
- // 3. 特殊格式处理(如"韶关市浈江区")
- const shortMatch = location.match(/韶关市([区市县][^市]{2,5})/);
- if (shortMatch && shortMatch[1]) return shortMatch[1];
- // 4. 修正常见拼写错误
- if (location.includes('乐昌')) return '乐昌市';
- if (location.includes('乳源')) return '乳源瑶族自治县';
- console.warn(`⚠️ 未识别地区: ${location}`);
- return '未知区县';
- };
- // ========== 数据处理流程(适配新接口单数据源) ==========
- const processData = (allData) => {
- // 1. 构建采样点ID到区县的映射(sample_number对应新接口的水样ID)
- const regionMap = new Map();
- allData.forEach(item => {
- const region = extractRegion(item.sampling_location || '');
- if (region && region !== '未知区县' && item.sample_number) {
- regionMap.set(item.sample_number, region);
- }
- });
- // 2. 关联重金属数据与区县(单条数据已包含所有信息)
- const mergedData = allData.map(item => ({
- ...item,
- // 通过sample_number关联区县
- region: regionMap.get(item.sample_number) || '未知区县'
- }));
- // 3. 识别重金属字段(新接口字段如cr_concentration、as_concentration等)
- const metals = Object.keys(mergedData[0] || {})
- .filter(key =>
- !EXCLUDE_FIELDS.includes(key) && // 排除非重金属字段
- !isNaN(parseFloat(mergedData[0][key])) && // 确保是数值
- key.includes('concentration') // 新接口重金属字段含concentration
- );
- // 4. 按区县分组统计
- const regionGroups = {};
- const cityWideAverages = {}; // 全市平均值
- const uniqueSampleIds = new Set();
-
- // 初始化统计计数器
- metals.forEach(metal => {
- cityWideAverages[metal] = { sum: 0, count: 0 };
- });
-
- mergedData.forEach(item => {
- const region = item.region;
- if (item.sample_number) {
- uniqueSampleIds.add(item.sample_number);
- }
- // 初始化区县分组
- if (!regionGroups[region]) {
- regionGroups[region] = {};
- metals.forEach(metal => {
- regionGroups[region][metal] = { sum: 0, count: 0 };
- });
- }
- // 统计各重金属含量
- metals.forEach(metal => {
- const val = parseFloat(item[metal]);
- if (!isNaN(val)) {
- // 区县统计
- regionGroups[region][metal].sum += val;
- regionGroups[region][metal].count++;
-
- // 全市统计
- cityWideAverages[metal].sum += val;
- cityWideAverages[metal].count++;
- }
- });
- });
-
- const totalSamples = uniqueSampleIds.size;
- // 5. 按官方顺序排序区县
- const regions = SG_REGIONS.filter(region => regionGroups[region]);
-
- // 6. 添加"全市平均"作为最后一个类别
- regions.push("全市平均");
- // 7. 构建ECharts数据(处理重金属字段名显示)
- const series = metals.map((metal, idx) => {
- // 格式化重金属名称(如cr_concentration → Cr)
- const prefix = metal.split('_')[0]; // 先获取前缀
- const metalName = prefix
- ? prefix[0].toUpperCase() + prefix.slice(1) // 首字母大写 + 剩余字符
- : ''; // 处理空字符串情况
-
- // 计算全市平均值
- const cityWideAvg = cityWideAverages[metal].count
- ? (cityWideAverages[metal].sum / cityWideAverages[metal].count).toFixed(2)
- : 0;
-
- return {
- name: metalName, // 显示简化名称(如Cr、As)
- type: 'bar',
- data: regions.map(region => {
- if (region === "全市平均") {
- return cityWideAvg;
- }
- const group = regionGroups[region][metal];
- return group.count ? (group.sum / group.count).toFixed(2) : 0;
- }),
- itemStyle: {
- color: COLORS[idx % COLORS.length],
- },
- label: {
- show: true,
- position: 'top',
- fontSize: 15,
- color: '#333',
- }
- };
- });
- return { regions, series, totalSamples };
- };
- // ========== ECharts 初始化(保持不变) ==========
- const initChart = ({ regions, series, totalSamples }) => {
- if (!chartRef.value) return;
- if (myChart) myChart.dispose();
- myChart = echarts.init(chartRef.value);
- const option = {
- title: {
- text: '各地区重金属含量平均值',
- left: 'center',
- subtext: `数据来源: ${totalSamples}个有效检测样本`,
- subtextStyle: {
- fontSize: 15
- }
- },
- tooltip: {
- trigger: 'axis',
- formatter: params => {
- const regionName = params[0].name;
- const isCityWide = regionName === "全市平均";
-
- let content = `${isCityWide ? "全市平均值" : regionName}:`;
- if (isCityWide) {
- content += `<br><span style="color: #666;">(基于${totalSamples}个样本计算)</span>`;
- }
-
- return content + params.map(p => `<br>${p.seriesName}: ${p.value} ug/L`).join('');
- },
- textStyle: {
- fontSize: 15
- }
- },
- xAxis: {
- type: 'category',
- data: regions,
- axisLabel: {
- rotate: 45,
- formatter: val => val.replace('韶关市', ''),
- fontSize: 15
- }
- },
- yAxis: {
- type: 'value',
- name: '浓度(ug/L)',
- nameTextStyle: {
- fontSize: 15,
- },
- axisLabel: {
- fontSize: 15,
- }
- },
- dataZoom: [{
- type: 'inside',
- start: 0,
- end: 100
- }],
- series,
- legend: { //图例
- data: series.map(s => s.name),
- top:'10%',
- right:'5%',
- textStyle: {
- fontSize: 15
- }
- },
- grid: { //整个图的位置
- left: '3%',
- right: '3%',
- bottom: '5%',
- containLabel: true
- },
- };
- myChart.setOption(option);
- };
- // ========== 生命周期钩子(使用 api8000 实例) ==========
- onMounted(async () => {
- try {
- loading.value = true;
- error.value = '';
-
- // 使用 api8000 实例获取数据
- const response = await api8000.get(`/api/vector/export/all?table_name=${TABLE_NAME}`);
-
- // 处理可能的字符串响应
- let data = response.data;
- if (typeof data === 'string') {
- try {
- // 替换 NaN 为 null
- const cleanedData = data.replace(/\bNaN\b/g, 'null');
- data = JSON.parse(cleanedData);
- } catch (parseErr) {
- throw new Error('接口返回的是字符串,但 JSON 解析失败');
- }
- }
-
- // 处理对象中的 NaN 值
- if (typeof data === 'object' && data !== null) {
- const replaceNaN = (obj) => {
- for (const key in obj) {
- if (typeof obj[key] === 'object' && obj[key] !== null) {
- replaceNaN(obj[key]);
- } else if (typeof obj[key] === 'number' && isNaN(obj[key])) {
- obj[key] = null;
- } else if (obj[key] === 'NaN') {
- obj[key] = null;
- }
- }
- };
- replaceNaN(data);
- }
-
- // 接口返回格式判断(GeoJSON或直接数组)
- const allData = data.features
- ? data.features.map(f => f.properties)
- : data;
-
- // 处理数据并初始化图表
- initChart(processData(allData));
- } catch (err) {
- error.value = '数据加载失败: ' + (err.message || '未知错误');
- console.error('接口错误:', err);
- } finally {
- loading.value = false;
- }
- });
- // 响应式布局(保持不变)
- const resizeHandler = () => myChart && myChart.resize();
- onMounted(() => window.addEventListener('resize', resizeHandler));
- onUnmounted(() => {
- window.removeEventListener('resize', resizeHandler);
- if (myChart) myChart.dispose();
- });
- </script>
- <style scoped>
- .region-average-chart {
- width: 100%;
- height: 500px;
- max-width: 1200px;
- margin: 0 auto;
- position: relative;
- }
- .chart-box {
- width: 100%;
- height: 500px;
- min-height: 400px;
- background-color: white;
- border-radius: 8px;
- box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
- }
- .status {
- position: absolute;
- top: 50%;
- left: 50%;
- transform: translate(-50%, -50%);
- padding: 15px;
- background: rgba(255,255,255,0.8);
- border-radius: 4px;
- font-size: 16px;
- z-index: 10;
- }
- .error {
- color: #ff4d4f;
- font-weight: bold;
- }
- </style>
|