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 {
|
.yumi-cp-stage {
|
||||||
|
--stage-height: 812px;
|
||||||
|
--design-width: 375px;
|
||||||
|
--design-height: 812px;
|
||||||
|
--design-scale: 1;
|
||||||
--accent: #ff0979;
|
--accent: #ff0979;
|
||||||
--accent-soft: #ff5762;
|
--accent-soft: #ff5762;
|
||||||
--tab-bg: rgba(94, 13, 35, 0.8);
|
--tab-bg: rgba(94, 13, 35, 0.8);
|
||||||
@ -47,14 +51,22 @@ button {
|
|||||||
--progress-to: #ffcf67;
|
--progress-to: #ffcf67;
|
||||||
--exp-pill-bg: rgba(255, 102, 169, 0.74);
|
--exp-pill-bg: rgba(255, 102, 169, 0.74);
|
||||||
position: relative;
|
position: relative;
|
||||||
width: min(100vw, 375px);
|
width: 100%;
|
||||||
height: min(812px, 216.533333vw);
|
height: var(--stage-height);
|
||||||
max-height: 812px;
|
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
background: #440b23;
|
background: #440b23;
|
||||||
flex: 0 0 auto;
|
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 {
|
.yumi-cp-page[data-tab='brother'] .yumi-cp-stage {
|
||||||
--accent: #5709ff;
|
--accent: #5709ff;
|
||||||
--accent-soft: #5709ff;
|
--accent-soft: #5709ff;
|
||||||
@ -108,7 +120,6 @@ button {
|
|||||||
object-fit: fill;
|
object-fit: fill;
|
||||||
}
|
}
|
||||||
|
|
||||||
.yumi-status,
|
|
||||||
.yumi-nav,
|
.yumi-nav,
|
||||||
.yumi-tabs,
|
.yumi-tabs,
|
||||||
.yumi-card {
|
.yumi-card {
|
||||||
@ -116,146 +127,9 @@ button {
|
|||||||
z-index: 1;
|
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 {
|
.yumi-nav {
|
||||||
left: 0;
|
left: 0;
|
||||||
top: 44px;
|
top: 0;
|
||||||
width: 375px;
|
width: 375px;
|
||||||
height: 44px;
|
height: 44px;
|
||||||
}
|
}
|
||||||
@ -318,7 +192,7 @@ button {
|
|||||||
|
|
||||||
.yumi-tabs {
|
.yumi-tabs {
|
||||||
left: 16px;
|
left: 16px;
|
||||||
top: 109px;
|
top: 65px;
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: repeat(3, 1fr);
|
grid-template-columns: repeat(3, 1fr);
|
||||||
width: 343px;
|
width: 343px;
|
||||||
@ -362,7 +236,7 @@ button {
|
|||||||
|
|
||||||
.yumi-card {
|
.yumi-card {
|
||||||
left: 14px;
|
left: 14px;
|
||||||
top: 169px;
|
top: 125px;
|
||||||
width: 347px;
|
width: 347px;
|
||||||
height: 199px;
|
height: 199px;
|
||||||
color: var(--accent);
|
color: var(--accent);
|
||||||
@ -636,13 +510,3 @@ button {
|
|||||||
[dir='rtl'] .yumi-tabs {
|
[dir='rtl'] .yumi-tabs {
|
||||||
direction: ltr;
|
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"
|
rel="stylesheet"
|
||||||
href="../../common/theme.css?v=20260708-yumi-layered"
|
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>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<main
|
<main
|
||||||
@ -27,173 +27,176 @@
|
|||||||
aria-label="Best Friend"
|
aria-label="Best Friend"
|
||||||
data-i18n-aria="cpSpace.pageLabel"
|
data-i18n-aria="cpSpace.pageLabel"
|
||||||
>
|
>
|
||||||
<img
|
<div class="yumi-stage-design">
|
||||||
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"
|
|
||||||
>
|
|
||||||
<img
|
<img
|
||||||
class="yumi-tab-jewel"
|
class="yumi-bg-layer"
|
||||||
id="yumiTabJewel"
|
id="yumiBgLayer"
|
||||||
src="./assets/yumi/layers/tab-cp.png"
|
src="./assets/yumi/layers/bg-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=""
|
alt=""
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<figure class="yumi-avatar yumi-avatar--left">
|
<nav
|
||||||
<span
|
class="yumi-nav"
|
||||||
class="yumi-avatar-photo"
|
aria-label="Best Friend navigation"
|
||||||
id="yumiLeftAvatar"
|
data-i18n-aria="cpSpace.navLabel"
|
||||||
></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"
|
|
||||||
>
|
>
|
||||||
<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
|
<img
|
||||||
class="yumi-level-bg"
|
class="yumi-avatar-frame"
|
||||||
src="./assets/yumi/layers/level-cp.png"
|
src="./assets/yumi/layers/avatar-frame.png"
|
||||||
alt=""
|
alt=""
|
||||||
draggable="false"
|
draggable="false"
|
||||||
/>
|
/>
|
||||||
<span id="yumiCurrentLevel">Lv1</span>
|
</figure>
|
||||||
</div>
|
|
||||||
<div class="yumi-progress-track">
|
<figure class="yumi-avatar yumi-avatar--right">
|
||||||
<span
|
<span
|
||||||
class="yumi-progress-fill"
|
class="yumi-avatar-photo"
|
||||||
id="yumiProgressFill"
|
id="yumiRightAvatar"
|
||||||
></span>
|
></span>
|
||||||
<strong
|
<img
|
||||||
class="yumi-progress-text"
|
class="yumi-avatar-frame"
|
||||||
id="yumiProgressText"
|
src="./assets/yumi/layers/avatar-frame.png"
|
||||||
>1000/200000</strong
|
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>
|
||||||
<div class="yumi-level-badge yumi-level-badge--right">
|
|
||||||
<img
|
<div class="yumi-exp-pill">
|
||||||
class="yumi-level-bg"
|
<span id="yumiExpAway"
|
||||||
src="./assets/yumi/layers/level-cp.png"
|
>888 Exp away from Lv. 2</span
|
||||||
alt=""
|
>
|
||||||
draggable="false"
|
|
||||||
/>
|
|
||||||
<span id="yumiNextLevel">Lv1</span>
|
|
||||||
</div>
|
</div>
|
||||||
<p class="yumi-time-text" id="yumiTimeText">
|
|
||||||
Time spent together: 4 days
|
<section
|
||||||
</p>
|
class="yumi-progress-block"
|
||||||
</section>
|
aria-label="1000/200000"
|
||||||
</article>
|
>
|
||||||
|
<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>
|
</section>
|
||||||
</main>
|
</main>
|
||||||
|
|
||||||
@ -201,6 +204,7 @@
|
|||||||
<script src="../../common/api.js?v=20260708-yumi-layered"></script>
|
<script src="../../common/api.js?v=20260708-yumi-layered"></script>
|
||||||
<script src="../../common/jsbridge.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="../../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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -25,20 +25,21 @@
|
|||||||
},
|
},
|
||||||
};
|
};
|
||||||
var DEFAULT_RELATION = {
|
var DEFAULT_RELATION = {
|
||||||
days: 999,
|
days: 0,
|
||||||
togetherDays: 4,
|
togetherDays: 0,
|
||||||
currentLevel: 1,
|
currentLevel: 0,
|
||||||
nextLevel: 1,
|
nextLevel: 0,
|
||||||
currentExp: 1000,
|
currentExp: 0,
|
||||||
nextExp: 200000,
|
nextExp: 0,
|
||||||
neededExp: 888,
|
neededExp: 0,
|
||||||
nextExpLevel: 2,
|
nextExpLevel: 0,
|
||||||
progressPercent: 22.3,
|
progressPercent: 0,
|
||||||
me: { avatar: '' },
|
me: { avatar: '' },
|
||||||
partner: { avatar: '' },
|
partner: { avatar: '' },
|
||||||
};
|
};
|
||||||
|
|
||||||
var page = document.getElementById('yumiCpPage');
|
var page = document.getElementById('yumiCpPage');
|
||||||
|
var stage = document.querySelector('.yumi-cp-stage');
|
||||||
var backButton = document.getElementById('yumiBackButton');
|
var backButton = document.getElementById('yumiBackButton');
|
||||||
var helpButton = document.getElementById('yumiHelpButton');
|
var helpButton = document.getElementById('yumiHelpButton');
|
||||||
var tabButtons = Array.prototype.slice.call(
|
var tabButtons = Array.prototype.slice.call(
|
||||||
@ -63,7 +64,10 @@
|
|||||||
var state = {
|
var state = {
|
||||||
activeTab: normalizeTab(readParam('tab')) || 'cp',
|
activeTab: normalizeTab(readParam('tab')) || 'cp',
|
||||||
data: clone(DEFAULT_RELATION),
|
data: clone(DEFAULT_RELATION),
|
||||||
|
tabData: {},
|
||||||
|
requestSeq: 0,
|
||||||
};
|
};
|
||||||
|
var stageResizeObserver = null;
|
||||||
|
|
||||||
function clone(value) {
|
function clone(value) {
|
||||||
return JSON.parse(JSON.stringify(value));
|
return JSON.parse(JSON.stringify(value));
|
||||||
@ -86,6 +90,15 @@
|
|||||||
return Number.isFinite(number) ? number : fallback || 0;
|
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) {
|
function t(key, fallback) {
|
||||||
if (window.HyAppI18n && window.HyAppI18n.t) {
|
if (window.HyAppI18n && window.HyAppI18n.t) {
|
||||||
return window.HyAppI18n.t(key, fallback);
|
return window.HyAppI18n.t(key, fallback);
|
||||||
@ -102,6 +115,28 @@
|
|||||||
return LAYER_BASE + name;
|
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) {
|
function syncURL(tab) {
|
||||||
if (!window.history || !window.history.replaceState) return;
|
if (!window.history || !window.history.replaceState) return;
|
||||||
var url = new URL(window.location.href);
|
var url = new URL(window.location.href);
|
||||||
@ -132,28 +167,31 @@
|
|||||||
return {
|
return {
|
||||||
days: readNumber(data.days, DEFAULT_RELATION.days),
|
days: readNumber(data.days, DEFAULT_RELATION.days),
|
||||||
togetherDays: readNumber(
|
togetherDays: readNumber(
|
||||||
data.togetherDays || data.together_days,
|
firstValue(data.togetherDays, data.together_days),
|
||||||
DEFAULT_RELATION.togetherDays
|
DEFAULT_RELATION.togetherDays
|
||||||
),
|
),
|
||||||
currentLevel: readNumber(
|
currentLevel: readNumber(
|
||||||
data.currentLevel || data.current_level || data.level,
|
firstValue(data.currentLevel, data.current_level, data.level),
|
||||||
1
|
DEFAULT_RELATION.currentLevel
|
||||||
|
),
|
||||||
|
nextLevel: readNumber(
|
||||||
|
firstValue(data.nextLevel, data.next_level),
|
||||||
|
DEFAULT_RELATION.nextLevel
|
||||||
),
|
),
|
||||||
nextLevel: readNumber(data.nextLevel || data.next_level, 1),
|
|
||||||
currentExp: readNumber(
|
currentExp: readNumber(
|
||||||
data.currentExp || data.current_exp,
|
firstValue(data.currentExp, data.current_exp),
|
||||||
DEFAULT_RELATION.currentExp
|
DEFAULT_RELATION.currentExp
|
||||||
),
|
),
|
||||||
nextExp: readNumber(
|
nextExp: readNumber(
|
||||||
data.nextExp || data.next_exp,
|
firstValue(data.nextExp, data.next_exp),
|
||||||
DEFAULT_RELATION.nextExp
|
DEFAULT_RELATION.nextExp
|
||||||
),
|
),
|
||||||
neededExp: readNumber(
|
neededExp: readNumber(
|
||||||
data.neededExp || data.needed_exp,
|
firstValue(data.neededExp, data.needed_exp),
|
||||||
DEFAULT_RELATION.neededExp
|
DEFAULT_RELATION.neededExp
|
||||||
),
|
),
|
||||||
nextExpLevel: readNumber(
|
nextExpLevel: readNumber(
|
||||||
data.nextExpLevel || data.next_exp_level,
|
firstValue(data.nextExpLevel, data.next_exp_level),
|
||||||
DEFAULT_RELATION.nextExpLevel
|
DEFAULT_RELATION.nextExpLevel
|
||||||
),
|
),
|
||||||
progressPercent: Math.max(
|
progressPercent: Math.max(
|
||||||
@ -161,7 +199,7 @@
|
|||||||
Math.min(
|
Math.min(
|
||||||
100,
|
100,
|
||||||
readNumber(
|
readNumber(
|
||||||
data.progressPercent || data.progress_percent,
|
firstValue(data.progressPercent, data.progress_percent),
|
||||||
DEFAULT_RELATION.progressPercent
|
DEFAULT_RELATION.progressPercent
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@ -237,18 +275,59 @@
|
|||||||
var data = payload || {};
|
var data = payload || {};
|
||||||
var tabData = data[state.activeTab] || data.relation || data;
|
var tabData = data[state.activeTab] || data.relation || data;
|
||||||
state.data = normalizedRelation(tabData);
|
state.data = normalizedRelation(tabData);
|
||||||
|
state.tabData[state.activeTab] = state.data;
|
||||||
renderCopy();
|
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() {
|
function bindEvents() {
|
||||||
tabButtons.forEach(function (button) {
|
tabButtons.forEach(function (button) {
|
||||||
button.addEventListener('click', function () {
|
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) {
|
if (backButton) {
|
||||||
backButton.addEventListener('click', function () {
|
backButton.addEventListener('click', function () {
|
||||||
|
if (window.YumiCommon && window.YumiCommon.back) {
|
||||||
|
window.YumiCommon.back();
|
||||||
|
return;
|
||||||
|
}
|
||||||
if (window.HyAppBridge && window.HyAppBridge.back) {
|
if (window.HyAppBridge && window.HyAppBridge.back) {
|
||||||
window.HyAppBridge.back();
|
window.HyAppBridge.back();
|
||||||
return;
|
return;
|
||||||
@ -274,13 +353,16 @@
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bindStageScale();
|
||||||
bindEvents();
|
bindEvents();
|
||||||
if (window.HyAppCPSpaceYumiData)
|
if (window.HyAppCPSpaceYumiData)
|
||||||
applyExternalData(window.HyAppCPSpaceYumiData);
|
applyExternalData(window.HyAppCPSpaceYumiData);
|
||||||
setActiveTab(state.activeTab, { syncURL: false });
|
setActiveTab(state.activeTab, { syncURL: false });
|
||||||
|
showTabData(state.activeTab);
|
||||||
|
loadTabData(state.activeTab);
|
||||||
|
|
||||||
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
||||||
// ready 只声明页面能力;真实关系数据由客户端事件或网关注入,避免 H5 自行判断关系状态。
|
// ready 只声明页面能力;真实关系数据由 YumiCommon 接口层拉取,客户端仍可通过事件覆盖。
|
||||||
window.HyAppBridge.ready({ page: 'yumi-cp-space' });
|
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);
|
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() {
|
function resolveEnv() {
|
||||||
var env = readQuery('env');
|
var env = readQuery('env');
|
||||||
if (env === 'test' || env === 'local') {
|
if (env === 'test' || env === 'local') {
|
||||||
@ -169,10 +177,9 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
var env = resolveEnv();
|
var env = resolveEnv();
|
||||||
var configuredAPI = activeAppAPIBaseURL(env);
|
var configuredAPI = activeAppAPIBaseURL(env);
|
||||||
if (configuredAPI) return configuredAPI;
|
if (configuredAPI) return configuredAPI;
|
||||||
if (env === 'test')
|
var devProxy = localDevProxyBase(env);
|
||||||
return isLocalDevOrigin()
|
if (devProxy) return devProxy;
|
||||||
? devProxyBase('/__api_test__/')
|
if (env === 'test') return normalizeBaseURL(test_api);
|
||||||
: normalizeBaseURL(test_api);
|
|
||||||
if (env === 'local') return normalizeBaseURL(local_api);
|
if (env === 'local') return normalizeBaseURL(local_api);
|
||||||
return normalizeBaseURL(default_api);
|
return normalizeBaseURL(default_api);
|
||||||
}
|
}
|
||||||
@ -597,6 +604,32 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
body: payload || {},
|
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 统一处理。
|
// 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.loading": "جار التحميل...",
|
||||||
"coinseller_center.load_failed": "فشل التحميل. حاول مرة أخرى لاحقا.",
|
"coinseller_center.load_failed": "فشل التحميل. حاول مرة أخرى لاحقا.",
|
||||||
"coinseller_center.transfer_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.message": "أرسل لك هدية. تعال والعب معي.",
|
||||||
"lalu_landing.frame_duration": "3 أيام",
|
"lalu_landing.frame_duration": "3 أيام",
|
||||||
"lalu_landing.join_party": "انضم إلى الحفلة",
|
"lalu_landing.join_party": "انضم إلى الحفلة",
|
||||||
@ -715,5 +730,30 @@
|
|||||||
"contact_card.required": "أدخل جهة اتصال WhatsApp.",
|
"contact_card.required": "أدخل جهة اتصال WhatsApp.",
|
||||||
"contact_card.api_unavailable": "واجهة جهة الاتصال غير متاحة.",
|
"contact_card.api_unavailable": "واجهة جهة الاتصال غير متاحة.",
|
||||||
"contact_card.saved": "تم الحفظ.",
|
"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.loading": "Loading...",
|
||||||
"coinseller_center.load_failed": "Load failed. Try again later.",
|
"coinseller_center.load_failed": "Load failed. Try again later.",
|
||||||
"coinseller_center.transfer_failed": "Transfer failed.",
|
"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.message": "Sent you a gift. Come play with me.",
|
||||||
"lalu_landing.frame_duration": "3 days",
|
"lalu_landing.frame_duration": "3 days",
|
||||||
"lalu_landing.join_party": "Join party",
|
"lalu_landing.join_party": "Join party",
|
||||||
@ -721,5 +736,30 @@
|
|||||||
"contact_card.required": "Enter WhatsApp contact.",
|
"contact_card.required": "Enter WhatsApp contact.",
|
||||||
"contact_card.api_unavailable": "Contact API unavailable.",
|
"contact_card.api_unavailable": "Contact API unavailable.",
|
||||||
"contact_card.saved": "Saved.",
|
"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.loading": "Cargando...",
|
||||||
"coinseller_center.load_failed": "Error al cargar. Inténtalo de nuevo más tarde.",
|
"coinseller_center.load_failed": "Error al cargar. Inténtalo de nuevo más tarde.",
|
||||||
"coinseller_center.transfer_failed": "Transferencia fallida.",
|
"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.message": "Te envió un regalo. Ven a jugar conmigo.",
|
||||||
"lalu_landing.frame_duration": "3 días",
|
"lalu_landing.frame_duration": "3 días",
|
||||||
"lalu_landing.join_party": "Unirse a la fiesta",
|
"lalu_landing.join_party": "Unirse a la fiesta",
|
||||||
@ -715,5 +730,30 @@
|
|||||||
"contact_card.required": "Ingresa el contacto de WhatsApp.",
|
"contact_card.required": "Ingresa el contacto de WhatsApp.",
|
||||||
"contact_card.api_unavailable": "API de contacto no disponible.",
|
"contact_card.api_unavailable": "API de contacto no disponible.",
|
||||||
"contact_card.saved": "Guardado.",
|
"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.loading": "Memuat...",
|
||||||
"coinseller_center.load_failed": "Gagal memuat. Coba lagi nanti.",
|
"coinseller_center.load_failed": "Gagal memuat. Coba lagi nanti.",
|
||||||
"coinseller_center.transfer_failed": "Transfer gagal.",
|
"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.message": "Mengirim hadiah untukmu. Ayo main bersama.",
|
||||||
"lalu_landing.frame_duration": "3 hari",
|
"lalu_landing.frame_duration": "3 hari",
|
||||||
"lalu_landing.join_party": "Gabung party",
|
"lalu_landing.join_party": "Gabung party",
|
||||||
@ -643,5 +658,30 @@
|
|||||||
"contact_card.required": "Masukkan kontak WhatsApp.",
|
"contact_card.required": "Masukkan kontak WhatsApp.",
|
||||||
"contact_card.api_unavailable": "API kontak tidak tersedia.",
|
"contact_card.api_unavailable": "API kontak tidak tersedia.",
|
||||||
"contact_card.saved": "Tersimpan.",
|
"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.loading": "Yükleniyor...",
|
||||||
"coinseller_center.load_failed": "Yükleme başarısız. Daha sonra tekrar deneyin.",
|
"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.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.message": "Sana bir hediye gönderdi. Gel birlikte oynayalım.",
|
||||||
"lalu_landing.frame_duration": "3 gün",
|
"lalu_landing.frame_duration": "3 gün",
|
||||||
"lalu_landing.join_party": "Partiye katıl",
|
"lalu_landing.join_party": "Partiye katıl",
|
||||||
@ -682,5 +697,30 @@
|
|||||||
"contact_card.required": "WhatsApp iletişimini gir.",
|
"contact_card.required": "WhatsApp iletişimini gir.",
|
||||||
"contact_card.api_unavailable": "İletişim API kullanılamıyor.",
|
"contact_card.api_unavailable": "İletişim API kullanılamıyor.",
|
||||||
"contact_card.saved": "Kaydedildi.",
|
"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.loading": "加载中...",
|
||||||
"coinseller_center.load_failed": "加载失败,请稍后重试。",
|
"coinseller_center.load_failed": "加载失败,请稍后重试。",
|
||||||
"coinseller_center.transfer_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.message": "送你一份礼物,快来和我一起玩吧!",
|
||||||
"lalu_landing.frame_duration": "3天",
|
"lalu_landing.frame_duration": "3天",
|
||||||
"lalu_landing.join_party": "加入派对",
|
"lalu_landing.join_party": "加入派对",
|
||||||
@ -649,5 +664,30 @@
|
|||||||
"contact_card.required": "请输入 WhatsApp 联系方式。",
|
"contact_card.required": "请输入 WhatsApp 联系方式。",
|
||||||
"contact_card.api_unavailable": "联系方式接口不可用。",
|
"contact_card.api_unavailable": "联系方式接口不可用。",
|
||||||
"contact_card.saved": "已保存。",
|
"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;
|
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>
|
</div>
|
||||||
</section>
|
</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">
|
<section class="card search-card">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
@ -131,7 +163,9 @@
|
|||||||
>
|
>
|
||||||
<svg viewBox="0 0 24 24" aria-hidden="true">
|
<svg viewBox="0 0 24 24" aria-hidden="true">
|
||||||
<circle cx="12" cy="12" r="9" />
|
<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" />
|
<path d="M12 17h.01" />
|
||||||
</svg>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
@ -152,34 +186,34 @@
|
|||||||
>
|
>
|
||||||
Search
|
Search
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<div class="form-status" id="searchStatus" hidden></div>
|
<div class="form-status" id="searchStatus" hidden></div>
|
||||||
|
|
||||||
<div class="target-card" id="targetCard" hidden>
|
<div class="target-card" id="targetCard" hidden>
|
||||||
<div class="target-avatar" id="targetAvatar"></div>
|
<div class="target-avatar" id="targetAvatar"></div>
|
||||||
<div class="target-main">
|
<div class="target-main">
|
||||||
<div
|
<div
|
||||||
class="role-pill"
|
class="role-pill"
|
||||||
data-i18n="coinseller_center.target_user"
|
data-i18n="coinseller_center.target_user"
|
||||||
>
|
>
|
||||||
Target user
|
Target user
|
||||||
</div>
|
</div>
|
||||||
<div class="target-name" id="targetName">-</div>
|
<div class="target-name" id="targetName">-</div>
|
||||||
<div class="target-meta" id="targetUID">UID: -</div>
|
<div class="target-meta" id="targetUID">UID: -</div>
|
||||||
</div>
|
</div>
|
||||||
<button
|
<button
|
||||||
class="target-coin-action"
|
class="target-coin-action"
|
||||||
id="openRechargeButton"
|
id="openRechargeButton"
|
||||||
type="button"
|
type="button"
|
||||||
aria-label="Recharge coins"
|
aria-label="Recharge coins"
|
||||||
data-i18n-aria="coinseller_center.recharge_coins"
|
data-i18n-aria="coinseller_center.recharge_coins"
|
||||||
>
|
>
|
||||||
<span class="coin-icon" aria-hidden="true">$</span>
|
<span class="coin-icon" aria-hidden="true">$</span>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
|
|
||||||
<section class="card recent-card">
|
<section class="card recent-card">
|
||||||
<div class="section-head">
|
<div class="section-head">
|
||||||
<div>
|
<div>
|
||||||
<h2 data-i18n="coinseller_center.recent_records">
|
<h2 data-i18n="coinseller_center.recent_records">
|
||||||
@ -241,7 +275,10 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="history-body">
|
<div class="history-body">
|
||||||
<form class="history-search-form" id="historySearchForm">
|
<form
|
||||||
|
class="history-search-form"
|
||||||
|
id="historySearchForm"
|
||||||
|
>
|
||||||
<input
|
<input
|
||||||
id="historySearchInput"
|
id="historySearchInput"
|
||||||
type="text"
|
type="text"
|
||||||
@ -290,8 +327,15 @@
|
|||||||
Expenditure
|
Expenditure
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
<div class="form-status" id="historyStatus" hidden></div>
|
<div
|
||||||
<div class="record-list history-records" id="historyList"></div>
|
class="form-status"
|
||||||
|
id="historyStatus"
|
||||||
|
hidden
|
||||||
|
></div>
|
||||||
|
<div
|
||||||
|
class="record-list history-records"
|
||||||
|
id="historyList"
|
||||||
|
></div>
|
||||||
<button
|
<button
|
||||||
class="load-more"
|
class="load-more"
|
||||||
id="loadMoreButton"
|
id="loadMoreButton"
|
||||||
@ -382,10 +426,151 @@
|
|||||||
|
|
||||||
<div
|
<div
|
||||||
class="sheet-modal"
|
class="sheet-modal"
|
||||||
id="helpModal"
|
id="inviteSubSellerModal"
|
||||||
hidden
|
hidden
|
||||||
aria-hidden="true"
|
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
|
<button
|
||||||
class="modal-backdrop"
|
class="modal-backdrop"
|
||||||
type="button"
|
type="button"
|
||||||
@ -419,13 +604,15 @@
|
|||||||
How to recharge?
|
How to recharge?
|
||||||
</p>
|
</p>
|
||||||
<span data-i18n="coinseller_center.faq_answer_1">
|
<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>
|
</span>
|
||||||
<p data-i18n="coinseller_center.faq_question_2">
|
<p data-i18n="coinseller_center.faq_question_2">
|
||||||
How to check records?
|
How to check records?
|
||||||
</p>
|
</p>
|
||||||
<span data-i18n="coinseller_center.faq_answer_2">
|
<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>
|
</span>
|
||||||
</div>
|
</div>
|
||||||
</section>
|
</section>
|
||||||
@ -434,9 +621,9 @@
|
|||||||
|
|
||||||
<script src="../../common/toast.js"></script>
|
<script src="../../common/toast.js"></script>
|
||||||
<script src="../../common/jsbridge.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/i18n.js?v=20260604-coinseller"></script>
|
||||||
<script src="../common/contact-card.js?v=20260629-user-contact"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -79,6 +79,29 @@
|
|||||||
time: '2026-06-03 18:42',
|
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 = {
|
var data = {
|
||||||
@ -87,6 +110,8 @@
|
|||||||
users: [],
|
users: [],
|
||||||
quickAmounts: mock.quickAmounts.slice(),
|
quickAmounts: mock.quickAmounts.slice(),
|
||||||
records: [],
|
records: [],
|
||||||
|
subSellerPermission: emptySubSellerPermission(),
|
||||||
|
subSellers: [],
|
||||||
};
|
};
|
||||||
|
|
||||||
var state = {
|
var state = {
|
||||||
@ -99,6 +124,11 @@
|
|||||||
loading: query.get('mock') !== '1',
|
loading: query.get('mock') !== '1',
|
||||||
searching: false,
|
searching: false,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
|
subSellerLoading: false,
|
||||||
|
invitingSubSeller: false,
|
||||||
|
subSellerTransferTarget: null,
|
||||||
|
subSellerTransferAmount: '',
|
||||||
|
subSellerTransferSubmitting: false,
|
||||||
};
|
};
|
||||||
|
|
||||||
function $(id) {
|
function $(id) {
|
||||||
@ -140,6 +170,14 @@
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function emptySubSellerPermission() {
|
||||||
|
return {
|
||||||
|
visible: false,
|
||||||
|
canInvite: false,
|
||||||
|
canTransfer: false,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
function initial(text) {
|
function initial(text) {
|
||||||
return String(text || '')
|
return String(text || '')
|
||||||
.trim()
|
.trim()
|
||||||
@ -163,6 +201,15 @@
|
|||||||
return number > 0 ? number : 0;
|
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) {
|
function firstValue(source, keys) {
|
||||||
if (!source) return '';
|
if (!source) return '';
|
||||||
for (var i = 0; i < keys.length; i += 1) {
|
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) {
|
function setImage(image, fallback, src, name) {
|
||||||
if (!image || !fallback) return;
|
if (!image || !fallback) return;
|
||||||
fallback.textContent = initial(name) || 'C';
|
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() {
|
function applyMockData() {
|
||||||
data.profile = Object.assign({}, mock.profile);
|
data.profile = Object.assign({}, mock.profile);
|
||||||
data.balance = Object.assign({}, mock.balance);
|
data.balance = Object.assign({}, mock.balance);
|
||||||
data.users = mock.users.slice();
|
data.users = mock.users.slice();
|
||||||
data.quickAmounts = mock.quickAmounts.slice();
|
data.quickAmounts = mock.quickAmounts.slice();
|
||||||
data.records = mock.records.slice();
|
data.records = mock.records.slice();
|
||||||
|
data.subSellerPermission = Object.assign({}, mock.subSellerPermission);
|
||||||
|
data.subSellers = mock.subSellers.slice();
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyEmptyData() {
|
function applyEmptyData() {
|
||||||
@ -550,6 +722,51 @@
|
|||||||
data.users = [];
|
data.users = [];
|
||||||
data.quickAmounts = mock.quickAmounts.slice();
|
data.quickAmounts = mock.quickAmounts.slice();
|
||||||
data.records = [];
|
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() {
|
function loadRealData() {
|
||||||
@ -584,6 +801,7 @@
|
|||||||
return null;
|
return null;
|
||||||
})
|
})
|
||||||
: Promise.resolve(null),
|
: Promise.resolve(null),
|
||||||
|
loadSubSellerCapability(),
|
||||||
])
|
])
|
||||||
.then(function (results) {
|
.then(function (results) {
|
||||||
data.profile = normalizeProfile(results[0]);
|
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() {
|
function renderQuickAmounts() {
|
||||||
replaceChildren(
|
replaceChildren(
|
||||||
$('quickAmounts'),
|
$('quickAmounts'),
|
||||||
@ -917,6 +1216,15 @@
|
|||||||
].join('-');
|
].join('-');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function subSellerCommandID(action) {
|
||||||
|
return [
|
||||||
|
'coin-seller-sub-seller',
|
||||||
|
action || 'action',
|
||||||
|
Date.now(),
|
||||||
|
Math.random().toString(36).slice(2, 8),
|
||||||
|
].join('-');
|
||||||
|
}
|
||||||
|
|
||||||
function buildTransferPayload(amount) {
|
function buildTransferPayload(amount) {
|
||||||
return {
|
return {
|
||||||
command_id: commandID(),
|
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() {
|
function rechargeNow() {
|
||||||
var amount = parsedCoinAmount();
|
var amount = parsedCoinAmount();
|
||||||
if (!state.target) {
|
if (!state.target) {
|
||||||
@ -1058,6 +1664,23 @@
|
|||||||
toast(t('coinseller_center.recharge_success', 'Recharge success.'));
|
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() {
|
function currentTimeLabel() {
|
||||||
var now = new Date();
|
var now = new Date();
|
||||||
return (
|
return (
|
||||||
@ -1112,6 +1735,14 @@
|
|||||||
$('helpButton').addEventListener('click', function () {
|
$('helpButton').addEventListener('click', function () {
|
||||||
openModal('helpModal');
|
openModal('helpModal');
|
||||||
});
|
});
|
||||||
|
$('inviteSubSellerButton').addEventListener('click', function () {
|
||||||
|
$('inviteSubSellerInput').value = '';
|
||||||
|
setStatus($('inviteSubSellerStatus'), '', '');
|
||||||
|
openModal('inviteSubSellerModal');
|
||||||
|
window.setTimeout(function () {
|
||||||
|
$('inviteSubSellerInput').focus();
|
||||||
|
}, 80);
|
||||||
|
});
|
||||||
document
|
document
|
||||||
.querySelectorAll('[data-close-modal]')
|
.querySelectorAll('[data-close-modal]')
|
||||||
.forEach(function (node) {
|
.forEach(function (node) {
|
||||||
@ -1153,6 +1784,26 @@
|
|||||||
event.preventDefault();
|
event.preventDefault();
|
||||||
rechargeNow();
|
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) {
|
document.querySelectorAll('[data-history-tab]').forEach(function (tab) {
|
||||||
tab.addEventListener('click', function () {
|
tab.addEventListener('click', function () {
|
||||||
state.historyTab = tab.getAttribute('data-history-tab');
|
state.historyTab = tab.getAttribute('data-history-tab');
|
||||||
@ -1180,6 +1831,8 @@
|
|||||||
if (event.key !== 'Escape') return;
|
if (event.key !== 'Escape') return;
|
||||||
closeModal('historyModal');
|
closeModal('historyModal');
|
||||||
closeModal('helpModal');
|
closeModal('helpModal');
|
||||||
|
closeModal('inviteSubSellerModal');
|
||||||
|
closeModal('subSellerTransferModal');
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -1189,6 +1842,8 @@
|
|||||||
renderQuickAmounts();
|
renderQuickAmounts();
|
||||||
renderTarget();
|
renderTarget();
|
||||||
renderRechargeState();
|
renderRechargeState();
|
||||||
|
renderSubSellers();
|
||||||
|
renderSubSellerTransferState();
|
||||||
renderRecent();
|
renderRecent();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@ -15,6 +15,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.balance-card,
|
.balance-card,
|
||||||
|
.sub-seller-card,
|
||||||
.search-card,
|
.search-card,
|
||||||
.recent-card {
|
.recent-card {
|
||||||
padding: 16px;
|
padding: 16px;
|
||||||
@ -118,6 +119,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.search-card,
|
.search-card,
|
||||||
|
.sub-seller-card,
|
||||||
.recent-card {
|
.recent-card {
|
||||||
display: grid;
|
display: grid;
|
||||||
gap: 14px;
|
gap: 14px;
|
||||||
@ -205,6 +207,10 @@
|
|||||||
display: none;
|
display: none;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.sub-seller-card[hidden] {
|
||||||
|
display: none;
|
||||||
|
}
|
||||||
|
|
||||||
.target-avatar,
|
.target-avatar,
|
||||||
.record-avatar {
|
.record-avatar {
|
||||||
display: flex;
|
display: flex;
|
||||||
@ -373,6 +379,54 @@
|
|||||||
gap: 10px;
|
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 {
|
.record-row {
|
||||||
display: grid;
|
display: grid;
|
||||||
grid-template-columns: 42px minmax(0, 1fr) auto;
|
grid-template-columns: 42px minmax(0, 1fr) auto;
|
||||||
@ -515,6 +569,52 @@
|
|||||||
padding: 16px 18px 22px;
|
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 h3,
|
||||||
.help-content p {
|
.help-content p {
|
||||||
margin: 0;
|
margin: 0;
|
||||||
@ -565,4 +665,13 @@ body.modal-open .coinseller-center {
|
|||||||
.history-search-form {
|
.history-search-form {
|
||||||
grid-template-columns: minmax(0, 1fr);
|
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"
|
name="viewport"
|
||||||
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no, viewport-fit=cover"
|
||||||
/>
|
/>
|
||||||
<title>Control Panel</title>
|
<title>Manage Center</title>
|
||||||
<link rel="stylesheet" href="../../common/theme.css?v=20260608" />
|
|
||||||
<link rel="stylesheet" href="../common/center.css?v=20260608" />
|
|
||||||
<link
|
<link
|
||||||
rel="stylesheet"
|
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>
|
</head>
|
||||||
|
|
||||||
<body>
|
<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>
|
<div class="hero-bg" aria-hidden="true"></div>
|
||||||
|
|
||||||
<nav class="title-bar">
|
<nav class="title-bar">
|
||||||
@ -28,10 +28,12 @@
|
|||||||
data-i18n-aria="manager_center.back"
|
data-i18n-aria="manager_center.back"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 32 32" aria-hidden="true">
|
<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>
|
</svg>
|
||||||
</button>
|
</button>
|
||||||
<h1 data-i18n="manager_center.title">Control Panel</h1>
|
<h1 data-i18n="manager_center.title">Manage Center</h1>
|
||||||
<div class="language-switcher">
|
<div class="language-switcher">
|
||||||
<button
|
<button
|
||||||
class="language-button"
|
class="language-button"
|
||||||
@ -39,10 +41,16 @@
|
|||||||
aria-label="Change language"
|
aria-label="Change language"
|
||||||
aria-expanded="false"
|
aria-expanded="false"
|
||||||
data-language-toggle
|
data-language-toggle
|
||||||
data-current-lang
|
|
||||||
data-i18n-aria="manager_center.change_language"
|
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>
|
</button>
|
||||||
<div class="language-menu" data-language-menu hidden>
|
<div class="language-menu" data-language-menu hidden>
|
||||||
<button type="button" data-lang-option="en">EN</button>
|
<button type="button" data-lang-option="en">EN</button>
|
||||||
@ -56,262 +64,447 @@
|
|||||||
</nav>
|
</nav>
|
||||||
|
|
||||||
<main class="content">
|
<main class="content">
|
||||||
<section class="card manager-panel">
|
<section class="card manager-profile-card">
|
||||||
<div class="manager-grid" aria-label="Manager tools">
|
<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
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-send-frame"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="send-frame"
|
data-action="send-frame"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon frame-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 48 48">
|
<svg viewBox="0 0 36 36">
|
||||||
<path d="M10 10h28v28H10Z" />
|
|
||||||
<path d="M16 16h16v16H16Z" />
|
|
||||||
<path
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.send_frame"
|
||||||
class="tile-title"
|
|
||||||
data-i18n="manager_center.send_frame"
|
|
||||||
>Send Frame</span
|
>Send Frame</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-send-car"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="send-car"
|
data-action="send-car"
|
||||||
>
|
>
|
||||||
<span class="tile-icon car-icon" aria-hidden="true">
|
<span
|
||||||
<svg viewBox="0 0 48 48">
|
class="manager-action-icon"
|
||||||
<path d="M9 28h30l-4-10H13Z" />
|
aria-hidden="true"
|
||||||
<path d="M13 28v7M35 28v7" />
|
>
|
||||||
|
<svg viewBox="0 0 36 36">
|
||||||
<path
|
<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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.send_car"
|
||||||
class="tile-title"
|
|
||||||
data-i18n="manager_center.send_car"
|
|
||||||
>Send Car</span
|
>Send Car</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-send-badge"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="send-badge"
|
data-action="send-badge"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon badge-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 48 48">
|
<svg viewBox="0 0 36 36">
|
||||||
<path
|
<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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.send_badge"
|
||||||
class="tile-title"
|
|
||||||
data-i18n="manager_center.send_badge"
|
|
||||||
>Send Badge</span
|
>Send Badge</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-send-vip"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="send-vip"
|
data-action="send-vip"
|
||||||
>
|
>
|
||||||
<span class="tile-icon vip-icon" aria-hidden="true">
|
<span
|
||||||
<svg viewBox="0 0 48 48">
|
class="manager-action-icon"
|
||||||
|
aria-hidden="true"
|
||||||
|
>
|
||||||
|
<svg viewBox="0 0 36 36">
|
||||||
<path
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.send_vip"
|
||||||
class="tile-title"
|
|
||||||
data-i18n="manager_center.send_vip"
|
|
||||||
>Send VIP</span
|
>Send VIP</span
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-update-level"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="update-user-level"
|
data-action="update-user-level"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon level-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.user_level"
|
||||||
class="tile-title"
|
>User Level</span
|
||||||
data-i18n="manager_center.update_user_level"
|
|
||||||
>Update User Level</span
|
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-bd"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="add-bd-leader"
|
data-action="add-bd-leader"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon bd-leader-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 48 48">
|
<svg viewBox="0 0 36 36">
|
||||||
|
<circle cx="15" cy="11.5" r="6" />
|
||||||
<path
|
<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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.bd_leader"
|
||||||
class="tile-title"
|
>BD Leader</span
|
||||||
data-i18n="manager_center.add_bd_leader"
|
|
||||||
>Add BD Leader</span
|
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-invite-admin"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="add-admin"
|
data-action="add-admin"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon admin-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 48 48">
|
<svg viewBox="0 0 36 36">
|
||||||
|
<circle cx="15" cy="11.5" r="6" />
|
||||||
<path
|
<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
|
<path
|
||||||
d="M9 42c2.3-8 7.4-12 15-12s12.7 4 15 12"
|
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"
|
||||||
<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"
|
|
||||||
/>
|
/>
|
||||||
</svg>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.admin">Admin</span>
|
||||||
class="tile-title"
|
|
||||||
data-i18n="manager_center.add_admin"
|
|
||||||
>Add Admin</span
|
|
||||||
>
|
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile"
|
class="manager-action-item action-super"
|
||||||
type="button"
|
type="button"
|
||||||
data-action="add-superadmin"
|
data-action="add-superadmin"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon superadmin-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
aria-hidden="true"
|
||||||
>
|
>
|
||||||
<svg viewBox="0 0 48 48">
|
<svg viewBox="0 0 36 36">
|
||||||
<path
|
<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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.super_admin"
|
||||||
class="tile-title"
|
>Super Admin</span
|
||||||
data-i18n="manager_center.add_superadmin"
|
|
||||||
>Add Superadmin</span
|
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
|
|
||||||
<button
|
<button
|
||||||
class="manager-tile manager-tile-wide"
|
class="manager-action-item action-block-users"
|
||||||
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"
|
|
||||||
type="button"
|
type="button"
|
||||||
data-action="block-user"
|
data-action="block-user"
|
||||||
>
|
>
|
||||||
<span
|
<span
|
||||||
class="tile-icon block-icon"
|
class="manager-action-icon"
|
||||||
aria-hidden="true"
|
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
|
<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
|
<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>
|
</svg>
|
||||||
</span>
|
</span>
|
||||||
<span
|
<span data-i18n="manager_center.block_users"
|
||||||
class="tile-title"
|
>Block Users</span
|
||||||
data-i18n="manager_center.block_user"
|
|
||||||
>Block User</span
|
|
||||||
>
|
>
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
@ -519,10 +712,12 @@
|
|||||||
</div>
|
</div>
|
||||||
|
|
||||||
<script src="../../common/toast.js"></script>
|
<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/api.js?v=20260624-manager-badge-country"></script>
|
||||||
<script src="../../common/params.js?v=20260608-manager-real"></script>
|
<script src="../../common/params.js?v=20260608-manager-real"></script>
|
||||||
<script src="../../common/jsbridge.js"></script>
|
<script src="../../common/jsbridge.js"></script>
|
||||||
<script src="../../common/i18n.js?v=20260608"></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>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
|
|||||||
@ -70,7 +70,7 @@
|
|||||||
resourceType: '',
|
resourceType: '',
|
||||||
title: '',
|
title: '',
|
||||||
searchedUser: null,
|
searchedUser: null,
|
||||||
selectedResourceId: '',
|
selectedResourceIds: [],
|
||||||
selectedResourceDurationMs: DEFAULT_RESOURCE_GRANT_DURATION_MS,
|
selectedResourceDurationMs: DEFAULT_RESOURCE_GRANT_DURATION_MS,
|
||||||
selectedVipLevel: 0,
|
selectedVipLevel: 0,
|
||||||
submitting: false,
|
submitting: false,
|
||||||
@ -148,6 +148,46 @@
|
|||||||
return user.avatar || user.target_avatar || '';
|
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) {
|
function initial(text) {
|
||||||
return String(text || 'M')
|
return String(text || 'M')
|
||||||
.trim()
|
.trim()
|
||||||
@ -175,6 +215,246 @@
|
|||||||
node.textContent = initial(userName(user) || userDisplayId(user));
|
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) {
|
function formatDateTime(ms) {
|
||||||
if (!ms) return 'Never';
|
if (!ms) return 'Never';
|
||||||
var date = new Date(Number(ms));
|
var date = new Date(Number(ms));
|
||||||
@ -187,10 +467,17 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function setPageLoading(loading) {
|
function setPageLoading(loading) {
|
||||||
|
// 首屏加载态用页面级骨架屏整层覆盖(common/skeleton.js),数据就绪后整层淡出;
|
||||||
|
// is-loading 状态类保留给页面内其他降级样式使用。
|
||||||
pageState.loading = loading;
|
pageState.loading = loading;
|
||||||
var container = root();
|
var container = root();
|
||||||
if (!container) return;
|
if (container) container.classList.toggle('is-loading', loading);
|
||||||
container.classList.toggle('is-loading', loading);
|
if (!window.HyAppSkeleton) return;
|
||||||
|
if (loading) {
|
||||||
|
window.HyAppSkeleton.show();
|
||||||
|
} else {
|
||||||
|
window.HyAppSkeleton.hide();
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
function setDenied(denied) {
|
function setDenied(denied) {
|
||||||
@ -201,6 +488,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
function normalizeUser(item) {
|
function normalizeUser(item) {
|
||||||
|
var vip = item.current_vip || item.currentVip || item.vip || null;
|
||||||
return {
|
return {
|
||||||
user_id: item.user_id || item.userId || item.target_user_id || '',
|
user_id: item.user_id || item.userId || item.target_user_id || '',
|
||||||
display_user_id:
|
display_user_id:
|
||||||
@ -212,6 +500,16 @@
|
|||||||
avatar: item.avatar || item.target_avatar || '',
|
avatar: item.avatar || item.target_avatar || '',
|
||||||
country: item.country || '',
|
country: item.country || '',
|
||||||
levels: item.levels || {},
|
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) {
|
if (!api || !api.searchUsers) {
|
||||||
return Promise.reject(new Error('API not available.'));
|
return Promise.reject(new Error('API not available.'));
|
||||||
}
|
}
|
||||||
return api.searchUsers(keyword, 20, scope, action).then(function (data) {
|
return api
|
||||||
var items = (data && data.items) || [];
|
.searchUsers(keyword, 20, scope, action)
|
||||||
return items.map(normalizeUser);
|
.then(function (data) {
|
||||||
});
|
var items = (data && data.items) || [];
|
||||||
|
return items.map(normalizeUser);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function loadOverview() {
|
function loadOverview() {
|
||||||
if (!api || !api.overview) {
|
if (!api || !api.overview) {
|
||||||
setDenied(true);
|
// 无 API 注入(本地纯静态预览等)时立即渲染一次,把骨架屏收敛成中性占位符。
|
||||||
toast('Permission denied.');
|
pageState.permissions = normalizePermissions({});
|
||||||
|
renderDashboard();
|
||||||
|
renderActionPermissions();
|
||||||
|
setDenied(false);
|
||||||
return Promise.resolve();
|
return Promise.resolve();
|
||||||
}
|
}
|
||||||
setPageLoading(true);
|
setPageLoading(true);
|
||||||
@ -342,6 +645,7 @@
|
|||||||
pageState.permissions = normalizePermissions(
|
pageState.permissions = normalizePermissions(
|
||||||
pageState.overview.permissions || pageState.overview
|
pageState.overview.permissions || pageState.overview
|
||||||
);
|
);
|
||||||
|
renderDashboard();
|
||||||
renderActionPermissions();
|
renderActionPermissions();
|
||||||
setDenied(false);
|
setDenied(false);
|
||||||
return canUseAction('block-user')
|
return canUseAction('block-user')
|
||||||
@ -349,7 +653,16 @@
|
|||||||
: Promise.resolve();
|
: Promise.resolve();
|
||||||
})
|
})
|
||||||
.catch(function (error) {
|
.catch(function (error) {
|
||||||
|
if (isLocalPreviewError(error)) {
|
||||||
|
pageState.permissions = normalizePermissions({});
|
||||||
|
renderDashboard();
|
||||||
|
renderActionPermissions();
|
||||||
|
setDenied(false);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
// 接口失败也要渲染一次:把骨架屏收敛成中性占位符,页面不残留加载态。
|
||||||
setDenied(true);
|
setDenied(true);
|
||||||
|
renderDashboard();
|
||||||
showError(error);
|
showError(error);
|
||||||
})
|
})
|
||||||
.finally(function () {
|
.finally(function () {
|
||||||
@ -389,6 +702,11 @@
|
|||||||
copy.appendChild(
|
copy.appendChild(
|
||||||
createNode('div', 'searched-meta', 'UID: ' + userDisplayId(user))
|
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);
|
var button = createNode('button', 'user-block-button', actionText);
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
button.addEventListener('click', onAction);
|
button.addEventListener('click', onAction);
|
||||||
@ -681,7 +999,8 @@
|
|||||||
toast('Enter user ID.');
|
toast('Enter user ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiSearchUser(value, '', grantState.action)
|
// 封禁搜索只校验 Block User 权限;不能借用赠送弹窗状态,否则会按资源/VIP 赠送权限误判。
|
||||||
|
apiSearchUser(value, '', 'block-user')
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
blockState.searchedUser = null;
|
blockState.searchedUser = null;
|
||||||
@ -733,7 +1052,8 @@
|
|||||||
toast('Enter user ID.');
|
toast('Enter user ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiSearchUser(value)
|
// 等级调整搜索按自己的能力开关校验,避免资源赠送关闭时误挡等级操作。
|
||||||
|
apiSearchUser(value, '', 'update-user-level')
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
levelState.searchedUser = null;
|
levelState.searchedUser = null;
|
||||||
@ -815,7 +1135,7 @@
|
|||||||
grantState.resourceType = RESOURCE_TYPES[action] || '';
|
grantState.resourceType = RESOURCE_TYPES[action] || '';
|
||||||
grantState.title = ACTION_LABELS[action] || 'Send';
|
grantState.title = ACTION_LABELS[action] || 'Send';
|
||||||
grantState.searchedUser = null;
|
grantState.searchedUser = null;
|
||||||
grantState.selectedResourceId = '';
|
grantState.selectedResourceIds = [];
|
||||||
grantState.selectedResourceDurationMs =
|
grantState.selectedResourceDurationMs =
|
||||||
DEFAULT_RESOURCE_GRANT_DURATION_MS;
|
DEFAULT_RESOURCE_GRANT_DURATION_MS;
|
||||||
grantState.selectedVipLevel = 0;
|
grantState.selectedVipLevel = 0;
|
||||||
@ -860,7 +1180,8 @@
|
|||||||
toast('Enter user ID.');
|
toast('Enter user ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiSearchUser(value)
|
// 搜索入口也必须携带当前赠送动作;Send VIP 只校验 VIP 赠送权限,不能落回资源赠送权限导致误拒。
|
||||||
|
apiSearchUser(value, '', grantState.action)
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
grantState.searchedUser = null;
|
grantState.searchedUser = null;
|
||||||
@ -890,6 +1211,10 @@
|
|||||||
var grid = $('grantOptionGrid');
|
var grid = $('grantOptionGrid');
|
||||||
if (!grid) return;
|
if (!grid) return;
|
||||||
grid.innerHTML = '';
|
grid.innerHTML = '';
|
||||||
|
grid.classList.toggle(
|
||||||
|
'is-resource-grid',
|
||||||
|
grantState.action !== 'send-vip'
|
||||||
|
);
|
||||||
var durationTitle = $('grantDurationTitle');
|
var durationTitle = $('grantDurationTitle');
|
||||||
var durationGrid = $('grantDurationGrid');
|
var durationGrid = $('grantDurationGrid');
|
||||||
if (durationTitle) {
|
if (durationTitle) {
|
||||||
@ -905,24 +1230,55 @@
|
|||||||
}
|
}
|
||||||
if (grantState.action === 'send-vip') {
|
if (grantState.action === 'send-vip') {
|
||||||
$('grantOptionTitle').textContent = 'Select VIP level';
|
$('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 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) {
|
VIP_GRANT_LEVELS.forEach(function (level) {
|
||||||
var button = createNode('button', '', 'VIP ' + level);
|
var button = createNode('button', '', 'VIP ' + level);
|
||||||
|
var blockedByCurrentVip = activeVipLevel > level;
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
|
button.disabled = blockedByCurrentVip;
|
||||||
|
if (blockedByCurrentVip) {
|
||||||
|
button.title = 'Current VIP is higher';
|
||||||
|
}
|
||||||
button.classList.toggle(
|
button.classList.toggle(
|
||||||
'is-selected',
|
'is-selected',
|
||||||
grantState.selectedVipLevel === level
|
grantState.selectedVipLevel === level
|
||||||
);
|
);
|
||||||
button.addEventListener('click', function () {
|
button.addEventListener('click', function () {
|
||||||
|
if (blockedByCurrentVip) return;
|
||||||
grantState.selectedVipLevel = level;
|
grantState.selectedVipLevel = level;
|
||||||
renderGrantOptions();
|
renderGrantOptions();
|
||||||
});
|
});
|
||||||
grid.appendChild(button);
|
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 {
|
} else {
|
||||||
$('grantOptionTitle').textContent = 'Select resource';
|
$('grantOptionTitle').textContent = 'Select ' + resourceKindLabel();
|
||||||
var resources =
|
var resources =
|
||||||
pageState.resourcesByType[grantState.resourceType] || [];
|
pageState.resourcesByType[grantState.resourceType] || [];
|
||||||
|
var selectedResourceSet = grantState.selectedResourceIds.reduce(
|
||||||
|
function (result, resourceId) {
|
||||||
|
result[String(resourceId)] = true;
|
||||||
|
return result;
|
||||||
|
},
|
||||||
|
{}
|
||||||
|
);
|
||||||
if (!resources.length) {
|
if (!resources.length) {
|
||||||
grid.appendChild(
|
grid.appendChild(
|
||||||
createNode('div', 'blocked-empty', 'No resource available.')
|
createNode('div', 'blocked-empty', 'No resource available.')
|
||||||
@ -930,20 +1286,56 @@
|
|||||||
}
|
}
|
||||||
resources.forEach(function (resource) {
|
resources.forEach(function (resource) {
|
||||||
var button = createNode('button', 'grant-resource-option');
|
var button = createNode('button', 'grant-resource-option');
|
||||||
|
var imageURL = resourceImageUrl(resource);
|
||||||
|
var resourceID = resource.resource_id;
|
||||||
|
var resourceKey = String(resourceID);
|
||||||
button.type = 'button';
|
button.type = 'button';
|
||||||
|
button.setAttribute(
|
||||||
|
'aria-label',
|
||||||
|
'Select ' + resourceKindLabel()
|
||||||
|
);
|
||||||
button.classList.toggle(
|
button.classList.toggle(
|
||||||
'is-selected',
|
'is-selected',
|
||||||
String(grantState.selectedResourceId) ===
|
!!selectedResourceSet[resourceKey]
|
||||||
String(resource.resource_id)
|
|
||||||
);
|
);
|
||||||
button.textContent =
|
if (imageURL) {
|
||||||
resource.name || 'Resource #' + resource.resource_id;
|
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 () {
|
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();
|
renderGrantOptions();
|
||||||
});
|
});
|
||||||
grid.appendChild(button);
|
grid.appendChild(button);
|
||||||
});
|
});
|
||||||
|
if (grantState.selectedResourceIds.length) {
|
||||||
|
grid.appendChild(
|
||||||
|
createNode(
|
||||||
|
'div',
|
||||||
|
'resource-selected-note',
|
||||||
|
grantState.selectedResourceIds.length + ' selected'
|
||||||
|
)
|
||||||
|
);
|
||||||
|
}
|
||||||
if (durationGrid) {
|
if (durationGrid) {
|
||||||
RESOURCE_GRANT_DURATIONS.forEach(function (item) {
|
RESOURCE_GRANT_DURATIONS.forEach(function (item) {
|
||||||
var durationButton = createNode('button', '', item.label);
|
var durationButton = createNode('button', '', item.label);
|
||||||
@ -966,8 +1358,10 @@
|
|||||||
!grantState.searchedUser ||
|
!grantState.searchedUser ||
|
||||||
grantState.submitting ||
|
grantState.submitting ||
|
||||||
(grantState.action === 'send-vip'
|
(grantState.action === 'send-vip'
|
||||||
? !grantState.selectedVipLevel
|
? !grantState.selectedVipLevel ||
|
||||||
: !grantState.selectedResourceId ||
|
currentVipLevel(grantState.searchedUser) >
|
||||||
|
grantState.selectedVipLevel
|
||||||
|
: !grantState.selectedResourceIds.length ||
|
||||||
!grantState.selectedResourceDurationMs);
|
!grantState.selectedResourceDurationMs);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -986,7 +1380,7 @@
|
|||||||
payload.level = grantState.selectedVipLevel;
|
payload.level = grantState.selectedVipLevel;
|
||||||
request = api.grantVip(payload);
|
request = api.grantVip(payload);
|
||||||
} else {
|
} else {
|
||||||
payload.resource_id = grantState.selectedResourceId;
|
payload.resource_ids = grantState.selectedResourceIds.slice();
|
||||||
payload.duration_ms = grantState.selectedResourceDurationMs;
|
payload.duration_ms = grantState.selectedResourceDurationMs;
|
||||||
request = api.grantResource(payload);
|
request = api.grantResource(payload);
|
||||||
}
|
}
|
||||||
@ -1065,7 +1459,8 @@
|
|||||||
toast('Enter user ID.');
|
toast('Enter user ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiSearchUser(value)
|
// BD/Admin/Superadmin 共用弹窗,搜索阶段先按当前按钮的能力开关过滤;写入口仍会二次校验真实落库能力。
|
||||||
|
apiSearchUser(value, '', bdLeaderState.action)
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
bdLeaderState.searchedUser = null;
|
bdLeaderState.searchedUser = null;
|
||||||
@ -1257,7 +1652,8 @@
|
|||||||
toast('Enter user ID.');
|
toast('Enter user ID.');
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
apiSearchUser(value, 'all')
|
// 转国家是唯一允许跨国家搜索的入口,必须同时携带 scope=all 和动作,避免额外依赖资源赠送权限。
|
||||||
|
apiSearchUser(value, 'all', 'transfer-user-country')
|
||||||
.then(function (items) {
|
.then(function (items) {
|
||||||
if (!items.length) {
|
if (!items.length) {
|
||||||
countryState.searchedUser = null;
|
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() {
|
function bindEvents() {
|
||||||
var backButton = $('backButton');
|
var backButton = $('backButton');
|
||||||
if (backButton) backButton.addEventListener('click', goBack);
|
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) {
|
document.querySelectorAll('[data-action]').forEach(function (button) {
|
||||||
button.addEventListener('click', function () {
|
button.addEventListener('click', function () {
|
||||||
var action = button.getAttribute('data-action');
|
var action = button.getAttribute('data-action');
|
||||||
@ -1453,6 +1870,7 @@
|
|||||||
function init() {
|
function init() {
|
||||||
bindEvents();
|
bindEvents();
|
||||||
renderBlockedList();
|
renderBlockedList();
|
||||||
|
// 首屏不预渲染仪表盘:骨架屏保持到 loadOverview 的任一分支调用 renderDashboard 收敛。
|
||||||
loadOverview();
|
loadOverview();
|
||||||
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
if (window.HyAppBridge && window.HyAppBridge.ready) {
|
||||||
window.HyAppBridge.ready({ page: 'manager-center' });
|
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 {
|
.manager-panel {
|
||||||
position: relative;
|
position: relative;
|
||||||
display: grid;
|
display: grid;
|
||||||
@ -392,6 +933,7 @@
|
|||||||
}
|
}
|
||||||
|
|
||||||
.searched-meta,
|
.searched-meta,
|
||||||
|
.searched-vip,
|
||||||
.blocked-meta,
|
.blocked-meta,
|
||||||
.blocked-time {
|
.blocked-time {
|
||||||
margin-top: 3px;
|
margin-top: 3px;
|
||||||
@ -404,6 +946,10 @@
|
|||||||
white-space: nowrap;
|
white-space: nowrap;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.searched-vip {
|
||||||
|
color: #7d57c7;
|
||||||
|
}
|
||||||
|
|
||||||
.user-block-button {
|
.user-block-button {
|
||||||
min-width: 70px;
|
min-width: 70px;
|
||||||
height: 40px;
|
height: 40px;
|
||||||
@ -486,14 +1032,114 @@
|
|||||||
color: #fff;
|
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 {
|
.grant-option-grid {
|
||||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
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 {
|
.grant-resource-option {
|
||||||
|
position: relative;
|
||||||
overflow: hidden;
|
overflow: hidden;
|
||||||
text-overflow: ellipsis;
|
display: grid;
|
||||||
white-space: nowrap;
|
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 {
|
.country-option {
|
||||||
|
|||||||
@ -3,6 +3,8 @@
|
|||||||
"version": "1.0.0",
|
"version": "1.0.0",
|
||||||
"description": "H5 project",
|
"description": "H5 project",
|
||||||
"scripts": {
|
"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": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||||
},
|
},
|
||||||
|
|||||||
@ -12,6 +12,7 @@ import urllib.request
|
|||||||
|
|
||||||
|
|
||||||
PROXIES = {
|
PROXIES = {
|
||||||
|
"/__api_prod__/": "https://api.global-interaction.com/",
|
||||||
"/__api_test__/": "https://api-test.global-interaction.com/",
|
"/__api_test__/": "https://api-test.global-interaction.com/",
|
||||||
"/__api_local__/": "http://127.0.0.1:13000/",
|
"/__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")
|
length = int(self.headers.get("Content-Length", "0") or "0")
|
||||||
body = self.rfile.read(length) if length > 0 else b""
|
body = self.rfile.read(length) if length > 0 else b""
|
||||||
|
|
||||||
|
# 代理只转发业务相关 header;Host、Connection 等 hop-by-hop header 必须由 urllib 按目标域重建,
|
||||||
|
# 否则生产网关会看到本地 Host 或错误的连接语义,排查时会像业务接口异常。
|
||||||
headers = {
|
headers = {
|
||||||
key: value
|
key: value
|
||||||
for key, value in self.headers.items()
|
for key, value in self.headers.items()
|
||||||
@ -179,7 +182,7 @@ class ReusableTCPServer(socketserver.ThreadingTCPServer):
|
|||||||
def main() -> None:
|
def main() -> None:
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
parser.add_argument("--host", default="127.0.0.1")
|
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())
|
parser.add_argument("--directory", default=os.getcwd())
|
||||||
args = parser.parse_args()
|
args = parser.parse_args()
|
||||||
|
|
||||||
@ -190,6 +193,8 @@ def main() -> None:
|
|||||||
)
|
)
|
||||||
with ReusableTCPServer((args.host, args.port), handler) as server:
|
with ReusableTCPServer((args.host, args.port), handler) as server:
|
||||||
print(f"Serving H5 at http://{args.host}:{args.port}/")
|
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_test__/ -> https://api-test.global-interaction.com/")
|
||||||
print("API proxy: /__api_local__/ -> http://127.0.0.1:13000/")
|
print("API proxy: /__api_local__/ -> http://127.0.0.1:13000/")
|
||||||
server.serve_forever()
|
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