WholeProcessPlatform/frontend/src/modules/rightSearchDrawer/monitoringTable/components/Plant.vue

445 lines
11 KiB
Vue
Raw Normal View History

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