diff --git a/h5/ranking/index.html b/h5/ranking/index.html index cdaeb9c..46fe2d2 100644 --- a/h5/ranking/index.html +++ b/h5/ranking/index.html @@ -22,12 +22,9 @@ -webkit-overflow-scrolling: touch; } - - - + +
diff --git a/h5/ranking/ranking.css b/h5/ranking/ranking.css new file mode 100644 index 0000000..cff2fe1 --- /dev/null +++ b/h5/ranking/ranking.css @@ -0,0 +1,507 @@ +html, +body, +#app { + width: 100%; + min-height: 100%; + margin: 0; + padding: 0; + max-width: none; +} + +body { + display: block; + background: #000; + overflow-x: hidden; +} + +#app { + display: block; + font-weight: 400; +} + +@media (min-width: 1024px) { + body { + display: block; + place-items: initial; + } + + #app { + display: block; + grid-template-columns: none; + padding: 0; + } +} + +.ranking-page { + width: 100vw; + min-height: 100vh; + position: relative; + overflow-x: hidden; + background: #000; + color: #fff; + font-size: 14px; +} + +.ranking-page * { + box-sizing: border-box; +} + +.bg-layer { + width: 100vw; + min-height: 100vh; + position: absolute; + top: 0; + left: 0; + z-index: 0; + pointer-events: none; +} + +.bg-layer .bg-image { + width: 100vw; + object-fit: cover; + object-position: center top; + display: block; +} + +.loading-container { + width: 100vw; + height: 100vh; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + color: #666; + overflow: hidden; + position: relative; +} + +.ranking-loading-image { + position: relative; + z-index: 4; + display: block; + width: 180px; + max-width: 70vw; + height: auto; + margin: 0 auto 12px; + object-fit: contain; +} + +.loading-container p { + position: relative; + z-index: 4; + margin: 0; +} + +.ranking-content { + position: relative; + z-index: 4; + min-height: 100vh; +} + +.ranking-content .bg-layer { + z-index: -1; +} + +.sticky-header { + position: sticky; + top: 0; + z-index: 999; + border-radius: 0 0 12px 12px; + transition: all 0.3s; + height: 35vw; +} + +.sticky-header.stuck { + background-color: rgba(255, 255, 255, 0.1); + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); +} + +.sticky-spacer { + height: 13vw; +} + +.type-tabs-wrap { + padding: 4px 8%; +} + +.type-tabs { + display: flex; + justify-content: space-around; + position: relative; +} + +.type-tab { + color: #fff; + border: 0; + background: transparent; + padding: 0; + font: inherit; + font-weight: 700; + font-size: 1em; + line-height: 1.4; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.type-tab.active { + color: #ff9500; +} + +.back-btn { + position: absolute; + left: -4%; + top: 45%; + transform: translateY(-50%); + width: 6%; + min-width: 18px; + display: block; + border: 0; + background: transparent; + padding: 0; + cursor: pointer; +} + +.back-btn img { + display: block; + width: 100%; +} + +.period-tabs { + margin: 1vw 6vw 4vw; + padding: 4px; + border-radius: 32px; + border: 1px solid #b2a67d; + background: linear-gradient(90deg, #0d0d0d 0%, #232323 100%); + -webkit-backdrop-filter: blur(32px); + backdrop-filter: blur(32px); + display: grid; + grid-template-columns: repeat(3, 1fr); +} + +.period-item { + display: flex; + justify-content: center; + align-items: center; +} + +.period-item.active { + background-image: url("/assets/yumi-ranking/Ranking/Overall/tabSelectedBg.png"); + background-size: 100% 100%; + background-repeat: no-repeat; + background-position: center; +} + +.period-tab { + border: 0; + background: transparent; + color: #fff; + font: inherit; + font-weight: 510; + padding: 4px 10px; + line-height: 1.4; + cursor: pointer; + -webkit-tap-highlight-color: transparent; +} + +.period-item.active .period-tab { + color: #000; + display: flex; + justify-content: center; + align-items: center; +} + +.ranking-main { + position: relative; +} + +.top-section { + margin: 0 10px; +} + +.top-one-wrap { + display: flex; + justify-content: center; +} + +.top-pair-wrap { + display: flex; + justify-content: space-between; + margin-top: 0; +} + +.top-user { + color: #000; + cursor: pointer; +} + +.top-user-inner { + position: relative; +} + +.top-frame { + display: block; + position: relative; + z-index: 2; + width: 100%; +} + +.top-avatar-layer { + position: absolute; + top: 0; + left: 0; + right: 0; + z-index: 1; + display: flex; + justify-content: center; + align-items: center; +} + +.top-avatar-box img { + display: block; + width: 100%; + aspect-ratio: 1 / 1; + border-radius: 50%; + object-fit: cover; +} + +.top-user-info { + position: absolute; + bottom: 0; + left: 0; + right: 0; + z-index: 3; + display: flex; + flex-direction: column; + justify-content: flex-end; + align-items: center; + gap: 1vw; +} + +.top-name-box { + display: flex; + justify-content: center; + align-items: center; +} + +.show-text { + color: #fff; + text-align: center; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.top1-text { + font-size: 0.9em; +} + +.top-text { + font-size: 0.7em; +} + +.quantity-pill { + position: relative; + border-radius: 80px; + border: 1px solid #ffdf36; + background: linear-gradient(270deg, #000 0%, #3b3b3b 47.45%, #000 100%); + box-shadow: 0 0 4px 0 rgba(0, 0, 0, 0.4) inset; +} + +.quantity-inner { + display: flex; + justify-content: center; + align-items: center; + gap: 2px; + padding: 3px 5px; +} + +.coin-icon { + display: block; + width: 1.3em; + aspect-ratio: 1 / 1; + object-fit: cover; +} + +.quantity-text { + font-weight: 700; + background: linear-gradient(180deg, #ffe656 0%, #fff 100%); + background-clip: text; + -webkit-background-clip: text; + -webkit-text-fill-color: transparent; +} + +.rank-list { + padding: 3vw 3vw 0; + display: flex; + flex-direction: column; + gap: 2vw; +} + +.item-center { + position: relative; + width: 100%; +} + +.item-center > .item-bg { + width: 100%; + height: 100%; + display: block; +} + +.item-content { + position: absolute; + inset: 0; + display: flex; + justify-content: center; + align-items: center; + overflow-y: auto; +} + +.item-content::-webkit-scrollbar { + display: none; +} + +.rank-row { + min-height: 19vw; + cursor: pointer; +} + +.rank-row .item-content { + padding: 3vw; + justify-content: space-between; + gap: 8px; +} + +.rank-left { + flex: 1; + min-width: 0; + align-self: stretch; + display: flex; + justify-content: space-around; + align-items: center; + gap: 8px; +} + +.rank-number, +.mine-rank { + width: auto; + min-width: 0; + align-self: stretch; + display: flex; + justify-content: center; + align-items: center; +} + +.rank-number div, +.mine-rank div { + font-weight: 700; +} + +.avatar { + display: block; + width: 3em; + aspect-ratio: 1 / 1; + border-radius: 50%; + object-fit: cover; +} + +.rank-name, +.mine-name { + flex: 1; + min-width: 0; + padding: 0 4px; + font-weight: 700; + overflow: hidden; + white-space: nowrap; + text-overflow: ellipsis; +} + +.rank-value, +.mine-value { + width: auto; + min-width: 0; + align-self: stretch; + display: flex; + align-items: center; +} + +.rank-value-text, +.mine-value-text { + font-weight: 500; + color: rgba(248, 182, 45, 1); +} + +.bottom-spacer { + height: 26vw; +} + +.my-ranking { + min-height: 25vw; + position: fixed; + left: 0; + right: 0; + bottom: 0; + z-index: 999; + padding: 0 1px; +} + +.my-ranking .item-content { + padding: 4vw 4vw 0; + justify-content: space-between; + gap: 8px; +} + +.mine-left { + flex: 1; + min-width: 0; + align-self: stretch; + display: flex; + justify-content: space-around; + align-items: center; + gap: 8px; +} + +.load-more-sentinel { + width: 100%; + height: 1px; +} + +.sr-status { + position: absolute; + width: 1px; + height: 1px; + overflow: hidden; + clip: rect(0, 0, 0, 0); +} + +@media screen and (max-width: 360px) { + .ranking-page, + .top-user { + font-size: 10px; + } +} + +@media screen and (min-width: 360px) { + .ranking-page { + font-size: 14px; + } + + .top-user { + font-size: 12px; + } +} + +@media screen and (min-width: 768px) { + .ranking-page, + .top-user { + font-size: 24px; + } +} + +@media screen and (min-width: 1024px) { + .ranking-page, + .top-user { + font-size: 32px; + } +} diff --git a/h5/ranking/ranking.js b/h5/ranking/ranking.js new file mode 100644 index 0000000..9a3d100 --- /dev/null +++ b/h5/ranking/ranking.js @@ -0,0 +1,809 @@ +(function () { + "use strict"; + + var API_BASE_URL = "https://jvapi.haiyihy.com"; + var ASSET_BASE = "/assets/yumi-ranking/Ranking/Overall/"; + var BG_VERSION = "20260424c"; + var DEFAULT_AVATAR = "/assets/defaultAvatar-CdxWBK1k-1776148661448.png"; + var COIN_ICON = "/assets/coin-lz3Tp0nX-1776148661447.png"; + var LOADING_GIF = "/assets/loading-ranking.gif"; + var BACK_ICON = + "data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAEgAAABICAYAAABV7bNHAAAACXBIWXMAACE4AAAhOAFFljFgAAAAAXNSR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAADxSURBVHgB7dvBDYJAFEXRHyuhBEuzA+3EWAmlUIJ2MDJCNATy5m1I1H9PwoZhww3Mgh8iAAAAAACllG48joGlOUxfPoZ6LvCOM5S1PrITcV4is1acuhY7O8SXmveX+gp14rJbZGQ8OdUlMiKOQByBOAJxBOIIxBGIIxBHII5AHIE4AnEE4gjEEYgjEKeBOMJ488d/irPHN+lHYz33JKIqywHflnNkZm7SRCJSA5EMRDIQyUAkA5EMRDIQyUAkA5EMRDIQyUAkA5EMRDIQyWBGOkVmRqR7ZNeKFJCRroHJRqS+8DvUWpkGk10AAAAAwO96AiOznp1YXpZWAAAAAElFTkSuQmCC"; + + var RANKING_TYPES = ["Wealth", "Charm", "Room"]; + var PERIODS = [ + { key: "Daily", api: "DAY", label: "ranking_daily" }, + { key: "Weekly", api: "WEEK", label: "ranking_weekly" }, + { key: "Monthly", api: "MONTH", label: "ranking_monthly" } + ]; + var TYPE_LABELS = { + Wealth: "ranking_wealth", + Charm: "ranking_charm", + Room: "ranking_room" + }; + var TYPE_TO_MY_RANK = { + Wealth: "GIFTS_SEND", + Charm: "GIFTS_RECEIVED", + Room: "ROOM_GIFTS" + }; + var TYPE_ENDPOINTS = { + Wealth: "/activity/leaderboard/gifts-send", + Charm: "/activity/leaderboard/gifts-received", + Room: "/activity/leaderboard/room-gifts" + }; + var TYPE_ASSETS = { + Wealth: { + bg: "bgWealth", + top1: "top1-wealth", + top2: "top2", + top3: "top3" + }, + Charm: { + bg: "bgCharm", + top1: "top1-charm", + top2: "top2", + top3: "top3" + }, + Room: { + bg: "bgRoom", + top1: "top1-room", + top2: "top2-room", + top3: "top3-room" + } + }; + var I18N = { + en: { + ranking_wealth: "Wealth", + ranking_charm: "Charm", + ranking_room: "Room", + ranking_daily: "Daily", + ranking_weekly: "Weekly", + ranking_monthly: "Monthly", + loading: "Loading" + } + }; + + var commonHeaders = { + authorization: "Bearer undefined", + "req-app-intel": "version=1.0.0;build=1;model=SM-G9550;sysVersion=9;channel=Google", + "req-client": "Android", + "req-imei": "8fa54d728ab449e04f9292329ed44bda", + "req-lang": "id-ID", + "req-sys-origin": "origin=ATYOU;originChild=ATYOU", + "req-version": "V2", + "req-zone": "Asia/Shanghai" + }; + + var state = { + rankingType: getInitialRankingType(), + period: "Daily", + dateType: "DAY", + myRankType: "GIFTS_SEND", + sourceRanking: {}, + visibleRanking: [], + myRanking: {}, + visibleCount: 10, + hasMore: true, + loading: true, + locale: getLocale(), + isApp: isAppEnvironment(), + requestId: 0 + }; + + var stickyObserver = null; + var loadObserver = null; + var lastTypeClick = 0; + var lastPeriodClick = 0; + + function asset(name) { + return ASSET_BASE + name + ".png"; + } + + function backgroundAsset(type) { + return asset(TYPE_ASSETS[type].bg) + "?v=" + BG_VERSION; + } + + function t(key) { + var messages = I18N[state.locale] || I18N.en; + return messages[key] || I18N.en[key] || key; + } + + function getLocale() { + var raw = getParam("lang") || navigator.language || "en"; + raw = String(raw).toLowerCase(); + if (raw.indexOf("ar") === 0) return "ar"; + if (raw.indexOf("tr") === 0) return "tr"; + if (raw.indexOf("bn") === 0) return "bn"; + return "en"; + } + + function getInitialRankingType() { + var first = getParam("first") || "Wealth"; + return RANKING_TYPES.indexOf(first) >= 0 ? first : "Wealth"; + } + + 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 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("child=" + info.sysOriginChild); + next["req-sys-origin"] = origin.join(";"); + } + commonHeaders = Object.assign({}, commonHeaders, next); + } + + function isMobileDevice() { + return /Android|iPad|iPhone|iPod/.test(navigator.userAgent); + } + + function isAppEnvironment() { + return !!(window.app || window.webkit || window.FlutterPageControl); + } + + function defaultBrowserHeaders() { + var lang = navigator.language || ""; + return { + authorization: "", + reqLang: lang.toLowerCase().indexOf("ar") === 0 + ? "ar" + : lang.toLowerCase().indexOf("tr") === 0 + ? "tr" + : lang.toLowerCase().indexOf("bn") === 0 + ? "bn" + : "en", + appVersion: "1.0.0", + buildVersion: "1", + appChannel: "Web", + reqImei: "H5-STATIC", + sysOrigin: "LIKEI", + sysOriginChild: "LIKEI" + }; + } + + function mockAppAccessOrigin() { + return JSON.stringify({ + Authorization: "Bearer FLUTTER_MOCK_TOKEN", + "Req-Lang": "en", + "Req-App-Intel": "build=1.0;version=2.1;channel=flutter;Req-Imei=flutter-device", + "Req-Sys-Origin": "origin=ATYOU;originChild=ATYOU" + }); + } + + function parseKeyValueList(value) { + var result = {}; + if (!value) return result; + String(value).split(";").forEach(function (part) { + if (!part || part.indexOf("=") < 0) return; + var index = part.indexOf("="); + result[part.slice(0, index)] = part.slice(index + 1); + }); + return result; + } + + 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.child + }; + } + + 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; + + var attempts = 0; + var check = function () { + attempts += 1; + if (window.app && typeof window.app.getAccessOrigin === "function") { + try { + finish(window.app.getAccessOrigin()); + } catch (error) { + console.error("getAccessOrigin failed:", error); + finish(mockAppAccessOrigin()); + } + return; + } + if (attempts < 50) { + window.setTimeout(check, 100); + return; + } + if (window.FlutterApp && window.FlutterApp.postMessage) { + window.FlutterApp.postMessage("requestAccessOrigin"); + } + finish(mockAppAccessOrigin()); + }; + + if (isMobileDevice()) { + check(); + } else { + finish(mockAppAccessOrigin()); + } + }); + } + + async function initializeAppConnection() { + if (!isAppEnvironment()) { + setHeadersFromApp(defaultBrowserHeaders()); + await fetchUserProfileAfterConnection(); + return; + } + + try { + var raw = await getAccessOriginFromApp(); + var parsed = JSON.parse(raw); + setHeadersFromApp(normalizeAccessOrigin(parsed)); + } catch (error) { + console.error("Failed to parse access origin:", error); + setHeadersFromApp(defaultBrowserHeaders()); + } + await fetchUserProfileAfterConnection(); + } + + async function fetchUserProfileAfterConnection() { + try { + var response = await apiGet("/team/member/profile"); + if (response && response.status && response.body) { + localStorage.setItem("userProfile", JSON.stringify(response.body)); + } + } catch (error) { + console.warn("Failed to fetch user info after connection:", error); + } + } + + 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 makeApiError("HTTP Error: " + response.status + " " + response.statusText, response.status, data); + } + if (data && data.status === false) { + throw makeApiError(data.errorMsg || data.message || "API Error: " + data.errorCode, response.status, data); + } + return data; + } + + function apiGet(path) { + return apiRequest(path, { method: "GET" }); + } + + function makeApiError(message, status, response) { + var error = new Error(message); + error.name = "ApiError"; + error.status = status || null; + error.response = response || null; + error.errorMsg = response && response.errorMsg; + error.errorCode = response && response.errorCode; + return error; + } + + async function fetchLeaderboard(type) { + var response = await apiGet(TYPE_ENDPOINTS[type]); + if (response && response.status && response.body) { + state.sourceRanking = response.body; + updatePeriodRanking(); + } + } + + async function fetchMyRanking() { + var path = "/activity/leaderboard/my-rank?type=" + + encodeURIComponent(state.myRankType) + + "&dateType=" + + encodeURIComponent(state.dateType); + var response = await apiGet(path); + state.myRanking = response && response.status && response.body ? response.body : {}; + } + + function updatePeriodRanking() { + var body = state.sourceRanking || {}; + var rows = []; + if (state.period === "Hourly" && body.hourly) rows = body.hourly; + if (state.period === "Daily" && body.daily) rows = body.daily; + if (state.period === "Weekly" && body.weekly) rows = body.weekly; + if (state.period === "Monthly" && body.monthly) rows = body.monthly; + state.visibleRanking = Array.isArray(rows) ? rows : []; + state.visibleCount = 10; + state.hasMore = true; + } + + async function preloadImages(urls) { + await Promise.all(urls.map(function (url) { + return new Promise(function (resolve) { + var image = new Image(); + image.onload = resolve; + image.onerror = resolve; + image.src = url; + }); + })); + } + + function preloadRankingAssets() { + return preloadImages([ + backgroundAsset("Wealth"), + backgroundAsset("Charm"), + backgroundAsset("Room"), + asset("top1-wealth"), + asset("top1-charm"), + asset("top1-room"), + asset("top2"), + asset("top3"), + asset("top2-room"), + asset("top3-room") + ]); + } + + async function loadInitialData() { + applyTypeState(state.rankingType); + render(); + try { + await initializeAppConnection(); + await Promise.all([ + fetchCurrentRankingGroup(), + preloadRankingAssets() + ]); + } catch (error) { + console.error("Ranking preload failed:", error); + } finally { + state.loading = false; + render(); + } + } + + async function fetchCurrentRankingGroup() { + var requestId = ++state.requestId; + var type = state.rankingType; + await Promise.all([ + fetchLeaderboard(type).catch(function (error) { + console.error("Leaderboard request failed:", error); + }), + fetchMyRanking().catch(function (error) { + console.error("My ranking request failed:", error); + }) + ]); + if (requestId !== state.requestId) return; + render(); + } + + async function fetchOnlyMyRanking() { + var requestId = ++state.requestId; + try { + await fetchMyRanking(); + } catch (error) { + console.error("My ranking request failed:", error); + } + if (requestId !== state.requestId) return; + render(); + } + + function applyTypeState(type) { + state.rankingType = type; + state.myRankType = TYPE_TO_MY_RANK[type]; + } + + function switchRankingType(type) { + if (state.rankingType === type) return; + var now = Date.now(); + if (now - lastTypeClick < 1000) return; + lastTypeClick = now; + + applyTypeState(type); + state.period = "Daily"; + state.dateType = "DAY"; + state.sourceRanking = {}; + state.visibleRanking = []; + state.myRanking = {}; + state.visibleCount = 10; + state.hasMore = true; + render(); + fetchCurrentRankingGroup(); + } + + function switchPeriod(period) { + if (state.period === period.key) return; + var now = Date.now(); + if (now - lastPeriodClick < 500) return; + lastPeriodClick = now; + + state.period = period.key; + state.dateType = period.api; + state.visibleRanking = []; + state.myRanking = {}; + updatePeriodRanking(); + render(); + fetchOnlyMyRanking(); + } + + function loadMoreRows() { + if (!state.hasMore) return; + var total = state.visibleRanking.filter(function (_, index) { + return index >= 3; + }).length; + var current = state.visibleCount; + if (current < total) { + state.visibleCount = Math.min(current + 10, total); + if (state.visibleCount >= total) state.hasMore = false; + render(); + } else { + state.hasMore = false; + } + } + + function getTopUsers() { + var list = state.visibleRanking.slice(); + while (list.length < 3) { + list.push({ + avatar: "", + id: "", + nickname: "", + quantityFormat: "" + }); + } + return list.slice(0, 3); + } + + function getListRows() { + return state.visibleRanking.filter(function (_, index) { + return index >= 3; + }).slice(0, state.visibleCount); + } + + function topLayout(type, isTopOne) { + var result = {}; + if (type === "Wealth") { + result.minHeight = isTopOne ? "76.5vw" : "44vw"; + result.avatarLayer = isTopOne ? "height:110%" : "height:72%"; + result.avatarWidth = isTopOne ? "31%" : "45%"; + result.nameBox = isTopOne ? "height:8vw;width:30%" : "height:5vw;width:50%"; + result.pillWidth = isTopOne ? "30%" : "50%"; + } else if (type === "Charm") { + result.minHeight = isTopOne ? "80vw" : "44vw"; + result.avatarLayer = isTopOne ? "height:98%" : "height:74%"; + result.avatarWidth = isTopOne ? "30%" : "45%"; + result.nameBox = isTopOne ? "height:10vw;width:40%" : "height:5vw;width:40%"; + result.pillWidth = isTopOne ? "35%" : "50%"; + } else { + result.minHeight = isTopOne ? "76vw" : "52vw"; + result.avatarLayer = isTopOne ? "height:110%" : "height:80%"; + result.avatarWidth = isTopOne ? "30%" : "50%"; + result.nameBox = isTopOne ? "height:7vw;width:28%" : "height:4.5vw;width:40%"; + result.pillWidth = isTopOne ? "30%" : "50%"; + } + return result; + } + + function render() { + var app = document.getElementById("app"); + if (!app) return; + if (state.loading) { + app.innerHTML = renderLoading(); + return; + } + app.innerHTML = renderRanking(); + bindEvents(); + setupObservers(); + } + + function renderBgLayer(zIndex) { + var style = zIndex === undefined ? "" : ' style="z-index:' + zIndex + '"'; + return '
background-0
'; + } + + function renderLoading() { + return '
' + + '
' + + renderBgLayer() + + '' + + '

