Files
dinner.tootaio.com/20251115/sponsorList/mobile/script.js
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

296 lines
8.5 KiB
JavaScript
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.
const { createApp, ref, computed, onMounted } = Vue;
const SponsorCard = {
props: {
sponsor: { type: Object, required: true },
},
template: "#sponsor-card-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("sponsor-card", SponsorCard); // 全局注册
app.mount("#app");