CP和经理中心添加bd leader

This commit is contained in:
zhx 2026-05-22 16:04:43 +08:00
parent 5069eef903
commit 56cdf46df4
36 changed files with 1824 additions and 6 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 77 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 73 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 1080 948.834" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Rectangle 33" d="M0 0H1080V922C1080 922 1041 861.5 830 861.5C619 861.5 381.082 973.074 179 944C70.6212 928.407 0 858 0 858V0Z" fill="var(--fill-0, #D9D9D9)"/>
</svg>

After

Width:  |  Height:  |  Size: 353 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 78 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 51 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 202 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 57 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 240 240" fill="none" xmlns="http://www.w3.org/2000/svg">
<rect id="Rectangle 43" width="240" height="240" fill="var(--fill-0, #D9D9D9)"/>
</svg>

After

Width:  |  Height:  |  Size: 260 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 253 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.9 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 98 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 94 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 505 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 112 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 106 KiB

View File

@ -4,7 +4,7 @@
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover" />
<title>Manager Center</title>
<link rel="stylesheet" href="./style.css?v=20260511-0109" />
<link rel="stylesheet" href="./style.css?v=20260522-0101" />
</head>
<body>
<div class="manager-center" aria-label="Manager Center" data-i18n-aria="page_label" data-loading="true">
@ -66,6 +66,22 @@
</section>
<section class="card action-card" id="actionCard" hidden>
<button class="action-item" id="addBDLeaderButton" type="button" disabled>
<span class="action-item-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
<path d="M16 21v-2a4 4 0 0 0-4-4H6a4 4 0 0 0-4 4v2" />
<circle cx="9" cy="7" r="4" />
<path d="M19 8v6" />
<path d="M22 11h-6" />
</svg>
</span>
<span class="action-item-copy">
<span class="action-item-title" data-i18n="add_bd_leader">Add BD Leader</span>
</span>
<svg class="action-item-arrow" viewBox="0 0 24 24" aria-hidden="true">
<path d="m9 6 6 6-6 6" />
</svg>
</button>
<button class="action-item" id="giftEntryButton" type="button">
<span class="action-item-icon" aria-hidden="true">
<svg viewBox="0 0 24 24">
@ -112,6 +128,18 @@
</svg>
</button>
</section>
<section class="card bd-leader-card">
<div class="section-head">
<div>
<h2 data-i18n="bd_leaders">BD Leaders</h2>
<div class="section-subtitle" id="bdLeaderCount">0</div>
</div>
<button class="text-button" id="refreshBDLeaderButton" type="button" data-i18n="refresh">Refresh</button>
</div>
<div class="form-status" id="bdLeaderStatus" role="status" hidden></div>
<div class="bd-leader-list" id="bdLeaderList"></div>
</section>
</main>
<div class="modal-mask" id="giftModal" role="dialog" aria-modal="true" aria-labelledby="giftModalTitle" hidden>
@ -200,6 +228,6 @@
</div>
<div class="toast" id="toast" role="status" hidden></div>
</div>
<script src="./script.js?v=20260511-0112" defer></script>
<script src="./script.js?v=20260522-0101" defer></script>
</body>
</html>

View File

@ -11,6 +11,14 @@
"search": "بحث",
"searching": "جار البحث...",
"target_user": "المستخدم المستهدف",
"bd_leaders": "قادة BD",
"bd_leader_count": "{count} قادة BD",
"refresh": "تحديث",
"refreshing": "جار التحديث...",
"no_bd_leaders": "لا يوجد قادة BD بعد",
"add_bd_leader": "إضافة قائد BD",
"adding_bd_leader": "جار الإضافة...",
"add_bd_leader_success": "تمت إضافة قائد BD",
"gift_props": "إرسال العناصر",
"gift_type": "نوع الهدية",
"prop_type_ride": "مركبة",
@ -48,6 +56,11 @@
"user_recharged": "لدى هذا المستخدم سجلات شحن ولا يمكن حظره",
"user_already_banned": "هذا المستخدم محظور بالفعل",
"user_not_banned": "هذا المستخدم غير محظور",
"manager_region_required": "منطقة المدير مطلوبة",
"bd_leader_origin_mismatch": "المستخدم ليس ضمن نظام هذا المدير",
"user_is_admin": "لا يمكن إضافة مستخدم إداري كقائد BD",
"user_already_bd_leader": "هذا المستخدم قائد BD بالفعل",
"user_already_bound_bd_leader": "هذا المستخدم مرتبط بالفعل بقائد BD",
"prop_not_giftable": "هذا العنصر غير متاح كهديّة من المدير",
"vip_not_giftable": "لا يمكن إهداء عنصر VIP هذا",
"request_failed": "فشل الطلب",

View File

@ -11,6 +11,14 @@
"search": "Search",
"searching": "Searching...",
"target_user": "Target User",
"bd_leaders": "BD Leaders",
"bd_leader_count": "{count} BD Leaders",
"refresh": "Refresh",
"refreshing": "Refreshing...",
"no_bd_leaders": "No BD Leaders yet",
"add_bd_leader": "Add BD Leader",
"adding_bd_leader": "Adding...",
"add_bd_leader_success": "BD Leader added",
"gift_props": "Gift Props",
"gift_type": "Gift Type",
"prop_type_ride": "Ride",
@ -48,6 +56,11 @@
"user_recharged": "This user has recharge records and cannot be banned",
"user_already_banned": "This user is already banned",
"user_not_banned": "This user is not banned",
"manager_region_required": "Manager region is required",
"bd_leader_origin_mismatch": "The user is not in this manager's origin",
"user_is_admin": "Admin users cannot be added as BD Leaders",
"user_already_bd_leader": "This user is already a BD Leader",
"user_already_bound_bd_leader": "This user is already bound to a BD Leader",
"prop_not_giftable": "This prop is not available for manager gifting",
"vip_not_giftable": "This noble prop cannot be gifted",
"request_failed": "Request failed",

View File

@ -11,6 +11,14 @@
"search": "Cari",
"searching": "Mencari...",
"target_user": "Pengguna Target",
"bd_leaders": "BD Leader",
"bd_leader_count": "{count} BD Leader",
"refresh": "Segarkan",
"refreshing": "Menyegarkan...",
"no_bd_leaders": "Belum ada BD Leader",
"add_bd_leader": "Tambah BD Leader",
"adding_bd_leader": "Menambahkan...",
"add_bd_leader_success": "BD Leader ditambahkan",
"gift_props": "Kirim Properti",
"gift_type": "Jenis Hadiah",
"prop_type_ride": "Kendaraan",
@ -48,6 +56,11 @@
"user_recharged": "Pengguna ini memiliki riwayat isi ulang dan tidak dapat diblokir",
"user_already_banned": "Pengguna ini sudah diblokir",
"user_not_banned": "Pengguna ini tidak diblokir",
"manager_region_required": "Region manajer diperlukan",
"bd_leader_origin_mismatch": "Pengguna tidak berada di origin manajer ini",
"user_is_admin": "Pengguna admin tidak dapat ditambahkan sebagai BD Leader",
"user_already_bd_leader": "Pengguna ini sudah menjadi BD Leader",
"user_already_bound_bd_leader": "Pengguna ini sudah terhubung ke BD Leader",
"prop_not_giftable": "Properti ini tidak tersedia untuk hadiah manajer",
"vip_not_giftable": "Properti noble ini tidak dapat dikirim",
"request_failed": "Permintaan gagal",

View File