' + escapeHtml(t("loading")) + '...

' + + '
' + + '
'; + } + + function renderRanking() { + return '
' + + '
' + + renderBgLayer(-1) + + '
' + + renderHeader() + + '
' + + renderTopSection() + + renderRankList() + + '
' + + '
' + + '
' + + renderMyRanking() + + '
' + + '
'; + } + + function renderHeader() { + return ''; + } + + function renderTopSection() { + var users = getTopUsers(); + return '
' + + '
' + + renderTopUser(users[0], 0, true) + + '
' + + '
' + + renderTopUser(users[1], 1, false) + + renderTopUser(users[2], 2, false) + + '
' + + '
'; + } + + function renderTopUser(user, index, isTopOne) { + var assets = TYPE_ASSETS[state.rankingType]; + var frame = isTopOne ? assets.top1 : index === 1 ? assets.top2 : assets.top3; + var layout = topLayout(state.rankingType, isTopOne); + var width = isTopOne ? "85%" : "43%"; + var name = user && user.nickname ? user.nickname : ""; + var avatar = user && user.avatar ? user.avatar : ""; + var quantity = user && user.quantityFormat ? user.quantityFormat : ""; + var id = user && user.id ? user.id : ""; + return '
' + + '
' + + '' + + '
' + + '
' + + '' + + '
' + + '
' + + '' + + '
' + + '
'; + } + + function renderQuantityPill(quantity, width, isTopOne) { + return '
' + + '
' + + '' + + '
' + + escapeHtml(quantity) + + '
' + + '
' + + '
'; + } + + function renderRankList() { + var rows = getListRows(); + return '
' + + rows.map(function (row, index) { + return renderRankRow(row, index + 4); + }).join("") + + (state.hasMore ? '
' : "") + + '
'; + } + + function renderItemCenter(classes, minHeight, image, content, attrs) { + return '
' + + '' + + '
' + content + '
' + + '
'; + } + + function renderRankRow(row, rank) { + var id = row && row.id ? row.id : ""; + var avatar = row && row.avatar ? row.avatar : ""; + var name = row && row.nickname ? row.nickname : ""; + var quantity = row && row.quantityFormat ? row.quantityFormat : ""; + var content = '
' + + '
' + rank + '
' + + '' + + '
' + escapeHtml(name) + '
' + + '
' + + '
' + + '' + + '
' + escapeHtml(quantity) + '
' + + '
'; + return renderItemCenter( + "rank-row", + "19vw", + asset("RankingItem"), + content, + ' data-user-id="' + escapeAttribute(id) + '"' + ); + } + + function renderMyRanking() { + var mine = state.myRanking || {}; + var rank = mine.rank || 999; + var avatar = mine.avatar || ""; + var name = mine.nickname || ""; + var quantity = mine.quantityFormat || 0; + var content = '
' + + '
' + escapeHtml(String(rank)) + '
' + + '' + + '
' + escapeHtml(name) + '
' + + '
' + + '
' + + '' + + '
' + escapeHtml(String(quantity)) + '
' + + '
'; + return renderItemCenter("my-ranking", "25vw", asset("myRankingBg"), content); + } + + function bindEvents() { + document.querySelectorAll("[data-ranking-type]").forEach(function (button) { + button.addEventListener("click", function () { + switchRankingType(button.getAttribute("data-ranking-type")); + }); + }); + + document.querySelectorAll("[data-period]").forEach(function (button) { + button.addEventListener("click", function () { + var key = button.getAttribute("data-period"); + var period = PERIODS.filter(function (item) { + return item.key === key; + })[0]; + if (period) switchPeriod(period); + }); + }); + + var back = document.querySelector(".back-btn"); + if (back) back.addEventListener("click", closePage); + + document.querySelectorAll("[data-user-id]").forEach(function (node) { + node.addEventListener("click", function () { + openAppTarget(node.getAttribute("data-user-id")); + }); + }); + + document.querySelectorAll("[data-avatar]").forEach(function (img) { + img.addEventListener("error", function () { + if (img.getAttribute("src") !== DEFAULT_AVATAR) { + img.setAttribute("src", DEFAULT_AVATAR); + } + }); + }); + } + + function setupObservers() { + if (stickyObserver) stickyObserver.disconnect(); + if (loadObserver) loadObserver.disconnect(); + + var sentinel = document.querySelector(".sentinel"); + var stickyHeader = document.querySelector(".sticky-header"); + if (sentinel && stickyHeader && "IntersectionObserver" in window) { + stickyObserver = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting) { + stickyHeader.classList.remove("stuck"); + } else { + stickyHeader.classList.add("stuck"); + } + }); + }); + stickyObserver.observe(sentinel); + } + + var loadMore = document.querySelector(".load-more-sentinel"); + if (loadMore && "IntersectionObserver" in window) { + loadObserver = new IntersectionObserver(function (entries) { + entries.forEach(function (entry) { + if (entry.isIntersecting && state.hasMore) loadMoreRows(); + }); + }, { rootMargin: "100px" }); + loadObserver.observe(loadMore); + } + } + + function closePage() { + try { + if (window.app && window.app.closePage) { + window.app.closePage(); + } else if (window.FlutterPageControl && window.FlutterPageControl.postMessage) { + window.FlutterPageControl.postMessage("close_page"); + } else { + window.history.back(); + } + } catch (error) { + console.error("close page failed:", error); + window.history.back(); + } + } + + function openAppTarget(id) { + if (!id || !state.isApp) return; + try { + if (state.rankingType === "Room") { + if (window.app && window.app.gotoRoom) { + window.app.gotoRoom(id); + } else if (window.FlutterPageControl && window.FlutterPageControl.postMessage) { + window.FlutterPageControl.postMessage("go_to_room:" + id); + } + } else 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.error("open app target failed:", error); + } + } + + function escapeHtml(value) { + return String(value == null ? "" : value) + .replace(/&/g, "&") + .replace(//g, ">") + .replace(/"/g, """) + .replace(/'/g, "'"); + } + + function escapeAttribute(value) { + return escapeHtml(value); + } + + loadInitialData(); +})();