2026-05-12 14:34:58 +08:00
|
|
|
|
<!-- SidePanelItem.vue -->
|
|
|
|
|
|
<template>
|
2026-06-24 11:26:19 +08:00
|
|
|
|
<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>
|
2026-05-12 14:34:58 +08:00
|
|
|
|
</div>
|
2026-06-24 11:26:19 +08:00
|
|
|
|
</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>
|
2026-05-12 14:34:58 +08:00
|
|
|
|
</div>
|
2026-06-24 11:26:19 +08:00
|
|
|
|
|
2026-05-12 14:34:58 +08:00
|
|
|
|
<!-- 独立的文字说明区域(随跑马灯切换而变化) -->
|
2026-06-24 11:26:19 +08:00
|
|
|
|
<div class="description-text" @click="handleItemClick">
|
|
|
|
|
|
{{ currentDescription }}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
</div>
|
2026-06-24 11:26:19 +08:00
|
|
|
|
</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>
|
2026-05-12 14:34:58 +08:00
|
|
|
|
</template>
|
|
|
|
|
|
|
|
|
|
|
|
<script lang="ts" setup>
|
2026-06-24 11:26:19 +08:00
|
|
|
|
import { ref, onMounted, onUnmounted, computed, watch } from 'vue';
|
2026-05-12 14:34:58 +08:00
|
|
|
|
import SidePanelItem from '@/components/SidePanelItem/index.vue';
|
2026-06-24 11:26:19 +08:00
|
|
|
|
import { useJidiSelectEventStore } from '@/store/modules/jidiSelectEvent';
|
|
|
|
|
|
import { vpcGetKendoListCust } from '@/api/zxdzw';
|
|
|
|
|
|
import ArtsDetail from '@/components/carouselIntroduce/ArtsDetail.vue';
|
2026-07-07 10:33:29 +08:00
|
|
|
|
import { useDraggable } from '@/utils/drag';
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 定义组件名(便于调试和递归)
|
|
|
|
|
|
defineOptions({
|
2026-06-24 11:26:19 +08:00
|
|
|
|
name: 'zengZhiZhanJieShaoMod'
|
2026-05-12 14:34:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 媒体类型定义
|
|
|
|
|
|
interface MediaItem {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
type: 'image' | 'video';
|
|
|
|
|
|
url: string;
|
|
|
|
|
|
text: string;
|
|
|
|
|
|
description: string;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
}
|
2026-06-24 11:26:19 +08:00
|
|
|
|
const baseUrl = import.meta.env.VITE_APP_ATTACHMENT_URL;
|
|
|
|
|
|
const JidiSelectEventStore = useJidiSelectEventStore();
|
|
|
|
|
|
const baseid = ref('');
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
// 弹框控制
|
|
|
|
|
|
const modalVisible = ref(false);
|
|
|
|
|
|
const currentItem = ref<any>(null);
|
2026-07-07 10:33:29 +08:00
|
|
|
|
useDraggable(modalVisible, { boundary: true, resetOnOpen: true });
|
2026-06-24 11:26:19 +08:00
|
|
|
|
// 加载状态
|
|
|
|
|
|
const loading = ref(false);
|
|
|
|
|
|
|
|
|
|
|
|
// 原始媒体数据
|
|
|
|
|
|
const originalMediaData = ref<MediaItem[]>([]);
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
|
|
|
|
|
// 克隆首尾项后的渲染数组(用于无缝循环)
|
|
|
|
|
|
// 结构:[最后一项克隆, 原始数据..., 第一项克隆]
|
|
|
|
|
|
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 = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
const length = originalMediaData.value.length;
|
|
|
|
|
|
if (length === 0) return;
|
|
|
|
|
|
|
|
|
|
|
|
renderMediaData.value = [
|
|
|
|
|
|
originalMediaData.value[length - 1], // 克隆最后一项
|
|
|
|
|
|
...originalMediaData.value, // 原始数据
|
|
|
|
|
|
originalMediaData.value[0] // 克隆第一项
|
|
|
|
|
|
];
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 启动自动轮播
|
|
|
|
|
|
const startAutoPlay = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
if (timer) clearInterval(timer);
|
|
|
|
|
|
timer = setInterval(() => {
|
|
|
|
|
|
if (!isHovering.value && !isTransitioning.value) {
|
|
|
|
|
|
nextSlide();
|
|
|
|
|
|
}
|
|
|
|
|
|
}, 4000);
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 切换到下一张
|
|
|
|
|
|
const nextSlide = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
currentIndex.value++;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
// 延迟检查是否需要无缝跳转
|
|
|
|
|
|
setTimeout(() => {
|
|
|
|
|
|
checkSeamlessJump();
|
|
|
|
|
|
}, 500); // 与transition时间一致
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 检查是否需要无缝跳转(克隆项处理)
|
|
|
|
|
|
const checkSeamlessJump = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
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;
|
|
|
|
|
|
});
|
|
|
|
|
|
});
|
|
|
|
|
|
}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理鼠标进入
|
|
|
|
|
|
const handleMouseEnter = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
isHovering.value = true;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 处理鼠标离开
|
|
|
|
|
|
const handleMouseLeave = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
isHovering.value = false;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 计算当前显示的描述文字
|
|
|
|
|
|
const currentDescription = computed(() => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
const realIndex = getCurrentRealIndex();
|
|
|
|
|
|
return originalMediaData.value[realIndex]?.description || '';
|
2026-05-12 14:34:58 +08:00
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
|
|
// 获取当前真实的索引(排除克隆项的影响)
|
|
|
|
|
|
const getCurrentRealIndex = () => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
const realLength = originalMediaData.value.length;
|
|
|
|
|
|
let realIndex = currentIndex.value - 1; // 减去克隆的首项偏移
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
// 处理边界情况(无缝循环时的克隆项)
|
|
|
|
|
|
if (realIndex < 0) realIndex = realLength - 1;
|
|
|
|
|
|
if (realIndex >= realLength) realIndex = 0;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
return realIndex;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
|
|
// 跳转到指定幻灯片
|
|
|
|
|
|
const goToSlide = (targetIndex: number) => {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
if (isTransitioning.value) return;
|
|
|
|
|
|
|
|
|
|
|
|
// 计算目标索引(考虑克隆项偏移)
|
|
|
|
|
|
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;
|
|
|
|
|
|
};
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
};
|
2026-06-24 11:26:19 +08:00
|
|
|
|
|
|
|
|
|
|
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);
|
|
|
|
|
|
});
|
2026-05-12 14:34:58 +08:00
|
|
|
|
</script>
|
|
|
|
|
|
|
|
|
|
|
|
<style lang="scss" scoped>
|
|
|
|
|
|
.container {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 228px;
|
|
|
|
|
|
// border: 1px solid #7fd6ff;
|
|
|
|
|
|
// border-radius: 5px;
|
|
|
|
|
|
position: relative;
|
|
|
|
|
|
overflow: hidden; // 隐藏超出容器的内容
|
|
|
|
|
|
|
|
|
|
|
|
// 跑马灯轨道
|
|
|
|
|
|
.carousel-track {
|
|
|
|
|
|
display: flex; // 横向排列所有媒体项
|
2026-05-12 14:34:58 +08:00
|
|
|
|
width: 100%;
|
2026-06-24 11:26:19 +08:00
|
|
|
|
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 {
|
2026-05-12 14:34:58 +08:00
|
|
|
|
width: 100%;
|
|
|
|
|
|
height: 100%;
|
2026-06-24 11:26:19 +08:00
|
|
|
|
object-fit: cover; // 保持比例填充
|
|
|
|
|
|
}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
|
2026-06-24 11:26:19 +08:00
|
|
|
|
.text {
|
2026-05-12 14:34:58 +08:00
|
|
|
|
position: absolute;
|
2026-06-24 11:26:19 +08:00
|
|
|
|
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;
|
|
|
|
|
|
}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
}
|
2026-06-24 11:26:19 +08:00
|
|
|
|
}
|
2026-05-12 14:34:58 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 独立的文字说明区域
|
|
|
|
|
|
.description-text {
|
2026-06-24 11:26:19 +08:00
|
|
|
|
// 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;
|
2026-05-12 14:34:58 +08:00
|
|
|
|
}
|
2026-06-24 11:26:19 +08:00
|
|
|
|
</style>
|