WholeProcessPlatform/frontend/src/modules/rightSearchDrawer/monitoringTable/components/Plant.vue
2026-08-11 16:24:41 +08:00

445 lines
11 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<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>