This commit is contained in:
zhx 2026-06-08 19:16:41 +08:00
parent a006d26a50
commit 2d84b43109
38 changed files with 3780 additions and 111 deletions

Binary file not shown.

After

Width:  |  Height:  |  Size: 225 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 107 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 109 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 104 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 113 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 147 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 313 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 230 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 60 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 31 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 979 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 167 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 183 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 127 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 129 KiB

109
activity/cp/index.html Normal file
View File

@ -0,0 +1,109 @@
<!doctype html>
<html lang="en">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Last Weekly CP</title>
<link rel="stylesheet" href="../../common/theme.css?v=20260608-cp" />
<link rel="stylesheet" href="./style.css?v=20260608-cp" />
</head>
<body>
<div class="app-viewport" id="appViewport">
<main class="page" id="page" data-tab="plaza">
<div class="stage-wrap" id="stageWrap">
<section
class="stage"
aria-label="Last Weekly CP activity"
data-i18n-aria="cp.pageLabel"
>
<div class="language-switcher">
<button
class="language-button"
type="button"
aria-label="Change language"
aria-expanded="false"
data-language-toggle
data-current-lang
data-i18n-aria="cp.changeLanguage"
>
EN
</button>
<div class="language-menu" data-language-menu hidden>
<button type="button" data-lang-option="en">EN</button>
<button type="button" data-lang-option="ar">AR</button>
<button type="button" data-lang-option="tr">TR</button>
<button type="button" data-lang-option="es">ES</button>
<button type="button" data-lang-option="zh">ZH</button>
<button type="button" data-lang-option="id">ID</button>
</div>
</div>
<img class="hero-bg" src="./assets/hero-bg.png" alt="" aria-hidden="true" />
<section class="countdown" aria-label="Activity countdown">
<div class="time-unit">
<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />
<strong data-count="days">02</strong>
<span data-i18n="cp.days">D</span>
</div>
<div class="time-unit">
<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />
<strong data-count="hours">02</strong>
<span data-i18n="cp.hours">H</span>
</div>
<div class="time-unit">
<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />
<strong data-count="minutes">02</strong>
<span data-i18n="cp.minutes">M</span>
</div>
<div class="time-unit">
<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />
<strong data-count="seconds">02</strong>
<span data-i18n="cp.seconds">S</span>
</div>
</section>
<nav class="tabs" aria-label="CP activity tabs" data-i18n-aria="cp.tabsLabel">
<button class="tab is-active" type="button" data-tab="plaza">
<img src="./assets/tab-active.png" alt="" aria-hidden="true" />
<span data-i18n="cp.tabPlaza">CP Plaza</span>
</button>
<button class="tab" type="button" data-tab="rank">
<img src="./assets/tab-inactive.png" alt="" aria-hidden="true" />
<span data-i18n="cp.tabRank">CP Rank</span>
</button>
<button class="tab" type="button" data-tab="rewards">
<img src="./assets/tab-inactive.png" alt="" aria-hidden="true" />
<span data-i18n="cp.tabRewards">Rewards</span>
</button>
</nav>
<div class="tab-content" id="tabContent"></div>
<footer class="copyright" data-i18n="cp.copyright">
The copyright of this activity belongs to Shila
</footer>
</section>
</div>
</main>
</div>
<script>
(function () {
try {
var params = new URLSearchParams(window.location.search);
if (!params.get('lang') && !window.localStorage.getItem('cp_activity_lang_ready')) {
window.localStorage.setItem('hyapp_h5_lang', 'en');
window.localStorage.setItem('cp_activity_lang_ready', '1');
}
} catch (error) {
document.documentElement.lang = 'en';
}
})();
</script>
<script src="../../common/i18n.js?v=20260608-cp"></script>
<script src="./script.js?v=20260608-cp"></script>
</body>
</html>

370
activity/cp/script.js Normal file
View File

