From ba744f3381e7a4e41002165394f146dd0f0f3f0e Mon Sep 17 00:00:00 2001 From: xiaomai Date: Thu, 13 Nov 2025 06:56:29 +0800 Subject: [PATCH] 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. --- 20251115/sponsorList.json | 1 + 20251115/sponsorList/index.html | 5 +- 20251115/sponsorList/mobile/index.html | 239 ++++++++++++++++++++ 20251115/sponsorList/mobile/script.js | 295 +++++++++++++++++++++++++ 4 files changed, 537 insertions(+), 3 deletions(-) create mode 100644 20251115/sponsorList/mobile/index.html create mode 100644 20251115/sponsorList/mobile/script.js diff --git a/20251115/sponsorList.json b/20251115/sponsorList.json index 063b6f4..d5cb5e0 100644 --- a/20251115/sponsorList.json +++ b/20251115/sponsorList.json @@ -1,5 +1,6 @@ { "eventTitle": "永平新港汕河体育协会 2 周年庆联欢晚宴", + "specialSponsors": "感谢 V World2.0 的特别赞助,现在下载 APP 并进行实名认证即可获得 RM50 的登录奖励!", "logos": [{ "imgSrc": "SamHor-HighRes.png" }, { "imgSrc": "关公文化-HighRes.png" }, { "imgSrc": "VWorld2 Logo.png" }], "poems": ["汕水流长通四海", "河川万里泽邦家"], "sponsorList": [ diff --git a/20251115/sponsorList/index.html b/20251115/sponsorList/index.html index 76dc2fb..b0a76cf 100644 --- a/20251115/sponsorList/index.html +++ b/20251115/sponsorList/index.html @@ -232,9 +232,7 @@ const eventTitle = ref(""); const logos = ref([]); const sponsorList = ref([]); // Load from JSON - const specialSponsor = ref( - "感谢 V World2.0 的特别赞助,现在下载 APP 并进行实名认证即可获得 RM50 的登录奖励!" - ); + const specialSponsor = ref(""); const typePriority = { cash: 4, @@ -271,6 +269,7 @@ } const sponsorListJsonData = await sponsorListResult.json(); eventTitle.value = sponsorListJsonData.eventTitle || "活动名称"; + specialSponsor.value = sponsorListJsonData.specialSponsor || ""; logos.value = sponsorListJsonData.logos || []; sponsorList.value = ( diff --git a/20251115/sponsorList/mobile/index.html b/20251115/sponsorList/mobile/index.html new file mode 100644 index 0000000..45a8c57 --- /dev/null +++ b/20251115/sponsorList/mobile/index.html @@ -0,0 +1,239 @@ + + + + + + 永平新港汕河体育协会 2 周年庆联欢晚宴|电子征信录与赞助名单 + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
+ +
+
+

{{ eventTitle }}

+ +
+ + +
+
+
+ +
+
+
+
+ + +
+
+ +

{{ specialSponsor }}

+
+
+ + +
+

+ 赞助统计 +

+ +
+
+

赞助商总数

+

{{ stats.totalSponsors }}

+
+
+

现金赞助总额

+

+ RM {{ stats.totalCash.toLocaleString() }} +

+
+
+

标品赞助数量

+

{{ stats.totalItems }}

+
+
+

赞助席位总数

+

+ {{ tableToSeats(stats.totalTables) }} +

+
+
+ +
+

赞助类型分布

+
+
+
{{ type.name }}
+
+
+
+
{{ type.count }}
+
+
+
+
+ + +
+ +
+
+ +

暂无此类赞助商

+
+ +
+ + + + +
+
+
+ + +
+ +
+
+ + + + + + diff --git a/20251115/sponsorList/mobile/script.js b/20251115/sponsorList/mobile/script.js new file mode 100644 index 0000000..144ac90 --- /dev/null +++ b/20251115/sponsorList/mobile/script.js @@ -0,0 +1,295 @@ +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");