WholeProcessPlatform/frontend/src/modules/alongFishMod/index.vue
2026-07-15 18:24:05 +08:00

514 lines
13 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="沿程鱼类变化情况"
: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"
:tm="modalYear"
:dataDimensionVal="moreSelect.value.value"
:hbrvcd="modalHbrvcd"
:jdList="JidiSelectEventStore.jidiData"
:stateName="modalStateName"
:sectionName="modalSectionName"
:stcd="modalStcd"
:ly="true"
/>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, watch, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import Yanchengyulei from './TwoLayers/Yanchengyulei.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { getDftYear, fishChanges2 } from '@/api/stdc';
import moment from 'moment';
import { useDraggable } from '@/utils/drag';
defineOptions({
name: 'alongFishMod'
});
// ==================== Store ====================
const JidiSelectEventStore = useJidiSelectEventStore();
// ==================== 基础状态 ====================
/** 图表数据加载状态 */
const dataLoading = ref(false);
/** 初始化标志位:初始化期间不响应控件变化触发数据请求 */
const isInitializing = ref(false);
// ==================== 选择器状态 ====================
/** 流域树形选择器配置 */
const moreSelect = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[]
});
/** 年份选择器配置 */
const select = ref({
show: true,
value: undefined as string | undefined,
options: [] as any[],
picker: undefined,
format: undefined,
width: '120px'
});
/**
* 按流域分组的年份数据
* 结构:{ "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('');
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
// ==================== 图表相关 ====================
/**
* 初始化 ECharts 实例
* 在 onMounted 中调用,绑定点击事件用于打开详情弹框
*/
const initChart = () => {
if (!chartRef.value) return;
if (chartInstance) {
chartInstance.dispose();
}
chartInstance = echarts.init(chartRef.value);
// 点击柱子弹出详情
chartInstance.on('click', (params: any) => {
if (params.componentType === 'series') {
openDetail(params);
}
});
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']
};
chartInstance.setOption(option);
nextTick(() => {
chartInstance.resize();
});
};
/** 窗口大小变化时自适应 */
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
};
// ==================== 数据获取 ====================
/**
* 获取年份选项数据
* 调用 getDftYear API按基地/流域分组返回可用年份
* 初始化时调用,为每个流域缓存其对应的年份列表
*/
const getYearData = async () => {
try {
const filter: any = {};
if (moreSelect.value.value) {
filter.baseId = moreSelect.value.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];
// 取第一个有数据的流域
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');
}
};
/**
* 根据选中的流域代码更新年份下拉选项
* 从 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 = '';
}
};
/**
* 获取流域树形选择器数据 + 年份数据 + 图表数据
* 在基地变化时调用,是整个模块数据加载的入口
*/
/**
* 获取图表数据
* 根据当前选中的流域(hbrvcd)、年份(yr)、基地(baseId)调用 fishChanges2 API
*/
const getChartData = async () => {
if (!select.value.value) return;
dataLoading.value = true;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: moreSelect.value.value
},
{
field: 'yr',
operator: 'eq',
dataType: 'string',
value: moment(select.value.value).format('YYYY')
}
].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 || [];
nextTick(() => {
updateChart();
});
} catch (error) {
console.error('获取图表数据失败:', error);
chartData.value = [];
nextTick(() => {
updateChart();
});
} finally {
dataLoading.value = false;
}
};
// ==================== 弹框相关 ====================
/**
* 打开详情弹框
* 点击图表柱子时触发,传递当前选中的年份、流域、站点名称等参数
*/
const openDetail = (params: any) => {
// 从 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 || '';
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) {
if (newVal.wbsCode === 'all') {
moreSelect.value.value = moreSelect.value.options[0]?.value;
} else {
moreSelect.value.value = newVal.wbsCode;
}
await getYearData();
if (moreSelect.value.value) {
await updateYearOptionsByBase(moreSelect.value.value);
}
await getChartData();
}
},
{ deep: true, immediate: true }
);
// 监听 JidiSelectEventStore.jidiData 变化,同步 moreSelect 下拉框选项
watch(
() => JidiSelectEventStore.jidiData,
newVal => {
const filtered = newVal.filter(item => item.wbsCode !== 'all');
moreSelect.value.options = filtered.map(item => ({
value: item.wbsCode,
title: item.wbsName
}));
if (!moreSelect.value.value && filtered.length > 0) {
moreSelect.value.value = filtered[0].wbsCode;
}
},
{ deep: true, immediate: true }
);
// ==================== 生命周期钩子 ====================
onMounted(() => {
nextTick(() => {
setTimeout(() => {
initChart();
window.addEventListener('resize', handleResize);
}, 50);
});
});
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
});
</script>
<style lang="scss" scoped>
.chart-container {
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;
}
</style>