This commit introduces a dynamic photo wall for the 2025-11-16 event. It features a 3D page-turning carousel built with Vue 3 and Tailwind CSS. The page includes an auto-playing background video, manual and automatic slideshow controls, and is responsive for mobile devices. 25 initial photos have been added to the gallery.
276 lines
8.5 KiB
HTML
276 lines
8.5 KiB
HTML
<!DOCTYPE html>
|
|
<html lang="zh-CN">
|
|
<head>
|
|
<meta charset="UTF-8" />
|
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
|
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
|
|
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
|
|
<title>动态翻页轮播图</title>
|
|
<style>
|
|
.carousel-container {
|
|
perspective: 1000px;
|
|
}
|
|
.carousel-item {
|
|
transition: transform 0.6s ease-in-out, opacity 0.6s ease-in-out;
|
|
transform-style: preserve-3d;
|
|
}
|
|
/* 响应式调整 */
|
|
@media (max-width: 768px) {
|
|
.carousel-container {
|
|
width: 100% !important;
|
|
height: 60vh !important;
|
|
}
|
|
.carousel-image {
|
|
width: 90% !important;
|
|
height: auto !important;
|
|
}
|
|
}
|
|
</style>
|
|
</head>
|
|
<body class="bg-red-500 min-h-screen flex items-center justify-center">
|
|
<div id="app">
|
|
<!-- 背景视频(在最底层)打开页面自动播放 -->
|
|
<video
|
|
src="assets/麦卉 - 前人种树后人凉《潮州劲歌金曲》.mp4"
|
|
class="absolute w-full h-full object-cover blur-2xl"
|
|
autoplay
|
|
loop
|
|
controls
|
|
></video>
|
|
<div
|
|
class="carousel-container w-screen h-screen flex items-center justify-center overflow-hidden"
|
|
>
|
|
<div class="relative w-full h-full flex items-center justify-center">
|
|
<!-- 轮播图片容器 -->
|
|
<div class="relative w-full h-full flex items-center justify-center">
|
|
<!-- 第一张图片 (prev) -->
|
|
<div
|
|
class="absolute carousel-item carousel-image h-172 w-7xl object-cover rounded-2xl shadow-2xl z-10"
|
|
:class="getImageClass(0)"
|
|
>
|
|
<img
|
|
:src="getImage(displayImages[0])"
|
|
:alt="`图片 ${displayImages[0] + 1}`"
|
|
class="w-full h-full object-cover rounded-2xl"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 第二张图片 (current) -->
|
|
<div
|
|
class="absolute carousel-item carousel-image h-172 w-7xl object-cover rounded-2xl shadow-2xl z-20"
|
|
:class="getImageClass(1)"
|
|
>
|
|
<img
|
|
:src="getImage(displayImages[1])"
|
|
:alt="`图片 ${displayImages[1] + 1}`"
|
|
class="w-full h-full object-cover rounded-2xl"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 第三张图片 (next) -->
|
|
<div
|
|
class="absolute carousel-item carousel-image h-172 w-7xl object-cover rounded-2xl shadow-2xl z-10"
|
|
:class="getImageClass(2)"
|
|
>
|
|
<img
|
|
:src="getImage(displayImages[2])"
|
|
:alt="`图片 ${displayImages[2] + 1}`"
|
|
class="w-full h-full object-cover rounded-2xl"
|
|
/>
|
|
</div>
|
|
|
|
<!-- 第四张图片 (helper) -->
|
|
<div
|
|
class="absolute carousel-item carousel-image h-172 w-7xl object-cover rounded-2xl shadow-2xl z-0"
|
|
:class="getImageClass(3)"
|
|
>
|
|
<img
|
|
:src="getImage(displayImages[3])"
|
|
:alt="`图片 ${displayImages[3] + 1}`"
|
|
class="w-full h-full object-cover rounded-2xl"
|
|
/>
|
|
</div>
|
|
</div>
|
|
|
|
<!-- 控制按钮 -->
|
|
<button
|
|
@click="prevSlide"
|
|
class="absolute left-4 z-30 bg-white/20 hover:bg-white/30 text-white p-3 rounded-full transition-all"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M15 19l-7-7 7-7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<button
|
|
@click="nextSlide"
|
|
class="absolute right-4 z-30 bg-white/20 hover:bg-white/30 text-white p-3 rounded-full transition-all"
|
|
>
|
|
<svg
|
|
xmlns="http://www.w3.org/2000/svg"
|
|
class="h-6 w-6"
|
|
fill="none"
|
|
viewBox="0 0 24 24"
|
|
stroke="currentColor"
|
|
>
|
|
<path
|
|
stroke-linecap="round"
|
|
stroke-linejoin="round"
|
|
stroke-width="2"
|
|
d="M9 5l7 7-7 7"
|
|
/>
|
|
</svg>
|
|
</button>
|
|
|
|
<!-- 指示器 -->
|
|
<!-- <div class="absolute bottom-4 z-30 flex space-x-2">
|
|
<button
|
|
v-for="i in images.length"
|
|
:key="i"
|
|
@click="goToSlide(i-1)"
|
|
class="w-3 h-3 rounded-full transition-all"
|
|
:class="currentIndex === i-1 ? 'bg-white' : 'bg-white/50'"
|
|
></button>
|
|
</div> -->
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
<script>
|
|
const { createApp, ref, computed, onMounted, onUnmounted } = Vue;
|
|
|
|
createApp({
|
|
setup() {
|
|
// 图片数组
|
|
const images = Array.from(
|
|
{ length: 25 },
|
|
(_, i) => `assets/image (${i + 1}).png`
|
|
);
|
|
|
|
// 当前索引
|
|
const currentIndex = ref(0);
|
|
|
|
// 显示图片的索引数组
|
|
const displayImages = ref([]);
|
|
|
|
// 初始化显示图片
|
|
const initializeDisplayImages = () => {
|
|
const len = images.length;
|
|
displayImages.value = [
|
|
(currentIndex.value - 1 + len) % len,
|
|
currentIndex.value,
|
|
(currentIndex.value + 1) % len,
|
|
(currentIndex.value + 2) % len,
|
|
];
|
|
};
|
|
|
|
// 获取图片URL
|
|
const getImage = (index) => images[index];
|
|
|
|
// 获取图片类名
|
|
const getImageClass = (position) => {
|
|
switch (position) {
|
|
case 0: // 上一张
|
|
return "scale-90 -translate-x-3/4 opacity-80";
|
|
case 1: // 当前
|
|
return "scale-100 translate-x-0 opacity-100";
|
|
case 2: // 下一张
|
|
return "scale-90 translate-x-3/4 opacity-80";
|
|
case 3: // 隐藏的搬运工
|
|
return "scale-90 translate-x-full opacity-0";
|
|
default:
|
|
return "";
|
|
}
|
|
};
|
|
|
|
// 轮播控制
|
|
const nextSlide = () => {
|
|
// 将第一个元素移到末尾
|
|
displayImages.value.push(displayImages.value.shift());
|
|
|
|
// 更新当前索引
|
|
currentIndex.value = (currentIndex.value + 1) % images.length;
|
|
|
|
// 更新最后一个元素(搬运工角色)
|
|
displayImages.value[3] = (currentIndex.value + 2) % images.length;
|
|
};
|
|
|
|
const prevSlide = () => {
|
|
// 将最后一个元素移到开头
|
|
displayImages.value.unshift(displayImages.value.pop());
|
|
|
|
// 更新当前索引
|
|
currentIndex.value =
|
|
(currentIndex.value - 1 + images.length) % images.length;
|
|
|
|
// 更新第一个元素(搬运工角色)
|
|
displayImages.value[0] =
|
|
(currentIndex.value - 1 + images.length) % images.length;
|
|
};
|
|
|
|
const goToSlide = (index) => {
|
|
const diff = index - currentIndex.value;
|
|
if (diff > 0) {
|
|
for (let i = 0; i < diff; i++) {
|
|
nextSlide();
|
|
}
|
|
} else if (diff < 0) {
|
|
for (let i = 0; i < Math.abs(diff); i++) {
|
|
prevSlide();
|
|
}
|
|
}
|
|
};
|
|
|
|
// 自动轮播
|
|
let autoPlayInterval = null;
|
|
|
|
const startAutoPlay = () => {
|
|
autoPlayInterval = setInterval(nextSlide, 3000);
|
|
};
|
|
|
|
const stopAutoPlay = () => {
|
|
if (autoPlayInterval) {
|
|
clearInterval(autoPlayInterval);
|
|
autoPlayInterval = null;
|
|
}
|
|
};
|
|
|
|
// 生命周期
|
|
onMounted(() => {
|
|
initializeDisplayImages();
|
|
startAutoPlay();
|
|
});
|
|
|
|
onUnmounted(() => {
|
|
stopAutoPlay();
|
|
});
|
|
|
|
return {
|
|
images,
|
|
currentIndex,
|
|
displayImages,
|
|
getImage,
|
|
getImageClass,
|
|
nextSlide,
|
|
prevSlide,
|
|
goToSlide,
|
|
startAutoPlay,
|
|
stopAutoPlay,
|
|
};
|
|
},
|
|
}).mount("#app");
|
|
</script>
|
|
</body>
|
|
</html>
|