This commit is contained in:
zhx 2026-07-16 18:52:12 +08:00
parent de739902dc
commit 5d1d743990
10 changed files with 149 additions and 51 deletions

View File

@ -10,7 +10,7 @@
<link rel="stylesheet" href="../../common/theme.css?v=20260608-cp" /> <link rel="stylesheet" href="../../common/theme.css?v=20260608-cp" />
<link <link
rel="stylesheet" rel="stylesheet"
href="./style.css?v=20260625-cp-plaza-api-reward-auto" href="./style.css?v=20260716-cp-formation-gift-carousel"
/> />
</head> </head>
<body> <body>
@ -177,7 +177,7 @@
})(); })();
</script> </script>
<script src="../../common/i18n.js?v=20260608-cp"></script> <script src="../../common/i18n.js?v=20260608-cp"></script>
<script src="../../common/api.js?v=20260625-cp-plaza-api"></script> <script src="../../common/api.js?v=20260716-cp-formation-gift"></script>
<script src="./script.js?v=20260625-cp-plaza-api-reward-auto-coin"></script> <script src="./script.js?v=20260716-cp-formation-gift-carousel"></script>
</body> </body>
</html> </html>

View File

@ -10,6 +10,11 @@
var REWARD_PANEL_BOTTOM_PADDING = 80; var REWARD_PANEL_BOTTOM_PADDING = 80;
var REWARD_STAGE_BOTTOM_PADDING = 133; var REWARD_STAGE_BOTTOM_PADDING = 133;
var REWARD_MIN_PANEL_HEIGHT = 320; var REWARD_MIN_PANEL_HEIGHT = 320;
var PLAZA_VISIBLE_ROWS = 4;
var PLAZA_ROW_HEIGHT = 199;
var PLAZA_ROW_GAP = 24;
var PLAZA_SECONDS_PER_ROW = 3.2;
var PLAZA_FEED_LIMIT = 20;
var STORAGE_KEY = 'hy_cp_activity_end_at'; var STORAGE_KEY = 'hy_cp_activity_end_at';
var stageHeights = { var stageHeights = {
plaza: 4283, plaza: 4283,
@ -210,10 +215,14 @@
renderAvatar('avatar-left', row.leftAvatar), renderAvatar('avatar-left', row.leftAvatar),
renderAvatar('avatar-right', row.rightAvatar), renderAvatar('avatar-right', row.rightAvatar),
'<span class="name name-left">', '<span class="name name-left">',
'<bdi>',
escapeHTML(row.leftName), escapeHTML(row.leftName),
'</bdi>',
'</span>', '</span>',
'<span class="name name-right">', '<span class="name name-right">',
'<bdi>',
escapeHTML(row.rightName), escapeHTML(row.rightName),
'</bdi>',
'</span>', '</span>',
'<img class="deco-line deco-left" src="./assets/deco-line.png" alt="" aria-hidden="true" />', '<img class="deco-line deco-left" src="./assets/deco-line.png" alt="" aria-hidden="true" />',
'<img class="deco-line deco-right" src="./assets/deco-line.png" alt="" aria-hidden="true" />', '<img class="deco-line deco-right" src="./assets/deco-line.png" alt="" aria-hidden="true" />',
@ -222,6 +231,40 @@
].join(''); ].join('');
} }
function renderPlazaCarousel(rows) {
var sourceRows = Array.isArray(rows) ? rows : [];
var shouldLoop = sourceRows.length > PLAZA_VISIBLE_ROWS;
var rowHTML = sourceRows.map(renderCPRow).join('');
var loopDistance =
sourceRows.length * (PLAZA_ROW_HEIGHT + PLAZA_ROW_GAP);
var loopDuration = Math.max(
sourceRows.length * PLAZA_SECONDS_PER_ROW,
PLAZA_VISIBLE_ROWS * PLAZA_SECONDS_PER_ROW
);
// 一组数据末尾保留一个 row-gap再紧接完全相同的第二组轨道移动一整组高度后第二组首行与初始首行位置一致循环重置时不会闪白或跳帧。
return [
'<div class="plaza-carousel-viewport">',
'<div class="plaza-carousel-track',
shouldLoop ? ' is-looping' : '',
'" style="--plaza-loop-distance:',
loopDistance,
'px;--plaza-loop-duration:',
loopDuration,
's">',
'<div class="plaza-carousel-set">',
rowHTML,
'</div>',
shouldLoop
? '<div class="plaza-carousel-set" aria-hidden="true">' +
rowHTML +
'</div>'
: '',
'</div>',
'</div>',
].join('');
}
function renderPlaza() { function renderPlaza() {
var plazaRows = activityData.plazaRows || []; var plazaRows = activityData.plazaRows || [];
var previousRows = activityData.previousRows || []; var previousRows = activityData.previousRows || [];
@ -229,12 +272,10 @@
'<h2 class="panel-title">', '<h2 class="panel-title">',
t( t(
'cp.plazaGiftTitle', 'cp.plazaGiftTitle',
'CP pairs with intimacy ≥ 5000 are shown here' 'CP formation gifts worth more than 5,000 coins are shown here'
), ),
'</h2>', '</h2>',
'<div class="cp-list">', renderPlazaCarousel(plazaRows),
plazaRows.map(renderCPRow).join(''),
'</div>',
].join(''); ].join('');
var panels = [renderPanel('plaza-panel-a', 1816, 1152, firstBody)]; var panels = [renderPanel('plaza-panel-a', 1816, 1152, firstBody)];
if (previousRows.length) { if (previousRows.length) {
@ -463,21 +504,37 @@
}; };
} }
function normalizePlazaEntry(entry) { function normalizeFormationGiftEntry(entry) {
var intimacyValue = Number( var giftCoinValue = Number(
entry.intimacy_value || entry.intimacyValue || 0 (entry &&
(entry.gift_coin_value ||
entry.giftCoinValue ||
entry.coin_spent ||
entry.coinSpent)) ||
0
); );
var userA = normalizePairUser(entry.user_a); var requester = normalizePairUser(
var userB = normalizePairUser(entry.user_b); entry && (entry.requester || entry.user_a || entry.userA)
if (intimacyValue < 5000 || !userA || !userB) return null; );
var target = normalizePairUser(
entry && (entry.target || entry.user_b || entry.userB)
);
// 业务定义是“超过 5000 金币”,不是大于等于;即使旧网关误返回 5000 或更低的记录H5 也不能把它展示出来。
if (
!Number.isFinite(giftCoinValue) ||
giftCoinValue <= 5000 ||
!requester ||
!target
)
return null;
return { return {
type: 'score', type: 'score',
rawScore: intimacyValue, rawGiftCoinValue: giftCoinValue,
score: formatScore(intimacyValue), score: formatScore(giftCoinValue),
leftName: userA.name, leftName: requester.name,
rightName: userB.name, rightName: target.name,
leftAvatar: userA.avatar, leftAvatar: requester.avatar,
rightAvatar: userB.avatar, rightAvatar: target.avatar,
}; };
} }
@ -549,18 +606,14 @@
} }
} }
function applyPlazaLeaderboard(data) { function applyPlazaFormationGiftFeed(data) {
var items = data && Array.isArray(data.items) ? data.items : []; var items = data && Array.isArray(data.items) ? data.items : [];
// CP 广场只展示真实接口里亲密值达到门槛的 CP接口为空或低于门槛时保持空列表不再用 mock 用户兜底 // feed 由后端按 CP 成立时间倒序返回H5 只做口径校验并保留返回顺序,不再按金币额排成另一个榜单
activityData.plazaRows = items activityData.plazaRows = items
.map(normalizePlazaEntry) .map(normalizeFormationGiftEntry)
.filter(function (item) { .filter(function (item) {
return Boolean(item); return Boolean(item);
}) });
.sort(function (left, right) {
return right.rawScore - left.rawScore;
})
.slice(0, 4);
} }
function loadActivityStatus() { function loadActivityStatus() {
@ -575,23 +628,19 @@
.catch(function () {}); .catch(function () {});
} }
function loadPlazaLeaderboard() { function loadPlazaFormationGiftFeed() {
if ( if (
!window.HyAppAPI || !window.HyAppAPI ||
!window.HyAppAPI.cpSpace || !window.HyAppAPI.cpSpace ||
!window.HyAppAPI.cpSpace.intimacyLeaderboard !window.HyAppAPI.cpSpace.formationGiftFeed
) { ) {
return; return;
} }
// 后端亲密榜已按亲密值降序聚合H5 仍二次过滤 5000 门槛并限制 4 个,防止旧数据或异常分页把低亲密 CP 展示出来 // 20 条可让四行视窗持续轮播,同时限制头像 DOM 和图片解码数量gateway/user-service 还会再次夹紧上限,避免参数被篡改后无界拉取
window.HyAppAPI.cpSpace window.HyAppAPI.cpSpace
.intimacyLeaderboard({ .formationGiftFeed({ limit: PLAZA_FEED_LIMIT })
relation_type: 'cp',
page: 1,
page_size: 4,
})
.then(function (data) { .then(function (data) {
applyPlazaLeaderboard(data); applyPlazaFormationGiftFeed(data);
render(); render();
}) })
.catch(function () {}); .catch(function () {});
@ -668,7 +717,7 @@
countdownEndAt = resolveFallbackEndAt(); countdownEndAt = resolveFallbackEndAt();
bindTabs(); bindTabs();
render(); render();
loadPlazaLeaderboard(); loadPlazaFormationGiftFeed();
loadActivityStatus(); loadActivityStatus();
countdownTimer = window.setInterval(updateCountdown, 1000); countdownTimer = window.setInterval(updateCountdown, 1000);
window.addEventListener('resize', updateStageHeight); window.addEventListener('resize', updateStageHeight);

View File

@ -372,6 +372,38 @@ button {
top: 170px; top: 170px;
} }
.plaza-carousel-viewport {
position: absolute;
left: 60px;
top: 170px;
width: 840px;
height: 868px;
overflow: hidden;
}
.plaza-carousel-track {
width: 840px;
will-change: auto;
}
.plaza-carousel-track.is-looping {
animation: plaza-carousel-scroll var(--plaza-loop-duration) linear infinite;
will-change: transform;
}
.plaza-carousel-set {
display: grid;
row-gap: 24px;
width: 840px;
padding-bottom: 24px;
}
@keyframes plaza-carousel-scroll {
to {
transform: translateY(calc(-1 * var(--plaza-loop-distance)));
}
}
.panel-title { .panel-title {
position: absolute; position: absolute;
left: 55px; left: 55px;
@ -454,6 +486,7 @@ button {
text-align: center; text-align: center;
white-space: nowrap; white-space: nowrap;
text-overflow: ellipsis; text-overflow: ellipsis;
unicode-bidi: plaintext;
} }
.cp-row .name-left { .cp-row .name-left {
@ -492,15 +525,17 @@ button {
.score-value { .score-value {
position: absolute; position: absolute;
left: 359px; left: 308px;
top: 71px; top: 71px;
width: 120px; width: 222px;
height: 36px; height: 36px;
color: #fff7f7; color: #fff7f7;
font-size: 30px; font-size: 30px;
font-weight: 900; font-weight: 900;
line-height: 36px; line-height: 36px;
text-align: center; text-align: center;
white-space: nowrap;
font-variant-numeric: tabular-nums;
text-shadow: text-shadow:
0 2px 0 #a51f32, 0 2px 0 #a51f32,
0 0 8px rgba(255, 255, 255, 0.32); 0 0 8px rgba(255, 255, 255, 0.32);
@ -729,3 +764,11 @@ html[dir='rtl'] .reward-note {
html[dir='rtl'] .language-switcher { html[dir='rtl'] .language-switcher {
direction: ltr; direction: ltr;
} }
@media (prefers-reduced-motion: reduce) {
.plaza-carousel-track.is-looping {
animation: none;
transform: none;
will-change: auto;
}
}

View File

@ -1892,6 +1892,12 @@ var default_api = 'https://api.global-interaction.com/';
query: params || {}, query: params || {},
}); });
}, },
formationGiftFeed: function (params) {
return request('/api/v1/cp/formation-gift-feed', {
method: 'GET',
query: params || {},
});
},
intimacyLeaderboard: function (params) { intimacyLeaderboard: function (params) {
return request('/api/v1/cp/intimacy-leaderboard', { return request('/api/v1/cp/intimacy-leaderboard', {
method: 'GET', method: 'GET',

View File

@ -520,8 +520,8 @@
"cp.tabPlaza": "ساحة CP", "cp.tabPlaza": "ساحة CP",
"cp.tabRank": "ترتيب CP", "cp.tabRank": "ترتيب CP",
"cp.tabRewards": "المكافآت", "cp.tabRewards": "المكافآت",
"cp.plazaGiftTitle": "سيتم عرض أزواج CP التي لديها قيمة ألفة >= 5000 هنا", "cp.plazaGiftTitle": "تظهر هنا هدايا تكوين CP التي تزيد قيمتها عن 5,000 عملة",
"cp.plazaGiftSubtitle": "(عرض آخر 10 سجلات مرتبة تصاعديا.)", "cp.plazaGiftSubtitle": "تُعرض أحدث أزواج CP المؤهلة هنا باستمرار.",
"cp.previousNo1Title": "أفضل 3 في الفترة السابقة", "cp.previousNo1Title": "أفضل 3 في الفترة السابقة",
"cp.rewardMedal7": "وسام*7 أيام", "cp.rewardMedal7": "وسام*7 أيام",
"cp.rewardMedalNote": "(وسام دائم بعد الحصول عليه 3 مرات)", "cp.rewardMedalNote": "(وسام دائم بعد الحصول عليه 3 مرات)",

View File

@ -526,8 +526,8 @@
"cp.tabPlaza": "CP Plaza", "cp.tabPlaza": "CP Plaza",
"cp.tabRank": "CP Rank", "cp.tabRank": "CP Rank",
"cp.tabRewards": "Rewards", "cp.tabRewards": "Rewards",
"cp.plazaGiftTitle": "CP pairs with intimacy >= 5000 are shown here", "cp.plazaGiftTitle": "CP formation gifts worth more than 5,000 coins are shown here",
"cp.plazaGiftSubtitle": "(Display the last 10 records, sorted in ascending order.)", "cp.plazaGiftSubtitle": "The latest qualifying CP pairs scroll here continuously.",
"cp.previousNo1Title": "Previous Top 3", "cp.previousNo1Title": "Previous Top 3",
"cp.rewardMedal7": "Medal*7 days", "cp.rewardMedal7": "Medal*7 days",
"cp.rewardMedalNote": "(Permanent medal after getting 3 times)", "cp.rewardMedalNote": "(Permanent medal after getting 3 times)",

View File

@ -520,8 +520,8 @@
"cp.tabPlaza": "Plaza CP", "cp.tabPlaza": "Plaza CP",
"cp.tabRank": "Ranking CP", "cp.tabRank": "Ranking CP",
"cp.tabRewards": "Recompensas", "cp.tabRewards": "Recompensas",
"cp.plazaGiftTitle": "Los CP con intimidad >= 5000 se mostrarán aquí", "cp.plazaGiftTitle": "Aquí se muestran los regalos para formar CP que valen más de 5.000 monedas",
"cp.plazaGiftSubtitle": "(Muestra los últimos 10 registros en orden ascendente.)", "cp.plazaGiftSubtitle": "Las parejas CP elegibles más recientes se desplazan aquí continuamente.",
"cp.previousNo1Title": "Top 3 anterior", "cp.previousNo1Title": "Top 3 anterior",
"cp.rewardMedal7": "Medalla*7 días", "cp.rewardMedal7": "Medalla*7 días",
"cp.rewardMedalNote": "(Medalla permanente tras obtenerla 3 veces)", "cp.rewardMedalNote": "(Medalla permanente tras obtenerla 3 veces)",

View File

@ -487,8 +487,8 @@
"cp.tabPlaza": "CP Plaza", "cp.tabPlaza": "CP Plaza",
"cp.tabRank": "Peringkat CP", "cp.tabRank": "Peringkat CP",
"cp.tabRewards": "Hadiah", "cp.tabRewards": "Hadiah",
"cp.plazaGiftTitle": "CP dengan intimacy >= 5000 akan ditampilkan di sini", "cp.plazaGiftTitle": "Hadiah pembentukan CP bernilai lebih dari 5.000 koin ditampilkan di sini",
"cp.plazaGiftSubtitle": "(Menampilkan 10 catatan terbaru, diurutkan naik.)", "cp.plazaGiftSubtitle": "Pasangan CP terbaru yang memenuhi syarat bergulir terus di sini.",
"cp.previousNo1Title": "Top 3 periode sebelumnya", "cp.previousNo1Title": "Top 3 periode sebelumnya",
"cp.rewardMedal7": "Medali*7 hari", "cp.rewardMedal7": "Medali*7 hari",
"cp.rewardMedalNote": "(Medali permanen setelah mendapatkannya 3 kali)", "cp.rewardMedalNote": "(Medali permanen setelah mendapatkannya 3 kali)",

View File

@ -487,8 +487,8 @@
"cp.tabPlaza": "CP Plaza", "cp.tabPlaza": "CP Plaza",
"cp.tabRank": "CP Sıralaması", "cp.tabRank": "CP Sıralaması",
"cp.tabRewards": "Ödüller", "cp.tabRewards": "Ödüller",
"cp.plazaGiftTitle": "Samimiyet değeri >= 5000 olan CP çiftleri burada görünür", "cp.plazaGiftTitle": "5.000 coinden daha değerli CP kurma hediyeleri burada gösterilir",
"cp.plazaGiftSubtitle": "(Son 10 kayıt artan sırayla gösterilir.)", "cp.plazaGiftSubtitle": "Koşulları karşılayan en yeni CP çiftleri burada sürekli kayar.",
"cp.previousNo1Title": "Önceki Top 3", "cp.previousNo1Title": "Önceki Top 3",
"cp.rewardMedal7": "Madalya*7 gün", "cp.rewardMedal7": "Madalya*7 gün",
"cp.rewardMedalNote": "(3 kez alındıktan sonra kalıcı madalya)", "cp.rewardMedalNote": "(3 kez alındıktan sonra kalıcı madalya)",

View File

@ -493,8 +493,8 @@
"cp.tabPlaza": "CP 广场", "cp.tabPlaza": "CP 广场",
"cp.tabRank": "CP 排行", "cp.tabRank": "CP 排行",
"cp.tabRewards": "奖励", "cp.tabRewards": "奖励",
"cp.plazaGiftTitle": "CP 之间亲密值 >= 5000 的 CP 会展示在这里", "cp.plazaGiftTitle": "组 CP 时赠送价值超过 5000 金币的礼物会展示在这里",
"cp.plazaGiftSubtitle": "(展示最近 10 条记录,按时间升序排列。)", "cp.plazaGiftSubtitle": "最近符合条件的 CP 组合会在这里持续轮播。",
"cp.previousNo1Title": "上期前三名", "cp.previousNo1Title": "上期前三名",
"cp.rewardMedal7": "勋章*7天", "cp.rewardMedal7": "勋章*7天",
"cp.rewardMedalNote": "(获得 3 次后永久)", "cp.rewardMedalNote": "(获得 3 次后永久)",