Files
dinner.tootaio.com/demo/photoWall/v3/index.html
xiaomai 067f9d4828 feat: Update index.html and add media files for photo wall project
- Modified index.html to include favicon, title, and linked assets for Vite app.
- Added three new media files: LaguBangsaJohor.mp4, LaguNegaraku.mp4, and LaguTeoChew.mp4.
- Created nameList.json containing the names of the first founders with their status.
- Introduced demo/photoWall/v0/index.html for a dynamic carousel with background video and marquee text.
- Added demo/photoWall/v1/index.html for a photo wall layout with responsive design.
- Created demo/photoWall/v3/images.json and nameList.json for image and name data.
- Implemented demo/photoWall/v3/index.html with Vue.js for an interactive photo wall experience.
2025-11-09 23:38:01 +08:00

357 lines
12 KiB
HTML
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.
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>照片墙</title>
<!-- Vue 3 -->
<script src="https://unpkg.com/vue@3/dist/vue.global.prod.js"></script>
<!-- Tailwind 浏览器版(用于快速原型) -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
/* 顶部走马灯 */
.special-marquee-track {
animation: special-marquee-move-text 120s linear infinite;
}
@keyframes special-marquee-move-text {
to {
transform: translateX(-50%);
}
}
/* 布局小调整 */
.carousel-item {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
transition: transform 520ms cubic-bezier(0.2, 0.9, 0.2, 1), width 520ms,
opacity 520ms;
cursor: pointer;
}
</style>
</head>
<body class="bg-gray-900 text-white">
<div id="app" class="relative flex flex-col h-screen w-screen">
<!-- 背景视频(在最底层)打开页面自动播放 -->
<video
src="assets/麦卉 - 前人种树后人凉《潮州劲歌金曲》.mp4"
class="absolute w-full h-full object-cover blur-2xl"
autoplay
loop
controls
ref="bgVideo"
></video>
<!-- 顶部走马灯 -->
<div
class="bg-red-500 backdrop-blur-md rounded-t-xl border-t border-white/40 shadow-lg"
>
<div class="flex items-center">
<div
class="p-4 text-6xl font-bold text-nowrap bg-black/50 text-white"
>
{{bannerTitle}}
</div>
<div class="overflow-hidden flex-1 mask-x-from-95% mask-x-to-100%">
<div
class="flex w-max pl-8 gap-8 special-marquee-track items-center"
>
<template v-for="(n, idx) in nameListDoubled" :key="n._uid">
<div class="text-2xl select-none">
<div
v-if="n.isPassedAway"
class="px-3 py-1 border-2 border-white inline-block"
>
{{ n.name }}
</div>
<div v-else class="inline-block">{{ n.name }}</div>
</div>
</template>
</div>
</div>
</div>
</div>
<!-- 主体:图片走马灯 / 照片墙 -->
<div
class="relative flex-1 bg-linear-to-br from-red-600 via-purple-800 to-slate-900 overflow-hidden"
>
<!-- 左箭头 -->
<button
@click="prevImage"
class="absolute left-6 top-1/2 -translate-y-1/2 z-40 bg-black/40 p-3 rounded-full hover:bg-black/60"
>
</button>
<!-- 右箭头 -->
<button
@click="nextImage"
class="absolute right-6 top-1/2 -translate-y-1/2 z-40 bg-black/40 p-3 rounded-full hover:bg-black/60"
>
</button>
<!-- 图片:通过 computed 分配 transform -->
<template v-for="(img, i) in images" :key="img.id">
<img
:src="img.src"
:alt="img.alt || `Image ${i}`"
class="carousel-item rounded-2xl shadow-2xl pointer-events-none"
:class="imageWidthClass(i)"
:style="imageStyle(i)"
@click="onClickImage(i)"
@mouseenter="pauseAutoplay"
@mouseleave="resumeAutoplay"
:aria-hidden="i === currentIndex ? 'false' : 'true'"
:tabindex="i === currentIndex ? 0 : -1"
:title="img.alt || ''"
loading="lazy"
/>
</template>
<!-- 底部小点 -->
<div
class="absolute bottom-6 left-1/2 -translate-x-1/2 z-50 flex gap-2"
>
<button
v-for="(img, i) in images"
:key="img.id+'dot'"
@click="goTo(i)"
:class="['w-3 h-3 rounded-full', i===currentIndex ? 'bg-white' : 'bg-white/30']"
aria-label="'跳转到图片 '+(i+1)"
></button>
</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted, onBeforeUnmount } = Vue;
createApp({
setup() {
const bannerTitle = ref("照片墙");
const nameList = ref([]); // 从 JSON 加载
const images = ref([]); // 从 JSON 加载
const currentIndex = ref(0);
// 背景视频
const bgVideo = ref(null);
// 自动轮播控制
let timer = null;
const autoplayInterval = 4500;
const isPaused = ref(false);
// fetch 数据(示例 JSON 路径,按需修改)
const loadData = async () => {
try {
// 并行加载名字和图片(如果没有这些 JSON请用示例文件
const [r1, r2] = await Promise.all([
fetch("./nameList.json"),
fetch("./images.json"),
]);
if (!r1.ok)
throw new Error("加载 nameList.json 失败: " + r1.status);
if (!r2.ok)
throw new Error("加载 images.json 失败: " + r2.status);
const jd1 = await r1.json();
const jd2 = await r2.json();
bannerTitle.value = jd1.title || bannerTitle.value;
// 保证每个名字有稳定唯一 id避免重复 key
nameList.value = (jd1.nameList || []).map((n, idx) => ({
...n,
_uid: `n-${idx}`,
}));
images.value = (jd2.images || []).map((img, idx) => ({
id: img.id ?? `img-${idx}`,
src: img.src,
alt: img.alt ?? "",
}));
// 防止空数组导致问题:提供占位
if (!images.value.length) {
images.value = [
{
id: "placeholder",
src: "https://via.placeholder.com/1600x900?text=No+Image",
alt: "占位图",
},
];
}
} catch (err) {
console.error(err);
// 失败时回退到最小数据
bannerTitle.value = "照片墙";
nameList.value = [
{ name: "示例名字", isPassedAway: false, _uid: "n-demo" },
];
images.value = [
{
id: "fallback",
src: "https://placehold.co/1600x900?text=Fallback",
alt: "回退图",
},
];
}
};
onMounted(async () => {
await loadData();
startAutoplay();
// 键盘支持
window.addEventListener("keydown", onKeydown);
});
onBeforeUnmount(() => {
stopAutoplay();
window.removeEventListener("keydown", onKeydown);
});
// 走马灯:返回重复一遍的数组,用不同 uid
const nameListDoubled = computed(() => {
const first = nameList.value.map((n) => ({
...n,
_uid: n._uid + "-a",
}));
const second = nameList.value.map((n) => ({
...n,
_uid: n._uid + "-b",
}));
return [...first, ...second];
});
// 图片位置/样式逻辑(通用、支持任意图片数量)
const imageStyle = (i) => {
const n = images.value.length;
if (n === 0) return {};
// 相对偏移以百分比表示每张图片间隔125%
const rel = (i - currentIndex.value + n) % n; // 0..n-1
// 将 rel 转换成 -k .. +k 的范围(把较大的移到负端)
const middle = Math.floor(n / 2);
let signed = rel;
if (rel > middle) signed = rel - n;
const offsetPercent = signed * 125; // 每张间隔 125%
const isCenter = i === currentIndex.value;
const scale = isCenter ? 1.06 : 0.92;
const opacity =
Math.abs(signed) > 3
? 0
: Math.max(0.25, 1 - Math.abs(signed) * 0.18);
return {
transform: `translate(-50%, -50%) translateX(${offsetPercent}%) scale(${scale})`,
opacity: opacity,
};
};
// 宽度类(让中心图更大)
const imageWidthClass = (i) => {
return i === currentIndex.value ? "w-7xl" : "w-5xl";
};
// 操作
const prevImage = () => {
const n = images.value.length;
currentIndex.value = (currentIndex.value - 1 + n) % n;
};
const nextImage = () => {
const n = images.value.length;
currentIndex.value = (currentIndex.value + 1) % n;
};
const goTo = (i) => {
currentIndex.value = i;
};
// 点击图片:如果不是中心则跳到它
const onClickImage = (i) => {
if (i !== currentIndex.value) {
goTo(i);
} else {
// 中心图点击可以触发展示大图/详情(可扩展)
console.log("点击中心图", images.value[i]);
}
};
// 键盘支持
const onKeydown = (e) => {
const k = String(e.key);
if (k === "ArrowLeft") {
prevImage();
} else if (k === "ArrowRight") {
nextImage();
} else if (k === " ") {
// 空格暂停/恢复轮播(阻止页面滚动)
e.preventDefault();
if (isPaused.value) resumeAutoplay();
else pauseAutoplay();
} else if (k.toLowerCase() === "p") {
// 新增:按 'p' 切换背景视频播放/暂停
toggleBgVideo();
}
};
// 自动轮播
const startAutoplay = () => {
stopAutoplay();
timer = setInterval(() => {
if (!isPaused.value) nextImage();
}, autoplayInterval);
};
const stopAutoplay = () => {
if (timer) {
clearInterval(timer);
timer = null;
}
};
const pauseAutoplay = () => {
isPaused.value = true;
};
const resumeAutoplay = () => {
isPaused.value = false;
};
// 切换背景视频播放/暂停
const toggleBgVideo = () => {
const vid = bgVideo.value;
if (!vid) return;
try {
if (vid.paused) {
// play() 返回 Promisecatch 防止未授权自动播放报错
vid.play().catch(() => {});
} else {
vid.pause();
}
} catch (err) {
console.error("切换背景视频失败:", err);
}
};
return {
bannerTitle,
nameList,
nameListDoubled,
images,
currentIndex,
imageStyle,
imageWidthClass,
prevImage,
nextImage,
goTo,
onClickImage,
pauseAutoplay,
resumeAutoplay,
toggleBgVideo,
};
},
}).mount("#app");
</script>
</body>
</html>