WholeProcessPlatform/frontend/src/modules/dianxingcuoshijieshao/index.vue
2026-07-07 13:50:21 +08:00

420 lines
9.9 KiB
Vue
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

<!-- SidePanelItem.vue -->
<template>
<SidePanelItem
title="典型设施介绍"
:select="selectConfig"
@update-values="handlePanelChange"
>
<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.image" :alt="item.title" />
<!-- 说明文字(随媒体项移动) -->
<!-- <div class="text">{{ item.title }}</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>
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
<div class="description-text">
{{ currentDescription }}
</div>
</template>
<a-empty v-else description="暂无数据" />
</a-spin>
</SidePanelItem>
<!-- 设施详情弹框 -->
<a-modal v-model:open="modalVisibleone" :title="'设施详情'" width="80%" :footer="null">
<div v-if="currentItem" class="detail-container">
<ArtsDetail :dataSource="currentItem" :index="getCurrentRealIndex()" />
</div>
</a-modal>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
import { getMsstbprptKendoList } from '@/api/home';
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
import { useDraggable } from '@/utils/drag';
// 定义组件名(便于调试和递归)
defineOptions({
name: 'dianxingcuoshijieshao'
});
// 基地选择 store
const JidiSelectEventStore = useJidiSelectEventStore();
const baseid = ref('');
// 媒体类型定义
interface MediaItem {
title: string;
description: string;
image: string;
}
// 弹框控制
const modalVisibleone = ref(false);
const currentItem = ref<any>(null);
useDraggable(modalVisibleone, { boundary: true, resetOnOpen: true });
// 加载状态
const loading = ref(false);
// 下拉选择配置
const selectConfig = ref({
show: true,
value: 'all',
width: '142px',
options: [
{ label: '全部', value: 'all' },
{ label: '鱼类增殖站', value: 'FB' },
{ label: '过鱼设施', value: 'FP' },
{ label: '低温水减缓设施', value: 'DW' }
]
});
// 当前选择的设施类型
const selectValue = ref('all');
// 原始媒体数据
const originalMediaData = ref<MediaItem[]>([]);
// 克隆首尾项后的渲染数组(用于无缝循环)
const renderMediaData = ref<MediaItem[]>([]);
// 当前显示索引指向renderMediaData
const currentIndex = ref(1);
// 定时器引用
let timer: any = null;
// 鼠标悬停状态
const isHovering = ref(false);
// 是否正在切换动画中
const isTransitioning = ref(false);
// 设施类型对应的 sttpCode 列表
const dw = ['DW', 'DW_1', 'DW_2', 'DW_3', 'DW_4', 'DW_5', 'DW_6', 'DW_9'];
const fp = ['FP', 'FP_1', 'FP_2', 'FP_3', 'FP_4', 'FP_5'];
// 初始化渲染数组(克隆首尾项)
const initRenderData = () => {
const length = originalMediaData.value.length;
if (length === 0) {
renderMediaData.value = [];
return;
}
renderMediaData.value = [
originalMediaData.value[length - 1],
...originalMediaData.value,
originalMediaData.value[0]
];
currentIndex.value = 1;
};
// 启动自动轮播
const startAutoPlay = () => {
if (timer) clearInterval(timer);
timer = setInterval(() => {
if (!isHovering.value && !isTransitioning.value && originalMediaData.value.length > 0) {
nextSlide();
}
}, 4000);
};
// 切换到下一张
const nextSlide = () => {
currentIndex.value++;
setTimeout(() => {
checkSeamlessJump();
}, 500);
};
// 检查是否需要无缝跳转
const checkSeamlessJump = () => {
const realLength = originalMediaData.value.length;
if (currentIndex.value >= realLength + 1) {
isTransitioning.value = true;
currentIndex.value = 1;
requestAnimationFrame(() => {
requestAnimationFrame(() => {
isTransitioning.value = false;
});
});
}
};
// 处理鼠标进入
const handleMouseEnter = () => {
isHovering.value = true;
};
// 处理鼠标离开
const handleMouseLeave = () => {
isHovering.value = false;
};
// 计算当前显示的描述文字
const currentDescription = computed(() => {
const realIndex = getCurrentRealIndex();
return originalMediaData.value[realIndex]?.description || '';
});
// 获取当前真实的索引
const getCurrentRealIndex = () => {
const realLength = originalMediaData.value.length;
let realIndex = currentIndex.value - 1;
if (realIndex < 0) realIndex = realLength - 1;
if (realIndex >= realLength) realIndex = 0;
return realIndex;
};
// 跳转到指定幻灯片
const goToSlide = (targetIndex: number) => {
if (isTransitioning.value) return;
currentIndex.value = targetIndex + 1;
};
// 处理项目点击事件
const handleItemClick = () => {
currentItem.value = originalMediaData.value.map((item: any) => ({
url: item.image || '',
description: item.description || '',
title: item.title || ''
}));
modalVisibleone.value = true;
};
// 处理下拉选择变化
const handlePanelChange = (payload: any) => {
if (payload.select !== undefined && payload.select !== selectValue.value) {
selectValue.value = payload.select;
getData();
}
};
// 获取数据
const getData = async () => {
loading.value = true;
try {
// 根据选择确定 sttpCode 列表
let sttpCodes: string[] = ['FB', ...dw, ...fp];
if (selectValue.value === 'FB') {
sttpCodes = ['FB'];
} else if (selectValue.value === 'DW') {
sttpCodes = [...dw];
} else if (selectValue.value === 'FP') {
sttpCodes = [...fp];
}
const filters: any[] = [
{
field: 'sttpCode',
operator: 'in',
dataType: 'string',
value: sttpCodes
},
{
logic: 'and',
filters: [
{ field: 'logo', dataType: 'string', operator: 'isnotnull' },
{ field: 'introduce', dataType: 'string', operator: 'isnotnull' }
]
}
];
// 添加基地过滤
if (baseid.value && baseid.value !== 'all') {
filters.push({
field: 'baseId',
operator: 'eq',
dataType: 'string',
value: baseid.value
});
}
const params = {
filter: {
logic: 'and',
filters: filters
},
select: ['introduce', 'logo', 'stnm', 'precis']
};
const res = await getMsstbprptKendoList(params);
if (res?.data?.data && Array.isArray(res.data.data)) {
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL || '';
originalMediaData.value = res.data.data
.filter((item: any) => item.logo || item.introduce)
.map((item: any) => ({
image: item.logo ? baseUrl + '?' + item.logo + '&view=jpg' : '',
title: item.stnm || '',
description: item.introduce || ''
}));
} else {
originalMediaData.value = [];
}
initRenderData();
} catch (error) {
console.error('获取设施介绍数据失败:', error);
originalMediaData.value = [];
initRenderData();
} finally {
loading.value = false;
}
};
// 监听基地变化
watch(
() => JidiSelectEventStore.selectedItem,
(newVal) => {
if (newVal && newVal.wbsCode) {
baseid.value = newVal.wbsCode;
getData();
}
},
{ deep: true, immediate: true }
);
// 页面加载时执行
onMounted(() => {
startAutoPlay();
});
// 组件卸载时清理
onUnmounted(() => {
if (timer) clearInterval(timer);
});
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
position: relative;
overflow: hidden;
.carousel-track {
display: flex;
width: 100%;
height: 100%;
transition: transform 0.5s ease-in-out;
&.no-transition {
transition: none;
}
.carousel-item {
min-width: 100%;
height: 100%;
position: relative;
flex-shrink: 0;
cursor: pointer;
img {
width: 100%;
height: 100%;
object-fit: cover;
}
.text {
position: absolute;
bottom: 0;
left: 0;
width: 100%;
height: 28px;
line-height: 28px;
background: rgba(0, 0, 0, 0.5);
color: #fff;
padding-left: 12px;
font-size: 14px;
}
}
}
.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 {
font-size: 14px;
line-height: 1.5;
transition: all 0.3s ease;
margin-top: 12px;
min-height: 44px;
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
.detail-container {
width: 100%;
height: 100%;
}
</style>