增殖放流和珍稀动植物

This commit is contained in:
王兴凯 2026-06-24 11:26:19 +08:00
parent 8fba065bc4
commit 60adfa2eb1
21 changed files with 3332 additions and 1258 deletions

View File

@ -0,0 +1,35 @@
import request from '@/utils/request';
export function vpcGetKendoListCust(data: any) {
return request({
url: '/api/wmp-env-server/env/vpc/GetKendoListCust',
method: 'post',
data
});
}
// 植物园建设及接入情况 - 表格数据
export function vmsstbprptGetKendoList(data: any) {
return request({
url: '/api/dec-lygk-base-server/base/vmsstbprpt/GetKendoList',
method: 'post',
data
});
}
// 动物救助情况
export function wvaGetKendoListCust(data: any) {
return request({
url: '/api/wmp-env-server/env/wva/GetKendoListCust',
method: 'post',
data
});
}
//植物园情况介绍
// export function vpcGetKendoListCust(data: any) {
// return request({
// url: '/api/wmp-env-server/env/vpc/GetKendoListCust',
// method: 'post',
// data
// });
// }

View File

@ -0,0 +1,76 @@
import request from '@/utils/request';
// 增殖站建设运行情况 - 概览统计
export function overviewGetKendoListCust(data: any) {
return request({
url: '/fb/station/overview/GetKendoListCust',
method: 'post',
data
});
}
// 增殖站概况详情 - 全国概览接口all=true时使用
export function qgcoverviewGetOverviewSecond(data: any) {
return request({
url: '/fb/station/qgcoverview/getOverviewSecond',
method: 'post',
data
});
}
// 增殖站概况详情 - 普通接口all=false时使用
export function overviewGetOverviewSecond(data: any) {
return request({
url: '/fb/station/overview/getOverviewSecond',
method: 'post',
data
});
}
//增殖站介绍 /api/dec-lygk-base-server/base/msstbprpt/GetKendoList
export function msstbprptGetKendoList(data: any) {
return request({
url: '/fb/msstbprpt/GetKendoList',
method: 'post',
data
});
}
// 增殖站运行数据统计
export function qgcgetFbStaticsData(data: any) {
return request({
url: '/fb/station/qgc/getFbStaticsData',
method: 'post',
data
});
}
// 增殖计划完成情况,下拉框 ///////
export function fishTypegetStcdList(data: any) {
return request({
url: '/fb/fishType/getStcdList',
method: 'post',
data
});
}
//增殖计划完成情况-数量完成情况//////////////
export function fishCountGetKendoListCust(data: any) {
return request({
url: '/fb/fishCount/GetKendoListCust',
method: 'post',
data
});
}
//增殖计划完成情况-种类完成情况///////////
export function fishTypeGetKendoListCust(data: any) {
return request({
url: '/fb/fishType/GetKendoListCust',
method: 'post',
data
});
}
// 增殖站运行情况统计列表 (TwoLayers 弹窗表格) ///////////////
export function msfbrdmGetKendoListCust(data: any) {
return request({
url: '/fb/msfbrdm/qgc/GetKendoListCust',
method: 'post',
data
});
}

View File

