580 lines
19 KiB
JavaScript
580 lines
19 KiB
JavaScript
(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="' + DEFAULT_AVATAR + '"">' +
|
|
'<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="' + DEFAULT_AVATAR + '"">' +
|
|
'<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="' + DEFAULT_AVATAR + '"">' +
|
|
'<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="' + DEFAULT_AVATAR + '"">' +
|
|
'<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="' + ASSET_ROOT + '/reward-icon.png""></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, "&")
|
|
.replace(/</g, "<")
|
|
.replace(/>/g, ">")
|
|
.replace(/"/g, """)
|
|
.replace(/'/g, "'");
|
|
}
|
|
|
|
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" }
|
|
]
|
|
];
|
|
}
|
|
})();
|