@ -11,6 +11,14 @@
"search": "Ara",
"searching": "Aranıyor...",
"target_user": "Hedef Kullanıcı",
"bd_leaders": "BD Liderleri",
"bd_leader_count": "{count} BD Lideri",
"refresh": "Yenile",
"refreshing": "Yenileniyor...",
"no_bd_leaders": "Henüz BD lideri yok",
"add_bd_leader": "BD Lideri Ekle",
"adding_bd_leader": "Ekleniyor...",
"add_bd_leader_success": "BD lideri eklendi",
"gift_props": "Eşya Gönder",
"gift_type": "Hediye Türü",
"prop_type_ride": "Araç",
@ -48,6 +56,11 @@
"user_recharged": "Bu kullanıcının yükleme kaydı var, yasaklanamaz",
"user_already_banned": "Bu kullanıcı zaten yasaklı",
"user_not_banned": "Bu kullanıcı yasaklı değil",
"manager_region_required": "Yönetici bölgesi gerekli",
"bd_leader_origin_mismatch": "Kullanıcı bu yöneticinin origin değerinde değil",
"user_is_admin": "Admin kullanıcılar BD lideri eklenemez",
"user_already_bd_leader": "Bu kullanıcı zaten BD lideri",
"user_already_bound_bd_leader": "Bu kullanıcı zaten bir BD liderine bağlı",
"prop_not_giftable": "Bu eşya yönetici hediyesi için açık değil",
"vip_not_giftable": "Bu noble eşyası gönderilemez",
"request_failed": "İstek başarısız",

View File