@ -215,7 +215,7 @@ const getList = async (filter?: Record<string, any>) => {
let finalTotal = 0;
if (props.transformData) {
const result = props.transformData(res);
const result:any = props.transformData(res);
finalRecords = result.records || result.data || [];
finalTotal = result.total || 0;
} else {
@ -392,8 +392,11 @@ const enhancedColumns = computed(() => {
}
// merge
if (enhancedCol.merge && enhancedCol.dataIndex) {
enhancedCol.customCell = createMergeCellHandler(enhancedCol.dataIndex);
if (enhancedCol.merge) {
const mergeField = enhancedCol.dataIndex || enhancedCol.mergeWith;
if (mergeField) {
enhancedCol.customCell = createMergeCellHandler(mergeField);
}
// slots.customRender slots
if (enhancedCol.slots?.customRender) {

View File

@ -295,7 +295,14 @@ watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
console.log('isActive', modelStore.filter.year);
if (modelStore.filter.year) {
searchParams.value.tm = modelStore.filter.year;
getFtpOptions();
} else {
getYearList();
}
hasLoaded.value = true;
}
},

View File

@ -150,7 +150,13 @@ watch(
() => props.isActive,
active => {
if (active && !hasLoaded.value) {
getYearList();
// getYearList();
if (modelStore.filter.year) {
searchParams.value.tm = modelStore.filter.year;
refreshTable();
} else {
getYearList();
}
hasLoaded.value = true;
}
},

View File

@ -209,7 +209,7 @@
v-show="currentActiveKey === 'ProcessDiagram'"
:is-active="currentActiveKey === 'ProcessDiagram'"
/>
<!-- 放流情况 -->
<!-- 放流情况 -->
<NormalAddedSituation
v-show="currentActiveKey === 'NormalAddedSituation'"
:is-active="currentActiveKey === 'NormalAddedSituation'"
@ -405,63 +405,6 @@ const isLCJ = env?.startsWith('lcj');
const sttpList = ['ENG', 'WT', 'WQ', 'FP', 'FB', 'VP', 'DW', 'EQ', 'VA'];
// ==================== Tab ====================
// 6: AI
const checkVerticalWaterAndAI = async (stcd: string) => {
try {
const res = await getStInfoByStcd({ stcd });
const data = res?.data || res;
if (data && data.relList) {
const jianCei = data.relList.find(
(item: any) => item.sttpCode === 'WTVT'
);
const aiShiBie = data.relList.filter((item: any) =>
item?.sttpCode?.startsWith('AI')
);
if (!jianCei) {
// Tab
tabsConfig.value = tabsConfig.value.filter(
(item: any) => item.type !== 'VerticalWaterTemperature'
);
} else {
modelStore.verticalWaterStcd = jianCei.stcd;
}
if (!aiShiBie.length) {
// AI AI Tab
tabsConfig.value = tabsConfig.value.filter(
(item: any) => item.type !== 'AIYXSB'
);
} else {
modelStore.aiSites = aiShiBie;
}
}
} catch (error) {
console.error('垂向水温/AI 检查失败:', error);
}
};
// 7:
const checkVideoExists = async (stcd: string) => {
try {
const res = await getVideoList({
filter: {
logic: 'and',
filters: [{ field: 'stcd', operator: 'eq', value: stcd }]
}
});
if (!res?.data?.data?.length && !res?.data?.records?.length) {
// Tab
tabsConfig.value = tabsConfig.value.filter(
(item: any) => item.type !== 'video'
);
}
} catch (error) {
console.error('视频检查失败:', error);
}
};
// 8: (React: isLCJ && sttpCode === 'WE')
const checkFishTab = async (stcd: string) => {
try {
@ -604,7 +547,7 @@ const setQGCTabList = async (_tabs: any, stcd: string) => {
// 2. & AI React
const res4 = await getStInfoByStcd({ stcd });
let newTabs = [...tabs];
// debugger
if (res4 && res4.data && res4.data.relList) {
const relList = res4.data.relList;
const jianCei = relList.find((item: any) => item.sttpCode === 'WTVT');
@ -722,14 +665,14 @@ const applyTabFilters = async (params: any) => {
// LCJ
await checkFishTab(stcd);
} else if (
((baseId === '07' || baseId === '02') && sttpCode === 'WQ') ||
( sttpCode === 'WQ') ||
sttpCode === 'WQFB' ||
sttpCode === 'wq'
) {
// AI React WQ WQFB/wq
await checkAnalysisAvailability(stcd);
} else if (
((baseId === '07' || baseId === '02') && sttpCode === 'WT') ||
( sttpCode === 'WT') ||
sttpCode === 'wt'
) {
// React WT wt

View File

@ -64,6 +64,7 @@
@focus="handleFocus"
@blur="handleBlur"
@change="handleChange"
@select="handleTreeSelect1"
></a-select>
</div>
<div v-if="shrink" class="title_shrink" @click="isExpand = !isExpand">
@ -668,7 +669,16 @@ const handleTreeSelect = (selectedKeys: string | string[], info: any) => {
};
}
};
const handleTreeSelect1 = (selectedKeys: string | string[], info: any) => {
// debugger
// lgtdlttd
if (info && typeof info === 'object') {
selectedNodeExtra.value = {
lgtd: info.lgtd,
lttd: info.lttd
};
}
};
/**
* 处理树节点展开/收起事件
* @param keys 展开的节点键值数组
@ -679,6 +689,7 @@ const handleTreeExpand = (keys: string[]) => {
//
const handleChange = (value: string) => {
// Handle change
};

View File

@ -1,76 +1,94 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="植物园情况介绍">
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 跑马灯轨道容器 -->
<div class="carousel-track" :class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }">
<!-- 遍历所有媒体项包含克隆项 -->
<div v-for="(item, index) in renderMediaData" :key="index" class="carousel-item">
<!-- 图片 -->
<img :src="item.url" alt="" />
<!-- 说明文字随媒体项移动 -->
<div class="text">{{ item.text }}</div>
</div>
<SidePanelItem title="植物园情况介绍">
<a-spin :spinning="loading">
<template v-if="originalMediaData.length > 0">
<div
class="container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<!-- 跑马灯轨道容器 -->
<div
class="carousel-track"
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<!-- 遍历所有媒体项包含克隆项 -->
<div
v-for="(item, index) in renderMediaData"
:key="index"
class="carousel-item"
@click="handleItemClick"
>
<!-- 图片 -->
<img :src="item.url" alt="" />
<!-- 说明文字随媒体项移动 -->
<div class="text">{{ item.text }}</div>
</div>
</div>
<!-- 面板指示器固定在底部右侧 -->
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in originalMediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
<!-- 面板指示器固定在底部右侧 -->
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in originalMediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
</div>
<!-- 独立的文字说明区域随跑马灯切换而变化 -->
<div class="description-text">
{{ currentDescription }}
<div class="description-text" @click="handleItemClick">
{{ currentDescription }}
</div>
</SidePanelItem>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
<!-- 设施详情弹框 -->
<a-modal v-model:open="modalVisible" :title="'植物园详情'" width="1536px" :footer="null">
<div v-if="currentItem">
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
</div>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { vpcGetKendoListCust } from '@/api/zxdzw';
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
// 便
defineOptions({
name: 'zengZhiZhanJieShaoMod'
name: 'zengZhiZhanJieShaoMod'
});
//
interface MediaItem {
type: 'image' | 'video';
url: string;
text: string;
description: string;
type: 'image' | 'video';
url: string;
text: string;
description: string;
}
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
// 3
const originalMediaData = ref<MediaItem[]>([
{
type: 'image',
url: 'https://211.99.26.225:12125/?20230814205611342377136845462200&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
text: '松岗鱼类增殖站',
description: '松岗鱼类增殖放流站位于四川省阿坝藏族羌族自治州马尔康市松岗镇,主要服务于大渡河上游的双江口和金川两座水电站,同时还承担多种珍稀特有鱼类的救护和科研任务,实现工程建设与生态环境共同推进、相互促进。'
},
{
type: 'video',
url: 'https://211.99.26.225:12125/?20230805205848575430105387253710&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920', // URL
text: '猴子岩鱼类增殖站',
description: '猴子岩水电站鱼类增殖放流站位于猴子岩水电站坝址下游约7.0km(业主营地下游约1.5km)大渡河左岸桃花渣场顶部平台上紧邻枢纽桃花大桥下游侧占地面积47.3亩其中一期工程27.0亩预留二期工程用地20.3亩(二期工程目前为丹巴、巴底水电站预留工程)增殖放流站工作流程为:亲鱼收集购买、亲鱼驯养培育、人工催产和授精、人工孵化、苗种培育和放流。 猴子岩鱼类增殖放流站近期放流对象中齐口裂腹鱼、重口裂腹鱼、大渡软刺裸裂尻鱼增殖放流技术水平已趋于熟化,中期放流对象大渡软刺裸裂尻鱼人工繁殖技术逐渐趋于熟化'
},
{
type: 'image',
url: 'https://211.99.26.225:12125/?20230805205924378504010675106305&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
text: '黑马鱼类增殖站',
description: '大渡河黑马鱼类增殖放流站位于四川省甘洛县黑马乡黑马业主营地内距离甘洛县城45km,区域交通路况较好。主要承担瀑布沟、深溪沟、大岗山、枕头坝一级、沙坪二级等五座水电站鱼类增殖放流的重任。放流鱼类包含:齐口裂腹鱼、重口裂腹鱼、鲈鲤、长薄鳅、白甲鱼、中华倒刺、长吻脆、稀有鮊鲫、华鲮、侧沟爬岩鳅等10个种类共计约918.07万尾珍稀特有鱼苗。'
}
]);
//
const modalVisible = ref(false);
const currentItem = ref<any>(null);
//
const loading = ref(false);
//
const originalMediaData = ref<MediaItem[]>([]);
//
// [, ..., ]
@ -90,197 +108,257 @@ const isTransitioning = ref(false);
//
const initRenderData = () => {
const length = originalMediaData.value.length;
if (length === 0) return;
const length = originalMediaData.value.length;
if (length === 0) return;
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
};
//
const startAutoPlay = () => {
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 4000);
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 4000);
};
//
const nextSlide = () => {
currentIndex.value++;
currentIndex.value++;
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
};
//
const checkSeamlessJump = () => {
const realLength = originalMediaData.value.length;
const realLength = originalMediaData.value.length;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// 2. 1
currentIndex.value = 1;
// 2. 1
currentIndex.value = 1;
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
};
//
const handleMouseEnter = () => {
isHovering.value = true;
isHovering.value = true;
};
//
const handleMouseLeave = () => {
isHovering.value = false;
isHovering.value = false;
};
//
const currentDescription = computed(() => {
const realIndex = getCurrentRealIndex();
return originalMediaData.value[realIndex]?.description || '';
});
//
onMounted(() => {
initRenderData();
startAutoPlay();
});
//
onUnmounted(() => {
if (timer) clearInterval(timer);
const realIndex = getCurrentRealIndex();
return originalMediaData.value[realIndex]?.description || '';
});
//
const getCurrentRealIndex = () => {
const realLength = originalMediaData.value.length;
let realIndex = currentIndex.value - 1; //
const realLength = originalMediaData.value.length;
let realIndex = currentIndex.value - 1; //
//
if (realIndex < 0) realIndex = realLength - 1;
if (realIndex >= realLength) realIndex = 0;
//
if (realIndex < 0) realIndex = realLength - 1;
if (realIndex >= realLength) realIndex = 0;
return realIndex;
return realIndex;
};
//
const goToSlide = (targetIndex: number) => {
if (isTransitioning.value) return;
if (isTransitioning.value) return;
//
currentIndex.value = targetIndex + 1;
//
currentIndex.value = targetIndex + 1;
};
//
const handleItemClick = () => {
currentItem.value = originalMediaData.value.map((item: any) => ({
url: item.url || '',
description: item.description || '',
title: item.text || ''
}));
modalVisible.value = true;
};
const getData = async () => {
loading.value = true;
try {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
]
}
};
let res = await vpcGetKendoListCust(params);
let data = res?.data?.data;
if (data && data.length > 0) {
originalMediaData.value = data.map((item: any) => {
return {
type: 'image',
url: baseUrl + '?' + item.logo + '&view=jpg',
text: item.stnm,
description: item.introduce
};
});
} else {
originalMediaData.value = [];
}
} finally {
loading.value = false;
}
};
watch(
() => JidiSelectEventStore.selectedItem,
async newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
await getData();
initRenderData();
startAutoPlay();
}
},
{ deep: true, immediate: true }
);
//
onMounted(() => {
initRenderData();
startAutoPlay();
});
//
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
// border: 1px solid #7fd6ff;
// border-radius: 5px;
position: relative;
overflow: hidden; //
width: 100%;
height: 228px;
// border: 1px solid #7fd6ff;
// border-radius: 5px;
position: relative;
overflow: hidden; //
//
.carousel-track {
display: flex; //
//
.carousel-track {
display: flex; //
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
//
&.no-transition {
transition: none;
}
//
.carousel-item {
min-width: 100%; //
height: 100%;
position: relative;
flex-shrink: 0; //
cursor: pointer;
img,
video {
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
object-fit: cover; //
}
//
&.no-transition {
transition: none;
}
//
.carousel-item {
min-width: 100%; //
height: 100%;
position: relative;
flex-shrink: 0; //
img,
video {
width: 100%;
height: 100%;
object-fit: cover; //
}
.text {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 22px;
line-height: 22px;
background: rgba(0, 0, 0, 0.1);
color: #fff;
padding-left: 10px;
}
}
}
//
.pagination-dots-fixed {
.text {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10; //
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #D8D8D8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
bottom: 0;
left: 0;
width: 100%;
height: 22px;
line-height: 22px;
background: rgba(0, 0, 0, 0.1);
color: #fff;
padding-left: 10px;
}
}
}
//
.pagination-dots-fixed {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10; //
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #d8d8d8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
}
}
//
.description-text {
// padding: 8px 12px;
font-size: 14px;
line-height: 1.5;
// min-height: 40px;
transition: all 0.3s ease;
margin-bottom: 20px;
// padding: 8px 12px;
font-size: 14px;
line-height: 1.5;
// min-height: 40px;
transition: all 0.3s ease;
margin-bottom: 20px;
cursor: pointer;
//
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
//
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
</style>

View File

@ -1,56 +1,64 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="动物救助情况">
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 跑马灯轨道容器 -->
<div class="carousel-track" :class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }">
<!-- 遍历所有媒体项包含克隆项 -->
<div v-for="(item, index) in renderMediaData" :key="index" class="carousel-item">
<!-- 图片 -->
<a-image :src="item.url" />
<!-- 说明文字 -->
<div class="text">{{ item.text }}</div>
</div>
</div>
<SidePanelItem title="动物救助情况">
<!-- Loading 状态 -->
<div v-if="loading" class="loading-container">
<a-spin />
</div>
<!-- 暂无数据 -->
<div v-else-if="originalMediaData.length === 0" class="empty-container">
<a-empty description="暂无数据" />
</div>
<div
v-else
class="container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<!-- 跑马灯轨道容器 -->
<div
class="carousel-track"
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<!-- 遍历所有媒体项包含克隆项 -->
<div
v-for="(item, index) in renderMediaData"
:key="index"
class="carousel-item"
>
<!-- 图片 -->
<a-image :src="item.url" />
<!-- 说明文字 -->
<div class="text">{{ item.text }}</div>
</div>
</SidePanelItem>
</div>
</div>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { wvaGetKendoListCust } from '@/api/zxdzw';
// 便
defineOptions({
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
});
//
interface MediaItem {
type: 'image' | 'video';
url: string;
text: string;
url: string;
text: string;
}
// 3
const originalMediaData = ref<MediaItem[]>([
{
type: 'image',
url: 'https://211.99.26.225:12125/?20250709001636453909987812803850&view=jpg&token=bearer 0a2549fe-9bc6-4bef-b098-1344e569c395',
text: '糯扎渡动物救护站 2025-04-11 15'
},
{
type: 'image',
url: 'https://211.99.26.225:12125/?20250508094042468361071230016027&view=jpg&token=bearer fee092f0-b748-4019-bcbb-1386fa8c4fa4', // URL
text: '野生动物救助中心 2025-04-12 10'
},
{
type: 'image',
url: 'https://211.99.26.225:12125/?20250709001636453909987812803850&view=jpg&token=bearer 0a2549fe-9bc6-4bef-b098-1344e569c395',
text: '珍稀动物保护基地 2025-04-13 14'
}
]);
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
const loading = ref(false);
//
const originalMediaData = ref<MediaItem[]>([]);
//
// [, ..., ]
@ -70,139 +78,214 @@ const isTransitioning = ref(false);
//
const initRenderData = () => {
const length = originalMediaData.value.length;
if (length === 0) return;
const length = originalMediaData.value.length;
if (length === 0) return;
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
};
//
const startAutoPlay = () => {
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 2000);
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 2000);
};
//
const nextSlide = () => {
currentIndex.value++;
currentIndex.value++;
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
};
//
const checkSeamlessJump = () => {
const realLength = originalMediaData.value.length;
const realLength = originalMediaData.value.length;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// 2. 1
currentIndex.value = 1;
// 2. 1
currentIndex.value = 1;
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
};
//
const handleMouseEnter = () => {
isHovering.value = true;
isHovering.value = true;
};
//
const handleMouseLeave = () => {
isHovering.value = false;
isHovering.value = false;
};
const getData = async () => {
loading.value = true;
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'recordUser',
operator: 'eq',
dataType: 'string',
value: '1'
},
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
}
: null
].filter(Boolean)
}
};
const res = await wvaGetKendoListCust(params);
if (res?.data?.data?.length > 0) {
// imgPath null
const filteredData = res.data.data
originalMediaData.value = filteredData.map((item: any) => {
return {
url: `${baseUrl}?${item.fid}&view=jpg`,
text: item.flnm
};
});
} else {
originalMediaData.value = [];
}
loading.value = false;
};
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
//
watch(originalMediaData, () => {
initRenderData();
currentIndex.value = 1; //
});
//
onMounted(() => {
initRenderData();
startAutoPlay();
initRenderData();
startAutoPlay();
});
//
onUnmounted(() => {
if (timer) clearInterval(timer);
if (timer) clearInterval(timer);
});
</script>
<style lang="scss" scoped>
.container {
// Loading
.loading-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height:290px;
}
//
.empty-container {
display: flex;
align-items: center;
justify-content: center;
width: 100%;
height: 290px;
border: 1px solid #7fd6ff;
border-radius: 5px;
position: relative;
overflow: hidden; //
}
.container {
width: 100%;
height: 290px;
border: 1px solid #7fd6ff;
border-radius: 5px;
position: relative;
overflow: hidden; //
//
.carousel-track {
display: flex; //
//
.carousel-track {
display: flex; //
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
//
&.no-transition {
transition: none;
}
//
.carousel-item {
width: 100% !important; //
height: 100%;
position: relative;
flex-shrink: 0; //
img,
video {
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
object-fit: cover; //
}
//
&.no-transition {
transition: none;
// Ant Design Vue a-image
:deep(.ant-image) {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
//
.carousel-item {
width: 100% !important; //
height: 100%;
position: relative;
flex-shrink: 0; //
img,
video {
width: 100%;
height: 100%;
object-fit: cover; //
}
// Ant Design Vue a-image
:deep(.ant-image) {
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
}
.text {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
align-items: center;
line-height: 40px;
background: rgba(0, 0, 0, 0.4);
color: #fff;
padding-left: 10px;
}
}
.text {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 40px;
align-items: center;
line-height: 40px;
background: rgba(0, 0, 0, 0.4);
color: #fff;
padding-left: 10px;
}
}
}
}
:deep(.ant-empty-description){
text-indent:0em !important;
}
</style>

View File

@ -0,0 +1,293 @@
<template>
<div class="zwy-detail-table">
<!-- 搜索表单 -->
<a-form layout="inline" class="search-form">
<a-form-item label="所属基地">
<a-select
v-model:value="jidiValue"
placeholder="请选择"
style="width: 150px"
:options="jidiOptions"
/>
</a-form-item>
<a-form-item label="建设状态">
<a-select
v-model:value="buildStatus"
placeholder="请选择"
style="width: 120px"
:options="buildStatusOptions"
/>
</a-form-item>
<a-form-item label="">
<a-input
v-model:value="searchInput"
placeholder="请输入设施名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 表格 -->
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll-y="500"
:scroll-x="800"
:list-url="listUrl"
:search-params="searchParams"
:transform-data="customTransform"
>
<template #stnm="{ record }">
<!-- 操作列 -->
<a @click="handleViewDetail(record, 'VP')" class="text-link">
{{ record.stnm }}
</a>
</template>
<template #ennm="{ record }">
<!-- 操作列 -->
<a @click="handleViewDetail(record, 'ENG')" class="text-link">
{{ record.ennm }}
</a>
</template>
</BasicTable>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, onMounted } from 'vue';
import BasicTable from '@/components/BasicTable/index.vue';
import { vmsstbprptGetKendoList } from '@/api/zxdzw';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
// ==================== Props ====================
const props = defineProps<{
jidi?: string; // ID
jidiList?: Array<{ label: string; value: string }>; //
}>();
// ==================== ====================
const jidiValue = ref(props.jidi || 'all');
const buildStatus = ref(''); //
const searchInput = ref('');
//
const buildStatusOptions = [
{ label: '全部', value: '' },
{ label: '未建', value: '0' },
{ label: '在建', value: '1' },
{ label: '已建', value: '2' }
];
//
const jidiOptions = computed(() => {
if (props.jidiList && props.jidiList.length > 0) {
return [{ label: '全部', value: 'all' }, ...props.jidiList];
}
return [{ label: '全部', value: 'all' }];
});
// ==================== URL ====================
const listUrl = vmsstbprptGetKendoList;
// ==================== ====================
const searchParams = computed(() => ({
sort: [
{
"field": "rvcdStepSort",
"dir": "asc"
},
{
"field": "rstcdStepSort",
"dir": "asc"
}
],
group: [],
select: [],
groupResultFlat: false
}));
// ==================== ====================
const columns = [
{
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
fixed: 'left',
width: 120,
ellipsis: true,
slots: { customRender: 'stnm' }
},
{
key: 'baseName',
title: '所在基地',
dataIndex: 'baseName',
fixed: 'left',
width: 120,
ellipsis: true,
merge: true
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
width: 120,
ellipsis: true,
slots: { customRender: 'ennm' }
},
{
key: 'bldsttCcodeName',
title: '建设状态',
dataIndex: 'bldsttCcodeName',
width: 120,
ellipsis: true
},
{
key: 'ststdt',
title: '开工日期',
dataIndex: 'ststdt',
width: 120,
ellipsis: true
},
{
key: 'jcdt',
title: '建成日期',
dataIndex: 'jcdt',
width: 120,
ellipsis: true
},
{
key: 'dtinName',
title: '接入状态',
dataIndex: 'dtinName',
width: 120,
ellipsis: true
}
];
// ==================== ====================
const tableRef = ref();
// ==================== ====================
const getFilter = () => {
const filters: any[] = [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'VP'
}
];
//
if (jidiValue.value && jidiValue.value !== 'all') {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: jidiValue.value
});
}
//
if (buildStatus.value !== '') {
filters.push({
field: 'bldsttCcode',
operator: 'eq',
dataType: 'string',
value: buildStatus.value
});
}
//
if (searchInput.value) {
filters.push({
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: searchInput.value
});
}
return {
logic: 'and',
filters
};
};
// ==================== ====================
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
// ==================== ====================
const handleSearch = () => {
const filter = getFilter();
tableRef.value?.getList(filter);
};
const handleReset = () => {
jidiValue.value = props.jidi || 'all';
buildStatus.value = '';
searchInput.value = '';
handleSearch();
};
//
const handleViewDetail = (record: any, type: any) => {
modelStore.modalVisible = true;
modelStore.params = record;
modelStore.title = type == 'ENG' ? record.ennm : record.stnm;
modelStore.params.sttp = type;
};
// ==================== jidi prop ====================
watch(
() => props.jidi,
newVal => {
if (newVal) {
jidiValue.value = newVal;
handleSearch();
}
},
{ immediate: false }
);
// ==================== ====================
onMounted(() => {
handleSearch();
});
</script>
<style lang="scss" scoped>
.zwy-detail-table {
width: 100%;
.search-form {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
.table-wrapper {
width: 100%;
}
}
.text-link {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
</style>

View File

@ -1,135 +1,275 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="植物园情况">
<div ref="chartRef" class="chart-container"></div>
</SidePanelItem>
<SidePanelItem title="植物园情况">
<a-spin :spinning="loading" tip="加载中...">
<div class="chart-wrapper">
<div ref="chartRef" class="chart-container" v-show="chartData.length > 0"></div>
<a-empty v-show="chartData.length === 0" description="暂无数据" />
</div>
</a-spin>
<!-- 详情弹窗 -->
<a-modal
v-model:open="modalVisible"
title="植物园建设情况"
width="1536px"
:footer="null"
>
<ZhiWuYuanDetailTable
v-if="modalVisible"
:jidi="modalJidi"
:jidi-list="jidiOptions"
/>
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted } from 'vue';
import { ref, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { vpcGetKendoListCust } from '@/api/zxdzw';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import ZhiWuYuanDetailTable from './ZhiWuYuanDetailTable.vue';
// 便
defineOptions({
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const chartRef = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
//
const jdColor = ['#5470c6', '#91cc75', '#fac858', '#ee6666', '#73c0de', '#3ba272', '#fc8452', '#9a60b4'];
//
const chartData = ref<any[]>([]);
//
const barData = [
{ basename: '栖息地A', count: 45 },
{ basename: '栖息地B', count: 32 },
{ basename: '栖息地C', count: 68 },
{ basename: '栖息地D', count: 25 },
{ basename: '栖息地E', count: 53 },
{ basename: '栖息地F', count: 41 }
// loading
const loading = ref(false);
//
const modalVisible = ref(false);
const modalJidi = ref('all');
//
const jidiOptions = ref<any[]>([]);
//
const jdColor = [
'#5470c6',
'#91cc75',
'#fac858',
'#ee6666',
'#73c0de',
'#3ba272',
'#fc8452',
'#9a60b4'
];
//
const initChart = () => {
if (!chartRef.value) return;
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
} else {
chartInstance.resize();
}
const option: any = {
color: jdColor,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
textStyle: {
fontSize: 12
},
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: barData.map((v: any) => v.basename),
axisLabel: {
rotate: 30, //
interval: 0 //
},
axisTick: {
alignWithLabel: true //
}
},
yAxis: {
type: 'value',
name: '数量(个)',
splitLine: {
show: true,
lineStyle: {
color: '#eee',
type: 'dashed'
}
},
axisLine: {
show: true
},
axisLabel: {
formatter: '{value}'
}
},
series: [
{
type: 'bar',
barWidth: '46%',
barGap: '60%',
data: barData.map(item => item.count),
label: {
show: true,
fontSize: 12,
position: 'top',
formatter: (params: any) => {
return Math.round(params.value);
}
},
}
]
};
const option: any = {
color: jdColor,
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
},
textStyle: {
fontSize: 12
}
},
grid: {
left: '3%',
right: '4%',
bottom: '3%',
containLabel: true
},
xAxis: {
type: 'category',
data: chartData.value.map((v: any) => v.basename),
axisLabel: {
rotate: 30, //
interval: 0 //
},
axisTick: {
alignWithLabel: true //
}
},
yAxis: {
type: 'value',
name: '数量(个)',
splitLine: {
show: true,
lineStyle: {
color: '#eee',
type: 'dashed'
}
},
axisLine: {
show: true
},
axisLabel: {
formatter: '{value}'
}
},
series: [
{
type: 'bar',
barWidth: '46%',
barGap: '60%',
data: chartData.value.map(item => item.count),
label: {
show: true,
fontSize: 12,
position: 'top',
formatter: (params: any) => {
return Math.round(params.value);
}
}
}
]
};
chartInstance.setOption(option);
chartInstance.setOption(option);
//
chartInstance.on('click', handleChartClick);
};
//
const handleChartClick = (params: any) => {
//
if (params.componentType !== 'series') return;
//
const jidiItem = JidiSelectEventStore.jidiData.find(
(item: any) => item.wbsName === params.name
);
modalJidi.value = jidiItem?.wbsCode || 'all';
modalVisible.value = true;
};
const getData = async () => {
loading.value = true;
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'isNationwide',
operator: 'eq',
dataType: 'string',
value: 1
},
{
field: 'isBotanicalGarden',
operator: 'eq',
dataType: 'string',
value: 1
}
]
},
group: [
{
dir: 'asc',
field: 'bldstt'
},
{
dir: 'asc',
field: 'dtin'
},
{
dir: 'asc',
field: 'baseId'
}
]
};
try {
let res = await vpcGetKendoListCust(params);
//
const apiData = res?.data?.data?.[0]?.items || [];
//
const baseCountMap: Record<string, number> = {};
apiData.forEach((dtinGroup: any) => {
dtinGroup.items?.forEach((baseItem: any) => {
const baseId = baseItem.key;
const count = baseItem.count || 0;
baseCountMap[baseId] = (baseCountMap[baseId] || 0) + count;
});
});
// jidiData 'all'
chartData.value = JidiSelectEventStore.jidiData
.filter((item: any) => item.wbsCode !== 'all' && baseCountMap[item.wbsCode])
.map((item: any) => ({
basename: item.wbsName,
count: baseCountMap[item.wbsCode]
}));
//
nextTick(() => {
initChart();
});
} catch (error) {
chartData.value = [];
} finally {
loading.value = false;
}
};
//
onMounted(() => {
//
setTimeout(() => {
initChart();
}, 50);
//
jidiOptions.value = JidiSelectEventStore.jidiData
.filter((item: any) => item.wbsCode !== 'all')
.map((item: any) => ({
label: item.wbsName,
value: item.wbsCode
}));
//
window.addEventListener('resize', handleResize);
getData();
//
window.addEventListener('resize', handleResize);
});
//
const handleResize = () => {
chartInstance?.resize();
chartInstance?.resize();
};
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
});
</script>
<style lang="scss" scoped>
.chart-wrapper {
position: relative;
width: 100%;
height: 310px !important;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 310px;
width: 100%;
height: 100%;
display: block;
box-sizing: border-box;
}
</style>

View File

@ -4,13 +4,9 @@
:datetimePicker="datetimePicker"
@update-values="handlePanelChange"
>
<a-spin :spinning="loading">
<div v-show="!sourceData.length && !loading" class="empty-wrapper">
<a-empty description="暂无数据" />
</div>
<div v-show="sourceData.length" class="chart-wrapper">
<div ref="chartContainer" class="chart-container"></div>
</div>
<a-spin :spinning="loading" style="width: 100%;">
<div ref="chartContainer" class="chart-container" v-show="sourceData.length > 0"></div>
<a-empty v-show="sourceData.length === 0" description="暂无数据" class="empty-container" />
</a-spin>
</SidePanelItem>
</template>
@ -18,6 +14,7 @@
<script lang="ts" setup>
import { ref, computed, watch, onMounted, onUnmounted, nextTick } from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import type { EChartsOption } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { sdFprdRGetFtpChangeInfo } from '@/api/yldc';
@ -50,8 +47,7 @@ const loading = ref(false);
//
const chartContainer = ref<HTMLDivElement | null>(null);
let chartInstance: echarts.ECharts | null = null;
let resizeObserver: ResizeObserver | null = null;
let chartInstance: ECharts | null = null;
// 1
function getMonthsUntilNow(year: number) {
@ -119,10 +115,10 @@ const option = computed<EChartsOption>(() => {
textStyle: { fontSize: baseFontSize }
},
grid: {
left: '50px',
right: '60px',
top: '45px',
bottom: '40px'
left: 50,
right: 60,
top: 45,
bottom: 40
},
xAxis: {
type: 'category',
@ -220,6 +216,11 @@ const getData = async () => {
} finally {
loading.value = false;
}
nextTick(() => {
if (sourceData.value.length > 0) {
initChart();
}
});
};
//
@ -252,9 +253,7 @@ const handlePanelChange = (data: any) => {
//
const handleResize = () => {
if (chartInstance) {
chartInstance.resize();
}
chartInstance?.resize();
};
let baseid = ref('');
watch(
@ -266,73 +265,39 @@ watch(
},
{ deep: true, immediate: true }
);
onMounted(() => {
nextTick(() => {
//
setTimeout(() => {
if (chartContainer.value) {
chartInstance = echarts.init(chartContainer.value);
chartInstance.setOption(option.value);
}
//
window.addEventListener('resize', handleResize);
}, 50);
});
getData();
window.addEventListener('resize', handleResize);
});
onUnmounted(() => {
if (chartInstance) {
chartInstance.dispose();
chartInstance = null;
}
if (resizeObserver) {
resizeObserver.disconnect();
}
window.removeEventListener('resize', handleResize);
});
//
watch(sourceData, newData => {
if (newData && newData.length > 0) {
// observer
if (resizeObserver) {
resizeObserver.disconnect();
}
// 使 ResizeObserver
resizeObserver = new ResizeObserver(entries => {
for (const entry of entries) {
const { width, height } = entry.contentRect;
if (width > 0 && height > 0) {
nextTick(() => {
if (!chartInstance && chartContainer.value) {
initChart();
} else {
updateChart();
}
});
resizeObserver?.disconnect();
resizeObserver = null;
}
}
});
nextTick(() => {
if (chartContainer.value) {
resizeObserver.observe(chartContainer.value);
}
});
}
chartInstance?.dispose();
chartInstance = null;
});
</script>
<style lang="scss" scoped>
.empty-wrapper {
display: flex;
align-items: center;
justify-content: center;
height: 200px;
}
.chart-wrapper {
margin: 0;
width: 100%;
position: relative;
}
.chart-container {
width: 100%;
height: 200px;
min-height: 200px;
}
.empty-container {
display: flex;
justify-content: center;
align-items: center;
height: 200px;
}
</style>

View File

@ -0,0 +1,588 @@
<template>
<div class="zengzhi-zhan-qingkuang-two-layers">
<!-- 搜索表单 -->
<a-form layout="inline" class="search-form">
<a-form-item label="年份">
<a-date-picker
v-model:value="years"
picker="year"
:allow-clear="false"
format="YYYY"
value-format="YYYY"
/>
</a-form-item>
<!-- baseId 'all' 时显示基地选择否则显示设施名称 -->
<a-form-item v-if="baseId === 'all'" label="所属基地">
<a-select
v-model:value="dataDimensionVal"
placeholder="请选择基地"
style="width: 180px"
>
<a-select-option
v-for="item in datas"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else label="设施名称">
<a-select
v-model:value="dzstcd"
placeholder="请选择设施"
allow-clear
style="width: 180px"
>
<a-select-option
v-for="item in DZOptions"
:key="item.value"
:value="item.value"
>
{{ item.label }}
</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="运行状态">
<a-select
v-model:value="runState"
placeholder="请选择状态"
style="width: 160px"
>
<a-select-option value="">全部</a-select-option>
<a-select-option value="1">正常运行</a-select-option>
<a-select-option value="3">未正常运行</a-select-option>
<a-select-option value="2">暂无数据</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="">
<a-input
v-model:value="search"
placeholder="请输入电站或设施名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 表格 -->
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll-y="500"
:scroll-x="1000"
:list-url="listUrl"
:search-params="searchParams"
:transform-data="customTransform"
>
<template #ennm="{ record }">
<template v-if="record.ennm && record.rstcd">
<span
v-for="(item, index) in parseStationList(record)"
:key="index"
class="station-item"
>
<a @click="handleViewDetail(item, 'eng')" class="text-link">{{
item.name
}}</a>
</span>
</template>
<span v-else>{{ record.ennm || '-' }}</span>
</template>
<template #action="{ record }">
<a @click="handleViewDetail(record, 'FB')" class="text-link"
>查看详情</a
>
</template>
</BasicTable>
</div>
<!-- 嵌套详情弹窗 - 后续补充 -->
<!-- <a-modal v-model:open="modalVisible" :title="modalTitle" width="800px" :footer="null">
详情内容
</a-modal> -->
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, h, onMounted } from 'vue';
import { msfbrdmGetKendoListCust, fishTypegetStcdList } from '@/api/zzfl';
import BasicTable from '@/components/BasicTable/index.vue';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
//
const unitVal = '万尾';
//
const transUnitRender = (val: any): string | number => {
if (val === null || val === undefined || val === '') return '-';
const num = Number(val);
if (isNaN(num) || num < 0) return '-';
//
return num / 10000;
};
// ==================== Props ====================
const props = defineProps<{
datas?: any[]; //
nian?: string; // YYYY
data?: any; //
baseId?: string; // ID
selectData?: any; //
}>();
// ==================== ====================
const years = ref(props.nian || `${new Date().getFullYear()}`);
const dataDimensionVal = ref(props.baseId || 'all');
const runState = ref(props.data?.dictValue ?? '');
const search = ref('');
const dzstcd = ref(props.selectData?.value ?? '');
const DZOptions = ref<any[]>([]);
// ==================== URL ====================
const listUrl = msfbrdmGetKendoListCust;
// ==================== ====================
const unit = unitVal;
const columns = computed(() => [
// {
// key: 'stcd',
// title: '',
// dataIndex: 'stcd',
// visible: false,
// width: 100
// },
// {
// key: 'sttpName',
// title: '',
// dataIndex: 'sttpName',
// visible: false,
// width: 100
// },
{
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
fixed: 'left',
width: 120,
ellipsis: true,
merge: true
},
{
key: 'ennm',
title: '所属电站',
dataIndex: 'ennm',
// width: 180,
ellipsis: true,
merge: true,
slots: { customRender: 'ennm' }
},
{
key: 'baseName',
title: '所在基地',
dataIndex: 'baseName',
width: 120,
ellipsis: true,
merge: true
},
{
key: 'ftpName',
title: '养殖鱼类',
dataIndex: 'ftpName',
width: 120,
ellipsis: true
},
{
key: 'fcntjh',
title: `计划养殖规模(${unit})`,
dataIndex: 'fcntjh',
width: 160,
sort: true,
customRender: ({ text, record }: { text: number; record: any }) => {
const rendered = transUnitRender(text);
const displayVal =
typeof rendered === 'number' ? rendered.toFixed(4) : rendered;
if (typeof rendered === 'number' && rendered > 0) {
return h('div', { style: { color: 'red' } }, displayVal);
}
return h('div', {}, displayVal);
}
},
{
key: 'fcntjc',
title: '实际完成数量(万尾)',
dataIndex: 'fcntjc',
width: 160,
sort: true,
customRender: ({ value }: { value: number }) => {
const rendered = transUnitRender(value);
const displayVal =
typeof rendered === 'number' ? rendered.toFixed(4) : rendered;
if (typeof rendered === 'number' && rendered > 0) {
return h('div', { style: { color: 'red' } }, displayVal);
}
return h('div', {}, displayVal);
}
},
{
key: 'runStateName',
title: '运行状态',
dataIndex: 'runStateName',
width: 120,
ellipsis: true,
merge: true
},
{
title: '操作',
key: 'action',
dataIndex: 'action',
width: 100,
fixed: 'right',
merge: true,
slots: { customRender: 'action' }
}
]);
// ==================== ====================
const tableRef = ref();
const searchParams = computed(() => {
const startTime = `${years.value}-01-01 00:00:00`;
const endTime = `${years.value}-12-31 23:59:59`;
const filters: any[] = [
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
}
];
if (props.baseId === 'all') {
filters.push({
field: 'dtin',
operator: 'eq',
dataType: 'string',
value: 1
});
if (dataDimensionVal.value !== 'all') {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: dataDimensionVal.value
});
}
} else {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: props.baseId
});
if (dzstcd.value) {
filters.push({
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: dzstcd.value
});
}
}
if (search.value) {
filters.push({
field: 'ennm',
operator: 'contains',
dataType: 'string',
value: search.value
});
}
if (!runState.value) {
filters.push({
field: 'runState',
operator: 'in',
dataType: 'string',
value: ['1', '2', '3']
});
} else {
filters.push({
field: 'runState',
operator: 'eq',
dataType: 'string',
value: runState.value
});
}
return {
sort: [],
group: [],
select: [],
groupResultFlat: false
};
});
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
// ==================== ====================
const getFilter = () => {
const startTime = `${years.value}-01-01 00:00:00`;
const endTime = `${years.value}-12-31 23:59:59`;
const filters: any[] = [
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: startTime
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: endTime
}
];
if (props.baseId === 'all') {
filters.push({
field: 'dtin',
operator: 'eq',
dataType: 'string',
value: 1
});
if (dataDimensionVal.value !== 'all') {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: dataDimensionVal.value
});
}
} else {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: props.baseId
});
if (dzstcd.value) {
filters.push({
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: dzstcd.value
});
}
}
if (search.value) {
filters.push({
field: 'ennm',
operator: 'contains',
dataType: 'string',
value: search.value
});
}
if (!runState.value) {
filters.push({
field: 'runState',
operator: 'in',
dataType: 'string',
value: ['1', '2', '3']
});
} else {
filters.push({
field: 'runState',
operator: 'eq',
dataType: 'string',
value: runState.value
});
}
return {
logic: 'and',
filters
};
};
const handleSearch = () => {
const filter = getFilter();
tableRef.value?.getList(filter);
};
const initialState = {
years: `${new Date().getFullYear()}`,
dataDimensionVal: props.baseId || 'all',
runState: props.data?.dictValue ?? '',
search: '',
dzstcd: props.selectData?.value ?? ''
};
const handleReset = () => {
years.value = initialState.years;
dataDimensionVal.value = initialState.dataDimensionVal;
runState.value = initialState.runState;
search.value = initialState.search;
dzstcd.value = initialState.dzstcd;
handleSearch();
};
// ==================== ====================
const fetchDZOptions = async () => {
if (props.baseId === 'all') return;
try {
const params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: props.baseId
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'FB'
}
]
}
};
const res = await fishTypegetStcdList(params);
if (res?.data?.data) {
DZOptions.value = res.data.data.map((item: any) => ({
value: item.stcd,
label: item.stnm
}));
}
} catch (error) {
console.error('获取设施选项失败:', error);
}
};
// ==================== ====================
const handleViewDetail = (record: any, type: any) => {
//
if (type == 'FB') {
modelStore.modalVisible = true;
modelStore.params = record;
modelStore.title = record.stnm;
modelStore.params.sttp = 'FB';
modelStore.filter.year = years.value;
} else {
// debugger
modelStore.modalVisible = true;
modelStore.params.sttp = 'ENG';
modelStore.title = record.name;
modelStore.params.stcd = record.code;
}
};
// ennm rstcd
const parseStationList = (record: any) => {
const names = (record.ennm || '').split(',').filter(Boolean);
const codes = (record.rstcd || '').split(',').filter(Boolean);
return names.map((name, index) => ({
name,
code: codes[index] || '',
baseId: record.baseId,
stcd: record.stcd,
stnm: record.stnm
}));
};
const handleViewStationDetail = (station: any) => {
//
console.log('查看电站详情:', station);
// 使 station.code (rstcd)
};
// ==================== props ====================
watch(
() => props.nian,
newVal => {
if (newVal) {
years.value = newVal;
handleSearch();
}
},
{ immediate: true }
);
watch(
() => props.baseId,
newVal => {
if (newVal) {
dataDimensionVal.value = newVal;
fetchDZOptions();
handleSearch();
}
},
{ immediate: true }
);
onMounted(() => {
fetchDZOptions();
// DOM
setTimeout(() => {
handleSearch();
}, 100);
});
</script>
<style lang="scss" scoped>
.zengzhi-zhan-qingkuang-two-layers {
width: 100%;
.search-form {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
.table-wrapper {
width: 100%;
}
}
.text-link {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
.station-item {
&:not(:last-child)::after {
content: ', ';
}
}
</style>

View File

@ -1,339 +1,606 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="增殖计划完成情况" :select="select" :datetimePicker="datetimePicker">
<div ref="chartRef" class="chart-container"></div>
</SidePanelItem>
<SidePanelItem
title="增殖计划完成情况"
:select="select"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange1"
>
<div
v-show="!noData && !loading"
ref="chartRef"
class="chart-container"
></div>
<div v-show="loading" class="center-container">
<a-spin size="large" />
</div>
<div v-show="noData" class="center-container">
<a-empty description="暂无数据" />
</div>
</SidePanelItem>
<!-- 弹窗 -->
<a-modal
v-model:open="dialogVisible"
title="增殖计划完成情况"
width="1536px"
:footer="null"
>
<ZengZhiZhanQingKuangTwoLayers
v-if="dialogVisible"
:selectData="selectedOption"
:baseId="baseid"
:data="dialogData"
:datas="JidiSelectEventStore.jidiData"
:nian="datetimePicker.value"
></ZengZhiZhanQingKuangTwoLayers>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ref, onMounted, onUnmounted, computed, watch, nextTick } from 'vue';
import * as echarts from 'echarts';
import type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import {
fishTypegetStcdList,
fishCountGetKendoListCust,
fishTypeGetKendoListCust
} from '@/api/zzfl';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { MapClass } from '@/components/gis/map.class';
import ZengZhiZhanQingKuangTwoLayers from './TwoLayers/ZengZhiZhanQingKuangTwoLayers.vue';
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
// 便
defineOptions({
name: 'zengZhiJiHuaWanChengQingKuang'
name: 'zengZhiJiHuaWanChengQingKuang'
});
//
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
//
const dialogVisible = ref(false);
const dialogData = ref<any>(null);
//
const selectedOption = computed(() => {
return select.value.options.find(
(item: any) => item.value === select.value.value
);
});
//
const select = ref({
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined
show: true,
value: undefined,
options: [],
picker: undefined,
format: undefined,
width: '120px'
});
const datetimePicker = ref({
show: true,
value: `${new Date().getFullYear() }`,
format: 'YYYY', // YYYY-MM-DD HH
picker: 'year' as const, // date | week | month | quarter | year
options: []
show: true,
value: `${new Date().getFullYear()}`,
format: 'YYYY', // YYYY-MM-DD HH
picker: 'year' as const, // date | week | month | quarter | year
options: []
});
// - API
const wangCheng = ref([
{ unfinished: 35.2468, finished: 64.7532 }
]);
const zhongLei = ref([
{ unfinished: 3, finished: 7 }
]);
const jihua = ref([10, 20, 30, 40]);
//
const wangCheng = ref([]);
const zhongLei = ref([]);
const jihua = ref([]);
const loading = ref(false);
const noData = ref(false);
const unit = ref('万尾');
// filterNumberByConfig
const filterNumberByConfig = (value: number, config: any) => {
if (value < 0) return 0;
return value;
if (value < 0) return 0;
return value;
};
//
const chartOption = computed(() => {
let wcval = wangCheng.value?.[0]?.unfinished || 0;
let wcval = wangCheng.value?.[0]?.unfinished || 0;
let datapie = [
{
value: filterNumberByConfig(wcval, { tbCode: 'Other', ys: 'SL' }),
name: "未完成数量"
let datapie = [
{
value: filterNumberByConfig(wcval, { tbCode: 'Other', ys: 'SL' }),
name: '未完成数量'
},
{
value: filterNumberByConfig(wangCheng.value?.[0]?.finished || 0, {
tbCode: 'Other',
ys: 'SL'
}),
name: '已完成数量'
}
];
let datas = [
{ value: zhongLei.value?.[0]?.unfinished || 0, name: '未完成种类' },
{ value: zhongLei.value?.[0]?.finished || 0, name: '已完成种类' }
];
const wcStr = jihua.value
.reduce((pre: any, i: any) => pre + i, 0)
?.toFixed(4);
const zlStr = datas.reduce((pre, i: any) => pre + i.value, 0);
// 线
const isShowdatas =
datas.filter((i: any) => i.value).length !== 1
? {
itemStyle: {
normal: {
borderWidth: 3,
borderColor: '#fff'
}
}
}
: {};
// 线
const isShowdatapie =
datapie.filter((i: any) => i.value).length !== 1
? {
itemStyle: {
normal: {
borderWidth: 3,
borderColor: '#fff'
}
}
}
: {};
const textStyle = {
rich: {
a: {
color: '#2F6B98',
fontSize: 18,
height: 30
},
c: {
fontSize: 12,
color: 'rgba(0,0,0)',
padding: [0, 2]
}
}
};
return {
tooltip: {
trigger: 'item'
},
title: [
{
text:
'{a|' +
wcStr +
' ' +
'}\n{c|' +
'计划完成总数' +
'\n\n' +
'(' +
unit.value +
')' +
'}',
x: '108',
y: '98',
textAlign: 'center',
textStyle
},
{
text:
'{a|' +
zlStr +
' ' +
'}\n{c|' +
'计划完成种类' +
'\n\n' +
'(种)' +
'}',
x: '298',
y: '98',
textAlign: 'center',
textStyle
},
{
text: '数量完成情况',
textAlign: 'center',
left: '24%',
top: '5',
textStyle: {
fontSize: '14',
color: 'rgba(0,0,0,0.8)'
}
},
{
textAlign: 'center',
text: '种类完成情况',
left: '71%',
top: '5',
textStyle: {
fontSize: '14',
color: 'rgba(0,0,0,0.8)'
}
}
],
legend: [
{
data: datapie,
bottom: '5%',
x: '10%',
height: 60,
width: 160,
orient: 'vertical',
selectedMode: 'multiple',
textStyle: {
fontSize: 12,
color: ' rgba(0, 0, 0, 0.85)',
rich: {
name: {
verticalAlign: 'right',
align: 'left',
width: 60,
fontSize: 14,
color: ' rgba(0, 0, 0, 0.85)'
},
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: '#2F6B98 ' }
},
borderWidth: 53 //
},
{
value: filterNumberByConfig(wangCheng.value?.[0]?.finished || 0, { tbCode: 'Other', ys: 'SL' }),
name: "已完成数量"
}
];
let datas = [
{ value: zhongLei.value?.[0]?.unfinished || 0, name: "未完成种类" },
{ value: zhongLei.value?.[0]?.finished || 0, name: "已完成种类" }
];
const wcStr = jihua.value.reduce((pre: any, i: any) => pre + i, 0)?.toFixed(4);
const zlStr = datas.reduce((pre, i: any) => pre + i.value, 0);
// 线
const isShowdatas = datas.filter((i: any) => i.value).length !== 1
? {
itemStyle: {
normal: {
borderWidth: 3,
borderColor: "#fff"
}
formatter(name: string) {
let data = datapie;
let value = '';
data.forEach((item, index) => {
if (item.name == name) {
value =
item.value < 0
? ' ' + 0
: item.value < 10
? ' ' + item.value
: (item.value as any);
}
});
return (
'{name|' + name + '}' + '{percent|' + value + '}' + ' ' + unit.value
);
}
: {};
// 线
const isShowdatapie = datapie.filter((i: any) => i.value).length !== 1
? {
itemStyle: {
normal: {
borderWidth: 3,
borderColor: "#fff"
}
}
}
: {};
const textStyle = {
rich: {
a: {
color: "#2F6B98",
fontSize: 18,
height: 30
},
{
data: datas,
bottom: '5%',
left: '232',
height: 60,
width: 200,
orient: 'vertical',
selectedMode: 'multiple',
textStyle: {
fontSize: 12,
color: ' rgba(0, 0, 0, 0.85)',
rich: {
name: {
verticalAlign: 'right',
align: 'left',
width: 70,
fontSize: 14,
color: ' rgba(0, 0, 0, 0.85)'
},
c: {
fontSize: 12,
color: "rgba(0,0,0)",
padding: [0, 2]
}
}
};
return {
tooltip: {
trigger: "item"
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: '#2F6B98 ' }
},
borderWidth: 53 //
},
title: [
{
text: "{a|" + wcStr + " " + "}\n{c|" + "计划完成总数" + "\n\n" + "(" + unit.value + ")" + "}",
x: "108",
y: "98",
textAlign: "center",
textStyle,
},
{
text: "{a|" + zlStr + " " + "}\n{c|" + "计划完成种类" + "\n\n" + "(种)" + "}",
x: "298",
y: "98",
textAlign: "center",
textStyle,
},
{
text: "数量完成情况",
textAlign: "center",
left: "24%",
top: "5",
textStyle: {
fontSize: "14",
color: "rgba(0,0,0,0.8)"
}
},
{
textAlign: "center",
text: "种类完成情况",
left: "71%",
top: "5",
textStyle: {
fontSize: "14",
color: "rgba(0,0,0,0.8)"
}
formatter(name: string) {
let data = datas;
let value = '';
data.forEach((item, index) => {
if (item.name == name) {
value = item.value < 10 ? ' ' + item.value : (item.value as any);
}
],
legend: [
{
data: datapie,
bottom: "5%",
x: "10%",
height: 60,
width: 160,
orient: "vertical",
selectedMode: 'multiple',
textStyle: {
fontSize: 12,
color: " rgba(0, 0, 0, 0.85)",
rich: {
name: {
verticalAlign: "right",
align: "left",
width: 60,
fontSize: 14,
color: " rgba(0, 0, 0, 0.85)"
},
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: "#2F6B98 " }
},
borderWidth: 53 //
},
formatter(name: string) {
let data = datapie;
let value = "";
data.forEach((item, index) => {
if (item.name == name) {
value = item.value < 0 ? " " + 0 : item.value < 10 ? " " + item.value : (item.value as any);
}
});
return "{name|" + name + "}" + "{percent|" + value + "}" + " " + unit.value;
}
},
{
data: datas,
bottom: "5%",
left: "232",
height: 60,
width: 200,
orient: "vertical",
selectedMode: 'multiple',
textStyle: {
fontSize: 12,
color: " rgba(0, 0, 0, 0.85)",
rich: {
name: {
verticalAlign: "right",
align: "left",
width: 70,
fontSize: 14,
color: " rgba(0, 0, 0, 0.85)"
},
percent: { fontSize: 14, padding: [0, 0, 0, 8], color: "#2F6B98 " }
},
borderWidth: 53 //
},
formatter(name: string) {
let data = datas;
let value = "";
data.forEach((item, index) => {
if (item.name == name) {
value = item.value < 10 ? " " + item.value : (item.value as any);
}
});
return "{name|" + name + "}" + "{percent|" + value + "}" + " " + "种";
}
}
],
series: [
{
name: "数量完成情况",
id: "parentpie",
type: "pie",
radius: ["62", "80"],
center: ["110", "130"],
data: datapie,
label: {
show: false
},
...isShowdatapie
},
{
name: "种类完成情况",
id: "pie",
type: "pie",
radius: ["62", "80"],
center: ["300", "130"],
label: {
show: false
},
data: datas,
...isShowdatas
}
]
};
});
return '{name|' + name + '}' + '{percent|' + value + '}' + ' ' + '种';
}
}
],
series: [
{
name: '数量完成情况',
id: 'parentpie',
type: 'pie',
radius: ['62', '80'],
center: ['110', '130'],
data: datapie,
label: {
show: false
},
...isShowdatapie
},
{
name: '种类完成情况',
id: 'pie',
type: 'pie',
radius: ['62', '80'],
center: ['300', '130'],
label: {
show: false
},
data: datas,
...isShowdatas
}
]
};
});
//
const initChart = (retryCount = 0) => {
if (!chartRef.value) {
console.warn('图表容器不存在');
return;
if (!chartRef.value) {
console.warn('图表容器不存在');
return;
}
//
const rect = chartRef.value.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
// 5,
if (retryCount < 5) {
console.warn(`图表容器尺寸无效,延迟重试... (${retryCount + 1}/5)`, {
width: rect.width,
height: rect.height
});
setTimeout(() => {
initChart(retryCount + 1);
}, 100 * Math.pow(2, retryCount)); // 退
} else {
console.error('图表初始化失败:达到最大重试次数');
}
return;
}
//
const rect = chartRef.value.getBoundingClientRect();
if (rect.width === 0 || rect.height === 0) {
// 5,
if (retryCount < 5) {
console.warn(`图表容器尺寸无效,延迟重试... (${retryCount + 1}/5)`, {
width: rect.width,
height: rect.height
});
setTimeout(() => {
initChart(retryCount + 1);
}, 100 * Math.pow(2, retryCount)); // 退
} else {
console.error('图表初始化失败:达到最大重试次数');
}
return;
}
//
if (chartInstance) {
chartInstance.dispose();
}
//
if (chartInstance) {
chartInstance.dispose();
}
//
chartInstance = echarts.init(chartRef.value);
//
chartInstance = echarts.init(chartRef.value);
//
chartInstance.setOption(chartOption.value);
//
chartInstance.setOption(chartOption.value);
//
bindChartClick();
console.log('图表初始化成功');
console.log('图表初始化成功');
};
//
const updateChart = () => {
if (!chartInstance) return;
if (!chartInstance) return;
// 使
chartInstance.setOption(chartOption.value, true);
// 使
chartInstance.setOption(chartOption.value, true);
};
//
const handleResize = () => {
chartInstance?.resize();
chartInstance?.resize();
};
//
const handleChartClick = (params: any) => {
dialogData.value = params;
dialogVisible.value = true;
};
//
const bindChartClick = () => {
if (chartInstance) {
chartInstance.off('click');
chartInstance.on('click', handleChartClick);
}
};
const getSelectData = async () => {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
},
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'FB'
}
]
}
};
const res = await fishTypegetStcdList(params);
if (res.data.data.length == 0) {
noData.value = true;
return;
}
select.value.options = res.data.data.map(item => {
return {
value: item.stcd,
label: item.stnm,
lgtd: item.lgtd,
lttd: item.lttd
};
});
select.value.value = select.value.options[0].value;
// ,
getEcharts();
// debugger;
};
const getEcharts = async () => {
loading.value = true;
noData.value = false;
// , loading
if (chartInstance) {
chartInstance.clear();
}
let parmas = {
filter: {
logic: 'and',
filters: [
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
},
{
field: 'stcd',
operator: 'eq',
dataType: 'string',
value: select.value.value
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: datetimePicker.value.value + '-12-31 23:59:59'
},
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: datetimePicker.value.value + '-01-01 00:00:00'
}
]
}
};
try {
const res = await fishCountGetKendoListCust(parmas);
const res2 = await fishTypeGetKendoListCust(parmas);
// ()
if (res.data.data?.length > 0) {
const countData = res.data.data[0];
wangCheng.value = [
{
finished: countData.finished / 10000,
unfinished: countData.unfinished / 10000
}
];
//
jihua.value = [countData.finished / 10000, countData.unfinished / 10000];
} else {
wangCheng.value = [];
jihua.value = [];
}
//
if (res2.data.data?.length > 0) {
const typeData = res2.data.data[0];
zhongLei.value = [
{
finished: typeData.finished,
unfinished: typeData.unfinished
}
];
} else {
zhongLei.value = [];
}
//
noData.value = wangCheng.value.length === 0 && zhongLei.value.length === 0;
console.log('图表数据:', noData.value);
// debugger;
//
if (!noData.value) {
updateChart();
} else {
chartInstance?.clear();
}
} catch (error) {
console.error('获取图表数据失败:', error);
noData.value = true;
} finally {
loading.value = false;
}
};
//
onMounted(() => {
// 使 nextTick + setTimeout DOM
// 使 nextTick + setTimeout DOM
setTimeout(() => {
initChart();
// resize
setTimeout(() => {
initChart();
// resize
setTimeout(() => {
chartInstance?.resize();
}, 50);
chartInstance?.resize();
}, 50);
}, 50);
window.addEventListener('resize', handleResize);
window.addEventListener('resize', handleResize);
});
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
});
//
const handlePanelChange1 = async data => {
console.log('当前所有控件状态:', data);
// debugger;
// ,
if (data.datetime && data.select) {
await (datetimePicker.value.value = data.datetime);
select.value.value = data.select;
getEcharts();
}
if (data?.selectedNodeExtra?.lttd && data?.selectedNodeExtra?.lgtd) {
const mapClass = MapClass.getInstance();
mapClass.flyTopanto(
[
Number(data.selectedNodeExtra.lgtd),
Number(data.selectedNodeExtra.lttd)
],
12
);
}
};
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getSelectData();
}
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.chart-container {
width: 100%;
height: 280px;
min-height: 280px;
width: 100%;
height: 280px;
min-height: 280px;
}
.center-container {
width: 100%;
height: 280px;
min-height: 280px;
display: flex;
align-items: center;
justify-content: center;
}
</style>

View File

@ -1,76 +1,92 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="增殖站介绍">
<div class="container" @mouseenter="handleMouseEnter" @mouseleave="handleMouseLeave">
<!-- 跑马灯轨道容器 -->
<div class="carousel-track" :class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }">
<!-- 遍历所有媒体项包含克隆项 -->
<div v-for="(item, index) in renderMediaData" :key="index" class="carousel-item">
<!-- 图片 -->
<img :src="item.url" alt="" />
<!-- 说明文字随媒体项移动 -->
<div class="text">{{ item.text }}</div>
</div>
<SidePanelItem title="增殖站介绍">
<a-spin :spinning="loading">
<template v-if="originalMediaData.length > 0">
<div
class="container"
@mouseenter="handleMouseEnter"
@mouseleave="handleMouseLeave"
>
<!-- 跑马灯轨道容器 -->
<div
class="carousel-track"
:class="{ 'no-transition': isTransitioning }"
:style="{ transform: `translateX(-${currentIndex * 100}%)` }"
>
<!-- 遍历所有媒体项包含克隆项 -->
<div
v-for="(item, index) in renderMediaData"
:key="index"
class="carousel-item"
@click="handleItemClick"
>
<!-- 图片 -->
<img :src="item.url" alt="" />
<!-- 说明文字随媒体项移动 -->
<div class="text">{{ item.text }}</div>
</div>
</div>
<!-- 面板指示器固定在底部右侧 -->
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in originalMediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
<!-- 面板指示器固定在底部右侧 -->
<div class="pagination-dots-fixed">
<span
v-for="(dot, index) in originalMediaData"
:key="index"
class="dot"
:class="{ active: getCurrentRealIndex() === index }"
@click="goToSlide(index)"
></span>
</div>
</div>
<!-- 独立的文字说明区域随跑马灯切换而变化 -->
<div class="description-text">
{{ currentDescription }}
<div class="description-text" @click="handleItemClick">
{{ currentDescription }}
</div>
</SidePanelItem>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
</SidePanelItem>
<!-- 设施详情弹框 -->
<a-modal v-model:open="modalVisible" :title="'设施类型介绍'" width="1536px" :footer="null">
<div v-if="currentItem">
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { msstbprptGetKendoList } from '@/api/zzfl';
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
// 便
defineOptions({
name: 'zengZhiZhanJieShaoMod'
name: 'zengZhiZhanJieShaoMod'
});
const baseid = ref('');
const JidiSelectEventStore = useJidiSelectEventStore();
//
interface MediaItem {
type: 'image' | 'video';
url: string;
text: string;
description: string;
type: 'image' | 'video';
url: string;
text: string;
description: string;
}
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// 3
const originalMediaData = ref<MediaItem[]>([
{
type: 'image',
url: 'https://211.99.26.225:12125/?20230814205611342377136845462200&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
text: '松岗鱼类增殖站',
description: '松岗鱼类增殖放流站位于四川省阿坝藏族羌族自治州马尔康市松岗镇,主要服务于大渡河上游的双江口和金川两座水电站,同时还承担多种珍稀特有鱼类的救护和科研任务,实现工程建设与生态环境共同推进、相互促进。'
},
{
type: 'video',
url: 'https://211.99.26.225:12125/?20230805205848575430105387253710&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920', // URL
text: '猴子岩鱼类增殖站',
description: '猴子岩水电站鱼类增殖放流站位于猴子岩水电站坝址下游约7.0km(业主营地下游约1.5km)大渡河左岸桃花渣场顶部平台上紧邻枢纽桃花大桥下游侧占地面积47.3亩其中一期工程27.0亩预留二期工程用地20.3亩(二期工程目前为丹巴、巴底水电站预留工程)增殖放流站工作流程为:亲鱼收集购买、亲鱼驯养培育、人工催产和授精、人工孵化、苗种培育和放流。 猴子岩鱼类增殖放流站近期放流对象中齐口裂腹鱼、重口裂腹鱼、大渡软刺裸裂尻鱼增殖放流技术水平已趋于熟化,中期放流对象大渡软刺裸裂尻鱼人工繁殖技术逐渐趋于熟化'
},
{
type: 'image',
url: 'https://211.99.26.225:12125/?20230805205924378504010675106305&view=jpg&token=bearer c2e76c28-14db-4a0f-9ff2-10cc3f835920',
text: '黑马鱼类增殖站',
description: '大渡河黑马鱼类增殖放流站位于四川省甘洛县黑马乡黑马业主营地内距离甘洛县城45km,区域交通路况较好。主要承担瀑布沟、深溪沟、大岗山、枕头坝一级、沙坪二级等五座水电站鱼类增殖放流的重任。放流鱼类包含:齐口裂腹鱼、重口裂腹鱼、鲈鲤、长薄鳅、白甲鱼、中华倒刺、长吻脆、稀有鮊鲫、华鲮、侧沟爬岩鳅等10个种类共计约918.07万尾珍稀特有鱼苗。'
}
]);
//
const modalVisible = ref(false);
const currentItem = ref<any>(null);
//
const loading = ref(false);
// API
const originalMediaData = ref<MediaItem[]>([]);
//
// [, ..., ]
@ -90,197 +106,283 @@ const isTransitioning = ref(false);
//
const initRenderData = () => {
const length = originalMediaData.value.length;
if (length === 0) return;
const length = originalMediaData.value.length;
if (length === 0) return;
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
renderMediaData.value = [
originalMediaData.value[length - 1], //
...originalMediaData.value, //
originalMediaData.value[0] //
];
};
//
const startAutoPlay = () => {
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 4000);
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value) {
nextSlide();
}
}, 4000);
};
//
const nextSlide = () => {
currentIndex.value++;
currentIndex.value++;
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
//
setTimeout(() => {
checkSeamlessJump();
}, 500); // transition
};
//
const checkSeamlessJump = () => {
const realLength = originalMediaData.value.length;
const realLength = originalMediaData.value.length;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1.
isTransitioning.value = true;
// 2. 1
currentIndex.value = 1;
// 2. 1
currentIndex.value = 1;
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
// 3. DOM
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
};
//
const handleMouseEnter = () => {
isHovering.value = true;
isHovering.value = true;
};
//
const handleMouseLeave = () => {
isHovering.value = false;
isHovering.value = false;
};
//
const currentDescription = computed(() => {
const realIndex = getCurrentRealIndex();
return originalMediaData.value[realIndex]?.description || '';
const realIndex = getCurrentRealIndex();
return originalMediaData.value[realIndex]?.description || '';
});
//
onMounted(() => {
initRenderData();
startAutoPlay();
initRenderData();
startAutoPlay();
});
//
onUnmounted(() => {
if (timer) clearInterval(timer);
if (timer) clearInterval(timer);
});
//
const getCurrentRealIndex = () => {
const realLength = originalMediaData.value.length;
let realIndex = currentIndex.value - 1; //
const realLength = originalMediaData.value.length;
let realIndex = currentIndex.value - 1; //
//
if (realIndex < 0) realIndex = realLength - 1;
if (realIndex >= realLength) realIndex = 0;
//
if (realIndex < 0) realIndex = realLength - 1;
if (realIndex >= realLength) realIndex = 0;
return realIndex;
return realIndex;
};
//
const goToSlide = (targetIndex: number) => {
if (isTransitioning.value) return;
if (isTransitioning.value) return;
//
currentIndex.value = targetIndex + 1;
//
currentIndex.value = targetIndex + 1;
};
const getData = async () => {
loading.value = true;
try {
let params = {
filter: {
logic: 'and',
filters: [
{
field: 'sttpCode',
operator: 'eq',
dataType: 'string',
value: 'FB'
},
{
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
},
{
logic: 'or',
filters: [
{
field: 'logo',
dataType: 'string',
operator: 'isnotnull'
},
{
field: 'introduce',
dataType: 'string',
operator: 'isnotnull'
}
]
}
]
},
select: ['introduce', 'logo', 'stnm', 'precis']
};
let res = await msstbprptGetKendoList(params);
let data = res?.data?.data;
originalMediaData.value = data.map((item: any) => {
return {
url: baseUrl + '?' + item.logo + '&view=jpg',
text: item.stnm,
description: item.introduce,
stcd: item.stcd
};
});
} finally {
loading.value = false;
}
};
//
onMounted(async () => {
// watch
startAutoPlay();
});
//
onUnmounted(() => {
if (timer) clearInterval(timer);
});
//
const handleItemClick = () => {
currentItem.value = originalMediaData.value.map((item: any) => ({
url: item.url || '',
description: item.description || '',
title: item.text || ''
}));
modalVisible.value = true;
};
watch(
() => JidiSelectEventStore.selectedItem,
async newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
await getData();
initRenderData();
}
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
// border: 1px solid #7fd6ff;
// border-radius: 5px;
position: relative;
overflow: hidden; //
width: 100%;
height: 228px;
// border: 1px solid #7fd6ff;
// border-radius: 5px;
position: relative;
overflow: hidden; //
//
.carousel-track {
display: flex; //
//
.carousel-track {
display: flex; //
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
//
&.no-transition {
transition: none;
}
//
.carousel-item {
min-width: 100%; //
max-width: 100%;
height: 100%;
position: relative;
flex-shrink: 0; //
img,
video {
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out; // 0.5
object-fit: cover; //
}
//
&.no-transition {
transition: none;
}
//
.carousel-item {
min-width: 100%; //
height: 100%;
position: relative;
flex-shrink: 0; //
img,
video {
width: 100%;
height: 100%;
object-fit: cover; //
}
.text {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 22px;
line-height: 22px;
background: rgba(0, 0, 0, 0.1);
color: #fff;
padding-left: 10px;
}
}
}
//
.pagination-dots-fixed {
.text {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10; //
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #D8D8D8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
bottom: 0;
left: 0;
width: 100%;
height: 22px;
line-height: 22px;
background: rgba(0, 0, 0, 0.1);
color: #fff;
padding-left: 10px;
}
}
}
//
.pagination-dots-fixed {
position: absolute;
bottom: 10px;
right: 10px;
display: flex;
gap: 6px;
z-index: 10; //
.dot {
width: 5px;
height: 5px;
border-radius: 50%;
background-color: #d8d8d8;
cursor: pointer;
transition: background-color 0.3s ease;
&.active {
background-color: #005293;
}
&:hover {
opacity: 0.8;
}
}
}
}
//
.description-text {
// padding: 8px 12px;
font-size: 14px;
line-height: 1.5;
// min-height: 40px;
transition: all 0.3s ease;
margin-bottom: 20px;
// padding: 8px 12px;
font-size: 14px;
line-height: 1.5;
// min-height: 40px;
transition: all 0.3s ease;
margin-bottom: 20px;
//
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
//
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
</style>

View File

@ -0,0 +1,260 @@
<template>
<div class="whole-valued-station-detail">
<!-- 搜索表单 -->
<a-form layout="inline" class="search-form">
<a-form-item label="年份">
<a-date-picker
v-model:value="yearValue"
picker="year"
:allow-clear="false"
format="YYYY"
value-format="YYYY"
/>
</a-form-item>
<a-form-item label="">
<a-input
v-model:value="search"
placeholder="请输入设施名称"
allow-clear
style="width: 200px"
/>
</a-form-item>
<a-form-item>
<a-space>
<a-button type="primary" @click="handleSearch">查询</a-button>
<a-button @click="handleReset">重置</a-button>
</a-space>
</a-form-item>
</a-form>
<!-- 表格 -->
<div class="table-wrapper">
<BasicTable
ref="tableRef"
:columns="columns"
:scroll-y="500"
:scroll-x="800"
:list-url="listUrl"
:search-params="searchParams"
:transform-data="customTransform"
>
<template #action="{ record }">
<!-- 操作列 -->
<a @click="handleViewDetail(record)" class="text-link"> 查看详情 </a>
</template>
</BasicTable>
</div>
</div>
</template>
<script lang="ts" setup>
import { ref, computed, watch, h, onMounted } from 'vue';
import {
qgcoverviewGetOverviewSecond,
overviewGetOverviewSecond
} from '@/api/zzfl';
import BasicTable from '@/components/BasicTable/index.vue';
import { List } from 'ant-design-vue';
import { useModelStore } from '@/store/modules/model';
const modelStore = useModelStore();
//
const renderList = (text: string) => {
if (!text) return '-';
const items = text.split(',');
return h(
List,
{ size: 'small' },
{
renderItem: (item: string) => h(List.Item, {}, () => item),
default: () => items.map(item => h(List.Item, {}, () => item))
}
);
};
// ==================== Props ====================
const props = defineProps<{
time?: string; // YYYY
all?: boolean; //
baseId?: string; // ID
}>();
// ==================== ====================
const yearValue = ref(props.time || `${new Date().getFullYear()}`);
const search = ref('');
// ==================== URL ====================
const listUrl = computed(() => {
return props.all ? qgcoverviewGetOverviewSecond : overviewGetOverviewSecond;
});
// ==================== ====================
const searchParams = computed(() => ({
sort: [],
group: [],
select: [],
groupResultFlat: false
}));
// ==================== ====================
const columns = [
{
key: 'stnm',
title: '设施名称',
dataIndex: 'stnm',
fixed: 'left',
width: 120,
ellipsis: true
},
{
key: 'ennm',
title: '服务电站',
dataIndex: 'ennm',
fixed: 'left',
width: 120,
ellipsis: true
},
{
key: 'totalInv',
title: '总投资',
dataIndex: 'totalInv',
width: 120
},
{
key: 'planFtp',
title: '计划放流种类',
dataIndex: 'planFtp',
width: 160,
customRender: ({ text }: { text: string }) => renderList(text)
},
{
key: 'fcntjh',
title: '计划放流数量',
dataIndex: 'fcntjh',
width: 140
},
{
key: 'realftp',
title: '实际放流种类',
dataIndex: 'realftp',
width: 160,
customRender: ({ text }: { text: string }) => renderList(text)
},
{
key: 'realfcntStr',
title: '实际放流数量',
dataIndex: 'realfcntStr',
width: 160,
customRender: ({ text }: { text: string }) => renderList(text)
},
{
title: '操作',
key: 'action',
width: 100,
fixed: 'right',
slots: { customRender: 'action' }
}
];
// ==================== ====================
const tableRef = ref();
//
const getFilter = () => {
return {
logic: 'and',
filters: [
{
field: 'year',
operator: 'eq',
dataType: 'string',
value: yearValue.value
},
props.all && props.baseId && props.baseId !== 'all'
? {
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: props.baseId
}
: null,
search.value
? {
field: 'stnm',
operator: 'contains',
dataType: 'string',
value: search.value
}
: null
].filter(Boolean)
};
};
// ==================== ====================
const customTransform = (res: any) => {
return {
records: res?.data?.data || [],
total: res?.data?.total || 0
};
};
// ==================== ====================
const handleSearch = () => {
// debugger;
const filter = getFilter();
tableRef.value?.getList(filter);
};
const handleReset = () => {
yearValue.value = props.time || `${new Date().getFullYear()}`;
search.value = '';
handleSearch();
};
//
const handleViewDetail = (record: any) => {
// debugger;
modelStore.modalVisible = true;
modelStore.params = record;
modelStore.title = record.stnm;
modelStore.params.sttp = 'FB';
modelStore.filter.year = yearValue.value;
// modelStore.params.s = record.stnm;
};
// ==================== time prop ====================
watch(
() => props.time,
newVal => {
if (newVal) {
yearValue.value = newVal;
handleSearch();
}
},
{ immediate: true }
);
onMounted(() => {
handleSearch();
});
</script>
<style lang="scss" scoped>
.whole-valued-station-detail {
width: 100%;
.search-form {
padding: 16px 0;
border-bottom: 1px solid #f0f0f0;
margin-bottom: 16px;
}
.table-wrapper {
width: 100%;
}
}
.text-link {
color: #2f6b98;
&:hover {
color: #40a9ff;
}
}
</style>

View File

@ -1,128 +1,232 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="增殖站建设运行情况" :datetimePicker="datetimePicker" >
<div class="facility-grid">
<div v-for="facility in facilities" :key="facility.name" class="facility-card">
<div style="width: 70px;height: 62px;display: flex;align-items: center;justify-content: center;">
<div class="facility-icon">
<i style="color: #fff;" :class="facility.icon" type="icon-shengtailiuliang2"></i>
</div>
</div>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 14px;"> <span class="facility-count">{{ facility.count }}</span>
<span v-if="facility.name == '增殖站数量'"></span>
<span v-if="facility.name == '放流量'">万尾</span>
<span v-if="facility.name == '放流种类'"></span>
</div>
</div>
</div>
<SidePanelItem
title="增殖站建设运行情况"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange1"
>
<a-spin :spinning="loading">
<div class="facility-grid">
<div
v-for="facility in facilities"
:key="facility.name"
class="facility-card"
@click="openDialog(facility)"
>
<div
style="
width: 70px;
height: 62px;
display: flex;
align-items: center;
justify-content: center;
"
>
<div class="facility-icon">
<i
style="color: #fff"
:class="facility.icon"
type="icon-shengtailiuliang2"
></i>
</div>
</div>
</SidePanelItem>
<div class="facility-info">
<div class="facility-name">{{ facility.name }}</div>
<div style="font-size: 14px">
<span class="facility-count">{{ facility.nowNum }}</span>
<span v-if="facility.name == '增殖站数量'"></span>
<span v-if="facility.name == '放流量'">万尾</span>
<span v-if="facility.name == '放流种类'"></span>
</div>
</div>
</div>
</div>
</a-spin>
<a-modal
v-model:visible="dialogVisible"
:title="dialogTitle"
:width="1536"
:footer="null"
>
<WholeValuedStationOverviewDetail v-if="dialogVisible" :time="datetimePicker.value" all :baseId="baseid" />
</a-modal>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, watch, nextTick } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { overviewGetKendoListCust } from '@/api/zzfl';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import WholeValuedStationOverviewDetail from "./WholeValuedStationOverviewDetail/index.vue"
// 便
defineOptions({
name: 'zengzhizhanjiansheyunxing'
name: 'zengzhizhanjiansheyunxing'
});
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
const loading = ref(false);
const dialogVisible = ref(false);
const dialogTitle = ref('增殖站概况');
const datetimePicker = ref({
show: true,
value: `${new Date().getFullYear() - 1}`, //
format: 'YYYY',
picker: 'year' as const,
options: []
show: true,
value: `${new Date().getFullYear() - 1}`, //
format: 'YYYY',
picker: 'year' as const,
options: []
});
//
const facilities = ref([
{
name: '增殖站数量',
count: "56",
icon: 'icon iconfont icon-yuleizengzhizhan1'
},
{
name: '放流量',
count: '1722',
icon: 'icon iconfont icon-fangliushuliang'
},
{
name: '放流种类',
count: '135',
icon: 'icon iconfont icon-yuzhongshuliang'
}
const facilities:any = ref([
{
name: '增殖站数量',
nowNum: '56',
icon: 'icon iconfont icon-yuleizengzhizhan1'
},
{
name: '放流量',
nowNum: '1722',
icon: 'icon iconfont icon-fangliushuliang'
},
{
name: '放流种类',
nowNum: '135',
icon: 'icon iconfont icon-yuzhongshuliang'
}
]);
const getData = async () => {
loading.value = true;
try {
const year = datetimePicker.value.value;
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
value: baseid.value
}
: null,
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: `${year}-01-01 00:00:00`
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: `${year}-12-31 23:59:59`
}
].filter(Boolean)
}
};
let res = await overviewGetKendoListCust(params);
if (res.data?.data?.length > 0) {
const item = res.data.data[0];
facilities.value[0].nowNum = item.count ?? '-';
facilities.value[1].nowNum = item.realfcnt
? (item.realfcnt / 10000).toFixed(4)
: '0.0000';
facilities.value[2].nowNum = item.realftpCount ?? '0';
}
} finally {
loading.value = false;
}
};
//
onMounted(() => {
//
//
});
//
onUnmounted(() => {
onUnmounted(() => {});
});
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
//
const handlePanelChange1 = async data => {
console.log('当前所有控件状态:', data);
// ,
if (data.datetime) {
await (datetimePicker.value.value = data.datetime);
getData();
}
};
const openDialog = (facility) => {
dialogTitle.value = `增殖站概况`;
dialogVisible.value = true;
};
</script>
<style lang="scss" scoped>
.facility-grid {
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto, Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji, Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
width: 406px;
flex-flow: wrap;
display: flex;
justify-content: space-between;
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Roboto,
Helvetica Neue, Arial, Noto Sans, sans-serif, Apple Color Emoji,
Segoe UI Emoji, Segoe UI Symbol, Noto Color Emoji;
}
.facility-card {
width: 200px;
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
width: 200px;
height: 70px;
display: flex;
align-items: center;
justify-content: space-between;
margin: 4px 0px;
background: #fff;
border: 1px solid #e8e8e8;
border-radius: 2px;
transition: all 0.3s;
cursor: pointer;
box-sizing: border-box;
}
.facility-icon {
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
// margin-right: 8px;
background: rgb(47, 107, 152);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
width: 40px;
height: 40px;
// margin-right: 8px;
background: rgb(47, 107, 152);
border-radius: 50%;
.anticon {
font-size: 24px;
color: #fff;
}
.anticon {
font-size: 24px;
color: #fff;
}
}
.facility-info {
flex: 1;
flex: 1;
}
.facility-name {
font-size: 16px;
color: #333;
// margin-bottom: 4px;
// font-weight: 500;
font-size: 16px;
color: #333;
// margin-bottom: 4px;
// font-weight: 500;
}
.facility-count {
font-size: 18px;
color: #2f6b98;
// font-weight: 600;
font-size: 18px;
color: #2f6b98;
// font-weight: 600;
}
</style>

View File

@ -1,214 +1,326 @@
<!-- SidePanelItem.vue -->
<template>
<SidePanelItem title="增殖站运行数据统计" :datetimePicker="datetimePicker">
<div ref="chartRef" class="chart-container"></div>
</SidePanelItem>
<SidePanelItem
title="增殖站运行数据统计"
:datetimePicker="datetimePicker"
@update-values="handlePanelChange1"
>
<a-spin :spinning="loading" tip="加载中...">
<div class="chart-wrapper">
<div ref="chartRef" class="chart-container" v-show="chartData.length > 0"></div>
<a-empty v-show="chartData.length === 0" description="暂无数据" />
</div>
</a-spin>
</SidePanelItem>
</template>
<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 type { ECharts } from 'echarts';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { qgcgetFbStaticsData } from '@/api/zzfl';
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
// 便
defineOptions({
name: 'ZengZhiZhanYunXingSJ'
name: 'ZengZhiZhanYunXingSJ'
});
const datetimePicker = ref({
show: true,
value: `${new Date().getFullYear() - 1}`, //
format: 'YYYY',
picker: 'year' as const,
options: []
show: true,
value: `${new Date().getFullYear() - 1}`, //
format: 'YYYY',
picker: 'year' as const,
options: []
});
//
const chartRef = ref<HTMLElement | null>(null);
let chartInstance: ECharts | null = null;
//
const mockData = [
{ hbrvcdName: '长江上游', fcnt: 1200, ftp: 15 },
{ hbrvcdName: '长江中游', fcnt: 980, ftp: 12 },
{ hbrvcdName: '长江下游', fcnt: 1500, ftp: 18 },
{ hbrvcdName: '珠江流域', fcnt: 750, ftp: 10 },
{ hbrvcdName: '黄河流域', fcnt: 600, ftp: 8 },
{ hbrvcdName: '淮河流域', fcnt: 450, ftp: 6 },
];
//
const chartData = ref<any[]>([]);
// loading
const loading = ref(false);
//
const getChartStep = (data: number[]) => {
if (!data || data.length === 0) return {};
const maxVal = Math.max(...data.filter(v => v !== null && v !== undefined));
const minVal = Math.min(...data.filter(v => v !== null && v !== undefined));
const range = maxVal - minVal;
const step = range / 5;
return {
min: Math.floor(minVal),
max: Math.ceil(maxVal),
interval: Math.ceil(step)
};
if (!data || data.length === 0) return {};
const maxVal = Math.max(...data.filter(v => v !== null && v !== undefined));
const range = maxVal; // 0
const step = range / 5;
return {
min: 0, // y0
max: Math.ceil(maxVal * 1.1), // 10%
interval: Math.ceil(step * 1.1)
};
};
//
//
const transUnit = (value: number, type: string, unit: string) => {
//
return value;
if (unit === 'SL') {
//
return value / 10000;
}
return value;
};
//
const initChart = () => {
if (!chartRef.value) return;
if (!chartRef.value) return;
if (!chartInstance) {
chartInstance = echarts.init(chartRef.value);
} else {
chartInstance.resize();
}
const fcntData = mockData.map(el => el.fcnt ? transUnit(el.fcnt, 'Other', 'SL') : el.fcnt);
const ftpData = mockData.map(el => el.ftp);
const fcntStep = getChartStep(fcntData);
const ftpStep = getChartStep(ftpData);
const fcntData = chartData.value.map(el =>
el.fcnt ? transUnit(el.fcnt, 'Other', 'SL') : el.fcnt
);
const ftpData = chartData.value.map(el => el.ftp);
const fcntStep = getChartStep(fcntData);
const ftpStep = getChartStep(ftpData);
const option: any = {
grid: {
left: 50,
right: 50,
bottom: 60,
top: 60,
const option: any = {
grid: {
left: 50,
right: 50,
bottom: 60,
top: 60
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
},
legend: {
data: ['放流量(万尾)', '放流种类(种)'],
top: 10,
itemWidth: 12,
itemHeight: 8,
itemGap: 8
},
xAxis: {
show: true,
type: 'category',
axisLine: { show: true },
splitLine: { show: false },
axisLabel: {
fontSize: 12,
color: '#686868',
rotate: 37,
interval: 0
},
data: chartData.value.map(el => el.baseName)
},
yAxis: [
{
show: true,
type: 'value',
name: '放流量(万尾)',
nameTextStyle: {
fontSize: 12,
color: '#848484',
align: 'center'
},
tooltip: {
trigger: 'axis',
axisPointer: {
type: 'shadow'
}
axisTick: { show: true },
axisLine: { show: true },
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#ccc'
}
},
legend: {
data: ['放流量(万尾)', '放流种类(种)'],
top: 10,
itemWidth: 12,
itemHeight: 8,
itemGap: 8
axisLabel: {
fontSize: 12,
color: '#343434'
},
xAxis: {
show: true,
type: 'category',
axisLine: { show: true },
splitLine: { show: false },
axisLabel: {
fontSize: 12,
color: '#686868',
rotate: 37,
interval: 0
},
data: mockData.map(el => el.hbrvcdName),
position: 'left',
...fcntStep
},
{
show: true,
type: 'value',
name: '种类(种)',
nameTextStyle: {
fontSize: 12,
color: '#848484',
align: 'left'
},
yAxis: [{
show: true,
type: 'value',
name: '放流量(万尾)',
nameTextStyle: {
fontSize: 12,
color: '#848484',
align: 'center'
},
axisTick: { show: true },
axisLine: { show: true },
splitLine: {
show: true,
lineStyle: {
type: 'solid',
color: '#ccc'
}
},
axisLabel: {
fontSize: 12,
color: '#343434'
},
position: 'left',
...fcntStep,
}, {
show: true,
type: 'value',
name: '种类(种)',
nameTextStyle: {
fontSize: 12,
color: '#848484',
align: 'left'
},
axisTick: { show: true },
axisLine: { show: true },
splitLine: { show: false },
axisLabel: {
fontSize: 12,
color: '#343434'
},
position: 'right',
...ftpStep,
}],
series: [{
name: '放流量(万尾)',
type: 'bar',
barGap: 0,
label: { show: false },
emphasis: {
focus: 'series'
},
data: fcntData,
yAxisIndex: 0,
barMaxWidth: 20,
itemStyle: {
color: '#5470C6'
}
}, {
name: '放流种类(种)',
type: 'bar',
barGap: 0,
label: { show: false },
emphasis: {
focus: 'series'
},
data: ftpData,
yAxisIndex: 1,
barMaxWidth: 20,
itemStyle: {
color: '#91CC75'
}
}]
};
axisTick: { show: true },
axisLine: { show: true },
splitLine: { show: false },
axisLabel: {
fontSize: 12,
color: '#343434'
},
position: 'right',
...ftpStep
}
],
series: [
{
name: '放流量(万尾)',
type: 'bar',
barGap: 0,
label: { show: false },
emphasis: {
focus: 'series'
},
data: fcntData,
yAxisIndex: 0,
barMaxWidth: 20,
itemStyle: {
color: '#5470C6'
}
},
{
name: '放流种类(种)',
type: 'bar',
barGap: 0,
label: { show: false },
emphasis: {
focus: 'series'
},
data: ftpData,
yAxisIndex: 1,
barMaxWidth: 20,
itemStyle: {
color: '#91CC75'
}
}
]
};
chartInstance.setOption(option);
chartInstance.setOption(option);
};
//
onMounted(() => {
nextTick(() => {
// 50ms
setTimeout(() => {
initChart();
nextTick(() => {
// 50ms
setTimeout(() => {
initChart();
//
window.addEventListener('resize', handleResize);
}, 50);
});
//
window.addEventListener('resize', handleResize);
}, 50);
});
});
const getData = async () => {
loading.value = true;
let params = {
filter: {
logic: 'and',
filters: [
baseid.value != 'all'
? {
field: 'baseId',
operator: 'eq',
value: baseid.value
}
: null,
{
field: 'startTime',
operator: 'gte',
dataType: 'date',
value: datetimePicker.value.value + '-01-01 00:00:00'
},
{
field: 'endTime',
operator: 'lte',
dataType: 'date',
value: datetimePicker.value.value + '-12-31 23:59:59'
}
].filter(Boolean)
}
};
try {
let res = await qgcgetFbStaticsData(params);
// 使
if (res?.data?.data) {
chartData.value = res.data.data;
nextTick(() => {
initChart();
});
} else {
chartData.value = [];
}
} catch (error) {
chartData.value = [];
} finally {
loading.value = false;
}
};
//
const handleResize = () => {
chartInstance?.resize();
chartInstance?.resize();
};
//
onUnmounted(() => {
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
window.removeEventListener('resize', handleResize);
chartInstance?.dispose();
chartInstance = null;
});
//
const handlePanelChange1 = async data => {
console.log('当前所有控件状态:', data);
// ,
if (data.datetime) {
await (datetimePicker.value.value = data.datetime);
getData();
}
};
watch(
() => JidiSelectEventStore.selectedItem,
newVal => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
</script>
<style lang="scss" scoped>
.chart-wrapper {
position: relative;
width: 100%;
height: 300px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
}
.chart-container {
width: 100%;
height: 300px;
min-height: 300px;
width: 100%;
height: 100%;
display: block;
box-sizing: border-box;
}
.empty-container {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
display: flex;
align-items: center;
justify-content: center;
background: #fff;
}
</style>

View File

@ -53,7 +53,8 @@ export const useModelStore = defineStore('model', () => {
stllgzlx: '', // 生态流量 - 规则类型
stllTmType: '', // 生态流量 - 时间类型
tm: '',
rangeTm: []
rangeTm: [],
year:'',
});
return {
params,

View File

@ -34,7 +34,7 @@ service.interceptors.request.use(
config.headers._sysid = '10EC2E0B-AEA9-4757-83A2-201BA1BC54E9';
config.headers.authorization =
'bearer 0952e56d-8a77-4c43-abe4-a44367a22d73';
'bearer cb68557d-2861-497d-b708-6b73ae68ba95';
config.baseURL = '/';
} else {
const user = useUserStoreHook();

View File

@ -21,7 +21,7 @@ watch(
wbsCode.value = newVal.wbsCode;
}
},
{ deep: true }
{ deep: true, immediate: true }
);
</script>