445 lines
11 KiB
Vue
445 lines
11 KiB
Vue
<template>
|
||
<SidePanelItem title="监测数据" :shrink="false">
|
||
<template #title-right-content>
|
||
<a-date-picker
|
||
class="w-[110px]"
|
||
v-model:value="year"
|
||
picker="year"
|
||
format="YYYY"
|
||
value-format="YYYY"
|
||
:allowClear="false"
|
||
size="small"
|
||
@change="handleSearch"
|
||
/>
|
||
</template>
|
||
<a-spin :spinning="chartLoading" tip="加载中...">
|
||
<div class="monitor-chart">
|
||
<div ref="chartRef" class="chart-container"></div>
|
||
<a-empty
|
||
v-if="isChartEmpty"
|
||
description="暂无数据"
|
||
class="chart-empty"
|
||
/>
|
||
</div>
|
||
</a-spin>
|
||
<div class="monitor-table">
|
||
<BasicTable
|
||
ref="tableRef"
|
||
:scrollY="300"
|
||
:scrollX="tableScrollX"
|
||
:columns="tableColumns"
|
||
:list-url="getTableList"
|
||
:transform-data="transformTableData"
|
||
row-key="_rowKey"
|
||
:paginationConfig="{
|
||
showSizeChanger: false,
|
||
showQuickJumper: false
|
||
}"
|
||
/>
|
||
</div>
|
||
</SidePanelItem>
|
||
</template>
|
||
|
||
<script lang="ts" setup>
|
||
import {
|
||
ref,
|
||
watch,
|
||
computed,
|
||
onMounted,
|
||
onBeforeUnmount,
|
||
nextTick
|
||
} from 'vue';
|
||
import * as echarts from 'echarts';
|
||
import { useUiStore } from '@/store/modules/ui';
|
||
import {
|
||
getNormalDataMonitoring2Year,
|
||
getNormalDataMonitoring2List
|
||
} from '@/api/mapModal';
|
||
import { useModelStore } from '@/store/modules/model';
|
||
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
||
import BasicTable from '@/components/BasicTable/index.vue';
|
||
|
||
const uiStore = useUiStore();
|
||
const modelStore = useModelStore();
|
||
|
||
const DEFAULT_PAGE_SIZE = 20;
|
||
|
||
const tableRef = ref();
|
||
const year = ref<string>('');
|
||
|
||
// ==================== 图表(柱状图:保护对象 x 轴,两两堆叠) ====================
|
||
const chartLoading = ref(false);
|
||
const chartData = ref<any[]>([]);
|
||
const chartRef = ref<HTMLElement>();
|
||
let chartInstance: echarts.ECharts | null = null;
|
||
let resizeObserver: ResizeObserver | null = null;
|
||
|
||
const toNum = (val: any) => {
|
||
if (val === undefined || val === null || val === '' || isNaN(Number(val)))
|
||
return 0;
|
||
return Number(val);
|
||
};
|
||
|
||
// 相同保护对象聚合求和
|
||
const chartAggData = computed(() => {
|
||
const map = new Map<
|
||
string,
|
||
{ tecnt: number; plantask: number; surnum: number; transplant: number }
|
||
>();
|
||
(Array.isArray(chartData.value) ? chartData.value : []).forEach(item => {
|
||
const name = item?.tetp ?? '未知';
|
||
if (!map.has(name)) {
|
||
map.set(name, { tecnt: 0, plantask: 0, surnum: 0, transplant: 0 });
|
||
}
|
||
const agg = map.get(name)!;
|
||
agg.tecnt += toNum(item.tecnt);
|
||
agg.plantask += toNum(item.plantask);
|
||
agg.surnum += toNum(item.surnum);
|
||
agg.transplant += toNum(item.transplant);
|
||
});
|
||
return [...map.entries()].map(([name, agg]) => ({ name, ...agg }));
|
||
});
|
||
|
||
const isChartEmpty = computed(() => chartAggData.value.length === 0);
|
||
|
||
const initChart = () => {
|
||
if (!chartRef.value) return;
|
||
if (chartInstance) chartInstance.dispose();
|
||
chartInstance = echarts.init(chartRef.value);
|
||
updateChart();
|
||
};
|
||
|
||
const updateChart = () => {
|
||
if (!chartInstance) return;
|
||
if (isChartEmpty.value) {
|
||
chartInstance.clear();
|
||
return;
|
||
}
|
||
|
||
const data = chartAggData.value;
|
||
const xAxisData = data.map(item => item.name);
|
||
|
||
const option = {
|
||
tooltip: {
|
||
trigger: 'axis',
|
||
axisPointer: { type: 'shadow' },
|
||
backgroundColor: 'rgba(50,50,50,0.9)',
|
||
textStyle: { color: '#fff', fontSize: 12 }
|
||
},
|
||
legend: {
|
||
top: 0,
|
||
data: ['实际种植', '种植要求', '存活数量', '移栽规模'],
|
||
textStyle: { fontSize: 12 }
|
||
},
|
||
grid: { left: 50, right: 20, top: 50, bottom: 30 },
|
||
xAxis: {
|
||
type: 'category',
|
||
data: xAxisData,
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
axisTick: { show: false },
|
||
axisLabel: { fontSize: 12, interval: 0 }
|
||
},
|
||
yAxis: {
|
||
type: 'value',
|
||
axisLine: { lineStyle: { color: '#000000' } },
|
||
splitLine: {
|
||
show: true,
|
||
lineStyle: { color: '#bfbfbf', type: 'solid' }
|
||
}
|
||
},
|
||
series: [
|
||
{
|
||
name: '实际种植',
|
||
type: 'bar',
|
||
stack: 'plant',
|
||
barMaxWidth: 40,
|
||
data: data.map(item => item.tecnt),
|
||
itemStyle: { color: '#5B8FF9' }
|
||
},
|
||
{
|
||
name: '种植要求',
|
||
type: 'bar',
|
||
stack: 'plant',
|
||
barMaxWidth: 40,
|
||
data: data.map(item => item.plantask),
|
||
itemStyle: { color: '#63DAAB' }
|
||
},
|
||
{
|
||
name: '存活数量',
|
||
type: 'bar',
|
||
stack: 'transplant',
|
||
barMaxWidth: 40,
|
||
data: data.map(item => item.surnum),
|
||
itemStyle: { color: '#657798' }
|
||
},
|
||
{
|
||
name: '移栽规模',
|
||
type: 'bar',
|
||
stack: 'transplant',
|
||
barMaxWidth: 40,
|
||
data: data.map(item => item.transplant),
|
||
itemStyle: { color: '#F6C022' }
|
||
}
|
||
],
|
||
toolbox: {
|
||
show: true,
|
||
feature: {
|
||
saveAsImage: { title: '保存为图片', type: 'png', pixelRatio: 2 }
|
||
},
|
||
right: 0,
|
||
top: 10
|
||
}
|
||
};
|
||
|
||
chartInstance.setOption(option, true);
|
||
};
|
||
|
||
// 图表数据变化时动态刷新
|
||
watch(chartAggData, () => {
|
||
updateChart();
|
||
});
|
||
|
||
const safeResize = (
|
||
chart: echarts.ECharts | null,
|
||
el: HTMLElement | undefined
|
||
) => {
|
||
if (!chart) return;
|
||
// 容器不可见(display:none / 宽度为 0)时跳过,避免收起/切换时做昂贵的同步重排
|
||
if (!el || el.clientWidth === 0) return;
|
||
chart.resize();
|
||
};
|
||
|
||
onMounted(() => {
|
||
nextTick(() => {
|
||
initChart();
|
||
setTimeout(() => {
|
||
safeResize(chartInstance, chartRef.value);
|
||
}, 200);
|
||
if (!resizeObserver && chartRef.value) {
|
||
resizeObserver = new ResizeObserver(() =>
|
||
safeResize(chartInstance, chartRef.value)
|
||
);
|
||
resizeObserver.observe(chartRef.value);
|
||
}
|
||
});
|
||
window.addEventListener('resize', () =>
|
||
safeResize(chartInstance, chartRef.value)
|
||
);
|
||
});
|
||
|
||
onBeforeUnmount(() => {
|
||
if (chartInstance) {
|
||
chartInstance.dispose();
|
||
chartInstance = null;
|
||
}
|
||
window.removeEventListener('resize', () =>
|
||
safeResize(chartInstance, chartRef.value)
|
||
);
|
||
if (resizeObserver) {
|
||
resizeObserver.disconnect();
|
||
resizeObserver = null;
|
||
}
|
||
});
|
||
|
||
// ==================== 表格 ====================
|
||
const formatValue = (val: any) =>
|
||
val !== undefined && val !== null && String(val).trim() !== '' ? val : '-';
|
||
|
||
const tableColumns = ref([
|
||
{
|
||
title: '保护对象',
|
||
dataIndex: 'tetp',
|
||
width: 120
|
||
},
|
||
{
|
||
title: '实际种植/种植要求',
|
||
dataIndex: 'surnum',
|
||
width: 140,
|
||
customRender: ({ record }: any) =>
|
||
`${formatValue(record?.tecnt)}/${formatValue(record?.plantask)}`
|
||
},
|
||
{
|
||
title: '存活数量/移栽规模',
|
||
dataIndex: 'treeNum',
|
||
width: 140,
|
||
customRender: ({ record }: any) =>
|
||
`${formatValue(record?.surnum)}/${formatValue(record?.transplant)}`
|
||
}
|
||
]);
|
||
const tableScrollX = ref(320);
|
||
|
||
const transformTableData = (res: any) => {
|
||
const records = res?.data?.records || res?.data?.data || res?.data || [];
|
||
const total = res?.data?.data?.total || res?.data?.total || res?.total || 0;
|
||
|
||
return {
|
||
records: records.map((item: any, index: number) => ({
|
||
...item,
|
||
_rowKey:
|
||
item?.id ||
|
||
item?.fid ||
|
||
`${item?.stcd || 'stcd'}-${item?.tm || year.value || 'tm'}-${index}`
|
||
})),
|
||
total
|
||
};
|
||
};
|
||
|
||
// ==================== 年份与数据查询 ====================
|
||
const getDefaultYear = (yearList: any[]) => {
|
||
const firstItem = yearList?.[0];
|
||
if (typeof firstItem === 'string' || typeof firstItem === 'number') {
|
||
return String(firstItem);
|
||
}
|
||
return firstItem?.yr || firstItem?.tm || firstItem?.year || null;
|
||
};
|
||
|
||
const buildFilter = () => ({
|
||
logic: 'and',
|
||
filters: [
|
||
{
|
||
field: 'stcd',
|
||
operator: 'in',
|
||
dataType: 'string',
|
||
value: [modelStore.selectedAnchorPoint?.stcd]
|
||
},
|
||
{
|
||
field: 'tm',
|
||
operator: 'eq',
|
||
dataType: 'string',
|
||
value: year.value
|
||
}
|
||
]
|
||
});
|
||
|
||
const getTableList = (params: any) => {
|
||
const take = Number(params?.take || DEFAULT_PAGE_SIZE);
|
||
const currentPage = Number(params?.skip || 1);
|
||
const skip = Math.max(currentPage - 1, 0) * take;
|
||
|
||
return getNormalDataMonitoring2List({
|
||
...params,
|
||
take,
|
||
skip,
|
||
filter: params?.filter || buildFilter()
|
||
});
|
||
};
|
||
|
||
const refreshTable = () => {
|
||
if (!modelStore.selectedAnchorPoint?.stcd || !year.value) return;
|
||
tableRef.value?.getList(buildFilter());
|
||
};
|
||
|
||
// 图表使用全量数据(不分页)按保护对象聚合
|
||
const fetchChartData = async () => {
|
||
if (!modelStore.selectedAnchorPoint?.stcd || !year.value) return;
|
||
chartLoading.value = true;
|
||
try {
|
||
const res = await getNormalDataMonitoring2List({
|
||
filter: buildFilter(),
|
||
take: 10000,
|
||
skip: 0
|
||
});
|
||
chartData.value = res?.data?.records || res?.data?.data || res?.data || [];
|
||
} catch (error) {
|
||
console.error('获取植物园图表数据失败:', error);
|
||
chartData.value = [];
|
||
} finally {
|
||
chartLoading.value = false;
|
||
}
|
||
};
|
||
|
||
const refreshAll = () => {
|
||
refreshTable();
|
||
fetchChartData();
|
||
};
|
||
|
||
// 获取年份列表并赋值默认年份(年份列表第一个),然后自动查询
|
||
const getYearList = async () => {
|
||
const stcd = modelStore.selectedAnchorPoint?.stcd;
|
||
if (!stcd) return;
|
||
try {
|
||
const res: any = await getNormalDataMonitoring2Year({
|
||
filter: {
|
||
logic: 'and',
|
||
filters: [
|
||
{
|
||
field: 'stcd',
|
||
operator: 'eq',
|
||
dataType: 'string',
|
||
value: stcd
|
||
}
|
||
]
|
||
}
|
||
});
|
||
const yearData = res?.data?.data || res?.data?.records || res?.data || [];
|
||
const defaultYear =
|
||
getDefaultYear(yearData) || String(new Date().getFullYear());
|
||
|
||
year.value = String(defaultYear);
|
||
refreshAll();
|
||
} catch (error) {
|
||
console.error('获取年份列表失败:', error);
|
||
if (tableRef.value) {
|
||
tableRef.value.loading = false;
|
||
}
|
||
}
|
||
};
|
||
|
||
const handleSearch = () => {
|
||
if (!year.value) return;
|
||
chartData.value = []; // 切换年份时先清空图表,避免加载期间显示旧数据
|
||
refreshAll();
|
||
};
|
||
|
||
// ==================== 联动 ====================
|
||
watch(
|
||
() => uiStore.searchDrawerOpen,
|
||
newVal => {
|
||
if (!newVal) {
|
||
year.value = '';
|
||
chartData.value = [];
|
||
}
|
||
}
|
||
);
|
||
|
||
// 选中点位变化时重新加载;初始已有点位时立即加载
|
||
watch(
|
||
() => modelStore.selectedAnchorPoint?.stcd,
|
||
(newStcd, oldStcd) => {
|
||
if (!newStcd) return;
|
||
if (newStcd !== oldStcd) {
|
||
year.value = '';
|
||
chartData.value = [];
|
||
getYearList();
|
||
}
|
||
},
|
||
{ immediate: true }
|
||
);
|
||
</script>
|
||
|
||
<style lang="scss" scoped>
|
||
.monitor-chart {
|
||
position: relative;
|
||
width: 100%;
|
||
height: 300px;
|
||
|
||
.chart-container {
|
||
width: 100%;
|
||
height: 100%;
|
||
}
|
||
}
|
||
|
||
.chart-empty {
|
||
position: absolute;
|
||
top: 50%;
|
||
left: 50%;
|
||
transform: translate(-50%, -50%);
|
||
}
|
||
|
||
.monitor-table {
|
||
margin-top: 10px;
|
||
width: 410px;
|
||
overflow-x: auto;
|
||
}
|
||
</style>
|