过鱼设施的接口对接

This commit is contained in:
王兴凯 2026-06-08 10:31:19 +08:00
parent f58982afc1
commit 897bbe0d41
6 changed files with 354 additions and 100 deletions

View File

@ -7,7 +7,7 @@ VITE_APP_TITLE = '水电水利建设项目全过程环境管理信息平台'
VITE_APP_PORT = 3000 VITE_APP_PORT = 3000
VITE_APP_BASE_API = '/dev-api' VITE_APP_BASE_API = '/dev-api'
# 本地环境 # 本地环境
VITE_APP_BASE_URL = 'http://localhost:8093' VITE_APP_BASE_URL = 'http://10.84.121.199:8093'
# 测试环境 # 测试环境
# VITE_APP_BASE_URL = 'http://172.16.21.142:8093' # VITE_APP_BASE_URL = 'http://172.16.21.142:8093'
# VITE_APP_BASE_URL = 'http://172.16.21.142:8096' # VITE_APP_BASE_URL = 'http://172.16.21.142:8096'
@ -15,7 +15,7 @@ VITE_APP_BASE_URL = 'http://localhost:8093'
# VITE_APP_BASE_URL = 'http://10.84.121.21:8093' # VITE_APP_BASE_URL = 'http://10.84.121.21:8093'
# VITE_APP_BASE_URL = 'http://192.168.1.162:8093' # VITE_APP_BASE_URL = 'http://192.168.1.162:8093'
# 测试环境线上 # 测试环境线上10.84.121.122:
VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122' VITE_APP_TEST_ONLINE_URL = 'https://211.99.26.225:12122'

View File

@ -15,3 +15,11 @@ export function buildGetKendoListCust(data: any) {
data data
}); });
} }
//过鱼总量
export function yearGetYearFpStatistics(data: any) {
return request({
url: '/api/wmp-env-server/env/fp/run/qgc/year/GetYearFpStatistics',
method: 'get',
params:data
});
}

View File

