286 lines
9.5 KiB
Vue
286 lines
9.5 KiB
Vue
<!-- 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: 'zengZhiZhanJieShaoMod'
|
||
});
|
||
|
||
// 媒体类型定义
|
||
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/?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 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> |