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

391 lines
9.7 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="增殖站介绍">
<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>
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
<div class="description-text" @click="handleItemClick">
{{ currentDescription }}
</div>
</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, 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';
import { useDraggable } from '@/utils/drag';
// 定义组件名(便于调试和递归)
defineOptions({
name: 'zengZhiZhanJieShaoMod'
});
const baseid = ref('');
const JidiSelectEventStore = useJidiSelectEventStore();
// 媒体类型定义
interface MediaItem {
type: 'image' | 'video';
url: string;
text: string;
description: string;
}
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
// 弹框控制
const modalVisible = ref(false);
const currentItem = ref<any>(null);
// 加载状态
const loading = ref(false);
// 原始媒体数据从API获取
const originalMediaData = ref<MediaItem[]>([]);
// 克隆首尾项后的渲染数组(用于无缝循环)
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
const renderMediaData = ref<MediaItem[]>([]);
// 当前显示索引指向renderMediaData
const currentIndex = ref(1); // 从1开始跳过克隆的首项
// 定时器引用
let timer: any = null;
// 鼠标悬停状态
const isHovering = ref(false);
// 是否正在切换动画中用于禁用transition
const isTransitioning = ref(false);
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
// 初始化渲染数组(克隆首尾项)
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;
};
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; // 隐藏超出容器的内容
// 跑马灯轨道
.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%;
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>