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