@ -0,0 +1,370 @@
(function () {
var DESIGN_WIDTH = 1080;
var VIEWPORT_WIDTH = 375;
var STORAGE_KEY = 'hy_cp_activity_end_at';
var stageHeights = {
plaza: 4283,
rank: 3670,
rewards: 4797,
};
var activeTab = 'plaza';
var countdownEndAt = 0;
var countdownTimer = 0;
var viewport = document.getElementById('appViewport');
var page = document.getElementById('page');
var stage = document.querySelector('.stage');
var stageWrap = document.getElementById('stageWrap');
var tabContent = document.getElementById('tabContent');
var copyright = document.querySelector('.copyright');
var tabs = Array.prototype.slice.call(document.querySelectorAll('.tab'));
var mockData = {
plaza: {
exchangeRows: buildRows('gift', 4),
previousRows: buildRows('score', 4),
},
rank: [
{ place: 1, score: '1,234,567', leftName: 'Namename', rightName: 'Namename' },
{ place: 2, score: '987,654', leftName: 'Namename', rightName: 'Namename' },
{ place: 3, score: '876,543', leftName: 'Namename', rightName: 'Namename' },
],
rewards: [
{
crown: 'crown-gold.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardHeadwear7' },
{ key: 'cp.rewardMount7' },
{ key: 'cp.rewardCoins20000' },
{ key: 'cp.rewardDuke7' },
],
},
{
crown: 'crown-silver.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardMount7' },
{ key: 'cp.rewardCoins20000' },
{ key: 'cp.rewardMarquis7' },
],
},
{
crown: 'crown-bronze.png',
items: [
{ key: 'cp.rewardMedal7', noteKey: 'cp.rewardMedalNote' },
{ key: 'cp.rewardHonorBanner7' },
{ key: 'cp.rewardCoins10000' },
{ key: 'cp.rewardCount7' },
],
},
],
};
function buildRows(type, count) {
var rows = [];
for (var index = 0; index < count; index += 1) {
rows.push({
type: type,
leftName: 'Namename',
rightName: 'Namename',
score: '1234567',
});
}
return rows;
}
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function twoDigits(value) {
return String(Math.max(0, value)).padStart(2, '0');
}
function resolveFallbackEndAt() {
var params = new URLSearchParams(window.location.search || '');
var explicitEnd = Date.parse(params.get('end_at') || params.get('endAt') || '');
if (Number.isFinite(explicitEnd) && explicitEnd > Date.now()) return explicitEnd;
try {
var cached = Number(window.localStorage.getItem(STORAGE_KEY));
if (Number.isFinite(cached) && cached > Date.now()) return cached;
var fallback = Date.now() + 2 * 86400 * 1000 + 2 * 3600 * 1000 + 2 * 60 * 1000 + 2 * 1000;
window.localStorage.setItem(STORAGE_KEY, String(fallback));
return fallback;
} catch (error) {
return Date.now() + 2 * 86400 * 1000 + 2 * 3600 * 1000 + 2 * 60 * 1000 + 2 * 1000;
}
}
function getTimeParts() {
var diff = Math.max(0, countdownEndAt - Date.now());
var totalSeconds = Math.floor(diff / 1000);
return {
days: Math.floor(totalSeconds / 86400),
hours: Math.floor((totalSeconds % 86400) / 3600),
minutes: Math.floor((totalSeconds % 3600) / 60),
seconds: totalSeconds % 60,
};
}
function updateCountdown() {
var parts = getTimeParts();
Object.keys(parts).forEach(function (key) {
document.querySelectorAll('[data-count="' + key + '"]').forEach(function (node) {
node.textContent = twoDigits(parts[key]);
});
});
}
function renderPanel(className, top, height, bodyHTML) {
return [
'<section class="ornate-panel ',
className,
'" style="top:',
top,
'px;height:',
height,
'px">',
'<img class="panel-top" src="./assets/panel-top.png" alt="" aria-hidden="true" />',
'<div class="panel-mid" aria-hidden="true"></div>',
'<img class="panel-bottom" src="./assets/panel-bottom.png" alt="" aria-hidden="true" />',
'<div class="panel-body">',
bodyHTML,
'</div>',
'</section>',
].join('');
}
function renderCPRow(row) {
var center = row.type === 'score'
? [
'<img class="score-heart" src="./assets/score-heart.png" alt="" aria-hidden="true" />',
'<strong class="score-value">',
escapeHTML(row.score),
'</strong>',
].join('')
: [
'<div class="gift-center">',
'<img src="./assets/gift-plane.png" alt="" aria-hidden="true" />',
'</div>',
].join('');
return [
'<article class="cp-row">',
'<div class="avatar avatar-left"><img src="./assets/avatar-sample.png" alt="" /></div>',
'<div class="avatar avatar-right"><img src="./assets/avatar-sample.png" alt="" /></div>',
'<span class="name name-left">',
escapeHTML(row.leftName),
'</span>',
'<span class="name name-right">',
escapeHTML(row.rightName),
'</span>',
'<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" />',
center,
'</article>',
].join('');
}
function renderPlaza() {
var firstBody = [
'<h2 class="panel-title">',
t('cp.plazaGiftTitle', 'Gifts exchanged between CPs ≥ 5000 gold will be displayed here'),
'</h2>',
'<p class="panel-subtitle">',
t('cp.plazaGiftSubtitle', '(Display the last 10 records, sorted in ascending order.)'),
'</p>',
'<div class="cp-list">',
mockData.plaza.exchangeRows.map(renderCPRow).join(''),
'</div>',
].join('');
var secondBody = [
'<h2 class="panel-title">',
t('cp.previousNo1Title', 'The previous number one position'),
'</h2>',
'<p class="panel-subtitle">',
t('cp.plazaGiftSubtitle', '(Display the last 10 records, sorted in ascending order.)'),
'</p>',
'<div class="cp-list">',
mockData.plaza.previousRows.map(renderCPRow).join(''),
'</div>',
].join('');
return [
renderPanel('plaza-panel-a', 1816, 1152, firstBody),
renderPanel('plaza-panel-b', 3024, 1126, secondBody),
].join('');
}
function renderInlineCountdown() {
var units = [
['days', 'cp.days', 'D'],
['hours', 'cp.hours', 'H'],
['minutes', 'cp.minutes', 'M'],
['seconds', 'cp.seconds', 'S'],
];
return [
'<div class="countdown-inline">',
units
.map(function (unit) {
return [
'<div class="time-unit">',
'<img src="./assets/countdown-heart.png" alt="" aria-hidden="true" />',
'<strong data-count="',
unit[0],
'">02</strong>',
'<span>',
t(unit[1], unit[2]),
'</span>',
'</div>',
].join('');
})
.join(''),
'</div>',
].join('');
}
function renderPodiumCard(item) {
var crown = item.place === 1 ? 'rank-crown-gold.png' : item.place === 2 ? 'rank-crown-silver.png' : 'crown-bronze.png';
return [
'<article class="podium-card rank-',
item.place,
'">',
'<img class="podium-bg" src="./assets/rank-wing-heart.png" alt="" aria-hidden="true" />',
'<img class="podium-crown" src="./assets/',
crown,
'" alt="" aria-hidden="true" />',
'<div class="avatar podium-avatar left"><img src="./assets/avatar-sample.png" alt="" /></div>',
'<div class="avatar podium-avatar right"><img src="./assets/avatar-sample.png" alt="" /></div>',
'<div class="podium-names"><span>',
escapeHTML(item.leftName),
'</span><span>',
escapeHTML(item.rightName),
'</span></div>',
'</article>',
].join('');
}
function renderRankRow(item) {
return renderCPRow({
type: 'score',
leftName: item.leftName,
rightName: item.rightName,
score: item.score,
});
}
function renderRank() {
var podium = [
'<section class="rank-podium">',
mockData.rank.map(renderPodiumCard).join(''),
'</section>',
].join('');
var panelBody = [renderInlineCountdown(), '<div class="cp-list">', mockData.rank.concat(mockData.rank.slice(0, 1)).map(renderRankRow).join(''), '</div>'].join('');
return podium + renderPanel('rank-panel', 2430, 1107, panelBody);
}
function renderRewardItem(item) {
return [
'<article class="reward-card">',
'<div class="reward-icon-box"><img src="./assets/reward-frame.png" alt="" aria-hidden="true" /></div>',
'<div class="reward-name">',
t(item.key, item.key),
'</div>',
item.noteKey ? '<div class="reward-note">' + t(item.noteKey, '') + '</div>' : '',
'</article>',
].join('');
}
function renderRewardPanel(section, index) {
var className = ['reward-panel', 'reward-' + ['a', 'b', 'c'][index]].join(' ');
var top = [1816, 2784, 3752][index];
var body = [
'<img class="reward-crown" src="./assets/',
section.crown,
'" alt="" aria-hidden="true" />',
'<div class="reward-grid">',
section.items.map(renderRewardItem).join(''),
'</div>',
].join('');
return renderPanel(className, top, 912, body);
}
function renderRewards() {
return mockData.rewards.map(renderRewardPanel).join('');
}
function render() {
document.title = t('cp.pageTitle', 'Last Weekly CP');
tabContent.innerHTML = activeTab === 'rank' ? renderRank() : activeTab === 'rewards' ? renderRewards() : renderPlaza();
applyTabState();
updateStageHeight();
updateCountdown();
}
function applyTabState() {
tabs.forEach(function (tab) {
var isActive = tab.getAttribute('data-tab') === activeTab;
tab.classList.toggle('is-active', isActive);
tab.querySelector('img').src = isActive ? './assets/tab-active.png' : './assets/tab-inactive.png';
});
page.setAttribute('data-tab', activeTab);
}
function updateStageHeight() {
var designHeight = stageHeights[activeTab] || stageHeights.plaza;
stage.style.minHeight = designHeight + 'px';
stage.style.height = designHeight + 'px';
stageWrap.style.height = Math.ceil(designHeight * (VIEWPORT_WIDTH / DESIGN_WIDTH)) + 'px';
copyright.style.top = designHeight - 77 + 'px';
var scale = Math.min(window.innerWidth / VIEWPORT_WIDTH, 1);
document.documentElement.style.setProperty('--app-scale', String(scale));
document.body.style.minHeight = Math.ceil(stageWrap.offsetHeight * scale) + 'px';
viewport.style.minHeight = stageWrap.style.height;
page.style.minHeight = stageWrap.style.height;
}
function bindTabs() {
tabs.forEach(function (tab) {
tab.addEventListener('click', function () {
activeTab = tab.getAttribute('data-tab') || 'plaza';
render();
});
});
}
function init() {
countdownEndAt = resolveFallbackEndAt();
bindTabs();
render();
countdownTimer = window.setInterval(updateCountdown, 1000);
window.addEventListener('resize', updateStageHeight);
window.addEventListener('hyapp:i18n-ready', render);
}
window.addEventListener('beforeunload', function () {
if (countdownTimer) window.clearInterval(countdownTimer);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

707
activity/cp/style.css Normal file
View File

@ -0,0 +1,707 @@
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--app-scale: 1;
--design-scale: 0.3472222222;
--cp-red: #c52d45;
--cp-dark-red: #8c2130;
--cp-panel-text: #a8302b;
--cp-panel-sub: #d26984;
--cp-cream: #fff2f3;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: #1d080d;
color: #fff;
font-family:
"Source Han Sans SC",
"Noto Sans CJK SC",
Arial,
"Helvetica Neue",
Helvetica,
sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
outline: none;
}
.app-viewport {
position: relative;
width: 375px;
min-height: 1300px;
margin: 0 auto;
transform: scale(var(--app-scale));
transform-origin: top center;
}
.page {
position: relative;
width: 375px;
min-height: 1300px;
overflow: hidden;
background: #1d080d;
}
.stage-wrap {
position: relative;
width: 375px;
height: 1487px;
overflow: hidden;
background: var(--cp-red);
}
.stage {
position: absolute;
left: 0;
top: 0;
width: 1080px;
min-height: 4283px;
overflow: hidden;
transform: scale(var(--design-scale));
transform-origin: top left;
background: var(--cp-red);
}
.stage > * {
position: absolute;
}
.hero-bg {
left: 0;
top: 0;
z-index: 0;
width: 1080px;
height: 1919px;
object-fit: cover;
pointer-events: none;
}
.language-switcher {
z-index: 20;
top: 38px;
right: 36px;
width: 188px;
direction: ltr;
}
.language-button {
display: flex;
align-items: center;
justify-content: center;
width: 150px;
height: 78px;
margin-left: auto;
border: 2px solid rgba(255, 236, 238, 0.72);
border-radius: 999px;
background: rgba(152, 30, 51, 0.52);
box-shadow:
inset 0 0 16px rgba(255, 210, 220, 0.3),
0 10px 20px rgba(78, 4, 16, 0.28);
color: #fff;
font-size: 32px;
font-weight: 900;
line-height: 32px;
letter-spacing: 0;
}
.language-menu {
position: absolute;
top: 92px;
right: 0;
z-index: 22;
width: 188px;
overflow: hidden;
border: 2px solid rgba(255, 236, 238, 0.7);
border-radius: 18px;
background: rgba(121, 14, 33, 0.94);
box-shadow: 0 18px 32px rgba(87, 1, 17, 0.36);
}
.language-menu button {
display: block;
width: 100%;
height: 58px;
color: #fff0f3;
font-size: 28px;
font-weight: 800;
}
.language-menu button.is-active {
background: rgba(255, 255, 255, 0.2);
color: #ffe184;
}
.countdown {
left: 48px;
top: 1521px;
z-index: 2;
width: 974px;
height: 106px;
}
.time-unit {
position: relative;
width: 166px;
height: 106px;
}
.countdown > .time-unit {
position: absolute;
top: 0;
}
.countdown > .time-unit:nth-child(1) {
left: 0;
}
.countdown > .time-unit:nth-child(2) {
left: 250px;
}
.countdown > .time-unit:nth-child(3) {
left: 501px;
}
.countdown > .time-unit:nth-child(4) {
left: 756px;
}
.time-unit img {
position: absolute;
inset: 0;
width: 166px;
height: 106px;
object-fit: fill;
}
.time-unit strong {
position: absolute;
left: 0;
top: 23px;
width: 166px;
color: #fff;
font-size: 40px;
font-weight: 900;
line-height: 42px;
text-align: center;
text-shadow:
0 2px 0 rgba(119, 16, 30, 0.6),
0 0 12px rgba(255, 255, 255, 0.46);
}
.time-unit span {
position: absolute;
left: 193px;
top: 25px;
width: 42px;
color: #fff;
font-size: 36px;
font-weight: 900;
line-height: 40px;
text-align: center;
text-shadow: 0 2px 0 rgba(116, 9, 27, 0.62);
}
.tabs {
left: 40px;
top: 1683px;
z-index: 4;
display: grid;
grid-template-columns: repeat(3, 303px);
column-gap: 46px;
width: 1000px;
height: 93px;
direction: ltr;
}
.tab {
position: relative;
width: 303px;
height: 93px;
color: rgba(255, 255, 255, 0.62);
font-size: 40px;
font-weight: 900;
line-height: 44px;
text-align: center;
text-shadow: 0 2px 0 rgba(105, 30, 36, 0.55);
}
.tab img {
position: absolute;
inset: 0;
width: 303px;
height: 93px;
object-fit: fill;
}
.tab span {
position: absolute;
left: 0;
top: 25px;
width: 303px;
overflow: hidden;
white-space: nowrap;
unicode-bidi: plaintext;
}
.tab.is-active {
color: #fff;
}
.tab-content {
left: 0;
top: 0;
z-index: 3;
width: 1080px;
}
.copyright {
left: 114px;
z-index: 3;
width: 850px;
height: 30px;
color: #fff;
font-size: 24px;
font-weight: 600;
line-height: 30px;
text-align: center;
}
.ornate-panel {
position: absolute;
left: 60px;
width: 960px;
overflow: hidden;
border: 8px solid #f62658;
border-radius: 48px;
background:
radial-gradient(circle at 50% 0%, rgba(255, 255, 255, 0.98) 0, rgba(255, 255, 255, 0.86) 23%, rgba(255, 232, 236, 0.92) 100%),
linear-gradient(180deg, #fff8f8 0%, #ffe6eb 100%);
box-shadow:
inset 0 0 0 4px rgba(255, 204, 119, 0.95),
inset 0 0 0 12px rgba(255, 255, 255, 0.82),
inset 0 0 24px rgba(255, 75, 113, 0.22),
0 0 0 2px rgba(255, 237, 180, 0.92),
0 0 16px rgba(255, 34, 87, 0.45);
color: var(--cp-panel-text);
}
.ornate-panel::after {
position: absolute;
content: "";
pointer-events: none;
}
.ornate-panel::after {
left: 70px;
right: 70px;
top: 0;
z-index: 1;
height: 14px;
background: linear-gradient(90deg, transparent, rgba(255, 241, 177, 0.95), transparent);
filter: drop-shadow(0 0 8px rgba(255, 255, 255, 0.8));
}
.panel-top,
.panel-bottom,
.panel-mid {
display: none;
}
.panel-top {
top: 0;
height: 90px;
}
.panel-bottom {
bottom: 0;
height: 86px;
}
.panel-mid {
top: 89px;
bottom: 85px;
background-image: url("./assets/panel-mid.png");
background-size: 960px 184px;
background-repeat: repeat-y;
}
.panel-body {
position: absolute;
inset: 0;
z-index: 2;
}
.plaza-panel-a {
top: 1816px;
height: 1152px;
}
.plaza-panel-b {
top: 3024px;
height: 1126px;
}
.panel-title {
position: absolute;
left: 55px;
top: 70px;
width: 850px;
margin: 0;
color: var(--cp-panel-text);
font-size: 36px;
font-weight: 800;
line-height: 46px;
text-align: center;
}
.panel-subtitle {
position: absolute;
left: 55px;
top: 161px;
width: 850px;
margin: 0;
color: var(--cp-panel-sub);
font-size: 24px;
font-weight: 700;
line-height: 30px;
text-align: center;
}
.cp-list {
position: absolute;
left: 60px;
top: 224px;
display: grid;
row-gap: 24px;
width: 840px;
}
.cp-row {
position: relative;
width: 840px;
height: 199px;
border-radius: 24px;
background: rgba(197, 45, 69, 0.12);
}
.avatar {
position: absolute;
overflow: hidden;
border-radius: 50%;
background: #ffd6df;
}
.avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.cp-row .avatar {
top: 28px;
width: 113px;
height: 113px;
}
.cp-row .avatar-left {
left: 42px;
}
.cp-row .avatar-right {
right: 42px;
}
.cp-row .name {
position: absolute;
top: 146px;
width: 144px;
overflow: hidden;
color: #9f1c32;
font-size: 26px;
font-weight: 800;
line-height: 30px;
text-align: center;
white-space: nowrap;
text-overflow: ellipsis;
}
.cp-row .name-left {
left: 42px;
}
.cp-row .name-right {
right: 42px;
}
.gift-center {
position: absolute;
left: 344px;
top: 15px;
display: flex;
align-items: center;
justify-content: center;
width: 144px;
height: 144px;
}
.gift-center img {
max-width: 152px;
max-height: 152px;
object-fit: contain;
}
.score-heart {
position: absolute;
left: 308px;
top: 28px;
width: 222px;
height: 143px;
object-fit: contain;
}
.score-value {
position: absolute;
left: 359px;
top: 71px;
width: 120px;
height: 36px;
color: #fff7f7;
font-size: 30px;
font-weight: 900;
line-height: 36px;
text-align: center;
text-shadow:
0 2px 0 #a51f32,
0 0 8px rgba(255, 255, 255, 0.32);
}
.deco-line {
position: absolute;
top: 70px;
width: 159px;
height: 30px;
object-fit: fill;
opacity: 0.9;
}
.deco-left {
left: 252px;
}
.deco-right {
right: 252px;
transform: scaleX(-1);
}
.rank-podium {
position: absolute;
left: 0;
top: 1816px;
width: 1080px;
height: 580px;
}
.podium-card {
position: absolute;
width: 716px;
height: 288px;
}
.podium-card.rank-1 {
left: 182px;
top: 14px;
}
.podium-card.rank-2 {
left: -34px;
top: 332px;
transform: scale(0.77);
transform-origin: top left;
}
.podium-card.rank-3 {
left: 565px;
top: 332px;
transform: scale(0.77);
transform-origin: top left;
}
.podium-bg {
position: absolute;
inset: 0;
width: 716px;
height: 288px;
object-fit: contain;
}
.podium-crown {
position: absolute;
left: 310px;
top: -8px;
width: 120px;
height: 106px;
object-fit: contain;
}
.podium-card.rank-2 .podium-crown,
.podium-card.rank-3 .podium-crown {
top: -2px;
width: 104px;
height: 90px;
}
.podium-avatar {
position: absolute;
top: 92px;
width: 104px;
height: 104px;
border: 5px solid #ffd5a3;
box-shadow: 0 0 10px rgba(255, 255, 255, 0.42);
}
.podium-avatar.left {
left: 236px;
}
.podium-avatar.right {
left: 386px;
}
.podium-names {
position: absolute;
left: 146px;
top: 214px;
display: grid;
grid-template-columns: repeat(2, 168px);
column-gap: 64px;
width: 400px;
color: #fff3f5;
font-size: 28px;
font-weight: 800;
line-height: 32px;
text-align: center;
text-shadow: 0 2px 0 rgba(126, 15, 31, 0.62);
}
.rank-panel {
top: 2430px;
height: 1107px;
}
.rank-panel .countdown-inline {
position: absolute;
left: 110px;
top: 64px;
display: grid;
grid-template-columns: repeat(4, 166px);
column-gap: 44px;
width: 830px;
}
.rank-panel .cp-list {
top: 252px;
}
.reward-panel {
height: 912px;
}
.reward-panel.reward-a {
top: 1816px;
}
.reward-panel.reward-b {
top: 2784px;
}
.reward-panel.reward-c {
top: 3752px;
}
.reward-crown {
position: absolute;
left: 390px;
top: 58px;
width: 180px;
height: 158px;
object-fit: contain;
}
.reward-grid {
position: absolute;
left: 106px;
top: 220px;
display: grid;
grid-template-columns: repeat(3, 217px);
gap: 24px 84px;
width: 750px;
}
.reward-card {
position: relative;
width: 217px;
min-height: 264px;
color: #9f1c32;
text-align: center;
}
.reward-icon-box {
position: relative;
width: 217px;
height: 217px;
border-radius: 24px;
background: rgba(197, 45, 69, 0.12);
}
.reward-card img {
position: absolute;
left: 9px;
top: 9px;
width: 200px;
height: 200px;
object-fit: contain;
}
.reward-name {
margin-top: 10px;
color: #9f1c32;
font-size: 24px;
font-weight: 800;
line-height: 28px;
}
.reward-note {
color: var(--cp-panel-sub);
font-size: 19px;
font-weight: 700;
line-height: 22px;
}
html[dir="rtl"] .tab span,
html[dir="rtl"] .panel-title,
html[dir="rtl"] .panel-subtitle,
html[dir="rtl"] .copyright,
html[dir="rtl"] .reward-name,
html[dir="rtl"] .reward-note {
direction: rtl;
}
html[dir="rtl"] .language-switcher {
direction: ltr;
}

View File

@ -106,50 +106,50 @@
<div class="table-content" id="tierTable"></div>
</section>
<section
class="user-panel"
aria-label="My room reward progress"
data-i18n-aria="roomReward.progressLabel"
>
<img class="user-avatar" id="userAvatar" src="./assets/294_8091.png" alt="" />
<div class="user-name" id="userName">Namename...</div>
<div class="user-id" id="userId">ID: 123456789</div>
<div class="metric-label contribution-label" data-i18n="roomReward.contribution">
This week's contribution:
</div>
<div class="metric-label rewards-label" data-i18n="roomReward.rewards">
This week's rewards:
</div>
<button
class="reward-button is-active"
id="contributionButton"
type="button"
aria-label="Contribution reward"
data-i18n-aria="roomReward.contributionReward"
data-active-src="./assets/296_8424.png"
data-inactive-src="./assets/296_8429.png"
>
<img src="./assets/296_8424.png" alt="" aria-hidden="true" />
<span id="contributionValue">100</span>
</button>
<button
class="reward-button is-disabled"
id="rewardsButton"
type="button"
aria-label="Weekly rewards"
data-i18n-aria="roomReward.weeklyRewards"
disabled
data-active-src="./assets/296_8424.png"
data-inactive-src="./assets/296_8429.png"
>
<img src="./assets/296_8429.png" alt="" aria-hidden="true" />
<span id="weeklyRewardValue">100</span>
</button>
</section>
</section>
</div>
</main>
</div>
<section
class="user-panel"
aria-label="My room reward progress"
data-i18n-aria="roomReward.progressLabel"
>
<img class="user-avatar" id="userAvatar" src="./assets/294_8091.png" alt="" />
<div class="user-name" id="userName">Namename...</div>
<div class="user-id" id="userId">ID: 123456789</div>
<div class="metric-label contribution-label" data-i18n="roomReward.contribution">
This week's contribution:
</div>
<div class="metric-label rewards-label" data-i18n="roomReward.rewards">
This week's rewards:
</div>
<button
class="reward-button is-active"
id="contributionButton"
type="button"
aria-label="Contribution reward"
data-i18n-aria="roomReward.contributionReward"
data-active-src="./assets/296_8424.png"
data-inactive-src="./assets/296_8429.png"
>
<img src="./assets/296_8424.png" alt="" aria-hidden="true" />
<span id="contributionValue">100</span>
</button>
<button
class="reward-button is-disabled"
id="rewardsButton"
type="button"
aria-label="Weekly rewards"
data-i18n-aria="roomReward.weeklyRewards"
disabled
data-active-src="./assets/296_8424.png"
data-inactive-src="./assets/296_8429.png"
>
<img src="./assets/296_8429.png" alt="" aria-hidden="true" />
<span id="weeklyRewardValue">100</span>
</button>
</section>
<script>
(function () {

View File

@ -120,9 +120,10 @@
}
function updateBodyHeight() {
var scale = Math.min(window.innerWidth / 375, 1);
var scale = window.innerWidth / 375;
document.documentElement.style.setProperty('--app-scale', String(scale));
document.body.style.minHeight = viewport.offsetHeight * scale + 'px';
document.body.style.minHeight =
Math.ceil(viewport.offsetHeight * scale) + 'px';
}
function readQuery(name) {

View File

@ -14,6 +14,7 @@ body {
width: 100%;
min-height: 100%;
overflow-x: hidden;
overflow-y: auto;
background: #291000;
color: #fff;
font-family:
@ -21,6 +22,7 @@ body {
"Helvetica Neue",
Helvetica,
sans-serif;
padding-bottom: calc((169 / 1080) * 100vw + env(safe-area-inset-bottom, 0px));
}
button {
@ -49,15 +51,15 @@ button {
position: relative;
width: 375px;
min-height: 772px;
overflow: hidden;
overflow: visible;
background: #291000;
}
.stage-wrap {
position: relative;
width: 375px;
height: 772px;
overflow: hidden;
height: 830px;
overflow: visible;
}
.stage {
@ -65,7 +67,7 @@ button {
left: 0;
top: 0;
width: 1080px;
height: 2221px;
height: 2388px;
overflow: hidden;
transform: scale(var(--design-scale));
transform-origin: top left;
@ -297,34 +299,37 @@ button {
}
.user-panel {
position: absolute;
position: fixed;
z-index: 80;
left: 0;
top: 2052px;
width: 1080px;
height: 169px;
right: 0;
bottom: 0;
width: 100vw;
height: calc((169 / 1080) * 100vw + env(safe-area-inset-bottom, 0px));
background: #190901;
box-shadow: 0 -8px 24px rgba(0, 0, 0, 0.32);
}
.user-avatar {
position: absolute;
left: 46px;
top: 25px;
width: 119px;
height: 119px;
left: calc((46 / 1080) * 100vw);
top: calc((25 / 1080) * 100vw);
width: calc((119 / 1080) * 100vw);
height: calc((119 / 1080) * 100vw);
object-fit: cover;
border-radius: 50%;
}
.user-name {
position: absolute;
left: 189px;
top: 36px;
width: 280px;
left: calc((189 / 1080) * 100vw);
top: calc((36 / 1080) * 100vw);
width: calc((280 / 1080) * 100vw);
overflow: hidden;
color: #fff;
font-size: 40px;
font-size: calc((40 / 1080) * 100vw);
font-weight: 500;
line-height: 42px;
line-height: calc((42 / 1080) * 100vw);
letter-spacing: 0;
white-space: nowrap;
text-overflow: ellipsis;
@ -332,14 +337,14 @@ button {
.user-id {
position: absolute;
left: 189px;
top: 102px;
width: 292px;
left: calc((189 / 1080) * 100vw);
top: calc((102 / 1080) * 100vw);
width: calc((292 / 1080) * 100vw);
overflow: hidden;
color: rgba(255, 255, 255, 0.7);
font-size: 30px;
font-size: calc((30 / 1080) * 100vw);
font-weight: 400;
line-height: 36px;
line-height: calc((36 / 1080) * 100vw);
letter-spacing: 0;
white-space: nowrap;
text-overflow: ellipsis;
@ -349,30 +354,30 @@ button {
position: absolute;
overflow: hidden;
color: #fff;
font-size: 28px;
font-size: calc((28 / 1080) * 100vw);
font-weight: 400;
line-height: 32px;
line-height: calc((32 / 1080) * 100vw);
letter-spacing: 0;
white-space: nowrap;
text-overflow: ellipsis;
}
.contribution-label {
left: 494px;
top: 43px;
width: 328px;
left: calc((494 / 1080) * 100vw);
top: calc((43 / 1080) * 100vw);
width: calc((328 / 1080) * 100vw);
}
.rewards-label {
left: 552px;
top: 111px;
width: 270px;
left: calc((552 / 1080) * 100vw);
top: calc((111 / 1080) * 100vw);
width: calc((270 / 1080) * 100vw);
}
.reward-button {
position: absolute;
width: 213px;
height: 69px;
width: calc((213 / 1080) * 100vw);
height: calc((69 / 1080) * 100vw);
cursor: pointer;
}
@ -394,21 +399,21 @@ button {
width: 100%;
height: 100%;
color: #fff;
font-size: 24px;
font-size: calc((24 / 1080) * 100vw);
font-weight: 500;
line-height: 24px;
line-height: calc((24 / 1080) * 100vw);
letter-spacing: 0;
text-align: center;
}
#contributionButton {
left: 838px;
top: 19px;
left: calc((838 / 1080) * 100vw);
top: calc((19 / 1080) * 100vw);
}
#rewardsButton {
left: 837px;
top: 87px;
left: calc((837 / 1080) * 100vw);
top: calc((87 / 1080) * 100vw);
}
.reward-button.is-disabled {

View File

@ -1,6 +1,5 @@
(function () {
var page = document.getElementById('page');
var viewport = document.getElementById('appViewport');
var tabs = Array.prototype.slice.call(document.querySelectorAll('[data-tab-target]'));
var panels = Array.prototype.slice.call(document.querySelectorAll('[data-panel]'));
var stageWrap = document.getElementById('stageWrap');
@ -117,7 +116,7 @@
input.find(function (item) {
return Number(item.rank_no || item.rankNo) === rankNo;
}) || {};
var items = reward.items || reward.resources || [];
var items = normalizeRewardItems(reward);
if (!items.length) {
items = [
{
@ -132,6 +131,41 @@
});
}
function normalizeRewardItems(reward) {
var group = reward.resource_group || reward.resourceGroup || reward.reward_resource_group || reward.rewardResourceGroup || {};
var items = reward.items || reward.resources || group.items || [];
return (items || []).map(function (item) {
var resource = item.resource || {};
var quantity = Number(item.quantity || 0);
var walletAmount = Number(item.wallet_asset_amount || item.walletAssetAmount || 0);
var walletType = item.wallet_asset_type || item.walletAssetType || '';
var name =
item.label ||
resource.name ||
item.name ||
(walletType ? walletType.replace(/_/g, ' ') : '') ||
group.name ||
reward.name ||
'Reward';
var suffix = quantity > 1 ? ' x' + quantity : walletAmount > 0 ? ' x' + formatNumber(walletAmount) : '';
return {
label: name + suffix,
resource_group_id: reward.resource_group_id || reward.resourceGroupId || group.group_id || group.groupId,
image:
item.image ||
item.image_url ||
item.imageUrl ||
resource.asset_url ||
resource.assetUrl ||
resource.preview_url ||
resource.previewUrl ||
resource.animation_url ||
resource.animationUrl,
single: false,
};
});
}
function renderGiftStrip(gifts) {
var cards = Array.prototype.slice.call(document.querySelectorAll('.gift-strip .gift-card'));
cards.forEach(function (card, index) {
@ -202,7 +236,6 @@
panels.forEach(function (panel) {
panel.hidden = panel.getAttribute('data-panel') !== next;
});
stageWrap.style.height = next === 'rewards' ? '1090px' : '802px';
updateBodyHeight();
}
@ -212,9 +245,11 @@
}
function updateBodyHeight() {
var scale = Math.min(window.innerWidth / 375, 1);
document.documentElement.style.setProperty('--app-scale', String(scale));
document.body.style.minHeight = viewport.offsetHeight * scale + 'px';
var scale = window.innerWidth / 1080;
var designHeight = page.getAttribute('data-tab') === 'rewards' ? 3138 : 2310;
document.documentElement.style.setProperty('--design-scale', String(scale));
stageWrap.style.height = Math.ceil(designHeight * scale) + 'px';
document.body.style.minHeight = stageWrap.offsetHeight + 'px';
}
function loadMock() {

View File

@ -4,7 +4,6 @@
}
:root {
--app-scale: 1;
--design-scale: 0.3472222222;
}
@ -37,16 +36,15 @@ button {
.app-viewport {
position: relative;
width: 375px;
width: 100vw;
min-height: 800px;
margin: 0 auto;
transform: scale(var(--app-scale));
transform-origin: top center;
margin: 0;
overflow: hidden;
}
.page {
position: relative;
width: 375px;
width: 100vw;
min-height: 800px;
overflow: hidden;
background: #050a36;
@ -54,15 +52,11 @@ button {
.stage-wrap {
position: relative;
width: 375px;
width: 100vw;
height: 802px;
overflow: hidden;
}
.page[data-tab="rewards"] .stage-wrap {
height: 1090px;
}
.stage {
position: absolute;
left: 0;
@ -445,10 +439,3 @@ button {
white-space: nowrap;
text-shadow: 0 2px 6px rgba(0, 0, 0, 0.45);
}
@media (max-width: 374px) {
.app-viewport {
margin-left: 0;
margin-right: 0;
}
}

View File

@ -436,6 +436,76 @@ var default_api = 'https://api.global-interaction.com/';
},
};
var managerCenterAPI = {
overview: function () {
return request('/api/v1/manager-center/overview', {
method: 'GET',
});
},
searchUsers: function (keyword, pageSize) {
return request('/api/v1/manager-center/users/search', {
method: 'GET',
query: {
keyword: keyword,
page_size: pageSize || 20,
},
});
},
listResources: function (resourceType, pageSize) {
return request('/api/v1/manager-center/resource-grants/resources', {
method: 'GET',
query: {
resource_type: resourceType,
page_size: pageSize || 20,
},
});
},
grantResource: function (payload) {
return request('/api/v1/manager-center/resource-grants', {
method: 'POST',
body: payload || {},
});
},
grantVip: function (payload) {
return request('/api/v1/manager-center/vip-grants', {
method: 'POST',
body: payload || {},
});
},
listBlocks: function (status, pageSize) {
return request('/api/v1/manager-center/blocks', {
method: 'GET',
query: {
status: status || 'active',
page_size: pageSize || 50,
},
});
},
blockUser: function (payload) {
return request('/api/v1/manager-center/blocks', {
method: 'POST',
body: payload || {},
});
},
unblockUser: function (blockId, payload) {
return request(
'/api/v1/manager-center/blocks/' +
encodeURIComponent(blockId || '') +
'/unblock',
{
method: 'POST',
body: payload || {},
}
);
},
updateLevel: function (payload) {
return request('/api/v1/manager-center/levels', {
method: 'POST',
body: payload || {},
});
},
};
var superadminCenterAPI = bdLeaderCenterAPI;
var levelAPI = {
@ -510,6 +580,7 @@ var default_api = 'https://api.global-interaction.com/';
agencyCenter: agencyCenterAPI,
bdCenter: bdCenterAPI,
bdLeaderCenter: bdLeaderCenterAPI,
managerCenter: managerCenterAPI,
superadminCenter: superadminCenterAPI,
level: levelAPI,
weeklyStar: weeklyStarAPI,

View File

@ -363,5 +363,30 @@
"lalu_landing.room_no": "رقم الغرفة:",
"lalu_landing.tagline": "تعرف على أصدقاء جدد",
"lalu_landing.download": "تنزيل",
"lalu_landing.open_download_failed": "رابط التنزيل غير مهيأ."
"lalu_landing.open_download_failed": "رابط التنزيل غير مهيأ.",
"cp.pageTitle": "أفضل CP في الأسبوع الماضي",
"cp.pageLabel": "نشاط أفضل CP في الأسبوع الماضي",
"cp.changeLanguage": "تغيير اللغة",
"cp.tabsLabel": "تبويبات نشاط CP",
"cp.days": "ي",
"cp.hours": "س",
"cp.minutes": "د",
"cp.seconds": "ث",
"cp.tabPlaza": "ساحة CP",
"cp.tabRank": "ترتيب CP",
"cp.tabRewards": "المكافآت",
"cp.plazaGiftTitle": "الهدايا المتبادلة بين CP بقيمة >= 5000 ذهب ستظهر هنا",
"cp.plazaGiftSubtitle": "(عرض آخر 10 سجلات مرتبة تصاعديا.)",
"cp.previousNo1Title": "مركز الرقم واحد السابق",
"cp.rewardMedal7": "وسام*7 أيام",
"cp.rewardMedalNote": "(وسام دائم بعد الحصول عليه 3 مرات)",
"cp.rewardHonorBanner7": "لافتة شرف*7 أيام",
"cp.rewardHeadwear7": "زينة رأس*7 أيام",
"cp.rewardMount7": "مركبة*7 أيام",
"cp.rewardCoins20000": "عملات*20000",
"cp.rewardDuke7": "دوق*7 أيام",
"cp.rewardMarquis7": "ماركيز*7 أيام",
"cp.rewardCoins10000": "عملات*10000",
"cp.rewardCount7": "عدّاد*7 أيام",
"cp.copyright": "حقوق هذا النشاط تعود إلى Shila"
}

View File

@ -369,5 +369,30 @@
"lalu_landing.room_no": "Room No:",
"lalu_landing.tagline": "Meet new friends",
"lalu_landing.download": "Download",
"lalu_landing.open_download_failed": "Download URL is not configured."
"lalu_landing.open_download_failed": "Download URL is not configured.",
"cp.pageTitle": "Last Weekly CP",
"cp.pageLabel": "Last Weekly CP activity",
"cp.changeLanguage": "Change language",
"cp.tabsLabel": "CP activity tabs",
"cp.days": "D",
"cp.hours": "H",
"cp.minutes": "M",
"cp.seconds": "S",
"cp.tabPlaza": "CP Plaza",
"cp.tabRank": "CP Rank",
"cp.tabRewards": "Rewards",
"cp.plazaGiftTitle": "Gifts exchanged between CPs >= 5000 gold will be displayed here",
"cp.plazaGiftSubtitle": "(Display the last 10 records, sorted in ascending order.)",
"cp.previousNo1Title": "The previous number one position",
"cp.rewardMedal7": "Medal*7 days",
"cp.rewardMedalNote": "(Permanent medal after getting 3 times)",
"cp.rewardHonorBanner7": "Honor banner*7 days",
"cp.rewardHeadwear7": "Headwear*7days",
"cp.rewardMount7": "Mount*7days",
"cp.rewardCoins20000": "Coins*20000",
"cp.rewardDuke7": "Duke*7days",
"cp.rewardMarquis7": "Marquis*7days",
"cp.rewardCoins10000": "Coins*10000",
"cp.rewardCount7": "Count*7days",
"cp.copyright": "The copyright of this activity belongs to Shila"
}

View File

@ -363,5 +363,30 @@
"lalu_landing.room_no": "Sala No:",
"lalu_landing.tagline": "Conoce nuevos amigos",
"lalu_landing.download": "Descargar",
"lalu_landing.open_download_failed": "La URL de descarga no está configurada."
"lalu_landing.open_download_failed": "La URL de descarga no está configurada.",
"cp.pageTitle": "CP de la semana pasada",
"cp.pageLabel": "Actividad CP de la semana pasada",
"cp.changeLanguage": "Cambiar idioma",
"cp.tabsLabel": "Pestañas de actividad CP",
"cp.days": "D",
"cp.hours": "H",
"cp.minutes": "M",
"cp.seconds": "S",
"cp.tabPlaza": "Plaza CP",
"cp.tabRank": "Ranking CP",
"cp.tabRewards": "Recompensas",
"cp.plazaGiftTitle": "Los regalos entre CPs >= 5000 de oro se mostrarán aquí",
"cp.plazaGiftSubtitle": "(Muestra los últimos 10 registros en orden ascendente.)",
"cp.previousNo1Title": "La posición número uno anterior",
"cp.rewardMedal7": "Medalla*7 días",
"cp.rewardMedalNote": "(Medalla permanente tras obtenerla 3 veces)",
"cp.rewardHonorBanner7": "Banner de honor*7 días",
"cp.rewardHeadwear7": "Tocado*7 días",
"cp.rewardMount7": "Montura*7 días",
"cp.rewardCoins20000": "Monedas*20000",
"cp.rewardDuke7": "Duque*7 días",
"cp.rewardMarquis7": "Marqués*7 días",
"cp.rewardCoins10000": "Monedas*10000",
"cp.rewardCount7": "Contador*7 días",
"cp.copyright": "Los derechos de esta actividad pertenecen a Shila"
}

View File

@ -363,5 +363,30 @@
"lalu_landing.room_no": "No. room:",
"lalu_landing.tagline": "Kenalan dengan teman baru",
"lalu_landing.download": "Unduh",
"lalu_landing.open_download_failed": "URL unduhan belum dikonfigurasi."
"lalu_landing.open_download_failed": "URL unduhan belum dikonfigurasi.",
"cp.pageTitle": "CP Minggu Lalu",
"cp.pageLabel": "Aktivitas CP minggu lalu",
"cp.changeLanguage": "Ganti bahasa",
"cp.tabsLabel": "Tab aktivitas CP",
"cp.days": "H",
"cp.hours": "J",
"cp.minutes": "M",
"cp.seconds": "D",
"cp.tabPlaza": "CP Plaza",
"cp.tabRank": "Peringkat CP",
"cp.tabRewards": "Hadiah",
"cp.plazaGiftTitle": "Hadiah antar CP >= 5000 gold akan ditampilkan di sini",
"cp.plazaGiftSubtitle": "(Menampilkan 10 catatan terbaru, diurutkan naik.)",
"cp.previousNo1Title": "Posisi nomor satu sebelumnya",
"cp.rewardMedal7": "Medali*7 hari",
"cp.rewardMedalNote": "(Medali permanen setelah mendapatkannya 3 kali)",
"cp.rewardHonorBanner7": "Banner kehormatan*7 hari",
"cp.rewardHeadwear7": "Hiasan kepala*7 hari",
"cp.rewardMount7": "Mount*7 hari",
"cp.rewardCoins20000": "Koin*20000",
"cp.rewardDuke7": "Duke*7 hari",
"cp.rewardMarquis7": "Marquis*7 hari",
"cp.rewardCoins10000": "Koin*10000",
"cp.rewardCount7": "Hitungan*7 hari",
"cp.copyright": "Hak cipta aktivitas ini milik Shila"
}

View File

@ -363,5 +363,30 @@
"lalu_landing.room_no": "Oda No:",
"lalu_landing.tagline": "Yeni arkadaşlarla tanış",
"lalu_landing.download": "İndir",
"lalu_landing.open_download_failed": "İndirme URL'si yapılandırılmadı."
"lalu_landing.open_download_failed": "İndirme URL'si yapılandırılmadı.",
"cp.pageTitle": "Geçen Haftanın CP Etkinliği",
"cp.pageLabel": "Geçen Haftanın CP etkinliği",
"cp.changeLanguage": "Dili değiştir",
"cp.tabsLabel": "CP etkinliği sekmeleri",
"cp.days": "G",
"cp.hours": "S",
"cp.minutes": "D",
"cp.seconds": "Sn",
"cp.tabPlaza": "CP Plaza",
"cp.tabRank": "CP Sıralaması",
"cp.tabRewards": "Ödüller",
"cp.plazaGiftTitle": "CPler arasında >= 5000 altın değerinde gönderilen hediyeler burada görünür",
"cp.plazaGiftSubtitle": "(Son 10 kayıt artan sırayla gösterilir.)",
"cp.previousNo1Title": "Önceki birincilik konumu",
"cp.rewardMedal7": "Madalya*7 gün",
"cp.rewardMedalNote": "(3 kez alındıktan sonra kalıcı madalya)",
"cp.rewardHonorBanner7": "Onur afişi*7 gün",
"cp.rewardHeadwear7": "Başlık*7 gün",
"cp.rewardMount7": "Binek*7 gün",
"cp.rewardCoins20000": "Coin*20000",
"cp.rewardDuke7": "Dük*7 gün",
"cp.rewardMarquis7": "Marki*7 gün",
"cp.rewardCoins10000": "Coin*10000",
"cp.rewardCount7": "Sayaç*7 gün",
"cp.copyright": "Bu etkinliğin telif hakkı Shila'ya aittir"
}

View File

@ -369,5 +369,30 @@
"lalu_landing.room_no": "房间号:",
"lalu_landing.tagline": "结识新朋友",
"lalu_landing.download": "下载",
"lalu_landing.open_download_failed": "下载地址未配置。"
"lalu_landing.open_download_failed": "下载地址未配置。",
"cp.pageTitle": "上周 CP",
"cp.pageLabel": "上周 CP 活动",
"cp.changeLanguage": "切换语言",
"cp.tabsLabel": "CP 活动标签",
"cp.days": "天",
"cp.hours": "时",
"cp.minutes": "分",
"cp.seconds": "秒",
"cp.tabPlaza": "CP 广场",
"cp.tabRank": "CP 排行",
"cp.tabRewards": "奖励",
"cp.plazaGiftTitle": "CP 之间互赠 >= 5000 金币的礼物会展示在这里",
"cp.plazaGiftSubtitle": "(展示最近 10 条记录,按时间升序排列。)",
"cp.previousNo1Title": "上期第一名位置",
"cp.rewardMedal7": "勋章*7天",
"cp.rewardMedalNote": "(获得 3 次后永久)",
"cp.rewardHonorBanner7": "荣誉横幅*7天",
"cp.rewardHeadwear7": "头饰*7天",
"cp.rewardMount7": "坐骑*7天",
"cp.rewardCoins20000": "金币*20000",
"cp.rewardDuke7": "公爵*7天",
"cp.rewardMarquis7": "侯爵*7天",
"cp.rewardCoins10000": "金币*10000",
"cp.rewardCount7": "计数*7天",
"cp.copyright": "本活动版权归 Shila 所有"
}

View File

@ -0,0 +1,322 @@
(function () {
var remoteBridgeVersion = '20260608.1';
function safeCall(fn, args) {
try {
if (typeof fn === 'function') {
return fn.apply(window, args || []);
}
} catch (error) {
post('debugLog', {
message: String((error && error.message) || error),
});
}
return null;
}
function readObject(value) {
return value && typeof value === 'object' && !Array.isArray(value)
? value
: {};
}
function readString(value) {
if (value === null || value === undefined) {
return '';
}
return String(value).trim();
}
function post(action, body) {
var message = JSON.stringify({ action: action, payload: body || {} });
if (postToChannel('GameBridgeChannel', message)) {
return;
}
if (postToChannel('BaishunBridgeChannel', message)) {
return;
}
if (postToChannel('LeaderBridgeChannel', message)) {
return;
}
postToChannel('NativeBridge', message);
}
function postToChannel(name, message) {
if (window[name] && window[name].postMessage) {
window[name].postMessage(message);
return true;
}
if (
window.webkit &&
window.webkit.messageHandlers &&
window.webkit.messageHandlers[name] &&
window.webkit.messageHandlers[name].postMessage
) {
window.webkit.messageHandlers[name].postMessage(message);
return true;
}
return false;
}
function launchPayload() {
return readObject(window.hyappGameLaunchPayload);
}
function launchConfig() {
var payload = launchPayload();
return readObject(window.hyappGameLaunchConfig || payload.launchConfig);
}
function launchEntry() {
var payload = launchPayload();
return readObject(window.hyappGameEntry || payload.entry);
}
function detectAdapter(config, payload) {
var values = [
config.adapterType,
config.adapter_type,
config.platformCode,
config.platform_code,
config.vendorType,
config.vendor_type,
payload.vendorType,
payload.vendor_type,
payload.platformCode,
payload.platform_code,
payload.gameId,
payload.game_id,
]
.map(readString)
.join('|')
.toLowerCase();
if (values.indexOf('vivagames') >= 0) {
return 'vivagames_v1';
}
if (values.indexOf('baishun') >= 0) {
return 'baishun_v1';
}
if (values.indexOf('leadercc') >= 0) {
return 'leadercc_v1';
}
if (values.indexOf('zeeone') >= 0) {
return 'zeeone_v1';
}
return 'generic';
}
function stringifyParams(body) {
try {
return JSON.stringify(body || {});
} catch (_) {
return '{}';
}
}
function notifyVivaGames(method, body) {
var params = body || {};
safeCall(window.OnCallGame, [method, params]);
try {
window.postMessage(
{ method: method, params: stringifyParams(params) },
'*'
);
} catch (_) {}
try {
if (
window.parent &&
window.parent !== window &&
window.parent.postMessage
) {
window.parent.postMessage(
{ method: method, params: stringifyParams(params) },
'*'
);
}
} catch (_) {}
}
function installAlias(name, action) {
var fn = function (body) {
return callNativeBridge(action || name, body);
};
fn.postMessage = function (body) {
callNativeBridge(action || name, body);
};
window[name] = fn;
window.webkit = window.webkit || {};
window.webkit.messageHandlers = window.webkit.messageHandlers || {};
window.webkit.messageHandlers[name] = { postMessage: fn.postMessage };
}
function callNativeBridge(action, body) {
var config = launchConfig();
var payload = launchPayload();
var adapter = detectAdapter(config, payload);
var normalized = readString(action);
if (normalized === 'getConfig') {
post('getConfig', body || {});
if (adapter === 'vivagames_v1') {
notifyVivaGames('getConfig', config);
}
return config;
}
if (normalized === 'getLaunchPayload') {
post('getLaunchPayload', body || {});
return payload;
}
post(normalized || 'NativeBridge', body || {});
if (adapter === 'vivagames_v1') {
notifyVivaGames(normalized || 'NativeBridge', body || {});
}
return null;
}
function installNativeBridge() {
var config = launchConfig();
var payload = launchPayload();
var entry = launchEntry();
var adapter = detectAdapter(config, payload);
var nativeBridge = window.NativeBridge || {};
var oldPostMessage =
typeof nativeBridge.postMessage === 'function'
? nativeBridge.postMessage.bind(nativeBridge)
: null;
window.__hyappRemoteGameBridge = {
adapter: adapter,
version: remoteBridgeVersion,
installedAt: Date.now(),
};
window.hyappGameLaunchPayload = payload;
window.hyappGameLaunchConfig = config;
window.hyappGameEntry = entry;
window.__baishunLastConfig = config;
window.baishunBridgeConfig = config;
window.leaderLaunchPayload = payload;
window.leaderLaunchConfig = config;
window.leaderLaunchEntry = entry;
nativeBridge.config = config;
nativeBridge.launchConfig = config;
nativeBridge.entry = entry;
nativeBridge.launchPayload = payload;
nativeBridge.getConfigSync = function () {
return launchConfig();
};
nativeBridge.getLaunchPayloadSync = function () {
return launchPayload();
};
nativeBridge.postMessage = function (body) {
if (oldPostMessage) {
oldPostMessage(
typeof body === 'string' ? body : stringifyParams(body)
);
return;
}
callNativeBridge('NativeBridge', body);
};
nativeBridge.getConfig = function (bodyOrCallback) {
var currentConfig = launchConfig();
if (typeof bodyOrCallback === 'function') {
safeCall(bodyOrCallback, [currentConfig, launchPayload()]);
}
callNativeBridge('getConfig', bodyOrCallback || {});
return currentConfig;
};
nativeBridge.getLaunchPayload = function (bodyOrCallback) {
var currentPayload = launchPayload();
if (typeof bodyOrCallback === 'function') {
safeCall(bodyOrCallback, [currentPayload]);
}
callNativeBridge('getLaunchPayload', bodyOrCallback || {});
return currentPayload;
};
nativeBridge.destroy = function (body) {
callNativeBridge('destroy', body || {});
};
nativeBridge.closeGame = function (body) {
callNativeBridge('closeGame', body || {});
};
nativeBridge.gameStart = function (body) {
callNativeBridge('gameStart', body || {});
};
nativeBridge.gameLoading = function (body) {
callNativeBridge('gameLoading', body || {});
};
nativeBridge.gameLoaded = function (body) {
callNativeBridge('gameLoaded', body || {});
};
nativeBridge.loadComplete = function (body) {
callNativeBridge('loadComplete', body || {});
};
nativeBridge.gameRecharge = function (body) {
callNativeBridge('gameRecharge', body || {});
};
nativeBridge.pay = function (body) {
callNativeBridge('pay', body || {});
};
nativeBridge.copyText = function (body) {
callNativeBridge('copyText', body || {});
};
nativeBridge.refreshBalance = function (body) {
callNativeBridge('refreshBalance', body || {});
};
nativeBridge.refreshBlance = function (body) {
callNativeBridge('refreshBalance', body || {});
};
nativeBridge.debugLog = function (body) {
callNativeBridge('debugLog', body || {});
};
window.NativeBridge = nativeBridge;
[
['getConfig', 'getConfig'],
['destroy', 'destroy'],
['closeGame', 'closeGame'],
['gameStart', 'gameStart'],
['gameLoading', 'gameLoading'],
['gameLoaded', 'gameLoaded'],
['loadComplete', 'loadComplete'],
['gameRecharge', 'gameRecharge'],
['pay', 'pay'],
['copyText', 'copyText'],
['refreshBalance', 'refreshBalance'],
['refreshBlance', 'refreshBalance'],
['debugLog', 'debugLog'],
].forEach(function (item) {
installAlias(item[0], item[1]);
});
if (adapter === 'vivagames_v1') {
safeCall(window.OnCallGame, ['getConfig', config]);
}
signalReady(config, payload);
}
function signalReady(config, payload) {
[
['hyappGameBridgeReady', payload],
['hyappRemoteGameBridgeReady', window.__hyappRemoteGameBridge],
['baishunBridgeReady', config],
['leaderLaunchConfig', payload],
['launchConfig', config],
].forEach(function (item) {
try {
window.dispatchEvent(
new CustomEvent(item[0], { detail: item[1] })
);
if (document && document.dispatchEvent) {
document.dispatchEvent(
new CustomEvent(item[0], { detail: item[1] })
);
}
} catch (_) {}
});
}
window.HYAppGameBridge = window.HYAppGameBridge || {};
window.HYAppGameBridge.install = installNativeBridge;
window.HYAppGameBridge.version = remoteBridgeVersion;
installNativeBridge();
})();

View File

@ -76,7 +76,8 @@ input:focus-visible {
.bd-center,
.agency-center,
.superadmin-center,
.coinseller-center {
.coinseller-center,
.manager-center {
position: relative;
width: 100%;
max-width: 430px;

View File

@ -0,0 +1,384 @@
<!doctype html>
<html lang="en">
<head>
<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>Control Panel</title>
<link rel="stylesheet" href="../../common/theme.css?v=20260608" />
<link rel="stylesheet" href="../common/center.css?v=20260608" />
<link rel="stylesheet" href="./style.css?v=20260608-manager-unblock" />
</head>
<body>
<div class="manager-center" aria-label="Control Panel">
<div class="hero-bg" aria-hidden="true"></div>
<nav class="title-bar">
<button
class="back-button"
id="backButton"
type="button"
aria-label="Back"
data-i18n-aria="manager_center.back"
>
<svg viewBox="0 0 32 32" aria-hidden="true">
<path d="M20 8 12 16l8 8" />
</svg>
</button>
<h1 data-i18n="manager_center.title">Control Panel</h1>
<div class="language-switcher">
<button
class="language-button"
type="button"
aria-label="Change language"
aria-expanded="false"
data-language-toggle
data-current-lang
data-i18n-aria="manager_center.change_language"
>
EN
</button>
<div class="language-menu" data-language-menu hidden>
<button type="button" data-lang-option="en">EN</button>
<button type="button" data-lang-option="ar">AR</button>
<button type="button" data-lang-option="tr">TR</button>
<button type="button" data-lang-option="es">ES</button>
<button type="button" data-lang-option="zh">ZH</button>
<button type="button" data-lang-option="id">ID</button>
</div>
</div>
</nav>
<main class="content">
<section class="card manager-panel">
<div class="manager-grid" aria-label="Manager tools">
<button
class="manager-tile"
type="button"
data-action="send-frame"
>
<span class="tile-icon frame-icon" aria-hidden="true">
<svg viewBox="0 0 48 48">
<path d="M10 10h28v28H10Z" />
<path d="M16 16h16v16H16Z" />
<path d="M10 18h6M32 18h6M10 30h6M32 30h6" />
</svg>
</span>
<span
class="tile-title"
data-i18n="manager_center.send_frame"
>Send Frame</span
>
</button>
<button
class="manager-tile"
type="button"
data-action="send-car"
>
<span class="tile-icon car-icon" aria-hidden="true">
<svg viewBox="0 0 48 48">
<path d="M9 28h30l-4-10H13Z" />
<path d="M13 28v7M35 28v7" />
<path d="M15 35a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z" />
<path d="M33 35a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z" />
<path d="M18 18h12" />
</svg>
</span>
<span
class="tile-title"
data-i18n="manager_center.send_car"
>Send Car</span
>
</button>
<button
class="manager-tile"
type="button"
data-action="send-vip"
>
<span class="tile-icon vip-icon" aria-hidden="true">
<svg viewBox="0 0 48 48">
<path d="m9 18 8 8 7-14 7 14 8-8-4 20H13Z" />
<path d="M13 38h22" />
<path d="M20 29h8" />
</svg>
</span>
<span
class="tile-title"
data-i18n="manager_center.send_vip"
>Send VIP</span
>
</button>
<button
class="manager-tile"
type="button"
data-action="update-user-level"
>
<span
class="tile-icon level-icon"
aria-hidden="true"
>
<svg viewBox="0 0 48 48">
<path
d="M24 7a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z"
/>
<path
d="M8 42c2.4-8 8.2-12 16-12s13.6 4 16 12"
/>
<path d="M35 10v7M31.5 13.5h7" />
</svg>
</span>
<span
class="tile-title"
data-i18n="manager_center.update_user_level"
>Update User Level</span
>
</button>
<button
class="manager-tile manager-tile-wide"
type="button"
data-action="block-user"
>
<span class="tile-icon block-icon" aria-hidden="true">
<svg viewBox="0 0 48 48">
<path
d="M18 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"
/>
<path
d="M5 42c1.8-9.2 6.2-13.8 13-13.8 4.3 0 7.6 1.8 10 5.3"
/>
<path
d="M35 23a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
/>
<path d="m29 29 12 12" />
</svg>
</span>
<span
class="tile-title"
data-i18n="manager_center.block_user"
>Block User</span
>
</button>
</div>
</section>
</main>
<div
class="block-modal level-modal"
id="levelModal"
aria-hidden="true"
hidden
>
<button
class="block-backdrop"
id="levelBackdrop"
type="button"
aria-label="Close update level dialog"
></button>
<section
class="block-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="levelDialogTitle"
>
<div class="block-dialog-header">
<h2 id="levelDialogTitle">Update User Level</h2>
<button
class="block-close"
id="levelCloseButton"
type="button"
aria-label="Close"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</button>
</div>
<div class="block-dialog-body">
<div class="block-field">
<label for="levelSearchInput">User ID</label>
<div class="block-search-row">
<input
id="levelSearchInput"
type="text"
inputmode="numeric"
autocomplete="off"
placeholder="Enter user ID"
/>
<button id="levelSearchButton" type="button">
Search
</button>
</div>
</div>
<div
class="block-search-result"
id="levelSearchResult"
aria-live="polite"
></div>
<div class="level-panel" id="levelPanel" hidden>
<div class="block-section-title">
Select user level
</div>
<div
class="level-grid"
id="levelGrid"
aria-label="Select user level"
></div>
<button
class="confirm-level-button"
id="confirmLevelButton"
type="button"
disabled
>
Confirm Update
</button>
</div>
</div>
</section>
</div>
<div
class="block-modal"
id="blockModal"
aria-hidden="true"
hidden
>
<button
class="block-backdrop"
id="blockBackdrop"
type="button"
aria-label="Close block user dialog"
></button>
<section
class="block-dialog"
role="dialog"
aria-modal="true"
aria-labelledby="blockDialogTitle"
>
<div class="block-dialog-header">
<h2 id="blockDialogTitle">Block User</h2>
<button
class="block-close"
id="blockCloseButton"
type="button"
aria-label="Close"
>
<svg viewBox="0 0 24 24" aria-hidden="true">
<path d="M6 6l12 12M18 6 6 18" />
</svg>
</button>
</div>
<div class="block-dialog-body">
<div class="block-field">
<label for="blockSearchInput">User ID</label>
<div class="block-search-row">
<input
id="blockSearchInput"
type="text"
inputmode="numeric"
autocomplete="off"
placeholder="Enter user ID"
/>
<button id="blockSearchButton" type="button">
Search
</button>
</div>
</div>
<div
class="block-search-result"
id="blockSearchResult"
aria-live="polite"
></div>
<div
class="block-duration-panel"
id="blockDurationPanel"
hidden
>
<div class="block-section-title">
Select block duration
</div>
<div class="duration-grid" id="durationGrid">
<button
type="button"
data-duration-ms="3600000"
data-duration-label="1 hour"
>
1 hour
</button>
<button
type="button"
data-duration-ms="86400000"
data-duration-label="1 day"
>
1 day
</button>
<button
type="button"
data-duration-ms="604800000"
data-duration-label="7 days"
>
7 days
</button>
<button
type="button"
data-duration-ms="2592000000"
data-duration-label="30 days"
>
30 days
</button>
<button
type="button"
data-duration-ms="0"
data-duration-label="Permanent"
>
Permanent
</button>
</div>
<button
class="confirm-block-button"
id="confirmBlockButton"
type="button"
disabled
>
Confirm Block
</button>
</div>
<div class="block-list-header">
<div class="block-section-title">Blocked Users</div>
<div class="block-list-count" id="blockListCount">
0
</div>
</div>
<div
class="blocked-list"
id="blockedList"
aria-live="polite"
></div>
</div>
</section>
</div>
<div class="home-indicator" aria-hidden="true"></div>
</div>
<script src="../../common/toast.js"></script>
<script src="../../common/api.js?v=20260608-manager-real"></script>
<script src="../../common/params.js?v=20260608-manager-real"></script>
<script src="../../common/jsbridge.js"></script>
<script src="../../common/i18n.js?v=20260608"></script>
<script src="./script.js?v=20260608-manager-duration"></script>
</body>
</html>

View File

@ -0,0 +1,937 @@
(function () {
var ACTION_LABELS = {
'block-user': 'Block User',
'send-frame': 'Send Frame',
'send-car': 'Send Car',
'send-vip': 'Send VIP',
'update-user-level': 'Update User Level',
};
var DAY_MS = 24 * 60 * 60 * 1000;
var PERMANENT_BLOCK_MS = 10 * 365 * DAY_MS;
var RESOURCE_TYPES = {
'send-frame': 'avatar_frame',
'send-car': 'vehicle',
};
var RESOURCE_GRANT_DURATIONS = [
{ label: '7 days', ms: 7 * DAY_MS },
{ label: '30 days', ms: 30 * DAY_MS },
{ label: '90 days', ms: 90 * DAY_MS },
];
var DEFAULT_RESOURCE_GRANT_DURATION_MS = RESOURCE_GRANT_DURATIONS[0].ms;
var TRACK_LABELS = {
wealth: 'Wealth',
charm: 'Charm',
game: 'Game',
};
var api = window.HyAppAPI && window.HyAppAPI.managerCenter;
var pageState = {
loading: true,
denied: false,
overview: null,
resourcesByType: {},
};
var blockState = {
searchedUser: null,
selectedDurationMs: 0,
selectedDurationLabel: '',
blockedUsers: [],
submitting: false,
};
var levelState = {
searchedUser: null,
selectedTrack: 'wealth',
selectedLevel: 0,
submitting: false,
};
var grantState = {
action: '',
resourceType: '',
title: '',
searchedUser: null,
selectedResourceId: '',
selectedResourceDurationMs: DEFAULT_RESOURCE_GRANT_DURATION_MS,
selectedVipLevel: 0,
submitting: false,
};
function $(id) {
return document.getElementById(id);
}
function root() {
return document.querySelector('.manager-center');
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function toast(message) {
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
return;
}
window.alert(message);
}
function showError(error) {
var message =
error && error.status === 403
? 'Permission denied.'
: (error && error.message) || 'Request failed.';
toast(message);
}
function commandId(prefix) {
return [
prefix || 'manager',
Date.now(),
Math.random().toString(16).slice(2, 10),
].join('-');
}
function userName(user) {
return (
user.username ||
user.name ||
user.target_username ||
user.display_user_id ||
user.target_display_user_id ||
'User'
);
}
function userDisplayId(user) {
return user.display_user_id || user.target_display_user_id || '';
}
function userId(user) {
return user.user_id || user.target_user_id || '';
}
function avatarUrl(user) {
return user.avatar || user.target_avatar || '';
}
function initial(text) {
return String(text || 'M')
.trim()
.charAt(0)
.toUpperCase();
}
function createNode(tag, className, text) {
var node = document.createElement(tag);
if (className) node.className = className;
if (text !== undefined) node.textContent = text;
return node;
}
function setAvatar(node, user) {
var url = avatarUrl(user);
node.innerHTML = '';
if (url) {
var img = document.createElement('img');
img.src = url;
img.alt = '';
node.appendChild(img);
return;
}
node.textContent = initial(userName(user) || userDisplayId(user));
}
function formatDateTime(ms) {
if (!ms) return 'Never';
var date = new Date(Number(ms));
var year = date.getFullYear();
var month = String(date.getMonth() + 1).padStart(2, '0');
var day = String(date.getDate()).padStart(2, '0');
var hour = String(date.getHours()).padStart(2, '0');
var minute = String(date.getMinutes()).padStart(2, '0');
return year + '-' + month + '-' + day + ' ' + hour + ':' + minute;
}
function setPageLoading(loading) {
pageState.loading = loading;
var container = root();
if (!container) return;
container.classList.toggle('is-loading', loading);
}
function setDenied(denied) {
pageState.denied = denied;
var container = root();
if (!container) return;
container.classList.toggle('is-denied', denied);
}
function normalizeUser(item) {
return {
user_id: item.user_id || item.userId || item.target_user_id || '',
display_user_id:
item.display_user_id ||
item.displayUserId ||
item.target_display_user_id ||
'',
username: item.username || item.name || item.target_username || '',
avatar: item.avatar || item.target_avatar || '',
country: item.country || '',
levels: item.levels || {},
};
}
function normalizeBlock(item) {
return {
block_id: item.block_id || item.blockId || '',
target_user_id: item.target_user_id || item.targetUserId || '',
target_display_user_id:
item.target_display_user_id || item.targetDisplayUserId || '',
target_username: item.target_username || item.targetUsername || '',
target_avatar: item.target_avatar || item.targetAvatar || '',
country: item.country || '',
blocked_until_ms:
item.blocked_until_ms || item.blockedUntilMs || item.unblockAtMs || 0,
status: item.status || 'active',
};
}
function apiSearchUser(keyword) {
if (!api || !api.searchUsers) {
return Promise.reject(new Error('API not available.'));
}
return api.searchUsers(keyword, 20).then(function (data) {
var items = (data && data.items) || [];
return items.map(normalizeUser);
});
}
function loadOverview() {
if (!api || !api.overview) {
setDenied(true);
toast('Permission denied.');
return Promise.resolve();
}
setPageLoading(true);
return api
.overview()
.then(function (data) {
pageState.overview = data || {};
setDenied(false);
return refreshBlocks();
})
.catch(function (error) {
setDenied(true);
showError(error);
})
.finally(function () {
setPageLoading(false);
});
}
function refreshBlocks() {
if (!api || !api.listBlocks) return Promise.resolve();
return api
.listBlocks('active', 50)
.then(function (data) {
blockState.blockedUsers = ((data && data.items) || []).map(
normalizeBlock
);
renderBlockedList();
})
.catch(showError);
}
function goBack() {
if (window.HyAppBridge && window.HyAppBridge.back) {
window.HyAppBridge.back();
return;
}
if (window.history.length > 1) {
window.history.back();
}
}
function renderUserRow(user, actionText, onAction) {
var row = createNode('div', 'searched-user');
var avatar = createNode('div', 'searched-avatar');
setAvatar(avatar, user);
var copy = createNode('div', 'blocked-row-main');
copy.appendChild(createNode('div', 'searched-name', userName(user)));
copy.appendChild(
createNode('div', 'searched-meta', 'UID: ' + userDisplayId(user))
);
var button = createNode('button', 'user-block-button', actionText);
button.type = 'button';
button.addEventListener('click', onAction);
row.appendChild(avatar);
row.appendChild(copy);
row.appendChild(button);
return row;
}
function renderSearchResult() {
var container = $('blockSearchResult');
if (!container) return;
container.innerHTML = '';
if (!blockState.searchedUser) return;
container.appendChild(
renderUserRow(blockState.searchedUser, 'Block', function () {
selectDuration('86400000', '1 day');
var panel = $('blockDurationPanel');
if (panel) panel.hidden = false;
})
);
}
function setupLevelGrid() {
var grid = $('levelGrid');
if (!grid || grid.children.length) return;
for (var level = 1; level <= 11; level += 1) {
var button = createNode('button', '', 'Lv. ' + level);
button.type = 'button';
button.setAttribute('data-level', String(level));
grid.appendChild(button);
}
}
function ensureTrackSelector() {
var panel = $('levelPanel');
if (!panel || $('levelTrackGrid')) return;
var title = createNode('div', 'block-section-title', 'Select level type');
var grid = createNode('div', 'track-grid');
grid.id = 'levelTrackGrid';
Object.keys(TRACK_LABELS).forEach(function (track) {
var button = createNode('button', '', TRACK_LABELS[track]);
button.type = 'button';
button.setAttribute('data-track', track);
button.addEventListener('click', function () {
levelState.selectedTrack = track;
renderLevelButtons();
renderLevelSearchResult();
});
grid.appendChild(button);
});
panel.insertBefore(grid, panel.firstChild);
panel.insertBefore(title, grid);
}
function currentTrackLevel(user) {
var track = levelState.selectedTrack;
var levels = (user && user.levels) || {};
return levels[track] && levels[track].level ? levels[track].level : 0;
}
function renderLevelSearchResult() {
var container = $('levelSearchResult');
if (!container) return;
container.innerHTML = '';
if (!levelState.searchedUser) return;
var user = levelState.searchedUser;
var row = createNode('div', 'searched-user');
var avatar = createNode('div', 'searched-avatar');
setAvatar(avatar, user);
var copy = createNode('div', 'blocked-row-main');
copy.appendChild(createNode('div', 'searched-name', userName(user)));
copy.appendChild(
createNode(
'div',
'searched-meta',
'UID: ' +
userDisplayId(user) +
' · ' +
TRACK_LABELS[levelState.selectedTrack] +
' Lv. ' +
currentTrackLevel(user)
)
);
var badge = createNode(
'div',
'block-list-count',
'Lv. ' + currentTrackLevel(user)
);
row.appendChild(avatar);
row.appendChild(copy);
row.appendChild(badge);
container.appendChild(row);
}
function renderLevelButtons() {
var trackButtons = document.querySelectorAll('#levelTrackGrid [data-track]');
trackButtons.forEach(function (button) {
button.classList.toggle(
'is-selected',
button.getAttribute('data-track') === levelState.selectedTrack
);
});
var buttons = document.querySelectorAll('#levelGrid [data-level]');
buttons.forEach(function (button) {
var isSelected =
Number(button.getAttribute('data-level')) ===
levelState.selectedLevel;
button.classList.toggle('is-selected', isSelected);
});
var confirmButton = $('confirmLevelButton');
if (confirmButton) {
confirmButton.disabled =
!levelState.searchedUser ||
!levelState.selectedLevel ||
levelState.submitting;
}
}
function selectLevel(level) {
levelState.selectedLevel = Number(level) || 0;
renderLevelButtons();
}
function renderDurationButtons() {
var buttons = document.querySelectorAll('#durationGrid [data-duration-ms]');
buttons.forEach(function (button) {
var isSelected =
Number(button.getAttribute('data-duration-ms')) ===
blockState.selectedDurationMs;
button.classList.toggle('is-selected', isSelected);
});
var confirmButton = $('confirmBlockButton');
if (confirmButton) {
confirmButton.disabled =
!blockState.searchedUser || blockState.submitting;
}
}
function selectDuration(durationMs, label) {
blockState.selectedDurationMs = Number(durationMs) || 0;
blockState.selectedDurationLabel = label || 'Permanent';
renderDurationButtons();
}
function renderBlockedList() {
var container = $('blockedList');
var count = $('blockListCount');
if (!container) return;
container.innerHTML = '';
if (count) count.textContent = String(blockState.blockedUsers.length);
if (!blockState.blockedUsers.length) {
container.appendChild(
createNode('div', 'blocked-empty', 'No blocked users.')
);
return;
}
blockState.blockedUsers.forEach(function (user) {
var row = createNode('div', 'blocked-row');
var avatar = createNode('div', 'blocked-avatar');
setAvatar(avatar, user);
var copy = createNode('div', 'blocked-row-main');
copy.appendChild(createNode('div', 'blocked-name', userName(user)));
copy.appendChild(
createNode('div', 'blocked-meta', 'UID: ' + userDisplayId(user))
);
copy.appendChild(
createNode(
'div',
'blocked-time',
'Unblock time: ' + formatDateTime(user.blocked_until_ms)
)
);
var unblockButton = createNode(
'button',
'user-unblock-button',
'Unblock'
);
unblockButton.type = 'button';
unblockButton.addEventListener('click', function () {
unblockUser(user.block_id);
});
row.appendChild(avatar);
row.appendChild(copy);
row.appendChild(unblockButton);
container.appendChild(row);
});
}
function unblockUser(blockId) {
if (!blockId || blockState.submitting) return;
blockState.submitting = true;
api.unblockUser(blockId, { reason: 'manager_center' })
.then(function () {
toast('User unblocked.');
return refreshBlocks();
})
.catch(showError)
.finally(function () {
blockState.submitting = false;
});
}
function resetBlockSearch() {
var input = $('blockSearchInput');
var panel = $('blockDurationPanel');
blockState.searchedUser = null;
blockState.selectedDurationMs = 0;
blockState.selectedDurationLabel = '';
if (input) input.value = '';
if (panel) panel.hidden = true;
renderSearchResult();
renderDurationButtons();
}
function openBlockModal() {
if (pageState.denied) {
toast('Permission denied.');
return;
}
var modal = $('blockModal');
if (!modal) return;
resetBlockSearch();
refreshBlocks();
modal.hidden = false;
modal.setAttribute('aria-hidden', 'false');
setTimeout(function () {
var input = $('blockSearchInput');
if (input) input.focus();
}, 30);
}
function closeBlockModal() {
var modal = $('blockModal');
if (!modal) return;
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
}
function resetLevelSearch() {
var input = $('levelSearchInput');
var panel = $('levelPanel');
levelState.searchedUser = null;
levelState.selectedTrack = 'wealth';
levelState.selectedLevel = 0;
if (input) input.value = '';
if (panel) panel.hidden = true;
renderLevelSearchResult();
renderLevelButtons();
}
function openLevelModal() {
if (pageState.denied) {
toast('Permission denied.');
return;
}
var modal = $('levelModal');
if (!modal) return;
resetLevelSearch();
modal.hidden = false;
modal.setAttribute('aria-hidden', 'false');
setTimeout(function () {
var input = $('levelSearchInput');
if (input) input.focus();
}, 30);
}
function closeLevelModal() {
var modal = $('levelModal');
if (!modal) return;
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
}
function searchBlockUser() {
var input = $('blockSearchInput');
var value = input ? input.value.trim() : '';
var panel = $('blockDurationPanel');
if (!value) {
toast('Enter user ID.');
return;
}
apiSearchUser(value)
.then(function (items) {
if (!items.length) {
blockState.searchedUser = null;
toast('No user found.');
return;
}
blockState.searchedUser = items[0];
blockState.selectedDurationMs = 0;
blockState.selectedDurationLabel = '';
if (panel) panel.hidden = true;
renderSearchResult();
renderDurationButtons();
})
.catch(showError);
}
function confirmBlockUser() {
if (!blockState.searchedUser) {
toast('Search a user first.');
return;
}
if (blockState.submitting) return;
var durationMs = blockState.selectedDurationMs || PERMANENT_BLOCK_MS;
blockState.submitting = true;
renderDurationButtons();
api.blockUser({
command_id: commandId('manager-block'),
target_user_id: userId(blockState.searchedUser),
blocked_until_ms: Date.now() + durationMs,
reason: 'manager_center',
})
.then(function () {
toast('User blocked.');
resetBlockSearch();
return refreshBlocks();
})
.catch(showError)
.finally(function () {
blockState.submitting = false;
renderDurationButtons();
});
}
function searchLevelUser() {
var input = $('levelSearchInput');
var value = input ? input.value.trim() : '';
var panel = $('levelPanel');
if (!value) {
toast('Enter user ID.');
return;
}
apiSearchUser(value)
.then(function (items) {
if (!items.length) {
levelState.searchedUser = null;
toast('No user found.');
return;
}
levelState.searchedUser = items[0];
levelState.selectedLevel = 0;
if (panel) panel.hidden = false;
renderLevelSearchResult();
renderLevelButtons();
})
.catch(showError);
}
function confirmLevelUpdate() {
if (!levelState.searchedUser) {
toast('Search a user first.');
return;
}
if (!levelState.selectedLevel) {
toast('Select a user level.');
return;
}
if (levelState.submitting) return;
levelState.submitting = true;
renderLevelButtons();
api.updateLevel({
command_id: commandId('manager-level'),
target_user_id: userId(levelState.searchedUser),
track: levelState.selectedTrack,
level: levelState.selectedLevel,
reason: 'manager_center',
})
.then(function () {
toast('User level updated.');
closeLevelModal();
})
.catch(showError)
.finally(function () {
levelState.submitting = false;
renderLevelButtons();
});
}
function ensureGrantModal() {
if ($('grantModal')) return;
var modal = createNode('div', 'block-modal grant-modal');
modal.id = 'grantModal';
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
modal.innerHTML =
'<button class="block-backdrop" id="grantBackdrop" type="button" aria-label="Close"></button>' +
'<section class="block-dialog" role="dialog" aria-modal="true" aria-labelledby="grantDialogTitle">' +
'<div class="block-dialog-header"><h2 id="grantDialogTitle">Send</h2>' +
'<button class="block-close" id="grantCloseButton" type="button" aria-label="Close"><svg viewBox="0 0 24 24" aria-hidden="true"><path d="M6 6l12 12M18 6 6 18"/></svg></button></div>' +
'<div class="block-dialog-body"><div class="block-field"><label for="grantSearchInput">User ID</label>' +
'<div class="block-search-row"><input id="grantSearchInput" type="text" inputmode="numeric" autocomplete="off" placeholder="Enter user ID"/><button id="grantSearchButton" type="button">Search</button></div></div>' +
'<div class="block-search-result" id="grantSearchResult" aria-live="polite"></div>' +
'<div class="level-panel" id="grantPanel" hidden><div class="block-section-title" id="grantOptionTitle">Select item</div><div class="grant-option-grid" id="grantOptionGrid"></div><div class="block-section-title" id="grantDurationTitle" hidden>Select duration</div><div class="duration-grid grant-duration-grid" id="grantDurationGrid" hidden></div><button class="confirm-level-button" id="confirmGrantButton" type="button" disabled>Send now</button></div></div></section>';
document.querySelector('.manager-center').appendChild(modal);
$('grantBackdrop').addEventListener('click', closeGrantModal);
$('grantCloseButton').addEventListener('click', closeGrantModal);
$('grantSearchButton').addEventListener('click', searchGrantUser);
$('grantSearchInput').addEventListener('keydown', function (event) {
if (event.key === 'Enter') searchGrantUser();
});
$('confirmGrantButton').addEventListener('click', confirmGrant);
}
function openGrantModal(action) {
if (pageState.denied) {
toast('Permission denied.');
return;
}
ensureGrantModal();
grantState.action = action;
grantState.resourceType = RESOURCE_TYPES[action] || '';
grantState.title = ACTION_LABELS[action] || 'Send';
grantState.searchedUser = null;
grantState.selectedResourceId = '';
grantState.selectedResourceDurationMs = DEFAULT_RESOURCE_GRANT_DURATION_MS;
grantState.selectedVipLevel = 0;
$('grantDialogTitle').textContent = grantState.title;
$('grantSearchInput').value = '';
$('grantSearchResult').innerHTML = '';
$('grantPanel').hidden = true;
$('grantModal').hidden = false;
$('grantModal').setAttribute('aria-hidden', 'false');
renderGrantOptions();
if (grantState.resourceType) loadGrantResources(grantState.resourceType);
setTimeout(function () {
$('grantSearchInput').focus();
}, 30);
}
function closeGrantModal() {
var modal = $('grantModal');
if (!modal) return;
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
}
function loadGrantResources(resourceType) {
if (pageState.resourcesByType[resourceType]) {
renderGrantOptions();
return;
}
api.listResources(resourceType, 20)
.then(function (data) {
pageState.resourcesByType[resourceType] =
(data && data.items) || [];
renderGrantOptions();
})
.catch(showError);
}
function searchGrantUser() {
var value = $('grantSearchInput').value.trim();
if (!value) {
toast('Enter user ID.');
return;
}
apiSearchUser(value)
.then(function (items) {
if (!items.length) {
grantState.searchedUser = null;
toast('No user found.');
return;
}
grantState.searchedUser = items[0];
var container = $('grantSearchResult');
container.innerHTML = '';
container.appendChild(
renderUserRow(grantState.searchedUser, 'Select', function () {
$('grantPanel').hidden = false;
renderGrantOptions();
})
);
$('grantPanel').hidden = false;
renderGrantOptions();
})
.catch(showError);
}
function renderGrantOptions() {
var grid = $('grantOptionGrid');
if (!grid) return;
grid.innerHTML = '';
var durationTitle = $('grantDurationTitle');
var durationGrid = $('grantDurationGrid');
if (durationTitle) {
durationTitle.hidden = false;
durationTitle.textContent =
grantState.action === 'send-vip'
? 'Duration: 30 days'
: 'Select duration';
}
if (durationGrid) {
durationGrid.innerHTML = '';
durationGrid.hidden = grantState.action === 'send-vip';
}
if (grantState.action === 'send-vip') {
$('grantOptionTitle').textContent = 'Select VIP level';
[1, 2, 3].forEach(function (level) {
var button = createNode('button', '', 'VIP ' + level);
button.type = 'button';
button.classList.toggle(
'is-selected',
grantState.selectedVipLevel === level
);
button.addEventListener('click', function () {
grantState.selectedVipLevel = level;
renderGrantOptions();
});
grid.appendChild(button);
});
} else {
$('grantOptionTitle').textContent = 'Select resource';
var resources = pageState.resourcesByType[grantState.resourceType] || [];
if (!resources.length) {
grid.appendChild(createNode('div', 'blocked-empty', 'No resource available.'));
}
resources.forEach(function (resource) {
var button = createNode('button', 'grant-resource-option');
button.type = 'button';
button.classList.toggle(
'is-selected',
String(grantState.selectedResourceId) ===
String(resource.resource_id)
);
button.textContent = resource.name || 'Resource #' + resource.resource_id;
button.addEventListener('click', function () {
grantState.selectedResourceId = resource.resource_id;
renderGrantOptions();
});
grid.appendChild(button);
});
if (durationGrid) {
RESOURCE_GRANT_DURATIONS.forEach(function (item) {
var durationButton = createNode('button', '', item.label);
durationButton.type = 'button';
durationButton.classList.toggle(
'is-selected',
grantState.selectedResourceDurationMs === item.ms
);
durationButton.addEventListener('click', function () {
grantState.selectedResourceDurationMs = item.ms;
renderGrantOptions();
});
durationGrid.appendChild(durationButton);
});
}
}
var confirm = $('confirmGrantButton');
if (confirm) {
confirm.disabled =
!grantState.searchedUser ||
grantState.submitting ||
(grantState.action === 'send-vip'
? !grantState.selectedVipLevel
: !grantState.selectedResourceId ||
!grantState.selectedResourceDurationMs);
}
}
function confirmGrant() {
if (!grantState.searchedUser || grantState.submitting) return;
grantState.submitting = true;
renderGrantOptions();
var payload = {
command_id: commandId('manager-grant'),
target_user_id: userId(grantState.searchedUser),
reason: 'manager_center',
};
var request;
if (grantState.action === 'send-vip') {
payload.level = grantState.selectedVipLevel;
request = api.grantVip(payload);
} else {
payload.resource_id = grantState.selectedResourceId;
payload.duration_ms = grantState.selectedResourceDurationMs;
request = api.grantResource(payload);
}
request
.then(function () {
toast('Sent successfully.');
closeGrantModal();
})
.catch(showError)
.finally(function () {
grantState.submitting = false;
renderGrantOptions();
});
}
function bindEvents() {
var backButton = $('backButton');
if (backButton) backButton.addEventListener('click', goBack);
document.querySelectorAll('[data-action]').forEach(function (button) {
button.addEventListener('click', function () {
var action = button.getAttribute('data-action');
if (action === 'block-user') {
openBlockModal();
return;
}
if (action === 'update-user-level') {
openLevelModal();
return;
}
openGrantModal(action);
});
});
setupLevelGrid();
ensureTrackSelector();
var closeButton = $('blockCloseButton');
var backdrop = $('blockBackdrop');
var searchButton = $('blockSearchButton');
var searchInput = $('blockSearchInput');
var confirmButton = $('confirmBlockButton');
var levelCloseButton = $('levelCloseButton');
var levelBackdrop = $('levelBackdrop');
var levelSearchButton = $('levelSearchButton');
var levelSearchInput = $('levelSearchInput');
var confirmLevelButton = $('confirmLevelButton');
if (closeButton) closeButton.addEventListener('click', closeBlockModal);
if (backdrop) backdrop.addEventListener('click', closeBlockModal);
if (levelCloseButton) levelCloseButton.addEventListener('click', closeLevelModal);
if (levelBackdrop) levelBackdrop.addEventListener('click', closeLevelModal);
if (searchButton) searchButton.addEventListener('click', searchBlockUser);
if (searchInput) {
searchInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') searchBlockUser();
});
}
if (levelSearchButton) levelSearchButton.addEventListener('click', searchLevelUser);
if (levelSearchInput) {
levelSearchInput.addEventListener('keydown', function (event) {
if (event.key === 'Enter') searchLevelUser();
});
}
if (confirmButton) confirmButton.addEventListener('click', confirmBlockUser);
if (confirmLevelButton) {
confirmLevelButton.addEventListener('click', confirmLevelUpdate);
}
document
.querySelectorAll('#durationGrid [data-duration-ms]')
.forEach(function (button) {
button.addEventListener('click', function () {
selectDuration(
button.getAttribute('data-duration-ms'),
button.getAttribute('data-duration-label')
);
});
});
document.querySelectorAll('#levelGrid [data-level]').forEach(function (button) {
button.addEventListener('click', function () {
selectLevel(button.getAttribute('data-level'));
});
});
}
function init() {
bindEvents();
renderBlockedList();
loadOverview();
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({ page: 'manager-center' });
}
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

View File

@ -0,0 +1,590 @@
.manager-panel {
position: relative;
display: grid;
gap: 14px;
padding: 16px;
border: 1px solid rgba(125, 87, 199, 0.06);
}
.manager-center.is-loading .manager-grid {
min-height: 410px;
opacity: 0;
pointer-events: none;
}
.manager-center.is-loading .manager-panel::after {
position: absolute;
inset: 16px;
content: "";
border-radius: 10px;
background:
linear-gradient(90deg, transparent, rgba(255, 255, 255, 0.72), transparent)
0 0 / 180px 100% no-repeat,
linear-gradient(#f1e7ff, #f1e7ff) 0 0 / 100% 132px no-repeat,
linear-gradient(#f1e7ff, #f1e7ff) 0 144px / 100% 132px no-repeat,
linear-gradient(#f1e7ff, #f1e7ff) 0 288px / 100% 104px no-repeat;
animation: managerSkeleton 1.1s linear infinite;
}
.manager-center.is-denied .manager-grid {
opacity: 0.46;
pointer-events: none;
}
@keyframes managerSkeleton {
0% {
background-position:
-180px 0,
0 0,
0 144px,
0 288px;
}
100% {
background-position:
calc(100% + 180px) 0,
0 0,
0 144px,
0 288px;
}
}
.manager-grid {
display: grid;
grid-template-columns: repeat(2, minmax(0, 1fr));
gap: 10px;
}
.manager-tile {
position: relative;
display: flex;
flex-direction: column;
justify-content: space-between;
min-height: 132px;
overflow: hidden;
padding: 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fbfcfc;
box-shadow:
inset 0 1px 0 rgba(255, 255, 255, 0.95),
0 6px 16px rgba(99, 69, 159, 0.05);
text-align: left;
}
.manager-tile:active {
transform: translateY(1px);
}
.manager-tile:focus-visible {
outline: 2px solid rgba(125, 87, 199, 0.28);
outline-offset: 2px;
}
.manager-tile-wide {
grid-column: 1 / -1;
grid-template-columns: 92px minmax(0, 1fr);
flex-direction: row;
min-height: 104px;
align-items: center;
justify-content: flex-start;
gap: 16px;
}
.tile-title {
position: relative;
z-index: 1;
order: -1;
align-self: start;
min-width: 0;
overflow-wrap: anywhere;
color: var(--text);
font-size: 17px;
font-weight: 950;
letter-spacing: 0;
line-height: 1.16;
}
.manager-tile-wide .tile-title {
order: 0;
align-self: center;
font-size: 19px;
}
.tile-icon {
position: relative;
z-index: 1;
display: flex;
align-items: center;
justify-content: center;
justify-self: center;
width: 72px;
height: 72px;
margin: 14px auto 0;
border-radius: 8px;
}
.manager-tile-wide .tile-icon {
flex: 0 0 72px;
justify-self: start;
margin: 0;
}
.tile-icon svg {
width: 58px;
height: 58px;
}
.tile-icon path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-linejoin: round;
stroke-width: 2.8;
}
.level-icon {
color: #7d57c7;
background: #f4ebff;
}
.frame-icon {
color: #7d57c7;
background: #f4ebff;
}
.car-icon {
color: #4e78d7;
background: #eef4ff;
}
.vip-icon {
color: #d5a032;
background: #fff7dc;
}
.block-icon {
color: #d95d72;
background: #fff1f4;
}
.block-modal[hidden] {
display: none;
}
.block-modal {
position: fixed;
inset: 0;
z-index: 80;
display: flex;
align-items: flex-end;
justify-content: center;
padding: 18px;
}
.block-backdrop {
position: absolute;
inset: 0;
border: 0;
background: rgba(24, 21, 33, 0.48);
}
.block-dialog {
position: relative;
z-index: 1;
display: flex;
width: min(100%, 430px);
max-height: min(86dvh, 720px);
flex-direction: column;
overflow: hidden;
border-radius: 18px;
background: #fff;
box-shadow: 0 -8px 34px rgba(43, 34, 65, 0.18);
}
.block-dialog-header {
display: flex;
min-height: 64px;
align-items: center;
justify-content: space-between;
padding: 16px 18px;
border-bottom: 1px solid var(--line);
}
.block-dialog-header h2 {
margin: 0;
color: var(--text);
font-size: 20px;
font-weight: 950;
letter-spacing: 0;
line-height: 1.1;
}
.block-close {
display: flex;
width: 40px;
height: 40px;
align-items: center;
justify-content: center;
border: 0;
border-radius: 50%;
background: var(--primary-soft);
color: var(--primary-deep);
}
.block-close svg {
width: 22px;
height: 22px;
}
.block-close path {
fill: none;
stroke: currentColor;
stroke-linecap: round;
stroke-width: 3;
}
.block-dialog-body {
min-height: 0;
overflow-y: auto;
padding: 16px 18px 20px;
}
.block-field {
display: grid;
gap: 8px;
}
.block-field label,
.block-section-title {
color: var(--text);
font-size: 14px;
font-weight: 900;
line-height: 1.2;
}
.block-search-row {
display: grid;
grid-template-columns: minmax(0, 1fr) 100px;
gap: 10px;
}
.block-search-row input {
min-width: 0;
height: 48px;
padding: 0 14px;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(252, 249, 255, 0.86);
color: var(--text);
font-size: 15px;
font-weight: 800;
outline: none;
}
.block-search-row input:focus {
border-color: rgba(125, 87, 199, 0.42);
box-shadow: 0 0 0 3px rgba(125, 87, 199, 0.12);
}
.block-search-row input::placeholder {
color: rgba(42, 38, 57, 0.38);
}
.block-search-row button,
.confirm-block-button,
.confirm-level-button,
.user-level-button,
.user-unblock-button,
.user-block-button {
border: 0;
border-radius: 8px;
background: var(--button);
color: #fff;
font-size: 14px;
font-weight: 950;
}
.block-search-row button {
height: 48px;
}
.block-search-result {
min-height: 28px;
margin-top: 12px;
}
.searched-user,
.blocked-row {
display: grid;
grid-template-columns: 48px minmax(0, 1fr) auto;
gap: 12px;
align-items: center;
border: 1px solid var(--line);
border-radius: 8px;
background: rgba(255, 255, 255, 0.94);
}
.searched-user {
padding: 10px;
}
.searched-avatar,
.blocked-avatar {
overflow: hidden;
display: flex;
width: 48px;
height: 48px;
align-items: center;
justify-content: center;
border-radius: 50%;
background: var(--primary);
color: var(--primary-deep);
font-size: 18px;
font-weight: 950;
}
.searched-avatar img,
.blocked-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.searched-name,
.blocked-name {
overflow: hidden;
color: var(--text);
font-size: 15px;
font-weight: 950;
line-height: 1.2;
text-overflow: ellipsis;
white-space: nowrap;
}
.searched-meta,
.blocked-meta,
.blocked-time {
margin-top: 3px;
overflow: hidden;
color: var(--muted);
font-size: 12px;
font-weight: 800;
line-height: 1.25;
text-overflow: ellipsis;
white-space: nowrap;
}
.user-block-button {
min-width: 70px;
height: 40px;
padding: 0 14px;
background: #d95d72;
}
.user-unblock-button {
min-width: 76px;
height: 38px;
padding: 0 12px;
border: 1px solid rgba(125, 87, 199, 0.22);
background: #fff;
color: var(--primary-deep);
font-size: 12px;
}
.block-duration-panel {
display: grid;
gap: 12px;
margin-top: 14px;
padding: 14px;
border: 1px solid rgba(125, 87, 199, 0.12);
border-radius: 10px;
background: rgba(248, 242, 255, 0.68);
}
.block-duration-panel[hidden] {
display: none;
}
.level-panel {
display: grid;
gap: 12px;
margin-top: 14px;
padding: 14px;
border: 1px solid rgba(125, 87, 199, 0.12);
border-radius: 10px;
background: rgba(248, 242, 255, 0.68);
}
.level-panel[hidden] {
display: none;
}
.duration-grid,
.track-grid,
.grant-option-grid {
display: grid;
grid-template-columns: repeat(3, minmax(0, 1fr));
gap: 8px;
}
.level-grid {
display: grid;
grid-template-columns: repeat(4, minmax(0, 1fr));
gap: 8px;
}
.duration-grid button,
.level-grid button,
.track-grid button,
.grant-option-grid button {
min-height: 38px;
padding: 0 8px;
border: 1px solid var(--line);
border-radius: 8px;
background: #fff;
color: var(--muted);
font-size: 12px;
font-weight: 900;
}
.duration-grid button.is-selected,
.level-grid button.is-selected,
.track-grid button.is-selected,
.grant-option-grid button.is-selected {
border-color: var(--primary-deep);
background: var(--primary-deep);
color: #fff;
}
.grant-option-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.grant-resource-option {
overflow: hidden;
text-overflow: ellipsis;
white-space: nowrap;
}
.confirm-block-button,
.confirm-level-button {
height: 46px;
}
.confirm-block-button:disabled,
.confirm-level-button:disabled {
background: var(--button-disabled);
color: rgba(42, 38, 57, 0.42);
}
.block-list-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 18px;
padding-top: 14px;
border-top: 1px solid var(--line);
}
.block-list-count {
min-width: 28px;
height: 24px;
padding: 0 8px;
border-radius: 999px;
background: var(--primary-soft);
color: var(--primary-deep);
font-size: 12px;
font-weight: 950;
line-height: 24px;
text-align: center;
}
.blocked-list {
display: grid;
max-height: 216px;
margin-top: 10px;
overflow-y: auto;
gap: 10px;
padding-right: 2px;
}
.blocked-row {
min-height: 76px;
padding: 10px;
}
.blocked-row-main {
min-width: 0;
}
.blocked-time {
color: #d95d72;
}
.blocked-empty {
display: flex;
min-height: 92px;
align-items: center;
justify-content: center;
border: 1px dashed var(--line);
border-radius: 8px;
color: var(--muted);
font-size: 13px;
font-weight: 850;
text-align: center;
}
@media (max-width: 360px) {
.manager-panel {
padding: 16px;
}
.manager-grid {
gap: 10px;
}
.manager-tile {
min-height: 126px;
padding: 12px 10px 14px;
}
.tile-title {
font-size: 16px;
}
.tile-icon {
width: 68px;
height: 68px;
}
.tile-icon svg {
width: 52px;
height: 52px;
}
.block-modal {
padding: 12px;
}
.block-dialog-header {
min-height: 58px;
padding: 14px 16px;
}
.block-dialog-body {
padding: 14px 16px 18px;
}
.block-search-row {
grid-template-columns: minmax(0, 1fr) 88px;
gap: 8px;
}
.duration-grid {
grid-template-columns: repeat(2, minmax(0, 1fr));
}
.level-grid {
grid-template-columns: repeat(3, minmax(0, 1fr));
}
}