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");