Compare commits

..

10 Commits

Author SHA1 Message Date
xiaomai
cbc105ad03 chore: remove ignored files from tracking 2025-11-13 09:34:30 +08:00
xiaomai
2e5ba93083 feat(sponsorList): add 'Designed by' remark component
This commit introduces a new 'DesignedRemark' component to display an attribution link.

- Adds the Pacifico font for custom styling of the remark.
- Creates and registers the global `DesignedRemark` Vue component.
- Integrates the component into the mobile sponsor list page.
2025-11-13 08:30:34 +08:00
xiaomai
ba744f3381 feat(sponsorList): add mobile view and statistics panel
This commit introduces a new mobile-first version of the sponsor list page.

Key features:
- A responsive layout optimized for mobile devices.
- A statistics panel showing total sponsors, contribution amounts, and type distribution.
- Category filtering via a bottom navigation bar.

Additionally, the special sponsor message is now loaded from `sponsorList.json` to centralize data for both desktop and mobile views.
2025-11-13 06:56:29 +08:00
xiaomai
0b267aa524 feat(seo): add comprehensive metadata to sponsor list page
This commit enhances the sponsor list page with comprehensive metadata for SEO and social sharing. Key additions include a descriptive title, standard meta tags (description, keywords), JSON-LD structured data, Open
Graph and Twitter Card tags, and a canonical URL to improve discoverability and social media previews.
2025-11-12 22:16:21 +08:00
xiaomai
326ddee342 feat(sponsorList): enhance display with item sponsors and poems
This commit significantly updates the sponsor list page for the 2025/11/15 event.

- Adds support for displaying item-based sponsorships, including their details.
- Introduces a new visual layout with vertical poems on the sides, utilizing custom fonts.
- Updates the sponsor data with the latest list of cash, item, and table sponsors.
- Refactors the sorting logic to correctly prioritize all sponsor types.
- Centralizes font assets by updating paths to be absolute and removing redundant font files.
2025-11-12 21:54:45 +08:00
xiaomai
f1fe8eb559 feat(sponsorList): implement sponsor grouping and redesign UI
This commit introduces a major update to the sponsor list display.

- Implements a feature to group cash sponsors below a certain amount threshold. This helps to declutter the display when there are many smaller donations.
- Adds a new `cash-group` display type to handle the rendering of these grouped sponsors.
- Completely redesigns the UI with a new color scheme, gradients, and improved typography for a more polished and visually engaging presentation.
- Updates the styling for individual sponsor cards to improve readability and visual hierarchy.
2025-11-12 20:45:13 +08:00
xiaomai
899bbb490c Add sponsor list JSON and corresponding HTML for event celebration
- Created sponsorList.json with details of sponsors for the event "永平新港汕河体育协会 2 周年庆联欢晚宴".
- Developed index.html to display the event title, sponsor logos, and a scrolling list of sponsors using Vue.js.
- Implemented sorting logic for sponsors based on type and amount.
- Added animations for sponsor display and ensured responsive design.
2025-11-12 17:44:58 +08:00
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
xiaomai
60afabb845 feat(ui): add QR code to sponsor list and improve styling
This commit enhances the user interface for the sponsor list pages.

- A QR code linking to the mobile view is now displayed on the main sponsor list page.
- The QR code on the landing page now has a pulsing animation to draw attention.
- Readability of the sponsor list is improved by increasing font sizes and adjusting title colors.
- The 'Special Thanks' banner has been moved to the top for better visibility.
2025-11-09 12:12:42 +08:00
xiaomai
871e66a13a feat(event): add mobile sponsor list and refactor landing pages
This commit introduces a new mobile-first sponsor list and restructures the event landing pages. Key changes include:

- A new responsive sponsor list page built with Vue.js and Tailwind CSS, optimized for SEO and on-site viewing.
- The main event index page is now a QR code display for easy access to the mobile list.
- The original landing page is refactored with Tailwind CSS and moved to a new `/landing` directory.
- Umami analytics script is integrated across all pages for usage tracking.
2025-11-08 16:21:30 +08:00
24 changed files with 2818 additions and 483 deletions

102
.gitignore vendored
View File

