WholeProcessPlatform/frontend/src/modules/alongFishMod/index.vue

632 lines
16 KiB
Vue
Raw Normal View History

2026-06-26 17:52:57 +08:00
<!-- 沿程鱼类变化情况 -->
2026-05-12 08:47:27 +08:00
<template>
2026-06-26 17:52:57 +08:00
<SidePanelItem
title="沿程鱼类变化情况"
:moreSelect="moreSelect"
:select="select"
@update-values="handlePanelChange"
>
<a-spin :spinning="dataLoading" tip="加载中...">
<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>
<!-- 沿程鱼类变化详情弹框 -->
<a-modal
v-model:open="modalVisible"
title="沿程鱼类变化"
width="1536px"
:footer="null"
@cancel="handleModalClose"
>
<Yanchengyulei
v-if="modalVisible && JidiSelectEventStore.jidiData.length > 0"
2026-06-26 17:52:57 +08:00
:tm="modalYear"
:dataDimensionVal="baseid"
:hbrvcd="modalHbrvcd"
:jdList="JidiSelectEventStore.jidiData"
:stateName="modalStateName"
:sectionName="modalSectionName"
:stcd="modalStcd"
:ly="true"
/>
</a-modal>
2026-05-12 08:47:27 +08:00
</template>
<script lang="ts" setup>
2026-06-26 17:52:57 +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-06-26 17:52:57 +08:00
import Yanchengyulei from './TwoLayers/Yanchengyulei.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { wbsbGetKendoList, getDftYear, fishChanges2 } from '@/api/stdc';
import moment from 'moment';
2026-05-12 08:47:27 +08:00
defineOptions({
2026-06-26 17:52:57 +08:00
name: 'alongFishMod'
2026-05-12 08:47:27 +08:00
});
2026-06-26 17:52:57 +08:00
// ==================== Store ====================
const JidiSelectEventStore = useJidiSelectEventStore();
2026-05-12 08:47:27 +08:00
2026-06-26 17:52:57 +08:00
// ==================== 基础状态 ====================
/** 当前选中的基地ID来自 JidiSelectEventStore.selectedItem */
const baseid = ref('');
/** 图表数据加载状态 */
const dataLoading = ref(false);
/** 初始化标志位:初始化期间不响应控件变化触发数据请求 */
const isInitializing = ref(false);
// ==================== 选择器状态 ====================
/** 流域树形选择器配置 */
const moreSelect = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[]
2026-05-12 08:47:27 +08:00
});
2026-06-26 17:52:57 +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-06-26 17:52:57 +08:00
/**
* 按流域分组的年份数据
* 结构{ "wbsCode1": [{label:'2023', value:'2023'}], "wbsCode2": [...] }
* 用于流域切换时快速更新年份下拉选项
*/
const yearOptions = ref<Record<string, any[]>>({});
// ==================== 图表状态 ====================
const chartRef = ref<HTMLDivElement>();
let chartInstance: any = null;
/** 图表原始数据,由 API 返回 */
const chartData = ref<any[]>([]);
// ==================== 弹框状态 ====================
const modalVisible = ref(false);
const modalYear = ref('');
const modalHbrvcd = ref('');
const modalStateName = ref('');
const modalSectionName = ref('');
const modalStcd = ref('');
// ==================== 图表相关 ====================
/**
* 初始化 ECharts 实例
* onMounted 中调用绑定点击事件用于打开详情弹框
*/
2026-05-12 08:47:27 +08:00
const initChart = () => {
2026-06-26 17:52:57 +08:00
if (!chartRef.value) return;
if (chartInstance) {
chartInstance.dispose();
}
2026-05-12 08:47:27 +08:00
2026-06-26 17:52:57 +08:00
chartInstance = echarts.init(chartRef.value);
// 点击柱子弹出详情
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
openDetail(params);
2026-05-12 08:47:27 +08:00
}
2026-06-26 17:52:57 +08:00
});
2026-05-12 08:47:27 +08:00
2026-06-26 17:52:57 +08:00
updateChart();
};
/**
* 根据 chartData 更新图表配置
* API 返回的 detail 对象key: 1=外来鱼类, 2=本土受威胁, 3=本土无危
* 转换为 ECharts 堆叠柱状图数据
*/
const updateChart = () => {
if (!chartInstance) return;
// 按 sort 字段排序
const sortedData = [...chartData.value].sort(
(a, b) => (a.sort || 0) - (b.sort || 0)
);
const waimaiData: number[] = [];
const bentuThreatData: number[] = [];
const bentuSafeData: number[] = [];
const xAxisData: string[] = [];
sortedData.forEach((item: any) => {
xAxisData.push(item.stnm || '');
waimaiData.push(item.detail?.['1'] || 0);
bentuThreatData.push(item.detail?.['2'] || 0);
bentuSafeData.push(item.detail?.['3'] || 0);
});
const option: EChartsOption = {
tooltip: {
trigger: 'axis',
axisPointer: { type: 'shadow' },
backgroundColor: 'rgba(0, 0, 0, 0.5)',
borderColor: 'transparent',
textStyle: { color: '#ffffff' }
},
grid: {
top: 30,
left: '2%',
right: '4%',
bottom: 30,
containLabel: true
},
legend: {
bottom: 0,
itemWidth: 10,
itemHeight: 10
},
yAxis: {
type: 'value',
minInterval: 1,
name: '种',
axisLine: { lineStyle: { color: '#e0e0e0' } },
axisLabel: { color: '#666' },
splitLine: { lineStyle: { color: '#e0e0e0', type: 'solid' } }
},
xAxis: {
type: 'category',
axisTick: { show: false },
axisLabel: {
interval: 0,
rotate: 0,
color: '#666',
rich: {
a: { height: 24, align: 'center' },
b: { height: 30, align: 'center' }
}
},
name: '数量(个)',
data: xAxisData
},
series: [
{
name: '外来鱼类',
type: 'bar',
stack: 'stack',
data: waimaiData,
label: { show: false }
},
{
name: '本土受威胁鱼类',
type: 'bar',
stack: 'stack',
data: bentuThreatData,
label: { show: false }
},
{
name: '本土无危鱼类',
type: 'bar',
stack: 'stack',
data: bentuSafeData,
label: { show: false }
}
],
barWidth: 15,
color: ['#4B79AB', '#78C300', '#9556A4']
};
2026-05-12 08:47:27 +08:00
2026-06-26 17:52:57 +08:00
chartInstance.setOption(option);
nextTick(() => {
chartInstance.resize();
});
2026-05-12 08:47:27 +08:00
};
2026-06-26 17:52:57 +08:00
/** 窗口大小变化时自适应 */
2026-05-12 08:47:27 +08:00
const handleResize = () => {
2026-06-26 17:52:57 +08:00
if (chartInstance) {
chartInstance.resize();
}
};
// ==================== 数据获取 ====================
/**
* 获取年份选项数据
* 调用 getDftYear API按基地/流域分组返回可用年份
* 初始化时调用为每个流域缓存其对应的年份列表
*/
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);
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];
// baseid 为 all 时取第一个有数据的流域
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');
2026-05-12 08:47:27 +08:00
}
2026-06-26 17:52:57 +08:00
} catch (error) {
console.error('获取年份数据失败:', error);
select.value.options = [];
select.value.value = moment().format('YYYY');
}
2026-05-12 08:47:27 +08:00
};
2026-06-26 17:52:57 +08:00
/**
* 根据选中的流域代码更新年份下拉选项
* yearOptions 缓存中查找对应流域的年份列表并更新 select.options
* @param wbsCode 流域代码
*/
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 = '';
}
};
2026-05-12 08:47:27 +08:00
2026-06-26 17:52:57 +08:00
/**
* 获取流域树形选择器数据 + 年份数据 + 图表数据
* 在基地变化时调用是整个模块数据加载的入口
*/
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 || [];
if (data.length > 0) {
const treeData = buildTreeData(data);
const filteredData = filterSelectOptions(treeData);
// 只有一个流域时隐藏树形选择器
moreSelect.value.options = filteredData;
moreSelect.value.value = treeData?.[0]?.children?.[0]?.value;
// 先获取年份数据缓存到 yearOptions再根据实际选中的流域设置年份选项
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)年份(yr)基地(baseId)调用 fishChanges2 API
*/
const getChartData = async () => {
if (!select.value.value) return;
dataLoading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
moreSelect.value.value
? {
field: 'hbrvcd',
operator: 'eq',
dataType: 'string',
value: moreSelect.value.value
}
: null,
{
field: 'yr',
operator: 'eq',
dataType: 'string',
value: moment(select.value.value).format('YYYY')
},
baseid.value !== 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null
].filter(Boolean)
},
group: [
{ dir: 'des', field: 'stnm' },
{ dir: 'des', field: 'stcd' },
{ dir: 'des', field: 'ty' }
],
groupResultFlat: 'true'
};
const res = await fishChanges2(params);
chartData.value = res?.data?.data || [];
2026-05-12 08:47:27 +08:00
nextTick(() => {
2026-06-26 17:52:57 +08:00
updateChart();
2026-05-12 08:47:27 +08:00
});
2026-06-26 17:52:57 +08:00
} catch (error) {
console.error('获取图表数据失败:', error);
chartData.value = [];
nextTick(() => {
updateChart();
});
} finally {
dataLoading.value = false;
}
};
// ==================== 树形工具函数 ====================
/**
* API 返回的扁平数据转换为树形结构
* objid基地分组每个基地下包含其流域列表
* @param data 扁平的流域数据数组
*/
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;
});
// 按 objid 分组并去重
const dataMapNameMap: Record<string, any[]> = {};
data.forEach(item => {
const objId = item.objid || item.objId;
if (!dataMapNameMap[objId]) {
dataMapNameMap[objId] = [];
}
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 openDetail = (params: any) => {
2026-06-26 17:52:57 +08:00
// 从 chartData 中查找对应站点的 stcd
const found = chartData.value.find((el: any) => el.stnm === params.name);
modalYear.value = select.value.value;
modalHbrvcd.value = moreSelect.value.value || '';
modalStateName.value = params.seriesName || '';
2026-06-26 17:52:57 +08:00
modalSectionName.value = params.name || '';
modalStcd.value = found?.stcd || '';
modalVisible.value = true;
};
/** 关闭详情弹框 */
const handleModalClose = () => {
modalVisible.value = false;
};
// ==================== 事件处理 ====================
/**
* 处理侧边面板控件变化流域或年份选择改变时触发
* SidePanelItem 内部通过 watch 监听用户操作后 emit 此事件
* @param data 包含 { select, moreSelect, ... } 的变更值
*/
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);
await getChartData();
return; // 已用新年份请求数据,避免年份变化再次触发请求
}
// 年份选择变化:直接请求图表数据
if (data.select !== undefined && data.select !== select.value.value) {
select.value.value = data.select;
await getChartData();
}
};
// ==================== Watch 监听 ====================
/**
* 监听基地变化
* 当用户在顶部切换基地时重新加载流域列表年份和图表数据
*/
watch(
() => JidiSelectEventStore.selectedItem,
async newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
await getMoreSelectData();
}
},
{ deep: true, immediate: true }
);
// ==================== 生命周期钩子 ====================
onMounted(() => {
nextTick(() => {
setTimeout(() => {
initChart();
window.addEventListener('resize', handleResize);
}, 50);
});
2026-05-12 08:47:27 +08:00
});
onUnmounted(() => {
2026-06-26 17:52:57 +08:00
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
2026-05-12 08:47:27 +08:00
});
</script>
<style lang="scss" scoped>
.chart-container {
2026-06-26 17:52:57 +08:00
width: 100%;
height: 231px;
min-height: 231px;
}
.empty-placeholder {
width: 100%;
min-height: 231px;
display: flex;
background: #fff;
flex-direction: column;
justify-content: center;
align-items: center;
}
:deep(.ant-empty-description) {
text-indent: 0em !important;
2026-05-12 08:47:27 +08:00
}
2026-06-26 17:52:57 +08:00
</style>