feat(sponsor-list): implement new design with animated canvas background
This commit introduces a completely redesigned sponsor list page. The previous two-column layout is replaced with a modern, single-column auto-scrolling list that unifies all sponsors. A dynamic canvas animation has been added to the background for a more engaging visual experience. Previous versions of the page have been archived. This update also includes data corrections for sponsor names and minor fixes to the related PPT assets.
This commit is contained in:
@@ -1,92 +1,115 @@
|
||||
// 自动更新年份
|
||||
document.getElementById(
|
||||
"footerText"
|
||||
).innerHTML = `© ${new Date().getFullYear()} Tootaio.com 保留所有权利。| 由 <a href="https://tootaio.com" target="_blank" rel="noopener">Tootaio</a> 制作。`;
|
||||
|
||||
// 页面初始化
|
||||
function initSponsorsAndSeats(sponsors, seats) {
|
||||
const moneyList = document.getElementById("moneyList");
|
||||
const seatList = document.getElementById("seatList");
|
||||
|
||||
// Sort by amount descending
|
||||
sponsors.sort((a, b) => parseFloat(b.amount) - parseFloat(a.amount));
|
||||
seats.sort((a, b) => parseInt(b.seat) - parseInt(a.seat));
|
||||
|
||||
let totalAmount = 0;
|
||||
sponsors.forEach((s) => {
|
||||
totalAmount += parseFloat(s.amount);
|
||||
const div = document.createElement("div");
|
||||
div.className = "sponsor-item";
|
||||
div.innerHTML = `<span>${s.name}</span><span class="amount">RM ${Number(
|
||||
s.amount
|
||||
).toLocaleString()}</span>`;
|
||||
moneyList.appendChild(div);
|
||||
});
|
||||
// 复制一份实现无缝滚动
|
||||
moneyList.innerHTML += moneyList.innerHTML;
|
||||
|
||||
let totalSeats = 0;
|
||||
seats.forEach((s) => {
|
||||
totalSeats += parseInt(s.seat);
|
||||
const div = document.createElement("div");
|
||||
div.className = "seat-item";
|
||||
div.innerHTML = `<span>${s.name}</span><span class="amount">${s.seat} 席</span>`;
|
||||
seatList.appendChild(div);
|
||||
});
|
||||
seatList.innerHTML += seatList.innerHTML;
|
||||
|
||||
// 更新统计数据
|
||||
const totalSponsors = document.getElementById("totalSponsors");
|
||||
if (totalSponsors) {
|
||||
totalSponsors.innerHTML = ""; // 清空现有内容
|
||||
var totalSponsorsIcon = document.createElement("i");
|
||||
totalSponsorsIcon.className = "fas fa-users";
|
||||
totalSponsors.prepend(totalSponsorsIcon);
|
||||
totalSponsors.appendChild(
|
||||
document.createTextNode(` 赞助单位: ${sponsors.length}`)
|
||||
);
|
||||
}
|
||||
const totalAmountEl = document.getElementById("totalAmount");
|
||||
totalAmountEl.innerHTML = ""; // 清空现有内容
|
||||
var totalAmountIcon = document.createElement("i");
|
||||
totalAmountIcon.className = "fas fa-coins";
|
||||
totalAmountEl.prepend(totalAmountIcon);
|
||||
totalAmountEl.appendChild(
|
||||
document.createTextNode(` 总金额: RM ${totalAmount.toLocaleString()}`)
|
||||
);
|
||||
const totalSeatsEl = document.getElementById("totalSeats");
|
||||
if (totalSeatsEl) {
|
||||
totalSeatsEl.innerHTML = ""; // 清空现有内容
|
||||
var totalSeatsIcon = document.createElement("i");
|
||||
totalSeatsIcon.className = "fas fa-chair";
|
||||
totalSeatsEl.prepend(totalSeatsIcon);
|
||||
totalSeatsEl.appendChild(
|
||||
document.createTextNode(` 席位总数: ${totalSeats}`)
|
||||
);
|
||||
}
|
||||
/**
|
||||
* 格式化金额,默认 MYR (RM)
|
||||
* @param {Number|String} amount 金额
|
||||
* @param {String} currency 币种(ISO 4217),默认 MYR
|
||||
* @param {String} locale 语言地区,默认 "ms-MY"
|
||||
* @returns {String} 格式化后的金额
|
||||
*/
|
||||
function formatCurrency(amount, currency = "MYR", locale = "ms-MY") {
|
||||
const num = Number(amount);
|
||||
if (isNaN(num)) return String(amount); // 非数字直接返回原值
|
||||
return new Intl.NumberFormat(locale, {
|
||||
style: "currency",
|
||||
currency,
|
||||
}).format(num);
|
||||
}
|
||||
|
||||
// 🚀 动态加载 JSON 数据
|
||||
const sponsorListDiv = document.getElementById("sponsorList");
|
||||
|
||||
Promise.all([
|
||||
fetch("../data/sponsors.json").then((res) => res.json()),
|
||||
fetch("../data/seats.json").then((res) => res.json()),
|
||||
])
|
||||
.then(([sponsors, seats]) => {
|
||||
initSponsorsAndSeats(sponsors, seats);
|
||||
})
|
||||
.catch((err) => {
|
||||
console.error("加载 JSON 数据失败,回退到本地 mock 数据", err);
|
||||
]).then(([sponsors, seats]) => {
|
||||
const sponsorList = sponsors.map((item) => ({
|
||||
name: item.name,
|
||||
amount: formatCurrency(item.amount), // 使用 Intl API
|
||||
}));
|
||||
|
||||
// 备用 mock 数据(防止页面空白)
|
||||
const mockSponsors = [
|
||||
{ name: "亮湘厨中国烧烤", amount: 8000 },
|
||||
{ name: "星空科技集团", amount: 15000 },
|
||||
{ name: "未来教育基金会", amount: 20000 },
|
||||
];
|
||||
const mockSeats = [
|
||||
{ name: "郑来兴", seat: 1 },
|
||||
{ name: "未来教育基金会", seat: 5 },
|
||||
];
|
||||
const seatList = seats.map((item) => ({
|
||||
name: item.name,
|
||||
amount: `${item.seat} 席`,
|
||||
}));
|
||||
|
||||
initSponsorsAndSeats(mockSponsors, mockSeats);
|
||||
[...sponsorList, ...seatList].forEach((entry) => {
|
||||
const card = document.createElement("div");
|
||||
card.className = "sponsor-item card";
|
||||
card.innerHTML = `<h2>${entry.name}</h2><p>${entry.amount}</p>`;
|
||||
sponsorListDiv.appendChild(card);
|
||||
});
|
||||
});
|
||||
|
||||
// ================= Canvas 背景 =================
|
||||
const canvas = document.getElementById("background");
|
||||
const ctx = canvas.getContext("2d");
|
||||
|
||||
function resizeCanvas() {
|
||||
canvas.width = window.innerWidth;
|
||||
canvas.height = window.innerHeight;
|
||||
}
|
||||
resizeCanvas();
|
||||
window.addEventListener("resize", resizeCanvas);
|
||||
|
||||
class Point {
|
||||
constructor() {
|
||||
this.radius = Math.random() * 4 + 2;
|
||||
this.x = Math.random() * canvas.width;
|
||||
this.y = Math.random() * canvas.height;
|
||||
this.xSpeed = (Math.random() - 0.5) * 60;
|
||||
this.ySpeed = (Math.random() - 0.5) * 60;
|
||||
this.color = `hsl(${Math.random() * 360}, 100%, 50%)`;
|
||||
this.lastDrawTime = null;
|
||||
}
|
||||
|
||||
draw() {
|
||||
const now = Date.now();
|
||||
if (this.lastDrawTime) {
|
||||
const dt = (now - this.lastDrawTime) / 1000;
|
||||
this.x += this.xSpeed * dt;
|
||||
this.y += this.ySpeed * dt;
|
||||
}
|
||||
if (this.x < 0 || this.x > canvas.width) this.xSpeed *= -1;
|
||||
if (this.y < 0 || this.y > canvas.height) this.ySpeed *= -1;
|
||||
|
||||
ctx.beginPath();
|
||||
ctx.arc(this.x, this.y, this.radius, 0, Math.PI * 2);
|
||||
ctx.fillStyle = this.color;
|
||||
ctx.shadowBlur = 15;
|
||||
ctx.shadowColor = this.color;
|
||||
ctx.fill();
|
||||
this.lastDrawTime = now;
|
||||
}
|
||||
}
|
||||
|
||||
class Graph {
|
||||
constructor(pointCount = 70, maxDistance = 120) {
|
||||
this.points = Array.from({ length: pointCount }, () => new Point());
|
||||
this.maxDist = maxDistance;
|
||||
}
|
||||
|
||||
draw() {
|
||||
requestAnimationFrame(() => this.draw());
|
||||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||||
|
||||
for (let i = 0; i < this.points.length; i++) {
|
||||
const p1 = this.points[i];
|
||||
p1.draw();
|
||||
for (let j = i + 1; j < this.points.length; j++) {
|
||||
const p2 = this.points[j];
|
||||
const dx = p1.x - p2.x;
|
||||
const dy = p1.y - p2.y;
|
||||
const dist = Math.sqrt(dx * dx + dy * dy);
|
||||
if (dist < this.maxDist) {
|
||||
const alpha = 1 - dist / this.maxDist;
|
||||
ctx.beginPath();
|
||||
ctx.moveTo(p1.x, p1.y);
|
||||
ctx.lineTo(p2.x, p2.y);
|
||||
ctx.strokeStyle = `rgba(255, 255, 255, ${alpha})`;
|
||||
ctx.lineWidth = 2 * alpha;
|
||||
ctx.stroke();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
new Graph().draw();
|
||||
|
||||
Reference in New Issue
Block a user