@@ -1 +1,101 @@
.vscode
# ------------------------------
# General
# ------------------------------
# VS Code settings
.vscode/
# Node and build artifacts (if any future usage)
node_modules/
dist/
build/
out/
temp/
tmp/
# System files
.DS_Store
Thumbs.db
desktop.ini
# Logs
*.log
*.cache
*.tmp
# ------------------------------
# Project big / binary files
# ------------------------------
# Images
*.jpg
*.jpeg
*.png
*.gif
*.bmp
*.tiff
*.ico
*.webp
*.avif
*.heic
*.heif
# Video
*.mp4
*.mov
*.avi
*.mkv
*.webm
*.flv
*.wmv
*.mpeg
*.mpg
*.3gp
# Audio
*.mp3
*.wav
*.ogg
*.flac
*.m4a
*.aac
# Fonts
*.ttf
*.otf
*.woff
*.woff2
*.eot
# Archives
*.zip
*.rar
*.7z
*.tar
*.gz
*.bz2
# Documents / presentations
*.pdf
*.ppt
*.pptx
*.key
*.doc
*.docx
*.xls
*.xlsx
# ------------------------------
# Optional: ignore all files larger than 50MB (requires Git LFS or manual pre-commit hook)
# ------------------------------
# This is not a gitignore feature by itself, but as a reminder:
# git lfs track "*.mp4" "*.png" "*.ttf"
# git add .gitattributes
# ------------------------------
# Keep essential small files
# ------------------------------
!.gitignore
!**/*.html
!**/*.css
!**/*.js
!**/*.md
!**/*.txt

View File

@@ -21,19 +21,19 @@
@font-face {
font-family: "BaiGeTianXing";
src: url("../fonts/字魂白鸽天行体.ttf") format("truetype");
src: url("/fonts/字魂白鸽天行体.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "ShangShouGuHuang";
src: url("../fonts/ShangShouGuHuangTi-2.ttf") format("truetype");
src: url("/fonts/ShangShouGuHuangTi-2.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "YeZiTangYingHei";
src: url("../fonts/YeZiGongChangTangYingHei-2.ttf") format("truetype");
src: url("/fonts/YeZiGongChangTangYingHei-2.ttf") format("truetype");
font-display: swap;
}

View File

@@ -1,491 +1,131 @@
<!DOCTYPE html>
<html lang="zh-CN">
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>永平捷兔会30周年庆大跑晚宴</title>
<title>QR Code Modal</title>
<script src="/analysis.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
font-family: "Microsoft YaHei", "Segoe UI", sans-serif;
}
body {
background: linear-gradient(
135deg,
#0c0c0c 0%,
#1a1a2e 50%,
#1e0b0b 100%
);
color: #e0e0e0;
min-height: 100vh;
overflow-x: hidden;
position: relative;
}
/* 装饰性背景元素 */
.bg-pattern {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23d4af37' fill-rule='evenodd'/%3E%3C/svg%3E");
z-index: 0;
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 2rem;
position: relative;
z-index: 1;
}
/* 头部样式 */
header {
text-align: center;
padding: 2rem 0;
position: relative;
}
.logo {
font-size: 4.5rem;
font-weight: bold;
margin-bottom: 1rem;
color: #d4af37;
text-transform: uppercase;
position: relative;
display: inline-block;
text-shadow: 0 0 10px rgba(212, 175, 55, 0.5);
letter-spacing: 4px;
}
.tagline {
font-size: 1.8rem;
font-weight: 300;
letter-spacing: 3px;
color: #e8e8e8;
margin-bottom: 1rem;
}
.anniversary {
font-size: 8rem;
font-weight: bold;
color: #d4af37;
text-shadow: 0 0 20px rgba(212, 175, 55, 0.7);
margin: 2rem 0;
position: relative;
display: inline-block;
}
.anniversary::before,
.anniversary::after {
content: "";
position: absolute;
top: 50%;
width: 200px;
height: 3px;
background: linear-gradient(90deg, transparent, #d4af37, transparent);
}
.anniversary::before {
left: -220px;
}
.anniversary::after {
right: -220px;
}
.event-title {
font-size: 3.5rem;
font-weight: 500;
color: #e8e8e8;
margin-bottom: 2rem;
text-shadow: 0 0 10px rgba(255, 255, 255, 0.2);
}
/* 主要内容区域 */
.main-content {
display: flex;
flex-direction: column;
align-items: center;
padding: 2rem 0;
}
.welcome-text {
text-align: center;
max-width: 900px;
margin-bottom: 4rem;
background: rgba(30, 30, 46, 0.6);
padding: 2.5rem;
border-radius: 15px;
border: 1px solid rgba(212, 175, 55, 0.3);
backdrop-filter: blur(10px);
}
.welcome-text h2 {
font-size: 2.5rem;
font-weight: 400;
margin-bottom: 1.5rem;
color: #e8e8e8;
}
.welcome-text p {
font-size: 1.4rem;
line-height: 1.8;
color: #c0c0c0;
}
/* 导航卡片区域 */
.nav-cards {
display: grid;
grid-template-columns: repeat(auto-fit, minmax(320px, 1fr));
gap: 2.5rem;
width: 100%;
max-width: 1200px;
}
.nav-card {
background: rgba(30, 30, 46, 0.7);
border: 2px solid rgba(212, 175, 55, 0.4);
border-radius: 15px;
padding: 2.5rem;
text-align: center;
transition: all 0.4s ease;
position: relative;
overflow: hidden;
backdrop-filter: blur(10px);
}
.nav-card::before {
content: "";
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
background: linear-gradient(
135deg,
rgba(212, 175, 55, 0.1) 0%,
transparent 50%
);
z-index: -1;
}
.nav-card:hover {
transform: translateY(-10px);
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.4);
border-color: rgba(212, 175, 55, 0.7);
}
.card-icon {
font-size: 4rem;
color: #d4af37;
margin-bottom: 1.5rem;
}
.nav-card h3 {
font-size: 2rem;
font-weight: 500;
margin-bottom: 1.5rem;
color: #e8e8e8;
}
.nav-card p {
font-size: 1.2rem;
color: #b0b0b0;
margin-bottom: 2rem;
line-height: 1.6;
}
.nav-link {
display: inline-block;
padding: 1.2rem 2.5rem;
background: transparent;
color: #d4af37;
border: 2px solid #d4af37;
border-radius: 40px;
text-decoration: none;
font-weight: 500;
font-size: 1.3rem;
letter-spacing: 1px;
transition: all 0.3s ease;
position: relative;
overflow: hidden;
}
.nav-link::before {
content: "";
position: absolute;
top: 0;
left: -100%;
width: 100%;
height: 100%;
background: linear-gradient(
90deg,
transparent,
rgba(212, 175, 55, 0.2),
transparent
);
transition: all 0.6s;
}
.nav-link:hover {
background: rgba(212, 175, 55, 0.1);
box-shadow: 0 0 20px rgba(212, 175, 55, 0.4);
}
.nav-link:hover::before {
left: 100%;
}
/* 底部样式 */
footer {
text-align: center;
padding: 3rem 0 2rem;
margin-top: 4rem;
border-top: 1px solid rgba(255, 255, 255, 0.05);
color: #888;
font-size: 1.1rem;
}
/* 彩带装饰 */
.ribbon {
position: absolute;
width: 150px;
height: 150px;
background: #d4af37;
transform: rotate(45deg);
top: -75px;
right: -75px;
z-index: 2;
}
.ribbon::before,
.ribbon::after {
content: "";
position: absolute;
width: 100%;
height: 100%;
background: #d4af37;
}
.ribbon::before {
transform: rotate(90deg);
}
/* 响应式设计 */
@media (max-width: 1200px) {
.logo {
font-size: 3.5rem;
/* QR Code 脉冲动画 */
@keyframes qr-pulse {
0% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0.7);
}
.anniversary {
font-size: 6rem;
70% {
transform: scale(1.08);
box-shadow: 0 0 30px 10px rgba(255, 255, 255, 0);
}
.anniversary::before,
.anniversary::after {
width: 150px;
}
.anniversary::before {
left: -170px;
}
.anniversary::after {
right: -170px;
}
.event-title {
font-size: 2.8rem;
100% {
transform: scale(1);
box-shadow: 0 0 0 0 rgba(255, 255, 255, 0);
}
}
@media (max-width: 768px) {
.logo {
font-size: 2.8rem;
}
.anniversary {
font-size: 5rem;
}
.anniversary::before,
.anniversary::after {
width: 100px;
}
.anniversary::before {
left: -120px;
}
.anniversary::after {
right: -120px;
}
.event-title {
font-size: 2.2rem;
}
.welcome-text h2 {
font-size: 2rem;
}
.nav-cards {
grid-template-columns: 1fr;
}
}
@media (max-width: 480px) {
.container {
padding: 1rem;
}
header {
padding: 1.5rem 0;
}
.logo {
font-size: 2.2rem;
}
.anniversary {
font-size: 4rem;
}
.anniversary::before,
.anniversary::after {
width: 60px;
}
.anniversary::before {
left: -70px;
}
.anniversary::after {
right: -70px;
}
.event-title {
font-size: 1.8rem;
}
}
/* 跑步剪影装饰 */
.runner-silhouette {
position: absolute;
bottom: 0;
left: 5%;
width: 200px;
height: 200px;
background: url('data:image/svg+xml;utf8,<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 100 100"><path d="M20,70 C30,50 40,30 50,20 C60,30 70,50 80,70" stroke="%23d4af37" stroke-width="3" fill="none"/><circle cx="20" cy="70" r="5" fill="%23d4af37"/><circle cx="80" cy="70" r="5" fill="%23d4af37"/></svg>')
no-repeat center;
opacity: 0.2;
z-index: 0;
}
.runner-silhouette.right {
left: auto;
right: 5%;
transform: scaleX(-1);
.qr-pulse {
animation: qr-pulse 2s ease-in-out infinite;
}
</style>
</head>
<body>
<div class="bg-pattern"></div>
<div class="runner-silhouette"></div>
<div class="runner-silhouette right"></div>
<body class="bg-gray-100">
<div
class="relative w-screen h-screen bg-[url('./hash30bg.png')] bg-no-repeat bg-cover bg-center"
>
<!-- QR Code Container -->
<div
class="absolute top-16 left-16 flex flex-col items-center justify-center w-fit"
>
<div
onclick="openModal()"
class="cursor-pointer transition-transform duration-300 hover:scale-105"
>
<img
class="size-32 ring-8 ring-white shadow-lg qr-pulse border-8 border-white"
src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://dinner.tootaio.com/20251108/sponsorList/mobile/"
alt="Mobile Sponsor List QR"
/>
</div>
<div
class="text-2xl font-bold mt-4 text-black bg-white/50 px-4 py-2 rounded-full"
>
Scan for sponsor list!
</div>
</div>
<div class="container">
<header>
<h1 class="logo">永平捷兔会</h1>
<p class="tagline">三十载奔跑,荣耀同行</p>
<div class="anniversary">30</div>
<h2 class="event-title">周年庆大跑晚宴</h2>
</header>
<!-- Floating Buttons -->
<div class="fixed bottom-5 right-5 flex flex-col gap-2 z-40">
<button
class="bg-white/70 backdrop-blur-md shadow-md hover:bg-white/90 transition-all duration-300 px-4 py-2 rounded-full text-sm font-medium"
onclick="window.location.href='sponsorList/'"
>
Sponsor List
</button>
<button
class="bg-white/70 backdrop-blur-md shadow-md hover:bg-white/90 transition-all duration-300 px-4 py-2 rounded-full text-sm font-medium"
onclick="window.location.href='sponsorList/mobile/'"
>
Mobile List
</button>
</div>
</div>
<main class="main-content">
<div class="welcome-text">
<h2>欢迎莅临30周年庆典</h2>
<p>
三十载风雨同舟,感恩每一位捷兔会员的陪伴与支持。今晚,让我们共同庆祝这个荣耀时刻,回顾辉煌历程,展望美好未来!
<!-- Modal -->
<div
id="qrModal"
class="hidden fixed inset-0 z-50 flex items-center justify-center bg-black/70 backdrop-blur-sm"
>
<div class="relative max-w-md w-full mx-4 animate-[zoomIn_0.3s_ease-out]">
<button
onclick="closeModal()"
class="absolute -top-10 right-0 text-white text-3xl font-bold hover:scale-110 transition-transform"
>
&times;
</button>
<div
class="bg-white p-8 rounded-xl shadow-2xl flex flex-col items-center"
>
<h2 class="text-2xl font-bold mb-4 text-gray-800">Scan QR Code</h2>
<img
class="w-64 h-64 mb-4"
src="https://api.qrserver.com/v1/create-qr-code/?size=256x256&data=https://dinner.tootaio.com/20251108/sponsorList/mobile/"
alt="QR Code"
/>
<p class="text-gray-600 text-center">
Scan this QR code to view the sponsor list on your mobile device
</p>
</div>
<div class="nav-cards">
<div class="nav-card">
<div class="ribbon"></div>
<div class="card-icon">🏃‍♂️</div>
<h3>活动流程</h3>
<p>查看大跑活动路线、时间安排及晚宴流程,不错过每一个精彩环节。</p>
<a href="./eventSchedule" class="nav-link">查看流程</a>
</div>
<div class="nav-card">
<div class="card-icon">💎</div>
<h3>赞助商列表</h3>
<p>
感谢所有赞助商对本次活动的大力支持,让我们共同见证他们的品牌魅力。
</p>
<a href="./sponsorList" class="nav-link">查看赞助商</a>
</div>
<div class="nav-card">
<div class="card-icon">🏆</div>
<h3>荣誉殿堂</h3>
<p>回顾捷兔会30年辉煌历程见证会员们的荣耀时刻与珍贵回忆。</p>
<a href="./hallOfFame" class="nav-link">进入殿堂</a>
</div>
</div>
</main>
<footer>
<p>&copy; 2023 永平捷兔会 30周年庆典. 保留所有权利.</p>
</footer>
</div>
</div>
<script>
// 添加页面加载动画效果
document.addEventListener("DOMContentLoaded", function () {
// 延迟显示内容以增强加载体验
const container = document.querySelector(".container");
container.style.opacity = "0";
setTimeout(() => {
container.style.transition = "opacity 1.5s ease";
container.style.opacity = "1";
}, 300);
// 为导航链接添加点击效果
const navLinks = document.querySelectorAll(".nav-link");
navLinks.forEach((link) => {
link.addEventListener("click", function (e) {
e.preventDefault();
// 添加点击反馈
this.style.transform = "scale(0.95)";
// 模拟页面跳转延迟
setTimeout(() => {
window.location.href = this.getAttribute("href");
}, 300);
});
});
// 添加数字30的脉冲动画
const anniversary = document.querySelector(".anniversary");
setInterval(() => {
anniversary.style.transform = "scale(1.05)";
setTimeout(() => {
anniversary.style.transform = "scale(1)";
}, 500);
}, 3000);
});
const modal = document.getElementById("qrModal");
function openModal() {
modal.classList.remove("hidden");
}
function closeModal() {
modal.classList.add("hidden");
}
window.onclick = (e) => e.target === modal && closeModal();
document.addEventListener(
"keydown",
(e) => e.key === "Escape" && closeModal()
);
</script>
<style>
@keyframes zoomIn {
from {
transform: scale(0.8);
opacity: 0;
}
to {
transform: scale(1);
opacity: 1;
}
}
</style>
</body>
</html>

253
20251108/landing/index.html Normal file
View File

@@ -0,0 +1,253 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="utf-8" />
<meta name="viewport" content="width=device-width,initial-scale=1" />
<title>永平捷兔会30周年庆大跑晚宴</title>
<!-- 使用 @tailwindcss/browser CDN你已提供 -->
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<style>
/* 装饰性 SVG 图案与剪影仍用少量自定义样式 */
.bg-pattern {
position: fixed;
inset: 0;
z-index: 0;
opacity: 0.03;
background-image: url("data:image/svg+xml,%3Csvg width='100' height='100' viewBox='0 0 100 100' xmlns='http://www.w3.org/2000/svg'%3E%3Cpath d='M11 18c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm48 25c3.866 0 7-3.134 7-7s-3.134-7-7-7-7 3.134-7 7 3.134 7 7 7zm-43-7c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm63 31c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM34 90c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zm56-76c1.657 0 3-1.343 3-3s-1.343-3-3-3-3 1.343-3 3 1.343 3 3 3zM12 86c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm28-65c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm23-11c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-6 60c2.21 0 4-1.79 4-4s-1.79-4-4-4-4 1.79-4 4 1.79 4 4 4zm29 22c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zM32 63c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm57-13c2.76 0 5-2.24 5-5s-2.24-5-5-5-5 2.24-5 5 2.24 5 5 5zm-9-21c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM60 91c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM35 41c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2zM12 60c1.105 0 2-.895 2-2s-.895-2-2-2-2 .895-2 2 .895 2 2 2z' fill='%23d4af37' fill-rule='evenodd'/%3E%3C/svg%3E");
background-repeat: repeat;
}
.runner-silhouette {
position: fixed;
bottom: 0;
width: 220px;
height: 220px;
background-repeat: no-repeat;
background-position: center;
opacity: 0.18;
z-index: 0;
pointer-events: none;
}
.runner-left {
left: 5%;
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='M20,70 C30,50 40,30 50,20 C60,30 70,50 80,70' stroke='%23d4af37' stroke-width='3' fill='none'/><circle cx='20' cy='70' r='5' fill='%23d4af37'/><circle cx='80' cy='70' r='5' fill='%23d4af37'/></svg>");
}
.runner-right {
right: 5%;
transform: scaleX(-1);
background-image: url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' viewBox='0 0 100 100'><path d='M20,70 C30,50 40,30 50,20 C60,30 70,50 80,70' stroke='%23d4af37' stroke-width='3' fill='none'/><circle cx='20' cy='70' r='5' fill='%23d4af37'/><circle cx='80' cy='70' r='5' fill='%23d4af37'/></svg>");
}
/* Anniversary 横线装饰与 ribbon 仍需伪元素 —— 这里用 minimal CSS */
.anniversary {
position: relative;
--gold: #d4af37;
}
.anniversary::before,
.anniversary::after {
content: "";
position: absolute;
top: 50%;
width: 200px;
height: 3px;
background: linear-gradient(
90deg,
transparent,
var(--gold),
transparent
);
transform: translateY(-50%);
}
.anniversary::before {
left: -220px;
}
.anniversary::after {
right: -220px;
}
.ribbon {
position: absolute;
width: 150px;
height: 150px;
background: #d4af37;
transform: rotate(45deg);
top: -75px;
right: -75px;
z-index: 20;
}
@media (max-width: 1200px) {
.anniversary::before,
.anniversary::after {
width: 150px;
left: -170px;
right: -170px;
}
}
@media (max-width: 768px) {
.anniversary::before,
.anniversary::after {
width: 100px;
left: -120px;
right: -120px;
}
}
@media (max-width: 480px) {
.anniversary::before,
.anniversary::after {
width: 60px;
left: -70px;
right: -70px;
}
}
</style>
</head>
<body
class="min-h-screen text-gray-100 relative overflow-x-hidden"
style="
background: linear-gradient(
135deg,
#0c0c0c 0%,
#1a1a2e 50%,
#1e0b0b 100%
);
font-family: 'Microsoft YaHei', 'Segoe UI', sans-serif;
"
>
<!-- 背景图案与剪影 -->
<div class="bg-pattern" aria-hidden="true"></div>
<div class="runner-silhouette runner-left" aria-hidden="true"></div>
<div class="runner-silhouette runner-right" aria-hidden="true"></div>
<div class="max-w-7xl mx-auto px-6 py-10 relative z-10">
<header class="text-center mb-8">
<h1
class="inline-block text-[#d4af37] text-4xl md:text-6xl font-extrabold tracking-widest drop-shadow-lg"
>
永平捷兔会
</h1>
<p
class="mt-3 text-lg md:text-xl text-gray-200 font-light tracking-wider"
>
三十载奔跑,荣耀同行
</p>
<div class="mt-8 inline-block anniversary">
<div
class="text-[#d4af37] font-extrabold text-[4rem] md:text-[8rem] leading-none transform-gpu"
style="text-shadow: 0 0 20px rgba(212, 175, 55, 0.7)"
>
30
</div>
</div>
<h2
class="mt-6 text-2xl md:text-4xl text-gray-100 font-medium drop-shadow"
>
周年庆大跑晚宴
</h2>
</header>
<main class="flex flex-col items-center">
<section
class="w-full text-center max-w-3xl bg-white/5 backdrop-blur-sm border border-[rgba(212,175,55,0.15)] rounded-2xl p-6 md:p-10 mb-10"
>
<h3 class="text-2xl md:text-3xl font-semibold text-gray-100 mb-4">
欢迎莅临30周年庆典
</h3>
<p class="text-gray-300 leading-relaxed text-base md:text-lg">
三十载风雨同舟,感恩每一位捷兔会员的陪伴与支持。今晚,让我们共同庆祝这个荣耀时刻,回顾辉煌历程,展望美好未来!
</p>
</section>
<section class="w-full grid grid-cols-1 md:grid-cols-3 gap-6">
<article
class="relative bg-white/5 border border-[rgba(212,175,55,0.18)] rounded-2xl p-6 md:p-8 text-center transition-transform transform hover:-translate-y-2 hover:shadow-2xl overflow-hidden backdrop-blur-sm"
>
<div class="ribbon hidden md:block" aria-hidden="true"></div>
<div class="text-4xl mb-4">🏃‍♂️</div>
<h3 class="text-2xl font-semibold mb-3">活动流程</h3>
<p class="text-gray-300 mb-6">
查看大跑活动路线、时间安排及晚宴流程,不错过每一个精彩环节。
</p>
<a
href="./eventSchedule"
class="inline-block border-2 border-[#d4af37] text-[#d4af37] rounded-full px-6 py-2 text-lg font-medium hover:bg-[rgba(212,175,55,0.06)] transition"
>查看流程</a
>
</article>
<article
class="bg-white/5 border border-[rgba(212,175,55,0.18)] rounded-2xl p-6 md:p-8 text-center transition-transform transform hover:-translate-y-2 hover:shadow-2xl backdrop-blur-sm"
>
<div class="text-4xl mb-4">💎</div>
<h3 class="text-2xl font-semibold mb-3">赞助商列表</h3>
<p class="text-gray-300 mb-6">
感谢所有赞助商对本次活动的大力支持,让我们共同见证他们的品牌魅力。
</p>
<a
href="./sponsorList"
class="inline-block border-2 border-[#d4af37] text-[#d4af37] rounded-full px-6 py-2 text-lg font-medium hover:bg-[rgba(212,175,55,0.06)] transition"
>查看赞助商</a
>
</article>
<article
class="bg-white/5 border border-[rgba(212,175,55,0.18)] rounded-2xl p-6 md:p-8 text-center transition-transform transform hover:-translate-y-2 hover:shadow-2xl backdrop-blur-sm"
>
<div class="text-4xl mb-4">🏆</div>
<h3 class="text-2xl font-semibold mb-3">荣誉殿堂</h3>
<p class="text-gray-300 mb-6">
回顾捷兔会30年辉煌历程见证会员们的荣耀时刻与珍贵回忆。
</p>
<a
href="./hallOfFame"
class="inline-block border-2 border-[#d4af37] text-[#d4af37] rounded-full px-6 py-2 text-lg font-medium hover:bg-[rgba(212,175,55,0.06)] transition"
>进入殿堂</a
>
</article>
</section>
</main>
<footer class="mt-12 text-center text-gray-400 text-sm">
&copy; 2023 永平捷兔会 30周年庆典. 保留所有权利.
</footer>
</div>
<script>
// 轻量的页面加载与交互效果
document.addEventListener("DOMContentLoaded", () => {
const root = document.querySelector(".max-w-7xl");
if (root) {
root.style.opacity = 0;
root.style.transition = "opacity 1s ease";
setTimeout(() => (root.style.opacity = 1), 200);
}
// 点击按钮的反馈并导航
document.querySelectorAll("a").forEach((a) => {
a.addEventListener("click", (e) => {
const href = a.getAttribute("href");
if (!href || href.startsWith("http")) return; // 保持正常外链行为
e.preventDefault();
a.style.transform = "scale(0.96)";
setTimeout(() => (window.location.href = href), 220);
});
});
// Anniversary 脉冲(用 transform
const ann = document.querySelector(".anniversary > div");
if (ann) {
setInterval(() => {
ann.style.transform = "scale(1.05)";
setTimeout(() => (ann.style.transform = "scale(1)"), 500);
}, 3000);
}
});
</script>
</body>
</html>

View File

@@ -4,6 +4,8 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>Sponsor List</title>
<script src="/analysis.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
@@ -29,6 +31,7 @@
}
</style>
</head>
<body>
<div id="app" class="w-screen h-screen overflow-clip select-none">
<div class="flex w-full h-full">
@@ -36,6 +39,26 @@
<div
class="flex-8 relative bg-linear-to-b from-pink-900 to-pink-500 px-6"
>
<!-- QR Code Container -->
<div
class="absolute z-99 top-16 left-16 flex flex-col items-center justify-center w-fit"
>
<div
class="cursor-pointer transition-transform duration-300 hover:scale-105"
>
<img
class="size-32 ring-8 ring-white shadow-lg"
src="https://api.qrserver.com/v1/create-qr-code/?size=200x200&data=https://dinner.tootaio.com/20251108/sponsorList/mobile/"
alt="Mobile Sponsor List QR"
/>
</div>
<div
class="text-2xl font-bold mt-4 text-black bg-white/50 px-4 py-2 rounded-full"
>
Scan for sponsor list!
</div>
</div>
<!-- 上:可滚动 / 填满剩余空间 -->
<div class="absolute inset-0 overflow-hidden">
<div class="sponsor-marquee-track">
@@ -66,7 +89,7 @@
class="backdrop-blur-xl bg-linear-to-br from-white/20 to-white/10 border border-white/30 rounded-2xl p-8 shadow-2xl"
>
<div
:class="['text-center font-bold mb-4', (value.titleFontSize || 'text-4xl')]"
:class="['text-center font-bold mb-4 text-amber-400', (value.titleFontSize || 'text-4xl')]"
>
{{ formatRm(key) }}
</div>
@@ -76,7 +99,7 @@
<div
v-for="sponsor in value.list"
:key="sponsor"
class="text-2xl border-white border px-4 py-2 rounded-md shadow-md text-white"
class="text-4xl border-white border px-4 py-2 rounded-md shadow-md text-white"
>
{{sponsor}}
</div>
@@ -89,7 +112,7 @@
<!-- 下:特别鸣谢 -->
<div
class="absolute bottom-0 left-0 right-0 bg-white/30 backdrop-blur-md rounded-t-xl border-t border-white/40 shadow-lg"
class="absolute top-0 left-0 right-0 bg-white/30 backdrop-blur-md rounded-t-xl border-t border-white/40 shadow-lg"
>
<div class="flex items-center">
<div class="p-6 text-3xl font-bold text-nowrap">特别鸣谢:</div>

View File

@@ -0,0 +1,511 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>永平捷兔会 30 周年庆赞助商 | 永平捷兔会大跑晚宴名单</title>
<script src="/analysis.js"></script>
<!-- ✅ SEO 基本信息 -->
<meta
name="description"
content="永平捷兔会 30 周年庆赞助商名单与赞助金额统计,包括特别赞助、各级别赞助人数与总额分布。感谢所有支持这场盛会的朋友与企业!"
/>
<meta
name="keywords"
content="永平捷兔会, 捷兔会, 30周年, 赞助商, 大跑晚宴, 永平, 马来西亚活动, TooTaio Studio"
/>
<meta name="author" content="TooTaio Studio" />
<meta name="robots" content="index, follow" />
<!-- ✅ Open Graph 社交分享 -->
<meta property="og:type" content="website" />
<meta
property="og:title"
content="永平捷兔会 30 周年庆赞助商名单 | 永平捷兔会大跑晚宴"
/>
<meta
property="og:description"
content="查看永平捷兔会 30 周年庆大跑晚宴赞助商名单与金额统计,感谢所有慷慨支持的赞助者!"
/>
<meta
property="og:image"
content="https://tootaio.com/assets/yphs30-share.jpg"
/>
<meta property="og:url" content="https://tootaio.com/yphs30" />
<meta property="og:site_name" content="TooTaio Studio" />
<!-- ✅ Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="永平捷兔会 30 周年庆赞助商名单" />
<meta
name="twitter:description"
content="感谢所有支持永平捷兔会 30 周年庆大跑晚宴的赞助者!"
/>
<meta
name="twitter:image"
content="https://tootaio.com/assets/yphs30-share.jpg"
/>
<!-- ✅ Favicon -->
<link rel="icon" type="image/png" href="https://tootaio.com/favicon.png" />
<!-- ✅ Canonical URL -->
<link rel="canonical" href="https://tootaio.com/yphs30" />
<!-- ✅ Schema.org JSON-LD 结构化数据 -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Event",
"name": "永平捷兔会 30 周年庆大跑晚宴",
"description": "永平捷兔会 30 周年庆典,展示所有赞助商名单、赞助级别与感谢名单。",
"image": "https://tootaio.com/assets/yphs30-share.jpg",
"startDate": "2025-11-30T19:00",
"location": {
"@type": "Place",
"name": "永平",
"address": {
"@type": "PostalAddress",
"addressLocality": "Yong Peng",
"addressCountry": "Malaysia"
}
},
"organizer": {
"@type": "Organization",
"name": "永平捷兔会",
"url": "https://tootaio.com"
}
}
</script>
<!-- icons + tailwind + vue -->
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<script src="https://cdn.tailwindcss.com"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style>
/* font + tiny custom animation kept (Tailwind 用于绝大多数样式) */
@import url("https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;600;700&display=swap");
body {
font-family: "Noto Sans SC", sans-serif;
}
/* animated gradient footer */
@keyframes glow {
0% {
background-position: 200% 0;
}
100% {
background-position: -200% 0;
}
}
.glow-bg {
background: linear-gradient(90deg, #ffff00, #bbbb00, #ffff00);
background-size: 200% 100%;
animation: glow 3s linear infinite;
}
</style>
</head>
<body class="bg-gray-50">
<div
id="app"
class="container mx-auto px-4 py-8 pb-24 md:py-10 md:max-w-6xl"
>
<!-- header -->
<header class="bg-[#2c5aa0] text-white rounded-lg p-5 text-center mb-6">
<h1 class="text-2xl md:text-3xl font-bold">永平捷兔会 30 周年庆</h1>
<p class="text-md md:text-lg mt-1">大跑晚宴赞助商名单</p>
</header>
<!-- stats -->
<section class="grid grid-cols-1 md:grid-cols-2 gap-4 mb-6">
<div
class="bg-white rounded-lg shadow-sm border border-gray-100 p-5 text-center"
>
<div class="text-2xl md:text-3xl font-bold text-[#2c5aa0]">
{{ totalSponsors }}
</div>
<div class="text-gray-600 mt-2">赞助人数</div>
</div>
<div
class="bg-white rounded-lg shadow-sm border border-gray-100 p-5 text-center"
>
<div class="text-2xl md:text-3xl font-bold text-[#2c5aa0]">
{{ totalAmount }}
</div>
<div class="text-gray-600 mt-2">赞助总额</div>
</div>
</section>
<!-- main content: sponsors + sidebar -->
<main class="grid grid-cols-1 lg:grid-cols-3 gap-6">
<!-- sponsors list (wide) -->
<section class="lg:col-span-2">
<h2 class="text-xl font-bold mb-4 text-gray-800">赞助商列表</h2>
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-2 gap-4">
<article
v-for="(category, key) in sponsors"
:key="key"
class="bg-white rounded-lg shadow-sm border border-gray-100 overflow-hidden"
>
<header
class="flex items-center justify-between px-4 py-3 bg-[#2c5aa0] text-white"
>
<h3 :class="category.titleFontSize || 'text-lg'">
{{ formatKey(key) }}
</h3>
<span class="text-sm bg-white bg-opacity-20 px-2 py-1 rounded"
>{{ category.list.length }} 人</span
>
</header>
<div class="p-4 flex flex-wrap gap-2">
<span
v-for="sponsor in category.list"
:key="sponsor"
class="text-sm bg-gray-100 px-3 py-1 rounded border border-gray-200"
>{{ sponsor }}</span
>
</div>
</article>
</div>
<!-- special sponsors -->
<h3 class="text-xl font-bold mt-8 mb-4 text-gray-800">特别赞助</h3>
<div class="space-y-3">
<div
v-for="sponsor in specialSponsors"
:key="sponsor"
class="bg-[#fff9e6] border-l-4 border-[#ffc107] px-4 py-3 rounded-r"
>
<i class="fas fa-star text-yellow-500 mr-2"></i>
<span>{{ sponsor }}</span>
</div>
</div>
</section>
<!-- sidebar stats -->
<aside>
<!-- level stats -->
<div
class="bg-white rounded-lg shadow-sm border border-gray-100 p-5 mb-6"
>
<h4 class="text-lg font-bold mb-4 text-center text-gray-800">
赞助级别统计
</h4>
<div v-for="level in levelStats" :key="level.name" class="mb-4">
<div class="flex justify-between text-sm mb-2">
<span>{{ level.name }}</span>
<span>{{ level.count }}人 ({{ level.percentage }}%)</span>
</div>
<div class="h-2 bg-gray-200 rounded overflow-hidden">
<div
class="h-full bg-[#2c5aa0]"
:style="{ width: level.percentage + '%' }"
></div>
</div>
</div>
</div>
<!-- amount distribution chart -->
<div class="bg-white rounded-lg shadow-sm border border-gray-100 p-5">
<h4 class="text-lg font-bold mb-4 text-center text-gray-800">
赞助金额分布
</h4>
<div
v-for="level in levelStats"
:key="level.name"
class="flex items-center mb-3"
>
<div class="w-24 text-sm">{{ level.name }}</div>
<div
class="flex-1 h-6 bg-gray-200 rounded overflow-hidden mx-3 relative"
>
<div
class="h-full rounded flex items-center justify-end pr-2 font-medium text-white bg-[#2c5aa0]"
:style="{ width: level.amountPercentage + '%' }"
>
<span v-if="level.amountPercentage > 10"
>{{ level.amountPercentage }}%</span
>
</div>
</div>
<div class="w-20 text-right font-medium text-sm">
{{ level.amount }}
</div>
</div>
</div>
</aside>
</main>
<!-- footer -->
<footer class="fixed bottom-0 left-0 w-full z-50">
<div class="glow-bg py-3 text-center">
<a
href="https://tootaio.com"
target="_blank"
class="text-black font-extrabold text-lg"
>
Designed by
<span class="font-extrabold"
><span class="text-red-500">TooTaio</span> Studio</span
>
</a>
</div>
</footer>
</div>
<script>
const { createApp, ref, computed } = Vue;
createApp({
setup() {
const eventTitle = ref("永平捷兔会 30 周年庆大跑晚宴");
const formatKey = (key) =>
key
.replace(/([A-Z])/g, " $1")
.replace(/^./, (s) => s.toUpperCase());
const sponsors = ref({
Rm5000: { list: ["Top Gan"] },
Rm2000: {
list: [
"Ketua Kampung",
"校长",
"Natural 9",
"明盛弟",
"联合周",
"新成酒家",
],
},
Rm1600: { list: ["Moon"] },
Rm1200: { list: ["Labis Bon"] },
Rm1000: { list: ["Angel"] },
Rm800: {
list: [
"拿督",
"Mari Chan",
"霖主席",
"MDL阿德",
"Cool Lo",
"刘薇薇",
],
},
Rm500: { list: ["Hun Shap Tou", "Good Man", "学生妹", "三太子"] },
Rm400: {
showTogether: true,
list: [
"Founder Koh",
"Founder Ang",
"Founder Koid",
"Superman",
"榴梿",
"阳阳",
"Lawyer",
"小老板",
"富婆",
"Farmer",
"Farmer嫂",
"Durian King",
"大傻",
"Datin",
"Mohamad Ali",
"Wireman",
"Ki Ka Poh",
"TV",
"Sexy",
"Uncle Low",
"小黑",
"黑夫人",
"Jimmy",
"Ah Boon",
"米桶",
"Pasar Malam",
"Public Ong",
"Pet pet",
"梅惠",
"910",
"伟哥",
"仙女",
"Boss",
"彩虹",
"花瓶",
"黑珍珠",
"木薯老板",
"美国佬",
"三公子",
"来",
"宝强",
"Steven",
"老二",
"车斗Lau",
"海南Huat",
"龙门铁宝",
"山竹祥",
"旺庆",
"福承",
"立家",
"鸿兴",
"江老板",
"Wong Long",
"健芳",
"杨文德",
"古早味",
"猪笼",
"Bangkali Pusing",
"999",
"012",
"Wu Wei Xiong",
"Fan Shu",
"肥福",
"Kulai:阿祥",
"Kulai:Robert",
"国宝",
"Puki Ayam",
"爱情鸟",
"William Soh",
"Darren",
"林总",
"High More",
"Tiger",
"Lim Kopi",
"林董",
"Lighting Chan",
"Jag",
"Corina",
"Jiu Xiao",
"Love Bird",
"Sepuluh Dua",
"哈哈",
"狗爷",
],
},
Rm200: {
showTogether: true,
list: [
"兰总",
"Kampopo",
"Lim Kee Meng",
"003",
"Tan Brother",
"E-Sun",
"Kami",
"Kami嫂",
"美发师",
"美女",
"Oong Lai",
"乃乃",
"Naluri",
"妹子",
"Joan",
"Kai De Tan",
"企鹅",
"老二",
"二娘",
"DJ Yap",
"菜头",
"Ketam",
"Roket",
"土豪",
"天鹅",
"老板娘",
"Momo",
"Happyman",
"阿琳",
"花木兰",
"财政",
"财政夫人",
"走火",
"表妹",
"鸡脚老大",
"阿锦",
"维哥",
"牛车轮",
"陈进平",
],
},
});
const specialSponsors = ref([
"HEINEKEN MARKETING MALAYSIA SDN.BHD.: 100 件 T-Shirt & 500 件小毛巾",
"花奇 Nou: 大蛋糕一个",
"绝世旅游 500 - 550 环保袋",
]);
const totalSponsors = computed(() => {
let count = 0;
Object.values(sponsors.value).forEach(
(cat) => (count += cat.list.length)
);
return count;
});
const totalAmount = computed(() => {
let total = 0;
Object.entries(sponsors.value).forEach(([key, value]) => {
const amount = parseFloat(key.replace(/[^\d.]/g, "")) || 0;
total += amount * value.list.length;
});
return "RM" + total.toLocaleString("en-MY");
});
const levelStats = computed(() => {
const stats = [];
let totalCount = 0;
let totalAmountNum = 0;
Object.entries(sponsors.value).forEach(([key, value]) => {
totalCount += value.list.length;
const amount = parseFloat(key.replace(/[^\d.]/g, "")) || 0;
totalAmountNum += amount * value.list.length;
});
Object.entries(sponsors.value).forEach(([key, value]) => {
const amount = parseFloat(key.replace(/[^\d.]/g, "")) || 0;
const levelAmount = amount * value.list.length;
const percentage = (
(value.list.length / (totalCount || 1)) *
100
).toFixed(1);
const amountPercentage = (
(levelAmount / (totalAmountNum || 1)) *
100
).toFixed(1);
stats.push({
name: formatKey(key),
count: value.list.length,
amount: "RM" + levelAmount.toLocaleString("en-MY"),
percentage,
amountPercentage,
});
});
return stats;
});
return {
eventTitle,
formatKey,
sponsors,
specialSponsors,
totalSponsors,
totalAmount,
levelStats,
};
},
}).mount("#app");
</script>
</body>
</html>

14
20251115/.prettierrc Normal file
View File

@@ -0,0 +1,14 @@
{
"overrides": [
{
"files": ["sponsorList.json"],
"options": {
"printWidth": 9999,
"tabWidth": 2,
"useTabs": false,
"trailingComma": "none",
"bracketSpacing": true
}
}
]
}

87
20251115/sponsorList.json Normal file
View File

@@ -0,0 +1,87 @@
{
"eventTitle": "永平新港汕河体育协会 2 周年庆联欢晚宴",
"specialSponsors": "感谢 V World2.0 的特别赞助,现在下载 APP 并进行实名认证即可获得 RM50 的登录奖励!",
"logos": [{ "imgSrc": "SamHor-HighRes.png" }, { "imgSrc": "关公文化-HighRes.png" }, { "imgSrc": "VWorld2 Logo.png" }],
"poems": ["汕水流长通四海", "河川万里泽邦家"],
"sponsorList": [
{ "name": "李金盛仁兄", "type": "item", "detail": "Camus Extra Elegance 一支" },
{ "name": "许敏捷(四哥)", "type": "cash", "amount": 1000 },
{ "name": "无尘", "type": "cash", "amount": 500 },
{ "name": "CC集团", "type": "item", "detail": "迎宾舞狮" },
{ "name": "永平福满家", "type": "item", "detail": "大彩" },
{ "name": "kenzo 祥", "type": "cash", "amount": 1000 },
{ "name": "莲花村洪河公司", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "砂益洪河公司", "type": "item", "detail": "Gold Label 一支" },
{ "name": "巴力西贡洪河公司", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "黄伟能", "type": "cash", "amount": 200 },
{ "name": "皇后福名氏", "type": "cash", "amount": 500 },
{ "name": "Richard Woon", "type": "item", "detail": "Hennessy VSOP 3L 大炮 一支" },
{ "name": "WHEEL MASTER TYRE & MOTORSPORTS SDN BHD", "type": "cash", "amount": 1000 },
{ "name": "海金贸易", "type": "cash", "amount": 300 },
{ "name": "molek洪河公司", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "新山大马关帝文化推广中心报效", "type": "item", "detail": "DON CRILLION XO 一支" },
{ "name": "南利三川福利联谊会", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "dabee", "type": "cash", "amount": 500 },
{ "name": "拿督刘书致", "type": "cash", "amount": 3000 },
{ "name": "兄弟", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "砂劳越洪河公司", "type": "cash", "amount": 3000 },
{ "name": "V-world 2.0", "type": "cash", "amount": 2000 },
{ "name": "陈德福报效", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "MDL Auto Werk", "type": "cash", "amount": 500 },
{ "name": "郑旭辉", "type": "cash", "amount": 2000 },
{ "name": "黄潮明", "type": "cash", "amount": 300 },
{ "name": "王添顺贤兄", "type": "item", "detail": "J.W Gold Label 色酒 2支" },
{ "name": "九酝云科技有限公司", "type": "item", "detail": "贵州茅台 4支" },
{ "name": "马六甲洪河公司", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "顺发汽车修理有限公司", "type": "cash", "amount": 500 },
{ "name": "Vworld2.0 88Group", "type": "item", "detail": "Remy Martin XO CNY 2025 700ml 一支" },
{ "name": "UU摄影艺术馆", "type": "item", "detail": "大笔进财酒 一支" },
{ "name": "颜在专", "type": "item", "detail": "Johnnie Walker X.R 21yrs CNY F25 750ml 一支" },
{ "name": "林樑展仁兄", "type": "item", "detail": "Hennessy XO 一支" },
{ "name": "新邦令金洪河公司", "type": "item", "detail": "Martell Cordon Bleu 700ml 一支" },
{ "name": "黄鎔敬", "type": "cash", "amount": 100 },
{ "name": "啊南", "type": "cash", "amount": 750 },
{ "name": "69 Group", "type": "table", "amount": 1, "note": "" },
{ "name": "领善精密刀具贸易", "type": "table", "amount": 1, "note": "" },
{ "name": "农景汶", "type": "table", "amount": 1, "note": "" },
{ "name": "地南祥子", "type": "table", "amount": 1, "note": "" },
{ "name": "好运99", "type": "table", "amount": 1, "note": "" },
{ "name": "宝哥榴莲", "type": "table", "amount": 1, "note": "" },
{ "name": "道士强", "type": "table", "amount": 1, "note": "" },
{ "name": "张章理", "type": "table", "amount": 1, "note": "" },
{ "name": "新成酒家", "type": "table", "amount": 1, "note": "" },
{ "name": "新港村委会", "type": "table", "amount": 1, "note": "" },
{ "name": "明安熟食广场有限公司", "type": "table", "amount": 1, "note": "" },
{ "name": "李桂安", "type": "table", "amount": 1, "note": "" },
{ "name": "林樑展", "type": "table", "amount": 1, "note": "" },
{ "name": "永平阿光", "type": "table", "amount": 1, "note": "" },
{ "name": "王添顺贤兄", "type": "table", "amount": 1, "note": "" },
{ "name": "美珍生果贸易有限公司", "type": "table", "amount": 1, "note": "" },
{ "name": "荣成生命礼仪关怀服务有限公司", "type": "table", "amount": 1, "note": "" },
{ "name": "莫少权", "type": "table", "amount": 1, "note": "" },
{ "name": "世纪白钢五金", "type": "table", "amount": 1, "note": "" },
{ "name": "小猪 & 小许", "type": "table", "amount": 1, "note": "" },
{ "name": "林联裕", "type": "table", "amount": 1, "note": "" },
{ "name": "阿富", "type": "table", "amount": 1, "note": "" },
{ "name": "颜在专", "type": "table", "amount": 1, "note": "" },
{ "name": "阿依淡洪日山世缘", "type": "table", "amount": 1, "note": "" },
{ "name": "峇株罗汉坛", "type": "table", "amount": 1, "note": "" },
{ "name": "新邦令金城阴殿", "type": "table", "amount": 1, "note": "" },
{ "name": "Momo Music cafe", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "黄伟胜", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "黑青", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "南利三川福利联谊会", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "旺兴打嗎甲烧焊喷漆车厂", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "李新标", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "永平业余钓鱼俱乐部", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "永平快乐园", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "龙园海鲜", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "阿平油棕", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "诚信油棕", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "和合汽车服务中心", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "陈德福", "type": "table", "amount": 0.5, "note": "半桌" },
{ "name": "二师兄", "type": "table", "amount": 2, "note": "" }
]
}

View File

@@ -0,0 +1,369 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>赞助名单 | Sponsor List - 永平新港汕河二周年联欢晚宴</title>
<script src="/analysis.js"></script>
<!-- ✅ 基本 SEO 元信息 -->
<meta
name="description"
content="汕河活动赞助名单与特别鸣谢。感谢所有支持汕河文化与社区发展的赞助商,让我们携手共建更美好的明天。"
/>
<meta
name="keywords"
content="汕河, Sponsor List, 赞助名单, 活动赞助, 马来西亚汕头同乡会, 捐款名单, 汕河活动"
/>
<meta name="author" content="汕河活动组委会" />
<meta name="robots" content="index, follow" />
<!-- ✅ 结构化数据Google Rich Snippet -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Event",
"name": "汕河赞助名单",
"description": "汕河文化活动赞助名单与鸣谢,包含现金赞助、物品赞助与特别支持单位。",
"startDate": "2025-11-12",
"eventAttendanceMode": "https://schema.org/OnlineEventAttendanceMode",
"eventStatus": "https://schema.org/EventScheduled",
"organizer": {
"@type": "Organization",
"name": "汕河活动组委会",
"url": "https://dinner.tootaio.com"
},
"image": "https://dinner.tootaio.com/assets/og-cover.jpg",
"url": "https://dinner.tootaio.com/sponsor-list"
}
</script>
<!-- ✅ Open Graph for Facebook / 微信 / LINE 分享 -->
<meta property="og:type" content="website" />
<meta property="og:title" content="汕河赞助名单 | Sponsor List" />
<meta
property="og:description"
content="感谢所有支持汕河文化活动的赞助单位与个人,让爱心与文化同行。"
/>
<meta
property="og:image"
content="https://dinner.tootaio.com/assets/og-cover.jpg"
/>
<meta property="og:url" content="https://dinner.tootaio.com/sponsor-list" />
<meta property="og:site_name" content="汕河活动" />
<!-- ✅ Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta name="twitter:title" content="汕河赞助名单 | Sponsor List" />
<meta
name="twitter:description"
content="汕河活动赞助名单与特别鸣谢。感谢所有支持汕河文化的伙伴!"
/>
<meta
name="twitter:image"
content="https://dinner.tootaio.com/assets/og-cover.jpg"
/>
<!-- ✅ Favicon -->
<link rel="icon" href="/favicon.ico" />
<!-- ✅ Canonical URL避免重复收录 -->
<link rel="canonical" href="https://dinner.tootaio.com/sponsor-list" />
<!-- ✅ PWA / Mobile 优化 -->
<meta name="theme-color" content="#B91C1C" />
<meta name="apple-mobile-web-app-title" content="汕河赞助名单" />
<link rel="apple-touch-icon" href="/assets/og-cover.jpg" />
<script src="/analysis.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<style type="text/tailwindcss">
@theme {
/* 🎨 定义自定义字体族变量 */
--font-bai-ge: "BaiGeTianXing", sans-serif;
--font-gu-huang: "ShangShouGuHuang", sans-serif;
--font-tang-ying: "YeZiTangYingHei", sans-serif;
}
@font-face {
font-family: "BaiGeTianXing";
src: url("/fonts/字魂白鸽天行体.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "ShangShouGuHuang";
src: url("/fonts/ShangShouGuHuangTi-2.ttf") format("truetype");
font-display: swap;
}
@font-face {
font-family: "YeZiTangYingHei";
src: url("/fonts/YeZiGongChangTangYingHei-2.ttf") format("truetype");
font-display: swap;
}
.poem {
@apply flex-1 [writing-mode:vertical-rl] flex items-center justify-center text-6xl font-bai-ge tracking-[0.5em] text-white ring-amber-300 text-shadow-lg text-shadow-black;
}
</style>
<style>
.sponsor-marquee-track {
animation: sponsor-marquee-move-text 120s linear infinite;
}
.special-marquee-track {
animation: special-marquee-move-text 30s linear infinite;
}
@keyframes sponsor-marquee-move-text {
to {
transform: translateY(-50%);
}
}
@keyframes special-marquee-move-text {
to {
transform: translateX(-50%);
}
}
</style>
</head>
<body>
<div id="app" class="h-screen flex flex-col select-none">
<div class="text-center text-6xl font-bold p-8 bg-red-700 text-white">
{{eventTitle}}
</div>
<!-- 特别赞助 -->
<div class="py-6 bg-yellow-300 text-red-500 shadow-yellow-500 shadow-lg">
<div class="overflow-clip mask-x-from-95% mask-x-to-100%">
<div class="flex pl-4 gap-4 w-max special-marquee-track text-4xl">
<div v-for="sponsor in specialSponsorDouble" :key="sponsor">
{{sponsor}}
</div>
</div>
</div>
</div>
<!-- 下半部分二八分,左侧放 image右侧走马灯 -->
<div
class="flex-1 flex min-h-0 overflow-clip bg-linear-to-b from-red-500 to-red-900"
>
<!-- <div class="flex-2 flex flex-col items-center justify-around">
<img
v-for="logo in logos"
:src="`../assets/${logo.imgSrc}`"
:alt="logo.imgSrc"
class="w-[80%] drop-shadow-2xl"
/>
</div> -->
<div class="poem">汕水流长通四海</div>
<div class="flex-8 inset-red-lg">
<div class="flex flex-col pt-4 gap-4 px-4 sponsor-marquee-track">
<div
v-for="sponsor in sponsorListDouble"
:key="sponsor"
class="bg-linear-to-br from-white/20 to-white/10 px-16 py-4 rounded-2xl border-2 border-white/40"
>
<div v-if="sponsor.type == 'cash'" class="text-center py-8">
<div class="text-7xl text-yellow-400 font-bold">
RM {{sponsor.displayAmount}}
</div>
<div
class="text-7xl text-white text-shadow-amber-400 text-shadow-lg"
>
{{sponsor.name}}
</div>
</div>
<div
v-else-if="sponsor.type == 'cash-group'"
class="text-center py-8"
>
<div class="text-7xl text-yellow-400 font-bold mb-8">
RM {{sponsor.amount}}
</div>
<div class="flex flex-wrap gap-4">
<div
v-for="child in sponsor.children"
class="text-3xl px-4 py-2 bg-white/40 rounded-2xl"
>
{{child}}
</div>
</div>
</div>
<div
v-else-if="sponsor.type == 'table'"
class="flex items-center"
>
<div
class="text-3xl bg-red-500 text-white font-bold px-4 py-2 border-2 border-white rounded-full"
>
{{tableToSeats(sponsor.amount)}}
</div>
<div class="text-4xl font-bold flex-1 ml-4 text-white">
{{sponsor.name}}
</div>
</div>
<div v-else-if="sponsor.type == 'item'" class="flex items-center">
<div
class="text-3xl bg-red-500 text-white font-bold px-4 py-2 border-2 border-white rounded-full"
>
{{sponsor.detail}}
</div>
<div class="text-4xl font-bold flex-1 ml-4 text-white">
{{sponsor.name}}
</div>
</div>
</div>
</div>
</div>
<div class="poem">河川万里泽邦家</div>
</div>
</div>
<script>
const { createApp, ref, computed, onMounted } = Vue;
createApp({
setup() {
const eventTitle = ref("");
const logos = ref([]);
const sponsorList = ref([]); // Load from JSON
const specialSponsor = ref("");
const typePriority = {
cash: 4,
"cash-group": 3,
table: 2,
item: 1,
};
const groupByCashLessThan = ref(0);
function sortSponsors(list) {
return list.sort((a, b) => {
// 先按 type 优先级排序
const typeDiff = typePriority[b.type] - typePriority[a.type];
if (typeDiff !== 0) return typeDiff;
// 若 type 相同,再按 amount 从大到小
if (b.amount !== a.amount) return b.amount - a.amount;
// 若 amount 相同,最后按名称排序(可选)
return a.name.localeCompare(b.name, "zh");
});
}
const loadData = async () => {
try {
const [sponsorListResult] = await Promise.all([
fetch("../sponsorList.json"),
]);
if (!sponsorListResult.ok) {
throw new Error(
"Error while loading sponsorList: " + sponsorListResult.status
);
}
const sponsorListJsonData = await sponsorListResult.json();
eventTitle.value = sponsorListJsonData.eventTitle || "活动名称";
specialSponsor.value = sponsorListJsonData.specialSponsor || "";
logos.value = sponsorListJsonData.logos || [];
sponsorList.value = (
sponsorListJsonData.sponsorList || []
).reduce((acc, s, idx) => {
const sponsor = {
...s,
displayAmount:
s.type == "cash"
? s.amount.toLocaleString("en-MY")
: s.amount,
_uid: `s-${idx}`,
};
// 如果是现金赞助且金额小于阈值
if (s.type === "cash" && s.amount < groupByCashLessThan.value) {
// 查找是否已存在该金额的分组
const groupId = `group-${s.amount}`;
let amountGroup = acc.find((item) => item._uid === groupId);
if (!amountGroup) {
// 创建新的金额分组
amountGroup = {
name: "其他赞助商",
type: "cash-group",
amount: s.amount, // 保持原始数字,不格式化
children: [],
_uid: groupId,
};
acc.push(amountGroup);
}
// 将赞助商名称添加到分组的children中
amountGroup.children.push(s.name);
} else {
// 其他赞助商直接添加到结果中
acc.push(sponsor);
}
return acc;
}, []);
// Sort SponsorList by type and amount
sponsorList.value = sortSponsors(sponsorList.value);
console.log(sponsorList.value);
} catch (err) {
console.error(err);
}
};
onMounted(async () => {
await loadData();
});
const sponsorListDouble = computed(() => {
const a = sponsorList.value.map((s) => ({
...s,
_uid: s._uid + "-a",
}));
const b = sponsorList.value.map((s) => ({
...s,
_uid: s._uid + "-b",
}));
return [...a, ...b];
});
const specialSponsorDouble = computed(() => [
specialSponsor.value,
specialSponsor.value,
specialSponsor.value,
specialSponsor.value,
]);
const tableToSeats = (amount) => {
switch (amount) {
case 2:
return "两席";
case 1:
return "一席";
case 0.5:
return "半席";
default:
console.error("Error while converting table amount: ", amount);
}
};
return {
eventTitle,
logos,
sponsorListDouble,
specialSponsorDouble,
tableToSeats,
};
},
}).mount("#app");
</script>
</body>
</html>

View File

@@ -0,0 +1,264 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>永平新港汕河体育协会 2 周年庆联欢晚宴|电子征信录与赞助名单</title>
<!-- ✅ SEO 基础优化 -->
<meta
name="description"
content="永平新港汕河体育协会 2 周年庆联欢晚宴电子征信录,完整收录所有赞助商与支持者名单,感谢社会各界的热心赞助与参与。"
/>
<meta
name="keywords"
content="永平新港汕河体育协会, 永平, 汕河, 体育会, 赞助名单, 周年庆, 晚宴, 电子征信录, Malaysia, Yong Peng"
/>
<!-- ✅ Open Graph (社交分享预览) -->
<meta property="og:type" content="website" />
<meta
property="og:title"
content="永平新港汕河体育协会 2 周年庆联欢晚宴 - 电子征信录"
/>
<meta
property="og:description"
content="感谢所有赞助商与嘉宾的支持,共襄盛举。查看完整赞助名单与活动记录。"
/>
<meta property="og:image" content="https://example.com/assets/cover.jpg" />
<meta property="og:url" content="https://example.com/sponsor-record" />
<meta property="og:site_name" content="永平新港汕河体育协会" />
<!-- ✅ Twitter Card -->
<meta name="twitter:card" content="summary_large_image" />
<meta
name="twitter:title"
content="永平新港汕河体育协会 2 周年庆联欢晚宴"
/>
<meta
name="twitter:description"
content="电子征信录|感谢所有赞助商与支持者"
/>
<meta name="twitter:image" content="https://example.com/assets/cover.jpg" />
<!-- ✅ Favicon -->
<link rel="icon" href="/favicon.ico" />
<!-- ✅ 性能优化 -->
<link rel="preconnect" href="https://cdn.jsdelivr.net" />
<link rel="preconnect" href="https://unpkg.com" />
<link rel="preconnect" href="https://cdnjs.cloudflare.com" />
<!-- ✅ JSON-LD 结构化数据 (Google 识别为活动页面) -->
<script type="application/ld+json">
{
"@context": "https://schema.org",
"@type": "Event",
"name": "永平新港汕河体育协会 2 周年庆联欢晚宴",
"startDate": "2025-11-22T19:00",
"location": {
"@type": "Place",
"name": "永平新港汕河体育协会会所",
"address": "Yong Peng, Johor, Malaysia"
},
"image": "https://example.com/assets/cover.jpg",
"description": "庆祝永平新港汕河体育协会成立两周年的晚宴活动,并公布电子征信录感谢所有赞助商与支持者。",
"organizer": {
"@type": "Organization",
"name": "永平新港汕河体育协会",
"url": "https://example.com"
}
}
</script>
<script src="/analysis.js"></script>
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
<script src="https://unpkg.com/vue@3/dist/vue.global.js"></script>
<link
rel="stylesheet"
href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css"
/>
<style type="text/tailwindcss">
@theme {
/* 🎨 定义自定义字体族变量 */
--font-pacifico: "Pacifico", cursive;
}
@font-face {
font-family: "Pacifico";
src: url("/fonts/Pacifico-Regular.ttf") format("truetype");
font-display: swap;
}
</style>
</head>
<body class="bg-gray-50">
<div id="app" class="h-screen flex flex-col">
<!-- 头部 -->
<header
class="bg-radial from-orange-600 to-red-600 text-white p-4 shadow-md"
>
<div class="flex justify-between items-center mb-2">
<h1 class="text-lg font-bold truncate">{{ eventTitle }}</h1>
<button
@click="showStats = !showStats"
class="bg-white/20 p-2 rounded-full"
>
<i class="fas fa-chart-bar"></i>
</button>
</div>
<!-- Logo区域 -->
<div class="flex justify-center space-x-4 overflow-x-auto py-2">
<div v-for="(logo, index) in logos" :key="index" class="shrink-0">
<div
class="bg-white rounded-lg p-2 w-16 h-16 flex items-center justify-center"
>
<img :src="`../../assets/${logo.imgSrc}`" :alt="logo.imgSrc" />
</div>
</div>
</div>
</header>
<!-- 特别赞助商 -->
<div
v-if="specialSponsor"
class="bg-yellow-100 border-l-4 border-yellow-500 p-3 mx-4 mt-4 rounded-r"
>
<div class="flex items-start">
<i class="fas fa-star text-yellow-500 mt-1 mr-2"></i>
<p class="text-sm text-yellow-800">{{ specialSponsor }}</p>
</div>
</div>
<!-- 统计数据 -->
<div v-if="showStats" class="bg-white m-4 p-4 rounded-lg shadow-sm">
<h2 class="text-lg font-semibold mb-3 flex items-center">
<i class="fas fa-chart-pie mr-2 text-blue-500"></i>赞助统计
</h2>
<div class="grid grid-cols-2 gap-3">
<div class="bg-blue-50 p-3 rounded-lg">
<p class="text-xs text-blue-700">赞助商总数</p>
<p class="text-xl font-bold">{{ stats.totalSponsors }}</p>
</div>
<div class="bg-green-50 p-3 rounded-lg">
<p class="text-xs text-green-700">现金赞助总额</p>
<p class="text-xl font-bold">
RM {{ stats.totalCash.toLocaleString() }}
</p>
</div>
<div class="bg-purple-50 p-3 rounded-lg">
<p class="text-xs text-purple-700">标品赞助数量</p>
<p class="text-xl font-bold">{{ stats.totalItems }}</p>
</div>
<div class="bg-orange-50 p-3 rounded-lg">
<p class="text-xs text-orange-700">赞助席位总数</p>
<p class="text-xl font-bold">
{{ tableToSeats(stats.totalTables) }}
</p>
</div>
</div>
<div class="mt-4">
<h3 class="text-sm font-medium mb-2">赞助类型分布</h3>
<div class="space-y-2">
<div
v-for="type in stats.typeDistribution"
:key="type.name"
class="flex items-center"
>
<div class="w-20 text-xs">{{ type.name }}</div>
<div class="flex-1 bg-gray-200 rounded-full h-2">
<div
:class="type.color"
class="h-2 rounded-full"
:style="{ width: type.percentage + '%' }"
></div>
</div>
<div class="w-10 text-right text-xs">{{ type.count }}</div>
</div>
</div>
</div>
<designed-remark />
</div>
<!-- 赞助商分类 -->
<div class="flex-1 overflow-auto">
<!-- 赞助商列表 -->
<div class="p-4">
<div
v-if="filteredSponsors.length === 0"
class="text-center py-8 text-gray-500"
>
<i class="fas fa-inbox text-4xl mb-2"></i>
<p>暂无此类赞助商</p>
</div>
<div v-else class="space-y-4">
<!-- 现金赞助商 -->
<sponsor-card
v-for="s in filteredSponsors"
:key="s._uid"
:sponsor="s"
/>
</div>
</div>
<designed-remark />
</div>
<!-- 底部导航 -->
<footer class="bg-white border-t p-3 flex justify-around">
<button
v-for="category in categories"
:key="category.type"
@click="activeCategory = category.type"
:class="['flex flex-col items-center text-xs', (activeCategory === category.type ? category.color : 'text-gray-400')]"
>
<i :class="['text-lg mb-1', category.icon]"></i>
<span>{{category.name}}</span>
</button>
</footer>
</div>
<template id="designed-remark-template">
<a href="https://tootaio.com/">
<div class="text-center my-4">
<i>Designed by</i>
<span class="font-pacifico">
<span class="text-red-500">TooTaio</span>
Studio
</span>
</div>
</a>
</template>
<template id="sponsor-card-template">
<div class="bg-white rounded-lg shadow-sm p-4">
<div class="flex justify-between items-start">
<h3 class="font-medium text-gray-800">{{ sponsor.name }}</h3>
<span
:class="['text-sm px-2 py-1 rounded-full', sponsor.pillBg, sponsor.pillColor]"
>
<span v-if="sponsor.type == 'cash'" class="whitespace-nowrap">
RM {{ sponsor.displayAmount }}
</span>
<span v-else-if="sponsor.type == 'item'">
<i class="fas fa-gift mr-1"></i>标品赞助
</span>
<span v-else-if="sponsor.type == 'table'">{{sponsor.seats}}</span>
</span>
</div>
<p v-if="sponsor.detail" class="text-sm text-gray-600 mt-2">
{{ sponsor.detail }}
</p>
<slot name="extra"></slot>
</div>
</template>
<script src="./script.js?v=3"></script>
</body>
</html>

View File

@@ -0,0 +1,300 @@
const { createApp, ref, computed, onMounted } = Vue;
const SponsorCard = {
props: {
sponsor: { type: Object, required: true },
},
template: "#sponsor-card-template",
};
const DesignedRemark = {
template: "#designed-remark-template",
};
const app = createApp({
setup() {
const eventTitle = ref("");
const logos = ref([]);
const sponsorList = ref([]); // Load from JSON
const specialSponsor = ref("");
const showStats = ref(false);
const activeCategory = ref("all");
const categories = [
{
type: "all",
name: "全部",
icon: "fas fa-list",
color: "text-blue-500",
},
{
type: "cash",
name: "现金",
icon: "fas fa-money-bill-wave",
color: "text-green-500",
pillBg: "bg-green-100",
pillColor: "text-green-800",
},
{
type: "item",
name: "标品",
icon: "fas fa-gift",
color: "text-purple-500",
pillBg: "bg-purple-100",
pillColor: "text-purple-800",
},
{
type: "table",
name: "桌位",
icon: "fas fa-utensils",
color: "text-orange-500",
pillBg: "bg-orange-100",
pillColor: "text-orange-800",
},
];
const typePriority = {
cash: 4,
"cash-group": 3,
table: 2,
item: 1,
};
const groupByCashLessThan = ref(0);
function sortSponsors(list) {
return list.sort((a, b) => {
// 先按 type 优先级排序
const typeDiff = typePriority[b.type] - typePriority[a.type];
if (typeDiff !== 0) return typeDiff;
// 若 type 相同,再按 amount 从大到小
if (b.amount !== a.amount) return b.amount - a.amount;
// 若 amount 相同,最后按名称排序(可选)
return a.name.localeCompare(b.name, "zh");
});
}
// 统计数据
const stats = computed(() => {
const result = {
totalSponsors: 0,
totalCash: 0,
totalItems: 0,
totalTables: 0,
typeDistribution: [],
};
// 计算各类赞助商数量
const typeCounts = {
cash: 0,
"cash-group": 0,
item: 0,
table: 0,
};
// 计算总额
sponsorList.value.forEach((sponsor) => {
if (sponsor.type === "cash") {
result.totalCash += sponsor.amount;
typeCounts.cash++;
} else if (sponsor.type === "cash-group") {
result.totalCash += sponsor.amount * sponsor.children.length;
typeCounts["cash-group"]++;
} else if (sponsor.type === "item") {
result.totalItems++;
typeCounts.item++;
} else if (sponsor.type === "table") {
result.totalTables += sponsor.amount;
typeCounts.table++;
}
});
// 计算总赞助商数量
result.totalSponsors = sponsorList.value.length;
// 计算类型分布
const total = result.totalSponsors;
result.typeDistribution = [
{
name: "现金赞助",
count: typeCounts.cash + typeCounts["cash-group"],
percentage: Math.round(
((typeCounts.cash + typeCounts["cash-group"]) / total) * 100
),
color: "bg-green-500",
},
{
name: "标品赞助",
count: typeCounts.item,
percentage: Math.round((typeCounts.item / total) * 100),
color: "bg-purple-500",
},
{
name: "桌位赞助",
count: typeCounts.table,
percentage: Math.round((typeCounts.table / total) * 100),
color: "bg-orange-500",
},
];
return result;
});
// 过滤赞助商
const filteredSponsors = computed(() => {
if (activeCategory.value === "all") {
return sponsorList.value;
}
return sponsorList.value.filter(
(sponsor) => sponsor.type === activeCategory.value
);
});
const loadData = async () => {
try {
const [sponsorListResult] = await Promise.all([
fetch("../../sponsorList.json"),
]);
if (!sponsorListResult.ok) {
throw new Error(
"Error while loading sponsorList: " + sponsorListResult.status
);
}
const sponsorListJsonData = await sponsorListResult.json();
eventTitle.value = sponsorListJsonData.eventTitle || "活动名称";
specialSponsor.value = sponsorListJsonData.specialSponsors || "";
logos.value = sponsorListJsonData.logos || [];
sponsorList.value = (sponsorListJsonData.sponsorList || []).reduce(
(acc, s, idx) => {
const category = categories.find((c) => c.type === s.type);
const sponsor = {
...s,
displayAmount:
s.type == "cash" ? s.amount.toLocaleString("en-MY") : s.amount,
seats: s.type == "table" ? tableToSeats(s.amount) : "",
pillBg: category?.pillBg,
pillColor: category?.pillColor,
_uid: `s-${idx}`,
};
// 如果是现金赞助且金额小于阈值
if (s.type === "cash" && s.amount < groupByCashLessThan.value) {
// 查找是否已存在该金额的分组
const groupId = `group-${s.amount}`;
let amountGroup = acc.find((item) => item._uid === groupId);
if (!amountGroup) {
// 创建新的金额分组
amountGroup = {
name: "其他赞助商",
type: "cash-group",
displayAmount:
s.type == "cash"
? s.amount.toLocaleString("en-MY")
: s.amount,
amount: s.amount, // 保持原始数字,不格式化
children: [],
_uid: groupId,
};
acc.push(amountGroup);
}
// 将赞助商名称添加到分组的children中
amountGroup.children.push(s.name);
} else {
// 其他赞助商直接添加到结果中
acc.push(sponsor);
}
return acc;
},
[]
);
// Sort SponsorList by type and amount
sponsorList.value = sortSponsors(sponsorList.value);
console.log(sponsorList.value);
} catch (err) {
console.error(err);
}
};
onMounted(async () => {
await loadData();
});
const tableToSeats = (input) => {
if (input === null || input === undefined || input === "") return "";
if (input === 0.5) return "半席";
const n = Number(input);
if (Number.isNaN(n)) throw new TypeError("输入不是有效数字: " + input);
const eps = 1e-9;
const sign = n < 0 ? "负" : "";
const abs = Math.abs(n);
// 把数值四舍五入到最接近的 0.5(保证容错)
const rounded = Math.round(abs * 2) / 2;
// 分离整数和小数部分
let intPart = Math.floor(rounded + eps);
const rem = rounded - intPart; // 只可能是 0 或 0.5
// 如果 rounded 恰好是 X.0 的情况下 rem ~ 0如果是 X.5 则 rem ~ 0.5
// 特殊:如果 rem === 1极罕见就进位上面已用 round 避免)
// 小范围容错处理
const isHalf = Math.abs(rem - 0.5) < eps;
const isInteger = Math.abs(rem) < eps;
// 数字到小范围中文(只对 0/1/2 做汉字)
const smallChinese = (num) => {
if (num === 0) return "零";
if (num === 1) return "一";
if (num === 2) return "两";
return null;
};
const numLabel = (() => {
const c = smallChinese(intPart);
return c !== null ? c : String(intPart);
})();
// 判断 numLabel 是阿拉伯数字还是汉字(用于决定是否在数字与“席”之间加空格)
const isDigits = /^\d+$/.test(numLabel);
if (isInteger) {
// 整数
return isDigits ? `${sign}${numLabel}` : `${sign}${numLabel}`;
} else if (isHalf) {
// 半席
return isDigits ? `${sign}${numLabel} 席半` : `${sign}${numLabel}席半`;
} else {
// 理论上不会到这里(因为已 round 到 0.5),但兜底返回字符串
return `${sign}${numLabel}`;
}
};
return {
eventTitle,
logos,
sponsorList,
specialSponsor,
showStats,
activeCategory,
categories,
stats,
filteredSponsors,
tableToSeats,
};
},
});
app.component("designed-remark", DesignedRemark); // 全局注册
app.component("sponsor-card", SponsorCard); // 全局注册
app.mount("#app");

View File

@@ -0,0 +1,3 @@
竞标软件
背景设计静态(要有 Logo
Sponsor List

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

29
20251116/images.json Normal file
View File

@@ -0,0 +1,29 @@
{
"images": [
{ "src": "./assets/image (1).png" },
{ "src": "./assets/image (2).png" },
{ "src": "./assets/image (3).png" },
{ "src": "./assets/image (4).png" },
{ "src": "./assets/image (5).png" },
{ "src": "./assets/image (6).png" },
{ "src": "./assets/image (7).png" },
{ "src": "./assets/image (8).png" },
{ "src": "./assets/image (9).png" },
{ "src": "./assets/image (10).png" },
{ "src": "./assets/image (11).png" },
{ "src": "./assets/image (12).png" },
{ "src": "./assets/image (13).png" },
{ "src": "./assets/image (14).png" },
{ "src": "./assets/image (15).png" },
{ "src": "./assets/image (16).png" },
{ "src": "./assets/image (17).png" },
{ "src": "./assets/image (18).png" },
{ "src": "./assets/image (19).png" },
{ "src": "./assets/image (20).png" },
{ "src": "./assets/image (21).png" },
{ "src": "./assets/image (22).png" },
{ "src": "./assets/image (23).png" },
{ "src": "./assets/image (24).png" },
{ "src": "./assets/image (25).png" }
]
}

View File

@@ -1,11 +1,14 @@
<!DOCTYPE html>
<html lang="en">
<head>
<html lang="">
<head>
<meta charset="UTF-8">
<link rel="icon" href="/20251116/favicon.ico">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>永平潮州会馆</title>
</head>
<body>
</body>
</html>
<title>Vite App</title>
<script type="module" crossorigin src="/20251116/assets/index-B7KJ4W4F.js"></script>
<link rel="stylesheet" crossorigin href="/20251116/assets/index-BCuZh3Sz.css">
</head>
<body>
<div id="app"></div>
</body>
</html>

46
20251116/nameList.json Normal file
View File

@@ -0,0 +1,46 @@
{
"title": "2005年第一届创馆人",
"nameList": [
{ "name": "楊吉陽", "isPassedAway": true },
{ "name": "唐華", "isPassedAway": true },
{ "name": "許仁菘", "isPassedAway": true },
{ "name": "許汶信", "isPassedAway": true },
{ "name": "劉炎松", "isPassedAway": true },
{ "name": "楊淑清", "isPassedAway": true },
{ "name": "黃芝芳", "isPassedAway": true },
{ "name": "劉吉棟", "isPassedAway": false },
{ "name": "許任隆", "isPassedAway": false },
{ "name": "楊順發", "isPassedAway": false },
{ "name": "吳祥森", "isPassedAway": false },
{ "name": "林庭芝", "isPassedAway": false },
{ "name": "林炳華", "isPassedAway": false },
{ "name": "林庭珠", "isPassedAway": false },
{ "name": "李玉媚", "isPassedAway": false },
{ "name": "林應財", "isPassedAway": false },
{ "name": "許斯杰", "isPassedAway": false },
{ "name": "許敏捷", "isPassedAway": false },
{ "name": "許智興", "isPassedAway": false },
{ "name": "劉德祥", "isPassedAway": false },
{ "name": "李豫梅", "isPassedAway": false },
{ "name": "黄潮明", "isPassedAway": false },
{ "name": "楊信陞", "isPassedAway": false },
{ "name": "蔡立義", "isPassedAway": false },
{ "name": "林炳龍", "isPassedAway": false },
{ "name": "劉振昌", "isPassedAway": false },
{ "name": "劉迪發", "isPassedAway": false },
{ "name": "楊美雄", "isPassedAway": false },
{ "name": "彭三媚", "isPassedAway": false },
{ "name": "楊光豐", "isPassedAway": false },
{ "name": "楊秀娥", "isPassedAway": false },
{ "name": "莊秀清", "isPassedAway": false },
{ "name": "李玉嬌", "isPassedAway": false },
{ "name": "趙惜嬌", "isPassedAway": false },
{ "name": "陳秀珠", "isPassedAway": false },
{ "name": "張彩雁", "isPassedAway": false },
{ "name": "劉暐康", "isPassedAway": false },
{ "name": "王貴興", "isPassedAway": false },
{ "name": "劉林順", "isPassedAway": false },
{ "name": "劉益華", "isPassedAway": false },
{ "name": "紀有平", "isPassedAway": false }
]
}

View File

10
analysis.js Normal file
View File

@@ -0,0 +1,10 @@
(function () {
const UMAMI_SCRIPT_JS = "https://umami.tootaio.com/script.js";
const UMAMI_ID = "8a1eb7cf-3173-4fa2-9824-36aa300ef868";
var script = document.createElement("script");
script.async = true;
script.src = UMAMI_SCRIPT_JS;
script.setAttribute("data-website-id", UMAMI_ID);
document.head.appendChild(script);
})();

View File

@@ -0,0 +1,250 @@
<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<title>宴会回忆录照片墙</title>
<style>
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: "Helvetica Neue", Arial, sans-serif;
background: linear-gradient(135deg, #1a2a6c, #b21f1f, #fdbb2d);
height: 100vh;
overflow: hidden;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
color: white;
padding: 20px;
}
.header {
text-align: center;
margin-bottom: 30px;
z-index: 10;
background: rgba(0, 0, 0, 0.3);
padding: 15px 30px;
border-radius: 10px;
backdrop-filter: blur(5px);
}
h1 {
font-size: 3.5rem;
margin-bottom: 10px;
text-shadow: 2px 2px 4px rgba(0, 0, 0, 0.5);
}
.subtitle {
font-size: 1.5rem;
opacity: 0.9;
}
.photo-wall {
display: grid;
grid-template-columns: repeat(5, 1fr);
grid-template-rows: repeat(5, 1fr);
gap: 15px;
width: 95%;
height: 70%;
max-width: 1400px;
perspective: 1000px;
}
.photo-container {
position: relative;
border-radius: 8px;
overflow: hidden;
box-shadow: 0 10px 20px rgba(0, 0, 0, 0.3);
transition: transform 0.5s ease, z-index 0.5s;
transform-style: preserve-3d;
}
.photo {
width: 100%;
height: 100%;
object-fit: cover;
transition: transform 0.8s ease, opacity 0.8s ease;
opacity: 0.85;
}
.photo-container.active {
transform: scale(1.15) rotateY(5deg);
z-index: 10;
box-shadow: 0 15px 30px rgba(0, 0, 0, 0.5);
}
.photo-container.active .photo {
opacity: 1;
transform: scale(1.05);
}
.overlay {
position: absolute;
bottom: 0;
left: 0;
right: 0;
background: linear-gradient(to top, rgba(0, 0, 0, 0.7), transparent);
padding: 20px 10px 10px;
color: white;
transform: translateY(100%);
transition: transform 0.5s ease;
}
.photo-container.active .overlay {
transform: translateY(0);
}
.photo-caption {
font-size: 0.9rem;
text-align: center;
}
.footer {
margin-top: 30px;
text-align: center;
font-size: 1.2rem;
opacity: 0.8;
z-index: 10;
}
/* 背景装饰元素 */
.bg-decoration {
position: absolute;
width: 100%;
height: 100%;
top: 0;
left: 0;
z-index: -1;
overflow: hidden;
}
.decoration {
position: absolute;
border-radius: 50%;
background: rgba(255, 255, 255, 0.1);
animation: float 15s infinite linear;
}
@keyframes float {
0% {
transform: translateY(0) rotate(0deg);
}
50% {
transform: translateY(-20px) rotate(180deg);
}
100% {
transform: translateY(0) rotate(360deg);
}
}
/* 响应式调整 */
@media (max-width: 1200px) {
.photo-wall {
grid-template-columns: repeat(4, 1fr);
grid-template-rows: repeat(7, 1fr);
}
}
@media (max-width: 768px) {
.photo-wall {
grid-template-columns: repeat(3, 1fr);
grid-template-rows: repeat(9, 1fr);
}
h1 {
font-size: 2.5rem;
}
}
</style>
</head>
<body>
<div class="bg-decoration" id="bgDecoration"></div>
<div class="header">
<h1>美好时光回忆录</h1>
<div class="subtitle">珍藏每一刻的欢笑与感动</div>
</div>
<div class="photo-wall" id="photoWall">
<!-- 照片将通过JavaScript动态添加 -->
</div>
<div class="footer">感谢与我们共度这段美好时光</div>
<script>
document.addEventListener("DOMContentLoaded", function () {
const photoWall = document.getElementById("photoWall");
const bgDecoration = document.getElementById("bgDecoration");
// 生成25张示例照片
for (let i = 1; i <= 25; i++) {
const photoContainer = document.createElement("div");
photoContainer.className = "photo-container";
const photo = document.createElement("img");
photo.className = "photo";
// 使用随机图片API生成示例图片实际使用中替换为真实图片URL
photo.src = `https://picsum.photos/400/300?random=${i}`;
photo.alt = `宴会照片 ${i}`;
const overlay = document.createElement("div");
overlay.className = "overlay";
const caption = document.createElement("div");
caption.className = "photo-caption";
caption.textContent = `美好时刻 ${i}`;
overlay.appendChild(caption);
photoContainer.appendChild(photo);
photoContainer.appendChild(overlay);
photoWall.appendChild(photoContainer);
}
// 创建背景装饰元素
for (let i = 0; i < 15; i++) {
const decoration = document.createElement("div");
decoration.className = "decoration";
const size = Math.random() * 100 + 50;
decoration.style.width = `${size}px`;
decoration.style.height = `${size}px`;
decoration.style.left = `${Math.random() * 100}%`;
decoration.style.top = `${Math.random() * 100}%`;
decoration.style.animationDelay = `${Math.random() * 15}s`;
decoration.style.animationDuration = `${15 + Math.random() * 10}s`;
bgDecoration.appendChild(decoration);
}
// 自动轮播逻辑
const photoContainers = document.querySelectorAll(".photo-container");
let currentIndex = 0;
function activatePhoto(index) {
// 移除所有活动状态
photoContainers.forEach((container) => {
container.classList.remove("active");
});
// 激活当前照片
photoContainers[index].classList.add("active");
// 更新索引
currentIndex = (index + 1) % photoContainers.length;
}
// 初始激活第一张照片
activatePhoto(currentIndex);
// 设置定时器每3秒切换一张照片
setInterval(() => {
activatePhoto(currentIndex);
}, 3000);
});
</script>
</body>
</html>

View File

@@ -0,0 +1,29 @@
{
"images": [
{ "src": "./assets/image (1).png" },
{ "src": "./assets/image (2).png" },
{ "src": "./assets/image (3).png" },
{ "src": "./assets/image (4).png" },
{ "src": "./assets/image (5).png" },
{ "src": "./assets/image (6).png" },
{ "src": "./assets/image (7).png" },
{ "src": "./assets/image (8).png" },
{ "src": "./assets/image (9).png" },
{ "src": "./assets/image (10).png" },
{ "src": "./assets/image (11).png" },
{ "src": "./assets/image (12).png" },
{ "src": "./assets/image (13).png" },
{ "src": "./assets/image (14).png" },
{ "src": "./assets/image (15).png" },
{ "src": "./assets/image (16).png" },
{ "src": "./assets/image (17).png" },
{ "src": "./assets/image (18).png" },
{ "src": "./assets/image (19).png" },
{ "src": "./assets/image (20).png" },
{ "src": "./assets/image (21).png" },
{ "src": "./assets/image (22).png" },
{ "src": "./assets/image (23).png" },
{ "src": "./assets/image (24).png" },
{ "src": "./assets/image (25).png" }
]
}

View File

@@ -0,0 +1,356 @@
<!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>

View File

@@ -0,0 +1,46 @@
{
"title": "2005年第一届创馆人",
"nameList": [
{ "name": "楊吉陽", "isPassedAway": true },
{ "name": "唐華", "isPassedAway": true },
{ "name": "許仁菘", "isPassedAway": true },
{ "name": "許汶信", "isPassedAway": true },
{ "name": "劉炎松", "isPassedAway": true },
{ "name": "楊淑清", "isPassedAway": true },
{ "name": "黃芝芳", "isPassedAway": true },
{ "name": "劉吉棟", "isPassedAway": false },
{ "name": "許任隆", "isPassedAway": false },
{ "name": "楊順發", "isPassedAway": false },
{ "name": "吳祥森", "isPassedAway": false },
{ "name": "林庭芝", "isPassedAway": false },
{ "name": "林炳華", "isPassedAway": false },
{ "name": "林庭珠", "isPassedAway": false },
{ "name": "李玉媚", "isPassedAway": false },
{ "name": "林應財", "isPassedAway": false },
{ "name": "許斯杰", "isPassedAway": false },
{ "name": "許敏捷", "isPassedAway": false },
{ "name": "許智興", "isPassedAway": false },
{ "name": "劉德祥", "isPassedAway": false },
{ "name": "李豫梅", "isPassedAway": false },
{ "name": "黄潮明", "isPassedAway": false },
{ "name": "楊信陞", "isPassedAway": false },
{ "name": "蔡立義", "isPassedAway": false },
{ "name": "林炳龍", "isPassedAway": false },
{ "name": "劉振昌", "isPassedAway": false },
{ "name": "劉迪發", "isPassedAway": false },
{ "name": "楊美雄", "isPassedAway": false },
{ "name": "彭三媚", "isPassedAway": false },
{ "name": "楊光豐", "isPassedAway": false },
{ "name": "楊秀娥", "isPassedAway": false },
{ "name": "莊秀清", "isPassedAway": false },
{ "name": "李玉嬌", "isPassedAway": false },
{ "name": "趙惜嬌", "isPassedAway": false },
{ "name": "陳秀珠", "isPassedAway": false },
{ "name": "張彩雁", "isPassedAway": false },
{ "name": "劉暐康", "isPassedAway": false },
{ "name": "王貴興", "isPassedAway": false },
{ "name": "劉林順", "isPassedAway": false },
{ "name": "劉益華", "isPassedAway": false },
{ "name": "紀有平", "isPassedAway": false }
]
}