@ -25,6 +25,14 @@
search: "Search",
searching: "Searching...",
target_user: "Target User",
bd_leaders: "BD Leaders",
bd_leader_count: "{count} BD Leaders",
refresh: "Refresh",
refreshing: "Refreshing...",
no_bd_leaders: "No BD Leaders yet",
add_bd_leader: "Add BD Leader",
adding_bd_leader: "Adding...",
add_bd_leader_success: "BD Leader added",
gift_props: "Gift Props",
gift_type: "Gift Type",
prop_type_ride: "Ride",
@ -62,6 +70,11 @@
user_recharged: "This user has recharge records and cannot be banned",
user_already_banned: "This user is already banned",
user_not_banned: "This user is not banned",
manager_region_required: "Manager region is required",
bd_leader_origin_mismatch: "The user is not in this manager's origin",
user_is_admin: "Admin users cannot be added as BD Leaders",
user_already_bd_leader: "This user is already a BD Leader",
user_already_bound_bd_leader: "This user is already bound to a BD Leader",
prop_not_giftable: "This prop is not available for manager gifting",
vip_not_giftable: "This noble prop cannot be gifted",
request_failed: "Request failed",
@ -80,6 +93,14 @@
search: "بحث",
searching: "جار البحث...",
target_user: "المستخدم المستهدف",
bd_leaders: "قادة BD",
bd_leader_count: "{count} قادة BD",
refresh: "تحديث",
refreshing: "جار التحديث...",
no_bd_leaders: "لا يوجد قادة BD بعد",
add_bd_leader: "إضافة قائد BD",
adding_bd_leader: "جار الإضافة...",
add_bd_leader_success: "تمت إضافة قائد BD",
gift_props: "إرسال العناصر",
gift_type: "نوع الهدية",
prop_type_ride: "مركبة",
@ -117,6 +138,11 @@
user_recharged: "لدى هذا المستخدم سجلات شحن ولا يمكن حظره",
user_already_banned: "هذا المستخدم محظور بالفعل",
user_not_banned: "هذا المستخدم غير محظور",
manager_region_required: "منطقة المدير مطلوبة",
bd_leader_origin_mismatch: "المستخدم ليس ضمن نظام هذا المدير",
user_is_admin: "لا يمكن إضافة مستخدم إداري كقائد BD",
user_already_bd_leader: "هذا المستخدم قائد BD بالفعل",
user_already_bound_bd_leader: "هذا المستخدم مرتبط بالفعل بقائد BD",
prop_not_giftable: "هذا العنصر غير متاح كهديّة من المدير",
vip_not_giftable: "لا يمكن إهداء عنصر VIP هذا",
request_failed: "فشل الطلب",
@ -135,6 +161,14 @@
search: "Ara",
searching: "Aranıyor...",
target_user: "Hedef Kullanıcı",
bd_leaders: "BD Liderleri",
bd_leader_count: "{count} BD Lideri",
refresh: "Yenile",
refreshing: "Yenileniyor...",
no_bd_leaders: "Henüz BD lideri yok",
add_bd_leader: "BD Lideri Ekle",
adding_bd_leader: "Ekleniyor...",
add_bd_leader_success: "BD lideri eklendi",
gift_props: "Eşya Gönder",
gift_type: "Hediye Türü",
prop_type_ride: "Araç",
@ -172,6 +206,11 @@
user_recharged: "Bu kullanıcının yükleme kaydı var, yasaklanamaz",
user_already_banned: "Bu kullanıcı zaten yasaklı",
user_not_banned: "Bu kullanıcı yasaklı değil",
manager_region_required: "Yönetici bölgesi gerekli",
bd_leader_origin_mismatch: "Kullanıcı bu yöneticinin origin değerinde değil",
user_is_admin: "Admin kullanıcılar BD lideri eklenemez",
user_already_bd_leader: "Bu kullanıcı zaten BD lideri",
user_already_bound_bd_leader: "Bu kullanıcı zaten bir BD liderine bağlı",
prop_not_giftable: "Bu eşya yönetici hediyesi için açık değil",
vip_not_giftable: "Bu noble eşyası gönderilemez",
request_failed: "İstek başarısız",
@ -190,6 +229,14 @@
search: "Cari",
searching: "Mencari...",
target_user: "Pengguna Target",
bd_leaders: "BD Leader",
bd_leader_count: "{count} BD Leader",
refresh: "Segarkan",
refreshing: "Menyegarkan...",
no_bd_leaders: "Belum ada BD Leader",
add_bd_leader: "Tambah BD Leader",
adding_bd_leader: "Menambahkan...",
add_bd_leader_success: "BD Leader ditambahkan",
gift_props: "Kirim Properti",
gift_type: "Jenis Hadiah",
prop_type_ride: "Kendaraan",
@ -227,6 +274,11 @@
user_recharged: "Pengguna ini memiliki riwayat isi ulang dan tidak dapat diblokir",
user_already_banned: "Pengguna ini sudah diblokir",
user_not_banned: "Pengguna ini tidak diblokir",
manager_region_required: "Region manajer diperlukan",
bd_leader_origin_mismatch: "Pengguna tidak berada di origin manajer ini",
user_is_admin: "Pengguna admin tidak dapat ditambahkan sebagai BD Leader",
user_already_bd_leader: "Pengguna ini sudah menjadi BD Leader",
user_already_bound_bd_leader: "Pengguna ini sudah terhubung ke BD Leader",
prop_not_giftable: "Properti ini tidak tersedia untuk hadiah manajer",
vip_not_giftable: "Properti noble ini tidak dapat dikirim",
request_failed: "Permintaan gagal",
@ -239,7 +291,11 @@
target: null,
propsType: defaultPropsType,
props: [],
selectedPropsId: ""
selectedPropsId: "",
bdLeaders: [],
bdLeadersLoading: false,
bdLeadersError: "",
addingBDLeader: false
};
const params = readURLParams();
@ -293,7 +349,7 @@
if (window.location.protocol === "file:") return fallbackMessages[normalizedLang];
try {
const response = await fetch(`./locales/${normalizedLang}.json?v=20260511-0107`, { cache: "no-store" });
const response = await fetch(`./locales/${normalizedLang}.json?v=20260522-0101`, { cache: "no-store" });
if (response.ok) return await response.json();
} catch (error) {
return fallbackMessages[normalizedLang];
@ -404,6 +460,7 @@
renderManager(state.manager);
renderTarget();
renderProps();
renderBDLeaders();
}
function $(selector) {
@ -505,6 +562,10 @@
return "/app/h5/manager-center/props/send";
}
function endpointBDLeaders() {
return "/app/h5/manager-center/bd-leaders";
}
function endpointBanUser() {
return "/app/h5/manager-center/users/ban";
}
@ -637,6 +698,10 @@
return String(user?.userId || user?.id || "-");
}
function sameUserID(left, right) {
return displayUserID(left) === displayUserID(right);
}
function displayShortID(user) {
return String(user?.actualAccount || user?.account || user?.shortId || user?.userId || user?.id || "-");
}
@ -663,6 +728,26 @@
return message("days", "{count} days").replace("{count}", String(days || 0));
}
function formatBDLeaderCount(count) {
return message("bd_leader_count", "{count} BD Leaders").replace("{count}", String(count || 0));
}
function formatDateTime(value) {
if (!value) return "";
const date = new Date(value);
if (Number.isNaN(date.getTime())) return "";
try {
return new Intl.DateTimeFormat(currentLang, {
month: "short",
day: "2-digit",
hour: "2-digit",
minute: "2-digit"
}).format(date);
} catch (error) {
return date.toLocaleString();
}
}
function giftDays(item) {
const days = Number(item?.days);
if (Number.isFinite(days) && days > 0) return days;
@ -766,16 +851,93 @@
updateActionState();
}
function isTargetBDLeader() {
if (!state.target) return false;
return state.bdLeaders.some((leader) => sameUserID(leader, state.target));
}
function renderBDLeaders() {
const list = $("#bdLeaderList");
const count = $("#bdLeaderCount");
if (count) count.textContent = formatBDLeaderCount(state.bdLeaders.length);
if (!list) return;
list.replaceChildren();
if (state.bdLeadersLoading) {
setStatus($("#bdLeaderStatus"), message("refreshing"));
updateActionState();
return;
}
if (state.bdLeadersError) {
setStatus($("#bdLeaderStatus"), state.bdLeadersError, "error");
updateActionState();
return;
}
if (!state.bdLeaders.length) {
setStatus($("#bdLeaderStatus"), message("no_bd_leaders"));
updateActionState();
return;
}
setStatus($("#bdLeaderStatus"), "");
state.bdLeaders.forEach((leader) => {
const row = document.createElement("div");
row.className = "bd-leader-row";
const avatar = document.createElement("span");
avatar.className = "bd-leader-avatar";
if (leader.userAvatar) {
const img = document.createElement("img");
img.alt = "";
img.src = leader.userAvatar;
avatar.appendChild(img);
} else {
const fallback = document.createElement("span");
fallback.textContent = firstLetter(displayName(leader), "B");
avatar.appendChild(fallback);
}
const copy = document.createElement("span");
copy.className = "bd-leader-copy";
const name = document.createElement("span");
name.className = "bd-leader-name";
name.textContent = displayName(leader);
const meta = document.createElement("span");
meta.className = "bd-leader-meta";
const metaItems = [userIDText(leader)];
if (leader.regionId) metaItems.push(leader.regionId);
if (leader.originSys) metaItems.push(leader.originSys);
meta.textContent = metaItems.join(" · ");
copy.append(name, meta);
const time = document.createElement("span");
time.className = "bd-leader-time";
time.textContent = formatDateTime(leader.createTime);
row.append(avatar, copy, time);
list.appendChild(row);
});
updateActionState();
}
function updateActionState() {
const addBDLeaderButton = $("#addBDLeaderButton");
const giftEntryButton = $("#giftEntryButton");
const banEntryButton = $("#banEntryButton");
const unbanEntryButton = $("#unbanEntryButton");
const refreshBDLeaderButton = $("#refreshBDLeaderButton");
const sendButton = $("#sendPropsButton");
const banButton = $("#banButton");
const unbanButton = $("#unbanButton");
if (addBDLeaderButton) {
addBDLeaderButton.disabled = !state.target || state.addingBDLeader || isTargetBDLeader();
const title = addBDLeaderButton.querySelector(".action-item-title");
if (title) title.textContent = state.addingBDLeader ? message("adding_bd_leader") : message("add_bd_leader");
}
if (giftEntryButton) giftEntryButton.disabled = !state.target;
if (banEntryButton) banEntryButton.disabled = !state.target;
if (unbanEntryButton) unbanEntryButton.disabled = !state.target;
if (refreshBDLeaderButton) refreshBDLeaderButton.disabled = state.bdLeadersLoading;
if (sendButton) sendButton.disabled = !state.target || !state.selectedPropsId;
if (banButton) banButton.disabled = !canBanTarget(state.target);
if (unbanButton) unbanButton.disabled = !canUnbanTarget(state.target);
@ -799,6 +961,23 @@
renderProps();
}
async function loadBDLeaders() {
state.bdLeadersLoading = true;
state.bdLeadersError = "";
renderBDLeaders();
try {
const body = await requestGoJSON(endpointBDLeaders());
state.bdLeaders = Array.isArray(body?.records) ? body.records : [];
setStatus($("#bdLeaderStatus"), "");
} catch (error) {
state.bdLeadersError = friendlyError(error);
throw error;
} finally {
state.bdLeadersLoading = false;
renderBDLeaders();
}
}
async function searchUser(event) {
event.preventDefault();
const input = $("#accountInput");
@ -824,6 +1003,32 @@
}
}
async function addBDLeader() {
if (!state.target || state.addingBDLeader) return;
if (isTargetBDLeader()) {
showToast(message("user_already_bd_leader"));
return;
}
state.addingBDLeader = true;
updateActionState();
try {
await requestGoJSON(endpointBDLeaders(), {
method: "POST",
body: {
userId: displayUserID(state.target)
}
});
showToast(message("add_bd_leader_success"));
await loadBDLeaders();
} catch (error) {
showToast(friendlyError(error));
} finally {
state.addingBDLeader = false;
updateActionState();
}
}
async function sendProps() {
if (!state.target || !state.selectedPropsId) return;
@ -906,6 +1111,11 @@
if (code === "user_recharged") return message("user_recharged");
if (code === "user_already_banned") return message("user_already_banned");
if (code === "user_not_banned") return message("user_not_banned");
if (code === "manager_region_required") return message("manager_region_required");
if (code === "bd_leader_origin_mismatch") return message("bd_leader_origin_mismatch");
if (code === "user_is_admin") return message("user_is_admin");
if (code === "user_already_bd_leader") return message("user_already_bd_leader");
if (code === "user_already_bound_bd_leader") return message("user_already_bound_bd_leader");
if (code === "prop_not_giftable") return message("prop_not_giftable");
if (code === "vip_not_giftable") return message("vip_not_giftable");
return error?.message || message("request_failed");
@ -928,6 +1138,12 @@
setLanguageMenuOpen(false);
setPropsTypeMenuOpen(false);
});
$("#addBDLeaderButton")?.addEventListener("click", addBDLeader);
$("#refreshBDLeaderButton")?.addEventListener("click", () => {
loadBDLeaders().catch((error) => {
showToast(friendlyError(error));
});
});
$("#giftEntryButton")?.addEventListener("click", openGiftModal);
$("#banEntryButton")?.addEventListener("click", openBanModal);
$("#unbanEntryButton")?.addEventListener("click", openUnbanModal);
@ -973,7 +1189,7 @@
}
try {
await Promise.all([loadProfile(), loadProps()]);
await Promise.all([loadProfile(), loadProps(), loadBDLeaders()]);
} catch (error) {
showToast(friendlyError(error));
} finally {

View File

@ -360,6 +360,100 @@ button:disabled {
letter-spacing: 0;
}
.section-subtitle {
margin-top: 3px;
color: var(--muted);
font-size: 12px;
font-weight: 800;
}
.text-button {
flex: 0 0 auto;
min-height: 32px;
border-radius: 8px;
padding: 0 10px;
background: #e7f8f4;
color: var(--primary-dark);
font-size: 12px;
font-weight: 900;
}
.bd-leader-card {
padding: 14px;
}
.bd-leader-list {
display: grid;
gap: 8px;
}
.bd-leader-row {
display: grid;
grid-template-columns: 42px minmax(0, 1fr) auto;
gap: 10px;
align-items: center;
min-height: 58px;
border: 1px solid var(--line);
border-radius: 8px;
padding: 8px;
background: #fff;
}
.bd-leader-avatar {
width: 42px;
height: 42px;
overflow: hidden;
border-radius: 50%;
background: #eef2f5;
}
.bd-leader-avatar img,
.bd-leader-avatar span {
display: block;
width: 100%;
height: 100%;
}
.bd-leader-avatar img {
object-fit: cover;
}
.bd-leader-avatar span {
display: flex;
align-items: center;
justify-content: center;
color: var(--primary-dark);
font-weight: 900;
}
.bd-leader-copy {
display: grid;
gap: 2px;
min-width: 0;
}
.bd-leader-name {
overflow: hidden;
font-size: 14px;
font-weight: 900;
line-height: 18px;
text-overflow: ellipsis;
white-space: nowrap;
}
.bd-leader-meta,
.bd-leader-time {
color: var(--muted);
font-size: 11px;
font-weight: 800;
line-height: 14px;
}
.bd-leader-time {
max-width: 76px;
text-align: right;
}
.search-form {
display: grid;
grid-template-columns: 1fr 88px;
@ -764,7 +858,8 @@ textarea:focus {
[dir="rtl"] .custom-select-button,
[dir="rtl"] .custom-select-menu button,
[dir="rtl"] .prop-option,
[dir="rtl"] .action-item {
[dir="rtl"] .action-item,
[dir="rtl"] .bd-leader-row {
text-align: right;
}
@ -776,6 +871,10 @@ textarea:focus {
transform: rotate(180deg);
}
[dir="rtl"] .bd-leader-time {
text-align: left;
}
@keyframes spin {
to {
transform: rotate(360deg);

View File

@ -0,0 +1,715 @@
:root {
--scale: 0.36;
--canvas-width: 1080px;
--canvas-height: 4560px;
--page-bg: #ffadd3;
--deep-red: #a01626;
--muted-pink: #da698c;
--gold: #ffe4a6;
--asset-root: "../../assets/yumi/cp-ranking";
}
* {
box-sizing: border-box;
}
html,
body {
margin: 0;
min-height: 100%;
background: var(--page-bg);
font-family:
"Source Han Sans SC", "PingFang SC", "Helvetica Neue", Arial, sans-serif;
-webkit-text-size-adjust: 100%;
}
body {
color: var(--deep-red);
padding-bottom: env(safe-area-inset-bottom);
}
button {
font: inherit;
appearance: none;
-webkit-appearance: none;
border-radius: 0;
}
.page {
width: calc(var(--canvas-width) * var(--scale));
max-width: 100vw;
height: calc(var(--canvas-height) * var(--scale));
margin: 0 auto;
overflow: hidden;
background:
linear-gradient(180deg, rgba(255, 173, 211, 0.97), rgba(255, 173, 211, 0.97)),
url("../../assets/yumi/cp-ranking/page-texture.jpg") center top / 100% auto repeat-y;
}
.canvas {
position: relative;
width: var(--canvas-width);
height: var(--canvas-height);
transform: scale(var(--scale));
transform-origin: left top;
overflow: hidden;
background:
linear-gradient(180deg, rgba(255, 173, 211, 0.95), rgba(255, 173, 211, 0.95)),
url("../../assets/yumi/cp-ranking/page-texture.jpg") center top / 1080px auto repeat-y;
}
.asset {
position: absolute;
display: block;
max-width: none;
user-select: none;
pointer-events: none;
}
.hero-image {
left: 0;
top: 0;
width: 1080px;
height: 1027px;
object-fit: fill;
}
.title-art {
left: -232px;
top: 356px;
width: 1478px;
height: 831px;
object-fit: fill;
}
.rules-button {
position: absolute;
left: 940px;
top: 169px;
width: 176px;
height: 94px;
padding: 0 0 8px;
border: 0;
color: #fff;
font-size: 24px;
font-weight: 800;
line-height: 40px;
text-align: center;
background: url("../../assets/yumi/cp-ranking/tab-default.png") center / 100% 100% no-repeat;
-webkit-tap-highlight-color: transparent;
}
.rules-button:active,
.tab-button:active {
transform: scale(0.97);
}
.countdown {
position: absolute;
left: 61px;
top: 1046px;
width: 964px;
height: 144px;
display: grid;
grid-template-columns: repeat(4, 148px);
justify-content: space-between;
align-items: start;
}
.countdown-item {
position: relative;
width: 148px;
height: 144px;
color: #fff;
text-align: center;
}
.countdown-item img,
.mini-countdown-item img {
position: absolute;
inset: 0;
width: 100%;
height: 100%;
object-fit: fill;
}
.countdown-value {
position: absolute;
left: 0;
top: 43px;
width: 148px;
color: #fff;
font-size: 40px;
font-weight: 800;
line-height: 40px;
text-align: center;
}
.countdown-label {
position: absolute;
left: 176px;
top: 43px;
width: 40px;
color: #a83015;
font-size: 40px;
font-weight: 800;
line-height: 40px;
}
.countdown-item:last-child .countdown-label {
display: none;
}
.tabs {
position: absolute;
left: 10px;
top: 1217px;
width: 1059px;
height: 182px;
display: grid;
grid-template-columns: repeat(3, 340px);
gap: 19px;
}
.tab-button {
position: relative;
display: block;
width: 340px;
height: 182px;
padding: 0;
border: 0;
color: #fff;
background: url("../../assets/yumi/cp-ranking/tab-default.png") center / 100% 100% no-repeat;
-webkit-tap-highlight-color: transparent;
}
.tab-button.is-active {
background-image: url("../../assets/yumi/cp-ranking/tab-active.png");
}
.tab-label {
position: absolute;
left: 0;
top: 73px;
width: 340px;
height: 40px;
color: #fff;
font-size: 36px;
font-weight: 800;
line-height: 40px;
text-align: center;
white-space: nowrap;
}
.tab-panel {
display: none;
}
.tab-panel.is-active {
display: block;
}
.ornate-panel {
position: absolute;
left: 16px;
width: 1048px;
overflow: visible;
}
.panel-bg {
position: absolute;
inset: 0;
pointer-events: none;
}
.panel-bg::before,
.panel-bg::after {
content: "";
position: absolute;
left: 0;
width: 1048px;
height: 308px;
background-position: center;
background-repeat: no-repeat;
background-size: 100% 100%;
}
.panel-bg::before {
top: 0;
background-image: url("../../assets/yumi/cp-ranking/panel-top.png");
}
.panel-bg::after {
bottom: 0;
background-image: url("../../assets/yumi/cp-ranking/panel-bottom.png");
}
.panel-bg {
background:
url("../../assets/yumi/cp-ranking/panel-middle.png") center 308px / 1048px calc(100% - 616px) no-repeat;
}
.ornate-panel h2 {
position: absolute;
left: 98px;
right: 98px;
margin: 0;
color: #a83015;
font-size: 30px;
font-weight: 650;
line-height: 38px;
text-align: center;
}
.panel-subtitle {
position: absolute;
left: 98px;
right: 98px;
margin: 0;
color: var(--muted-pink);
font-size: 22px;
font-weight: 500;
line-height: 28px;
text-align: center;
}
.plaza-live-panel {
top: 1414px;
height: 1517px;
}
.plaza-live-panel h2 {
top: 178px;
}
.plaza-live-panel .panel-subtitle {
top: 269px;
}
.plaza-history-panel {
top: 2941px;
height: 1491px;
}
.plaza-history-panel h2 {
top: 176px;
font-size: 32px;
}
.plaza-history-panel .panel-subtitle {
top: 242px;
}
.cp-row-list {
position: absolute;
left: 84px;
width: 880px;
display: flex;
flex-direction: column;
gap: 16px;
}
.plaza-live-panel .cp-row-list {
top: 332px;
}
.plaza-history-panel .cp-row-list {
top: 306px;
}
.rank-list-panel .cp-row-list {
top: 306px;
}
.cp-row {
position: relative;
width: 880px;
height: 237px;
background: url("../../assets/yumi/cp-ranking/cp-row-frame.png") center / 100% 100% no-repeat;
}
.cp-user {
position: absolute;
top: 33px;
width: 166px;
height: 171px;
text-align: center;
}
.cp-user-left {
left: 59px;
}
.cp-user-right {
right: 59px;
}
.cp-avatar {
width: 135px;
height: 135px;
margin: 0 auto;
border: 3px solid #ffebbb;
border-radius: 50%;
object-fit: cover;
background: #ffd4e6;
}
.cp-name {
display: block;
width: 166px;
margin-top: 0;
color: var(--deep-red);
font-size: 25px;
font-weight: 650;
line-height: 36px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.row-center {
position: absolute;
left: 278px;
top: 0;
width: 324px;
height: 237px;
display: flex;
align-items: center;
justify-content: center;
}
.row-wave {
position: absolute;
top: 93px;
width: 179px;
height: 50px;
object-fit: fill;
}
.row-wave-left {
left: 187px;
}
.row-wave-right {
right: 187px;
transform: scaleX(-1);
}
.gift-icon {
position: relative;
z-index: 2;
width: 160px;
height: 160px;
object-fit: contain;
}
.score-heart {
position: relative;
z-index: 2;
width: 264px;
height: 238px;
background: url("../../assets/yumi/cp-ranking/rank-heart-glow.png") center / contain no-repeat;
display: flex;
align-items: center;
justify-content: center;
color: var(--deep-red);
font-size: 30px;
font-weight: 650;
line-height: 36px;
}
.empty-state {
position: absolute;
left: 0;
right: 0;
top: 96px;
color: var(--muted-pink);
font-size: 24px;
line-height: 34px;
text-align: center;
}
.podium {
position: absolute;
left: 0;
top: 1392px;
width: 1080px;
height: 710px;
}
.podium-card {
position: absolute;
background: url("../../assets/yumi/cp-ranking/top-card.png") center / 100% 100% no-repeat;
}
.podium-card.rank-1 {
left: 158px;
top: 0;
width: 760px;
height: 397px;
}
.podium-card.rank-2 {
left: -70px;
top: 392px;
width: 608px;
height: 303px;
}
.podium-card.rank-3 {
left: 539px;
top: 392px;
width: 608px;
height: 303px;
}
.podium-crown {
position: absolute;
display: block;
object-fit: contain;
pointer-events: none;
}
.rank-1 .podium-crown {
left: 303px;
top: 22px;
width: 154px;
height: 108px;
}
.rank-2 .podium-crown,
.rank-3 .podium-crown {
left: 245px;
top: 29px;
width: 118px;
height: 83px;
}
.podium-person {
position: absolute;
top: 158px;
width: 181px;
height: 239px;
text-align: center;
}
.rank-1 .podium-person-left {
left: 183px;
}
.rank-1 .podium-person-right {
right: 178px;
}
.rank-2 .podium-person,
.rank-3 .podium-person {
top: 126px;
width: 145px;
height: 192px;
}
.rank-2 .podium-person-left,
.rank-3 .podium-person-left {
left: 146px;
}
.rank-2 .podium-person-right,
.rank-3 .podium-person-right {
left: 321px;
}
.podium-avatar {
width: 181px;
height: 181px;
border: 3px solid #ffebbb;
border-radius: 50%;
object-fit: cover;
}
.rank-2 .podium-avatar,
.rank-3 .podium-avatar {
width: 145px;
height: 145px;
border-width: 2px;
}
.podium-name {
display: block;
margin-top: 16px;
color: var(--deep-red);
font-size: 30px;
font-weight: 650;
line-height: 36px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
}
.rank-2 .podium-name,
.rank-3 .podium-name {
margin-top: 15px;
font-size: 24px;
line-height: 29px;
}
.rank-list-panel {
top: 2152px;
height: 1519px;
}
.mini-countdown {
position: absolute;
left: 99px;
top: 173px;
width: 849px;
height: 115px;
display: grid;
grid-template-columns: repeat(4, 118px);
justify-content: space-between;
}
.mini-countdown-item {
position: relative;
width: 118px;
height: 115px;
}
.mini-countdown-item span {
position: absolute;
left: 0;
top: 29px;
width: 118px;
color: #fff;
font-size: 40px;
font-weight: 800;
line-height: 40px;
text-align: center;
}
.mini-countdown-item b {
position: absolute;
left: 154px;
top: 29px;
color: #a83015;
font-size: 40px;
font-weight: 800;
line-height: 40px;
}
.mini-countdown-item:last-child b {
display: none;
}
.reward-panel {
height: 1075px;
}
.reward-panel-1 {
top: 1424px;
}
.reward-panel-2 {
top: 2509px;
}
.reward-panel-3 {
top: 3594px;
}
.reward-crown {
position: absolute;
left: 443px;
top: 148px;
width: 162px;
height: 114px;
object-fit: contain;
}
.reward-panel-2 .reward-crown {
width: 161px;
}
.reward-panel-3 .reward-crown {
width: 158px;
}
.reward-grid {
position: absolute;
left: 104px;
top: 272px;
width: 824px;
display: grid;
grid-template-columns: repeat(3, 240px);
column-gap: 60px;
row-gap: 52px;
}
.reward-card {
width: 240px;
min-height: 280px;
text-align: center;
}
.reward-frame {
position: relative;
width: 240px;
height: 240px;
background: url("../../assets/yumi/cp-ranking/reward-cell-frame.png") center / 100% 100% no-repeat;
}
.reward-frame img {
position: absolute;
left: 20px;
top: 25px;
width: 200px;
height: 200px;
object-fit: contain;
}
.reward-title {
margin-top: 14px;
color: var(--deep-red);
font-size: 24px;
font-weight: 650;
line-height: 30px;
min-height: 30px;
}
.reward-note {
margin-top: 4px;
color: var(--muted-pink);
font-size: 18px;
line-height: 20px;
}
.copyright {
position: absolute;
left: 115px;
top: 4472px;
width: 850px;
margin: 0;
color: var(--muted-pink);
font-size: 24px;
line-height: 40px;
text-align: center;
}
.tab-panel-rank.is-active ~ .copyright {
top: 3711px;
}
.tab-panel-rewards.is-active ~ .copyright {
top: 4709px;
}
.sr-only {
position: absolute;
width: 1px;
height: 1px;
padding: 0;
margin: -1px;
overflow: hidden;
clip: rect(0, 0, 0, 0);
white-space: nowrap;
border: 0;
}

View File

@ -0,0 +1,579 @@
(function () {
"use strict";
var API_BASE_URL = "https://jvapi.haiyihy.com";
var ASSET_ROOT = "../../assets/yumi/cp-ranking";
var DEFAULT_AVATAR = ASSET_ROOT + "/avatar-sample.png";
var DEBUG = /(?:^|[?&])debug=1(?:&|$)/.test(window.location.search);
var PAGE_HEIGHTS = {
plaza: 4560,
rank: 3825,
rewards: 4800
};
var commonHeaders = {
authorization: "",
"req-app-intel": "version=1.0.0;build=1;model=H5;sysVersion=web;channel=Web",
"req-client": "H5",
"req-imei": "H5-STATIC",
"req-lang": "en",
"req-sys-origin": "origin=ATYOU;originChild=ATYOU",
"req-version": "V2",
"req-zone": "Asia/Shanghai"
};
var state = {
activeTab: initialTab(),
ready: false,
rows: sampleRankRows(),
lastRows: sampleRankRows().slice(0, 4),
rewards: sampleRewards(),
countdownTarget: resolveCountdownTarget()
};
var dom = {};
var countdownTimer = null;
init();
function init() {
dom.page = document.getElementById("page");
dom.canvas = document.getElementById("canvas");
dom.tabs = Array.prototype.slice.call(document.querySelectorAll("[data-tab]"));
dom.panels = Array.prototype.slice.call(document.querySelectorAll("[data-panel]"));
dom.status = document.getElementById("status");
dom.plazaList = document.getElementById("plazaList");
dom.historyList = document.getElementById("historyList");
dom.rankList = document.getElementById("rankList");
dom.podium = document.getElementById("podium");
dom.rewardGroups = [
document.getElementById("rewardGroup1"),
document.getElementById("rewardGroup2"),
document.getElementById("rewardGroup3")
];
bindEvents();
syncScale();
applyTab(state.activeTab);
render();
startCountdown();
initializeAppConnection().then(loadRemoteData).catch(function (error) {
console.warn("CP ranking init failed:", error);
state.ready = true;
render();
});
}
function bindEvents() {
window.addEventListener("resize", syncScale);
dom.tabs.forEach(function (button) {
button.addEventListener("click", function () {
applyTab(button.getAttribute("data-tab"));
});
});
document.getElementById("rulesButton").addEventListener("click", function () {
openRules();
});
dom.canvas.addEventListener("click", function (event) {
var target = event.target.closest("[data-user-id]");
if (!target) return;
openUser(target.getAttribute("data-user-id"));
});
}
function syncScale() {
var viewport = Math.min(window.innerWidth || 375, 430);
var scale = viewport / 1080;
document.documentElement.style.setProperty("--scale", String(scale));
}
function initialTab() {
var value = getParam("tab") || "plaza";
return PAGE_HEIGHTS[value] ? value : "plaza";
}
function applyTab(tab) {
if (!PAGE_HEIGHTS[tab]) tab = "plaza";
state.activeTab = tab;
document.documentElement.style.setProperty("--canvas-height", PAGE_HEIGHTS[tab] + "px");
dom.tabs.forEach(function (button) {
button.classList.toggle("is-active", button.getAttribute("data-tab") === tab);
});
dom.panels.forEach(function (panel) {
panel.classList.toggle("is-active", panel.getAttribute("data-panel") === tab);
});
var title = tab === "rank" ? "CP Rank" : tab === "rewards" ? "Rewards" : "CP Plaza";
setStatus(title);
}
async function initializeAppConnection() {
if (!isAppEnvironment()) {
setHeadersFromApp(defaultBrowserHeaders());
return;
}
try {
var raw = await getAccessOriginFromApp();
var parsed = JSON.parse(raw);
setHeadersFromApp(normalizeAccessOrigin(parsed));
} catch (error) {
console.warn("Failed to parse app access origin:", error);
setHeadersFromApp(defaultBrowserHeaders());
}
}
async function loadRemoteData() {
try {
var rankingResponse = await apiGet("/activity/week-cp-gift/ranking-data?type=WEEK");
var ranking = unwrapBody(rankingResponse) || {};
var rows = normalizeRankRows(ranking.thisWeekUserTop);
var lastRows = normalizeRankRows(ranking.lastWeekUserTop);
if (rows.length) state.rows = rows;
if (lastRows.length) state.lastRows = lastRows;
if (!lastRows.length && rows.length) state.lastRows = rows.slice(0, 4);
} catch (error) {
debugLog("CP ranking data failed:", error);
}
try {
var rewardResponse = await apiGet("/activity/week-cp-gift/list-reward-pool?type=WEEK_CP_GIFT");
var rewards = normalizeRewardGroups(unwrapBody(rewardResponse));
if (rewards.length) state.rewards = rewards;
} catch (error) {
debugLog("CP reward pool failed:", error);
}
state.ready = true;
render();
}
function render() {
renderRows(dom.plazaList, state.rows.slice(0, 4), "gift");
renderRows(dom.historyList, state.lastRows.slice(0, 4), "score");
renderRows(dom.rankList, state.rows.slice(3, 7), "score");
renderPodium(state.rows.slice(0, 3));
renderRewards();
}
function renderRows(container, rows, mode) {
if (!container) return;
if (!rows.length) {
container.innerHTML = '<div class="cp-row"><div class="empty-state">No ranking data</div></div>';
return;
}
container.innerHTML = rows.map(function (row) {
return renderCpRow(row, mode);
}).join("");
}
function renderCpRow(row, mode) {
var left = normalizeUser(row.left);
var right = normalizeUser(row.right);
var total = row.total || "0";
var center = mode === "gift"
? '<img class="gift-icon" src="' + ASSET_ROOT + '/gift-plane.png" alt="">'
: '<div class="score-heart">' + escapeHtml(total) + '</div>';
return '<div class="cp-row">' +
'<img class="row-wave row-wave-left" src="' + ASSET_ROOT + '/row-wave.png" alt="">' +
'<img class="row-wave row-wave-right" src="' + ASSET_ROOT + '/row-wave.png" alt="">' +
'<div class="cp-user cp-user-left" data-user-id="' + escapeAttribute(left.id) + '">' +
'<img class="cp-avatar" src="' + escapeAttribute(left.avatar) + '" alt="" onerror="this.onerror=null;this.src=&quot;' + DEFAULT_AVATAR + '&quot;">' +
'<span class="cp-name">' + escapeHtml(left.name) + '</span>' +
'</div>' +
'<div class="row-center">' + center + '</div>' +
'<div class="cp-user cp-user-right" data-user-id="' + escapeAttribute(right.id) + '">' +
'<img class="cp-avatar" src="' + escapeAttribute(right.avatar) + '" alt="" onerror="this.onerror=null;this.src=&quot;' + DEFAULT_AVATAR + '&quot;">' +
'<span class="cp-name">' + escapeHtml(right.name) + '</span>' +
'</div>' +
'</div>';
}
function renderPodium(rows) {
if (!dom.podium) return;
var padded = rows.slice();
while (padded.length < 3) padded.push(sampleRankRows()[padded.length]);
var order = [padded[0], padded[1], padded[2]];
dom.podium.innerHTML = order.map(function (row, index) {
return renderPodiumCard(row, index + 1);
}).join("");
}
function renderPodiumCard(row, rank) {
var left = normalizeUser(row.left);
var right = normalizeUser(row.right);
var crown = ASSET_ROOT + "/rank-crown-" + rank + ".png";
return '<div class="podium-card rank-' + rank + '">' +
'<img class="podium-crown" src="' + crown + '" alt="Rank ' + rank + '">' +
'<div class="podium-person podium-person-left" data-user-id="' + escapeAttribute(left.id) + '">' +
'<img class="podium-avatar" src="' + escapeAttribute(left.avatar) + '" alt="" onerror="this.onerror=null;this.src=&quot;' + DEFAULT_AVATAR + '&quot;">' +
'<span class="podium-name">' + escapeHtml(left.name) + '</span>' +
'</div>' +
'<div class="podium-person podium-person-right" data-user-id="' + escapeAttribute(right.id) + '">' +
'<img class="podium-avatar" src="' + escapeAttribute(right.avatar) + '" alt="" onerror="this.onerror=null;this.src=&quot;' + DEFAULT_AVATAR + '&quot;">' +
'<span class="podium-name">' + escapeHtml(right.name) + '</span>' +
'</div></div>';
}
function renderRewards() {
dom.rewardGroups.forEach(function (container, index) {
if (!container) return;
var rewards = state.rewards[index] || [];
container.innerHTML = rewards.map(renderRewardCard).join("");
});
}
function renderRewardCard(item) {
var icon = item.cover || ASSET_ROOT + "/reward-icon.png";
var note = item.note ? '<div class="reward-note">' + escapeHtml(item.note) + '</div>' : "";
return '<div class="reward-card">' +
'<div class="reward-frame"><img src="' + escapeAttribute(icon) + '" alt="" onerror="this.onerror=null;this.src=&quot;' + ASSET_ROOT + '/reward-icon.png&quot;"></div>' +
'<div class="reward-title">' + escapeHtml(item.title) + '</div>' +
note +
'</div>';
}
function normalizeRankRows(list) {
if (!Array.isArray(list)) return [];
return list.map(function (item) {
return {
rank: item.rank,
total: item.total || formatNumber(item.totalNumber),
left: {
id: item.userId,
name: item.userNickname,
avatar: item.userAvatar
},
right: {
id: item.cpUserId,
name: item.cpUserNickname,
avatar: item.cpUserAvatar
}
};
}).filter(function (row) {
return row.left.id || row.right.id;
});
}
function normalizeRewardGroups(body) {
if (!Array.isArray(body)) return [];
return body.map(function (group) {
var list = Array.isArray(group.activityRewardProps) ? group.activityRewardProps : [];
return list.map(function (item) {
return {
title: rewardTitle(item),
note: item.remark || "",
cover: item.cover || ""
};
});
}).filter(function (group) {
return group.length;
}).slice(0, 3);
}
function rewardTitle(item) {
if (!item) return "Reward";
var name = item.content || item.detailType || item.type || "Reward";
var quantity = item.quantity || item.amount;
if (quantity && String(name).indexOf("*") < 0) {
return name + "*" + quantity;
}
return name;
}
function normalizeUser(user) {
user = user || {};
return {
id: user.id || "",
name: user.name || "Namename",
avatar: user.avatar || DEFAULT_AVATAR
};
}
function startCountdown() {
updateCountdown();
if (countdownTimer) window.clearInterval(countdownTimer);
countdownTimer = window.setInterval(updateCountdown, 1000);
}
function updateCountdown() {
var remain = Math.max(0, state.countdownTarget - Date.now());
var seconds = Math.floor(remain / 1000);
var days = Math.floor(seconds / 86400);
seconds -= days * 86400;
var hours = Math.floor(seconds / 3600);
seconds -= hours * 3600;
var minutes = Math.floor(seconds / 60);
seconds -= minutes * 60;
setCountdownValue("days", days);
setCountdownValue("hours", hours);
setCountdownValue("minutes", minutes);
setCountdownValue("seconds", seconds);
}
function setCountdownValue(key, value) {
document.querySelectorAll('[data-countdown="' + key + '"]').forEach(function (node) {
node.textContent = pad2(value);
});
}
function resolveCountdownTarget() {
var endTime = getParam("endTime") || getParam("endAt");
if (endTime) {
var parsed = /^\d+$/.test(endTime) ? Number(endTime) : Date.parse(endTime);
if (parsed && parsed < 1000000000000) parsed *= 1000;
if (parsed && parsed > Date.now()) return parsed;
}
return Date.now() + (2 * 86400 + 2 * 3600 + 2 * 60 + 2) * 1000;
}
async function apiRequest(path, options) {
var url = path.indexOf("http") === 0 ? path : API_BASE_URL + path;
var init = Object.assign({ method: "GET" }, options || {});
init.headers = Object.assign({}, commonHeaders, init.headers || {});
if (init.body && typeof init.body !== "string" && !(init.body instanceof FormData)) {
init.headers["Content-Type"] = "application/json";
init.body = JSON.stringify(init.body);
}
var response = await fetch(url, init);
var data = await response.json();
if (!response.ok) throw new Error("HTTP " + response.status);
if (data && data.status === false) throw new Error(data.errorMsg || data.message || "API Error");
return data;
}
function apiGet(path) {
return apiRequest(path, { method: "GET" });
}
function debugLog() {
if (!DEBUG || !window.console) return;
console.debug.apply(console, arguments);
}
function unwrapBody(response) {
if (response && Object.prototype.hasOwnProperty.call(response, "body")) return response.body;
return response;
}
function setHeadersFromApp(info) {
var next = {};
if (info.authorization !== undefined) next.authorization = info.authorization;
if (info.reqLang) next["req-lang"] = info.reqLang;
if (info.appVersion || info.buildVersion || info.appChannel) {
var appIntel = [];
if (info.appVersion) appIntel.push("version=" + info.appVersion);
if (info.buildVersion) appIntel.push("build=" + info.buildVersion);
if (info.appChannel) appIntel.push("channel=" + info.appChannel);
if (appIntel.length) next["req-app-intel"] = appIntel.join(";");
}
if (info.reqImei) next["req-imei"] = info.reqImei;
if (info.sysOrigin) {
var origin = ["origin=" + info.sysOrigin];
if (info.sysOriginChild) origin.push("originChild=" + info.sysOriginChild);
next["req-sys-origin"] = origin.join(";");
}
commonHeaders = Object.assign({}, commonHeaders, next);
}
function defaultBrowserHeaders() {
var lang = (navigator.language || "en").toLowerCase();
return {
authorization: "",
reqLang: lang.indexOf("ar") === 0 ? "ar" : lang.indexOf("tr") === 0 ? "tr" : lang.indexOf("bn") === 0 ? "bn" : "en",
appVersion: "1.0.0",
buildVersion: "1",
appChannel: "Web",
reqImei: "H5-STATIC",
sysOrigin: "ATYOU",
sysOriginChild: "ATYOU"
};
}
function getAccessOriginFromApp() {
return new Promise(function (resolve) {
var resolved = false;
var finish = function (value) {
if (resolved) return;
resolved = true;
resolve(value || "{}");
};
window.renderData = finish;
window.getIosAccessOriginParam = finish;
if (window.app && typeof window.app.getAccessOrigin === "function") {
try {
finish(window.app.getAccessOrigin());
} catch (error) {
finish("{}");
}
return;
}
if (window.FlutterApp && window.FlutterApp.postMessage) {
window.FlutterApp.postMessage("requestAccessOrigin");
}
window.setTimeout(function () {
finish("{}");
}, 800);
});
}
function normalizeAccessOrigin(data) {
if (!data) return {};
var appIntel = parseKeyValueList(data["Req-App-Intel"] || "");
var sysOrigin = parseKeyValueList(data["Req-Sys-Origin"] || "");
return {
reqLang: data["Req-Lang"],
authorization: data.Authorization,
buildVersion: appIntel.build,
appVersion: appIntel.version,
appChannel: appIntel.channel,
reqImei: appIntel["Req-Imei"],
sysOrigin: sysOrigin.origin,
sysOriginChild: sysOrigin.originChild || sysOrigin.child
};
}
function parseKeyValueList(value) {
var result = {};
String(value || "").split(";").forEach(function (part) {
var index = part.indexOf("=");
if (index < 0) return;
result[part.slice(0, index)] = part.slice(index + 1);
});
return result;
}
function isAppEnvironment() {
return !!(window.app || window.webkit || window.FlutterPageControl || window.FlutterApp);
}
function openUser(id) {
if (!id) return;
try {
if (window.app && window.app.viewUserInfo) {
window.app.viewUserInfo(id);
} else if (window.FlutterPageControl && window.FlutterPageControl.postMessage) {
window.FlutterPageControl.postMessage("view_user_info:" + id);
}
} catch (error) {
console.warn("open user failed:", error);
}
}
function openRules() {
var message = "CP Ranking Rules\\n1. Gifts exchanged between CPs count toward the ranking.\\n2. CPs with at least 5000 gold exchanged can appear in CP Plaza.\\n3. Final rewards follow the activity settlement result.";
try {
if (window.app && window.app.openRuleDialog) {
window.app.openRuleDialog(message);
} else if (window.FlutterPageControl && window.FlutterPageControl.postMessage) {
window.FlutterPageControl.postMessage("open_rules:" + message);
} else {
window.alert(message);
}
} catch (error) {
window.alert(message);
}
}
function getParam(name) {
var hashQuery = "";
if (window.location.hash && window.location.hash.indexOf("?") >= 0) {
hashQuery = window.location.hash.slice(window.location.hash.indexOf("?") + 1);
}
var hashValue = hashQuery ? new URLSearchParams(hashQuery).get(name) : null;
var searchValue = new URLSearchParams(window.location.search).get(name);
return hashValue || searchValue;
}
function setStatus(text) {
if (dom.status) dom.status.textContent = text;
}
function pad2(value) {
value = Math.max(0, Number(value) || 0);
return value < 10 ? "0" + value : String(value);
}
function formatNumber(value) {
if (value == null || value === "") return "0";
var number = Number(value);
if (!Number.isFinite(number)) return String(value);
if (number >= 1000000) return (number / 1000000).toFixed(1).replace(/\\.0$/, "") + "M";
if (number >= 1000) return (number / 1000).toFixed(1).replace(/\\.0$/, "") + "K";
return String(number);
}
function escapeHtml(value) {
return String(value == null ? "" : value)
.replace(/&/g, "&amp;")
.replace(/</g, "&lt;")
.replace(/>/g, "&gt;")
.replace(/"/g, "&quot;")
.replace(/'/g, "&#39;");
}
function escapeAttribute(value) {
return escapeHtml(value);
}
function sampleRankRows() {
var names = [
["Ariya", "Mira"],
["Sana", "Lina"],
["Nora", "Yara"],
["Hana", "Rina"],
["Maya", "Luna"],
["Iris", "Zara"],
["Alia", "Nina"]
];
return names.map(function (pair, index) {
return {
rank: index + 1,
total: ["1234567", "982340", "750120", "608880", "502340", "456700", "388200"][index],
left: {
id: "sample-left-" + index,
name: pair[0],
avatar: DEFAULT_AVATAR
},
right: {
id: "sample-right-" + index,
name: pair[1],
avatar: DEFAULT_AVATAR
}
};
});
}
function sampleRewards() {
return [
[
{ title: "Medal*7 days", note: "(Permanent medal after getting 3 times)" },
{ title: "Honor banner*7 days" },
{ title: "Headwear*7days" },
{ title: "Mount*7days" },
{ title: "Coins*20000" },
{ title: "Duke*7days" }
],
[
{ title: "Medal*7 days", note: "(Permanent medal after getting 3 times)" },
{ title: "Honor banner*7 days" },
{ title: "Mount*7days" },
{ title: "Coins*15000" },
{ title: "Marquis*7days" }
],
[
{ title: "Medal*7 days", note: "(Permanent medal after getting 3 times)" },
{ title: "Honor banner*7 days" },
{ title: "Coins*10000" },
{ title: "Count*7days" }
]
];
}
})();

View File

@ -0,0 +1,123 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1, viewport-fit=cover" />
<meta name="format-detection" content="telephone=no" />
<meta name="apple-mobile-web-app-capable" content="yes" />
<meta name="apple-mobile-web-app-status-bar-style" content="black-translucent" />
<title>Last Weekly CP</title>
<link rel="stylesheet" href="./cp-ranking.css?v=2026052202" />
<script defer src="./cp-ranking.js?v=2026052202"></script>
</head>
<body>
<main class="page" id="page">
<section class="canvas" id="canvas" aria-label="Last Weekly CP">
<img class="hero-image asset" src="../../assets/yumi/cp-ranking/hero.png" alt="" />
<img class="title-art asset" src="../../assets/yumi/cp-ranking/title-art.png" alt="Last Weekly CP" />
<button class="rules-button" id="rulesButton" type="button" aria-label="Rules">Rules</button>
<div class="countdown" aria-label="Activity countdown">
<div class="countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span class="countdown-value" data-countdown="days">02</span>
<span class="countdown-label">D</span>
</div>
<div class="countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span class="countdown-value" data-countdown="hours">02</span>
<span class="countdown-label">H</span>
</div>
<div class="countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span class="countdown-value" data-countdown="minutes">02</span>
<span class="countdown-label">M</span>
</div>
<div class="countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span class="countdown-value" data-countdown="seconds">02</span>
<span class="countdown-label">S</span>
</div>
</div>
<nav class="tabs" aria-label="CP activity tabs">
<button class="tab-button is-active" type="button" data-tab="plaza"><span class="tab-label">CP Plaza</span></button>
<button class="tab-button" type="button" data-tab="rank"><span class="tab-label">CP Rank</span></button>
<button class="tab-button" type="button" data-tab="rewards"><span class="tab-label">Rewards</span></button>
</nav>
<section class="tab-panel tab-panel-plaza is-active" data-panel="plaza" aria-label="CP Plaza">
<div class="ornate-panel plaza-live-panel">
<div class="panel-bg" aria-hidden="true"></div>
<h2>Gifts exchanged between CPs &ge; 5000 gold will be displayed here</h2>
<p class="panel-subtitle">(Display the last 10 records, sorted in ascending order.)</p>
<div class="cp-row-list" id="plazaList"></div>
</div>
<div class="ornate-panel plaza-history-panel">
<div class="panel-bg" aria-hidden="true"></div>
<h2>The previous number one position</h2>
<p class="panel-subtitle">(Display the last 10 records, sorted in ascending order.)</p>
<div class="cp-row-list" id="historyList"></div>
</div>
</section>
<section class="tab-panel tab-panel-rank" data-panel="rank" aria-label="CP Rank">
<div class="podium" id="podium"></div>
<div class="ornate-panel rank-list-panel">
<div class="panel-bg" aria-hidden="true"></div>
<div class="mini-countdown">
<div class="mini-countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span data-countdown="days">02</span>
<b>D</b>
</div>
<div class="mini-countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span data-countdown="hours">02</span>
<b>H</b>
</div>
<div class="mini-countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span data-countdown="minutes">02</span>
<b>M</b>
</div>
<div class="mini-countdown-item">
<img src="../../assets/yumi/cp-ranking/countdown-heart.png" alt="" />
<span data-countdown="seconds">02</span>
<b>S</b>
</div>
</div>
<div class="cp-row-list" id="rankList"></div>
</div>
</section>
<section class="tab-panel tab-panel-rewards" data-panel="rewards" aria-label="Rewards">
<div class="ornate-panel reward-panel reward-panel-1">
<div class="panel-bg" aria-hidden="true"></div>
<img class="reward-crown" src="../../assets/yumi/cp-ranking/rank-crown-1.png" alt="Rank 1" />
<div class="reward-grid" id="rewardGroup1"></div>
</div>
<div class="ornate-panel reward-panel reward-panel-2">
<div class="panel-bg" aria-hidden="true"></div>
<img class="reward-crown" src="../../assets/yumi/cp-ranking/rank-crown-2.png" alt="Rank 2" />
<div class="reward-grid" id="rewardGroup2"></div>
</div>
<div class="ornate-panel reward-panel reward-panel-3">
<div class="panel-bg" aria-hidden="true"></div>
<img class="reward-crown" src="../../assets/yumi/cp-ranking/rank-crown-3.png" alt="Rank 3" />
<div class="reward-grid" id="rewardGroup3"></div>
</div>
</section>
<p class="copyright">The copyright of this activity belongs to Shila</p>
<div class="sr-only" id="status" aria-live="polite"></div>
</section>
</main>
</body>
</html>