2026-07-01 18:23:41 +08:00
|
|
|
<!-- 水生生态调查 -->
|
2026-05-12 08:47:27 +08:00
|
|
|
<template>
|
2026-07-01 18:23:41 +08:00
|
|
|
<SidePanelItem
|
|
|
|
|
title="水生生态调查"
|
|
|
|
|
:moreSelect="moreSelect"
|
|
|
|
|
:select="select"
|
|
|
|
|
@update-values="handlePanelChange"
|
|
|
|
|
>
|
|
|
|
|
<a-spin :spinning="dataLoading" tip="加载中..." class="spin-wrapper">
|
|
|
|
|
<div
|
|
|
|
|
v-show="chartData.length > 0 && select.options.length > 0"
|
|
|
|
|
ref="chartRef"
|
|
|
|
|
class="chart-container"
|
|
|
|
|
></div>
|
|
|
|
|
<a-empty
|
|
|
|
|
v-show="
|
|
|
|
|
!dataLoading &&
|
|
|
|
|
(chartData.length === 0 || select.options.length === 0)
|
|
|
|
|
"
|
|
|
|
|
:description="select.options.length === 0 ? '暂无数据' : '暂无数据'"
|
|
|
|
|
class="empty-placeholder"
|
|
|
|
|
/>
|
|
|
|
|
</a-spin>
|
|
|
|
|
</SidePanelItem>
|
2026-05-12 08:47:27 +08:00
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2026-07-01 18:23:41 +08:00
|
|
|
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
|
2026-05-12 08:47:27 +08:00
|
|
|
import * as echarts from 'echarts';
|
|
|
|
|
import type { EChartsOption } from 'echarts';
|
|
|
|
|
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
2026-07-01 18:23:41 +08:00
|
|
|
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
|
|
|
|
import { wbsbGetKendoList, getDftYear, getFishListStat } from '@/api/stdc';
|
|
|
|
|
import moment from 'moment';
|
2026-05-12 08:47:27 +08:00
|
|
|
|
|
|
|
|
defineOptions({
|
2026-07-01 18:23:41 +08:00
|
|
|
name: 'shuiShengShengTaiDiaoCha'
|
2026-05-12 08:47:27 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
// ==================== Store ====================
|
|
|
|
|
const JidiSelectEventStore = useJidiSelectEventStore();
|
2026-05-12 08:47:27 +08:00
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
// ==================== 基础状态 ====================
|
|
|
|
|
const baseid = ref('');
|
|
|
|
|
const isInitializing = ref(false);
|
|
|
|
|
const dataLoading = ref(false);
|
|
|
|
|
|
|
|
|
|
// ==================== 选择器状态 ====================
|
|
|
|
|
const moreSelect = ref({
|
|
|
|
|
show: true,
|
|
|
|
|
value: undefined as string | undefined,
|
|
|
|
|
options: [] as any[]
|
2026-05-12 08:47:27 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
const select = ref({
|
|
|
|
|
show: true,
|
|
|
|
|
value: undefined as string | undefined,
|
|
|
|
|
options: [] as any[],
|
|
|
|
|
picker: undefined,
|
|
|
|
|
format: undefined,
|
|
|
|
|
width: '120px'
|
2026-05-12 08:47:27 +08:00
|
|
|
});
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
const yearOptions = ref<Record<string, any[]>>({});
|
|
|
|
|
|
|
|
|
|
// ==================== 图表状态 ====================
|
|
|
|
|
const chartRef = ref<HTMLDivElement>();
|
|
|
|
|
let chartInstance: any = null;
|
|
|
|
|
let resizeObserver: ResizeObserver | null = null;
|
|
|
|
|
const chartData = ref<any[]>([]);
|
|
|
|
|
|
|
|
|
|
// ==================== 图表相关 ====================
|
|
|
|
|
|
2026-05-12 08:47:27 +08:00
|
|
|
const initChart = () => {
|
2026-07-01 18:23:41 +08:00
|
|
|
if (!chartRef.value) return;
|
2026-05-12 08:47:27 +08:00
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
if (chartInstance) {
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
chartInstance = echarts.init(chartRef.value);
|
|
|
|
|
updateChart();
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateChart = () => {
|
|
|
|
|
// 如果图表实例不存在但 DOM 已就绪,先初始化
|
|
|
|
|
if (!chartInstance && chartRef.value) { chartInstance = echarts.init(chartRef.value);
|
|
|
|
|
}
|
|
|
|
|
if (!chartInstance) return;
|
|
|
|
|
|
|
|
|
|
const totalValue = chartData.value.reduce(
|
|
|
|
|
(sum: number, item: any) => sum + (item.fcnt || 0),
|
|
|
|
|
0
|
|
|
|
|
);
|
|
|
|
|
|
|
|
|
|
const option: EChartsOption = {
|
|
|
|
|
color: [
|
|
|
|
|
'#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de',
|
|
|
|
|
'#3ba272', '#fc8452', '#9a60b4', '#ea7ccc', '#8dd3a7'
|
|
|
|
|
],
|
|
|
|
|
tooltip: {
|
|
|
|
|
trigger: 'item',
|
|
|
|
|
backgroundColor: 'rgba(0, 0, 0, 0.5)',
|
|
|
|
|
borderColor: 'transparent',
|
|
|
|
|
textStyle: { color: '#ffffff' },
|
|
|
|
|
formatter: '{b}: {c} ({d}%)'
|
|
|
|
|
},
|
|
|
|
|
legend: {
|
|
|
|
|
type: 'scroll',
|
|
|
|
|
orient: 'horizontal',
|
|
|
|
|
bottom: '5%',
|
|
|
|
|
data: chartData.value.map((item: any) => item.name),
|
|
|
|
|
icon: 'rect',
|
|
|
|
|
itemWidth: 14,
|
|
|
|
|
itemHeight: 14,
|
|
|
|
|
textStyle: { fontSize: 12, height: 8 },
|
|
|
|
|
pageTextStyle: { color: '#333' },
|
|
|
|
|
pageIconColor: '#2F6B98',
|
|
|
|
|
pageIconInactiveColor: '#aaa'
|
|
|
|
|
},
|
|
|
|
|
title: [
|
|
|
|
|
{
|
|
|
|
|
text: `(类)`,
|
|
|
|
|
top: '42%',
|
|
|
|
|
textAlign: 'center',
|
|
|
|
|
left: '50%',
|
|
|
|
|
textStyle: { color: '#333', fontSize: 13, fontWeight: 500 }
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
text: totalValue.toString(),
|
|
|
|
|
left: 'center',
|
|
|
|
|
top: '32%',
|
|
|
|
|
textStyle: { color: '#2F6B98', fontSize: 24 }
|
|
|
|
|
}
|
|
|
|
|
],
|
|
|
|
|
series: [
|
|
|
|
|
{
|
|
|
|
|
name: '生态类别',
|
|
|
|
|
type: 'pie',
|
|
|
|
|
radius: ['35%', '65%'],
|
|
|
|
|
center: ['50%', '40%'],
|
|
|
|
|
avoidLabelOverlap: false,
|
|
|
|
|
label: {
|
|
|
|
|
show: true,
|
|
|
|
|
position: 'outside',
|
|
|
|
|
formatter: '{b}: {c}'
|
2026-05-12 08:47:27 +08:00
|
|
|
},
|
2026-07-01 18:23:41 +08:00
|
|
|
labelLine: { show: true, length: 8, length2: 10 },
|
|
|
|
|
emphasis: {
|
|
|
|
|
label: { show: true, fontSize: 14, fontWeight: 'bold' }
|
2026-05-12 08:47:27 +08:00
|
|
|
},
|
2026-07-01 18:23:41 +08:00
|
|
|
data: chartData.value.map((item: any) => ({
|
|
|
|
|
name: item.name,
|
|
|
|
|
value: item.fcnt || 0
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
chartInstance.setOption(option);
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
chartInstance.resize();
|
|
|
|
|
});
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const handleResize = () => {
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
chartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 数据获取 ====================
|
|
|
|
|
|
|
|
|
|
const getYearData = async () => {
|
|
|
|
|
try {
|
|
|
|
|
const filter: any = {};
|
|
|
|
|
if (baseid.value && baseid.value !== 'all') {
|
|
|
|
|
filter.baseId = baseid.value;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const params = {
|
|
|
|
|
filter,
|
|
|
|
|
group: [{ dir: 'des', field: 'yr' }]
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const res = await getDftYear(params);
|
|
|
|
|
// debugger
|
|
|
|
|
const data = res?.data?.data?.[0] || {};
|
|
|
|
|
|
|
|
|
|
if (Object.keys(data).length > 0) {
|
|
|
|
|
const objYear: Record<string, any[]> = {};
|
|
|
|
|
const allYears: any[] = [];
|
|
|
|
|
|
|
|
|
|
Object.keys(data).forEach(key => {
|
|
|
|
|
objYear[key] = data[key].map((item: any) => ({
|
|
|
|
|
label: String(item),
|
|
|
|
|
value: String(item)
|
|
|
|
|
}));
|
|
|
|
|
allYears.push(...data[key]);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
yearOptions.value = objYear;
|
|
|
|
|
|
|
|
|
|
const uniqueYears = [...new Set(allYears)].sort(
|
|
|
|
|
(a: any, b: any) => Number(b) - Number(a)
|
|
|
|
|
);
|
|
|
|
|
const latestYear = uniqueYears[0];
|
|
|
|
|
|
|
|
|
|
const firstWbsCode = Object.keys(data)[0];
|
|
|
|
|
select.value.options = objYear[firstWbsCode] || [];
|
|
|
|
|
select.value.value = latestYear
|
|
|
|
|
? String(latestYear)
|
|
|
|
|
: moment().format('YYYY');
|
|
|
|
|
} else {
|
|
|
|
|
yearOptions.value = {};
|
|
|
|
|
select.value.options = [];
|
|
|
|
|
select.value.value = moment().format('YYYY');
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取年份数据失败:', error);
|
|
|
|
|
select.value.options = [];
|
|
|
|
|
select.value.value = moment().format('YYYY');
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const updateYearOptionsByBase = async (wbsCode: string) => {
|
|
|
|
|
const options = yearOptions.value[wbsCode];
|
|
|
|
|
if (options) {
|
|
|
|
|
select.value.options = options;
|
|
|
|
|
await nextTick();
|
|
|
|
|
select.value.value = options[0]?.value || '';
|
|
|
|
|
} else {
|
|
|
|
|
select.value.options = [];
|
|
|
|
|
await nextTick();
|
|
|
|
|
select.value.value = '';
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const getMoreSelectData = async () => {
|
|
|
|
|
if (!baseid.value) return;
|
|
|
|
|
|
|
|
|
|
isInitializing.value = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const params = {
|
|
|
|
|
filter: {
|
|
|
|
|
logic: 'and',
|
|
|
|
|
filters: [
|
|
|
|
|
{
|
|
|
|
|
field: 'wbsType',
|
|
|
|
|
operator: 'eq',
|
|
|
|
|
value: 'PSB_RVCD'
|
|
|
|
|
},
|
|
|
|
|
baseid.value !== 'all'
|
|
|
|
|
? {
|
|
|
|
|
field: 'objid',
|
|
|
|
|
operator: 'eq',
|
|
|
|
|
value: baseid.value
|
|
|
|
|
}
|
|
|
|
|
: null
|
|
|
|
|
].filter(Boolean)
|
|
|
|
|
},
|
|
|
|
|
group: [
|
|
|
|
|
{ dir: 'asc', field: 'orderIndex' },
|
|
|
|
|
{ dir: 'asc', field: 'wbsCode' },
|
|
|
|
|
{ dir: 'asc', field: 'wbsName' },
|
|
|
|
|
{ dir: 'asc', field: 'objid' }
|
|
|
|
|
],
|
|
|
|
|
groupResultFlat: true
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const res = await wbsbGetKendoList(params);
|
|
|
|
|
const data = res?.data?.data || [];
|
|
|
|
|
// debugger
|
|
|
|
|
|
|
|
|
|
if (data.length > 0) {
|
|
|
|
|
const treeData = buildTreeData(data);
|
|
|
|
|
const filteredData = filterSelectOptions(treeData);
|
|
|
|
|
|
|
|
|
|
moreSelect.value.options = filteredData;
|
|
|
|
|
moreSelect.value.value = treeData?.[0]?.children?.[0]?.value;
|
|
|
|
|
|
|
|
|
|
await getYearData();
|
|
|
|
|
if (moreSelect.value.value) {
|
|
|
|
|
await updateYearOptionsByBase(moreSelect.value.value);
|
|
|
|
|
}
|
|
|
|
|
await getChartData();
|
|
|
|
|
} else {
|
|
|
|
|
moreSelect.value.options = [];
|
|
|
|
|
moreSelect.value.value = undefined;
|
|
|
|
|
await getYearData();
|
|
|
|
|
}
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取树形选择器数据失败:', error);
|
|
|
|
|
moreSelect.value.options = [];
|
|
|
|
|
moreSelect.value.value = undefined;
|
|
|
|
|
} finally {
|
|
|
|
|
isInitializing.value = false;
|
|
|
|
|
}
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
/**
|
|
|
|
|
* 获取图表数据
|
|
|
|
|
* 根据当前选中的流域(hbrvcd)调用 getFishListStat API
|
|
|
|
|
*/
|
|
|
|
|
const getChartData = async () => {
|
|
|
|
|
if (!moreSelect.value.value) return;
|
|
|
|
|
|
|
|
|
|
dataLoading.value = true;
|
|
|
|
|
|
|
|
|
|
try {
|
|
|
|
|
const params = {
|
|
|
|
|
filter: {
|
|
|
|
|
logic: 'and',
|
|
|
|
|
filters: [
|
|
|
|
|
{
|
|
|
|
|
field: 'hbrvcd',
|
|
|
|
|
operator: 'eq',
|
|
|
|
|
dataType: 'string',
|
|
|
|
|
value: moreSelect.value.value
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
]
|
2026-07-01 18:23:41 +08:00
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
};
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
const res = await getFishListStat(params);
|
|
|
|
|
// debugger
|
|
|
|
|
chartData.value = res?.data?.data || [];
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
updateChart();
|
|
|
|
|
});
|
|
|
|
|
} catch (error) {
|
|
|
|
|
console.error('获取图表数据失败:', error);
|
|
|
|
|
chartData.value = [];
|
|
|
|
|
nextTick(() => {
|
|
|
|
|
updateChart();
|
|
|
|
|
});
|
|
|
|
|
} finally {
|
|
|
|
|
dataLoading.value = false;
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
};
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
// ==================== 树形工具函数 ====================
|
|
|
|
|
|
|
|
|
|
const buildTreeData = (data: any[]) => {
|
|
|
|
|
if (!data || data.length === 0) return [];
|
|
|
|
|
|
|
|
|
|
const jiDiListMap: Record<string, string> = {};
|
|
|
|
|
JidiSelectEventStore.jidiData.forEach((item: any) => {
|
|
|
|
|
jiDiListMap[item.wbsCode] = item.wbsName;
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const dataMapNameMap: Record<string, any[]> = {};
|
|
|
|
|
data.forEach(item => {
|
|
|
|
|
const objId = item.objid || item.objId;
|
|
|
|
|
if (!dataMapNameMap[objId]) {
|
|
|
|
|
dataMapNameMap[objId] = [];
|
2026-05-12 08:47:27 +08:00
|
|
|
}
|
2026-07-01 18:23:41 +08:00
|
|
|
const exists = dataMapNameMap[objId].some(
|
|
|
|
|
existing => existing.wbsCode === item.wbsCode
|
|
|
|
|
);
|
|
|
|
|
if (!exists) {
|
|
|
|
|
dataMapNameMap[objId].push(item);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const treeData = Object.keys(dataMapNameMap).map(objId => ({
|
|
|
|
|
title: jiDiListMap[objId] || '---',
|
|
|
|
|
value: objId,
|
|
|
|
|
selectable: false,
|
|
|
|
|
children: dataMapNameMap[objId].map((item: any) => ({
|
|
|
|
|
title: item.wbsName,
|
|
|
|
|
value: item.wbsCode
|
|
|
|
|
}))
|
|
|
|
|
}));
|
|
|
|
|
|
|
|
|
|
treeData.sort((a, b) => {
|
|
|
|
|
const numA = Number(a.value);
|
|
|
|
|
const numB = Number(b.value);
|
|
|
|
|
const aIsNum = !isNaN(numA);
|
|
|
|
|
const bIsNum = !isNaN(numB);
|
|
|
|
|
if (aIsNum && bIsNum) return numA - numB;
|
|
|
|
|
if (aIsNum) return -1;
|
|
|
|
|
if (bIsNum) return 1;
|
|
|
|
|
return a.value.localeCompare(b.value);
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return treeData;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const filterSelectOptions = (options: any[]) => {
|
|
|
|
|
if (options?.length === 1 && options?.[0]?.children?.length === 1) {
|
|
|
|
|
return options[0].children;
|
|
|
|
|
}
|
|
|
|
|
return options;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
// ==================== 事件处理 ====================
|
|
|
|
|
|
|
|
|
|
const handlePanelChange = async (data: any) => {
|
|
|
|
|
if (isInitializing.value) return;
|
|
|
|
|
|
|
|
|
|
if (
|
|
|
|
|
data.moreSelect !== undefined &&
|
|
|
|
|
data.moreSelect !== moreSelect.value.value
|
|
|
|
|
) {
|
|
|
|
|
moreSelect.value.value = data.moreSelect;
|
|
|
|
|
await updateYearOptionsByBase(data.moreSelect);
|
|
|
|
|
return;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (data.select !== undefined && data.select !== select.value.value) {
|
|
|
|
|
select.value.value = data.select;
|
|
|
|
|
await getChartData();
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
};
|
|
|
|
|
|
2026-07-01 18:23:41 +08:00
|
|
|
// ==================== Watch 监听 ====================
|
|
|
|
|
|
|
|
|
|
watch(
|
|
|
|
|
() => JidiSelectEventStore.selectedItem,
|
|
|
|
|
async newVal => {
|
|
|
|
|
if (newVal && newVal.wbsCode) {
|
|
|
|
|
baseid.value = newVal.wbsCode;
|
|
|
|
|
await getMoreSelectData();
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
{ deep: true, immediate: true }
|
|
|
|
|
);
|
|
|
|
|
|
2026-05-12 08:47:27 +08:00
|
|
|
// ==================== 生命周期钩子 ====================
|
|
|
|
|
|
|
|
|
|
onMounted(() => {
|
2026-07-01 18:23:41 +08:00
|
|
|
nextTick(() => {
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
initChart();
|
|
|
|
|
window.addEventListener('resize', handleResize);
|
|
|
|
|
|
|
|
|
|
if (chartRef.value) {
|
|
|
|
|
resizeObserver = new ResizeObserver(() => {
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
chartInstance.resize();
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
resizeObserver.observe(chartRef.value);
|
|
|
|
|
}
|
|
|
|
|
}, 50);
|
|
|
|
|
});
|
2026-05-12 08:47:27 +08:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
onUnmounted(() => {
|
2026-07-01 18:23:41 +08:00
|
|
|
window.removeEventListener('resize', handleResize);
|
|
|
|
|
|
|
|
|
|
if (resizeObserver) {
|
|
|
|
|
resizeObserver.disconnect();
|
|
|
|
|
resizeObserver = null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if (chartInstance) {
|
|
|
|
|
chartInstance.dispose();
|
|
|
|
|
chartInstance = null;
|
|
|
|
|
}
|
2026-05-12 08:47:27 +08:00
|
|
|
});
|
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
2026-07-01 18:23:41 +08:00
|
|
|
.spin-wrapper {
|
|
|
|
|
width: 100%;
|
|
|
|
|
display: block;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.chart-container {
|
|
|
|
|
width: 100%;
|
|
|
|
|
height: 270px;
|
|
|
|
|
min-height: 270px;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
.empty-placeholder {
|
|
|
|
|
width: 100%;
|
|
|
|
|
min-height: 270px;
|
|
|
|
|
display: flex;
|
|
|
|
|
background: #fff;
|
|
|
|
|
flex-direction: column;
|
|
|
|
|
justify-content: center;
|
|
|
|
|
align-items: center;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-empty-description) {
|
|
|
|
|
text-indent: 0em !important;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
:deep(.ant-spin-container) {
|
|
|
|
|
width: 100%;
|
2026-05-12 08:47:27 +08:00
|
|
|
}
|
2026-07-01 18:23:41 +08:00
|
|
|
</style>
|