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

286 lines
9.5 KiB
Vue
Raw Normal View History

2026-05-09 17:04:48 +08:00
<!-- 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>
</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>
</SidePanelItem>
</template>
<script lang="ts" setup>
import { ref, onMounted, onUnmounted, computed } from 'vue';
import SidePanelItem from '@/components/SidePanelItem/index.vue';
// 定义组件名(便于调试和递归)
defineOptions({
name: 'ZhiWuYuanJianSheJiJieRuQingKuangBar'
});
// 媒体类型定义
interface MediaItem {
type: 'image' | 'video';
url: string;
text: string;
description: string;
}
// 原始媒体数据3条测试数据
const originalMediaData = ref<MediaItem[]>([
{
type: 'image',
url: 'https://211.99.26.225:12125/?20240328104745462272538202651107&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941',
text: '铜街子鱼道',
description: '鱼道布置在河床右岸,全长 1388.32m,由鱼道进口、进口段、绕过木阀闸段、坝后开挖段、过坝段、出口明渠段、鱼道出口组成。鱼道布置 1 个进口和 1 个出口其中进口位于新华电站尾水渠下游约15m 处。出口位于库区右岸,距离坝轴线约 150m。'
},
{
type: 'video',
url: 'https://211.99.26.225:12125/?20240328104800052128627215562160&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941', // 视频URL示例您可替换为真实地址
text: '枕头坝一级鱼道',
description: '枕头坝一级鱼道建筑物主要由鱼道进口、梯身、鱼道出口等组成。全长1300m、池室坡度i=0.033、鱼道净宽2.4m、池室长度为3m每隔10个水池设立一个长6m的休息池池室隔墙厚度为0.20m高3.00m设置3个进口进口断面尺寸为2.40×3.00m(宽×高)。'
},
{
type: 'image',
url: 'https://211.99.26.225:12125/?20240328104800052128627215562160&view=jpg&token=bearer 50b652db-035b-43a9-becd-48b7d54aa941',
text: '沙坪二级鱼道',
description: '沙坪二级鱼道位于泄洪闸与厂房之间,鱼道上下游方向长 224.42m,宽 10.0m14.0m,最大高度 29.85m37.00m。鱼道由诱鱼系统、入口、池室、休息池、观察室和出口等组成,其中诱鱼系统和入口布置于下游厂房尾水侧,出口布置于上游泄洪闸进口侧。鱼道采取连续“绕弯”方式布置,全长 597m平均坡降为 3.85%。沙坪二级鱼道位于泄洪闸与厂房之间,鱼道上下游方向长 224.42m,宽 10.0m14.0m,最大高度 29.85m37.00m。鱼道由诱鱼系统、入口、池室、休息池、观察室和出口等组成,其中诱鱼系统和入口布置于下游厂房尾水侧,出口布置于上游泄洪闸进口侧。鱼道采取连续“绕弯”方式布置,全长 597m平均坡降为 3.85%。'
}
]);
// 克隆首尾项后的渲染数组(用于无缝循环)
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
const renderMediaData = ref<MediaItem[]>([]);
// 当前显示索引指向renderMediaData
const currentIndex = ref(1); // 从1开始跳过克隆的首项
// 定时器引用
let timer: any = null;
// 鼠标悬停状态
const isHovering = ref(false);
// 是否正在切换动画中用于禁用transition
const isTransitioning = ref(false);
// 初始化渲染数组(克隆首尾项)
const initRenderData = () => {
const length = originalMediaData.value.length;
if (length === 0) return;
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);
};
// 切换到下一张
const nextSlide = () => {
currentIndex.value++;
// 延迟检查是否需要无缝跳转
setTimeout(() => {
checkSeamlessJump();
}, 500); // 与transition时间一致
};
// 检查是否需要无缝跳转(克隆项处理)
const checkSeamlessJump = () => {
const realLength = originalMediaData.value.length;
// 如果到达克隆的最后一项(索引 = realLength + 1
if (currentIndex.value >= realLength + 1) {
// 1. 禁用过渡动画,实现瞬间跳转
isTransitioning.value = true;
// 2. 瞬间跳转到真实的第二项索引1用户看不到跳变
currentIndex.value = 1;
// 3. 等待两帧后恢复过渡动画确保DOM已更新
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 || '';
});
// 页面加载时执行
onMounted(() => {
initRenderData();
startAutoPlay();
});
// 组件卸载时清理
onUnmounted(() => {
if (timer) clearInterval(timer);
});
// 获取当前真实的索引(排除克隆项的影响)
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;
};
</script>
<style lang="scss" scoped>
.container {
width: 100%;
height: 228px;
// border: 1px solid #7fd6ff;
// border-radius: 5px;
position: relative;
overflow: hidden; // 隐藏超出容器的内容
// 跑马灯轨道
.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; // 防止被压缩
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 {
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;
// 多行文本溢出省略
display: -webkit-box;
-webkit-line-clamp: 2;
-webkit-box-orient: vertical;
overflow: hidden;
text-overflow: ellipsis;
word-break: break-word;
}
</style>