feat: update h5 guild center pages
This commit is contained in:
parent
d5bc7ee440
commit
ae39abaa02
11
.claude/launch.json
Normal file
11
.claude/launch.json
Normal file
@ -0,0 +1,11 @@
|
||||
{
|
||||
"version": "0.0.1",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "h5-static",
|
||||
"runtimeExecutable": "npx",
|
||||
"runtimeArgs": ["--yes", "http-server", "-p", "18080", "-c-1"],
|
||||
"port": 18080
|
||||
}
|
||||
]
|
||||
}
|
||||
@ -34,6 +34,10 @@ button {
|
||||
}
|
||||
|
||||
.yumi-cp-stage {
|
||||
--stage-height: 812px;
|
||||
--design-width: 375px;
|
||||
--design-height: 812px;
|
||||
--design-scale: 1;
|
||||
--accent: #ff0979;
|
||||
--accent-soft: #ff5762;
|
||||
--tab-bg: rgba(94, 13, 35, 0.8);
|
||||
@ -47,14 +51,22 @@ button {
|
||||
--progress-to: #ffcf67;
|
||||
--exp-pill-bg: rgba(255, 102, 169, 0.74);
|
||||
position: relative;
|
||||
width: min(100vw, 375px);
|
||||
height: min(812px, 216.533333vw);
|
||||
max-height: 812px;
|
||||
width: 100%;
|
||||
height: var(--stage-height);
|
||||
overflow: hidden;
|
||||
background: #440b23;
|
||||
flex: 0 0 auto;
|
||||
}
|
||||
|
||||
.yumi-stage-design {
|
||||
/* 资源按 375x812 设计稿导出;统一缩放内层画布,避免逐个重算绝对定位和素材尺寸。 */
|
||||
position: relative;
|
||||
width: var(--design-width);
|
||||
height: var(--design-height);
|
||||
transform: scale(var(--design-scale));
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
.yumi-cp-page[data-tab='brother'] .yumi-cp-stage {
|
||||
--accent: #5709ff;
|
||||
--accent-soft: #5709ff;
|
||||
@ -108,7 +120,6 @@ button {
|
||||
object-fit: fill;
|
||||
}
|
||||
|
||||
.yumi-status,
|
||||
.yumi-nav,
|
||||
.yumi-tabs,
|
||||
.yumi-card {
|
||||
@ -116,146 +127,9 @@ button {
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.yumi-status {
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 375px;
|
||||
height: 44px;
|
||||
color: #fff;
|
||||
pointer-events: none;
|
||||
transform-origin: left top;
|
||||
}
|
||||
|
||||
.yumi-status-time {
|
||||
position: absolute;
|
||||
left: 33.87px;
|
||||
top: 13px;
|
||||
width: 54px;
|
||||
font-family: Helvetica, Arial, sans-serif;
|
||||
font-size: 15px;
|
||||
line-height: 15px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.yumi-status-cellular {
|
||||
position: absolute;
|
||||
left: 294.34px;
|
||||
top: 15px;
|
||||
display: flex;
|
||||
align-items: flex-end;
|
||||
gap: 2px;
|
||||
width: 17px;
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.yumi-status-cellular i {
|
||||
display: block;
|
||||
width: 3px;
|
||||
border-radius: 2px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.yumi-status-cellular i:nth-child(1) {
|
||||
height: 4px;
|
||||
}
|
||||
|
||||
.yumi-status-cellular i:nth-child(2) {
|
||||
height: 7px;
|
||||
}
|
||||
|
||||
.yumi-status-cellular i:nth-child(3) {
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.yumi-status-cellular i:nth-child(4) {
|
||||
height: 12px;
|
||||
}
|
||||
|
||||
.yumi-status-wifi {
|
||||
position: absolute;
|
||||
left: 316.34px;
|
||||
top: 16px;
|
||||
width: 16px;
|
||||
height: 12px;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.yumi-status-wifi::before,
|
||||
.yumi-status-wifi::after,
|
||||
.yumi-status-wifi i {
|
||||
position: absolute;
|
||||
left: 50%;
|
||||
border: 2px solid #fff;
|
||||
border-right-color: transparent;
|
||||
border-bottom-color: transparent;
|
||||
border-left-color: transparent;
|
||||
border-radius: 50%;
|
||||
content: '';
|
||||
transform: translateX(-50%);
|
||||
}
|
||||
|
||||
.yumi-status-wifi::before {
|
||||
top: 0;
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
}
|
||||
|
||||
.yumi-status-wifi::after {
|
||||
top: 4px;
|
||||
width: 10px;
|
||||
height: 10px;
|
||||
}
|
||||
|
||||
.yumi-status-wifi i {
|
||||
top: 8px;
|
||||
width: 4px;
|
||||
height: 4px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.yumi-status-battery {
|
||||
position: absolute;
|
||||
left: 336.67px;
|
||||
top: 16.33px;
|
||||
width: 24.33px;
|
||||
height: 11.33px;
|
||||
}
|
||||
|
||||
.yumi-status-battery::before {
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
width: 21px;
|
||||
height: 11px;
|
||||
border: 1.2px solid #fff;
|
||||
border-radius: 2.7px;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.yumi-status-battery::after {
|
||||
position: absolute;
|
||||
right: 0;
|
||||
top: 3.4px;
|
||||
width: 1.4px;
|
||||
height: 4.5px;
|
||||
border-radius: 0 1px 1px 0;
|
||||
background: #fff;
|
||||
content: '';
|
||||
}
|
||||
|
||||
.yumi-status-battery i {
|
||||
position: absolute;
|
||||
left: 2.3px;
|
||||
top: 2.3px;
|
||||
width: 17px;
|
||||
height: 6.8px;
|
||||
border-radius: 1.5px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.yumi-nav {
|
||||
left: 0;
|
||||
top: 44px;
|
||||
top: 0;
|
||||
width: 375px;
|
||||
height: 44px;
|
||||
}
|
||||
@ -318,7 +192,7 @@ button {
|
||||
|
||||
.yumi-tabs {
|
||||
left: 16px;
|
||||
top: 109px;
|
||||
top: 65px;
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, 1fr);
|
||||
width: 343px;
|
||||
@ -362,7 +236,7 @@ button {
|
||||
|
||||
.yumi-card {
|
||||
left: 14px;
|
||||
top: 169px;
|
||||
top: 125px;
|
||||
width: 347px;
|
||||
height: 199px;
|
||||
color: var(--accent);
|
||||
@ -636,13 +510,3 @@ button {
|
||||
[dir='rtl'] .yumi-tabs {
|
||||
direction: ltr;
|
||||
}
|
||||
|
||||
@media (max-width: 374.98px) {
|
||||
.yumi-status,
|
||||
.yumi-nav,
|
||||
.yumi-tabs,
|
||||
.yumi-card {
|
||||
transform: scale(calc(100vw / 375));
|
||||
transform-origin: left top;
|
||||
}
|
||||
}
|
||||
|
||||
@ -12,7 +12,7 @@
|
||||
rel="stylesheet"
|
||||
href="../../common/theme.css?v=20260708-yumi-layered"
|
||||
/>
|
||||
<link rel="stylesheet" href="./yumi.css?v=20260708-yumi-layered" />
|
||||
<link rel="stylesheet" href="./yumi.css?v=20260708-yumi-fullwidth" />
|
||||
</head>
|
||||
<body>
|
||||
<main
|
||||
@ -27,173 +27,176 @@
|
||||
aria-label="Best Friend"
|
||||
data-i18n-aria="cpSpace.pageLabel"
|
||||
>
|
||||
<img
|
||||
class="yumi-bg-layer"
|
||||
id="yumiBgLayer"
|
||||
src="./assets/yumi/layers/bg-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
<div class="yumi-status" aria-hidden="true">
|
||||
<span class="yumi-status-time">9:41</span>
|
||||
<span class="yumi-status-cellular"
|
||||
><i></i><i></i><i></i><i></i
|
||||
></span>
|
||||
<span class="yumi-status-wifi"><i></i></span>
|
||||
<span class="yumi-status-battery"><i></i></span>
|
||||
</div>
|
||||
|
||||
<nav
|
||||
class="yumi-nav"
|
||||
aria-label="Best Friend navigation"
|
||||
data-i18n-aria="cpSpace.navLabel"
|
||||
>
|
||||
<button
|
||||
class="yumi-nav-button yumi-nav-button--back"
|
||||
id="yumiBackButton"
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
data-i18n-aria="cpSpace.back"
|
||||
></button>
|
||||
<h1 class="yumi-title" data-i18n="cpSpace.title">
|
||||
Best Friend
|
||||
</h1>
|
||||
<button
|
||||
class="yumi-nav-button yumi-nav-button--help"
|
||||
id="yumiHelpButton"
|
||||
type="button"
|
||||
aria-label="Help"
|
||||
data-i18n-aria="cpSpace.help"
|
||||
></button>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
class="yumi-tabs"
|
||||
aria-label="Best Friend tabs"
|
||||
data-i18n-aria="cpSpace.tabsLabel"
|
||||
>
|
||||
<div class="yumi-stage-design">
|
||||
<img
|
||||
class="yumi-tab-jewel"
|
||||
id="yumiTabJewel"
|
||||
src="./assets/yumi/layers/tab-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<button class="yumi-tab-button" type="button" data-tab="cp">
|
||||
<span data-i18n="cpSpace.tabCp">CP</span>
|
||||
</button>
|
||||
<button
|
||||
class="yumi-tab-button"
|
||||
type="button"
|
||||
data-tab="brother"
|
||||
>
|
||||
<span data-i18n="cpSpace.tabBrother">Brother</span>
|
||||
</button>
|
||||
<button
|
||||
class="yumi-tab-button"
|
||||
type="button"
|
||||
data-tab="sister"
|
||||
>
|
||||
<span data-i18n="cpSpace.tabSister">Sister</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<article class="yumi-card" aria-label="Best friend level 1">
|
||||
<img
|
||||
class="yumi-card-skin"
|
||||
id="yumiCardSkin"
|
||||
src="./assets/yumi/layers/card-cp.png"
|
||||
class="yumi-bg-layer"
|
||||
id="yumiBgLayer"
|
||||
src="./assets/yumi/layers/bg-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
<figure class="yumi-avatar yumi-avatar--left">
|
||||
<span
|
||||
class="yumi-avatar-photo"
|
||||
id="yumiLeftAvatar"
|
||||
></span>
|
||||
<img
|
||||
class="yumi-avatar-frame"
|
||||
src="./assets/yumi/layers/avatar-frame.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<figure class="yumi-avatar yumi-avatar--right">
|
||||
<span
|
||||
class="yumi-avatar-photo"
|
||||
id="yumiRightAvatar"
|
||||
></span>
|
||||
<img
|
||||
class="yumi-avatar-frame"
|
||||
src="./assets/yumi/layers/avatar-frame.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<div class="yumi-center-art-wrap" id="yumiCenterWrap">
|
||||
<img
|
||||
class="yumi-center-art"
|
||||
id="yumiCenterArt"
|
||||
src="./assets/yumi/layers/center-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<strong class="yumi-days-value" id="yumiDaysValue"
|
||||
>999</strong
|
||||
>
|
||||
<span
|
||||
class="yumi-days-unit"
|
||||
data-i18n="cpSpace.daysUnit"
|
||||
>Days</span
|
||||
>
|
||||
</div>
|
||||
|
||||
<div class="yumi-exp-pill">
|
||||
<span id="yumiExpAway">888 Exp away from Lv. 2</span>
|
||||
</div>
|
||||
|
||||
<section
|
||||
class="yumi-progress-block"
|
||||
aria-label="1000/200000"
|
||||
<nav
|
||||
class="yumi-nav"
|
||||
aria-label="Best Friend navigation"
|
||||
data-i18n-aria="cpSpace.navLabel"
|
||||
>
|
||||
<div class="yumi-level-badge yumi-level-badge--left">
|
||||
<button
|
||||
class="yumi-nav-button yumi-nav-button--back"
|
||||
id="yumiBackButton"
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
data-i18n-aria="cpSpace.back"
|
||||
></button>
|
||||
<h1 class="yumi-title" data-i18n="cpSpace.title">
|
||||
Best Friend
|
||||
</h1>
|
||||
<button
|
||||
class="yumi-nav-button yumi-nav-button--help"
|
||||
id="yumiHelpButton"
|
||||
type="button"
|
||||
aria-label="Help"
|
||||
data-i18n-aria="cpSpace.help"
|
||||
></button>
|
||||
</nav>
|
||||
|
||||
<nav
|
||||
class="yumi-tabs"
|
||||
aria-label="Best Friend tabs"
|
||||
data-i18n-aria="cpSpace.tabsLabel"
|
||||
>
|
||||
<img
|
||||
class="yumi-tab-jewel"
|
||||
id="yumiTabJewel"
|
||||
src="./assets/yumi/layers/tab-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<button
|
||||
class="yumi-tab-button"
|
||||
type="button"
|
||||
data-tab="cp"
|
||||
>
|
||||
<span data-i18n="cpSpace.tabCp">CP</span>
|
||||
</button>
|
||||
<button
|
||||
class="yumi-tab-button"
|
||||
type="button"
|
||||
data-tab="brother"
|
||||
>
|
||||
<span data-i18n="cpSpace.tabBrother">Brother</span>
|
||||
</button>
|
||||
<button
|
||||
class="yumi-tab-button"
|
||||
type="button"
|
||||
data-tab="sister"
|
||||
>
|
||||
<span data-i18n="cpSpace.tabSister">Sister</span>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<article class="yumi-card" aria-label="Best friend level 1">
|
||||
<img
|
||||
class="yumi-card-skin"
|
||||
id="yumiCardSkin"
|
||||
src="./assets/yumi/layers/card-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
|
||||
<figure class="yumi-avatar yumi-avatar--left">
|
||||
<span
|
||||
class="yumi-avatar-photo"
|
||||
id="yumiLeftAvatar"
|
||||
></span>
|
||||
<img
|
||||
class="yumi-level-bg"
|
||||
src="./assets/yumi/layers/level-cp.png"
|
||||
class="yumi-avatar-frame"
|
||||
src="./assets/yumi/layers/avatar-frame.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span id="yumiCurrentLevel">Lv1</span>
|
||||
</div>
|
||||
<div class="yumi-progress-track">
|
||||
</figure>
|
||||
|
||||
<figure class="yumi-avatar yumi-avatar--right">
|
||||
<span
|
||||
class="yumi-progress-fill"
|
||||
id="yumiProgressFill"
|
||||
class="yumi-avatar-photo"
|
||||
id="yumiRightAvatar"
|
||||
></span>
|
||||
<strong
|
||||
class="yumi-progress-text"
|
||||
id="yumiProgressText"
|
||||
>1000/200000</strong
|
||||
<img
|
||||
class="yumi-avatar-frame"
|
||||
src="./assets/yumi/layers/avatar-frame.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
</figure>
|
||||
|
||||
<div class="yumi-center-art-wrap" id="yumiCenterWrap">
|
||||
<img
|
||||
class="yumi-center-art"
|
||||
id="yumiCenterArt"
|
||||
src="./assets/yumi/layers/center-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<strong class="yumi-days-value" id="yumiDaysValue"
|
||||
>999</strong
|
||||
>
|
||||
<span
|
||||
class="yumi-days-unit"
|
||||
data-i18n="cpSpace.daysUnit"
|
||||
>Days</span
|
||||
>
|
||||
</div>
|
||||
<div class="yumi-level-badge yumi-level-badge--right">
|
||||
<img
|
||||
class="yumi-level-bg"
|
||||
src="./assets/yumi/layers/level-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span id="yumiNextLevel">Lv1</span>
|
||||
|
||||
<div class="yumi-exp-pill">
|
||||
<span id="yumiExpAway"
|
||||
>888 Exp away from Lv. 2</span
|
||||
>
|
||||
</div>
|
||||
<p class="yumi-time-text" id="yumiTimeText">
|
||||
Time spent together: 4 days
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
|
||||
<section
|
||||
class="yumi-progress-block"
|
||||
aria-label="1000/200000"
|
||||
>
|
||||
<div
|
||||
class="yumi-level-badge yumi-level-badge--left"
|
||||
>
|
||||
<img
|
||||
class="yumi-level-bg"
|
||||
src="./assets/yumi/layers/level-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span id="yumiCurrentLevel">Lv1</span>
|
||||
</div>
|
||||
<div class="yumi-progress-track">
|
||||
<span
|
||||
class="yumi-progress-fill"
|
||||
id="yumiProgressFill"
|
||||
></span>
|
||||
<strong
|
||||
class="yumi-progress-text"
|
||||
id="yumiProgressText"
|
||||
>1000/200000</strong
|
||||
>
|
||||
</div>
|
||||
<div
|
||||
class="yumi-level-badge yumi-level-badge--right"
|
||||
>
|
||||
<img
|
||||
class="yumi-level-bg"
|
||||
src="./assets/yumi/layers/level-cp.png"
|
||||
alt=""
|
||||
draggable="false"
|
||||
/>
|
||||
<span id="yumiNextLevel">Lv1</span>
|
||||
</div>
|
||||
<p class="yumi-time-text" id="yumiTimeText">
|
||||
Time spent together: 4 days
|
||||
</p>
|
||||
</section>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
@ -201,6 +204,7 @@
|
||||
<script src="../../common/api.js?v=20260708-yumi-layered"></script>
|
||||
<script src="../../common/jsbridge.js?v=20260708-yumi-layered"></script>
|
||||
<script src="../../common/toast.js?v=20260708-yumi-layered"></script>
|
||||
<script src="./yumi.js?v=20260708-yumi-layered"></script>
|
||||
<script src="../../yumi_commmon/yumi-common.js?v=20260708-yumi-api"></script>
|
||||
<script src="./yumi.js?v=20260708-yumi-api"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -25,20 +25,21 @@
|
||||
},
|
||||
};
|
||||
var DEFAULT_RELATION = {
|
||||
days: 999,
|
||||
togetherDays: 4,
|
||||
currentLevel: 1,
|
||||
nextLevel: 1,
|
||||
currentExp: 1000,
|
||||
nextExp: 200000,
|
||||
neededExp: 888,
|
||||
nextExpLevel: 2,
|
||||
progressPercent: 22.3,
|
||||
days: 0,
|
||||
togetherDays: 0,
|
||||
currentLevel: 0,
|
||||
nextLevel: 0,
|
||||
currentExp: 0,
|
||||
nextExp: 0,
|
||||
neededExp: 0,
|
||||
nextExpLevel: 0,
|
||||
progressPercent: 0,
|
||||
me: { avatar: '' },
|
||||
partner: { avatar: '' },
|
||||
};
|
||||
|
||||
var page = document.getElementById('yumiCpPage');
|
||||
var stage = document.querySelector('.yumi-cp-stage');
|
||||
var backButton = document.getElementById('yumiBackButton');
|
||||
var helpButton = document.getElementById('yumiHelpButton');
|
||||
var tabButtons = Array.prototype.slice.call(
|
||||
@ -63,7 +64,10 @@
|
||||
var state = {
|
||||
activeTab: normalizeTab(readParam('tab')) || 'cp',
|
||||
data: clone(DEFAULT_RELATION),
|
||||
tabData: {},
|
||||
requestSeq: 0,
|
||||
};
|
||||
var stageResizeObserver = null;
|
||||
|
||||
function clone(value) {
|
||||
return JSON.parse(JSON.stringify(value));
|
||||
@ -86,6 +90,15 @@
|
||||
return Number.isFinite(number) ? number : fallback || 0;
|
||||
}
|
||||
|
||||
function firstValue() {
|
||||
for (var i = 0; i < arguments.length; i += 1) {
|
||||
var value = arguments[i];
|
||||
if (value !== undefined && value !== null && value !== '')
|
||||
return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function t(key, fallback) {
|
||||
if (window.HyAppI18n && window.HyAppI18n.t) {
|
||||
return window.HyAppI18n.t(key, fallback);
|
||||
@ -102,6 +115,28 @@
|
||||
return LAYER_BASE + name;
|
||||
}
|
||||
|
||||
function syncStageScale() {
|
||||
if (!stage) return;
|
||||
var width = stage.getBoundingClientRect().width || window.innerWidth;
|
||||
var scale = Math.max(0.1, width / 375);
|
||||
// Yumi 素材和定位都是 375x812 设计稿坐标;只同步一个缩放值,避免多处百分比换算造成错位。
|
||||
stage.style.setProperty('--design-scale', String(scale));
|
||||
stage.style.setProperty('--stage-height', 812 * scale + 'px');
|
||||
}
|
||||
|
||||
function bindStageScale() {
|
||||
syncStageScale();
|
||||
if (!stage) return;
|
||||
if (window.ResizeObserver) {
|
||||
stageResizeObserver = new ResizeObserver(syncStageScale);
|
||||
stageResizeObserver.observe(stage);
|
||||
return;
|
||||
}
|
||||
// 低版本 WebView 没有 ResizeObserver,只能监听窗口变化来覆盖旋转和容器宽度变更。
|
||||
window.addEventListener('resize', syncStageScale);
|
||||
window.addEventListener('orientationchange', syncStageScale);
|
||||
}
|
||||
|
||||
function syncURL(tab) {
|
||||
if (!window.history || !window.history.replaceState) return;
|
||||
var url = new URL(window.location.href);
|
||||
@ -132,28 +167,31 @@
|
||||
return {
|
||||
days: readNumber(data.days, DEFAULT_RELATION.days),
|
||||
togetherDays: readNumber(
|
||||
data.togetherDays || data.together_days,
|
||||
firstValue(data.togetherDays, data.together_days),
|
||||
DEFAULT_RELATION.togetherDays
|
||||
),
|
||||
currentLevel: readNumber(
|
||||
data.currentLevel || data.current_level || data.level,
|
||||
1
|
||||
firstValue(data.currentLevel, data.current_level, data.level),
|
||||
DEFAULT_RELATION.currentLevel
|
||||
),
|
||||
nextLevel: readNumber(
|
||||
firstValue(data.nextLevel, data.next_level),
|
||||
DEFAULT_RELATION.nextLevel
|
||||
),
|
||||
nextLevel: readNumber(data.nextLevel || data.next_level, 1),
|
||||
currentExp: readNumber(
|
||||
data.currentExp || data.current_exp,
|
||||
firstValue(data.currentExp, data.current_exp),
|
||||
DEFAULT_RELATION.currentExp
|
||||
),
|
||||
nextExp: readNumber(
|
||||
data.nextExp || data.next_exp,
|
||||
firstValue(data.nextExp, data.next_exp),
|
||||
DEFAULT_RELATION.nextExp
|
||||
),
|
||||
neededExp: readNumber(
|
||||
data.neededExp || data.needed_exp,
|
||||
firstValue(data.neededExp, data.needed_exp),
|
||||
DEFAULT_RELATION.neededExp
|
||||
),
|
||||
nextExpLevel: readNumber(
|
||||
data.nextExpLevel || data.next_exp_level,
|
||||
firstValue(data.nextExpLevel, data.next_exp_level),
|
||||
DEFAULT_RELATION.nextExpLevel
|
||||
),
|
||||
progressPercent: Math.max(
|
||||
@ -161,7 +199,7 @@
|
||||
Math.min(
|
||||
100,
|
||||
readNumber(
|
||||
data.progressPercent || data.progress_percent,
|
||||
firstValue(data.progressPercent, data.progress_percent),
|
||||
DEFAULT_RELATION.progressPercent
|
||||
)
|
||||
)
|
||||
@ -237,18 +275,59 @@
|
||||
var data = payload || {};
|
||||
var tabData = data[state.activeTab] || data.relation || data;
|
||||
state.data = normalizedRelation(tabData);
|
||||
state.tabData[state.activeTab] = state.data;
|
||||
renderCopy();
|
||||
}
|
||||
|
||||
function showTabData(tab) {
|
||||
state.data = clone(state.tabData[tab] || DEFAULT_RELATION);
|
||||
renderCopy();
|
||||
}
|
||||
|
||||
function loadTabData(tab) {
|
||||
var targetTab = normalizeTab(tab) || 'cp';
|
||||
if (!window.YumiCommon || !window.YumiCommon.cpSpace) {
|
||||
return Promise.resolve();
|
||||
}
|
||||
var seq = (state.requestSeq += 1);
|
||||
// 数据由 Java 的 CP 关系接口按 relationType 拉取;seq 防止用户快速切 tab 时旧响应覆盖新主题。
|
||||
return window.YumiCommon.cpSpace
|
||||
.loadRelation({
|
||||
tab: targetTab,
|
||||
userId: readParam('userId') || readParam('user_id') || '',
|
||||
cpUserId:
|
||||
readParam('cpUserId') || readParam('cp_user_id') || '',
|
||||
})
|
||||
.then(function (data) {
|
||||
if (seq !== state.requestSeq) return;
|
||||
state.tabData[targetTab] = normalizedRelation(data);
|
||||
if (state.activeTab === targetTab) showTabData(targetTab);
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (seq !== state.requestSeq) return;
|
||||
// WebView 首屏不能因为 token、网关或关系为空白屏;失败保留当前可交互页面,问题交给日志定位。
|
||||
if (window.console && console.warn) {
|
||||
console.warn('Yumi CP relation load failed:', error);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
tabButtons.forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
setActiveTab(button.getAttribute('data-tab'));
|
||||
var tab = button.getAttribute('data-tab');
|
||||
setActiveTab(tab);
|
||||
showTabData(state.activeTab);
|
||||
loadTabData(state.activeTab);
|
||||
});
|
||||
});
|
||||
|
||||
if (backButton) {
|
||||
backButton.addEventListener('click', function () {
|
||||
if (window.YumiCommon && window.YumiCommon.back) {
|
||||
window.YumiCommon.back();
|
||||
return;
|
||||
}
|
||||
if (window.HyAppBridge && window.HyAppBridge.back) {
|
||||
window.HyAppBridge.back();
|
||||
return;
|
||||
@ -274,13 +353,16 @@
|
||||
});
|
||||
}
|
||||
|
||||
bindStageScale();
|
||||
bindEvents();
|
||||
if (window.HyAppCPSpaceYumiData)
|
||||
applyExternalData(window.HyAppCPSpaceYumiData);
|
||||
setActiveTab(state.activeTab, { syncURL: false });
|
||||
showTabData(state.activeTab);
|
||||
loadTabData(state.activeTab);
|
||||
|
||||
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
||||
// ready 只声明页面能力;真实关系数据由客户端事件或网关注入,避免 H5 自行判断关系状态。
|
||||
// ready 只声明页面能力;真实关系数据由 YumiCommon 接口层拉取,客户端仍可通过事件覆盖。
|
||||
window.HyAppBridge.ready({ page: 'yumi-cp-space' });
|
||||
}
|
||||
})();
|
||||
|
||||
@ -153,6 +153,14 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
return normalizeBaseURL(window.location.origin + path);
|
||||
}
|
||||
|
||||
function localDevProxyBase(env) {
|
||||
if (!isLocalDevOrigin()) return '';
|
||||
if (env === 'test') return devProxyBase('/__api_test__/');
|
||||
if (env === 'local') return devProxyBase('/__api_local__/');
|
||||
// 本地 H5 调试默认仍访问生产数据,但浏览器只能请求同源代理;由 scripts/h5_dev_server.py 代发到生产 API。
|
||||
return devProxyBase('/__api_prod__/');
|
||||
}
|
||||
|
||||
function resolveEnv() {
|
||||
var env = readQuery('env');
|
||||
if (env === 'test' || env === 'local') {
|
||||
@ -169,10 +177,9 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
var env = resolveEnv();
|
||||
var configuredAPI = activeAppAPIBaseURL(env);
|
||||
if (configuredAPI) return configuredAPI;
|
||||
if (env === 'test')
|
||||
return isLocalDevOrigin()
|
||||
? devProxyBase('/__api_test__/')
|
||||
: normalizeBaseURL(test_api);
|
||||
var devProxy = localDevProxyBase(env);
|
||||
if (devProxy) return devProxy;
|
||||
if (env === 'test') return normalizeBaseURL(test_api);
|
||||
if (env === 'local') return normalizeBaseURL(local_api);
|
||||
return normalizeBaseURL(default_api);
|
||||
}
|
||||
@ -597,6 +604,32 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
body: payload || {},
|
||||
});
|
||||
},
|
||||
getCoinSellerSubSellerPermission: function () {
|
||||
return request(
|
||||
'/api/v1/wallet/coin-seller/sub-sellers/permission',
|
||||
{
|
||||
method: 'GET',
|
||||
}
|
||||
);
|
||||
},
|
||||
inviteCoinSellerSubSeller: function (payload) {
|
||||
return request('/api/v1/wallet/coin-seller/sub-sellers', {
|
||||
method: 'POST',
|
||||
body: payload || {},
|
||||
});
|
||||
},
|
||||
listCoinSellerSubSellers: function (params) {
|
||||
return request('/api/v1/wallet/coin-seller/sub-sellers', {
|
||||
method: 'GET',
|
||||
query: params || {},
|
||||
});
|
||||
},
|
||||
transferCoinSellerToSubSeller: function (payload) {
|
||||
return request('/api/v1/wallet/coin-seller/sub-sellers/transfer', {
|
||||
method: 'POST',
|
||||
body: payload || {},
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// H5 充值 API 只做路径和参数名封装;token、app_code、env、错误 envelope 仍由 request 统一处理。
|
||||
|
||||
104
common/loading.js
Normal file
104
common/loading.js
Normal file
@ -0,0 +1,104 @@
|
||||
(function () {
|
||||
// 通用全屏 loading(拉鲁猫吉祥物 + 转圈)。
|
||||
// 样式复用 common/theme.css 的 .hy-loading 系列类:新页面按仓库约定必须引入 theme.css,
|
||||
// 组件自身只负责 DOM 注入和显隐状态,不重复维护一份样式副本避免两处漂移。
|
||||
// 吉祥物图取 theme.css 默认值(lalu 紫猫动画 webp);其他 app 主题可在
|
||||
// [data-hy-app-code='...'] 下覆盖 .hy-loading__mascot 的 background 换皮。
|
||||
var LOADING_ID = 'hy-global-loading';
|
||||
// show/hide 采用引用计数:并发请求各自 show/hide 时,最后一个 hide 才真正关闭遮罩,
|
||||
// 避免先完成的请求把还在加载的请求的遮罩提前关掉。
|
||||
var visibleCount = 0;
|
||||
|
||||
function ensureNode() {
|
||||
var node = document.getElementById(LOADING_ID);
|
||||
if (node) return node;
|
||||
node = document.createElement('div');
|
||||
node.id = LOADING_ID;
|
||||
node.className = 'hy-loading';
|
||||
node.setAttribute('role', 'status');
|
||||
node.setAttribute('aria-live', 'polite');
|
||||
node.setAttribute('aria-hidden', 'true');
|
||||
|
||||
var mascot = document.createElement('div');
|
||||
mascot.className = 'hy-loading__mascot';
|
||||
mascot.setAttribute('aria-hidden', 'true');
|
||||
|
||||
var spinner = document.createElement('div');
|
||||
spinner.className = 'hy-loading__spinner';
|
||||
spinner.setAttribute('aria-hidden', 'true');
|
||||
|
||||
var text = document.createElement('div');
|
||||
text.className = 'hy-loading__text';
|
||||
text.hidden = true;
|
||||
|
||||
node.appendChild(mascot);
|
||||
node.appendChild(spinner);
|
||||
node.appendChild(text);
|
||||
document.body.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function render() {
|
||||
var node = ensureNode();
|
||||
var active = visibleCount > 0;
|
||||
node.classList.toggle('is-active', active);
|
||||
node.setAttribute('aria-hidden', active ? 'false' : 'true');
|
||||
}
|
||||
|
||||
function setText(node, message) {
|
||||
var text = node.querySelector('.hy-loading__text');
|
||||
if (!text) return;
|
||||
var value = String(message || '').trim();
|
||||
// 文案由调用方传入已本地化的字符串;组件不内置多语言字典(common/i18n.js 归页面消费)。
|
||||
text.textContent = value;
|
||||
text.hidden = !value;
|
||||
}
|
||||
|
||||
function show(options) {
|
||||
if (!document.body) {
|
||||
// 页面脚本可能在 body 就绪前调用;挂到 DOMContentLoaded 重放,保持调用方无需关心时序。
|
||||
document.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
function () {
|
||||
show(options);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
visibleCount += 1;
|
||||
var node = ensureNode();
|
||||
setText(node, options && options.text);
|
||||
render();
|
||||
}
|
||||
|
||||
function hide(force) {
|
||||
if (force) {
|
||||
visibleCount = 0;
|
||||
} else if (visibleCount > 0) {
|
||||
visibleCount -= 1;
|
||||
}
|
||||
if (!document.getElementById(LOADING_ID)) return;
|
||||
render();
|
||||
}
|
||||
|
||||
function isVisible() {
|
||||
return visibleCount > 0;
|
||||
}
|
||||
|
||||
window.HyAppLoading = {
|
||||
show: show,
|
||||
hide: hide,
|
||||
isVisible: isVisible,
|
||||
};
|
||||
|
||||
// 与 HyAppToast 的 hyapp:toast 对齐的事件桥:原生壳或其他脚本可通过事件控制遮罩。
|
||||
window.addEventListener('hyapp:loading', function (event) {
|
||||
var detail = event.detail || {};
|
||||
if (detail.visible === false) {
|
||||
hide(detail.force === true);
|
||||
return;
|
||||
}
|
||||
show(detail);
|
||||
});
|
||||
})();
|
||||
@ -479,6 +479,21 @@
|
||||
"coinseller_center.loading": "جار التحميل...",
|
||||
"coinseller_center.load_failed": "فشل التحميل. حاول مرة أخرى لاحقا.",
|
||||
"coinseller_center.transfer_failed": "فشل التحويل.",
|
||||
"coinseller_center.sub_sellers": "بائعو العملات الفرعيون",
|
||||
"coinseller_center.sub_sellers_desc": "ادع وأدر بائعي العملات الفرعيين المباشرين",
|
||||
"coinseller_center.invite_sub_seller": "دعوة بائع عملات فرعي",
|
||||
"coinseller_center.invite_sub_seller_hint": "أدخل معرف المستخدم لفتح بائع عملات فرعي مباشر.",
|
||||
"coinseller_center.invite_now": "ادع الآن",
|
||||
"coinseller_center.invite_success": "تم إرسال الدعوة.",
|
||||
"coinseller_center.invite_failed": "فشلت الدعوة.",
|
||||
"coinseller_center.no_sub_sellers": "لا يوجد بائعو عملات فرعيون بعد.",
|
||||
"coinseller_center.sub_seller_wallet": "المحفظة",
|
||||
"coinseller_center.transfer": "تحويل",
|
||||
"coinseller_center.transfer_now": "حوّل الآن",
|
||||
"coinseller_center.transfer_success": "تم التحويل بنجاح.",
|
||||
"coinseller_center.transfer_to_sub_seller": "تحويل إلى بائع فرعي",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "حوّل العملات إلى محفظة بائع العملات الفرعي.",
|
||||
"coinseller_center.select_sub_seller_first": "اختر بائع عملات فرعيا أولا.",
|
||||
"lalu_landing.message": "أرسل لك هدية. تعال والعب معي.",
|
||||
"lalu_landing.frame_duration": "3 أيام",
|
||||
"lalu_landing.join_party": "انضم إلى الحفلة",
|
||||
@ -715,5 +730,30 @@
|
||||
"contact_card.required": "أدخل جهة اتصال WhatsApp.",
|
||||
"contact_card.api_unavailable": "واجهة جهة الاتصال غير متاحة.",
|
||||
"contact_card.saved": "تم الحفظ.",
|
||||
"contact_card.save_failed": "فشل الحفظ."
|
||||
"contact_card.save_failed": "فشل الحفظ.",
|
||||
"manager_center.title": "مركز الإدارة",
|
||||
"manager_center.back": "رجوع",
|
||||
"manager_center.change_language": "تغيير اللغة",
|
||||
"manager_center.copy_id": "نسخ معرف المدير",
|
||||
"manager_center.role_manager": "مدير",
|
||||
"manager_center.this_month": "هذا الشهر",
|
||||
"manager_center.last_month": "الشهر الماضي",
|
||||
"manager_center.team_salary": "راتب الفريق",
|
||||
"manager_center.active_hosts": "المضيفون النشطون",
|
||||
"manager_center.total_active": "إجمالي النشطين",
|
||||
"manager_center.growth": "النمو",
|
||||
"manager_center.vs_last_month": "مقارنة بالشهر الماضي",
|
||||
"manager_center.wallet": "المحفظة",
|
||||
"manager_center.bank_cards": "البطاقات البنكية",
|
||||
"manager_center.admin": "مشرف",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "دعوة مشرف",
|
||||
"manager_center.invite_bd": "دعوة BD",
|
||||
"manager_center.super_admin": "مشرف عام",
|
||||
"manager_center.block_users": "حظر المستخدمين",
|
||||
"manager_center.send_frame": "إرسال إطار",
|
||||
"manager_center.send_car": "إرسال مركبة",
|
||||
"manager_center.send_badge": "إرسال شارة",
|
||||
"manager_center.send_vip": "إرسال VIP",
|
||||
"manager_center.user_level": "مستوى المستخدم"
|
||||
}
|
||||
|
||||
@ -485,6 +485,21 @@
|
||||
"coinseller_center.loading": "Loading...",
|
||||
"coinseller_center.load_failed": "Load failed. Try again later.",
|
||||
"coinseller_center.transfer_failed": "Transfer failed.",
|
||||
"coinseller_center.sub_sellers": "My sub coin sellers",
|
||||
"coinseller_center.sub_sellers_desc": "Invite and manage direct sub coin sellers",
|
||||
"coinseller_center.invite_sub_seller": "Invite sub coin seller",
|
||||
"coinseller_center.invite_sub_seller_hint": "Enter the user's display ID to open a direct sub coin seller.",
|
||||
"coinseller_center.invite_now": "Invite now",
|
||||
"coinseller_center.invite_success": "Invitation submitted.",
|
||||
"coinseller_center.invite_failed": "Invite failed.",
|
||||
"coinseller_center.no_sub_sellers": "No sub coin sellers yet.",
|
||||
"coinseller_center.sub_seller_wallet": "Wallet",
|
||||
"coinseller_center.transfer": "Transfer",
|
||||
"coinseller_center.transfer_now": "Transfer now",
|
||||
"coinseller_center.transfer_success": "Transfer success.",
|
||||
"coinseller_center.transfer_to_sub_seller": "Transfer to sub seller",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "Transfer coins into the sub seller coin-seller wallet.",
|
||||
"coinseller_center.select_sub_seller_first": "Select a sub coin seller first.",
|
||||
"lalu_landing.message": "Sent you a gift. Come play with me.",
|
||||
"lalu_landing.frame_duration": "3 days",
|
||||
"lalu_landing.join_party": "Join party",
|
||||
@ -721,5 +736,30 @@
|
||||
"contact_card.required": "Enter WhatsApp contact.",
|
||||
"contact_card.api_unavailable": "Contact API unavailable.",
|
||||
"contact_card.saved": "Saved.",
|
||||
"contact_card.save_failed": "Save failed."
|
||||
"contact_card.save_failed": "Save failed.",
|
||||
"manager_center.title": "Manage Center",
|
||||
"manager_center.back": "Back",
|
||||
"manager_center.change_language": "Change language",
|
||||
"manager_center.copy_id": "Copy manager ID",
|
||||
"manager_center.role_manager": "Manager",
|
||||
"manager_center.this_month": "This Month",
|
||||
"manager_center.last_month": "Last Month",
|
||||
"manager_center.team_salary": "Team Salary",
|
||||
"manager_center.active_hosts": "Active Hosts",
|
||||
"manager_center.total_active": "Total Active",
|
||||
"manager_center.growth": "Growth",
|
||||
"manager_center.vs_last_month": "vs Last Month",
|
||||
"manager_center.wallet": "Wallet",
|
||||
"manager_center.bank_cards": "Bank Cards",
|
||||
"manager_center.admin": "Admin",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "Invite Admin",
|
||||
"manager_center.invite_bd": "Invite BD",
|
||||
"manager_center.super_admin": "Super Admin",
|
||||
"manager_center.block_users": "Block Users",
|
||||
"manager_center.send_frame": "Send Frame",
|
||||
"manager_center.send_car": "Send Car",
|
||||
"manager_center.send_badge": "Send Badge",
|
||||
"manager_center.send_vip": "Send VIP",
|
||||
"manager_center.user_level": "User Level"
|
||||
}
|
||||
|
||||
@ -479,6 +479,21 @@
|
||||
"coinseller_center.loading": "Cargando...",
|
||||
"coinseller_center.load_failed": "Error al cargar. Inténtalo de nuevo más tarde.",
|
||||
"coinseller_center.transfer_failed": "Transferencia fallida.",
|
||||
"coinseller_center.sub_sellers": "Mis subvendedores de monedas",
|
||||
"coinseller_center.sub_sellers_desc": "Invita y gestiona subvendedores directos",
|
||||
"coinseller_center.invite_sub_seller": "Invitar subvendedor",
|
||||
"coinseller_center.invite_sub_seller_hint": "Ingresa el ID visible del usuario para abrir un subvendedor directo.",
|
||||
"coinseller_center.invite_now": "Invitar ahora",
|
||||
"coinseller_center.invite_success": "Invitación enviada.",
|
||||
"coinseller_center.invite_failed": "Error al invitar.",
|
||||
"coinseller_center.no_sub_sellers": "Aún no hay subvendedores.",
|
||||
"coinseller_center.sub_seller_wallet": "Billetera",
|
||||
"coinseller_center.transfer": "Transferir",
|
||||
"coinseller_center.transfer_now": "Transferir ahora",
|
||||
"coinseller_center.transfer_success": "Transferencia exitosa.",
|
||||
"coinseller_center.transfer_to_sub_seller": "Transferir al subvendedor",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "Transfiere monedas a la billetera de vendedor del subvendedor.",
|
||||
"coinseller_center.select_sub_seller_first": "Selecciona un subvendedor primero.",
|
||||
"lalu_landing.message": "Te envió un regalo. Ven a jugar conmigo.",
|
||||
"lalu_landing.frame_duration": "3 días",
|
||||
"lalu_landing.join_party": "Unirse a la fiesta",
|
||||
@ -715,5 +730,30 @@
|
||||
"contact_card.required": "Ingresa el contacto de WhatsApp.",
|
||||
"contact_card.api_unavailable": "API de contacto no disponible.",
|
||||
"contact_card.saved": "Guardado.",
|
||||
"contact_card.save_failed": "No se pudo guardar."
|
||||
"contact_card.save_failed": "No se pudo guardar.",
|
||||
"manager_center.title": "Centro de gestión",
|
||||
"manager_center.back": "Atrás",
|
||||
"manager_center.change_language": "Cambiar idioma",
|
||||
"manager_center.copy_id": "Copiar ID de manager",
|
||||
"manager_center.role_manager": "Gerente",
|
||||
"manager_center.this_month": "Este mes",
|
||||
"manager_center.last_month": "Mes pasado",
|
||||
"manager_center.team_salary": "Salario del equipo",
|
||||
"manager_center.active_hosts": "Anfitriones activos",
|
||||
"manager_center.total_active": "Total activos",
|
||||
"manager_center.growth": "Crecimiento",
|
||||
"manager_center.vs_last_month": "vs mes pasado",
|
||||
"manager_center.wallet": "Billetera",
|
||||
"manager_center.bank_cards": "Tarjetas bancarias",
|
||||
"manager_center.admin": "Admin",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "Invitar admin",
|
||||
"manager_center.invite_bd": "Invitar BD",
|
||||
"manager_center.super_admin": "Súper Admin",
|
||||
"manager_center.block_users": "Bloquear usuarios",
|
||||
"manager_center.send_frame": "Enviar marco",
|
||||
"manager_center.send_car": "Enviar vehículo",
|
||||
"manager_center.send_badge": "Enviar insignia",
|
||||
"manager_center.send_vip": "Enviar VIP",
|
||||
"manager_center.user_level": "Nivel de usuario"
|
||||
}
|
||||
|
||||
@ -446,6 +446,21 @@
|
||||
"coinseller_center.loading": "Memuat...",
|
||||
"coinseller_center.load_failed": "Gagal memuat. Coba lagi nanti.",
|
||||
"coinseller_center.transfer_failed": "Transfer gagal.",
|
||||
"coinseller_center.sub_sellers": "Sub penjual koin saya",
|
||||
"coinseller_center.sub_sellers_desc": "Undang dan kelola sub penjual koin langsung",
|
||||
"coinseller_center.invite_sub_seller": "Undang sub penjual koin",
|
||||
"coinseller_center.invite_sub_seller_hint": "Masukkan ID tampilan pengguna untuk membuka sub penjual koin langsung.",
|
||||
"coinseller_center.invite_now": "Undang sekarang",
|
||||
"coinseller_center.invite_success": "Undangan terkirim.",
|
||||
"coinseller_center.invite_failed": "Undangan gagal.",
|
||||
"coinseller_center.no_sub_sellers": "Belum ada sub penjual koin.",
|
||||
"coinseller_center.sub_seller_wallet": "Dompet",
|
||||
"coinseller_center.transfer": "Transfer",
|
||||
"coinseller_center.transfer_now": "Transfer sekarang",
|
||||
"coinseller_center.transfer_success": "Transfer berhasil.",
|
||||
"coinseller_center.transfer_to_sub_seller": "Transfer ke sub penjual",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "Transfer koin ke dompet penjual koin milik sub penjual.",
|
||||
"coinseller_center.select_sub_seller_first": "Pilih sub penjual koin terlebih dahulu.",
|
||||
"lalu_landing.message": "Mengirim hadiah untukmu. Ayo main bersama.",
|
||||
"lalu_landing.frame_duration": "3 hari",
|
||||
"lalu_landing.join_party": "Gabung party",
|
||||
@ -643,5 +658,30 @@
|
||||
"contact_card.required": "Masukkan kontak WhatsApp.",
|
||||
"contact_card.api_unavailable": "API kontak tidak tersedia.",
|
||||
"contact_card.saved": "Tersimpan.",
|
||||
"contact_card.save_failed": "Gagal menyimpan."
|
||||
"contact_card.save_failed": "Gagal menyimpan.",
|
||||
"manager_center.title": "Pusat Manajemen",
|
||||
"manager_center.back": "Kembali",
|
||||
"manager_center.change_language": "Ubah bahasa",
|
||||
"manager_center.copy_id": "Salin ID manager",
|
||||
"manager_center.role_manager": "Manajer",
|
||||
"manager_center.this_month": "Bulan Ini",
|
||||
"manager_center.last_month": "Bulan Lalu",
|
||||
"manager_center.team_salary": "Gaji Tim",
|
||||
"manager_center.active_hosts": "Host Aktif",
|
||||
"manager_center.total_active": "Total Aktif",
|
||||
"manager_center.growth": "Pertumbuhan",
|
||||
"manager_center.vs_last_month": "vs Bulan Lalu",
|
||||
"manager_center.wallet": "Dompet",
|
||||
"manager_center.bank_cards": "Kartu Bank",
|
||||
"manager_center.admin": "Admin",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "Undang Admin",
|
||||
"manager_center.invite_bd": "Undang BD",
|
||||
"manager_center.super_admin": "Super Admin",
|
||||
"manager_center.block_users": "Blokir Pengguna",
|
||||
"manager_center.send_frame": "Kirim Bingkai",
|
||||
"manager_center.send_car": "Kirim Kendaraan",
|
||||
"manager_center.send_badge": "Kirim Lencana",
|
||||
"manager_center.send_vip": "Kirim VIP",
|
||||
"manager_center.user_level": "Level Pengguna"
|
||||
}
|
||||
|
||||
@ -446,6 +446,21 @@
|
||||
"coinseller_center.loading": "Yükleniyor...",
|
||||
"coinseller_center.load_failed": "Yükleme başarısız. Daha sonra tekrar deneyin.",
|
||||
"coinseller_center.transfer_failed": "Transfer başarısız.",
|
||||
"coinseller_center.sub_sellers": "Alt coin sellerlarım",
|
||||
"coinseller_center.sub_sellers_desc": "Doğrudan alt coin seller davet edin ve yönetin",
|
||||
"coinseller_center.invite_sub_seller": "Alt coin seller davet et",
|
||||
"coinseller_center.invite_sub_seller_hint": "Doğrudan alt coin seller açmak için kullanıcının görünen ID'sini girin.",
|
||||
"coinseller_center.invite_now": "Şimdi davet et",
|
||||
"coinseller_center.invite_success": "Davet gönderildi.",
|
||||
"coinseller_center.invite_failed": "Davet başarısız.",
|
||||
"coinseller_center.no_sub_sellers": "Henüz alt coin seller yok.",
|
||||
"coinseller_center.sub_seller_wallet": "Cüzdan",
|
||||
"coinseller_center.transfer": "Transfer",
|
||||
"coinseller_center.transfer_now": "Şimdi transfer et",
|
||||
"coinseller_center.transfer_success": "Transfer başarılı.",
|
||||
"coinseller_center.transfer_to_sub_seller": "Alt sellera transfer",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "Coinleri alt sellerın coin-seller cüzdanına aktarın.",
|
||||
"coinseller_center.select_sub_seller_first": "Önce bir alt coin seller seçin.",
|
||||
"lalu_landing.message": "Sana bir hediye gönderdi. Gel birlikte oynayalım.",
|
||||
"lalu_landing.frame_duration": "3 gün",
|
||||
"lalu_landing.join_party": "Partiye katıl",
|
||||
@ -682,5 +697,30 @@
|
||||
"contact_card.required": "WhatsApp iletişimini gir.",
|
||||
"contact_card.api_unavailable": "İletişim API kullanılamıyor.",
|
||||
"contact_card.saved": "Kaydedildi.",
|
||||
"contact_card.save_failed": "Kaydetme başarısız."
|
||||
"contact_card.save_failed": "Kaydetme başarısız.",
|
||||
"manager_center.title": "Yönetim Merkezi",
|
||||
"manager_center.back": "Geri",
|
||||
"manager_center.change_language": "Dili değiştir",
|
||||
"manager_center.copy_id": "Manager ID kopyala",
|
||||
"manager_center.role_manager": "Yönetici",
|
||||
"manager_center.this_month": "Bu Ay",
|
||||
"manager_center.last_month": "Geçen Ay",
|
||||
"manager_center.team_salary": "Ekip Maaşı",
|
||||
"manager_center.active_hosts": "Aktif Yayıncılar",
|
||||
"manager_center.total_active": "Toplam Aktif",
|
||||
"manager_center.growth": "Büyüme",
|
||||
"manager_center.vs_last_month": "Geçen Aya Göre",
|
||||
"manager_center.wallet": "Cüzdan",
|
||||
"manager_center.bank_cards": "Banka Kartları",
|
||||
"manager_center.admin": "Admin",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "Admin Davet Et",
|
||||
"manager_center.invite_bd": "BD Davet Et",
|
||||
"manager_center.super_admin": "Süper Admin",
|
||||
"manager_center.block_users": "Kullanıcıları Engelle",
|
||||
"manager_center.send_frame": "Çerçeve Gönder",
|
||||
"manager_center.send_car": "Araç Gönder",
|
||||
"manager_center.send_badge": "Rozet Gönder",
|
||||
"manager_center.send_vip": "VIP Gönder",
|
||||
"manager_center.user_level": "Kullanıcı Seviyesi"
|
||||
}
|
||||
|
||||
@ -452,6 +452,21 @@
|
||||
"coinseller_center.loading": "加载中...",
|
||||
"coinseller_center.load_failed": "加载失败,请稍后重试。",
|
||||
"coinseller_center.transfer_failed": "转账失败。",
|
||||
"coinseller_center.sub_sellers": "我的子币商",
|
||||
"coinseller_center.sub_sellers_desc": "邀请并管理直开子币商",
|
||||
"coinseller_center.invite_sub_seller": "邀请子币商",
|
||||
"coinseller_center.invite_sub_seller_hint": "输入用户展示 ID,直开为你的子币商。",
|
||||
"coinseller_center.invite_now": "立即邀请",
|
||||
"coinseller_center.invite_success": "邀请已提交。",
|
||||
"coinseller_center.invite_failed": "邀请失败。",
|
||||
"coinseller_center.no_sub_sellers": "暂无子币商。",
|
||||
"coinseller_center.sub_seller_wallet": "钱包",
|
||||
"coinseller_center.transfer": "转账",
|
||||
"coinseller_center.transfer_now": "立即转账",
|
||||
"coinseller_center.transfer_success": "转账成功。",
|
||||
"coinseller_center.transfer_to_sub_seller": "转账给子币商",
|
||||
"coinseller_center.transfer_to_sub_seller_hint": "将金币转入子币商的币商钱包。",
|
||||
"coinseller_center.select_sub_seller_first": "请先选择子币商。",
|
||||
"lalu_landing.message": "送你一份礼物,快来和我一起玩吧!",
|
||||
"lalu_landing.frame_duration": "3天",
|
||||
"lalu_landing.join_party": "加入派对",
|
||||
@ -649,5 +664,30 @@
|
||||
"contact_card.required": "请输入 WhatsApp 联系方式。",
|
||||
"contact_card.api_unavailable": "联系方式接口不可用。",
|
||||
"contact_card.saved": "已保存。",
|
||||
"contact_card.save_failed": "保存失败。"
|
||||
"contact_card.save_failed": "保存失败。",
|
||||
"manager_center.title": "管理中心",
|
||||
"manager_center.back": "返回",
|
||||
"manager_center.change_language": "切换语言",
|
||||
"manager_center.copy_id": "复制经理 ID",
|
||||
"manager_center.role_manager": "经理",
|
||||
"manager_center.this_month": "本月",
|
||||
"manager_center.last_month": "上月",
|
||||
"manager_center.team_salary": "团队工资",
|
||||
"manager_center.active_hosts": "活跃主播",
|
||||
"manager_center.total_active": "累计活跃",
|
||||
"manager_center.growth": "增长",
|
||||
"manager_center.vs_last_month": "环比上月",
|
||||
"manager_center.wallet": "钱包",
|
||||
"manager_center.bank_cards": "银行卡",
|
||||
"manager_center.admin": "管理员",
|
||||
"manager_center.bd_leader": "BD Leader",
|
||||
"manager_center.invite_admin": "邀请管理员",
|
||||
"manager_center.invite_bd": "邀请 BD",
|
||||
"manager_center.super_admin": "超级管理员",
|
||||
"manager_center.block_users": "封禁用户",
|
||||
"manager_center.send_frame": "赠送头像框",
|
||||
"manager_center.send_car": "赠送座驾",
|
||||
"manager_center.send_badge": "赠送徽章",
|
||||
"manager_center.send_vip": "赠送 VIP",
|
||||
"manager_center.user_level": "用户等级"
|
||||
}
|
||||
|
||||
136
common/skeleton.js
Normal file
136
common/skeleton.js
Normal file
@ -0,0 +1,136 @@
|
||||
(function () {
|
||||
// 页面级骨架屏:加载期整页覆盖一层模拟页面结构的占位层,数据就绪后整层淡出。
|
||||
// 样式在 common/theme.css 的 .hy-page-skeleton 系列类,流光块复用 .hy-skeleton,主题色自动跟随 App。
|
||||
// 布局通过 section 描述数组配置,默认布局对齐各中心页的“资料卡 + 卡片栅格 + 操作宫格”结构;
|
||||
// 结构差异大的页面调用 show({ sections: [...] }) 自定义。
|
||||
var SKELETON_ID = 'hy-page-skeleton';
|
||||
var LEAVE_MS = 200;
|
||||
var leaveTimer = 0;
|
||||
|
||||
var DEFAULT_SECTIONS = [
|
||||
// 顶栏:返回按钮 + 标题 + 右侧按钮
|
||||
{ type: 'header' },
|
||||
// 资料卡:圆头像 + 两行文本
|
||||
{ type: 'profile' },
|
||||
// KPI 卡片 2x2:圆图标 + 两条文本
|
||||
{ type: 'grid', count: 4, columns: 2, lines: 2, height: 128 },
|
||||
// 资产行 2x1
|
||||
{ type: 'grid', count: 2, columns: 2, lines: 1, height: 78 },
|
||||
// 操作宫格 3x3:居中圆图标 + 短文本
|
||||
{ type: 'grid', count: 9, columns: 3, lines: 1, height: 112, center: true },
|
||||
];
|
||||
|
||||
function block(circle) {
|
||||
var node = document.createElement('div');
|
||||
node.className = circle ? 'hy-skeleton hy-skeleton--circle' : 'hy-skeleton';
|
||||
return node;
|
||||
}
|
||||
|
||||
function buildSection(section) {
|
||||
if (section.type === 'header') {
|
||||
var header = document.createElement('div');
|
||||
header.className = 'hy-page-skeleton__header';
|
||||
header.appendChild(block(true));
|
||||
header.appendChild(block(false));
|
||||
header.appendChild(block(true));
|
||||
return header;
|
||||
}
|
||||
if (section.type === 'profile') {
|
||||
var card = document.createElement('div');
|
||||
card.className = 'hy-page-skeleton__card hy-page-skeleton__profile';
|
||||
var avatar = block(true);
|
||||
avatar.style.width = '76px';
|
||||
avatar.style.height = '76px';
|
||||
card.appendChild(avatar);
|
||||
var lines = document.createElement('div');
|
||||
lines.className = 'hy-page-skeleton__lines';
|
||||
lines.appendChild(block(false));
|
||||
lines.appendChild(block(false));
|
||||
card.appendChild(lines);
|
||||
return card;
|
||||
}
|
||||
// grid:等宽单元格,每格圆图标 + N 条文本
|
||||
var grid = document.createElement('div');
|
||||
grid.className = 'hy-page-skeleton__grid';
|
||||
grid.style.gridTemplateColumns =
|
||||
'repeat(' + (section.columns || 2) + ', minmax(0, 1fr))';
|
||||
for (var i = 0; i < (section.count || 2); i += 1) {
|
||||
var cell = document.createElement('div');
|
||||
cell.className =
|
||||
'hy-page-skeleton__cell' +
|
||||
(section.center ? ' hy-page-skeleton__cell--center' : '');
|
||||
if (section.height) cell.style.minHeight = section.height + 'px';
|
||||
cell.appendChild(block(true));
|
||||
for (var line = 0; line < (section.lines || 1); line += 1) {
|
||||
var bar = block(false);
|
||||
bar.style.width = line === 0 ? '72%' : '48%';
|
||||
cell.appendChild(bar);
|
||||
}
|
||||
grid.appendChild(cell);
|
||||
}
|
||||
return grid;
|
||||
}
|
||||
|
||||
function ensureNode(sections) {
|
||||
var node = document.getElementById(SKELETON_ID);
|
||||
if (node) return node;
|
||||
node = document.createElement('div');
|
||||
node.id = SKELETON_ID;
|
||||
node.className = 'hy-page-skeleton';
|
||||
node.setAttribute('aria-hidden', 'true');
|
||||
for (var i = 0; i < sections.length; i += 1) {
|
||||
node.appendChild(buildSection(sections[i]));
|
||||
}
|
||||
// 骨架结构之上居中叠加拉鲁猫 loading 图 + 转圈(视觉与 .hy-loading 全屏遮罩同源)。
|
||||
var mascotWrap = document.createElement('div');
|
||||
mascotWrap.className = 'hy-page-skeleton__mascot-wrap';
|
||||
var mascot = document.createElement('div');
|
||||
mascot.className = 'hy-loading__mascot';
|
||||
var spinner = document.createElement('div');
|
||||
spinner.className = 'hy-loading__spinner';
|
||||
mascotWrap.appendChild(mascot);
|
||||
mascotWrap.appendChild(spinner);
|
||||
node.appendChild(mascotWrap);
|
||||
document.body.appendChild(node);
|
||||
return node;
|
||||
}
|
||||
|
||||
function show(options) {
|
||||
if (!document.body) {
|
||||
document.addEventListener(
|
||||
'DOMContentLoaded',
|
||||
function () {
|
||||
show(options);
|
||||
},
|
||||
{ once: true }
|
||||
);
|
||||
return;
|
||||
}
|
||||
window.clearTimeout(leaveTimer);
|
||||
var node = ensureNode((options && options.sections) || DEFAULT_SECTIONS);
|
||||
node.classList.remove('is-leaving');
|
||||
node.classList.add('is-active');
|
||||
}
|
||||
|
||||
function hide() {
|
||||
var node = document.getElementById(SKELETON_ID);
|
||||
if (!node || !node.classList.contains('is-active')) return;
|
||||
// 淡出后再摘除激活态,避免数据渲染瞬间硬切;重复 hide 幂等。
|
||||
node.classList.add('is-leaving');
|
||||
window.clearTimeout(leaveTimer);
|
||||
leaveTimer = window.setTimeout(function () {
|
||||
node.classList.remove('is-active', 'is-leaving');
|
||||
}, LEAVE_MS);
|
||||
}
|
||||
|
||||
function isVisible() {
|
||||
var node = document.getElementById(SKELETON_ID);
|
||||
return !!node && node.classList.contains('is-active');
|
||||
}
|
||||
|
||||
window.HyAppSkeleton = {
|
||||
show: show,
|
||||
hide: hide,
|
||||
isVisible: isVisible,
|
||||
};
|
||||
})();
|
||||
162
common/theme.css
162
common/theme.css
@ -170,3 +170,165 @@
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 通用骨架屏:加载中给文本/块级占位元素加 .hy-skeleton 变成流光占位条,数据到达后移除该类恢复真实内容。
|
||||
底色取主题软色变量,lalu/yumi/aslan 三套主题自动跟随;圆形占位(头像等)叠加 .hy-skeleton--circle。 */
|
||||
.hy-skeleton {
|
||||
position: relative;
|
||||
display: inline-block;
|
||||
overflow: hidden;
|
||||
min-width: 3.2em;
|
||||
min-height: 1em;
|
||||
border-radius: var(--hy-skeleton-radius, 6px);
|
||||
background: linear-gradient(
|
||||
90deg,
|
||||
var(--hy-theme-primary-soft) 25%,
|
||||
var(--hy-theme-line) 37%,
|
||||
var(--hy-theme-primary-soft) 63%
|
||||
);
|
||||
background-size: 400% 100%;
|
||||
color: transparent !important;
|
||||
animation: hy-skeleton-shimmer 1.4s ease infinite;
|
||||
pointer-events: none;
|
||||
user-select: none;
|
||||
}
|
||||
|
||||
.hy-skeleton--circle {
|
||||
--hy-skeleton-radius: 50%;
|
||||
}
|
||||
|
||||
/* 占位期间隐藏子节点(图标等),避免真实内容在流光条上透出。 */
|
||||
.hy-skeleton > * {
|
||||
visibility: hidden;
|
||||
}
|
||||
|
||||
@keyframes hy-skeleton-shimmer {
|
||||
0% {
|
||||
background-position: 100% 50%;
|
||||
}
|
||||
100% {
|
||||
background-position: 0 50%;
|
||||
}
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hy-skeleton {
|
||||
animation: none;
|
||||
}
|
||||
}
|
||||
|
||||
/* 页面级骨架屏(common/skeleton.js):整页覆盖一层模拟页面结构的占位层,
|
||||
数据就绪后整层淡出移除。块级元素复用上面的 .hy-skeleton 流光条。 */
|
||||
.hy-page-skeleton {
|
||||
position: fixed;
|
||||
inset: 0;
|
||||
z-index: 80;
|
||||
display: none;
|
||||
overflow: hidden;
|
||||
padding: calc(14px + env(safe-area-inset-top)) 16px 30px;
|
||||
background: var(--hy-theme-bg);
|
||||
opacity: 1;
|
||||
transition: opacity 0.2s ease;
|
||||
}
|
||||
|
||||
.hy-page-skeleton.is-active {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.hy-page-skeleton.is-leaving {
|
||||
opacity: 0;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
/* 骨架层中央叠加拉鲁猫 loading 图 + 转圈,视觉复用 .hy-loading__mascot / .hy-loading__spinner。 */
|
||||
.hy-page-skeleton__mascot-wrap {
|
||||
position: absolute;
|
||||
inset: 0;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__header {
|
||||
display: grid;
|
||||
grid-template-columns: 36px minmax(0, 1fr) 36px;
|
||||
align-items: center;
|
||||
justify-items: center;
|
||||
height: 58px;
|
||||
margin-bottom: 10px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__header .hy-skeleton:nth-child(2) {
|
||||
width: 42%;
|
||||
height: 20px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__card {
|
||||
margin-bottom: 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
background: var(--hy-theme-surface);
|
||||
}
|
||||
|
||||
.hy-page-skeleton__profile {
|
||||
display: grid;
|
||||
grid-template-columns: 76px minmax(0, 1fr);
|
||||
gap: 12px;
|
||||
align-items: center;
|
||||
min-height: 112px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__lines {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__lines .hy-skeleton:first-child {
|
||||
width: 56%;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__lines .hy-skeleton:last-child {
|
||||
width: 38%;
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__grid {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__cell {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
gap: 9px;
|
||||
padding: 16px 13px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
background: var(--hy-theme-surface);
|
||||
}
|
||||
|
||||
.hy-page-skeleton__cell--center {
|
||||
justify-items: center;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__cell .hy-skeleton {
|
||||
height: 13px;
|
||||
}
|
||||
|
||||
.hy-page-skeleton__cell .hy-skeleton--circle {
|
||||
width: 46px;
|
||||
height: 46px;
|
||||
min-width: 46px;
|
||||
}
|
||||
|
||||
@media (prefers-reduced-motion: reduce) {
|
||||
.hy-page-skeleton {
|
||||
transition: none;
|
||||
}
|
||||
}
|
||||
|
||||
@ -112,6 +112,38 @@
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section
|
||||
class="card sub-seller-card"
|
||||
id="subSellerSection"
|
||||
hidden
|
||||
>
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 data-i18n="coinseller_center.sub_sellers">
|
||||
My sub coin sellers
|
||||
</h2>
|
||||
<p data-i18n="coinseller_center.sub_sellers_desc">
|
||||
Invite and manage direct sub coin sellers
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
class="round-action small"
|
||||
id="inviteSubSellerButton"
|
||||
type="button"
|
||||
aria-label="Invite sub coin seller"
|
||||
data-i18n-aria="coinseller_center.invite_sub_seller"
|
||||
hidden
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="M12 5v14" />
|
||||
<path d="M5 12h14" />
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-status" id="subSellerStatus" hidden></div>
|
||||
<div class="sub-seller-list" id="subSellerList"></div>
|
||||
</section>
|
||||
|
||||
<section class="card search-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
@ -131,7 +163,9 @@
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<circle cx="12" cy="12" r="9" />
|
||||
<path d="M9.5 9a2.7 2.7 0 0 1 5.2.9c0 1.9-2.7 2.2-2.7 4.1" />
|
||||
<path
|
||||
d="M9.5 9a2.7 2.7 0 0 1 5.2.9c0 1.9-2.7 2.2-2.7 4.1"
|
||||
/>
|
||||
<path d="M12 17h.01" />
|
||||
</svg>
|
||||
</button>
|
||||
@ -152,34 +186,34 @@
|
||||
>
|
||||
Search
|
||||
</button>
|
||||
</form>
|
||||
<div class="form-status" id="searchStatus" hidden></div>
|
||||
</form>
|
||||
<div class="form-status" id="searchStatus" hidden></div>
|
||||
|
||||
<div class="target-card" id="targetCard" hidden>
|
||||
<div class="target-avatar" id="targetAvatar"></div>
|
||||
<div class="target-main">
|
||||
<div
|
||||
class="role-pill"
|
||||
data-i18n="coinseller_center.target_user"
|
||||
>
|
||||
Target user
|
||||
</div>
|
||||
<div class="target-name" id="targetName">-</div>
|
||||
<div class="target-meta" id="targetUID">UID: -</div>
|
||||
</div>
|
||||
<button
|
||||
class="target-coin-action"
|
||||
id="openRechargeButton"
|
||||
type="button"
|
||||
aria-label="Recharge coins"
|
||||
data-i18n-aria="coinseller_center.recharge_coins"
|
||||
>
|
||||
<span class="coin-icon" aria-hidden="true">$</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
<div class="target-card" id="targetCard" hidden>
|
||||
<div class="target-avatar" id="targetAvatar"></div>
|
||||
<div class="target-main">
|
||||
<div
|
||||
class="role-pill"
|
||||
data-i18n="coinseller_center.target_user"
|
||||
>
|
||||
Target user
|
||||
</div>
|
||||
<div class="target-name" id="targetName">-</div>
|
||||
<div class="target-meta" id="targetUID">UID: -</div>
|
||||
</div>
|
||||
<button
|
||||
class="target-coin-action"
|
||||
id="openRechargeButton"
|
||||
type="button"
|
||||
aria-label="Recharge coins"
|
||||
data-i18n-aria="coinseller_center.recharge_coins"
|
||||
>
|
||||
<span class="coin-icon" aria-hidden="true">$</span>
|
||||
</button>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="card recent-card">
|
||||
<section class="card recent-card">
|
||||
<div class="section-head">
|
||||
<div>
|
||||
<h2 data-i18n="coinseller_center.recent_records">
|
||||
@ -241,7 +275,10 @@
|
||||
</button>
|
||||
</div>
|
||||
<div class="history-body">
|
||||
<form class="history-search-form" id="historySearchForm">
|
||||
<form
|
||||
class="history-search-form"
|
||||
id="historySearchForm"
|
||||
>
|
||||
<input
|
||||
id="historySearchInput"
|
||||
type="text"
|
||||
@ -290,8 +327,15 @@
|
||||
Expenditure
|
||||
</button>
|
||||
</div>
|
||||
<div class="form-status" id="historyStatus" hidden></div>
|
||||
<div class="record-list history-records" id="historyList"></div>
|
||||
<div
|
||||
class="form-status"
|
||||
id="historyStatus"
|
||||
hidden
|
||||
></div>
|
||||
<div
|
||||
class="record-list history-records"
|
||||
id="historyList"
|
||||
></div>
|
||||
<button
|
||||
class="load-more"
|
||||
id="loadMoreButton"
|
||||
@ -382,10 +426,151 @@
|
||||
|
||||
<div
|
||||
class="sheet-modal"
|
||||
id="helpModal"
|
||||
id="inviteSubSellerModal"
|
||||
hidden
|
||||
aria-hidden="true"
|
||||
>
|
||||
<button
|
||||
class="modal-backdrop"
|
||||
type="button"
|
||||
data-close-modal="inviteSubSellerModal"
|
||||
aria-label="Close"
|
||||
data-i18n-aria="coinseller_center.close"
|
||||
></button>
|
||||
<section
|
||||
class="bottom-sheet invite-sub-seller-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="inviteSubSellerTitle"
|
||||
>
|
||||
<div class="sheet-head">
|
||||
<h2
|
||||
id="inviteSubSellerTitle"
|
||||
data-i18n="coinseller_center.invite_sub_seller"
|
||||
>
|
||||
Invite sub coin seller
|
||||
</h2>
|
||||
<button
|
||||
class="sheet-close"
|
||||
type="button"
|
||||
data-close-modal="inviteSubSellerModal"
|
||||
aria-label="Close"
|
||||
data-i18n-aria="coinseller_center.close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<form class="sub-seller-form" id="inviteSubSellerForm">
|
||||
<p data-i18n="coinseller_center.invite_sub_seller_hint">
|
||||
Enter the user's display ID to open a direct sub
|
||||
coin seller.
|
||||
</p>
|
||||
<input
|
||||
id="inviteSubSellerInput"
|
||||
type="text"
|
||||
inputmode="text"
|
||||
autocomplete="off"
|
||||
placeholder="Enter user ID"
|
||||
data-i18n-placeholder="coinseller_center.enter_user_id"
|
||||
/>
|
||||
<button
|
||||
id="submitInviteSubSellerButton"
|
||||
type="submit"
|
||||
data-i18n="coinseller_center.invite_now"
|
||||
>
|
||||
Invite now
|
||||
</button>
|
||||
<div
|
||||
class="form-status"
|
||||
id="inviteSubSellerStatus"
|
||||
hidden
|
||||
></div>
|
||||
</form>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div
|
||||
class="sheet-modal"
|
||||
id="subSellerTransferModal"
|
||||
hidden
|
||||
aria-hidden="true"
|
||||
>
|
||||
<button
|
||||
class="modal-backdrop"
|
||||
type="button"
|
||||
data-close-modal="subSellerTransferModal"
|
||||
aria-label="Close"
|
||||
data-i18n-aria="coinseller_center.close"
|
||||
></button>
|
||||
<section
|
||||
class="bottom-sheet recharge-sheet"
|
||||
role="dialog"
|
||||
aria-modal="true"
|
||||
aria-labelledby="subSellerTransferTitle"
|
||||
>
|
||||
<div class="sheet-head">
|
||||
<h2
|
||||
id="subSellerTransferTitle"
|
||||
data-i18n="coinseller_center.transfer_to_sub_seller"
|
||||
>
|
||||
Transfer to sub seller
|
||||
</h2>
|
||||
<button
|
||||
class="sheet-close"
|
||||
type="button"
|
||||
data-close-modal="subSellerTransferModal"
|
||||
aria-label="Close"
|
||||
data-i18n-aria="coinseller_center.close"
|
||||
>
|
||||
×
|
||||
</button>
|
||||
</div>
|
||||
<div class="recharge-sheet-body">
|
||||
<div
|
||||
class="recharge-target"
|
||||
id="subSellerTransferTargetName"
|
||||
>
|
||||
-
|
||||
</div>
|
||||
<p
|
||||
data-i18n="coinseller_center.transfer_to_sub_seller_hint"
|
||||
>
|
||||
Transfer coins into the sub seller coin-seller
|
||||
wallet.
|
||||
</p>
|
||||
<form class="recharge-form" id="subSellerTransferForm">
|
||||
<label class="coin-input-shell">
|
||||
<span class="input-coin" aria-hidden="true"
|
||||
>$</span
|
||||
>
|
||||
<input
|
||||
id="subSellerTransferAmountInput"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
autocomplete="off"
|
||||
placeholder="Enter coins amount"
|
||||
data-i18n-placeholder="coinseller_center.coin_amount_placeholder"
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
id="submitSubSellerTransferButton"
|
||||
type="submit"
|
||||
disabled
|
||||
data-i18n="coinseller_center.transfer_now"
|
||||
>
|
||||
Transfer now
|
||||
</button>
|
||||
</form>
|
||||
<div
|
||||
class="form-status"
|
||||
id="subSellerTransferStatus"
|
||||
hidden
|
||||
></div>
|
||||
</div>
|
||||
</section>
|
||||
</div>
|
||||
|
||||
<div class="sheet-modal" id="helpModal" hidden aria-hidden="true">
|
||||
<button
|
||||
class="modal-backdrop"
|
||||
type="button"
|
||||
@ -419,13 +604,15 @@
|
||||
How to recharge?
|
||||
</p>
|
||||
<span data-i18n="coinseller_center.faq_answer_1">
|
||||
Search a user, enter the coin amount, then confirm recharge.
|
||||
Search a user, enter the coin amount, then confirm
|
||||
recharge.
|
||||
</span>
|
||||
<p data-i18n="coinseller_center.faq_question_2">
|
||||
How to check records?
|
||||
</p>
|
||||
<span data-i18n="coinseller_center.faq_answer_2">
|
||||
Open history and switch between income and expenditure.
|
||||
Open history and switch between income and
|
||||
expenditure.
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
@ -434,9 +621,9 @@
|
||||
|
||||
<script src="../../common/toast.js"></script>
|
||||
<script src="../../common/jsbridge.js"></script>
|
||||
<script src="../../common/api.js?v=20260604-coinseller-records"></script>
|
||||
<script src="../../common/api.js?v=20260709-coinseller-sub-sellers"></script>
|
||||
<script src="../../common/i18n.js?v=20260604-coinseller"></script>
|
||||
<script src="../common/contact-card.js?v=20260629-user-contact"></script>
|
||||
<script src="./script.js?v=20260604-record-user-profile"></script>
|
||||
<script src="./script.js?v=20260709-sub-sellers"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -79,6 +79,29 @@
|
||||
time: '2026-06-03 18:42',
|
||||
},
|
||||
],
|
||||
subSellerPermission: {
|
||||
visible: true,
|
||||
canInvite: true,
|
||||
canTransfer: true,
|
||||
},
|
||||
subSellers: [
|
||||
{
|
||||
uid: '7001001',
|
||||
userId: '107001001',
|
||||
name: 'Sub Seller Sara',
|
||||
avatar: avatarData('S', '#efe4ff', '#7d57c7'),
|
||||
status: 'active',
|
||||
balance: 320000,
|
||||
},
|
||||
{
|
||||
uid: '7001002',
|
||||
userId: '107001002',
|
||||
name: 'Sub Seller Omar',
|
||||
avatar: avatarData('O', '#e7f7f3', '#31a88f'),
|
||||
status: 'active',
|
||||
balance: 180000,
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
var data = {
|
||||
@ -87,6 +110,8 @@
|
||||
users: [],
|
||||
quickAmounts: mock.quickAmounts.slice(),
|
||||
records: [],
|
||||
subSellerPermission: emptySubSellerPermission(),
|
||||
subSellers: [],
|
||||
};
|
||||
|
||||
var state = {
|
||||
@ -99,6 +124,11 @@
|
||||
loading: query.get('mock') !== '1',
|
||||
searching: false,
|
||||
submitting: false,
|
||||
subSellerLoading: false,
|
||||
invitingSubSeller: false,
|
||||
subSellerTransferTarget: null,
|
||||
subSellerTransferAmount: '',
|
||||
subSellerTransferSubmitting: false,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
@ -140,6 +170,14 @@
|
||||
};
|
||||
}
|
||||
|
||||
function emptySubSellerPermission() {
|
||||
return {
|
||||
visible: false,
|
||||
canInvite: false,
|
||||
canTransfer: false,
|
||||
};
|
||||
}
|
||||
|
||||
function initial(text) {
|
||||
return String(text || '')
|
||||
.trim()
|
||||
@ -163,6 +201,15 @@
|
||||
return number > 0 ? number : 0;
|
||||
}
|
||||
|
||||
function booleanValue(value) {
|
||||
if (value === true || value === 1) return true;
|
||||
if (value === false || value === 0) return false;
|
||||
var text = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return text === 'true' || text === '1' || text === 'yes';
|
||||
}
|
||||
|
||||
function firstValue(source, keys) {
|
||||
if (!source) return '';
|
||||
for (var i = 0; i < keys.length; i += 1) {
|
||||
@ -219,6 +266,14 @@
|
||||
);
|
||||
}
|
||||
|
||||
function hasSubSellerAPI() {
|
||||
return (
|
||||
hasAPI() &&
|
||||
window.HyAppAPI.wallet.getCoinSellerSubSellerPermission &&
|
||||
window.HyAppAPI.wallet.listCoinSellerSubSellers
|
||||
);
|
||||
}
|
||||
|
||||
function setImage(image, fallback, src, name) {
|
||||
if (!image || !fallback) return;
|
||||
fallback.textContent = initial(name) || 'C';
|
||||
@ -536,12 +591,129 @@
|
||||
});
|
||||
}
|
||||
|
||||
function normalizeSubSellerPermission(payload) {
|
||||
var source = unwrapData(payload) || {};
|
||||
var canInvite = booleanValue(
|
||||
firstValue(source, [
|
||||
'can_invite_sub_seller',
|
||||
'canInviteSubSeller',
|
||||
'can_open_sub_seller',
|
||||
'canOpenSubSeller',
|
||||
'can_invite',
|
||||
'canInvite',
|
||||
])
|
||||
);
|
||||
var canTransfer = booleanValue(
|
||||
firstValue(source, [
|
||||
'can_transfer_sub_seller',
|
||||
'canTransferSubSeller',
|
||||
'can_transfer',
|
||||
'canTransfer',
|
||||
])
|
||||
);
|
||||
var canList = booleanValue(
|
||||
firstValue(source, [
|
||||
'can_list_sub_sellers',
|
||||
'canListSubSellers',
|
||||
'can_list',
|
||||
'canList',
|
||||
])
|
||||
);
|
||||
var explicitVisible = firstValue(source, [
|
||||
'visible',
|
||||
'enabled',
|
||||
'allowed',
|
||||
'has_permission',
|
||||
'hasPermission',
|
||||
]);
|
||||
return {
|
||||
visible:
|
||||
explicitVisible === '' || explicitVisible === undefined
|
||||
? canInvite || canTransfer || canList
|
||||
: booleanValue(explicitVisible),
|
||||
canInvite: canInvite,
|
||||
canTransfer: canTransfer,
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSubSeller(item, index) {
|
||||
var source = item || {};
|
||||
var displayID =
|
||||
firstValue(source, [
|
||||
'display_user_id',
|
||||
'displayUserID',
|
||||
'displayUserId',
|
||||
'display_id',
|
||||
'displayId',
|
||||
]) || '';
|
||||
var userID = firstValue(source, ['user_id', 'userId', 'id']) || '';
|
||||
var name =
|
||||
firstValue(source, ['username', 'nickname', 'name']) ||
|
||||
t('coinseller_center.user_name_template', 'User {id}').replace(
|
||||
'{id}',
|
||||
displayID || userID || index + 1
|
||||
);
|
||||
return {
|
||||
uid: displayID || userID,
|
||||
userId: userID,
|
||||
name: name,
|
||||
avatar:
|
||||
firstValue(source, [
|
||||
'avatar',
|
||||
'avatar_url',
|
||||
'avatarUrl',
|
||||
'profile_picture_url',
|
||||
'profilePictureUrl',
|
||||
]) || '',
|
||||
status: firstValue(source, ['status']) || '',
|
||||
balance: toNumber(
|
||||
firstValue(source, [
|
||||
'coin_seller_balance',
|
||||
'coinSellerBalance',
|
||||
'available_amount',
|
||||
'availableAmount',
|
||||
'balance',
|
||||
'amount',
|
||||
])
|
||||
),
|
||||
region:
|
||||
firstValue(source, [
|
||||
'country_display_name',
|
||||
'countryDisplayName',
|
||||
'region_name',
|
||||
'regionName',
|
||||
'country_name',
|
||||
'countryName',
|
||||
]) || '',
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSubSellers(payload) {
|
||||
var source = unwrapData(payload) || {};
|
||||
var items = Array.isArray(source.items)
|
||||
? source.items
|
||||
: Array.isArray(source.sub_sellers)
|
||||
? source.sub_sellers
|
||||
: Array.isArray(source.subSellers)
|
||||
? source.subSellers
|
||||
: Array.isArray(source.children)
|
||||
? source.children
|
||||
: Array.isArray(source)
|
||||
? source
|
||||
: [];
|
||||
return items.map(function (item, index) {
|
||||
return normalizeSubSeller(item, index);
|
||||
});
|
||||
}
|
||||
|
||||
function applyMockData() {
|
||||
data.profile = Object.assign({}, mock.profile);
|
||||
data.balance = Object.assign({}, mock.balance);
|
||||
data.users = mock.users.slice();
|
||||
data.quickAmounts = mock.quickAmounts.slice();
|
||||
data.records = mock.records.slice();
|
||||
data.subSellerPermission = Object.assign({}, mock.subSellerPermission);
|
||||
data.subSellers = mock.subSellers.slice();
|
||||
}
|
||||
|
||||
function applyEmptyData() {
|
||||
@ -550,6 +722,51 @@
|
||||
data.users = [];
|
||||
data.quickAmounts = mock.quickAmounts.slice();
|
||||
data.records = [];
|
||||
data.subSellerPermission = emptySubSellerPermission();
|
||||
data.subSellers = [];
|
||||
}
|
||||
|
||||
function loadSubSellerCapability() {
|
||||
if (!hasSubSellerAPI()) {
|
||||
data.subSellerPermission = emptySubSellerPermission();
|
||||
data.subSellers = [];
|
||||
return Promise.resolve();
|
||||
}
|
||||
state.subSellerLoading = true;
|
||||
renderSubSellers();
|
||||
return window.HyAppAPI.wallet
|
||||
.getCoinSellerSubSellerPermission()
|
||||
.then(function (response) {
|
||||
data.subSellerPermission =
|
||||
normalizeSubSellerPermission(response);
|
||||
// 子币商列表属于权限增强区;只有服务端明确给到能力时才拉取,避免普通币商看到空功能入口。
|
||||
if (!data.subSellerPermission.visible) {
|
||||
data.subSellers = [];
|
||||
return null;
|
||||
}
|
||||
return window.HyAppAPI.wallet
|
||||
.listCoinSellerSubSellers({
|
||||
page: 1,
|
||||
page_size: 50,
|
||||
})
|
||||
.then(function (listResponse) {
|
||||
data.subSellers = normalizeSubSellers(listResponse);
|
||||
});
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn(
|
||||
'[coinseller-center] sub seller api failed',
|
||||
error
|
||||
);
|
||||
}
|
||||
data.subSellerPermission = emptySubSellerPermission();
|
||||
data.subSellers = [];
|
||||
})
|
||||
.finally(function () {
|
||||
state.subSellerLoading = false;
|
||||
renderSubSellers();
|
||||
});
|
||||
}
|
||||
|
||||
function loadRealData() {
|
||||
@ -584,6 +801,7 @@
|
||||
return null;
|
||||
})
|
||||
: Promise.resolve(null),
|
||||
loadSubSellerCapability(),
|
||||
])
|
||||
.then(function (results) {
|
||||
data.profile = normalizeProfile(results[0]);
|
||||
@ -647,6 +865,87 @@
|
||||
}
|
||||
}
|
||||
|
||||
function createSubSellerRow(item) {
|
||||
var row = document.createElement('article');
|
||||
var avatar = document.createElement('div');
|
||||
var main = document.createElement('div');
|
||||
var title = document.createElement('div');
|
||||
var meta = document.createElement('div');
|
||||
var balance = document.createElement('div');
|
||||
var action = document.createElement('button');
|
||||
|
||||
row.className = 'sub-seller-row';
|
||||
avatar.className = 'record-avatar';
|
||||
main.className = 'sub-seller-main';
|
||||
title.className = 'record-title';
|
||||
meta.className = 'record-meta';
|
||||
balance.className = 'sub-seller-balance';
|
||||
action.className = 'sub-seller-transfer';
|
||||
action.type = 'button';
|
||||
action.disabled =
|
||||
!data.subSellerPermission.canTransfer ||
|
||||
state.subSellerTransferSubmitting;
|
||||
action.textContent = t('coinseller_center.transfer', 'Transfer');
|
||||
|
||||
renderAvatar(avatar, item);
|
||||
title.textContent = item.name || '-';
|
||||
meta.textContent = t(
|
||||
'coinseller_center.uid_prefix',
|
||||
'UID: {id}'
|
||||
).replace('{id}', item.uid || '-');
|
||||
balance.textContent =
|
||||
t('coinseller_center.sub_seller_wallet', 'Wallet') +
|
||||
': ' +
|
||||
count(item.balance) +
|
||||
' ' +
|
||||
t('coinseller_center.coins_lower', 'coins');
|
||||
action.addEventListener('click', function () {
|
||||
openSubSellerTransfer(item);
|
||||
});
|
||||
|
||||
main.appendChild(title);
|
||||
main.appendChild(meta);
|
||||
if (item.balance > 0) main.appendChild(balance);
|
||||
row.appendChild(avatar);
|
||||
row.appendChild(main);
|
||||
if (data.subSellerPermission.canTransfer) row.appendChild(action);
|
||||
return row;
|
||||
}
|
||||
|
||||
function renderSubSellers() {
|
||||
var section = $('subSellerSection');
|
||||
if (!section) return;
|
||||
var permission = data.subSellerPermission || emptySubSellerPermission();
|
||||
section.hidden = !permission.visible;
|
||||
$('inviteSubSellerButton').hidden = !permission.canInvite;
|
||||
if (!permission.visible) return;
|
||||
|
||||
if (state.subSellerLoading) {
|
||||
setStatus(
|
||||
$('subSellerStatus'),
|
||||
t('coinseller_center.loading', 'Loading...'),
|
||||
''
|
||||
);
|
||||
} else {
|
||||
setStatus(
|
||||
$('subSellerStatus'),
|
||||
data.subSellers.length
|
||||
? ''
|
||||
: t(
|
||||
'coinseller_center.no_sub_sellers',
|
||||
'No sub coin sellers yet.'
|
||||
),
|
||||
data.subSellers.length ? '' : 'warning'
|
||||
);
|
||||
}
|
||||
replaceChildren(
|
||||
$('subSellerList'),
|
||||
data.subSellers.map(function (item) {
|
||||
return createSubSellerRow(item);
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function renderQuickAmounts() {
|
||||
replaceChildren(
|
||||
$('quickAmounts'),
|
||||
@ -917,6 +1216,15 @@
|
||||
].join('-');
|
||||
}
|
||||
|
||||
function subSellerCommandID(action) {
|
||||
return [
|
||||
'coin-seller-sub-seller',
|
||||
action || 'action',
|
||||
Date.now(),
|
||||
Math.random().toString(36).slice(2, 8),
|
||||
].join('-');
|
||||
}
|
||||
|
||||
function buildTransferPayload(amount) {
|
||||
return {
|
||||
command_id: commandID(),
|
||||
@ -947,6 +1255,304 @@
|
||||
});
|
||||
}
|
||||
|
||||
function appendSubSellerTransferRecord(amount, response) {
|
||||
var target = state.subSellerTransferTarget;
|
||||
if (!target) return;
|
||||
var payload = unwrapData(response) || {};
|
||||
var coins = toNumber(
|
||||
firstValue(payload, ['amount', 'coin_amount', 'coinAmount'])
|
||||
);
|
||||
data.records.unshift({
|
||||
id:
|
||||
firstValue(payload, ['transaction_id', 'transactionId']) ||
|
||||
'sub-seller-tx-' + Date.now(),
|
||||
type: 'expenditure',
|
||||
name: target.name,
|
||||
uid: target.uid,
|
||||
avatar: target.avatar,
|
||||
coins: coins || amount,
|
||||
amount: 0,
|
||||
currency: '',
|
||||
time: currentTimeLabel(),
|
||||
});
|
||||
}
|
||||
|
||||
function refreshSubSellerFromResponse(target, payload, amount) {
|
||||
if (!target) return;
|
||||
var balanceAfter = firstValue(payload, [
|
||||
'sub_seller_balance_after',
|
||||
'subSellerBalanceAfter',
|
||||
'child_balance_after',
|
||||
'childBalanceAfter',
|
||||
'target_balance_after',
|
||||
'targetBalanceAfter',
|
||||
]);
|
||||
data.subSellers = data.subSellers.map(function (item) {
|
||||
if (String(item.uid) !== String(target.uid)) return item;
|
||||
return Object.assign({}, item, {
|
||||
balance:
|
||||
balanceAfter !== '' && balanceAfter !== undefined
|
||||
? toNumber(balanceAfter)
|
||||
: item.balance + amount,
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function inviteSubSeller() {
|
||||
var input = $('inviteSubSellerInput');
|
||||
var displayID = String(input.value || '').trim();
|
||||
if (!displayID) {
|
||||
setStatus(
|
||||
$('inviteSubSellerStatus'),
|
||||
t('coinseller_center.enter_user_id', 'Enter user ID'),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.isMock) {
|
||||
data.subSellers.unshift({
|
||||
uid: displayID,
|
||||
userId: displayID,
|
||||
name: t(
|
||||
'coinseller_center.user_name_template',
|
||||
'User {id}'
|
||||
).replace('{id}', displayID),
|
||||
avatar: avatarData(displayID, '#efe4ff', '#7d57c7'),
|
||||
status: 'active',
|
||||
balance: 0,
|
||||
});
|
||||
input.value = '';
|
||||
renderSubSellers();
|
||||
closeModal('inviteSubSellerModal');
|
||||
toast(
|
||||
t('coinseller_center.invite_success', 'Invitation submitted.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (!hasAPI() || !window.HyAppAPI.wallet.inviteCoinSellerSubSeller) {
|
||||
setStatus(
|
||||
$('inviteSubSellerStatus'),
|
||||
t('coinseller_center.invite_failed', 'Invite failed.'),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
state.invitingSubSeller = true;
|
||||
$('submitInviteSubSellerButton').disabled = true;
|
||||
setStatus(
|
||||
$('inviteSubSellerStatus'),
|
||||
t('coinseller_center.loading', 'Loading...'),
|
||||
''
|
||||
);
|
||||
// 直开子币商是有状态写操作,必须使用 command_id 让移动端重试或重复点击时由服务端保持幂等。
|
||||
window.HyAppAPI.wallet
|
||||
.inviteCoinSellerSubSeller({
|
||||
command_id: subSellerCommandID('invite'),
|
||||
target_display_user_id: displayID,
|
||||
})
|
||||
.then(function (response) {
|
||||
var payload = unwrapData(response) || {};
|
||||
var seller = normalizeSubSeller(
|
||||
payload.child ||
|
||||
payload.sub_seller ||
|
||||
payload.subSeller ||
|
||||
payload,
|
||||
data.subSellers.length
|
||||
);
|
||||
if (seller.uid) {
|
||||
data.subSellers = data.subSellers.filter(function (item) {
|
||||
return String(item.uid) !== String(seller.uid);
|
||||
});
|
||||
data.subSellers.unshift(seller);
|
||||
}
|
||||
input.value = '';
|
||||
setStatus(
|
||||
$('inviteSubSellerStatus'),
|
||||
t(
|
||||
'coinseller_center.invite_success',
|
||||
'Invitation submitted.'
|
||||
),
|
||||
'success'
|
||||
);
|
||||
renderSubSellers();
|
||||
toast(
|
||||
t(
|
||||
'coinseller_center.invite_success',
|
||||
'Invitation submitted.'
|
||||
)
|
||||
);
|
||||
closeModal('inviteSubSellerModal');
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn(
|
||||
'[coinseller-center] invite sub seller failed',
|
||||
error
|
||||
);
|
||||
}
|
||||
setStatus(
|
||||
$('inviteSubSellerStatus'),
|
||||
error && error.message
|
||||
? error.message
|
||||
: t(
|
||||
'coinseller_center.invite_failed',
|
||||
'Invite failed.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
state.invitingSubSeller = false;
|
||||
$('submitInviteSubSellerButton').disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
function openSubSellerTransfer(item) {
|
||||
state.subSellerTransferTarget = item;
|
||||
state.subSellerTransferAmount = '';
|
||||
$('subSellerTransferAmountInput').value = '';
|
||||
$('subSellerTransferTargetName').textContent =
|
||||
t(
|
||||
'coinseller_center.transfer_to_sub_seller',
|
||||
'Transfer to sub seller'
|
||||
) +
|
||||
': ' +
|
||||
item.name;
|
||||
setStatus($('subSellerTransferStatus'), '', '');
|
||||
renderSubSellerTransferState();
|
||||
openModal('subSellerTransferModal');
|
||||
window.setTimeout(function () {
|
||||
$('subSellerTransferAmountInput').focus();
|
||||
}, 80);
|
||||
}
|
||||
|
||||
function parsedSubSellerTransferAmount() {
|
||||
var input = $('subSellerTransferAmountInput');
|
||||
if (input) {
|
||||
var sanitized = String(input.value || '').replace(/[^\d]/g, '');
|
||||
if (input.value !== sanitized) input.value = sanitized;
|
||||
state.subSellerTransferAmount = sanitized;
|
||||
}
|
||||
return Number(state.subSellerTransferAmount || 0);
|
||||
}
|
||||
|
||||
function renderSubSellerTransferState() {
|
||||
var amount = parsedSubSellerTransferAmount();
|
||||
$('submitSubSellerTransferButton').disabled =
|
||||
state.loading ||
|
||||
state.subSellerTransferSubmitting ||
|
||||
!state.subSellerTransferTarget ||
|
||||
amount <= 0;
|
||||
}
|
||||
|
||||
function transferToSubSeller() {
|
||||
var amount = parsedSubSellerTransferAmount();
|
||||
var target = state.subSellerTransferTarget;
|
||||
if (!target) {
|
||||
setStatus(
|
||||
$('subSellerTransferStatus'),
|
||||
t(
|
||||
'coinseller_center.select_sub_seller_first',
|
||||
'Select a sub coin seller first.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (amount <= 0) {
|
||||
setStatus(
|
||||
$('subSellerTransferStatus'),
|
||||
t(
|
||||
'coinseller_center.valid_amount_required',
|
||||
'Please enter a valid amount.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (amount > data.balance.coins) {
|
||||
setStatus(
|
||||
$('subSellerTransferStatus'),
|
||||
t(
|
||||
'coinseller_center.amount_exceeds_stock',
|
||||
'Amount exceeds stock.'
|
||||
),
|
||||
'warning'
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (state.isMock) {
|
||||
data.balance.coins -= amount;
|
||||
refreshSubSellerFromResponse(target, {}, amount);
|
||||
appendSubSellerTransferRecord(amount, {});
|
||||
finishSubSellerTransfer();
|
||||
return;
|
||||
}
|
||||
if (
|
||||
!hasAPI() ||
|
||||
!window.HyAppAPI.wallet.transferCoinSellerToSubSeller
|
||||
) {
|
||||
setStatus($('subSellerTransferStatus'), '', '');
|
||||
toast(t('coinseller_center.transfer_failed', 'Transfer failed.'));
|
||||
return;
|
||||
}
|
||||
|
||||
state.subSellerTransferSubmitting = true;
|
||||
setStatus(
|
||||
$('subSellerTransferStatus'),
|
||||
t('coinseller_center.loading', 'Loading...'),
|
||||
''
|
||||
);
|
||||
renderSubSellerTransferState();
|
||||
// 子币商转账目标是对方的币商专用钱包,不是玩家普通金币;payload 单独命名,避免误走老的玩家充值接口。
|
||||
window.HyAppAPI.wallet
|
||||
.transferCoinSellerToSubSeller({
|
||||
command_id: subSellerCommandID('transfer'),
|
||||
target_display_user_id: String(target.uid || '').trim(),
|
||||
amount: amount,
|
||||
reason: 'sub coin seller stock transfer',
|
||||
})
|
||||
.then(function (response) {
|
||||
var payload = unwrapData(response) || {};
|
||||
var balanceAfter = firstValue(payload, [
|
||||
'parent_balance_after',
|
||||
'parentBalanceAfter',
|
||||
'seller_balance_after',
|
||||
'sellerBalanceAfter',
|
||||
]);
|
||||
data.balance.coins =
|
||||
balanceAfter !== '' && balanceAfter !== undefined
|
||||
? toNumber(balanceAfter)
|
||||
: data.balance.coins - amount;
|
||||
refreshSubSellerFromResponse(target, payload, amount);
|
||||
appendSubSellerTransferRecord(amount, response);
|
||||
finishSubSellerTransfer();
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (window.console && window.console.warn) {
|
||||
window.console.warn(
|
||||
'[coinseller-center] sub seller transfer failed',
|
||||
error
|
||||
);
|
||||
}
|
||||
setStatus($('subSellerTransferStatus'), '', '');
|
||||
toast(
|
||||
error && error.message
|
||||
? error.message
|
||||
: t(
|
||||
'coinseller_center.transfer_failed',
|
||||
'Transfer failed.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
state.subSellerTransferSubmitting = false;
|
||||
renderSubSellerTransferState();
|
||||
renderSubSellers();
|
||||
});
|
||||
}
|
||||
|
||||
function rechargeNow() {
|
||||
var amount = parsedCoinAmount();
|
||||
if (!state.target) {
|
||||
@ -1058,6 +1664,23 @@
|
||||
toast(t('coinseller_center.recharge_success', 'Recharge success.'));
|
||||
}
|
||||
|
||||
function finishSubSellerTransfer() {
|
||||
state.subSellerTransferAmount = '';
|
||||
$('subSellerTransferAmountInput').value = '';
|
||||
setStatus(
|
||||
$('subSellerTransferStatus'),
|
||||
t('coinseller_center.transfer_success', 'Transfer success.'),
|
||||
'success'
|
||||
);
|
||||
renderBalance();
|
||||
renderSubSellers();
|
||||
renderRecent();
|
||||
renderSubSellerTransferState();
|
||||
if (!$('historyModal').hidden) renderHistory();
|
||||
closeModal('subSellerTransferModal');
|
||||
toast(t('coinseller_center.transfer_success', 'Transfer success.'));
|
||||
}
|
||||
|
||||
function currentTimeLabel() {
|
||||
var now = new Date();
|
||||
return (
|
||||
@ -1112,6 +1735,14 @@
|
||||
$('helpButton').addEventListener('click', function () {
|
||||
openModal('helpModal');
|
||||
});
|
||||
$('inviteSubSellerButton').addEventListener('click', function () {
|
||||
$('inviteSubSellerInput').value = '';
|
||||
setStatus($('inviteSubSellerStatus'), '', '');
|
||||
openModal('inviteSubSellerModal');
|
||||
window.setTimeout(function () {
|
||||
$('inviteSubSellerInput').focus();
|
||||
}, 80);
|
||||
});
|
||||
document
|
||||
.querySelectorAll('[data-close-modal]')
|
||||
.forEach(function (node) {
|
||||
@ -1153,6 +1784,26 @@
|
||||
event.preventDefault();
|
||||
rechargeNow();
|
||||
});
|
||||
$('inviteSubSellerForm').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
inviteSubSeller();
|
||||
});
|
||||
$('subSellerTransferAmountInput').addEventListener(
|
||||
'input',
|
||||
function () {
|
||||
state.subSellerTransferAmount = $(
|
||||
'subSellerTransferAmountInput'
|
||||
).value.replace(/[^\d]/g, '');
|
||||
$('subSellerTransferAmountInput').value =
|
||||
state.subSellerTransferAmount;
|
||||
setStatus($('subSellerTransferStatus'), '', '');
|
||||
renderSubSellerTransferState();
|
||||
}
|
||||
);
|
||||
$('subSellerTransferForm').addEventListener('submit', function (event) {
|
||||
event.preventDefault();
|
||||
transferToSubSeller();
|
||||
});
|
||||
document.querySelectorAll('[data-history-tab]').forEach(function (tab) {
|
||||
tab.addEventListener('click', function () {
|
||||
state.historyTab = tab.getAttribute('data-history-tab');
|
||||
@ -1180,6 +1831,8 @@
|
||||
if (event.key !== 'Escape') return;
|
||||
closeModal('historyModal');
|
||||
closeModal('helpModal');
|
||||
closeModal('inviteSubSellerModal');
|
||||
closeModal('subSellerTransferModal');
|
||||
});
|
||||
}
|
||||
|
||||
@ -1189,6 +1842,8 @@
|
||||
renderQuickAmounts();
|
||||
renderTarget();
|
||||
renderRechargeState();
|
||||
renderSubSellers();
|
||||
renderSubSellerTransferState();
|
||||
renderRecent();
|
||||
}
|
||||
|
||||
|
||||
@ -15,6 +15,7 @@
|
||||
}
|
||||
|
||||
.balance-card,
|
||||
.sub-seller-card,
|
||||
.search-card,
|
||||
.recent-card {
|
||||
padding: 16px;
|
||||
@ -118,6 +119,7 @@
|
||||
}
|
||||
|
||||
.search-card,
|
||||
.sub-seller-card,
|
||||
.recent-card {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
@ -205,6 +207,10 @@
|
||||
display: none;
|
||||
}
|
||||
|
||||
.sub-seller-card[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.target-avatar,
|
||||
.record-avatar {
|
||||
display: flex;
|
||||
@ -373,6 +379,54 @@
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sub-seller-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.sub-seller-row {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
padding: 12px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: #fbfcfc;
|
||||
}
|
||||
|
||||
.sub-seller-main {
|
||||
min-width: 0;
|
||||
}
|
||||
|
||||
.sub-seller-balance {
|
||||
margin-top: 5px;
|
||||
overflow: hidden;
|
||||
color: var(--primary-deep);
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
line-height: 1.2;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub-seller-transfer {
|
||||
min-height: 38px;
|
||||
padding: 0 12px;
|
||||
border-radius: 8px;
|
||||
background: var(--button);
|
||||
color: #fff;
|
||||
font-size: 13px;
|
||||
font-weight: 900;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.sub-seller-transfer:disabled {
|
||||
background: var(--button-disabled);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.record-row {
|
||||
display: grid;
|
||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||
@ -515,6 +569,52 @@
|
||||
padding: 16px 18px 22px;
|
||||
}
|
||||
|
||||
.sub-seller-form {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
padding: 16px 18px calc(18px + env(safe-area-inset-bottom));
|
||||
}
|
||||
|
||||
.sub-seller-form p {
|
||||
margin: 0;
|
||||
color: var(--muted);
|
||||
font-size: 13px;
|
||||
font-weight: 850;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.sub-seller-form input {
|
||||
width: 100%;
|
||||
min-width: 0;
|
||||
height: 52px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--line);
|
||||
border-radius: 8px;
|
||||
background: rgba(251, 249, 255, 0.9);
|
||||
color: var(--text);
|
||||
font-size: 15px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.sub-seller-form input::placeholder {
|
||||
color: #b5b0bd;
|
||||
}
|
||||
|
||||
.sub-seller-form button {
|
||||
width: 100%;
|
||||
min-height: 56px;
|
||||
border-radius: 28px;
|
||||
background: var(--button);
|
||||
color: #fff;
|
||||
font-size: 15px;
|
||||
font-weight: 900;
|
||||
}
|
||||
|
||||
.sub-seller-form button:disabled {
|
||||
background: var(--button-disabled);
|
||||
color: var(--muted);
|
||||
}
|
||||
|
||||
.help-content h3,
|
||||
.help-content p {
|
||||
margin: 0;
|
||||
@ -565,4 +665,13 @@ body.modal-open .coinseller-center {
|
||||
.history-search-form {
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sub-seller-row {
|
||||
grid-template-columns: 42px minmax(0, 1fr);
|
||||
}
|
||||
|
||||
.sub-seller-transfer {
|
||||
grid-column: 2;
|
||||
justify-self: start;
|
||||
}
|
||||
}
|
||||
|
||||
BIN
gonghui/manager-center/assets/demo-avatar.png
Normal file
BIN
gonghui/manager-center/assets/demo-avatar.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@ -6,17 +6,17 @@
|
||||
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" />
|
||||
<title>Manage Center</title>
|
||||
<link
|
||||
rel="stylesheet"
|
||||
href="./style.css?v=20260618-manager-permissions"
|
||||
href="../../common/theme.css?v=20260709-skeleton-mascot"
|
||||
/>
|
||||
<link rel="stylesheet" href="../common/center.css?v=20260608" />
|
||||
<link rel="stylesheet" href="./style.css?v=20260708-manager-actions" />
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="manager-center" aria-label="Control Panel">
|
||||
<div class="manager-center" aria-label="Manage Center">
|
||||
<div class="hero-bg" aria-hidden="true"></div>
|
||||
|
||||
<nav class="title-bar">
|
||||
@ -28,10 +28,12 @@
|
||||
data-i18n-aria="manager_center.back"
|
||||
>
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true">
|
||||
<path d="M20 8 12 16l8 8" />
|
||||
<path
|
||||
d="M20.3 6.8a2 2 0 0 1 0 2.8L14 15.9c-.1.1-.1.2 0 .3l6.3 6.3a2 2 0 1 1-2.8 2.8L9.8 17.6a2.3 2.3 0 0 1 0-3.2l7.7-7.7a2 2 0 0 1 2.8.1Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<h1 data-i18n="manager_center.title">Control Panel</h1>
|
||||
<h1 data-i18n="manager_center.title">Manage Center</h1>
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
@ -39,10 +41,16 @@
|
||||
aria-label="Change language"
|
||||
aria-expanded="false"
|
||||
data-language-toggle
|
||||
data-current-lang
|
||||
data-i18n-aria="manager_center.change_language"
|
||||
>
|
||||
EN
|
||||
<svg viewBox="0 0 32 32" aria-hidden="true">
|
||||
<path
|
||||
d="M16 11.2a4.8 4.8 0 1 0 0 9.6 4.8 4.8 0 0 0 0-9.6Z"
|
||||
/>
|
||||
<path
|
||||
d="M26.2 17.7v-3.4l-3.2-.7a8 8 0 0 0-.8-1.9l1.8-2.8-2.4-2.4-2.8 1.8c-.6-.3-1.2-.6-1.9-.8L16.3 4h-3.4l-.7 3.5c-.7.2-1.3.5-1.9.8L7.5 6.5 5.1 8.9l1.8 2.8c-.3.6-.6 1.2-.8 1.9l-3.3.7v3.4l3.3.7c.2.7.5 1.3.8 1.9l-1.8 2.8 2.4 2.4 2.8-1.8c.6.3 1.2.6 1.9.8l.7 3.5h3.4l.7-3.5c.7-.2 1.3-.5 1.9-.8l2.8 1.8 2.4-2.4-1.8-2.8c.3-.6.6-1.2.8-1.9l3.2-.7Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
<div class="language-menu" data-language-menu hidden>
|
||||
<button type="button" data-lang-option="en">EN</button>
|
||||
@ -56,262 +64,447 @@
|
||||
</nav>
|
||||
|
||||
<main class="content">
|
||||
<section class="card manager-panel">
|
||||
<div class="manager-grid" aria-label="Manager tools">
|
||||
<section class="card manager-profile-card">
|
||||
<div class="manager-avatar-shell">
|
||||
<img
|
||||
class="manager-avatar-image"
|
||||
id="managerAvatar"
|
||||
src=""
|
||||
alt=""
|
||||
hidden
|
||||
/>
|
||||
<div
|
||||
class="manager-avatar-fallback"
|
||||
id="managerAvatarFallback"
|
||||
aria-hidden="true"
|
||||
></div>
|
||||
</div>
|
||||
<div class="manager-profile-copy">
|
||||
<strong id="managerName"></strong>
|
||||
<span class="manager-role-badge">
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path d="m5 9 4 4 3-7 3 7 4-4-2 9H7Z" />
|
||||
</svg>
|
||||
<span data-i18n="manager_center.role_manager"
|
||||
>Manager</span
|
||||
>
|
||||
</span>
|
||||
<span class="manager-id-line">
|
||||
<span>ID:</span>
|
||||
<span id="managerDisplayId"></span>
|
||||
<button
|
||||
class="manager-copy-button"
|
||||
type="button"
|
||||
aria-label="Copy manager ID"
|
||||
data-i18n-aria="manager_center.copy_id"
|
||||
>
|
||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||
<path
|
||||
d="M8.5 7.2h9.1c1.2 0 2.2 1 2.2 2.2v9.1c0 1.2-1 2.2-2.2 2.2H8.5c-1.2 0-2.2-1-2.2-2.2V9.4c0-1.2 1-2.2 2.2-2.2Z"
|
||||
/>
|
||||
<path
|
||||
d="M5.2 3.3h8.7c1 0 1.8.8 1.8 1.8v.8H7.8c-1.6 0-2.9 1.3-2.9 2.9v7.9H4c-1 0-1.8-.8-1.8-1.8V6.3c0-1.7 1.3-3 3-3Z"
|
||||
/>
|
||||
</svg>
|
||||
</button>
|
||||
</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="manager-kpi-grid" aria-label="Manager summary">
|
||||
<article class="card manager-kpi-card kpi-wallet">
|
||||
<span class="manager-kpi-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M8 13.5h18.2a4.2 4.2 0 0 1 4.2 4.2v10.6H8Z"
|
||||
/>
|
||||
<path d="M8.2 12.8 23.8 7l2.2 6.5Z" />
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="M24 19.5h6.4v5.4H24a2.7 2.7 0 0 1 0-5.4Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-dot"
|
||||
cx="24.3"
|
||||
cy="22.2"
|
||||
r="1.1"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="manager-kpi-copy">
|
||||
<span data-i18n="manager_center.this_month"
|
||||
>This Month</span
|
||||
>
|
||||
<strong id="managerThisMonthSalary"></strong>
|
||||
<em data-i18n="manager_center.team_salary"
|
||||
>Team Salary</em
|
||||
>
|
||||
</div>
|
||||
<i class="manager-bars" aria-hidden="true"></i>
|
||||
</article>
|
||||
|
||||
<article class="card manager-kpi-card kpi-report">
|
||||
<span class="manager-kpi-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path d="M9.5 5.5h15.4l4.6 4.6v20.4h-20Z" />
|
||||
<path class="icon-cutout" d="M24 6.5v5h5Z" />
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="M15 15h9v2.2h-9ZM15 20h6.8v2.2H15Z"
|
||||
/>
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="m15.3 27.3 3.4-3.4 2.9 2.8 5-7 1.9 1.4-6.6 9.1-3.1-3-2.1 2.1Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="manager-kpi-copy">
|
||||
<span data-i18n="manager_center.last_month"
|
||||
>Last Month</span
|
||||
>
|
||||
<strong id="managerLastMonthSalary"></strong>
|
||||
<em data-i18n="manager_center.team_salary"
|
||||
>Team Salary</em
|
||||
>
|
||||
</div>
|
||||
<i class="manager-bars" aria-hidden="true"></i>
|
||||
</article>
|
||||
|
||||
<article class="card manager-kpi-card kpi-hosts">
|
||||
<span class="manager-kpi-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<circle cx="13.4" cy="12.3" r="5.5" />
|
||||
<path
|
||||
d="M4.8 30.5c1.3-6.4 4.2-9.6 8.6-9.6s7.3 3.2 8.6 9.6Z"
|
||||
/>
|
||||
<circle cx="24.7" cy="12.7" r="4.8" />
|
||||
<path
|
||||
d="M21.4 30.5c1.1-5.4 3.5-8.1 7.2-8.1 3 0 5.1 2 6.4 6.1v2Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<div class="manager-kpi-copy">
|
||||
<span data-i18n="manager_center.active_hosts"
|
||||
>Active Hosts</span
|
||||
>
|
||||
<strong id="managerActiveHosts"></strong>
|
||||
<em data-i18n="manager_center.total_active"
|
||||
>Total Active</em
|
||||
>
|
||||
</div>
|
||||
<i class="manager-bars" aria-hidden="true"></i>
|
||||
</article>
|
||||
|
||||
<article class="card manager-kpi-card kpi-growth">
|
||||
<span class="manager-kpi-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M7.5 25.5 15.3 17.7l4.4 4.3 8.1-11h3.4v3.4L20.2 29 15.4 24.2 10 29.5Z"
|
||||
/>
|
||||
<path d="M23.2 9.6h8v8h-3v-5h-5Z" />
|
||||
</svg>
|
||||
</span>
|
||||
<div class="manager-kpi-copy">
|
||||
<span data-i18n="manager_center.growth"
|
||||
>Growth</span
|
||||
>
|
||||
<strong id="managerGrowth"></strong>
|
||||
<em data-i18n="manager_center.vs_last_month"
|
||||
>vs Last Month</em
|
||||
>
|
||||
</div>
|
||||
<i class="manager-bars" aria-hidden="true"></i>
|
||||
</article>
|
||||
</section>
|
||||
|
||||
<!-- <section class="card manager-assets-card">
|
||||
<button class="manager-asset-item" type="button">
|
||||
<span
|
||||
class="manager-asset-icon wallet"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path d="M7 12h18.5a4 4 0 0 1 4 4v12H7Z" />
|
||||
<path d="M7.4 11.4 24.5 7l1.9 5Z" />
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="M23.4 18.3h6.1v5.6h-6.1a2.8 2.8 0 0 1 0-5.6Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-dot"
|
||||
cx="23.8"
|
||||
cy="21.1"
|
||||
r="1"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
<strong data-i18n="manager_center.wallet"
|
||||
>Wallet</strong
|
||||
>
|
||||
<em id="managerWallet"></em>
|
||||
</span>
|
||||
<i aria-hidden="true">›</i>
|
||||
</button>
|
||||
<button class="manager-asset-item" type="button">
|
||||
<span
|
||||
class="manager-asset-icon bank"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path d="M6 11h24v16H6Z" />
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="M6 15h24v3H6ZM10 22h7v2.4h-7Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span>
|
||||
<strong data-i18n="manager_center.bank_cards"
|
||||
>Bank Cards</strong
|
||||
>
|
||||
<em id="managerBankCards"></em>
|
||||
</span>
|
||||
<i aria-hidden="true">›</i>
|
||||
</button>
|
||||
</section> -->
|
||||
|
||||
<section class="card manager-actions-card">
|
||||
<!-- 操作入口按 overview.permissions 显隐:renderActionPermissions 按 data-action 对应的权限位隐藏未授权入口 -->
|
||||
<div class="manager-action-grid" aria-label="Manager tools">
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-send-frame"
|
||||
type="button"
|
||||
data-action="send-frame"
|
||||
>
|
||||
<span
|
||||
class="tile-icon frame-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<path d="M10 10h28v28H10Z" />
|
||||
<path d="M16 16h16v16H16Z" />
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M10 18h6M32 18h6M10 30h6M32 30h6"
|
||||
d="M9 4.5h18A4.5 4.5 0 0 1 31.5 9v18a4.5 4.5 0 0 1-4.5 4.5H9A4.5 4.5 0 0 1 4.5 27V9A4.5 4.5 0 0 1 9 4.5Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-cutout"
|
||||
cx="18"
|
||||
cy="14.6"
|
||||
r="4.8"
|
||||
/>
|
||||
<path
|
||||
class="icon-cutout"
|
||||
d="M10.4 28c1.2-5.1 3.8-7.6 7.6-7.6s6.4 2.5 7.6 7.6Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.send_frame"
|
||||
<span data-i18n="manager_center.send_frame"
|
||||
>Send Frame</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-send-car"
|
||||
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" />
|
||||
<span
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M15 35a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"
|
||||
d="m6.1 16 2.2-6a3.2 3.2 0 0 1 3-2.1h13.4a3.2 3.2 0 0 1 3 2.1l2.2 6c1.6.5 2.7 1.8 2.7 3.5v6.1a1.8 1.8 0 0 1-1.8 1.8h-1.6a1.8 1.8 0 0 1-1.8-1.8v-1.1H8.6v1.1a1.8 1.8 0 0 1-1.8 1.8H5.2a1.8 1.8 0 0 1-1.8-1.8v-6.1c0-1.7 1.1-3 2.7-3.5Z"
|
||||
/>
|
||||
<path
|
||||
d="M33 35a3 3 0 1 0 0 6 3 3 0 0 0 0-6Z"
|
||||
class="icon-cutout"
|
||||
d="m9.4 15.3 1.5-4.3c.2-.6.8-1 1.4-1h11.4c.6 0 1.2.4 1.4 1l1.5 4.3Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-cutout"
|
||||
cx="9.9"
|
||||
cy="20.3"
|
||||
r="1.7"
|
||||
/>
|
||||
<circle
|
||||
class="icon-cutout"
|
||||
cx="26.1"
|
||||
cy="20.3"
|
||||
r="1.7"
|
||||
/>
|
||||
<path d="M18 18h12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.send_car"
|
||||
<span data-i18n="manager_center.send_car"
|
||||
>Send Car</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-send-badge"
|
||||
type="button"
|
||||
data-action="send-badge"
|
||||
>
|
||||
<span
|
||||
class="tile-icon badge-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M24 7 37 13v10c0 9-5.2 15-13 18-7.8-3-13-9-13-18V13Z"
|
||||
d="m12.6 20.9-2.8 10 5.3-2.3 2.9 3.9 2.9-3.9 5.3 2.3-2.8-10Z"
|
||||
/>
|
||||
<circle cx="18" cy="13.4" r="9.4" />
|
||||
<path
|
||||
d="m24 16 2.5 5.1 5.6.8-4.1 4 .9 5.7-4.9-2.7-4.9 2.7.9-5.7-4.1-4 5.6-.8Z"
|
||||
class="icon-cutout"
|
||||
d="m18 8.4 1.6 3.1 3.4.5-2.5 2.4.6 3.4-3.1-1.6-3.1 1.6.6-3.4-2.5-2.4 3.4-.5Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.send_badge"
|
||||
<span data-i18n="manager_center.send_badge"
|
||||
>Send Badge</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-send-vip"
|
||||
type="button"
|
||||
data-action="send-vip"
|
||||
>
|
||||
<span class="tile-icon vip-icon" aria-hidden="true">
|
||||
<svg viewBox="0 0 48 48">
|
||||
<span
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="m9 18 8 8 7-14 7 14 8-8-4 20H13Z"
|
||||
d="m4.8 11.4 6.4 4.9L18 7.6l6.8 8.7 6.4-4.9-2.6 14.3a2.6 2.6 0 0 1-2.6 2.1H10a2.6 2.6 0 0 1-2.6-2.1Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-cutout"
|
||||
cx="18"
|
||||
cy="21.4"
|
||||
r="2"
|
||||
/>
|
||||
<path d="M13 38h22" />
|
||||
<path d="M20 29h8" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.send_vip"
|
||||
<span data-i18n="manager_center.send_vip"
|
||||
>Send VIP</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-update-level"
|
||||
type="button"
|
||||
data-action="update-user-level"
|
||||
>
|
||||
<span
|
||||
class="tile-icon level-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path d="M5.5 29.5V20h5.4v9.5Z" />
|
||||
<path d="M15.3 29.5V13.8h5.4v15.7Z" />
|
||||
<path d="M25.1 29.5V7.5h5.4v22Z" />
|
||||
<path
|
||||
d="M24 7a10 10 0 1 0 0 20 10 10 0 0 0 0-20Z"
|
||||
d="M6.2 13.2 12.9 6.5H8.5V3.7h9v9h-2.8V8.5l-6.6 6.6Z"
|
||||
/>
|
||||
<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
|
||||
<span data-i18n="manager_center.user_level"
|
||||
>User Level</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-bd"
|
||||
type="button"
|
||||
data-action="add-bd-leader"
|
||||
>
|
||||
<span
|
||||
class="tile-icon bd-leader-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<circle cx="15" cy="11.5" r="6" />
|
||||
<path
|
||||
d="M24 7a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
|
||||
d="M5.5 30.5c1.4-7.1 4.6-10.6 9.5-10.6 4.2 0 7.1 2.5 8.8 7.5v3.1Z"
|
||||
/>
|
||||
<circle cx="27" cy="24.8" r="5.8" />
|
||||
<path
|
||||
d="M9 42c2.3-8 7.4-12 15-12s12.7 4 15 12"
|
||||
class="icon-cutout"
|
||||
d="M26 20.8h2v3h3v2h-3v3h-2v-3h-3v-2h3Z"
|
||||
/>
|
||||
<path d="M36 8v10M31 13h10" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.add_bd_leader"
|
||||
>Add BD Leader</span
|
||||
<span data-i18n="manager_center.bd_leader"
|
||||
>BD Leader</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-invite-admin"
|
||||
type="button"
|
||||
data-action="add-admin"
|
||||
>
|
||||
<span
|
||||
class="tile-icon admin-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<circle cx="15" cy="11.5" r="6" />
|
||||
<path
|
||||
d="M24 7a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
|
||||
d="M5.5 30.5c1.4-7.1 4.6-10.6 9.5-10.6s8.1 3.5 9.5 10.6Z"
|
||||
/>
|
||||
<circle cx="26.5" cy="22.8" r="5.4" />
|
||||
<path
|
||||
d="M9 42c2.3-8 7.4-12 15-12s12.7 4 15 12"
|
||||
/>
|
||||
<path
|
||||
d="m35 9 2.2 4.4 4.8.7-3.5 3.4.8 4.8-4.3-2.3-4.3 2.3.8-4.8-3.5-3.4 4.8-.7Z"
|
||||
class="icon-cutout"
|
||||
d="m26.5 19.3 1 2 2.2.3-1.6 1.5.4 2.2-2-1-2 1 .4-2.2-1.6-1.5 2.2-.3Z"
|
||||
/>
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.add_admin"
|
||||
>Add Admin</span
|
||||
>
|
||||
<span data-i18n="manager_center.admin">Admin</span>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile"
|
||||
class="manager-action-item action-super"
|
||||
type="button"
|
||||
data-action="add-superadmin"
|
||||
>
|
||||
<span
|
||||
class="tile-icon superadmin-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<path
|
||||
d="M24 7a9 9 0 1 0 0 18 9 9 0 0 0 0-18Z"
|
||||
d="M18 5 29 10v8.2c0 7-4.3 10.8-11 13.3-6.7-2.5-11-6.3-11-13.3V10Z"
|
||||
/>
|
||||
<path
|
||||
d="M9 42c2.3-8 7.4-12 15-12s12.7 4 15 12"
|
||||
class="icon-cutout"
|
||||
d="m18 11 1.8 3.6 4 .6-2.9 2.8.7 4-3.6-1.9-3.6 1.9.7-4-2.9-2.8 4-.6Z"
|
||||
/>
|
||||
<path
|
||||
d="m34 8 2.5 5 5.5.8-4 3.9.9 5.5-4.9-2.6-4.9 2.6.9-5.5-4-3.9 5.5-.8Z"
|
||||
/>
|
||||
<path d="M34 12v8M30 16h8" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.add_superadmin"
|
||||
>Add Superadmin</span
|
||||
<span data-i18n="manager_center.super_admin"
|
||||
>Super Admin</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile manager-tile-wide"
|
||||
type="button"
|
||||
data-action="transfer-user-country"
|
||||
>
|
||||
<span
|
||||
class="tile-icon country-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<path
|
||||
d="M24 42c8-8 13-15 13-23A13 13 0 1 0 11 19c0 8 5 15 13 23Z"
|
||||
/>
|
||||
<path d="M18 19h12" />
|
||||
<path d="M24 13v12" />
|
||||
</svg>
|
||||
</span>
|
||||
<span
|
||||
class="tile-title"
|
||||
data-i18n="manager_center.transfer_user_country"
|
||||
>Transfer User Country</span
|
||||
>
|
||||
</button>
|
||||
|
||||
<button
|
||||
class="manager-tile manager-tile-wide"
|
||||
class="manager-action-item action-block-users"
|
||||
type="button"
|
||||
data-action="block-user"
|
||||
>
|
||||
<span
|
||||
class="tile-icon block-icon"
|
||||
class="manager-action-icon"
|
||||
aria-hidden="true"
|
||||
>
|
||||
<svg viewBox="0 0 48 48">
|
||||
<svg viewBox="0 0 36 36">
|
||||
<circle cx="16" cy="11.5" r="5.8" />
|
||||
<path
|
||||
d="M18 22a8 8 0 1 0 0-16 8 8 0 0 0 0 16Z"
|
||||
d="M7.2 28.8c1.3-6.9 4.2-10.3 8.8-10.3s7.5 3.4 8.8 10.3Z"
|
||||
/>
|
||||
<circle
|
||||
class="icon-cutout"
|
||||
cx="26"
|
||||
cy="23.5"
|
||||
r="7.4"
|
||||
/>
|
||||
<path
|
||||
d="M5 42c1.8-9.2 6.2-13.8 13-13.8 4.3 0 7.6 1.8 10 5.3"
|
||||
d="M21.4 18.9a7.4 7.4 0 0 1 9.2 9.2Z"
|
||||
/>
|
||||
<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
|
||||
<span data-i18n="manager_center.block_users"
|
||||
>Block Users</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
@ -519,10 +712,12 @@
|
||||
</div>
|
||||
|
||||
<script src="../../common/toast.js"></script>
|
||||
<script src="../../common/loading.js?v=20260709-lalu-loading"></script>
|
||||
<script src="../../common/skeleton.js?v=20260709-skeleton-mascot"></script>
|
||||
<script src="../../common/api.js?v=20260624-manager-badge-country"></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=20260705-manager-vip-permission"></script>
|
||||
<script src="./script.js?v=20260709-page-skeleton"></script>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@ -70,7 +70,7 @@
|
||||
resourceType: '',
|
||||
title: '',
|
||||
searchedUser: null,
|
||||
selectedResourceId: '',
|
||||
selectedResourceIds: [],
|
||||
selectedResourceDurationMs: DEFAULT_RESOURCE_GRANT_DURATION_MS,
|
||||
selectedVipLevel: 0,
|
||||
submitting: false,
|
||||
@ -148,6 +148,46 @@
|
||||
return user.avatar || user.target_avatar || '';
|
||||
}
|
||||
|
||||
function resourceImageUrl(resource) {
|
||||
return (
|
||||
resource.preview_url ||
|
||||
resource.previewUrl ||
|
||||
resource.asset_url ||
|
||||
resource.assetUrl ||
|
||||
resource.icon_url ||
|
||||
resource.iconUrl ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function resourceKindLabel() {
|
||||
if (grantState.action === 'send-frame') return 'Frame';
|
||||
if (grantState.action === 'send-car') return 'Car';
|
||||
if (grantState.action === 'send-badge') return 'Badge';
|
||||
return 'Resource';
|
||||
}
|
||||
|
||||
function currentVip(user) {
|
||||
return user.current_vip || user.currentVip || user.vip || null;
|
||||
}
|
||||
|
||||
function currentVipLevel(user) {
|
||||
var vip = currentVip(user);
|
||||
if (!vip || !vip.active) return 0;
|
||||
return Number(vip.level) || 0;
|
||||
}
|
||||
|
||||
function currentVipStatusText(user) {
|
||||
var vip = currentVip(user);
|
||||
if (!vip || !vip.active || !vip.level) return 'Current VIP: None';
|
||||
var label = vip.name || 'VIP ' + vip.level;
|
||||
var expiresAt = Number(vip.expires_at_ms || vip.expiresAtMs || 0);
|
||||
if (expiresAt > 0) {
|
||||
return label + ' active · Expires ' + formatDateTime(expiresAt);
|
||||
}
|
||||
return label + ' active';
|
||||
}
|
||||
|
||||
function initial(text) {
|
||||
return String(text || 'M')
|
||||
.trim()
|
||||
@ -175,6 +215,246 @@
|
||||
node.textContent = initial(userName(user) || userDisplayId(user));
|
||||
}
|
||||
|
||||
function textValue(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
return String(value);
|
||||
}
|
||||
|
||||
function isLocalPreviewError(error) {
|
||||
var host = window.location && window.location.hostname;
|
||||
var isLocalHost =
|
||||
host === 'localhost' ||
|
||||
host === '127.0.0.1' ||
|
||||
host === '::1' ||
|
||||
host === '';
|
||||
var message = (error && error.message) || '';
|
||||
return isLocalHost && /failed to fetch|network/i.test(message);
|
||||
}
|
||||
|
||||
function firstValue(source, keys) {
|
||||
source = source || {};
|
||||
for (var i = 0; i < keys.length; i += 1) {
|
||||
var key = keys[i];
|
||||
if (
|
||||
Object.prototype.hasOwnProperty.call(source, key) &&
|
||||
source[key] !== undefined &&
|
||||
source[key] !== null &&
|
||||
source[key] !== ''
|
||||
) {
|
||||
return source[key];
|
||||
}
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function firstNestedValue(sources, keys) {
|
||||
for (var i = 0; i < sources.length; i += 1) {
|
||||
var value = firstValue(sources[i], keys);
|
||||
if (value !== undefined) return value;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
|
||||
function formatMoney(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
if (typeof value === 'string' && value.indexOf('$') >= 0) {
|
||||
return value;
|
||||
}
|
||||
var number = Number(String(value).replace(/,/g, ''));
|
||||
if (!Number.isFinite(number)) return String(value);
|
||||
return (
|
||||
'$' +
|
||||
number.toLocaleString('en-US', {
|
||||
maximumFractionDigits: number % 1 === 0 ? 0 : 2,
|
||||
minimumFractionDigits: number % 1 === 0 ? 0 : 2,
|
||||
})
|
||||
);
|
||||
}
|
||||
|
||||
function formatPercent(value, fallback) {
|
||||
if (value === undefined || value === null || value === '') {
|
||||
return fallback;
|
||||
}
|
||||
if (typeof value === 'string' && value.indexOf('%') >= 0) {
|
||||
// 服务端已带符号下发;只对正数补 +,零和负数原样透传,避免出现 "+0%"。
|
||||
var parsed = parseFloat(value);
|
||||
if (!Number.isFinite(parsed) || parsed <= 0) {
|
||||
return value;
|
||||
}
|
||||
return '+' + value.replace(/^\+/, '');
|
||||
}
|
||||
var number = Number(value);
|
||||
if (!Number.isFinite(number)) return String(value);
|
||||
if (Math.abs(number) > 0 && Math.abs(number) <= 1) {
|
||||
number *= 100;
|
||||
}
|
||||
return (number > 0 ? '+' : '') + Math.round(number) + '%';
|
||||
}
|
||||
|
||||
function setText(id, value) {
|
||||
var node = $(id);
|
||||
if (node) node.textContent = value;
|
||||
}
|
||||
|
||||
function updateManagerAvatar(profile) {
|
||||
var image = $('managerAvatar');
|
||||
var fallback = $('managerAvatarFallback');
|
||||
if (!image || !fallback) return;
|
||||
// 没有头像时展示首字母占位,不再使用演示头像假数据。
|
||||
var url = avatarUrl(profile);
|
||||
if (url) {
|
||||
image.src = url;
|
||||
image.hidden = false;
|
||||
fallback.hidden = true;
|
||||
return;
|
||||
}
|
||||
image.hidden = true;
|
||||
fallback.hidden = false;
|
||||
fallback.textContent = initial(
|
||||
userName(profile) || userDisplayId(profile)
|
||||
);
|
||||
}
|
||||
|
||||
function dashboardSources() {
|
||||
var overview = pageState.overview || {};
|
||||
return [
|
||||
overview.dashboard,
|
||||
overview.summary,
|
||||
overview.metrics,
|
||||
overview.statistics,
|
||||
overview.wallet,
|
||||
overview,
|
||||
];
|
||||
}
|
||||
|
||||
function renderDashboard() {
|
||||
var overview = pageState.overview || {};
|
||||
var profile =
|
||||
overview.user ||
|
||||
overview.profile ||
|
||||
overview.manager ||
|
||||
overview.current_user ||
|
||||
overview.currentUser ||
|
||||
overview;
|
||||
updateManagerAvatar(profile || {});
|
||||
setText(
|
||||
'managerName',
|
||||
textValue(
|
||||
firstValue(profile || {}, [
|
||||
'username',
|
||||
'name',
|
||||
'nickname',
|
||||
'target_username',
|
||||
]),
|
||||
'--'
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerDisplayId',
|
||||
textValue(
|
||||
userDisplayId(profile || {}) || userId(profile || {}),
|
||||
'--'
|
||||
)
|
||||
);
|
||||
|
||||
var sources = dashboardSources();
|
||||
// 页面不再内置任何设计假数据:加载中由骨架屏占位,渲染时字段缺失(聚合降级/请求失败/无 API)
|
||||
// 一律显示中性占位符,避免把演示数值误当真实团队工资。
|
||||
var fallback = {
|
||||
thisMonth: '--',
|
||||
lastMonth: '--',
|
||||
hosts: '--',
|
||||
growth: '--',
|
||||
wallet: '--',
|
||||
bankCards: '--',
|
||||
};
|
||||
setText(
|
||||
'managerThisMonthSalary',
|
||||
formatMoney(
|
||||
firstNestedValue(sources, [
|
||||
'this_month_salary',
|
||||
'thisMonthSalary',
|
||||
'current_month_salary',
|
||||
'currentMonthSalary',
|
||||
'team_salary_this_month',
|
||||
'teamSalaryThisMonth',
|
||||
]),
|
||||
fallback.thisMonth
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerLastMonthSalary',
|
||||
formatMoney(
|
||||
firstNestedValue(sources, [
|
||||
'last_month_salary',
|
||||
'lastMonthSalary',
|
||||
'previous_month_salary',
|
||||
'previousMonthSalary',
|
||||
'team_salary_last_month',
|
||||
'teamSalaryLastMonth',
|
||||
]),
|
||||
fallback.lastMonth
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerActiveHosts',
|
||||
textValue(
|
||||
firstNestedValue(sources, [
|
||||
'active_hosts',
|
||||
'activeHosts',
|
||||
'total_active_hosts',
|
||||
'totalActiveHosts',
|
||||
'host_count',
|
||||
'hostCount',
|
||||
]),
|
||||
fallback.hosts
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerGrowth',
|
||||
formatPercent(
|
||||
firstNestedValue(sources, [
|
||||
'growth',
|
||||
'growth_rate',
|
||||
'growthRate',
|
||||
'salary_growth',
|
||||
'salaryGrowth',
|
||||
'month_growth',
|
||||
'monthGrowth',
|
||||
]),
|
||||
fallback.growth
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerWallet',
|
||||
formatMoney(
|
||||
firstNestedValue(sources, [
|
||||
'wallet',
|
||||
'wallet_balance',
|
||||
'walletBalance',
|
||||
'balance',
|
||||
]),
|
||||
fallback.wallet
|
||||
)
|
||||
);
|
||||
setText(
|
||||
'managerBankCards',
|
||||
textValue(
|
||||
firstNestedValue(sources, [
|
||||
'bank_cards',
|
||||
'bankCards',
|
||||
'bank_card_count',
|
||||
'bankCardCount',
|
||||
]),
|
||||
fallback.bankCards
|
||||
)
|
||||
);
|
||||
}
|
||||
|
||||
function formatDateTime(ms) {
|
||||
if (!ms) return 'Never';
|
||||
var date = new Date(Number(ms));
|
||||
@ -187,10 +467,17 @@
|
||||
}
|
||||
|
||||
function setPageLoading(loading) {
|
||||
// 首屏加载态用页面级骨架屏整层覆盖(common/skeleton.js),数据就绪后整层淡出;
|
||||
// is-loading 状态类保留给页面内其他降级样式使用。
|
||||
pageState.loading = loading;
|
||||
var container = root();
|
||||
if (!container) return;
|
||||
container.classList.toggle('is-loading', loading);
|
||||
if (container) container.classList.toggle('is-loading', loading);
|
||||
if (!window.HyAppSkeleton) return;
|
||||
if (loading) {
|
||||
window.HyAppSkeleton.show();
|
||||
} else {
|
||||
window.HyAppSkeleton.hide();
|
||||
}
|
||||
}
|
||||
|
||||
function setDenied(denied) {
|
||||
@ -201,6 +488,7 @@
|
||||
}
|
||||
|
||||
function normalizeUser(item) {
|
||||
var vip = item.current_vip || item.currentVip || item.vip || null;
|
||||
return {
|
||||
user_id: item.user_id || item.userId || item.target_user_id || '',
|
||||
display_user_id:
|
||||
@ -212,6 +500,16 @@
|
||||
avatar: item.avatar || item.target_avatar || '',
|
||||
country: item.country || '',
|
||||
levels: item.levels || {},
|
||||
current_vip: vip
|
||||
? {
|
||||
level: Number(vip.level) || 0,
|
||||
name: vip.name || '',
|
||||
active: Boolean(vip.active),
|
||||
started_at_ms: vip.started_at_ms || vip.startedAtMs || 0,
|
||||
expires_at_ms: vip.expires_at_ms || vip.expiresAtMs || 0,
|
||||
updated_at_ms: vip.updated_at_ms || vip.updatedAtMs || 0,
|
||||
}
|
||||
: null,
|
||||
};
|
||||
}
|
||||
|
||||
@ -322,16 +620,21 @@
|
||||
if (!api || !api.searchUsers) {
|
||||
return Promise.reject(new Error('API not available.'));
|
||||
}
|
||||
return api.searchUsers(keyword, 20, scope, action).then(function (data) {
|
||||
var items = (data && data.items) || [];
|
||||
return items.map(normalizeUser);
|
||||
});
|
||||
return api
|
||||
.searchUsers(keyword, 20, scope, action)
|
||||
.then(function (data) {
|
||||
var items = (data && data.items) || [];
|
||||
return items.map(normalizeUser);
|
||||
});
|
||||
}
|
||||
|
||||
function loadOverview() {
|
||||
if (!api || !api.overview) {
|
||||
setDenied(true);
|
||||
toast('Permission denied.');
|
||||
// 无 API 注入(本地纯静态预览等)时立即渲染一次,把骨架屏收敛成中性占位符。
|
||||
pageState.permissions = normalizePermissions({});
|
||||
renderDashboard();
|
||||
renderActionPermissions();
|
||||
setDenied(false);
|
||||
return Promise.resolve();
|
||||
}
|
||||
setPageLoading(true);
|
||||
@ -342,6 +645,7 @@
|
||||
pageState.permissions = normalizePermissions(
|
||||
pageState.overview.permissions || pageState.overview
|
||||
);
|
||||
renderDashboard();
|
||||
renderActionPermissions();
|
||||
setDenied(false);
|
||||
return canUseAction('block-user')
|
||||
@ -349,7 +653,16 @@
|
||||
: Promise.resolve();
|
||||
})
|
||||
.catch(function (error) {
|
||||
if (isLocalPreviewError(error)) {
|
||||
pageState.permissions = normalizePermissions({});
|
||||
renderDashboard();
|
||||
renderActionPermissions();
|
||||
setDenied(false);
|
||||
return;
|
||||
}
|
||||
// 接口失败也要渲染一次:把骨架屏收敛成中性占位符,页面不残留加载态。
|
||||
setDenied(true);
|
||||
renderDashboard();
|
||||
showError(error);
|
||||
})
|
||||
.finally(function () {
|
||||
@ -389,6 +702,11 @@
|
||||
copy.appendChild(
|
||||
createNode('div', 'searched-meta', 'UID: ' + userDisplayId(user))
|
||||
);
|
||||
if (currentVip(user)) {
|
||||
copy.appendChild(
|
||||
createNode('div', 'searched-vip', currentVipStatusText(user))
|
||||
);
|
||||
}
|
||||
var button = createNode('button', 'user-block-button', actionText);
|
||||
button.type = 'button';
|
||||
button.addEventListener('click', onAction);
|
||||
@ -681,7 +999,8 @@
|
||||
toast('Enter user ID.');
|
||||
return;
|
||||
}
|
||||
apiSearchUser(value, '', grantState.action)
|
||||
// 封禁搜索只校验 Block User 权限;不能借用赠送弹窗状态,否则会按资源/VIP 赠送权限误判。
|
||||
apiSearchUser(value, '', 'block-user')
|
||||
.then(function (items) {
|
||||
if (!items.length) {
|
||||
blockState.searchedUser = null;
|
||||
@ -733,7 +1052,8 @@
|
||||
toast('Enter user ID.');
|
||||
return;
|
||||
}
|
||||
apiSearchUser(value)
|
||||
// 等级调整搜索按自己的能力开关校验,避免资源赠送关闭时误挡等级操作。
|
||||
apiSearchUser(value, '', 'update-user-level')
|
||||
.then(function (items) {
|
||||
if (!items.length) {
|
||||
levelState.searchedUser = null;
|
||||
@ -815,7 +1135,7 @@
|
||||
grantState.resourceType = RESOURCE_TYPES[action] || '';
|
||||
grantState.title = ACTION_LABELS[action] || 'Send';
|
||||
grantState.searchedUser = null;
|
||||
grantState.selectedResourceId = '';
|
||||
grantState.selectedResourceIds = [];
|
||||
grantState.selectedResourceDurationMs =
|
||||
DEFAULT_RESOURCE_GRANT_DURATION_MS;
|
||||
grantState.selectedVipLevel = 0;
|
||||
@ -860,7 +1180,8 @@
|
||||
toast('Enter user ID.');
|
||||
return;
|
||||
}
|
||||
apiSearchUser(value)
|
||||
// 搜索入口也必须携带当前赠送动作;Send VIP 只校验 VIP 赠送权限,不能落回资源赠送权限导致误拒。
|
||||
apiSearchUser(value, '', grantState.action)
|
||||
.then(function (items) {
|
||||
if (!items.length) {
|
||||
grantState.searchedUser = null;
|
||||
@ -890,6 +1211,10 @@
|
||||
var grid = $('grantOptionGrid');
|
||||
if (!grid) return;
|
||||
grid.innerHTML = '';
|
||||
grid.classList.toggle(
|
||||
'is-resource-grid',
|
||||
grantState.action !== 'send-vip'
|
||||
);
|
||||
var durationTitle = $('grantDurationTitle');
|
||||
var durationGrid = $('grantDurationGrid');
|
||||
if (durationTitle) {
|
||||
@ -905,24 +1230,55 @@
|
||||
}
|
||||
if (grantState.action === 'send-vip') {
|
||||
$('grantOptionTitle').textContent = 'Select VIP level';
|
||||
var activeVipLevel = currentVipLevel(grantState.searchedUser || {});
|
||||
if (
|
||||
grantState.selectedVipLevel &&
|
||||
activeVipLevel > grantState.selectedVipLevel
|
||||
) {
|
||||
grantState.selectedVipLevel = 0;
|
||||
}
|
||||
// VIP grants must stay on the explicit product allow-list so the UI never sends a level the backend has not opened.
|
||||
VIP_GRANT_LEVELS.forEach(function (level) {
|
||||
var button = createNode('button', '', 'VIP ' + level);
|
||||
var blockedByCurrentVip = activeVipLevel > level;
|
||||
button.type = 'button';
|
||||
button.disabled = blockedByCurrentVip;
|
||||
if (blockedByCurrentVip) {
|
||||
button.title = 'Current VIP is higher';
|
||||
}
|
||||
button.classList.toggle(
|
||||
'is-selected',
|
||||
grantState.selectedVipLevel === level
|
||||
);
|
||||
button.addEventListener('click', function () {
|
||||
if (blockedByCurrentVip) return;
|
||||
grantState.selectedVipLevel = level;
|
||||
renderGrantOptions();
|
||||
});
|
||||
grid.appendChild(button);
|
||||
});
|
||||
if (activeVipLevel > 1) {
|
||||
grid.appendChild(
|
||||
createNode(
|
||||
'div',
|
||||
'vip-grant-note',
|
||||
activeVipLevel > Math.max.apply(null, VIP_GRANT_LEVELS)
|
||||
? 'Current VIP is higher than manager-center gift levels.'
|
||||
: 'Lower VIP levels are disabled because the user already has a higher VIP.'
|
||||
)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
$('grantOptionTitle').textContent = 'Select resource';
|
||||
$('grantOptionTitle').textContent = 'Select ' + resourceKindLabel();
|
||||
var resources =
|
||||
pageState.resourcesByType[grantState.resourceType] || [];
|
||||
var selectedResourceSet = grantState.selectedResourceIds.reduce(
|
||||
function (result, resourceId) {
|
||||
result[String(resourceId)] = true;
|
||||
return result;
|
||||
},
|
||||
{}
|
||||
);
|
||||
if (!resources.length) {
|
||||
grid.appendChild(
|
||||
createNode('div', 'blocked-empty', 'No resource available.')
|
||||
@ -930,20 +1286,56 @@
|
||||
}
|
||||
resources.forEach(function (resource) {
|
||||
var button = createNode('button', 'grant-resource-option');
|
||||
var imageURL = resourceImageUrl(resource);
|
||||
var resourceID = resource.resource_id;
|
||||
var resourceKey = String(resourceID);
|
||||
button.type = 'button';
|
||||
button.setAttribute(
|
||||
'aria-label',
|
||||
'Select ' + resourceKindLabel()
|
||||
);
|
||||
button.classList.toggle(
|
||||
'is-selected',
|
||||
String(grantState.selectedResourceId) ===
|
||||
String(resource.resource_id)
|
||||
!!selectedResourceSet[resourceKey]
|
||||
);
|
||||
button.textContent =
|
||||
resource.name || 'Resource #' + resource.resource_id;
|
||||
if (imageURL) {
|
||||
var media = createNode('span', 'grant-resource-media');
|
||||
var img = document.createElement('img');
|
||||
img.src = imageURL;
|
||||
img.alt = '';
|
||||
media.appendChild(img);
|
||||
button.appendChild(media);
|
||||
} else {
|
||||
button.appendChild(
|
||||
createNode('span', 'grant-resource-fallback')
|
||||
);
|
||||
}
|
||||
button.addEventListener('click', function () {
|
||||
grantState.selectedResourceId = resource.resource_id;
|
||||
// 资源赠送是素材发放,不是互斥装备选择;同一弹窗允许一次选择多个素材,由后端拆成独立 grant 命令保证可重试。
|
||||
if (selectedResourceSet[resourceKey]) {
|
||||
grantState.selectedResourceIds =
|
||||
grantState.selectedResourceIds.filter(
|
||||
function (selectedID) {
|
||||
return String(selectedID) !== resourceKey;
|
||||
}
|
||||
);
|
||||
} else {
|
||||
grantState.selectedResourceIds =
|
||||
grantState.selectedResourceIds.concat(resourceID);
|
||||
}
|
||||
renderGrantOptions();
|
||||
});
|
||||
grid.appendChild(button);
|
||||
});
|
||||
if (grantState.selectedResourceIds.length) {
|
||||
grid.appendChild(
|
||||
createNode(
|
||||
'div',
|
||||
'resource-selected-note',
|
||||
grantState.selectedResourceIds.length + ' selected'
|
||||
)
|
||||
);
|
||||
}
|
||||
if (durationGrid) {
|
||||
RESOURCE_GRANT_DURATIONS.forEach(function (item) {
|
||||
var durationButton = createNode('button', '', item.label);
|
||||
@ -966,8 +1358,10 @@
|
||||
!grantState.searchedUser ||
|
||||
grantState.submitting ||
|
||||
(grantState.action === 'send-vip'
|
||||
? !grantState.selectedVipLevel
|
||||
: !grantState.selectedResourceId ||
|
||||
? !grantState.selectedVipLevel ||
|
||||
currentVipLevel(grantState.searchedUser) >
|
||||
grantState.selectedVipLevel
|
||||
: !grantState.selectedResourceIds.length ||
|
||||
!grantState.selectedResourceDurationMs);
|
||||
}
|
||||
}
|
||||
@ -986,7 +1380,7 @@
|
||||
payload.level = grantState.selectedVipLevel;
|
||||
request = api.grantVip(payload);
|
||||
} else {
|
||||
payload.resource_id = grantState.selectedResourceId;
|
||||
payload.resource_ids = grantState.selectedResourceIds.slice();
|
||||
payload.duration_ms = grantState.selectedResourceDurationMs;
|
||||
request = api.grantResource(payload);
|
||||
}
|
||||
@ -1065,7 +1459,8 @@
|
||||
toast('Enter user ID.');
|
||||
return;
|
||||
}
|
||||
apiSearchUser(value)
|
||||
// BD/Admin/Superadmin 共用弹窗,搜索阶段先按当前按钮的能力开关过滤;写入口仍会二次校验真实落库能力。
|
||||
apiSearchUser(value, '', bdLeaderState.action)
|
||||
.then(function (items) {
|
||||
if (!items.length) {
|
||||
bdLeaderState.searchedUser = null;
|
||||
@ -1257,7 +1652,8 @@
|
||||
toast('Enter user ID.');
|
||||
return;
|
||||
}
|
||||
apiSearchUser(value, 'all')
|
||||
// 转国家是唯一允许跨国家搜索的入口,必须同时携带 scope=all 和动作,避免额外依赖资源赠送权限。
|
||||
apiSearchUser(value, 'all', 'transfer-user-country')
|
||||
.then(function (items) {
|
||||
if (!items.length) {
|
||||
countryState.searchedUser = null;
|
||||
@ -1362,9 +1758,30 @@
|
||||
});
|
||||
}
|
||||
|
||||
function copyManagerId() {
|
||||
var value = $('managerDisplayId')
|
||||
? $('managerDisplayId').textContent.trim()
|
||||
: '';
|
||||
if (!value) return;
|
||||
if (window.navigator && window.navigator.clipboard) {
|
||||
window.navigator.clipboard
|
||||
.writeText(value)
|
||||
.then(function () {
|
||||
toast('Copied.');
|
||||
})
|
||||
.catch(function () {
|
||||
toast(value);
|
||||
});
|
||||
return;
|
||||
}
|
||||
toast(value);
|
||||
}
|
||||
|
||||
function bindEvents() {
|
||||
var backButton = $('backButton');
|
||||
if (backButton) backButton.addEventListener('click', goBack);
|
||||
var copyButton = document.querySelector('.manager-copy-button');
|
||||
if (copyButton) copyButton.addEventListener('click', copyManagerId);
|
||||
document.querySelectorAll('[data-action]').forEach(function (button) {
|
||||
button.addEventListener('click', function () {
|
||||
var action = button.getAttribute('data-action');
|
||||
@ -1453,6 +1870,7 @@
|
||||
function init() {
|
||||
bindEvents();
|
||||
renderBlockedList();
|
||||
// 首屏不预渲染仪表盘:骨架屏保持到 loadOverview 的任一分支调用 renderDashboard 收敛。
|
||||
loadOverview();
|
||||
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
||||
window.HyAppBridge.ready({ page: 'manager-center' });
|
||||
|
||||
@ -1,3 +1,544 @@
|
||||
.manager-center {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 15% 0,
|
||||
rgba(222, 206, 255, 0.58),
|
||||
rgba(251, 248, 255, 0) 28%
|
||||
),
|
||||
radial-gradient(
|
||||
circle at 92% 18%,
|
||||
rgba(229, 238, 255, 0.62),
|
||||
rgba(251, 248, 255, 0) 31%
|
||||
),
|
||||
var(--page-bg);
|
||||
}
|
||||
|
||||
.manager-center .hero-bg {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.manager-center .home-indicator {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.manager-center .title-bar {
|
||||
grid-template-columns: 52px minmax(0, 1fr) 52px;
|
||||
height: 72px;
|
||||
padding: 13px 18px 0;
|
||||
}
|
||||
|
||||
.manager-center .title-bar h1 {
|
||||
color: #11162e;
|
||||
font-size: 22px;
|
||||
font-weight: 820;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.manager-center .back-button,
|
||||
.manager-center .language-button {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
}
|
||||
|
||||
.manager-center .language-button {
|
||||
border: 0;
|
||||
border-radius: 50%;
|
||||
background: transparent;
|
||||
box-shadow: none;
|
||||
color: #11162e;
|
||||
}
|
||||
|
||||
.manager-center .language-button svg {
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
}
|
||||
|
||||
.manager-center .back-button path,
|
||||
.manager-center .language-button path,
|
||||
.manager-center .manager-copy-button path {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.manager-center .content {
|
||||
gap: 12px;
|
||||
padding: 10px 16px 30px;
|
||||
}
|
||||
|
||||
.manager-center .card {
|
||||
border: 1px solid rgba(125, 87, 199, 0.12);
|
||||
border-radius: 16px;
|
||||
background:
|
||||
linear-gradient(
|
||||
145deg,
|
||||
rgba(255, 255, 255, 0.98),
|
||||
rgba(255, 255, 255, 0.89)
|
||||
),
|
||||
var(--card-bg);
|
||||
box-shadow:
|
||||
0 18px 42px rgba(88, 66, 142, 0.075),
|
||||
inset 0 1px 0 rgba(255, 255, 255, 0.96);
|
||||
}
|
||||
|
||||
.manager-profile-card {
|
||||
display: grid;
|
||||
grid-template-columns: 82px minmax(0, 1fr) auto;
|
||||
gap: 11px;
|
||||
align-items: center;
|
||||
min-height: 112px;
|
||||
padding: 13px 12px;
|
||||
}
|
||||
|
||||
.manager-avatar-shell {
|
||||
position: relative;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border: 3px solid transparent;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
linear-gradient(#fff, #fff) padding-box,
|
||||
linear-gradient(145deg, #f4eaff, #b894ff) border-box;
|
||||
box-shadow: 0 8px 22px rgba(125, 87, 199, 0.2);
|
||||
}
|
||||
|
||||
.manager-avatar-image,
|
||||
.manager-avatar-fallback {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.manager-avatar-image {
|
||||
display: block;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.manager-avatar-fallback {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
background: linear-gradient(145deg, #f7f0ff, #e3d5ff);
|
||||
color: var(--primary-deep);
|
||||
font-size: 32px;
|
||||
font-weight: 950;
|
||||
}
|
||||
|
||||
.manager-profile-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 7px;
|
||||
}
|
||||
|
||||
.manager-profile-copy strong {
|
||||
overflow: hidden;
|
||||
color: #151936;
|
||||
font-size: 24px;
|
||||
font-weight: 820;
|
||||
line-height: 1;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.manager-role-badge {
|
||||
display: inline-flex;
|
||||
width: max-content;
|
||||
max-width: 100%;
|
||||
height: 24px;
|
||||
align-items: center;
|
||||
gap: 6px;
|
||||
padding: 0 10px;
|
||||
border-radius: 7px;
|
||||
background: linear-gradient(135deg, #a678ff, #8b5ce8);
|
||||
color: #fff;
|
||||
font-size: 14px;
|
||||
font-weight: 780;
|
||||
line-height: 24px;
|
||||
box-shadow: 0 8px 18px rgba(139, 92, 232, 0.22);
|
||||
}
|
||||
|
||||
.manager-role-badge svg {
|
||||
width: 14px;
|
||||
height: 14px;
|
||||
}
|
||||
|
||||
.manager-role-badge path {
|
||||
fill: currentColor;
|
||||
}
|
||||
|
||||
.manager-id-line {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
color: #837ba3;
|
||||
font-size: 14px;
|
||||
font-weight: 650;
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.manager-id-line > span:nth-child(2) {
|
||||
overflow: hidden;
|
||||
flex: 0 0 auto;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.manager-copy-button {
|
||||
display: flex;
|
||||
flex: 0 0 22px;
|
||||
width: 22px;
|
||||
height: 22px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
color: #837ba3;
|
||||
}
|
||||
|
||||
.manager-copy-button svg {
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
}
|
||||
|
||||
.manager-kpi-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.manager-kpi-card {
|
||||
position: relative;
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr);
|
||||
min-height: 128px;
|
||||
overflow: hidden;
|
||||
padding: 18px 13px 16px;
|
||||
}
|
||||
|
||||
.manager-kpi-icon {
|
||||
position: absolute;
|
||||
top: 18px;
|
||||
left: 13px;
|
||||
z-index: 1;
|
||||
display: flex;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.manager-kpi-icon svg {
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
}
|
||||
|
||||
.manager-kpi-icon path,
|
||||
.manager-kpi-icon circle,
|
||||
.manager-asset-icon path,
|
||||
.manager-asset-icon circle {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.manager-kpi-icon .icon-cutout,
|
||||
.manager-asset-icon .icon-cutout {
|
||||
fill: #fff;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.manager-kpi-icon .icon-dot {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.manager-asset-icon.wallet .icon-cutout,
|
||||
.manager-asset-icon.wallet .icon-dot {
|
||||
fill: #ff6c08;
|
||||
}
|
||||
|
||||
.manager-asset-icon.bank .icon-cutout {
|
||||
fill: #2f61ff;
|
||||
}
|
||||
|
||||
.manager-kpi-copy {
|
||||
position: relative;
|
||||
z-index: 2;
|
||||
display: grid;
|
||||
align-content: center;
|
||||
min-width: 0;
|
||||
gap: 6px;
|
||||
padding-left: 58px;
|
||||
}
|
||||
|
||||
.manager-kpi-copy span,
|
||||
.manager-kpi-copy em {
|
||||
color: #827f99;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 540;
|
||||
line-height: 1.12;
|
||||
}
|
||||
|
||||
.manager-kpi-copy strong {
|
||||
color: #ff660a;
|
||||
font-size: 25px;
|
||||
font-weight: 840;
|
||||
line-height: 1;
|
||||
letter-spacing: 0;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.kpi-hosts .manager-kpi-copy strong {
|
||||
color: #171b35;
|
||||
}
|
||||
|
||||
.kpi-growth .manager-kpi-copy strong {
|
||||
color: #ff424d;
|
||||
}
|
||||
|
||||
.kpi-wallet .manager-kpi-icon {
|
||||
background: #f2e9ff;
|
||||
color: #854fe8;
|
||||
}
|
||||
|
||||
.kpi-report .manager-kpi-icon {
|
||||
background: #eef4ff;
|
||||
color: #3478f6;
|
||||
}
|
||||
|
||||
.kpi-hosts .manager-kpi-icon {
|
||||
background: #e8fbf8;
|
||||
color: #10bda6;
|
||||
}
|
||||
|
||||
.kpi-growth .manager-kpi-icon {
|
||||
background: #fff0f4;
|
||||
color: #ff3d52;
|
||||
}
|
||||
|
||||
.manager-bars {
|
||||
position: absolute;
|
||||
right: 13px;
|
||||
bottom: 16px;
|
||||
width: 39px;
|
||||
height: 34px;
|
||||
opacity: 0.38;
|
||||
background:
|
||||
linear-gradient(currentColor, currentColor) 0 21px / 7px 13px no-repeat,
|
||||
linear-gradient(currentColor, currentColor) 11px 14px / 7px 20px
|
||||
no-repeat,
|
||||
linear-gradient(currentColor, currentColor) 22px 7px / 7px 27px
|
||||
no-repeat,
|
||||
linear-gradient(currentColor, currentColor) 33px 0 / 7px 34px no-repeat;
|
||||
}
|
||||
|
||||
.kpi-wallet .manager-bars {
|
||||
color: #eadcff;
|
||||
}
|
||||
|
||||
.kpi-report .manager-bars {
|
||||
color: #d9e9ff;
|
||||
}
|
||||
|
||||
.kpi-hosts .manager-bars {
|
||||
color: #d9fbf5;
|
||||
}
|
||||
|
||||
.kpi-growth .manager-bars {
|
||||
color: #ffe0e7;
|
||||
}
|
||||
|
||||
.manager-assets-card {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
min-height: 88px;
|
||||
margin-top: 12px;
|
||||
overflow: hidden;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.manager-asset-item {
|
||||
display: grid;
|
||||
grid-template-columns: 48px minmax(0, 1fr) 14px;
|
||||
gap: 10px;
|
||||
align-items: center;
|
||||
min-width: 0;
|
||||
min-height: 88px;
|
||||
padding: 16px 13px;
|
||||
text-align: left;
|
||||
}
|
||||
|
||||
.manager-asset-item + .manager-asset-item {
|
||||
border-left: 1px solid rgba(125, 87, 199, 0.1);
|
||||
}
|
||||
|
||||
.manager-asset-icon,
|
||||
.manager-action-icon {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
border-radius: 50%;
|
||||
}
|
||||
|
||||
.manager-asset-icon {
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.manager-asset-icon.wallet {
|
||||
background: linear-gradient(145deg, #ff8a16, #ff5a00);
|
||||
}
|
||||
|
||||
.manager-asset-icon.bank {
|
||||
background: linear-gradient(145deg, #4d73ff, #215dff);
|
||||
}
|
||||
|
||||
.manager-asset-icon svg {
|
||||
width: 29px;
|
||||
height: 29px;
|
||||
}
|
||||
|
||||
.manager-asset-item strong,
|
||||
.manager-action-item > span:last-child {
|
||||
overflow-wrap: anywhere;
|
||||
color: #1d2140;
|
||||
font-size: 14px;
|
||||
font-weight: 560;
|
||||
line-height: 1.15;
|
||||
}
|
||||
|
||||
.manager-asset-item strong {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.manager-action-item > span:last-child {
|
||||
max-width: 100%;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.manager-asset-item em {
|
||||
display: block;
|
||||
margin-top: 7px;
|
||||
color: #ff660a;
|
||||
font-size: 14px;
|
||||
font-style: normal;
|
||||
font-weight: 700;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.manager-asset-item:nth-child(2) em {
|
||||
color: #858296;
|
||||
font-weight: 540;
|
||||
}
|
||||
|
||||
.manager-asset-item i {
|
||||
color: #a8a4b7;
|
||||
font-size: 30px;
|
||||
font-style: normal;
|
||||
font-weight: 300;
|
||||
line-height: 1;
|
||||
}
|
||||
|
||||
.manager-actions-card {
|
||||
overflow: hidden;
|
||||
margin-top: 8px;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.manager-action-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.manager-action-item {
|
||||
display: grid;
|
||||
min-height: 114px;
|
||||
align-content: center;
|
||||
justify-items: center;
|
||||
gap: 8px;
|
||||
padding: 11px 7px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.manager-action-item:nth-child(n + 4) {
|
||||
border-top: 1px solid rgba(125, 87, 199, 0.055);
|
||||
}
|
||||
|
||||
.manager-action-item:not(:nth-child(3n + 1)) {
|
||||
border-left: 1px solid rgba(125, 87, 199, 0.055);
|
||||
}
|
||||
|
||||
.manager-action-icon {
|
||||
width: 56px;
|
||||
height: 56px;
|
||||
}
|
||||
|
||||
.manager-action-icon svg {
|
||||
width: 33px;
|
||||
height: 33px;
|
||||
}
|
||||
|
||||
.manager-action-icon path,
|
||||
.manager-action-icon circle {
|
||||
fill: currentColor;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.manager-action-icon .icon-cutout {
|
||||
fill: #fff;
|
||||
stroke: none;
|
||||
}
|
||||
|
||||
.action-admin .manager-action-icon,
|
||||
.action-send-frame .manager-action-icon {
|
||||
background: #f0e6ff;
|
||||
color: #844dec;
|
||||
}
|
||||
|
||||
.action-block-users .manager-action-icon {
|
||||
background: #fff1f4;
|
||||
color: #d95d72;
|
||||
}
|
||||
|
||||
.action-bd .manager-action-icon,
|
||||
.action-send-car .manager-action-icon {
|
||||
background: #eef4ff;
|
||||
color: #2f6df1;
|
||||
}
|
||||
|
||||
.action-invite-admin .manager-action-icon,
|
||||
.action-send-vip .manager-action-icon {
|
||||
background: #ffe9ef;
|
||||
color: #ff304a;
|
||||
}
|
||||
|
||||
.action-invite-bd .manager-action-icon,
|
||||
.action-update-level .manager-action-icon {
|
||||
background: #e5fbf7;
|
||||
color: #08bca6;
|
||||
}
|
||||
|
||||
.action-super .manager-action-icon,
|
||||
.action-send-badge .manager-action-icon {
|
||||
background: #fff4db;
|
||||
color: #ff9700;
|
||||
}
|
||||
|
||||
.manager-center.is-loading .manager-profile-card,
|
||||
.manager-center.is-loading .manager-kpi-card,
|
||||
.manager-center.is-loading .manager-assets-card,
|
||||
.manager-center.is-loading .manager-actions-card {
|
||||
opacity: 0.72;
|
||||
}
|
||||
|
||||
.manager-center.is-denied .manager-action-grid {
|
||||
opacity: 0.46;
|
||||
pointer-events: none;
|
||||
}
|
||||
|
||||
.manager-panel {
|
||||
position: relative;
|
||||
display: grid;
|
||||
@ -392,6 +933,7 @@
|
||||
}
|
||||
|
||||
.searched-meta,
|
||||
.searched-vip,
|
||||
.blocked-meta,
|
||||
.blocked-time {
|
||||
margin-top: 3px;
|
||||
@ -404,6 +946,10 @@
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.searched-vip {
|
||||
color: #7d57c7;
|
||||
}
|
||||
|
||||
.user-block-button {
|
||||
min-width: 70px;
|
||||
height: 40px;
|
||||
@ -486,14 +1032,114 @@
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.duration-grid button:disabled,
|
||||
.level-grid button:disabled,
|
||||
.track-grid button:disabled,
|
||||
.grant-option-grid button:disabled {
|
||||
cursor: not-allowed;
|
||||
border-color: rgba(125, 87, 199, 0.12);
|
||||
background: #f3eef8;
|
||||
color: #b5a9c5;
|
||||
}
|
||||
|
||||
.grant-option-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.grant-option-grid.is-resource-grid {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.vip-grant-note {
|
||||
grid-column: 1 / -1;
|
||||
color: #7d6e90;
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.resource-selected-note {
|
||||
grid-column: 1 / -1;
|
||||
color: #7d6e90;
|
||||
font-size: 12px;
|
||||
font-weight: 900;
|
||||
line-height: 1.3;
|
||||
}
|
||||
|
||||
.grant-resource-option {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
display: grid;
|
||||
min-height: 96px;
|
||||
place-items: center;
|
||||
padding: 10px;
|
||||
border-color: rgba(125, 87, 199, 0.14);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(255, 255, 255, 0.96), #fff),
|
||||
radial-gradient(
|
||||
circle at 50% 24%,
|
||||
rgba(125, 87, 199, 0.12),
|
||||
transparent 58%
|
||||
);
|
||||
}
|
||||
|
||||
.grant-resource-option::after {
|
||||
position: absolute;
|
||||
top: 7px;
|
||||
right: 7px;
|
||||
width: 18px;
|
||||
height: 18px;
|
||||
content: '';
|
||||
border: 2px solid rgba(125, 87, 199, 0.2);
|
||||
border-radius: 999px;
|
||||
background: #fff;
|
||||
}
|
||||
|
||||
.grant-resource-option.is-selected {
|
||||
border-color: var(--primary-deep);
|
||||
background:
|
||||
linear-gradient(180deg, rgba(248, 243, 255, 0.98), #fff),
|
||||
radial-gradient(
|
||||
circle at 50% 24%,
|
||||
rgba(125, 87, 199, 0.2),
|
||||
transparent 58%
|
||||
);
|
||||
color: var(--text);
|
||||
box-shadow: inset 0 0 0 2px rgba(125, 87, 199, 0.22);
|
||||
}
|
||||
|
||||
.grant-resource-option.is-selected::after {
|
||||
border-color: var(--primary-deep);
|
||||
background: var(--primary-deep);
|
||||
box-shadow: inset 0 0 0 4px #fff;
|
||||
}
|
||||
|
||||
.grant-resource-media {
|
||||
display: flex;
|
||||
width: 68px;
|
||||
height: 68px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
}
|
||||
|
||||
.grant-resource-media img {
|
||||
display: block;
|
||||
max-width: 100%;
|
||||
max-height: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.grant-resource-fallback {
|
||||
width: 58px;
|
||||
height: 58px;
|
||||
border-radius: 50%;
|
||||
background:
|
||||
linear-gradient(
|
||||
135deg,
|
||||
rgba(125, 87, 199, 0.28),
|
||||
rgba(78, 120, 215, 0.18)
|
||||
),
|
||||
#f6f0ff;
|
||||
}
|
||||
|
||||
.country-option {
|
||||
|
||||
@ -3,6 +3,8 @@
|
||||
"version": "1.0.0",
|
||||
"description": "H5 project",
|
||||
"scripts": {
|
||||
"dev:h5": "python3 scripts/h5_dev_server.py --host 127.0.0.1 --port 8788 --directory .",
|
||||
"dev:manager-center": "python3 scripts/h5_dev_server.py --host 127.0.0.1 --port 8788 --directory .",
|
||||
"format": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||
},
|
||||
|
||||
@ -12,6 +12,7 @@ import urllib.request
|
||||
|
||||
|
||||
PROXIES = {
|
||||
"/__api_prod__/": "https://api.global-interaction.com/",
|
||||
"/__api_test__/": "https://api-test.global-interaction.com/",
|
||||
"/__api_local__/": "http://127.0.0.1:13000/",
|
||||
}
|
||||
@ -105,6 +106,8 @@ class H5DevHandler(http.server.SimpleHTTPRequestHandler):
|
||||
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||
body = self.rfile.read(length) if length > 0 else b""
|
||||
|
||||
# 代理只转发业务相关 header;Host、Connection 等 hop-by-hop header 必须由 urllib 按目标域重建,
|
||||
# 否则生产网关会看到本地 Host 或错误的连接语义,排查时会像业务接口异常。
|
||||
headers = {
|
||||
key: value
|
||||
for key, value in self.headers.items()
|
||||
@ -179,7 +182,7 @@ class ReusableTCPServer(socketserver.ThreadingTCPServer):
|
||||
def main() -> None:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("--host", default="127.0.0.1")
|
||||
parser.add_argument("--port", type=int, default=30000)
|
||||
parser.add_argument("--port", type=int, default=8788)
|
||||
parser.add_argument("--directory", default=os.getcwd())
|
||||
args = parser.parse_args()
|
||||
|
||||
@ -190,6 +193,8 @@ def main() -> None:
|
||||
)
|
||||
with ReusableTCPServer((args.host, args.port), handler) as server:
|
||||
print(f"Serving H5 at http://{args.host}:{args.port}/")
|
||||
print(f"Manager center: http://{args.host}:{args.port}/gonghui/manager-center/")
|
||||
print("API proxy: /__api_prod__/ -> https://api.global-interaction.com/")
|
||||
print("API proxy: /__api_test__/ -> https://api-test.global-interaction.com/")
|
||||
print("API proxy: /__api_local__/ -> http://127.0.0.1:13000/")
|
||||
server.serve_forever()
|
||||
|
||||
641
yumi_commmon/yumi-common.js
Normal file
641
yumi_commmon/yumi-common.js
Normal file
@ -0,0 +1,641 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PROD_API_BASE = 'https://jvapi.haiyihy.com';
|
||||
var TEST_API_BASE = 'https://jvapi-test.haiyihy.com';
|
||||
var LOCAL_API_BASE = 'http://127.0.0.1:13000';
|
||||
var APP_CODE = 'yumi';
|
||||
var RELATION_TYPE_BY_TAB = {
|
||||
cp: 'CP',
|
||||
brother: 'BROTHER',
|
||||
sister: 'SISTERS',
|
||||
};
|
||||
var headers = {
|
||||
authorization: '',
|
||||
'req-app-intel':
|
||||
'version=1.0.0;build=1;model=H5;sysVersion=web;channel=Web',
|
||||
'req-client': 'H5',
|
||||
'req-imei': 'H5-STATIC',
|
||||
'req-lang': 'en',
|
||||
'req-sys-origin': 'origin=YUMI;originChild=YUMI',
|
||||
'req-version': 'V2',
|
||||
'req-zone': 'Asia/Shanghai',
|
||||
};
|
||||
var readyPromise = null;
|
||||
|
||||
function currentParams() {
|
||||
var params = new URLSearchParams(window.location.search || '');
|
||||
var hash = window.location.hash || '';
|
||||
if (hash.indexOf('?') >= 0) {
|
||||
new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
|
||||
function (value, key) {
|
||||
if (!params.has(key)) params.set(key, value);
|
||||
}
|
||||
);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function query(name) {
|
||||
return currentParams().get(name);
|
||||
}
|
||||
|
||||
function normalizeBaseURL(value) {
|
||||
return String(value || '').replace(/\/+$/, '');
|
||||
}
|
||||
|
||||
function resolveEnv() {
|
||||
var env = String(query('env') || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return env === 'test' || env === 'local' || env === 'dev' ? env : '';
|
||||
}
|
||||
|
||||
function baseURL() {
|
||||
var config = window.YumiCommonConfig || {};
|
||||
var override =
|
||||
query('apiBase') ||
|
||||
query('api_base') ||
|
||||
query('yumi_api_base') ||
|
||||
window.YUMI_API_BASE_URL ||
|
||||
config.apiBase;
|
||||
if (override) return normalizeBaseURL(override);
|
||||
var env = resolveEnv();
|
||||
if (env === 'local')
|
||||
return normalizeBaseURL(config.localApi || LOCAL_API_BASE);
|
||||
if (env === 'test' || env === 'dev')
|
||||
return normalizeBaseURL(config.testApi || TEST_API_BASE);
|
||||
return normalizeBaseURL(config.prodApi || PROD_API_BASE);
|
||||
}
|
||||
|
||||
function buildURL(path, queryData) {
|
||||
var url = new URL(
|
||||
String(path || '').replace(/^\/+/, ''),
|
||||
baseURL() + '/'
|
||||
);
|
||||
Object.keys(queryData || {}).forEach(function (key) {
|
||||
var value = queryData[key];
|
||||
if (value === undefined || value === null || value === '') return;
|
||||
url.searchParams.set(key, value);
|
||||
});
|
||||
return url.toString();
|
||||
}
|
||||
|
||||
function normalizeAuthorization(value) {
|
||||
var text = String(value || '').trim();
|
||||
if (!text) return '';
|
||||
return /^Bearer\s+/i.test(text) ? text : 'Bearer ' + text;
|
||||
}
|
||||
|
||||
function tokenForHyAppAPI(value) {
|
||||
return String(value || '').replace(/^Bearer\s+/i, '');
|
||||
}
|
||||
|
||||
function splitHeaderPairs(value) {
|
||||
var result = {};
|
||||
String(value || '')
|
||||
.split(';')
|
||||
.forEach(function (item) {
|
||||
var index = item.indexOf('=');
|
||||
if (index > -1)
|
||||
result[item.slice(0, index)] = item.slice(index + 1);
|
||||
});
|
||||
return result;
|
||||
}
|
||||
|
||||
function putHeader(target, key, value) {
|
||||
if (
|
||||
value === undefined ||
|
||||
value === null ||
|
||||
value === '' ||
|
||||
target[key]
|
||||
)
|
||||
return;
|
||||
target[key] = String(value);
|
||||
}
|
||||
|
||||
function normalizeHeaderCandidate(value, depth) {
|
||||
depth = depth || 0;
|
||||
if (depth > 4 || value === null || value === undefined) return {};
|
||||
if (typeof value === 'string') {
|
||||
var trimmed = value.trim();
|
||||
if (!trimmed) return {};
|
||||
if (trimmed.charAt(0) === '{' || trimmed.charAt(0) === '[') {
|
||||
try {
|
||||
return normalizeHeaderCandidate(
|
||||
JSON.parse(trimmed),
|
||||
depth + 1
|
||||
);
|
||||
} catch (_) {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
return trimmed ? { authorization: trimmed } : {};
|
||||
}
|
||||
if (Array.isArray(value)) {
|
||||
return value.reduce(function (result, item) {
|
||||
return Object.assign(
|
||||
result,
|
||||
normalizeHeaderCandidate(item, depth + 1)
|
||||
);
|
||||
}, {});
|
||||
}
|
||||
if (typeof value !== 'object') return {};
|
||||
|
||||
var appIntel = splitHeaderPairs(
|
||||
value['Req-App-Intel'] || value['req-app-intel']
|
||||
);
|
||||
var sysOrigin = splitHeaderPairs(
|
||||
value['Req-Sys-Origin'] || value['req-sys-origin']
|
||||
);
|
||||
var normalized = {};
|
||||
putHeader(
|
||||
normalized,
|
||||
'authorization',
|
||||
value.Authorization ||
|
||||
value.authorization ||
|
||||
value.token ||
|
||||
value.accessToken
|
||||
);
|
||||
putHeader(
|
||||
normalized,
|
||||
'reqLang',
|
||||
value['Req-Lang'] ||
|
||||
value['req-lang'] ||
|
||||
value.reqLang ||
|
||||
value.lang
|
||||
);
|
||||
putHeader(
|
||||
normalized,
|
||||
'appVersion',
|
||||
appIntel.version || value.appVersion || value['App-Version']
|
||||
);
|
||||
putHeader(
|
||||
normalized,
|
||||
'buildVersion',
|
||||
appIntel.build || value.buildVersion
|
||||
);
|
||||
putHeader(normalized, 'model', appIntel.model || value.model);
|
||||
putHeader(normalized, 'channel', appIntel.channel || value.channel);
|
||||
putHeader(
|
||||
normalized,
|
||||
'reqImei',
|
||||
value['Req-Imei'] || value['req-imei'] || value.reqImei
|
||||
);
|
||||
putHeader(
|
||||
normalized,
|
||||
'origin',
|
||||
sysOrigin.origin || value.sysOrigin || value.origin
|
||||
);
|
||||
putHeader(
|
||||
normalized,
|
||||
'child',
|
||||
sysOrigin.originChild ||
|
||||
sysOrigin.child ||
|
||||
value.sysOriginChild ||
|
||||
value.child
|
||||
);
|
||||
|
||||
['headerInfo', 'headers', 'auth', 'data', 'state'].forEach(
|
||||
function (key) {
|
||||
Object.assign(
|
||||
normalized,
|
||||
normalizeHeaderCandidate(value[key], depth + 1)
|
||||
);
|
||||
}
|
||||
);
|
||||
return normalized;
|
||||
}
|
||||
|
||||
function setHeadersFromApp(rawOrObject) {
|
||||
var data = normalizeHeaderCandidate(rawOrObject || {});
|
||||
var next = {};
|
||||
if (data.authorization)
|
||||
next.authorization = normalizeAuthorization(data.authorization);
|
||||
if (data.reqLang) next['req-lang'] = data.reqLang;
|
||||
if (
|
||||
data.appVersion ||
|
||||
data.buildVersion ||
|
||||
data.model ||
|
||||
data.channel
|
||||
) {
|
||||
next['req-app-intel'] = [
|
||||
data.appVersion ? 'version=' + data.appVersion : '',
|
||||
data.buildVersion ? 'build=' + data.buildVersion : '',
|
||||
data.model ? 'model=' + data.model : '',
|
||||
data.channel ? 'channel=' + data.channel : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join(';');
|
||||
}
|
||||
if (data.reqImei) next['req-imei'] = data.reqImei;
|
||||
if (data.origin) {
|
||||
next['req-sys-origin'] = [
|
||||
'origin=' + data.origin,
|
||||
'originChild=' + (data.child || data.origin),
|
||||
].join(';');
|
||||
}
|
||||
headers = Object.assign(headers, next);
|
||||
|
||||
if (window.HyAppAPI) {
|
||||
if (next.authorization)
|
||||
window.HyAppAPI.setAccessToken(
|
||||
tokenForHyAppAPI(next.authorization)
|
||||
);
|
||||
window.HyAppAPI.setAppCode(APP_CODE);
|
||||
}
|
||||
return headers;
|
||||
}
|
||||
|
||||
function discoverQueryHeaders() {
|
||||
return {
|
||||
authorization:
|
||||
query('authorization') ||
|
||||
query('Authorization') ||
|
||||
query('token') ||
|
||||
query('accessToken') ||
|
||||
query('access_token'),
|
||||
reqLang: query('lang') || query('language') || query('locale'),
|
||||
origin: query('sysOrigin') || query('origin') || 'YUMI',
|
||||
child: query('sysOriginChild') || query('originChild') || 'YUMI',
|
||||
channel: query('channel') || query('appChannel'),
|
||||
};
|
||||
}
|
||||
|
||||
function readStorageHeaders(storage) {
|
||||
var collected = {};
|
||||
if (!storage) return collected;
|
||||
[
|
||||
'authorization',
|
||||
'Authorization',
|
||||
'token',
|
||||
'accessToken',
|
||||
'headerInfo',
|
||||
'headersFromApp',
|
||||
'appHeaders',
|
||||
'user',
|
||||
'userInfo',
|
||||
].forEach(function (key) {
|
||||
try {
|
||||
Object.assign(
|
||||
collected,
|
||||
normalizeHeaderCandidate(storage.getItem(key))
|
||||
);
|
||||
} catch (_) {}
|
||||
});
|
||||
return collected;
|
||||
}
|
||||
|
||||
function applyDiscoveredHeaders() {
|
||||
setHeadersFromApp(discoverQueryHeaders());
|
||||
setHeadersFromApp(window.__APP_HEADERS__);
|
||||
setHeadersFromApp(window.__HEADER_INFO__);
|
||||
setHeadersFromApp(window.headerInfo);
|
||||
try {
|
||||
setHeadersFromApp(readStorageHeaders(window.localStorage));
|
||||
setHeadersFromApp(readStorageHeaders(window.sessionStorage));
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
function ensureYumiAppCode() {
|
||||
if (window.HyAppAPI) window.HyAppAPI.setAppCode(APP_CODE);
|
||||
var params = currentParams();
|
||||
if (
|
||||
params.get('app_code') ||
|
||||
params.get('app') ||
|
||||
params.get('appCode')
|
||||
)
|
||||
return;
|
||||
if (!window.history || !window.history.replaceState) return;
|
||||
var url = new URL(window.location.href);
|
||||
url.searchParams.set('app_code', APP_CODE);
|
||||
// 目标仓 common/api.js 没有 app_code 会默认走 lalu;Yumi 页入口必须显式落到 Yumi 租户。
|
||||
window.history.replaceState(null, '', url.toString());
|
||||
}
|
||||
|
||||
function isAppEnvironment() {
|
||||
return Boolean(
|
||||
window.app ||
|
||||
window.FlutterApp ||
|
||||
window.FlutterPageControl ||
|
||||
window.webkit
|
||||
);
|
||||
}
|
||||
|
||||
function requestAccessOrigin() {
|
||||
return new Promise(function (resolve) {
|
||||
var settled = false;
|
||||
var timeoutId = null;
|
||||
function finish(raw) {
|
||||
if (raw) setHeadersFromApp(raw);
|
||||
if (settled) return;
|
||||
settled = true;
|
||||
if (timeoutId) window.clearTimeout(timeoutId);
|
||||
resolve(raw || null);
|
||||
}
|
||||
|
||||
window.renderData = finish;
|
||||
window.getIosAccessOriginParam = finish;
|
||||
|
||||
if (!isAppEnvironment()) {
|
||||
finish(null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Android 普通 WebView 会注入 window.app.getAccessOrigin;iOS/旧页可能只通过回调异步回填。
|
||||
if (
|
||||
window.app &&
|
||||
typeof window.app.getAccessOrigin === 'function'
|
||||
) {
|
||||
try {
|
||||
finish(window.app.getAccessOrigin());
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
if (
|
||||
window.FlutterApp &&
|
||||
typeof window.FlutterApp.postMessage === 'function'
|
||||
) {
|
||||
try {
|
||||
window.FlutterApp.postMessage('requestAccessOrigin');
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
var attempt = 0;
|
||||
var poll = function () {
|
||||
attempt += 1;
|
||||
if (
|
||||
window.app &&
|
||||
typeof window.app.getAccessOrigin === 'function'
|
||||
) {
|
||||
try {
|
||||
finish(window.app.getAccessOrigin());
|
||||
return;
|
||||
} catch (_) {}
|
||||
}
|
||||
if (!settled && attempt < 30) window.setTimeout(poll, 60);
|
||||
};
|
||||
poll();
|
||||
timeoutId = window.setTimeout(function () {
|
||||
finish(null);
|
||||
}, 1800);
|
||||
});
|
||||
}
|
||||
|
||||
function connectToApp() {
|
||||
if (readyPromise) return readyPromise;
|
||||
ensureYumiAppCode();
|
||||
applyDiscoveredHeaders();
|
||||
readyPromise = requestAccessOrigin().then(function () {
|
||||
return {
|
||||
baseURL: baseURL(),
|
||||
headers: Object.assign({}, headers),
|
||||
authenticated: Boolean(headers.authorization),
|
||||
};
|
||||
});
|
||||
return readyPromise;
|
||||
}
|
||||
|
||||
function parseResponse(response) {
|
||||
return response.text().then(function (text) {
|
||||
var payload = text ? JSON.parse(text) : {};
|
||||
if (!response.ok) {
|
||||
var httpError = new Error(
|
||||
payload.message || response.statusText || 'request_failed'
|
||||
);
|
||||
httpError.status = response.status;
|
||||
httpError.payload = payload;
|
||||
throw httpError;
|
||||
}
|
||||
if (payload && payload.status === false) {
|
||||
var apiError = new Error(
|
||||
payload.message || payload.errorMsg || 'api_request_failed'
|
||||
);
|
||||
apiError.payload = payload;
|
||||
throw apiError;
|
||||
}
|
||||
if (
|
||||
payload &&
|
||||
Object.prototype.hasOwnProperty.call(payload, 'body')
|
||||
)
|
||||
return payload.body;
|
||||
if (
|
||||
payload &&
|
||||
Object.prototype.hasOwnProperty.call(payload, 'data')
|
||||
)
|
||||
return payload.data;
|
||||
return payload;
|
||||
});
|
||||
}
|
||||
|
||||
function request(path, options) {
|
||||
var opts = options || {};
|
||||
var body = opts.body;
|
||||
if (body && typeof body === 'object' && !(body instanceof FormData)) {
|
||||
body = JSON.stringify(body);
|
||||
}
|
||||
return connectToApp().then(function () {
|
||||
var requestHeaders = Object.assign({}, headers, opts.headers || {});
|
||||
if (!requestHeaders.authorization)
|
||||
delete requestHeaders.authorization;
|
||||
if (body)
|
||||
requestHeaders['content-type'] =
|
||||
requestHeaders['content-type'] || 'application/json';
|
||||
return window
|
||||
.fetch(buildURL(path, opts.query), {
|
||||
method: opts.method || 'GET',
|
||||
headers: requestHeaders,
|
||||
body: body,
|
||||
credentials: 'omit',
|
||||
cache: 'no-store',
|
||||
})
|
||||
.then(parseResponse);
|
||||
});
|
||||
}
|
||||
|
||||
function readNumber(value, fallback) {
|
||||
var number = Number(value);
|
||||
return Number.isFinite(number) ? number : fallback || 0;
|
||||
}
|
||||
|
||||
function relationType(tabOrType) {
|
||||
var key = String(tabOrType || 'cp')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return (
|
||||
RELATION_TYPE_BY_TAB[key] || String(tabOrType || 'CP').toUpperCase()
|
||||
);
|
||||
}
|
||||
|
||||
function userAvatar(profile) {
|
||||
profile = profile || {};
|
||||
return (
|
||||
profile.userAvatar ||
|
||||
profile.avatar ||
|
||||
profile.headImg ||
|
||||
profile.headImage ||
|
||||
''
|
||||
);
|
||||
}
|
||||
|
||||
function userId(profile) {
|
||||
profile = profile || {};
|
||||
return profile.id || profile.userId || profile.uid || '';
|
||||
}
|
||||
|
||||
function choosePair(list, type) {
|
||||
var rows = Array.isArray(list) ? list : list ? [list] : [];
|
||||
for (var i = 0; i < rows.length; i += 1) {
|
||||
if (relationType(rows[i].relationType) === type) return rows[i];
|
||||
}
|
||||
return rows[0] || null;
|
||||
}
|
||||
|
||||
function sortedLevels(levels) {
|
||||
return (Array.isArray(levels) ? levels : [])
|
||||
.slice()
|
||||
.sort(function (a, b) {
|
||||
return readNumber(a.level, 0) - readNumber(b.level, 0);
|
||||
});
|
||||
}
|
||||
|
||||
function levelProgress(rights) {
|
||||
var intimacy = readNumber(rights && rights.intimacyValue, 0);
|
||||
var levels = sortedLevels(rights && rights.levels);
|
||||
var current = levels[0] || { level: 0, requiredIntimacyValue: 0 };
|
||||
var next = levels[1] || current;
|
||||
levels.forEach(function (level, index) {
|
||||
if (readNumber(level.requiredIntimacyValue, 0) <= intimacy) {
|
||||
current = level;
|
||||
next = levels[index + 1] || level;
|
||||
}
|
||||
});
|
||||
var currentThreshold = readNumber(current.requiredIntimacyValue, 0);
|
||||
var nextThreshold = Math.max(
|
||||
readNumber(next.requiredIntimacyValue, currentThreshold),
|
||||
currentThreshold
|
||||
);
|
||||
var currentExp = Math.max(0, intimacy - currentThreshold);
|
||||
var nextExp = Math.max(0, nextThreshold - currentThreshold);
|
||||
var progressPercent = nextExp ? (currentExp / nextExp) * 100 : 100;
|
||||
return {
|
||||
currentLevel: readNumber(current.level, 0),
|
||||
nextLevel: readNumber(next.level, current.level),
|
||||
currentExp: currentExp,
|
||||
nextExp: nextExp || intimacy,
|
||||
neededExp: Math.max(0, nextThreshold - intimacy),
|
||||
nextExpLevel: readNumber(next.level, current.level),
|
||||
progressPercent: Math.max(0, Math.min(100, progressPercent)),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeRelation(pair, rights) {
|
||||
pair = pair || {};
|
||||
rights = rights || {};
|
||||
var progress = levelProgress(rights);
|
||||
var me = pair.meUserProfile || pair.me || {};
|
||||
var partner = pair.cpUserProfile || pair.partner || {};
|
||||
return Object.assign(progress, {
|
||||
relationType: relationType(
|
||||
pair.relationType || rights.relationType
|
||||
),
|
||||
status: rights.relationshipStatus || pair.status || 'NONE',
|
||||
cpUserId: rights.cpUserId || userId(partner),
|
||||
days: readNumber(pair.days, 0),
|
||||
togetherDays: readNumber(pair.days, 0),
|
||||
me: { avatar: userAvatar(me), id: userId(me) },
|
||||
partner: { avatar: userAvatar(partner), id: userId(partner) },
|
||||
raw: { pair: pair, rights: rights },
|
||||
});
|
||||
}
|
||||
|
||||
function pair(options) {
|
||||
var opts = options || {};
|
||||
return request('/user/cp-relationship/pair', {
|
||||
query: {
|
||||
relationType: relationType(opts.relationType || opts.tab),
|
||||
userId: opts.userId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function rights(options) {
|
||||
var opts = options || {};
|
||||
return request('/user/cp/rights/config', {
|
||||
query: {
|
||||
relationType: relationType(opts.relationType || opts.tab),
|
||||
cpUserId: opts.cpUserId,
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
function loadRelation(options) {
|
||||
var opts = options || {};
|
||||
var type = relationType(opts.relationType || opts.tab);
|
||||
var selectedPair = null;
|
||||
var firstError = null;
|
||||
return pair({ relationType: type, userId: opts.userId })
|
||||
.then(function (pairList) {
|
||||
selectedPair = choosePair(pairList, type);
|
||||
var cpUserId =
|
||||
opts.cpUserId ||
|
||||
(selectedPair && userId(selectedPair.cpUserProfile)) ||
|
||||
'';
|
||||
return rights({ relationType: type, cpUserId: cpUserId });
|
||||
})
|
||||
.catch(function (error) {
|
||||
firstError = error;
|
||||
return rights({ relationType: type, cpUserId: opts.cpUserId });
|
||||
})
|
||||
.then(function (rightsData) {
|
||||
return normalizeRelation(selectedPair, rightsData);
|
||||
})
|
||||
.catch(function (error) {
|
||||
throw firstError || error;
|
||||
});
|
||||
}
|
||||
|
||||
function back() {
|
||||
if (
|
||||
window.FlutterPageControl &&
|
||||
window.FlutterPageControl.postMessage
|
||||
) {
|
||||
window.FlutterPageControl.postMessage('close_page');
|
||||
return;
|
||||
}
|
||||
if (window.HyAppBridge && window.HyAppBridge.back) {
|
||||
window.HyAppBridge.back();
|
||||
return;
|
||||
}
|
||||
window.history.back();
|
||||
}
|
||||
|
||||
window.YumiCommon = {
|
||||
baseURL: baseURL,
|
||||
buildURL: buildURL,
|
||||
headers: function () {
|
||||
return Object.assign({}, headers);
|
||||
},
|
||||
connectToApp: connectToApp,
|
||||
setHeadersFromApp: setHeadersFromApp,
|
||||
request: request,
|
||||
get: function (path, queryData) {
|
||||
return request(path, { method: 'GET', query: queryData });
|
||||
},
|
||||
post: function (path, body, queryData) {
|
||||
return request(path, {
|
||||
method: 'POST',
|
||||
body: body,
|
||||
query: queryData,
|
||||
});
|
||||
},
|
||||
back: back,
|
||||
cpSpace: {
|
||||
relationType: relationType,
|
||||
pair: pair,
|
||||
rights: rights,
|
||||
loadRelation: loadRelation,
|
||||
normalizeRelation: normalizeRelation,
|
||||
},
|
||||
};
|
||||
})();
|
||||
Loading…
x
Reference in New Issue
Block a user