@ -1,23 +1,23 @@
<!-- SidePanelItem.vue --> <!-- SidePanelItem.vue -->
<template> <template>
<SidePanelItem :title="title" :datetimePicker="datetimePicker"> <SidePanelItem :title="title" :datetimePicker="datetimePicker" @update-values="handlePanelChange1">
<div <!-- 加载状态 -->
class="container" <div v-if="loading" class="loading-container">
@mouseenter="handleMouseEnter" <a-spin tip="加载中..." />
@mouseleave="handleMouseLeave" </div>
>
<!-- 无数据状态 -->
<div v-else-if="!hasData" class="empty-container">
<a-empty description="暂无数据" />
</div>
<!-- 正常显示图表 -->
<div v-else class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 跑马灯轨道容器 --> <!-- 跑马灯轨道容器 -->
<div <div class="carousel-track" :class="{ 'no-transition': isTransitioning }"
class="carousel-track" :style="{ transform: `translateX(-${currentIndex * 100}%)` }">
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<!-- 遍历所有图表项包含克隆项 --> <!-- 遍历所有图表项包含克隆项 -->
<div <div v-for="(item, index) in renderChartData" :key="index" class="carousel-item">
v-for="(item, index) in renderChartData"
:key="index"
class="carousel-item"
>
<div class="pie-chart-container"> <div class="pie-chart-container">
<!-- 图表区域 --> <!-- 图表区域 -->
<div :ref="el => setChartRef(el, index)" class="chart"></div> <div :ref="el => setChartRef(el, index)" class="chart"></div>
@ -31,21 +31,44 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref, onMounted, onUnmounted, nextTick } from 'vue'; import { ref, onMounted, onUnmounted, nextTick, watch } from 'vue';
import * as echarts from 'echarts'; import * as echarts from 'echarts';
import type { ECharts } from 'echarts'; import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue'; import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { yearGetYearFpStatistics } from '@/api/gyss';
import { useJidiSelectEventStore } from "@/store/modules/jidiSelectEvent";
// API
interface FpFtpStatisticsVo {
baseId: string;
baseName: string;
ftp: string;
fishName: string;
fcnt: number;
}
interface BasinData {
fpCount: number;
baseId: string;
baseName: string;
fpFtpStatitcsVos: FpFtpStatisticsVo[];
}
// 便 // 便
defineOptions({ defineOptions({
name: 'GYZLLB' name: 'GYZLLB'
}); });
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const props = defineProps({ const props = defineProps({
title: { // title: { //
type: String, type: String,
default: '过鱼总量' default: '过鱼总量'
}, },
}); });
const datetimePicker = ref({ const datetimePicker = ref({
show: true, show: true,
value: `${new Date().getFullYear() - 1}`, // value: `${new Date().getFullYear() - 1}`, //
@ -54,6 +77,14 @@ const datetimePicker = ref({
options: [] options: []
}); });
//
const loading = ref<boolean>(false);
const hasData = ref<boolean>(true);
//
const cachedApiData = ref<BasinData[]>([]); // API
const currentYear = ref<string>(''); //
// //
interface ChartDataItem { interface ChartDataItem {
title: string; title: string;
@ -61,45 +92,7 @@ interface ChartDataItem {
} }
// //
const originalChartData = ref<ChartDataItem[]>( const originalChartData = ref<ChartDataItem[]>([]);
[
{
title: '过鱼总量统计 - 2025年',
data: [
{ name: '草鱼', value: 1250 },
{ name: '鲢鱼', value: 980 },
{ name: '鳙鱼', value: 760 },
{ name: '青鱼', value: 540 },
{ name: '鲤鱼', value: 430 },
{ name: '鲫鱼', value: 320 },
{ name: '其他', value: 280 }
]
},
{
title: '过鱼总量统计 - 2024年',
data: [
{ name: '草鱼1', value: 1250 },
{ name: '鲢鱼1', value: 980 },
{ name: '鳙鱼1', value: 760 },
{ name: '青鱼1', value: 540 },
{ name: '鲤鱼1', value: 430 },
{ name: '鲫鱼1', value: 320 },
{ name: '其他1', value: 280 }
]
},
{
title: '过鱼总量统计 - 2023年',
data: [
{ name: '草鱼2', value: 1250 },
{ name: '鲢鱼2', value: 980 },
{ name: '鳙鱼2', value: 760 },
{ name: '青鱼2', value: 540 },
{ name: '鲤鱼2', value: 430 },
{ name: '鲫鱼2', value: 320 },
{ name: '其他2', value: 280 }
]
}
]);
// //
const renderChartData = ref<ChartDataItem[]>([]); const renderChartData = ref<ChartDataItem[]>([]);
@ -110,6 +103,16 @@ const currentIndex = ref(1); // 从1开始跳过克隆的首项
// //
let timer: any = null; let timer: any = null;
/**
* 停止自动轮播
*/
const stopAutoPlay = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
// //
const isHovering = ref(false); const isHovering = ref(false);
@ -140,7 +143,7 @@ const getUnitConfigByCode = (code: string, type: string): { unit: string } => {
// - // -
const generateRandomColor = (names: string[]): string[] => { const generateRandomColor = (names: string[]): string[] => {
const colors: string[] = []; const colors: string[] = [];
names.forEach(() => { names.forEach(() => {
// HSL // HSL
// H: 0-360 () // H: 0-360 ()
@ -149,42 +152,42 @@ const generateRandomColor = (names: string[]): string[] => {
const hue = Math.floor(Math.random() * 360); const hue = Math.floor(Math.random() * 360);
const saturation = 40 + Math.floor(Math.random() * 30); // 40-70% const saturation = 40 + Math.floor(Math.random() * 30); // 40-70%
const lightness = 45 + Math.floor(Math.random() * 20); // 45-65% const lightness = 45 + Math.floor(Math.random() * 20); // 45-65%
// HSL HEX // HSL HEX
const h = hue / 360; const h = hue / 360;
const s = saturation / 100; const s = saturation / 100;
const l = lightness / 100; const l = lightness / 100;
let r, g, b; let r, g, b;
if (s === 0) { if (s === 0) {
r = g = b = l; r = g = b = l;
} else { } else {
const hue2rgb = (p: number, q: number, t: number) => { const hue2rgb = (p: number, q: number, t: number) => {
if (t < 0) t += 1; if (t < 0) t += 1;
if (t > 1) t -= 1; if (t > 1) t -= 1;
if (t < 1/6) return p + (q - p) * 6 * t; if (t < 1 / 6) return p + (q - p) * 6 * t;
if (t < 1/2) return q; if (t < 1 / 2) return q;
if (t < 2/3) return p + (q - p) * (2/3 - t) * 6; if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6;
return p; return p;
}; };
const q = l < 0.5 ? l * (1 + s) : l + s - l * s; const q = l < 0.5 ? l * (1 + s) : l + s - l * s;
const p = 2 * l - q; const p = 2 * l - q;
r = hue2rgb(p, q, h + 1/3); r = hue2rgb(p, q, h + 1 / 3);
g = hue2rgb(p, q, h); g = hue2rgb(p, q, h);
b = hue2rgb(p, q, h - 1/3); b = hue2rgb(p, q, h - 1 / 3);
} }
const toHex = (x: number) => { const toHex = (x: number) => {
const hex = Math.round(x * 255).toString(16); const hex = Math.round(x * 255).toString(16);
return hex.length === 1 ? '0' + hex : hex; return hex.length === 1 ? '0' + hex : hex;
}; };
colors.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`); colors.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`);
}); });
return colors; return colors;
}; };
@ -220,7 +223,7 @@ const initChart = (index: number) => {
const updateChart = (index: number) => { const updateChart = (index: number) => {
const chartInstance = chartInstances.value[index]; const chartInstance = chartInstances.value[index];
const chartData = renderChartData.value[index]; const chartData = renderChartData.value[index];
if (!chartInstance || !chartData) return; if (!chartInstance || !chartData) return;
const colorNames = chartData.data.map(item => item.name); const colorNames = chartData.data.map(item => item.name);
@ -243,7 +246,7 @@ const updateChart = (index: number) => {
{ {
text: `${totalAmount}`, text: `${totalAmount}`,
subtext: `总量(${unit})`, subtext: `总量(${unit})`,
left: '34%', left: '29%',
top: '45%', top: '45%',
textAlign: 'center', textAlign: 'center',
textVerticalAlign: 'middle', textVerticalAlign: 'middle',
@ -272,7 +275,7 @@ const updateChart = (index: number) => {
pageIconInactiveColor: '#ccc', pageIconInactiveColor: '#ccc',
pageIconSize: 12, pageIconSize: 12,
pageFormatter: '{current}/{total}', pageFormatter: '{current}/{total}',
formatter: function(name: string) { formatter: function (name: string) {
const found = chartData.data.find(item => item.name === name); const found = chartData.data.find(item => item.name === name);
const value = found ? found.value : '-'; const value = found ? found.value : '-';
const maxLength = 7; const maxLength = 7;
@ -280,7 +283,7 @@ const updateChart = (index: number) => {
return `${truncatedName} ${value}${unit}`; return `${truncatedName} ${value}${unit}`;
}, },
textStyle: { textStyle: {
fontSize: 13, fontSize: 12,
color: '#666' color: '#666'
} }
}, },
@ -288,7 +291,7 @@ const updateChart = (index: number) => {
{ {
type: 'pie', type: 'pie',
radius: ['50%', '70%'], radius: ['50%', '70%'],
center: ['35%', '50%'], center: ['30%', '50%'],
avoidLabelOverlap: false, avoidLabelOverlap: false,
itemStyle: { itemStyle: {
borderRadius: 4, borderRadius: 4,
@ -337,13 +340,13 @@ const handleResize = () => {
const initRenderData = () => { const initRenderData = () => {
const length = originalChartData.value.length; const length = originalChartData.value.length;
if (length === 0) return; if (length === 0) return;
renderChartData.value = [ renderChartData.value = [
originalChartData.value[length - 1], // originalChartData.value[length - 1], //
...originalChartData.value, // ...originalChartData.value, //
originalChartData.value[0] // originalChartData.value[0] //
]; ];
// //
chartInstances.value = new Array(renderChartData.value.length).fill(null); chartInstances.value = new Array(renderChartData.value.length).fill(null);
chartRefs.value = new Array(renderChartData.value.length).fill(null); chartRefs.value = new Array(renderChartData.value.length).fill(null);
@ -362,7 +365,7 @@ const startAutoPlay = () => {
// //
const nextSlide = () => { const nextSlide = () => {
currentIndex.value++; currentIndex.value++;
// //
setTimeout(() => { setTimeout(() => {
checkSeamlessJump(); checkSeamlessJump();
@ -372,15 +375,15 @@ const nextSlide = () => {
// //
const checkSeamlessJump = () => { const checkSeamlessJump = () => {
const realLength = originalChartData.value.length; const realLength = originalChartData.value.length;
// = realLength + 1 // = realLength + 1
if (currentIndex.value >= realLength + 1) { if (currentIndex.value >= realLength + 1) {
// 1. // 1.
isTransitioning.value = true; isTransitioning.value = true;
// 2. 1 // 2. 1
currentIndex.value = 1; currentIndex.value = 1;
// 3. DOM // 3. DOM
requestAnimationFrame(() => { requestAnimationFrame(() => {
requestAnimationFrame(() => { requestAnimationFrame(() => {
@ -403,7 +406,7 @@ const handleMouseLeave = () => {
// //
onMounted(async () => { onMounted(async () => {
initRenderData(); initRenderData();
// DOM // DOM
await nextTick(); await nextTick();
setTimeout(() => { setTimeout(() => {
@ -411,10 +414,10 @@ onMounted(async () => {
renderChartData.value.forEach((_, index) => { renderChartData.value.forEach((_, index) => {
initChart(index); initChart(index);
}); });
// //
window.addEventListener('resize', handleResize); window.addEventListener('resize', handleResize);
// //
startAutoPlay(); startAutoPlay();
}, 100); }, 100);
@ -423,16 +426,241 @@ onMounted(async () => {
// //
onUnmounted(() => { onUnmounted(() => {
if (timer) clearInterval(timer); if (timer) clearInterval(timer);
// //
chartInstances.value.forEach(instance => { chartInstances.value.forEach(instance => {
if (instance) { if (instance) {
instance.dispose(); instance.dispose();
} }
}); });
window.removeEventListener('resize', handleResize); window.removeEventListener('resize', handleResize);
}); });
/**
* 构建单一流域的图表数据
*/
const buildSingleBasinChartData = (basin: BasinData, year: string): ChartDataItem => {
return {
title: `${basin.baseName}`,
data: basin.fpFtpStatitcsVos.map((fish: FpFtpStatisticsVo) => ({
name: fish.fishName,
value: fish.fcnt
}))
};
};
/**
* 构建全部流域的图表数据包含流域对比图和各流域鱼类分布图
*/
const buildAllBasinsChartData = (apiData: BasinData[], year: string): ChartDataItem[] => {
const charts: ChartDataItem[] = [];
// 1 使 fpCount
const basinComparisonData = apiData
.filter(basin => basin.fpFtpStatitcsVos && basin.fpFtpStatitcsVos.length > 0)
.map(basin => ({
name: basin.baseName,
value: basin.fpCount
}));
if (basinComparisonData.length > 0) {
charts.push({
title: `总量`,
data: basinComparisonData
});
}
// 2 使 fcnt
apiData.forEach(basin => {
if (basin.fpFtpStatitcsVos && basin.fpFtpStatitcsVos.length > 0) {
charts.push(buildSingleBasinChartData(basin, year));
}
});
return charts;
};
/**
* 重新构建图表实例用于完全重建
*/
const rebuildCharts = async () => {
//
chartInstances.value.forEach(instance => {
if (instance) {
instance.dispose();
}
});
chartInstances.value = [];
//
initRenderData();
// DOM
await nextTick();
//
setTimeout(() => {
renderChartData.value.forEach((_, index) => {
initChart(index);
});
}, 100);
};
/**
* 控制跑马灯的启停
*/
const controlCarousel = () => {
//
const shouldStop = baseid.value !== 'all' || originalChartData.value.length <= 1;
if (shouldStop) {
stopAutoPlay(); // currentIndex
} else {
startAutoPlay(); //
}
};
/**
* 根据当前 baseid 更新图表显示不请求 API
*/
const updateChartByBaseid = () => {
if (!cachedApiData.value.length) {
hasData.value = false;
originalChartData.value = [];
renderChartData.value = []; //
stopAutoPlay(); //
return;
}
if (baseid.value === 'all') {
// === ===
originalChartData.value = buildAllBasinsChartData(cachedApiData.value, currentYear.value);
} else {
// === ===
const targetBasin = cachedApiData.value.find(item => item.baseId === baseid.value);
if (!targetBasin || !targetBasin.fpFtpStatitcsVos || targetBasin.fpFtpStatitcsVos.length === 0) {
hasData.value = false;
originalChartData.value = [];
renderChartData.value = []; //
stopAutoPlay(); //
return;
}
originalChartData.value = [buildSingleBasinChartData(targetBasin, currentYear.value)];
}
//
initRenderData();
//
hasData.value = true;
// DOM
nextTick(() => {
//
chartInstances.value.forEach(instance => {
if (instance) {
instance.dispose();
}
});
chartInstances.value = new Array(renderChartData.value.length).fill(null);
chartRefs.value = new Array(renderChartData.value.length).fill(null);
setTimeout(() => {
renderChartData.value.forEach((_, index) => {
initChart(index);
});
}, 100);
});
controlCarousel(); //
};
/**
* 获取并处理图表数据
*/
const getechartsdata = async (yearOne: string) => {
if (!baseid.value) {
hasData.value = false;
originalChartData.value = [];
renderChartData.value = [];
stopAutoPlay();
return;
}
// 使
if (yearOne === currentYear.value && cachedApiData.value.length > 0) {
updateChartByBaseid();
return;
}
loading.value = true;
try {
const params = { year: yearOne };
const res = await yearGetYearFpStatistics(params);
// axios res.data
const responseData = res.data;
if (!responseData || responseData.length === 0) {
console.warn('无有效数据');
hasData.value = false;
originalChartData.value = [];
renderChartData.value = [];
cachedApiData.value = [];
currentYear.value = '';
stopAutoPlay();
return;
}
// debugger
//
cachedApiData.value = responseData;
currentYear.value = yearOne;
// baseid
updateChartByBaseid();
} catch (error) {
console.error('获取过鱼统计数据失败:', error);
hasData.value = false;
originalChartData.value = [];
renderChartData.value = [];
cachedApiData.value = [];
currentYear.value = '';
stopAutoPlay();
} finally {
loading.value = false;
}
};
const handlePanelChange1 = (data: any) => {
if (data.datetime) {
getechartsdata(data.datetime); // API
} else {
//
hasData.value = false;
originalChartData.value = [];
renderChartData.value = []; //
cachedApiData.value = []; //
currentYear.value = '';
stopAutoPlay(); //
}
};
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
// API
if (cachedApiData.value.length > 0) {
updateChartByBaseid();
}
}
},
{ deep: true, immediate: true }
);
</script> </script>
<style lang="scss" scoped> <style lang="scss" scoped>
@ -443,26 +671,26 @@ onUnmounted(() => {
border-radius: 5px; border-radius: 5px;
position: relative; position: relative;
overflow: hidden; // overflow: hidden; //
// //
.carousel-track { .carousel-track {
display: flex; // display: flex; //
width: 100%; width: 100%;
height: 100%; height: 100%;
transition: transform 0.5s ease-in-out; // 0.5 transition: transform 0.5s ease-in-out; // 0.5
// //
&.no-transition { &.no-transition {
transition: none; transition: none;
} }
// //
.carousel-item { .carousel-item {
min-width: 100%; // min-width: 100%; //
height: 100%; height: 100%;
position: relative; position: relative;
flex-shrink: 0; // flex-shrink: 0; //
.pie-chart-container { .pie-chart-container {
width: 100%; width: 100%;
height: 100%; height: 100%;
@ -485,4 +713,22 @@ onUnmounted(() => {
} }
} }
} }
//
.loading-container {
width: 100%;
height: 290px;
display: flex;
align-items: center;
justify-content: center;
}
//
.empty-container {
width: 100%;
height: 290px;
display: flex;
align-items: center;
justify-content: center;
}
</style> </style>

View File

@ -81,8 +81,8 @@ const guoyuStatus = ref<any>([]);
const baseColumnsConfig: ColumnConfig[] = [ const baseColumnsConfig: ColumnConfig[] = [
{ {
dataIndex: 'hbrvnm', dataIndex: 'rvnm',
key: 'hbrvnm', key: 'rvnm',
title: '流域', title: '流域',
width: 120, width: 120,
fixed: 'left' fixed: 'left'

View File

@ -256,8 +256,8 @@ const guoyuStatus = ref<any>([]);
// --- --- // --- ---
const baseColumnsConfig: ColumnConfig[] = [ const baseColumnsConfig: ColumnConfig[] = [
{ {
dataIndex: 'hbrvnm', dataIndex: 'rvnm',
key: 'hbrvnm', key: 'rvnm',
title: '流域', title: '流域',
width: 120, width: 120,
fixed: 'left' fixed: 'left'

View File

@ -258,8 +258,8 @@ import PreviewMedia from '@/components/previewMedia/index.vue';
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL; const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
let columns = ref([ let columns = ref([
{ {
dataIndex: 'hbrvnm', dataIndex: 'rvnm',
key: 'hbrvnm', key: 'rvnm',
title: '流域', title: '流域',
width: 120, width: 120,
fixed: 'left' fixed: 'left'
@ -515,8 +515,8 @@ const previewListIndex = ref(0);
// //
const detailColumns = ref([ const detailColumns = ref([
{ {
dataIndex: 'hbrvnm', dataIndex: 'rvnm',
key: 'hbrvnm', key: 'rvnm',
title: '流域', title: '流域',
width: 120, width: 120,
fixed: 'left' fixed: 'left'