feat: add Fami and Huwaa H5 experiences

This commit is contained in:
ZuoZuo 2026-07-17 11:54:15 +08:00
parent 5d1d743990
commit 94c900c3c4
265 changed files with 29911 additions and 3861 deletions

7
.gitignore vendored
View File

@ -1 +1,6 @@
node_modules/
node_modules/
.waylog/
**/.audit/
.DS_Store
design-qa.md
**/design-qa.md

View File

@ -11,3 +11,11 @@ H5 多语言能力放在 common/i18n.js 和 common/locales/\*.json当前面
所有逻辑功能配置默认打开;只有密钥、地址、三方账号等必须由环境提供的敏感值使用占位符或环境配置,不在默认配置里伪造可用凭证。
所有业务逻辑、状态流转、权限判断、支付链路、异步流程都保持高密度逻辑注释,注释解释为什么这样处理和失败分支,不复述语法。
## Figma / 截图还原超级红线
- 全局永久禁止把整页截图、Figma 根 Frame、整段轮播、整块底栏或包含多个可独立元素的大区块直接作为页面图片实现。
- 设计稿必须拆成独立视觉图层;文字、按钮、状态、列表、动态媒体与交互区域必须使用 DOM、CSS 和真实接口数据实现。
- 导出的设计素材必须能追溯到具体的单一视觉节点,并记录 source node、用途、尺寸和哈希禁止素材内嵌动态文字、控件或业务媒体。
- 允许使用不包含文字、控件和动态业务内容的纯背景层,但它不能替代其上的任何可拆元素。
- 验收必须按图层、元素边界、间距、样式和交互逐项核对;禁止用整页设计截图覆盖页面,也禁止把整页截图像素差分作为通过依据。

267
activity/cp-space/fami.html Normal file
View File

@ -0,0 +1,267 @@
<!doctype html>
<html lang="en" dir="ltr" data-hy-app-code="fami">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title data-i18n="cpSpace.pageTitle">Best Friend</title>
<link rel="icon" href="data:," />
<link
rel="preload"
href="./fami/assets/fonts/source-han-sans-sc-medium-latin.woff2"
as="font"
type="font/woff2"
crossorigin
/>
<link rel="stylesheet" href="../../common/theme.css?v=20260716" />
<link rel="stylesheet" href="./fami/fami.css?v=20260716" />
</head>
<body>
<main
class="fami-page"
id="famiPage"
data-tab="cp"
aria-label="Best Friend space"
data-i18n-aria="cpSpace.pageLabel"
>
<section class="fami-stage" id="famiStage">
<div class="fami-design" id="famiDesign">
<img
class="fami-background"
id="famiBackground"
src="./fami/assets/layers/bg-cp.png"
alt=""
draggable="false"
/>
<section class="fami-status-bar" aria-hidden="true">
<span class="fami-status-time">9:41</span>
<img
class="fami-status-cellular"
src="./fami/assets/layers/cellular.svg"
alt=""
/>
<img
class="fami-status-wifi"
src="./fami/assets/layers/wifi.svg"
alt=""
/>
<img
class="fami-status-battery"
src="./fami/assets/layers/battery.svg"
alt=""
/>
</section>
<header
class="fami-nav"
aria-label="Best Friend navigation"
data-i18n-aria="cpSpace.navLabel"
>
<button
class="fami-nav-button fami-nav-button--back"
id="famiBackButton"
type="button"
aria-label="Back"
data-i18n-aria="cpSpace.back"
>
<img src="./fami/assets/layers/back.svg" alt="" />
</button>
<h1 data-i18n="cpSpace.title">Best Friend</h1>
<button
class="fami-nav-button fami-nav-button--help"
id="famiHelpButton"
type="button"
aria-label="Help"
data-i18n-aria="cpSpace.help"
>
<img src="./fami/assets/layers/help.svg" alt="" />
</button>
</header>
<nav
class="fami-tabs"
role="tablist"
aria-label="Best Friend tabs"
data-i18n-aria="cpSpace.tabsLabel"
>
<img
class="fami-tab-jewel"
id="famiTabJewel"
src="./fami/assets/layers/tab-cp.png"
alt=""
draggable="false"
/>
<button
id="famiTabCp"
type="button"
role="tab"
aria-controls="famiContent"
data-tab="cp"
>
<span data-i18n="cpSpace.tabCp">CP</span>
</button>
<button
id="famiTabBrother"
type="button"
role="tab"
aria-controls="famiContent"
data-tab="brother"
>
<span data-i18n="cpSpace.tabBrother">Brother</span>
</button>
<button
id="famiTabSister"
type="button"
role="tab"
aria-controls="famiContent"
data-tab="sister"
>
<span data-i18n="cpSpace.tabSister">Sister</span>
</button>
</nav>
<section
class="fami-content"
id="famiContent"
role="tabpanel"
aria-labelledby="famiTabCp"
aria-live="polite"
aria-busy="true"
>
<div
class="fami-relation-list"
id="famiRelationList"
></div>
<button
class="fami-state"
id="famiStateButton"
type="button"
hidden
></button>
</section>
</div>
</section>
</main>
<template id="famiRelationTemplate">
<article class="fami-relation-card">
<img
class="fami-card-skin"
data-layer="card"
src="./fami/assets/layers/card-cp.png"
alt=""
draggable="false"
/>
<figure class="fami-avatar fami-avatar--left">
<span class="fami-avatar-photo" data-avatar="me"></span>
<img
class="fami-avatar-frame"
src="./fami/assets/layers/avatar-frame.png"
alt=""
draggable="false"
/>
</figure>
<figure class="fami-avatar fami-avatar--right">
<span
class="fami-avatar-photo"
data-avatar="partner"
></span>
<img
class="fami-avatar-frame"
src="./fami/assets/layers/avatar-frame.png"
alt=""
draggable="false"
/>
</figure>
<div class="fami-center-art-wrap">
<img
class="fami-center-art"
data-layer="center"
src="./fami/assets/layers/center-cp.png"
alt=""
draggable="false"
/>
<strong class="fami-days-value" data-value="days">0</strong>
<span class="fami-days-unit" data-i18n="cpSpace.daysUnit"
>Days</span
>
</div>
<div class="fami-exp-pill">
<span class="fami-exp-pill-skin fami-exp-pill-skin--cp">
<img
src="./fami/assets/layers/pill-cp-left.png"
alt=""
/>
<img
src="./fami/assets/layers/pill-cp-center.png"
alt=""
/>
<img
src="./fami/assets/layers/pill-cp-right.png"
alt=""
/>
</span>
<img
class="fami-exp-pill-skin fami-exp-pill-skin--brother"
src="./fami/assets/layers/pill-brother.png"
alt=""
/>
<img
class="fami-exp-pill-skin fami-exp-pill-skin--sister"
src="./fami/assets/layers/pill-sister.png"
alt=""
/>
<span data-value="exp-away"></span>
</div>
<section class="fami-progress-block">
<div class="fami-level-badge fami-level-badge--left">
<img
data-layer="level"
src="./fami/assets/layers/level-cp.png"
alt=""
draggable="false"
/>
<span data-value="level-left"></span>
</div>
<div class="fami-progress-track">
<span
class="fami-progress-fill"
data-value="progress"
></span>
<strong
class="fami-progress-text"
data-value="progress-text"
>0/0</strong
>
</div>
<div class="fami-level-badge fami-level-badge--right">
<img
data-layer="level"
src="./fami/assets/layers/level-cp.png"
alt=""
draggable="false"
/>
<span data-value="level-right"></span>
</div>
<p class="fami-time-text" data-value="time"></p>
</section>
</article>
</template>
<script src="./fami/product.js?v=20260716"></script>
<script src="../../common/params.js?v=20260716"></script>
<script src="../../common/i18n.js?v=20260716"></script>
<script src="../../common/api.js?v=20260716"></script>
<script src="../../common/jsbridge.js?v=20260716"></script>
<script src="../../common/toast.js?v=20260716"></script>
<script src="./fami/fami.js?v=20260716"></script>
</body>
</html>

View File

@ -0,0 +1,96 @@
Copyright 2014-2025 Adobe (http://www.adobe.com/), with Reserved Font
Name 'Source'. Source is a trademark of Adobe in the United States
and/or other countries.
This Font Software is licensed under the SIL Open Font License,
Version 1.1.
This license is copied below, and is also available with a FAQ at:
http://scripts.sil.org/OFL
-----------------------------------------------------------
SIL OPEN FONT LICENSE Version 1.1 - 26 February 2007
-----------------------------------------------------------
PREAMBLE
The goals of the Open Font License (OFL) are to stimulate worldwide
development of collaborative font projects, to support the font
creation efforts of academic and linguistic communities, and to
provide a free and open framework in which fonts may be shared and
improved in partnership with others.
The OFL allows the licensed fonts to be used, studied, modified and
redistributed freely as long as they are not sold by themselves. The
fonts, including any derivative works, can be bundled, embedded,
redistributed and/or sold with any software provided that any reserved
names are not used by derivative works. The fonts and derivatives,
however, cannot be released under any other type of license. The
requirement for fonts to remain under this license does not apply to
any document created using the fonts or their derivatives.
DEFINITIONS
"Font Software" refers to the set of files released by the Copyright
Holder(s) under this license and clearly marked as such. This may
include source files, build scripts and documentation.
"Reserved Font Name" refers to any names specified as such after the
copyright statement(s).
"Original Version" refers to the collection of Font Software
components as distributed by the Copyright Holder(s).
"Modified Version" refers to any derivative made by adding to,
deleting, or substituting -- in part or in whole -- any of the
components of the Original Version, by changing formats or by porting
the Font Software to a new environment.
"Author" refers to any designer, engineer, programmer, technical
writer or other person who contributed to the Font Software.
PERMISSION & CONDITIONS
Permission is hereby granted, free of charge, to any person obtaining
a copy of the Font Software, to use, study, copy, merge, embed,
modify, redistribute, and sell modified and unmodified copies of the
Font Software, subject to the following conditions:
1) Neither the Font Software nor any of its individual components, in
Original or Modified Versions, may be sold by itself.
2) Original or Modified Versions of the Font Software may be bundled,
redistributed and/or sold with any software, provided that each copy
contains the above copyright notice and this license. These can be
included either as stand-alone text files, human-readable headers or
in the appropriate machine-readable metadata fields within text or
binary files as long as those fields can be easily viewed by the user.
3) No Modified Version of the Font Software may use the Reserved Font
Name(s) unless explicit written permission is granted by the
corresponding Copyright Holder. This restriction only applies to the
primary font name as presented to the users.
4) The name(s) of the Copyright Holder(s) or the Author(s) of the Font
Software shall not be used to promote, endorse or advertise any
Modified Version, except to acknowledge the contribution(s) of the
Copyright Holder(s) and the Author(s) or with their explicit written
permission.
5) The Font Software, modified or unmodified, in part or in whole,
must be distributed entirely under this license, and must not be
distributed under any other license. The requirement for fonts to
remain under this license does not apply to any document created using
the Font Software.
TERMINATION
This license becomes null and void if any of the above conditions are
not met.
DISCLAIMER
THE FONT SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO ANY WARRANTIES OF
MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT
OF COPYRIGHT, PATENT, TRADEMARK, OR OTHER RIGHT. IN NO EVENT SHALL THE
COPYRIGHT HOLDER BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
INCLUDING ANY GENERAL, SPECIAL, INDIRECT, INCIDENTAL, OR CONSEQUENTIAL
DAMAGES, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
FROM, OUT OF THE USE OR INABILITY TO USE THE FONT SOFTWARE OR FROM
OTHER DEALINGS IN THE FONT SOFTWARE.

Binary file not shown.

After

Width:  |  Height:  |  Size: 4.3 KiB

View File

@ -0,0 +1,9 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_20_&#232;&#191;&#148;&#229;&#155;&#158;">
<g id="Vector">
</g>
<g id="Vector_2">
</g>
<path id="Vector_3" d="M16 4L8 12L16 20" stroke="var(--stroke-0, white)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 419 B

View File

@ -0,0 +1,7 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24.3307 11.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Battery">
<path id="Border" opacity="0.35" d="M2.66699 0.5H19.333C20.5296 0.5 21.5 1.47038 21.5 2.66699V8.66699C21.4998 9.86346 20.5295 10.833 19.333 10.833H2.66699C1.4705 10.833 0.500175 9.86346 0.5 8.66699V2.66699C0.5 1.47038 1.47039 0.5 2.66699 0.5Z" stroke="var(--stroke-0, white)"/>
<path id="Cap" opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M23.0027 7.66213C23.8074 7.32336 24.3307 6.53526 24.3307 5.66213C24.3307 4.789 23.8074 4.00091 23.0027 3.66213V7.66213V7.66213Z" fill="var(--fill-0, white)"/>
<path id="Capacity" fill-rule="evenodd" clip-rule="evenodd" d="M18.666 1.99872C19.4024 1.99872 19.9993 2.59567 19.9993 3.33205V7.99872C19.9993 8.7351 19.4024 9.33205 18.666 9.33205H3.33268C2.5963 9.33205 1.99935 8.7351 1.99935 7.99872V3.33205C1.99935 2.59567 2.5963 1.99872 3.33268 1.99872H18.666V1.99872Z" fill="var(--fill-0, white)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 677 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 670 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 702 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 121 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 115 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 118 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 17.0001 11.3317" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Cellular Connection" fill-rule="evenodd" clip-rule="evenodd" d="M11.331 2.99835C11.8832 2.99835 12.331 3.44607 12.331 3.99835V10.3317C12.331 10.884 11.8832 11.3317 11.331 11.3317H10.331C9.77867 11.3317 9.33096 10.884 9.33096 10.3317V3.99835C9.33096 3.44607 9.77867 2.99835 10.331 2.99835H11.331V2.99835ZM16.0001 0C16.5524 0 17.0001 0.447712 17.0001 1V9.66667C17.0001 10.219 16.5524 10.6667 16.0001 10.6667H15.0001C14.4479 10.6667 14.0001 10.219 14.0001 9.66667V1C14.0001 0.447712 14.4479 0 15.0001 0V0H16.0001V0ZM2 7.00378C2.55229 7.00378 3 7.4515 3 8.00378V10.0038C3 10.5561 2.55229 11.0038 2 11.0038H0.999999C0.447714 11.0038 0 10.5561 0 10.0038V8.00378C0 7.4515 0.447714 7.00378 0.999999 7.00378H2V7.00378ZM6.66177 4.99767C7.21405 4.99767 7.66177 5.44538 7.66177 5.99767V9.99766C7.66177 10.55 7.21405 10.9977 6.66177 10.9977H5.66177C5.10948 10.9977 4.66177 10.55 4.66177 9.99766V5.99767C4.66177 5.44538 5.10948 4.99767 5.66177 4.99767H6.66177V4.99767Z" fill="var(--fill-0, white)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

View File

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 25.4 25.4" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="help">
<path id="Vector" d="M12.7 19.3667V19.38M12.7 14.7C12.6755 14.2672 12.7922 13.8381 13.0327 13.4774C13.2732 13.1166 13.6244 12.8438 14.0333 12.7C14.5345 12.5084 14.9843 12.203 15.3474 11.808C15.7105 11.4129 15.977 10.939 16.1258 10.4235C16.2746 9.90796 16.3017 9.36494 16.205 8.83717C16.1083 8.3094 15.8904 7.81128 15.5684 7.38204C15.2465 6.95279 14.8293 6.60413 14.3497 6.36351C13.8701 6.12289 13.3413 5.99687 12.8047 5.99538C12.2681 5.99389 11.7386 6.11697 11.2577 6.35493C10.7768 6.59289 10.3576 6.93923 10.0333 7.36668M0.7 12.7C0.7 14.2759 1.01039 15.8363 1.61345 17.2922C2.2165 18.7481 3.10042 20.071 4.21472 21.1853C5.32902 22.2996 6.65189 23.1835 8.1078 23.7866C9.56371 24.3896 11.1241 24.7 12.7 24.7C14.2759 24.7 15.8363 24.3896 17.2922 23.7866C18.7481 23.1835 20.071 22.2996 21.1853 21.1853C22.2996 20.071 23.1835 18.7481 23.7866 17.2922C24.3896 15.8363 24.7 14.2759 24.7 12.7C24.7 11.1241 24.3896 9.56371 23.7866 8.1078C23.1835 6.65189 22.2996 5.32902 21.1853 4.21472C20.071 3.10042 18.7481 2.2165 17.2922 1.61345C15.8363 1.01039 14.2759 0.7 12.7 0.7C11.1241 0.7 9.56371 1.01039 8.1078 1.61345C6.65189 2.2165 5.32902 3.10042 4.21472 4.21472C3.10042 5.32902 2.2165 6.65189 1.61345 8.1078C1.01039 9.56371 0.7 11.1241 0.7 12.7Z" stroke="var(--stroke-0, white)" stroke-width="1.4" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1.5 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1017 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 1016 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 967 B

View File

@ -0,0 +1,464 @@
{
"schemaVersion": 1,
"rule": "Layered restoration only. Root frames, full-page screenshots, dynamic text, controls, and production avatars are not implementation assets.",
"figma": {
"fileKey": "vaDp5wwn6AOQVlt9zk0f9r",
"page": {
"id": "1:27538",
"name": "yumi"
},
"entryFrames": {
"cp": "859:2027",
"brother": "859:2247",
"sister": "859:2454"
}
},
"baseFrame": {
"width": 375,
"height": 812
},
"assets": [
{
"file": "bg-cp.png",
"sourceNode": "859:2028",
"equivalentNodes": [],
"sourceName": "Mask group",
"sourceType": "frame",
"purpose": "CP pure background layer",
"bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
"exportFormat": "png",
"bytes": 685603,
"sha256": "a16b0d609a0485653e866b6f6cdaa22a8e22cf0d4bd2edd6c738d6e1b8e82747",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "bg-brother.png",
"sourceNode": "859:2248",
"equivalentNodes": [],
"sourceName": "Mask group",
"sourceType": "frame",
"purpose": "Brother pure background layer",
"bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
"exportFormat": "png",
"bytes": 693082,
"sha256": "866c6b111bd91894dab45d23a96493cb83fb526e282ce9ce26a4f243f5896c5e",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "bg-sister.png",
"sourceNode": "859:2455",
"equivalentNodes": [],
"sourceName": "Mask group",
"sourceType": "frame",
"purpose": "Sister pure background layer",
"bounds": { "x": 0, "y": 0, "width": 375, "height": 812 },
"exportFormat": "png",
"bytes": 718426,
"sha256": "701a6735476e4fe44c2c01a94968dc6704c94c72171f788bdf9be398dacbd554",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "tab-cp.png",
"sourceNode": "859:2059",
"equivalentNodes": [],
"sourceName": "Group 2090056311",
"sourceType": "frame",
"purpose": "Selected CP tab jewel without label text",
"bounds": { "x": 9, "y": 107, "width": 130, "height": 48 },
"exportFormat": "png",
"bytes": 12416,
"sha256": "2f024464ea3b42311732118312817f92af4558e2abf73331b96951515f961de0",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "tab-brother.png",
"sourceNode": "859:2279",
"equivalentNodes": [],
"sourceName": "Group 2090056311",
"sourceType": "frame",
"purpose": "Selected Brother tab jewel without label text",
"bounds": { "x": 126, "y": 107, "width": 130, "height": 48 },
"exportFormat": "png",
"bytes": 12330,
"sha256": "eb092fc233d2496860f8be984bb259da4c32357703edd3f4dec51517ea7ba66c",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "tab-sister.png",
"sourceNode": "859:2486",
"equivalentNodes": [],
"sourceName": "Group 2090056343",
"sourceType": "frame",
"purpose": "Selected Sister tab jewel without label text",
"bounds": { "x": 236, "y": 107, "width": 130, "height": 48 },
"exportFormat": "png",
"bytes": 11938,
"sha256": "ed44cc60056760e216131dda2b932783a315de9c6ba88992fbafb6d210948f76",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "card-cp.png",
"sourceNode": "859:2065",
"equivalentNodes": [],
"sourceName": "Group 2090056340",
"sourceType": "frame",
"purpose": "CP decorative card skin without relationship data",
"bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
"exportFormat": "png",
"bytes": 117918,
"sha256": "4f78f7349e7bdd2d501d7fd017a1fe955323d6d9d316b818f4d72b1645c84929",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "card-brother.png",
"sourceNode": "859:2287",
"equivalentNodes": [],
"sourceName": "Group 2090056325",
"sourceType": "frame",
"purpose": "Brother decorative card skin without relationship data",
"bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
"exportFormat": "png",
"bytes": 123692,
"sha256": "81e6bbf780fbccb7796c160d4e8e6596bed2af6cfec0a2b378f8ebf1fd466e9b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "card-sister.png",
"sourceNode": "859:2494",
"equivalentNodes": [],
"sourceName": "Group 2090056336",
"sourceType": "frame",
"purpose": "Sister decorative card skin without relationship data",
"bounds": { "x": 14, "y": 169, "width": 347, "height": 199 },
"exportFormat": "png",
"bytes": 120470,
"sha256": "803644ee833188033a2c4f7a2b4f051228ca7138eb0d478e426a24056a461653",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "center-cp.png",
"sourceNode": "859:2189",
"equivalentNodes": [],
"sourceName": "image 4790",
"sourceType": "rounded-rectangle",
"purpose": "CP center heart ornament without days or EXP text",
"bounds": { "x": 120, "y": 175, "width": 136, "height": 120 },
"exportFormat": "png",
"bytes": 20823,
"sha256": "5041a01b2856d7f28843601652da21791d47b81a6d0a22b08a80f83c487c5520",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "center-brother.png",
"sourceNode": "859:2409",
"equivalentNodes": [],
"sourceName": "image 4799",
"sourceType": "rounded-rectangle",
"purpose": "Brother center shield ornament without days or EXP text",
"bounds": { "x": 120, "y": 175, "width": 136, "height": 136 },
"exportFormat": "png",
"bytes": 18728,
"sha256": "1765558831f54c2404464d5ae907ddf0011d094eecb8c927b5be47af53d8ee62",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "center-sister.png",
"sourceNode": "859:2618",
"equivalentNodes": [],
"sourceName": "image 4796",
"sourceType": "rounded-rectangle",
"purpose": "Sister floral ornament without days or EXP text",
"bounds": { "x": 120, "y": 175, "width": 136, "height": 136 },
"exportFormat": "png",
"bytes": 34323,
"sha256": "eccd16b84a61a262953eac81cd5b511256bfb595c9c3a130785c5861dfb2fd43",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "avatar-frame.png",
"sourceNode": "859:2183",
"equivalentNodes": [
"859:2211",
"859:2403",
"859:2418",
"859:2612",
"859:2627"
],
"sourceName": "3 4456240",
"sourceType": "rounded-rectangle",
"purpose": "Decorative avatar ring shared by both relationship members",
"bounds": { "width": 66, "height": 66 },
"exportFormat": "png",
"bytes": 4406,
"sha256": "a2d56dd2b81fafa471ca6a41f7f59df9d8aba2c29c9e90f5348d6818b4326e30",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "level-cp.png",
"sourceNode": "859:2218",
"equivalentNodes": ["859:2228"],
"sourceName": "image 4816",
"sourceType": "rounded-rectangle",
"purpose": "CP level badge background without level text",
"bounds": { "width": 24, "height": 13 },
"exportFormat": "png",
"bytes": 1016,
"sha256": "36e0a7bbcf4cb5d39bd61153b8ca1cf4e76e8c3293c064127e5c097bf7a5d148",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "level-brother.png",
"sourceNode": "859:2425",
"equivalentNodes": ["859:2435"],
"sourceName": "image 4817",
"sourceType": "rounded-rectangle",
"purpose": "Brother level badge background without level text",
"bounds": { "width": 24, "height": 13 },
"exportFormat": "png",
"bytes": 1017,
"sha256": "da0b413aab96eb7890af30f9a3daef16c2ea03d607767430f2b8c6fb483b4b4b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "level-sister.png",
"sourceNode": "859:2634",
"equivalentNodes": ["859:2644"],
"sourceName": "image 4819",
"sourceType": "rounded-rectangle",
"purpose": "Sister level badge background without level text",
"bounds": { "width": 24, "height": 13 },
"exportFormat": "png",
"bytes": 967,
"sha256": "ac5339c0505f194757e1ae0f67e3e69633f73a8c19f99355b956c0d1b038024c",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "pill-cp-left.png",
"sourceNode": "859:2191",
"equivalentNodes": [],
"sourceName": "Group 2085668693 2",
"sourceType": "rounded-rectangle",
"purpose": "Left CP EXP pill cap without dynamic text",
"bounds": { "width": 49, "height": 28 },
"exportFormat": "png",
"bytes": 2196,
"sha256": "01a8328f9e1f2ebc0973f5cdecb104af634bb28c5d47f5b75d78d0837d7bafdd",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "pill-cp-center.png",
"sourceNode": "859:2192",
"equivalentNodes": [
"859:2193",
"859:2194",
"859:2195",
"859:2196",
"859:2197",
"859:2198",
"859:2199",
"859:2200",
"859:2201",
"859:2202",
"859:2203",
"859:2204",
"859:2205"
],
"sourceName": "Group 2085668693 4",
"sourceType": "rounded-rectangle",
"purpose": "Repeatable CP EXP pill center slice without dynamic text",
"bounds": { "width": 1, "height": 28 },
"exportFormat": "png",
"bytes": 241,
"sha256": "e9940fdbe009e5db2c38992609d56bc162262da870bb5032cc08218aa979444c",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "pill-cp-right.png",
"sourceNode": "859:2206",
"equivalentNodes": [],
"sourceName": "Group 2085668693 3",
"sourceType": "rounded-rectangle",
"purpose": "Right CP EXP pill cap without dynamic text",
"bounds": { "width": 49, "height": 28 },
"exportFormat": "png",
"bytes": 2236,
"sha256": "1ad302ea0c693b0544e666c0a0289a42eaf27cd1a3b210040cf1fe7fc839fa9d",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "pill-brother.png",
"sourceNode": "859:2411",
"equivalentNodes": [],
"sourceName": "Mask group",
"sourceType": "frame",
"purpose": "Brother EXP pill background without dynamic text",
"bounds": { "width": 112, "height": 28 },
"exportFormat": "png",
"bytes": 5154,
"sha256": "90af1c3725f500489f84e25b1a025aa20ca6bc6edab56087f108a32dfeecccfc",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "pill-sister.png",
"sourceNode": "859:2620",
"equivalentNodes": [],
"sourceName": "Mask group",
"sourceType": "frame",
"purpose": "Sister EXP pill background without dynamic text",
"bounds": { "width": 112, "height": 28 },
"exportFormat": "png",
"bytes": 5095,
"sha256": "b7587a4b7b2e1c40bbb9cc42689349351f55b933ab0a687d4ea4a4e2c65440d9",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "back.svg",
"sourceNode": "859:2051",
"equivalentNodes": ["859:2271", "859:2478"],
"sourceName": "icon_20_返回",
"sourceType": "frame",
"purpose": "Back glyph rendered inside a real DOM button",
"bounds": { "x": 16, "y": 54, "width": 24, "height": 24 },
"exportFormat": "svg",
"bytes": 419,
"sha256": "50d06f24adb0019fcdfb3c3f5db012808c230cd8c2f06c61b775fcccdbf525d4",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "help.svg",
"sourceNode": "859:2055",
"equivalentNodes": ["859:2275", "859:2482"],
"sourceName": "help",
"sourceType": "frame",
"purpose": "Help glyph rendered inside a real DOM button",
"bounds": { "x": 335, "y": 54, "width": 24, "height": 24 },
"exportFormat": "svg",
"bytes": 1535,
"sha256": "18087727ea208b257df9d875523dd45e29ccd0b05912a4504f72555cd15c045e",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "battery.svg",
"sourceNode": "859:2034",
"equivalentNodes": ["859:2254", "859:2461"],
"sourceName": "Battery",
"sourceType": "frame",
"purpose": "Static Figma status-bar battery glyph",
"bounds": {
"x": 336.67,
"y": 16.33,
"width": 24.33,
"height": 11.33
},
"exportFormat": "svg",
"bytes": 1055,
"sha256": "e4d18984f66b6e24e3ecf95ac7a893e3ac2e863a0b52c02ff075860b30a8b837",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "wifi.svg",
"sourceNode": "859:2038",
"equivalentNodes": ["859:2258", "859:2465"],
"sourceName": "Wifi",
"sourceType": "boolean-operation",
"purpose": "Static Figma status-bar Wi-Fi glyph",
"bounds": {
"x": 316.34,
"y": 16.33,
"width": 15.33,
"height": 11.01
},
"exportFormat": "svg",
"bytes": 1655,
"sha256": "d84c6cdf1a0f03a824a5b40174d8f326b62301a9c83e90a65600cfb04f1d9411",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "cellular.svg",
"sourceNode": "859:2042",
"equivalentNodes": ["859:2262", "859:2469"],
"sourceName": "Cellular Connection",
"sourceType": "boolean-operation",
"purpose": "Static Figma status-bar cellular glyph",
"bounds": { "x": 294.34, "y": 15, "width": 17, "height": 11.33 },
"exportFormat": "svg",
"bytes": 1183,
"sha256": "f5df0579b8435fef2e8a8cd8db3998c72947a409f44d25a5486f91f8adb745f3",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "mock-avatar.png",
"sourceNode": "859:2181",
"equivalentNodes": [
"859:2209",
"859:2401",
"859:2416",
"859:2610",
"859:2625"
],
"sourceName": "image 4626",
"sourceType": "rounded-rectangle",
"purpose": "Figma reference avatar used only by the explicit mock visual-QA mode",
"bounds": { "width": 61.29, "height": 61.29 },
"exportFormat": "png",
"bytes": 186219,
"sha256": "31f699f244b15bdace7b0c655f254ecd79de89428bcaaae7d8d8738a65b2dcea",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": true,
"testOnly": true
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 182 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 241 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.2 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 5.0 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 12 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 11.0067" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Wifi" fill-rule="evenodd" clip-rule="evenodd" d="M9.99304 8.66064C9.99127 8.57386 9.95373 8.49166 9.8893 8.43345C8.60864 7.35118 6.733 7.35118 5.45234 8.43345C5.38786 8.49162 5.35027 8.57379 5.34843 8.66057C5.34659 8.74735 5.38068 8.83104 5.44263 8.89188L7.44827 10.9139C7.50705 10.9733 7.5872 11.0067 7.67082 11.0067C7.75444 11.0067 7.83458 10.9733 7.89337 10.9139L9.89867 8.89188C9.96067 8.83108 9.99482 8.74742 9.99304 8.66064V8.66064ZM13.6438 4.67028C13.7652 4.78852 13.9593 4.78703 14.0789 4.66693L15.2395 3.4966C15.3 3.43569 15.3338 3.35318 15.3333 3.26733C15.3328 3.18148 15.2981 3.09937 15.2368 3.03917C11.0049 -1.01306 4.32858 -1.01306 0.0966678 3.03917C0.0353653 3.09933 0.00057401 3.18141 6.67831e-06 3.26726C-0.000545321 3.35311 0.0331419 3.43564 0.0936471 3.4966L1.25459 4.66693C1.3741 4.78721 1.56831 4.7887 1.68966 4.67028C3.30222 3.13859 5.44213 2.28447 7.66707 2.28448C9.89181 2.28457 12.0315 3.13869 13.6438 4.67028V4.67028ZM10.9782 7.36722C11.1007 7.48356 11.2938 7.48104 11.4132 7.36153L12.5725 6.1912C12.6336 6.12982 12.6674 6.04654 12.6665 5.96001C12.6657 5.87347 12.6301 5.7909 12.5678 5.73077C9.80866 3.16636 5.53574 3.16636 2.77658 5.73077C2.71425 5.7909 2.67869 5.87351 2.67787 5.96008C2.67706 6.04664 2.71106 6.12991 2.77223 6.1912L3.93117 7.36153C4.05063 7.48104 4.24368 7.48356 4.36623 7.36722C5.2717 6.54799 6.44949 6.09407 7.67102 6.09357C8.89337 6.09349 10.0721 6.54745 10.9782 7.36722V7.36722Z" fill="var(--fill-0, white)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

View File

@ -0,0 +1,681 @@
@font-face {
font-family: 'Source Han Sans SC Fami';
font-style: normal;
font-weight: 500;
font-display: swap;
src: url('./assets/fonts/source-han-sans-sc-medium-latin.woff2')
format('woff2');
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: #440b23;
color: #fff;
font-family:
'SF Pro Text', 'SF Pro Display', 'Noto Sans Arabic', Arial,
'Helvetica Neue', Helvetica, sans-serif;
}
button {
border: 0;
padding: 0;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
[hidden] {
display: none !important;
}
.fami-page {
display: flex;
justify-content: center;
width: 100%;
min-height: 100svh;
background: #440b23;
}
.fami-page[data-tab='brother'] {
background: #080636;
}
.fami-page[data-tab='sister'] {
background: #301008;
}
.fami-stage {
--design-scale: 1;
--design-height: 812px;
--stage-height: 812px;
--accent: #ff0979;
--accent-soft: #ff5762;
--tab-bg: rgba(94, 13, 35, 0.8);
--tab-glow: #ff7948;
--tab-jewel-left: -7px;
--avatar-top: 35px;
--center-height: 120px;
--exp-top: 102px;
--progress-from: #ff1e37;
--progress-mid: #fe6a30;
--progress-to: #ffcf67;
position: relative;
flex: 0 0 auto;
width: min(100vw, 414px);
height: var(--stage-height);
overflow: hidden;
background: #440b23;
}
.fami-page[data-tab='brother'] .fami-stage {
--accent: #5709ff;
--accent-soft: #5709ff;
--tab-bg: rgba(17, 13, 94, 0.8);
--tab-glow: #4872ff;
--tab-jewel-left: 110px;
--avatar-top: 41px;
--center-height: 136px;
--exp-top: 102px;
--progress-from: #471eff;
--progress-mid: #30bcfe;
--progress-to: #67acff;
background: #080636;
}
.fami-page[data-tab='sister'] .fami-stage {
--accent: #ff4a09;
--accent-soft: #ff4a09;
--tab-bg: rgba(94, 29, 13, 0.8);
--tab-glow: #ff7948;
--tab-jewel-left: 220px;
--avatar-top: 41px;
--center-height: 136px;
--exp-top: 93px;
--progress-from: #ff5e1e;
--progress-mid: #fe6a30;
--progress-to: #ffb167;
background: #301008;
}
.fami-design {
position: relative;
width: 375px;
height: var(--design-height);
transform: scale(var(--design-scale));
transform-origin: left top;
}
.fami-background,
.fami-card-skin,
.fami-tab-jewel,
.fami-center-art,
.fami-avatar-frame,
.fami-level-badge img,
.fami-status-bar img,
.fami-nav-button img {
display: block;
user-select: none;
-webkit-user-drag: none;
pointer-events: none;
}
.fami-background {
position: absolute;
z-index: 0;
inset: 0;
width: 375px;
height: 812px;
object-fit: fill;
object-position: center top;
}
.fami-status-bar {
position: absolute;
z-index: 5;
left: 0;
top: 0;
width: 375px;
height: 44px;
}
.fami-page.has-native-status .fami-status-bar {
visibility: hidden;
}
.fami-status-time {
position: absolute;
left: 33.87px;
top: 13px;
width: 54px;
height: 15px;
color: #fff;
font-family: Helvetica, Arial, sans-serif;
font-size: 15px;
font-weight: 400;
line-height: 15px;
text-align: center;
}
.fami-status-cellular {
position: absolute;
left: 294.34px;
top: 15px;
width: 17px;
height: 11.33px;
}
.fami-status-wifi {
position: absolute;
left: 316.34px;
top: 16.33px;
width: 15.33px;
height: 11.01px;
}
.fami-status-battery {
position: absolute;
left: 336.67px;
top: 16.33px;
width: 24.33px;
height: 11.33px;
}
.fami-nav {
position: absolute;
z-index: 5;
left: 0;
top: 44px;
width: 375px;
height: 44px;
}
.fami-nav h1 {
position: absolute;
left: 139px;
top: 13px;
width: 97px;
height: 18px;
margin: 0;
color: #fff;
font-family: 'Source Han Sans SC Fami', 'Noto Sans Arabic', sans-serif;
font-size: 18px;
font-weight: 500;
line-height: 18px;
text-align: center;
white-space: nowrap;
}
.fami-nav-button {
position: absolute;
top: 10px;
width: 24px;
height: 24px;
}
.fami-nav-button--back {
left: 16px;
}
.fami-nav-button--help {
left: 335px;
}
.fami-nav-button img {
width: 24px;
height: 24px;
object-fit: contain;
}
.fami-nav-button--help img {
width: 25.4px;
height: 25.4px;
transform: translate(-0.7px, -0.7px);
}
.fami-tabs {
position: absolute;
z-index: 5;
left: 16px;
top: 109px;
display: grid;
grid-template-columns: repeat(3, 1fr);
width: 343px;
height: 37px;
border: 0.5px solid #f4b747;
border-radius: 32px;
background: var(--tab-bg);
box-shadow: inset 0 0 5.5px var(--tab-glow);
direction: ltr;
}
.fami-tab-jewel {
position: absolute;
z-index: 0;
left: calc(var(--tab-jewel-left) - 0.5px);
top: -2.5px;
width: 130px;
height: 48px;
object-fit: fill;
}
.fami-tabs button {
position: relative;
z-index: 1;
height: 37px;
color: rgba(255, 255, 255, 0.8);
font-family: 'SF Pro Text', Arial, sans-serif;
font-size: 18px;
font-weight: 400;
line-height: 18px;
text-align: center;
}
.fami-tabs button[aria-selected='true'] {
color: #fff;
font-family: 'Source Han Sans SC Fami', 'Noto Sans Arabic', sans-serif;
font-weight: 500;
}
.fami-tabs button[data-tab='brother'][aria-selected='false'],
.fami-tabs button[data-tab='sister'][aria-selected='false'] {
letter-spacing: 0.5px;
}
.fami-tabs button[data-tab='brother'][aria-selected='false'] {
letter-spacing: 0.57px;
}
.fami-tabs button[data-tab='cp'][aria-selected='true'] {
letter-spacing: 0.35px;
}
.fami-tabs button[data-tab='brother'][aria-selected='true'] {
letter-spacing: -0.384px;
}
.fami-tabs button[data-tab='sister'][aria-selected='true'] {
letter-spacing: 0.042px;
}
.fami-tabs button span {
position: relative;
top: -1px;
}
.fami-tabs button[data-tab='cp'][aria-selected='true'] span {
left: -0.5px;
}
.fami-tabs button[data-tab='brother'] span,
.fami-tabs button[data-tab='sister'] span {
left: 1px;
}
.fami-content {
position: absolute;
z-index: 3;
left: 0;
top: 0;
width: 375px;
}
.fami-relation-list {
position: absolute;
left: 14px;
top: 169px;
display: flex;
flex-direction: column;
gap: 14px;
width: 347px;
}
.fami-relation-card {
position: relative;
flex: 0 0 199px;
width: 347px;
height: 199px;
color: var(--accent);
}
.fami-card-skin {
position: absolute;
z-index: 0;
inset: 0;
width: 347px;
height: 199px;
object-fit: fill;
}
.fami-avatar {
position: absolute;
z-index: 2;
top: var(--avatar-top);
width: 66px;
height: 66px;
margin: 0;
}
.fami-avatar--left {
left: 32px;
}
.fami-avatar--right {
left: 250px;
}
.fami-avatar-photo {
position: absolute;
z-index: 1;
left: 2.36px;
top: 2.36px;
display: grid;
place-items: center;
width: 61.29px;
height: 61.29px;
overflow: hidden;
border-radius: 50%;
background: rgba(255, 255, 255, 0.26);
color: rgba(255, 255, 255, 0.9);
font-size: 18px;
font-weight: 600;
}
.fami-avatar-photo img {
display: block;
width: 100%;
height: 100%;
object-fit: cover;
}
.fami-avatar-frame {
position: absolute;
z-index: 2;
inset: 0;
width: 66px;
height: 66px;
object-fit: contain;
}
.fami-center-art-wrap {
position: absolute;
z-index: 1;
left: 106px;
top: 6px;
width: 136px;
height: var(--center-height);
text-align: center;
}
.fami-center-art {
position: absolute;
z-index: 0;
inset: 0;
width: 136px;
height: var(--center-height);
object-fit: fill;
}
.fami-days-value {
position: absolute;
z-index: 1;
left: 50%;
top: 45px;
color: var(--accent);
font-size: 18px;
font-family: 'SF Pro Text', Arial, sans-serif;
font-weight: 400;
line-height: 18px;
text-shadow:
0 1px 0 #fff,
0 0 4px rgba(255, 255, 255, 0.75);
transform: translateX(-50%);
}
.fami-days-unit {
position: absolute;
z-index: 1;
left: 50%;
top: 70px;
color: var(--accent);
font-family: 'SF Pro Text', Arial, sans-serif;
font-size: 8px;
font-weight: 400;
line-height: 8px;
text-shadow: 0 1px 0 #fff;
transform: translateX(-50%);
}
.fami-exp-pill {
position: absolute;
z-index: 3;
left: 119px;
top: var(--exp-top);
display: grid;
place-items: center;
width: 112px;
height: 28px;
}
.fami-exp-pill-skin {
position: absolute;
z-index: 0;
inset: 0;
display: none;
width: 112px;
height: 28px;
pointer-events: none;
}
.fami-page[data-tab='cp'] .fami-exp-pill-skin--cp,
.fami-page[data-tab='brother'] .fami-exp-pill-skin--brother,
.fami-page[data-tab='sister'] .fami-exp-pill-skin--sister {
display: block;
}
.fami-exp-pill-skin--cp img {
position: absolute;
top: 0;
display: block;
height: 28px;
pointer-events: none;
}
.fami-exp-pill-skin--cp img:nth-child(1) {
left: 0;
width: 49px;
}
.fami-exp-pill-skin--cp img:nth-child(2) {
left: 49px;
width: 14px;
}
.fami-exp-pill-skin--cp img:nth-child(3) {
left: 63px;
width: 49px;
}
.fami-exp-pill [data-value='exp-away'] {
position: relative;
z-index: 1;
color: var(--accent);
font-family: 'SF Pro Text', Arial, sans-serif;
font-size: 8px;
font-weight: 400;
line-height: 8px;
text-align: center;
text-shadow:
0 1px 0 rgba(255, 255, 255, 0.95),
0 0 2px rgba(255, 255, 255, 0.85);
white-space: nowrap;
}
.fami-progress-block {
position: absolute;
z-index: 3;
left: 32px;
top: 134px;
width: 289px;
height: 35px;
}
.fami-level-badge {
position: absolute;
top: 1.5px;
width: 24px;
height: 13px;
}
.fami-level-badge--left {
left: 0;
}
.fami-level-badge--right {
right: 0;
}
.fami-level-badge img {
position: absolute;
inset: 0;
width: 24px;
height: 13px;
object-fit: fill;
}
.fami-level-badge span {
position: absolute;
inset: 0;
display: grid;
place-items: center;
color: #fff;
font-family: 'SF Pro Text', Arial, sans-serif;
font-size: 9px;
font-weight: 500;
line-height: 9px;
}
.fami-progress-track {
position: absolute;
left: 28px;
top: 0;
width: 233px;
height: 16px;
overflow: hidden;
border-radius: 74px;
background: rgba(255, 255, 255, 0.3);
backdrop-filter: blur(6px);
}
.fami-progress-fill {
position: absolute;
left: 1px;
top: 1px;
width: 1px;
height: 14px;
overflow: hidden;
border-radius: 7px;
background: linear-gradient(
90deg,
var(--progress-from) 0%,
var(--progress-mid) 50%,
var(--progress-to) 100%
);
box-shadow:
inset -4px 0 3px #fffef8,
inset 0 2px 2px rgba(255, 254, 239, 0.48),
inset 0 -2px 2px rgba(255, 255, 255, 0.45);
}
.fami-progress-fill::after {
position: absolute;
inset: 1px 2px;
border-radius: inherit;
background: radial-gradient(
circle,
rgba(255, 255, 255, 0.95) 0 1px,
transparent 2px
)
0 0 / 7px 7px;
content: '';
mix-blend-mode: screen;
opacity: 0.9;
}
.fami-progress-text {
position: absolute;
left: 50%;
top: 2px;
color: #fff;
font-size: 14px;
font-weight: 700;
line-height: 14px;
text-align: center;
white-space: nowrap;
transform: translateX(-50%);
}
.fami-time-text {
position: absolute;
left: 0;
top: 20px;
width: 289px;
margin: 0;
color: var(--accent-soft);
font-family: 'SF Pro Text', Arial, sans-serif;
font-size: 11px;
font-weight: 400;
line-height: 15px;
text-align: center;
text-shadow:
0 1px 0 rgba(255, 255, 255, 0.82),
0 0 2px rgba(255, 255, 255, 0.72);
}
.fami-state {
position: absolute;
left: 14px;
top: 169px;
display: grid;
place-items: center;
width: 347px;
min-height: 199px;
padding: 24px;
border: 1px solid rgba(255, 255, 255, 0.45);
border-radius: 12px;
background: rgba(20, 8, 35, 0.76);
color: #fff;
font-size: 14px;
line-height: 20px;
text-align: center;
}
.fami-state[data-retry='true'] {
cursor: pointer;
}
.fami-nav-button:focus-visible,
.fami-tabs button:focus-visible,
.fami-state:focus-visible {
outline: 2px solid var(--hy-theme-focus-ring, rgba(255, 255, 255, 0.75));
outline-offset: 2px;
}
[dir='rtl'] .fami-nav-button--back img {
transform: scaleX(-1);
}
@media (prefers-reduced-motion: reduce) {
* {
scroll-behavior: auto !important;
}
}

View File

@ -0,0 +1,673 @@
(function () {
'use strict';
var VALID_TABS = ['cp', 'brother', 'sister'];
var LAYER_BASE = './fami/assets/layers/';
var THEMES = {
cp: {
background: 'bg-cp.png',
tab: 'tab-cp.png',
card: 'card-cp.png',
center: 'center-cp.png',
level: 'level-cp.png',
},
brother: {
background: 'bg-brother.png',
tab: 'tab-brother.png',
card: 'card-brother.png',
center: 'center-brother.png',
level: 'level-brother.png',
},
sister: {
background: 'bg-sister.png',
tab: 'tab-sister.png',
card: 'card-sister.png',
center: 'center-sister.png',
level: 'level-sister.png',
},
};
var page = document.getElementById('famiPage');
var stage = document.getElementById('famiStage');
var design = document.getElementById('famiDesign');
var background = document.getElementById('famiBackground');
var tabJewel = document.getElementById('famiTabJewel');
var relationList = document.getElementById('famiRelationList');
var stateButton = document.getElementById('famiStateButton');
var content = document.getElementById('famiContent');
var template = document.getElementById('famiRelationTemplate');
var backButton = document.getElementById('famiBackButton');
var helpButton = document.getElementById('famiHelpButton');
var tabButtons = Array.prototype.slice.call(
document.querySelectorAll('.fami-tabs [data-tab]')
);
var params = new URLSearchParams(window.location.search || '');
var resizeObserver = null;
var state = {
activeTab: normalizeTab(params.get('tab')) || 'cp',
mock: params.get('mock') === '1',
tabs: {
cp: createTabState(),
brother: createTabState(),
sister: createTabState(),
},
};
function createTabState() {
return {
loading: false,
loaded: false,
error: '',
relationships: [],
requestId: 0,
};
}
function normalizeTab(value) {
var tab = String(value || '')
.trim()
.toLowerCase();
return VALID_TABS.indexOf(tab) >= 0 ? tab : '';
}
function readNumber(value, fallback) {
var number = Number(value);
return Number.isFinite(number) ? number : fallback || 0;
}
function readString(value) {
return String(value == null ? '' : value).trim();
}
function firstValue() {
for (var index = 0; index < arguments.length; index += 1) {
var value = arguments[index];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return undefined;
}
function hasValue(value) {
return value !== undefined && value !== null && value !== '';
}
function t(key, fallback) {
return window.HyAppI18n && window.HyAppI18n.t
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function formatNumber(value) {
var number = Math.max(0, Math.floor(readNumber(value, 0)));
return String(number).replace(/\B(?=(\d{3})+(?!\d))/g, ',');
}
function layer(fileName) {
return LAYER_BASE + fileName;
}
function setSource(node, fileName) {
if (!node) return;
var source = layer(fileName);
if (node.getAttribute('src') !== source) {
node.setAttribute('src', source);
}
}
function profileFromData(raw) {
var profile = raw || {};
return {
id: readString(profile.user_id || profile.userId || profile.id),
displayId: readString(
profile.display_user_id || profile.displayUserId
),
name: readString(
profile.username ||
profile.nickname ||
profile.display_name ||
profile.name
),
avatar: readString(
profile.avatar ||
profile.avatar_url ||
profile.avatarUrl ||
profile.avatar_path
),
};
}
function daysFromTimestamp(value) {
var formedAt = readNumber(value, 0);
if (formedAt <= 0) return 0;
if (formedAt < 100000000000) formedAt *= 1000;
if (Date.now() <= formedAt) return 1;
return Math.floor((Date.now() - formedAt) / 86400000) + 1;
}
function normalizeLevel(value) {
var level = Math.floor(readNumber(value, 1));
return Math.max(1, Math.min(99, level));
}
function normalizeRelationship(raw, tab) {
var item = raw || {};
var level = normalizeLevel(item.level || item.current_level);
var intimacy = Math.max(
0,
readNumber(item.intimacy_value || item.intimacyValue, 0)
);
var currentThreshold = Math.max(
0,
readNumber(
item.current_level_threshold || item.currentLevelThreshold,
0
)
);
var nextThreshold = Math.max(
currentThreshold,
readNumber(item.next_level_threshold || item.nextLevelThreshold, 0)
);
var currentExp = hasValue(firstValue(item.current_exp, item.currentExp))
? Math.max(
0,
readNumber(firstValue(item.current_exp, item.currentExp))
)
: Math.max(0, intimacy - currentThreshold);
var nextExp = hasValue(firstValue(item.next_exp, item.nextExp))
? Math.max(0, readNumber(firstValue(item.next_exp, item.nextExp)))
: Math.max(0, nextThreshold - currentThreshold);
if (!nextExp && intimacy > 0) nextExp = intimacy;
var neededExp = hasValue(
firstValue(item.needed_for_next_level, item.neededExp)
)
? Math.max(
0,
readNumber(
firstValue(item.needed_for_next_level, item.neededExp)
)
)
: Math.max(0, nextExp - currentExp);
var explicitProgress = firstValue(
item.level_progress_percent,
item.progress_percent,
item.progressPercent
);
var progressPercent = hasValue(explicitProgress)
? readNumber(explicitProgress, 0)
: nextExp > 0
? (currentExp / nextExp) * 100
: item.max_level
? 100
: 0;
var days = Math.max(
0,
readNumber(
firstValue(
item.days,
item.relationship_days,
daysFromTimestamp(item.formed_at_ms || item.formedAtMS)
),
0
)
);
return {
id: readString(
item.relationship_id || item.relationshipId || item.id
),
relationType:
normalizeTab(item.relation_type || item.relationType) || tab,
status: readString(item.status || 'active').toLowerCase(),
level: level,
nextLevel: normalizeLevel(
firstValue(item.next_level, item.nextLevel, level + 1)
),
days: days,
togetherDays: Math.max(
0,
readNumber(
firstValue(
item.together_days,
item.togetherDays,
item.time_spent_days,
days
),
days
)
),
currentExp: currentExp,
nextExp: nextExp,
neededExp: neededExp,
progressPercent: Math.max(0, Math.min(100, progressPercent)),
maxLevel: Boolean(item.max_level || item.maxLevel),
me: profileFromData(item.me || item.me_profile),
partner: profileFromData(
item.partner || item.partner_profile || item.cp_user
),
};
}
function activeRelationships(payload, tab) {
var data = payload || {};
var rows = Array.isArray(data)
? data
: data.relationships || data.items || [];
return (Array.isArray(rows) ? rows : [])
.map(function (item) {
return normalizeRelationship(item, tab);
})
.filter(function (item) {
return (
item.relationType === tab &&
(!item.status || item.status === 'active')
);
});
}
function mockRelationship(tab) {
return normalizeRelationship(
{
relationship_id: 'figma-' + tab,
relation_type: tab,
status: 'active',
level: 1,
next_level: 2,
days: 999,
together_days: 4,
current_exp: 1000,
next_exp: 200000,
needed_for_next_level: 888,
level_progress_percent: 22.3175965665,
me: {
user_id: 'figma-left',
username: 'Fami',
avatar: layer('mock-avatar.png'),
},
partner: {
user_id: 'figma-right',
username: 'Fami',
avatar: layer('mock-avatar.png'),
},
},
tab
);
}
function renderAvatar(node, profile) {
if (!node) return;
node.replaceChildren();
var data = profile || {};
if (data.avatar) {
var image = document.createElement('img');
image.src = data.avatar;
image.alt = data.name || '';
image.addEventListener('error', function () {
image.remove();
node.textContent = (data.name || data.displayId || '?')
.charAt(0)
.toUpperCase();
});
node.appendChild(image);
return;
}
node.textContent = (data.name || data.displayId || '?')
.charAt(0)
.toUpperCase();
}
function renderRelationship(item, tab) {
var theme = THEMES[tab];
var fragment = template.content.cloneNode(true);
var card = fragment.querySelector('.fami-relation-card');
var levelText = t('cpSpace.levelValue', 'Lv.{level}').replace(
'{level}',
formatNumber(item.level)
);
var nextLevel = item.maxLevel ? item.level : item.nextLevel;
var expAwayText = item.maxLevel
? t('cpSpace.maxLevelExp', '{exp} EXP').replace(
'{exp}',
formatNumber(item.currentExp)
)
: t('cpSpace.expAway', '{exp} Exp away from Lv. {level}')
.replace('{exp}', formatNumber(item.neededExp))
.replace('{level}', formatNumber(nextLevel));
setSource(card.querySelector('[data-layer="card"]'), theme.card);
setSource(card.querySelector('[data-layer="center"]'), theme.center);
card.querySelectorAll('[data-layer="level"]').forEach(function (node) {
setSource(node, theme.level);
});
renderAvatar(card.querySelector('[data-avatar="me"]'), item.me);
renderAvatar(
card.querySelector('[data-avatar="partner"]'),
item.partner
);
card.querySelector('[data-value="days"]').textContent = formatNumber(
item.days
);
card.querySelector('[data-value="exp-away"]').textContent = expAwayText;
// 两侧徽标在 Figma 中表示这段关系当前等级,不是“当前/下一等级”;下一等级只出现在经验提示里。
card.querySelector('[data-value="level-left"]').textContent = levelText;
card.querySelector('[data-value="level-right"]').textContent =
levelText;
card.querySelector('[data-value="progress-text"]').textContent =
String(Math.max(0, Math.floor(item.currentExp))) +
'/' +
String(Math.max(0, Math.floor(item.nextExp)));
card.querySelector('[data-value="progress"]').style.width =
Math.max(1, Math.min(233, (item.progressPercent / 100) * 233)) +
'px';
card.querySelector('[data-value="time"]').textContent = t(
'cpSpace.timeTogether',
'Time spent together: {days} days'
).replace('{days}', formatNumber(item.togetherDays));
card.setAttribute(
'aria-label',
t('cpSpace.levelCard', 'Best friend level {level}').replace(
'{level}',
formatNumber(item.level)
)
);
return fragment;
}
function renderTheme() {
var tab = state.activeTab;
var theme = THEMES[tab];
page.setAttribute('data-tab', tab);
setSource(background, theme.background);
setSource(tabJewel, theme.tab);
tabButtons.forEach(function (button) {
var selected = button.getAttribute('data-tab') === tab;
button.setAttribute('aria-selected', String(selected));
button.tabIndex = selected ? 0 : -1;
if (selected) content.setAttribute('aria-labelledby', button.id);
});
}
function setStateMessage(message, retry) {
stateButton.hidden = !message;
stateButton.textContent = message || '';
stateButton.setAttribute('data-retry', String(Boolean(retry)));
stateButton.disabled = !retry;
}
function updateDesignHeight(cardCount) {
var count = Math.max(1, Number(cardCount || 0));
var designHeight = Math.max(
812,
169 + count * 199 + (count - 1) * 14 + 40
);
design.style.setProperty('--design-height', designHeight + 'px');
stage.style.setProperty('--design-height', designHeight + 'px');
syncScale();
}
function render() {
renderTheme();
var tabState = state.tabs[state.activeTab];
content.setAttribute('aria-busy', String(tabState.loading));
relationList.replaceChildren();
if (tabState.loading && !tabState.loaded) {
setStateMessage(t('cpSpace.loading', 'Loading...'), false);
updateDesignHeight(0);
return;
}
if (tabState.error) {
setStateMessage(
tabState.error ||
t('cpSpace.loadFailed', 'Load failed. Tap to retry.'),
true
);
updateDesignHeight(0);
return;
}
if (!tabState.relationships.length) {
setStateMessage(
t('cpSpace.empty', 'No relationship data yet.'),
false
);
updateDesignHeight(0);
return;
}
setStateMessage('', false);
tabState.relationships.forEach(function (item) {
relationList.appendChild(renderRelationship(item, state.activeTab));
});
updateDesignHeight(tabState.relationships.length);
}
function syncScale() {
if (!stage) return;
var width = stage.getBoundingClientRect().width || window.innerWidth;
var scale = Math.max(0.1, width / 375);
var designHeight = parseFloat(
stage.style.getPropertyValue('--design-height') || '812'
);
stage.style.setProperty('--design-scale', String(scale));
stage.style.setProperty('--stage-height', designHeight * scale + 'px');
}
function bindScale() {
syncScale();
if (window.ResizeObserver) {
resizeObserver = new ResizeObserver(syncScale);
resizeObserver.observe(stage);
return;
}
window.addEventListener('resize', syncScale);
window.addEventListener('orientationchange', syncScale);
}
function syncURL(tab) {
if (!window.history || !window.history.replaceState) return;
var url = new URL(window.location.href);
url.searchParams.set('tab', tab);
// tab 只属于本页视觉和查询状态replaceState 避免每次切换都污染 App WebView 的返回栈。
window.history.replaceState(null, '', url.toString());
}
function loadTab(tab, force) {
var target = normalizeTab(tab) || 'cp';
var tabState = state.tabs[target];
if (!force && (tabState.loading || tabState.loaded)) {
render();
return Promise.resolve(tabState.relationships);
}
var requestId = ++tabState.requestId;
tabState.loading = true;
tabState.error = '';
render();
if (state.mock) {
tabState.relationships = [mockRelationship(target)];
tabState.loaded = true;
tabState.loading = false;
render();
return Promise.resolve(tabState.relationships);
}
if (
!window.HyAppAPI ||
!window.HyAppAPI.cpSpace ||
!window.HyAppAPI.cpSpace.relationships
) {
tabState.loading = false;
tabState.error = t(
'cpSpace.loadFailed',
'Load failed. Tap to retry.'
);
render();
return Promise.reject(new Error('cp_space_api_unavailable'));
}
// 关系类型和分页值原样交给 gateway页面不猜测用户关系也不把一个 tab 的结果复制到另一个 tab。
return window.HyAppAPI.cpSpace
.relationships({
relation_type: target,
page: 1,
page_size: target === 'cp' ? 1 : 20,
})
.then(function (payload) {
if (requestId !== tabState.requestId) return [];
tabState.relationships = activeRelationships(payload, target);
tabState.loaded = true;
return tabState.relationships;
})
.catch(function (error) {
if (requestId !== tabState.requestId) return [];
tabState.error =
(error && error.message) ||
t('cpSpace.loadFailed', 'Load failed. Tap to retry.');
return [];
})
.then(function (rows) {
if (requestId !== tabState.requestId) return rows;
tabState.loading = false;
render();
return rows;
});
}
function setActiveTab(tab, options) {
var target = normalizeTab(tab) || 'cp';
state.activeTab = target;
render();
if (!options || options.syncURL !== false) syncURL(target);
return loadTab(target, Boolean(options && options.force));
}
function hasNativeBridge() {
return Boolean(
(window.flutter_inappwebview &&
window.flutter_inappwebview.callHandler) ||
(window.HyAppBridgeChannel &&
window.HyAppBridgeChannel.postMessage) ||
(window.FlutterBridge && window.FlutterBridge.postMessage) ||
(window.webkit &&
window.webkit.messageHandlers &&
window.webkit.messageHandlers.HyAppBridge)
);
}
function contextualURL(path) {
var url = new URL(path, window.location.href);
['token', 'access_token', 'env', 'language', 'lang', 'locale'].forEach(
function (key) {
if (params.has(key)) url.searchParams.set(key, params.get(key));
}
);
url.searchParams.set('app_code', 'fami');
return url.toString();
}
function applyExternalData(payload) {
var data = payload || {};
VALID_TABS.forEach(function (tab) {
if (!data[tab]) return;
state.tabs[tab].relationships = activeRelationships(
Array.isArray(data[tab])
? { relationships: data[tab] }
: data[tab],
tab
);
state.tabs[tab].loaded = true;
state.tabs[tab].loading = false;
state.tabs[tab].error = '';
});
if (normalizeTab(data.activeTab)) state.activeTab = data.activeTab;
render();
}
function bindEvents() {
tabButtons.forEach(function (button) {
button.addEventListener('click', function () {
setActiveTab(button.getAttribute('data-tab'));
});
button.addEventListener('keydown', function (event) {
var current = VALID_TABS.indexOf(
button.getAttribute('data-tab')
);
var next = current;
if (event.key === 'ArrowLeft') next = current - 1;
else if (event.key === 'ArrowRight') next = current + 1;
else if (event.key === 'Home') next = 0;
else if (event.key === 'End') next = VALID_TABS.length - 1;
else return;
event.preventDefault();
// 标签视觉顺序固定为从左到右键盘同样按可见顺序循环RTL 只翻转返回箭头而不倒置关系类型。
next = (next + VALID_TABS.length) % VALID_TABS.length;
var targetButton = tabButtons[next];
targetButton.focus();
setActiveTab(targetButton.getAttribute('data-tab'));
});
});
stateButton.addEventListener('click', function () {
if (stateButton.getAttribute('data-retry') === 'true') {
loadTab(state.activeTab, true);
}
});
backButton.addEventListener('click', function () {
if (
window.FlutterPageControl &&
window.FlutterPageControl.postMessage
) {
window.FlutterPageControl.postMessage('close_page');
return;
}
if (hasNativeBridge() && window.HyAppBridge) {
window.HyAppBridge.back();
return;
}
window.history.back();
});
helpButton.addEventListener('click', function () {
if (hasNativeBridge() && window.HyAppBridge) {
window.HyAppBridge.post('cpSpaceHelpTap', {
page: 'fami-cp-space',
relation_type: state.activeTab,
});
return;
}
window.location.href = contextualURL('../../cp-rules/');
});
window.addEventListener('hyapp:i18n-ready', render);
window.addEventListener('hyapp:cp-space-fami-data', function (event) {
applyExternalData(event.detail || {});
});
window.addEventListener('hyapp:cp-space-data', function (event) {
applyExternalData(event.detail || {});
});
}
if (hasNativeBridge() && params.get('figma_status') !== '1') {
page.classList.add('has-native-status');
}
bindScale();
bindEvents();
if (window.HyAppCPSpaceFamiData) {
applyExternalData(window.HyAppCPSpaceFamiData);
setActiveTab(state.activeTab, { syncURL: false });
} else {
setActiveTab(state.activeTab, { syncURL: false });
}
if (window.HyAppBridge && window.HyAppBridge.ready) {
window.HyAppBridge.ready({
page: 'fami-cp-space',
app_code: 'fami',
tabs: VALID_TABS.slice(),
mock: state.mock,
});
}
})();

View File

@ -0,0 +1,8 @@
(function () {
'use strict';
// common/api.js 在未显式传 app_code 时读取页面默认值;必须在 API 脚本执行前声明,避免 Fami 页面误落到 Lalu 租户。
window.HyAppDefaultAppCode = 'fami';
window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
document.documentElement.setAttribute('data-hy-app-code', 'fami');
})();

205
activity/cp/huwaa.html Normal file
View File

@ -0,0 +1,205 @@
<!doctype html>
<html lang="en" dir="ltr" data-hy-app-code="huwaa">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, viewport-fit=cover"
/>
<title data-i18n="cp.pageTitle">Last Weekly CP</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="../../common/theme.css?v=20260717" />
<link rel="stylesheet" href="./huwaa/huwaa.css?v=20260717" />
</head>
<body>
<div class="huwaa-cp-viewport" id="huwaaCpViewport">
<main class="huwaa-cp-page" id="huwaaCpPage" data-tab="rank">
<div class="huwaa-cp-stage-wrap" id="huwaaCpStageWrap">
<section
class="huwaa-cp-stage"
id="huwaaCpStage"
aria-label="Last Weekly CP activity"
data-i18n-aria="cp.pageLabel"
>
<img
class="huwaa-cp-background"
id="huwaaCpBackground"
src="./huwaa/assets/layers/bg-texture.webp"
alt=""
aria-hidden="true"
draggable="false"
/>
<header class="huwaa-cp-hero">
<img
src="./huwaa/assets/layers/hero-art-text-free.webp"
alt=""
aria-hidden="true"
draggable="false"
/>
<h1
class="huwaa-cp-wordmark"
data-i18n="cp.pageTitle"
>
Last Weekly CP
</h1>
</header>
<section
class="huwaa-cp-countdown"
aria-label="Activity countdown"
data-i18n-aria="cp.countdownLabel"
>
<div class="huwaa-cp-time-unit">
<img
src="./huwaa/assets/layers/countdown-heart.webp"
alt=""
aria-hidden="true"
/>
<strong data-count="days">00</strong>
<span data-i18n="cp.days">D</span>
</div>
<div class="huwaa-cp-time-unit">
<img
src="./huwaa/assets/layers/countdown-heart.webp"
alt=""
aria-hidden="true"
/>
<strong data-count="hours">00</strong>
<span data-i18n="cp.hours">H</span>
</div>
<div class="huwaa-cp-time-unit">
<img
src="./huwaa/assets/layers/countdown-heart.webp"
alt=""
aria-hidden="true"
/>
<strong data-count="minutes">00</strong>
<span data-i18n="cp.minutes">M</span>
</div>
<div class="huwaa-cp-time-unit">
<img
src="./huwaa/assets/layers/countdown-heart.webp"
alt=""
aria-hidden="true"
/>
<strong data-count="seconds">00</strong>
<span data-i18n="cp.seconds">S</span>
</div>
</section>
<nav
class="huwaa-cp-tabs"
role="tablist"
aria-label="CP activity tabs"
data-i18n-aria="cp.tabsLabel"
>
<button
class="huwaa-cp-tab"
id="huwaaCpTabPlaza"
type="button"
role="tab"
aria-selected="false"
aria-controls="huwaaCpTabContent"
tabindex="-1"
data-tab="plaza"
>
<img
src="./huwaa/assets/layers/tab-inactive.webp"
alt=""
aria-hidden="true"
/>
<span data-i18n="cp.tabPlaza">CP Plaza</span>
</button>
<button
class="huwaa-cp-tab is-active"
id="huwaaCpTabRank"
type="button"
role="tab"
aria-selected="true"
aria-controls="huwaaCpTabContent"
data-tab="rank"
>
<img
src="./huwaa/assets/layers/tab-active.webp"
alt=""
aria-hidden="true"
/>
<span data-i18n="cp.tabRank">CP Rank</span>
</button>
<button
class="huwaa-cp-tab"
id="huwaaCpTabRewards"
type="button"
role="tab"
aria-selected="false"
aria-controls="huwaaCpTabContent"
tabindex="-1"
data-tab="rewards"
>
<img
src="./huwaa/assets/layers/tab-inactive.webp"
alt=""
aria-hidden="true"
/>
<span data-i18n="cp.tabRewards">Rewards</span>
</button>
</nav>
<section
class="huwaa-cp-tab-content"
id="huwaaCpTabContent"
role="tabpanel"
aria-labelledby="huwaaCpTabRank"
></section>
<footer
class="huwaa-cp-copyright"
data-i18n="cp.copyright"
>
The copyright of this activity belongs to Shila
</footer>
<p
class="huwaa-cp-sr-only"
id="huwaaCpAnnouncement"
role="status"
aria-live="polite"
></p>
</section>
</div>
</main>
</div>
<script>
window.HyAppI18nSupported = ['en', 'ar', 'tr', 'es'];
window.HyAppDefaultAppCode = 'huwaa';
(function enforceHuwaaAppCode() {
try {
var entryURL = new URL(window.location.href);
var aliases = ['app', 'app_code', 'appCode'];
var needsReplace = aliases.some(function (key) {
return entryURL.searchParams.get(key) !== 'huwaa';
});
if (needsReplace) {
// common/api.js 与 common/params.js 为兼容旧入口会按不同顺序读取三个别名;
// 页面在公共脚本执行前统一它们,避免 UI 主题身份与 X-App-Code 请求租户分裂。
aliases.forEach(function (key) {
entryURL.searchParams.set(key, 'huwaa');
});
window.history.replaceState(
null,
'',
entryURL.toString()
);
}
} catch (_) {
// 部分旧 WebView 禁止修改 file:// history默认 app code 仍会让请求层发送 Huwaa 身份。
}
})();
</script>
<script src="../../common/i18n.js?v=20260717"></script>
<script src="../../common/api.js?v=20260717"></script>
<script src="../../common/params.js?v=20260717"></script>
<script src="./huwaa/huwaa.js?v=20260717"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 695 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 348 KiB

View File

@ -0,0 +1,424 @@
{
"schemaVersion": 1,
"figma": {
"fileKey": "OTOg5cwbhVNWxkImGmzTJl",
"canvas": {
"id": "543:1998",
"name": "Huwaa运营"
},
"entryFrames": {
"plaza": "831:3320",
"rank": "831:4048",
"rewards": "831:3913"
},
"designSizes": {
"plaza": {
"width": 1080,
"height": 4283
},
"rank": {
"width": 1080,
"height": 3733
},
"rewards": {
"width": 1080,
"height": 4782
}
},
"forbiddenAggregateNodes": [
"543:1998",
"823:3005",
"831:3320",
"831:4048",
"831:3913",
"831:3323",
"831:3326",
"831:3339",
"831:3349",
"831:3642",
"831:4381",
"831:3942"
]
},
"assets": [
{
"file": "bg-texture.webp",
"sourceNode": "831:3321",
"sourceName": "花瓣素材_情人节白色情人节七夕520梦幻浪漫感粉丝爱心i底纹背景元素素材_193406578 1",
"sourceType": "RECTANGLE",
"purpose": "Pure petal texture positioned at x=-487, y=489 over the independent #e98c9b stage fill with Figma opacity 0.5 and screen blend mode",
"bounds": {
"width": 2415,
"height": 4293
},
"original": {
"width": 1242,
"height": 2208
},
"exportFormat": "webp",
"bytes": 711912,
"sha256": "e0c9a81e2bb893731f8c70cf25f322693da52fe12e8a48513d4d7de8bd6d182c",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false,
"derivedFromSha256": "9b13664bd6b77c64075da4891a95acef6116cc2055f3c9d9fda794493a8b6ede",
"transform": "Figma raw image fill encoded as WebP quality 94; runtime CSS preserves the source node bounds, opacity and screen blend over the frame fill"
},
{
"file": "hero-art-text-free.webp",
"sourceNode": "831:3325",
"sourceName": "c7ffd4a5-20dc-41c9-93f6-7721e460aa70 2",
"sourceType": "RECTANGLE",
"purpose": "Text-free event illustration behind the independent localized DOM wordmark; countdown, tabs, state and business data remain separate DOM layers",
"bounds": {
"width": 1080,
"height": 1333
},
"original": {
"width": 1080,
"height": 1333
},
"exportFormat": "webp",
"bytes": 356714,
"sha256": "33620a230108ca0559062de12263356349c26b0b18ffbb863e5966ea23d6e194",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false,
"derivedFromSha256": "ad52de31140cb9d72415f86b570c44aff121b0000e1aef88470b28472c1c4137",
"transform": "Localized inpainting was limited to the embedded wordmark region; before WebP encoding, normalized source pixels above y=830 and below y=1275 were preserved, while the title was moved to independent localized DOM text",
"preEncodingPixelExactCoverage": 0.6981
},
{
"file": "countdown-heart.webp",
"sourceNode": "831:3327",
"sourceName": "ffutuft 3",
"sourceType": "RECTANGLE",
"purpose": "Empty countdown heart skin behind live DOM values",
"bounds": {
"width": 163,
"height": 106
},
"original": {
"width": 163,
"height": 106
},
"exportFormat": "webp-lossless",
"bytes": 20946,
"sha256": "d0a83f2b9695c2d15330d25203947dad171c9f03afbfb9d053e444765f0220fb",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "tab-active.webp",
"sourceNode": "831:3347",
"sourceName": "图层 0 34",
"sourceType": "RECTANGLE",
"purpose": "Active tab skin without its DOM label or interaction target",
"bounds": {
"width": 303,
"height": 126
},
"original": {
"width": 303,
"height": 126
},
"exportFormat": "webp-lossless",
"bytes": 40482,
"sha256": "dcf5231b062cfc09529493285ee42e35b7f3b0e1c1295b5fd8432573cdf9a811",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "tab-inactive.webp",
"sourceNode": "831:3344",
"sourceName": "图层 0 34",
"sourceType": "RECTANGLE",
"purpose": "Inactive tab skin without its DOM label or interaction target",
"bounds": {
"width": 303,
"height": 126
},
"original": {
"width": 303,
"height": 126
},
"exportFormat": "webp-lossless",
"bytes": 37228,
"sha256": "6aa5d9166eac1894fd64f014004a58afbb0a0930aa3967562cb82fb829315e91",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "panel-skin.webp",
"sourceNode": "831:3350",
"sourceName": "想吃vxcvxvdsfsdf 1",
"sourceType": "RECTANGLE",
"purpose": "Reusable empty pink-and-gold panel skin rendered with CSS nine-slicing",
"bounds": {
"width": 960,
"height": 671
},
"original": {
"width": 1254,
"height": 1254
},
"exportFormat": "webp-lossless",
"bytes": 865498,
"sha256": "b3011ae288d78d5f92a5e8d610eb889088798ab9061b739b403ad95e2ce1e5b4",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "gift-plane.webp",
"sourceNode": "831:3568",
"sourceName": "06黄金小飞机缩略图 (1) 1",
"sourceType": "RECTANGLE",
"purpose": "Decorative gift icon between CP partners in the plaza list",
"bounds": {
"width": 152,
"height": 152
},
"original": {
"width": 152,
"height": 152
},
"exportFormat": "webp-lossless",
"bytes": 40268,
"sha256": "880f57ca597bd1625700aaf3869fba1daeaefdf921c0807ff6a242695bc2e714",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "deco-line-right.webp",
"sourceNode": "831:3583",
"sourceName": "520 (431) 1",
"sourceType": "RECTANGLE",
"purpose": "Right-side heartbeat line between the CP avatar and center icon",
"bounds": {
"width": 159,
"height": 30
},
"original": {
"width": 159,
"height": 30
},
"exportFormat": "webp-lossless",
"bytes": 1600,
"sha256": "f2ecdcfa3a07d3fe9ac5b19de4d9de9c3aa802093a62f8e8227ce6a79333b23d",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "deco-line-left.webp",
"sourceNode": "831:3584",
"sourceName": "520 (431) 2",
"sourceType": "RECTANGLE",
"purpose": "Left-side heartbeat line between the CP avatar and center icon",
"bounds": {
"width": 159,
"height": 30
},
"original": {
"width": 159,
"height": 30
},
"exportFormat": "webp-lossless",
"bytes": 1612,
"sha256": "5e8afa2c2ab1b6117961bc9ae1162ff50213eb4be2b034293eb7421c69b246b6",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "score-heart.webp",
"sourceNode": "831:3849",
"sourceName": "ffutuft 2",
"sourceType": "RECTANGLE",
"purpose": "Empty score heart behind live DOM rank values",
"bounds": {
"width": 220,
"height": 143
},
"original": {
"width": 220,
"height": 143
},
"exportFormat": "webp-lossless",
"bytes": 35024,
"sha256": "4e9052c29e375553b9fecf77dbba8bfaab51b94fba30c00b500b31c776a1496b",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "podium-gold.webp",
"sourceNode": "831:4421",
"sourceName": "图层 0 31",
"sourceType": "RECTANGLE",
"purpose": "First-place double-avatar podium frame without avatars or names",
"bounds": {
"width": 716,
"height": 434
},
"original": {
"width": 716,
"height": 434
},
"exportFormat": "webp-lossless",
"bytes": 278480,
"sha256": "01cab5779b58c65256b25d2dd7784a73d777c5b9a2f75d4dc22df0ea062e3a5d",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "podium-silver.webp",
"sourceNode": "831:4426",
"sourceName": "04144 1",
"sourceType": "RECTANGLE",
"purpose": "Second-place double-avatar podium frame without avatars or names",
"bounds": {
"width": 500,
"height": 284
},
"original": {
"width": 500,
"height": 284
},
"exportFormat": "webp-lossless",
"bytes": 125178,
"sha256": "13dd006d1840d1fd0ed5545446bd2a493293a3a64e8b333b9b88e3e95cbb3e57",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "podium-bronze.webp",
"sourceNode": "831:4437",
"sourceName": "轻微我去恶 1",
"sourceType": "RECTANGLE",
"purpose": "Third-place double-avatar podium frame without avatars or names",
"bounds": {
"width": 500,
"height": 286
},
"original": {
"width": 500,
"height": 286
},
"exportFormat": "webp-lossless",
"bytes": 137328,
"sha256": "060b52606840c41717664c9dc5b5721d3f69c10305d79b2f935d1f20353e65a8",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "reward-frame.webp",
"sourceNode": "831:3950",
"sourceName": "8d9ca1c38fb3b780ff6b5d6d3cd318b8f108e7b83f3cd-59aVD7 4",
"sourceType": "RECTANGLE",
"purpose": "Empty reward wreath placed around live reward cover images",
"bounds": {
"width": 200,
"height": 200
},
"original": {
"width": 200,
"height": 200
},
"exportFormat": "webp-lossless",
"bytes": 39558,
"sha256": "e26e9f171b44296471cd9fd0c84fed4f0835c30f54e7d48435a18162aa62bfe7",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "crown-gold.webp",
"sourceNode": "831:3946",
"sourceName": "冠军 1",
"sourceType": "RECTANGLE",
"purpose": "Gold reward-tier crown",
"bounds": {
"width": 162,
"height": 144
},
"original": {
"width": 162,
"height": 144
},
"exportFormat": "webp-lossless",
"bytes": 24612,
"sha256": "b9c26c70a5a2f22273639bcd68c4af63cd87bc104cc93676b5f6feaaf7eacaaa",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "crown-silver.webp",
"sourceNode": "831:3981",
"sourceName": "冠军 2",
"sourceType": "RECTANGLE",
"purpose": "Silver reward-tier crown",
"bounds": {
"width": 162,
"height": 144
},
"original": {
"width": 162,
"height": 144
},
"exportFormat": "webp-lossless",
"bytes": 23164,
"sha256": "21a95c439ca10aa615ab1b03db6f3c23f68fd8b22ddbd1690d723baf27be53f3",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "crown-bronze.webp",
"sourceNode": "831:4016",
"sourceName": "冠军 2",
"sourceType": "RECTANGLE",
"purpose": "Bronze reward-tier crown",
"bounds": {
"width": 162,
"height": 144
},
"original": {
"width": 162,
"height": 144
},
"exportFormat": "webp-lossless",
"bytes": 25124,
"sha256": "5dbba6acd0eb8f92ac9278b9720a3aba8fcaef97716a26d8db47b68ee4201dca",
"containsText": false,
"containsDynamicText": false,
"containsControls": false,
"containsDynamicMedia": false
}
]
}

Binary file not shown.

After

Width:  |  Height:  |  Size: 845 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 272 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 122 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 39 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 34 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 40 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

832
activity/cp/huwaa/huwaa.css Normal file
View File

@ -0,0 +1,832 @@
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--huwaa-cp-page-width: 375px;
--huwaa-cp-design-scale: 0.3472222222;
--huwaa-cp-brand: var(--hy-theme-primary-strong, #c52d45);
--huwaa-cp-brand-deep: #a1152f;
--huwaa-cp-text: #a11626;
--huwaa-cp-muted: #dd708d;
--huwaa-cp-row: rgba(197, 45, 69, 0.13);
--huwaa-cp-page-bg: var(--hy-theme-bg, #f8c7d5);
}
html,
body {
width: 100%;
min-height: 100%;
margin: 0;
overflow-x: hidden;
scrollbar-width: none;
background: var(--huwaa-cp-page-bg);
color: var(--hy-theme-text, #21182b);
font-family:
Inter, Arial, 'Helvetica Neue', Helvetica, 'Noto Sans', sans-serif;
}
html::-webkit-scrollbar,
body::-webkit-scrollbar {
display: none;
}
button {
padding: 0;
border: 0;
outline: none;
background: transparent;
color: inherit;
font: inherit;
cursor: pointer;
}
img {
display: block;
}
.huwaa-cp-sr-only {
position: absolute !important;
width: 1px !important;
height: 1px !important;
padding: 0 !important;
margin: -1px !important;
overflow: hidden !important;
clip: rect(0, 0, 0, 0) !important;
white-space: nowrap !important;
border: 0 !important;
}
.huwaa-cp-viewport,
.huwaa-cp-page,
.huwaa-cp-stage-wrap {
position: relative;
width: var(--huwaa-cp-page-width);
max-width: 768px;
min-height: 1297px;
margin: 0 auto;
overflow: hidden;
background: var(--huwaa-cp-page-bg);
}
.huwaa-cp-stage-wrap {
min-height: 0;
}
.huwaa-cp-stage {
position: absolute;
top: 0;
left: 0;
width: 1080px;
min-height: 3733px;
overflow: hidden;
direction: ltr;
transform: scale(var(--huwaa-cp-design-scale));
transform-origin: top left;
background: #e98c9b;
}
.huwaa-cp-stage > * {
position: absolute;
}
.huwaa-cp-background {
top: 489px;
left: -487px;
z-index: 0;
width: 2415px;
height: 4293px;
opacity: 0.5;
mix-blend-mode: screen;
object-fit: cover;
pointer-events: none;
}
.huwaa-cp-hero {
top: 0;
left: 0;
z-index: 1;
width: 1080px;
height: 1333px;
overflow: hidden;
pointer-events: none;
}
.huwaa-cp-hero > img {
width: 1080px;
height: 1333px;
object-fit: cover;
-webkit-mask-image: linear-gradient(
to bottom,
#000 0,
#000 88.93%,
transparent 100%
);
mask-image: linear-gradient(
to bottom,
#000 0,
#000 88.93%,
transparent 100%
);
}
.huwaa-cp-wordmark {
position: absolute;
top: 856px;
left: 20px;
z-index: 2;
display: grid;
width: 1040px;
min-height: 370px;
margin: 0;
place-items: center;
color: #ffe0b2;
font-family: Georgia, 'Times New Roman', serif;
font-size: 181px;
font-style: italic;
font-weight: 800;
letter-spacing: -8px;
line-height: 0.82;
overflow-wrap: normal;
paint-order: stroke fill;
text-align: center;
text-shadow:
0 8px 0 #7f1d17,
0 13px 22px rgba(91, 10, 25, 0.66),
0 0 12px rgba(255, 248, 212, 0.78);
text-wrap: balance;
-webkit-text-stroke: 7px #b75f31;
unicode-bidi: plaintext;
}
.huwaa-cp-countdown {
top: 1333px;
left: 52px;
z-index: 3;
width: 974px;
height: 106px;
}
.huwaa-cp-time-unit {
position: absolute;
top: 0;
width: 223px;
height: 106px;
}
.huwaa-cp-time-unit:nth-child(1) {
left: 0;
}
.huwaa-cp-time-unit:nth-child(2) {
left: 250px;
}
.huwaa-cp-time-unit:nth-child(3) {
left: 501px;
}
.huwaa-cp-time-unit:nth-child(4) {
left: 756px;
}
.huwaa-cp-time-unit img {
position: absolute;
top: 0;
left: 0;
width: 163px;
height: 106px;
}
.huwaa-cp-time-unit strong {
position: absolute;
top: 25px;
left: 0;
z-index: 1;
width: 163px;
color: #fff;
font-size: 40px;
font-weight: 900;
line-height: 40px;
text-align: center;
text-shadow:
0 2px 0 rgba(176, 47, 78, 0.52),
0 0 12px rgba(255, 255, 255, 0.78);
}
.huwaa-cp-time-unit span {
position: absolute;
top: 25px;
left: 194px;
width: 31px;
color: #fff;
font-size: 40px;
font-weight: 900;
line-height: 40px;
text-align: center;
text-shadow: 0 2px 0 rgba(176, 47, 78, 0.52);
}
.huwaa-cp-tabs {
top: 1495px;
left: 39px;
z-index: 5;
display: grid;
width: 1000px;
height: 126px;
grid-template-columns: repeat(3, 303px);
column-gap: 46px;
}
.huwaa-cp-tab {
position: relative;
width: 303px;
height: 126px;
color: rgba(161, 22, 52, 0.34);
font-size: 40px;
font-weight: 800;
line-height: 40px;
text-align: center;
text-shadow: 0 2px 1px rgba(255, 255, 255, 0.78);
}
.huwaa-cp-tab img {
position: absolute;
inset: 0;
width: 303px;
height: 126px;
pointer-events: none;
}
.huwaa-cp-tab span {
position: absolute;
top: 34px;
left: 20px;
width: 263px;
overflow: hidden;
white-space: nowrap;
text-overflow: ellipsis;
unicode-bidi: plaintext;
}
html[lang='es'] .huwaa-cp-tab[data-tab='rewards'] {
font-size: 32px;
}
.huwaa-cp-tab::after {
position: absolute;
inset: -12px 0;
content: '';
}
.huwaa-cp-tab.is-active {
color: #ff4766;
text-shadow:
0 2px 0 rgba(255, 255, 255, 0.9),
0 0 10px rgba(255, 139, 166, 0.36);
}
.huwaa-cp-tab:focus-visible {
border-radius: 58px;
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.78);
}
.huwaa-cp-tab-content {
top: 0;
left: 0;
z-index: 4;
width: 1080px;
}
.huwaa-cp-copyright {
left: 114px;
z-index: 4;
width: 850px;
height: 30px;
color: #fff;
font-size: 24px;
font-weight: 600;
line-height: 30px;
text-align: center;
text-shadow: 0 2px 8px rgba(163, 48, 76, 0.3);
}
.huwaa-cp-panel {
position: absolute;
left: 60px;
width: 960px;
border: 84px solid transparent;
border-image-source: url('./assets/layers/panel-skin.webp');
border-image-slice: 110 fill;
border-image-width: 84px;
border-image-repeat: stretch;
color: var(--huwaa-cp-text);
}
.huwaa-cp-panel-body {
position: absolute;
inset: -84px;
}
.huwaa-cp-panel-title,
.huwaa-cp-panel-subtitle {
position: absolute;
left: 55px;
width: 850px;
margin: 0;
text-align: center;
unicode-bidi: plaintext;
}
.huwaa-cp-panel-title {
top: 75px;
min-height: 45px;
color: #b82e2d;
font-size: 32px;
font-weight: 800;
line-height: 36px;
}
.huwaa-cp-panel-subtitle {
top: 140px;
min-height: 27px;
color: var(--huwaa-cp-muted);
font-size: 24px;
font-weight: 600;
line-height: 30px;
}
.huwaa-cp-panel.is-gift-panel .huwaa-cp-panel-title {
top: 75px;
line-height: 36px;
}
.huwaa-cp-panel.is-gift-panel .huwaa-cp-panel-subtitle {
top: 166px;
}
.huwaa-cp-list {
position: absolute;
top: 203px;
left: 60px;
display: grid;
width: 840px;
gap: 24px;
}
.huwaa-cp-panel.is-gift-panel .huwaa-cp-list {
top: 229px;
}
.huwaa-cp-row {
position: relative;
width: 840px;
height: 199px;
overflow: hidden;
border-radius: 30px;
background: var(--huwaa-cp-row);
}
.huwaa-cp-avatar {
position: absolute;
z-index: 2;
display: grid;
width: 113px;
height: 113px;
place-items: center;
overflow: hidden;
border: 5px solid rgba(255, 235, 207, 0.96);
border-radius: 50%;
background: #f2b9c6;
box-shadow:
0 0 0 3px rgba(183, 81, 64, 0.48),
0 6px 14px rgba(143, 25, 52, 0.18);
color: #fff;
font-size: 34px;
font-weight: 800;
}
.huwaa-cp-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.huwaa-cp-row .huwaa-cp-avatar.is-left {
top: 26px;
left: 58px;
}
.huwaa-cp-row .huwaa-cp-avatar.is-right {
top: 26px;
left: 669px;
}
.huwaa-cp-name {
position: absolute;
top: 147px;
z-index: 3;
width: 144px;
overflow: hidden;
color: var(--huwaa-cp-text);
font-size: 26px;
font-weight: 700;
line-height: 30px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
unicode-bidi: plaintext;
}
.huwaa-cp-name.is-left {
left: 42px;
}
.huwaa-cp-name.is-right {
left: 653px;
}
.huwaa-cp-deco-line {
position: absolute;
top: 71px;
z-index: 1;
width: 159px;
height: 30px;
}
.huwaa-cp-deco-line.is-left {
left: 336px;
}
.huwaa-cp-deco-line.is-right {
left: 504px;
}
.huwaa-cp-gift {
position: absolute;
top: 15px;
left: 344px;
z-index: 2;
width: 152px;
height: 152px;
object-fit: contain;
}
.huwaa-cp-score-heart {
position: absolute;
top: 28px;
left: 308px;
z-index: 2;
width: 220px;
height: 143px;
}
.huwaa-cp-score {
position: absolute;
top: 71px;
left: 359px;
z-index: 3;
width: 120px;
overflow: hidden;
color: #ff4460;
font-size: 30px;
font-weight: 800;
line-height: 36px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.huwaa-cp-state {
position: absolute;
top: 50%;
left: 110px;
width: 740px;
transform: translateY(-50%);
color: var(--huwaa-cp-muted);
font-size: 30px;
font-weight: 700;
line-height: 42px;
text-align: center;
unicode-bidi: plaintext;
}
.huwaa-cp-state.is-loading::before {
display: inline-block;
width: 20px;
height: 20px;
margin-right: 14px;
border: 4px solid rgba(197, 45, 69, 0.2);
border-top-color: var(--huwaa-cp-brand);
border-radius: 50%;
content: '';
animation: huwaa-cp-spin 0.8s linear infinite;
vertical-align: -2px;
}
.huwaa-cp-retry {
position: relative;
display: block;
min-width: 200px;
height: 64px;
margin: 22px auto 0;
padding: 0 28px;
border-radius: 32px;
background: var(--hy-theme-button, #a868dc);
color: #fff;
font-size: 26px;
font-weight: 800;
}
.huwaa-cp-retry::after {
position: absolute;
inset: -43px -20px;
content: '';
}
.huwaa-cp-retry:focus-visible {
box-shadow: 0 0 0 8px rgba(255, 255, 255, 0.82);
}
.huwaa-cp-rank-podium {
position: absolute;
top: 1677px;
left: 25px;
width: 1030px;
height: 751px;
}
.huwaa-cp-podium-art,
.huwaa-cp-podium-avatar,
.huwaa-cp-podium-name {
position: absolute;
}
.huwaa-cp-podium-art {
z-index: 2;
pointer-events: none;
}
.huwaa-cp-podium-avatar {
z-index: 1;
display: grid;
place-items: center;
overflow: hidden;
border-radius: 50%;
background: #efb2c1;
color: #fff;
font-weight: 900;
text-align: center;
}
.huwaa-cp-podium-avatar img {
width: 100%;
height: 100%;
object-fit: cover;
}
.huwaa-cp-podium-name {
z-index: 3;
overflow: hidden;
color: #fff;
font-weight: 700;
text-align: center;
text-overflow: ellipsis;
text-shadow: 0 2px 4px rgba(144, 33, 59, 0.36);
white-space: nowrap;
unicode-bidi: plaintext;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-art {
top: 0;
left: 157px;
width: 716px;
height: 434px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar {
top: 119px;
width: 223px;
height: 223px;
font-size: 54px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar.is-left {
left: 236px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-avatar.is-right {
left: 557px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-name {
top: 386px;
width: 315px;
height: 50px;
background: rgba(188, 30, 65, 0.26);
font-size: 35px;
line-height: 49px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-name.is-left {
left: 219px;
}
.huwaa-cp-podium.is-gold .huwaa-cp-podium-name.is-right {
left: 497px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-art,
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-art {
top: 467px;
width: 500px;
height: 284px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-art {
left: 0;
}
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-art {
top: 465px;
left: 530px;
height: 286px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar,
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar {
top: 550px;
width: 153px;
height: 153px;
font-size: 40px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar.is-left {
left: 62px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-avatar.is-right {
left: 281px;
}
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar.is-left {
left: 598px;
}
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-avatar.is-right {
left: 812px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-name,
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name {
top: 716px;
width: 172px;
height: 34px;
background: rgba(188, 30, 65, 0.22);
font-size: 24px;
line-height: 34px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-name.is-left {
left: 94px;
}
.huwaa-cp-podium.is-silver .huwaa-cp-podium-name.is-right {
left: 281px;
}
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name.is-left {
left: 624px;
}
.huwaa-cp-podium.is-bronze .huwaa-cp-podium-name.is-right {
left: 811px;
}
.huwaa-cp-rank-empty {
position: absolute;
top: 1990px;
left: 170px;
z-index: 4;
width: 740px;
color: #fff;
font-size: 30px;
font-weight: 800;
line-height: 42px;
text-align: center;
text-shadow: 0 2px 8px rgba(161, 21, 47, 0.42);
}
.huwaa-cp-reward-panel .huwaa-cp-panel-body {
overflow: hidden;
}
.huwaa-cp-reward-crown {
position: absolute;
top: 75px;
left: 399px;
width: 162px;
height: 144px;
object-fit: contain;
}
.huwaa-cp-reward-grid {
position: absolute;
top: 260px;
left: 81px;
display: grid;
width: 800px;
height: 604px;
overflow-x: hidden;
overflow-y: auto;
grid-template-columns: repeat(3, 200px);
grid-auto-rows: 272px;
column-gap: 100px;
row-gap: 60px;
scrollbar-width: none;
}
.huwaa-cp-reward-grid::-webkit-scrollbar {
display: none;
}
.huwaa-cp-reward-card {
position: relative;
width: 200px;
height: 272px;
color: var(--huwaa-cp-text);
text-align: center;
}
.huwaa-cp-reward-icon {
position: relative;
width: 200px;
height: 200px;
overflow: hidden;
border-radius: 26px;
background: rgba(197, 45, 69, 0.11);
}
.huwaa-cp-reward-frame,
.huwaa-cp-reward-cover {
position: absolute;
object-fit: contain;
}
.huwaa-cp-reward-frame {
inset: 0;
z-index: 2;
width: 200px;
height: 200px;
}
.huwaa-cp-reward-cover {
top: 48px;
left: 48px;
z-index: 1;
width: 104px;
height: 104px;
border-radius: 18px;
}
.huwaa-cp-reward-name {
display: -webkit-box;
max-height: 60px;
min-height: 36px;
margin-top: 19px;
overflow-wrap: anywhere;
font-size: 24px;
font-weight: 700;
line-height: 30px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
unicode-bidi: plaintext;
}
.huwaa-cp-reward-note {
display: -webkit-box;
max-height: 44px;
margin-top: -2px;
color: var(--huwaa-cp-muted);
font-size: 20px;
font-weight: 600;
line-height: 22px;
-webkit-box-orient: vertical;
-webkit-line-clamp: 2;
overflow: hidden;
unicode-bidi: plaintext;
}
@keyframes huwaa-cp-spin {
to {
transform: rotate(1turn);
}
}
@media (prefers-reduced-motion: reduce) {
.huwaa-cp-state.is-loading::before {
animation: none;
}
}

964
activity/cp/huwaa/huwaa.js Normal file
View File

@ -0,0 +1,964 @@
(function () {
'use strict';
var DESIGN_WIDTH = 1080;
var MAX_PAGE_WIDTH = 768;
var ASSET_BASE = './huwaa/assets/layers/';
var PODIUM_ASSETS = {
gold: 'podium-gold.webp',
silver: 'podium-silver.webp',
bronze: 'podium-bronze.webp',
};
var CROWN_ASSETS = {
1: 'crown-gold.webp',
2: 'crown-silver.webp',
3: 'crown-bronze.webp',
};
var STAGE_HEIGHTS = {
plaza: 4283,
rank: 3733,
rewards: 4782,
};
var PANEL_LAYOUT = {
plazaFirstTop: 1677,
plazaFirstHeight: 1168,
plazaSecondTop: 2904,
plazaSecondHeight: 1142,
rankPanelTop: 2484,
rankPanelHeight: 1142,
rewardFirstTop: 1677,
rewardPanelHeight: 960,
rewardPanelGap: 56,
};
var viewport = document.getElementById('huwaaCpViewport');
var page = document.getElementById('huwaaCpPage');
var stage = document.getElementById('huwaaCpStage');
var stageWrap = document.getElementById('huwaaCpStageWrap');
var tabContent = document.getElementById('huwaaCpTabContent');
var copyright = document.querySelector('.huwaa-cp-copyright');
var announcement = document.getElementById('huwaaCpAnnouncement');
var tabs = Array.prototype.slice.call(
document.querySelectorAll('.huwaa-cp-tab')
);
var requestGeneration = 0;
var countdownEndAt = 0;
var countdownTimer = 0;
var serverTimeOffsetMS = 0;
var activeTab = resolveInitialTab();
var activityData = {
statusState: 'loading',
plazaState: 'loading',
rank: [],
previousRows: [],
plazaRows: [],
rewards: [],
enabled: true,
};
function t(key, fallback) {
if (window.HyAppI18n && window.HyAppI18n.t) {
return window.HyAppI18n.t(key, fallback);
}
return fallback || key;
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function safeImageURL(value) {
var source = String(value || '').trim();
if (!source) return '';
if (/^(https?:|data:image\/|blob:|\.\.?\/)/i.test(source)) {
return escapeHTML(source);
}
return '';
}
function resolveInitialTab() {
try {
var requested = new URLSearchParams(window.location.search).get(
'tab'
);
if (['plaza', 'rank', 'rewards'].indexOf(requested) >= 0) {
return requested;
}
} catch (_) {
// 无 URLSearchParams 的旧 WebView 使用设计稿默认的 CP Rank不影响核心接口加载。
}
return 'rank';
}
function isLocalRuntime() {
try {
var params = new URLSearchParams(window.location.search);
var hostname = String(window.location.hostname || '').toLowerCase();
return (
hostname === 'localhost' ||
hostname === '127.0.0.1' ||
hostname === '::1' ||
params.get('env') === 'local'
);
} catch (_) {
return false;
}
}
function isExplicitMockMode() {
try {
return (
isLocalRuntime() &&
new URLSearchParams(window.location.search).get('mock') === '1'
);
} catch (_) {
return false;
}
}
function twoDigits(value) {
return String(Math.max(0, Number(value) || 0)).padStart(2, '0');
}
function resolveExplicitEndAt() {
if (!isLocalRuntime()) return 0;
try {
var params = new URLSearchParams(window.location.search);
var parsed = Date.parse(
params.get('end_at') || params.get('endAt') || ''
);
return Number.isFinite(parsed) && parsed > Date.now() ? parsed : 0;
} catch (_) {
return 0;
}
}
function nowMS() {
return Date.now() + serverTimeOffsetMS;
}
function getTimeParts() {
var diff =
countdownEndAt > 0 ? Math.max(0, countdownEndAt - nowMS()) : 0;
var totalSeconds = Math.floor(diff / 1000);
return {
days: Math.floor(totalSeconds / 86400),
hours: Math.floor((totalSeconds % 86400) / 3600),
minutes: Math.floor((totalSeconds % 3600) / 60),
seconds: totalSeconds % 60,
};
}
function updateCountdown() {
var parts = getTimeParts();
Object.keys(parts).forEach(function (key) {
document
.querySelectorAll('[data-count="' + key + '"]')
.forEach(function (node) {
node.textContent = twoDigits(parts[key]);
});
});
}
function initials(name) {
var normalized = String(name || '').trim();
return normalized ? normalized.charAt(0).toUpperCase() : 'CP';
}
function renderAvatar(className, avatar, name) {
var imageURL = safeImageURL(avatar);
return [
'<span class="huwaa-cp-avatar ',
className,
'" aria-hidden="true">',
imageURL
? '<img src="' + imageURL + '" alt="" loading="lazy" />'
: '<span>' + escapeHTML(initials(name)) + '</span>',
'</span>',
].join('');
}
function renderPodiumAvatar(className, avatar, name) {
var imageURL = safeImageURL(avatar);
return [
'<span class="huwaa-cp-podium-avatar ',
className,
'" aria-hidden="true">',
imageURL
? '<img src="' + imageURL + '" alt="" loading="lazy" />'
: '<span>' + escapeHTML(initials(name)) + '</span>',
'</span>',
].join('');
}
function renderState(state, retryScope) {
var key =
state === 'error'
? 'cp.error'
: state === 'loading'
? 'cp.loading'
: state === 'disabled'
? 'cp.disabled'
: state === 'unavailable'
? 'cp.unavailable'
: 'cp.empty';
var fallback =
state === 'error'
? 'Unable to load activity data.'
: state === 'loading'
? 'Loading...'
: state === 'disabled'
? 'This activity is not available.'
: state === 'unavailable'
? 'No data source is available yet.'
: 'No data yet.';
return [
'<div class="huwaa-cp-state ',
state === 'loading' ? 'is-loading' : '',
'" role="status" aria-live="polite">',
'<span>',
escapeHTML(t(key, fallback)),
'</span>',
retryScope
? '<button class="huwaa-cp-retry" type="button" data-retry="' +
escapeHTML(retryScope) +
'">' +
escapeHTML(t('cp.retry', 'Retry')) +
'</button>'
: '',
'</div>',
].join('');
}
function renderPanel(options) {
var classes = ['huwaa-cp-panel'];
if (options.className) classes.push(options.className);
return [
'<section class="',
classes.join(' '),
'"',
options.ariaLabel
? ' aria-label="' + escapeHTML(options.ariaLabel) + '"'
: '',
' style="top:',
options.top,
'px;height:',
options.height,
'px">',
'<div class="huwaa-cp-panel-body">',
options.body,
'</div>',
'</section>',
].join('');
}
function renderPanelHeader(
titleKey,
titleFallback,
subtitleKey,
subtitleFallback
) {
return [
'<h2 class="huwaa-cp-panel-title">',
escapeHTML(t(titleKey, titleFallback)),
'</h2>',
subtitleKey
? '<p class="huwaa-cp-panel-subtitle">' +
escapeHTML(t(subtitleKey, subtitleFallback)) +
'</p>'
: '',
].join('');
}
function renderCPRow(row) {
var center =
row.type === 'gift'
? '<img class="huwaa-cp-gift" src="' +
ASSET_BASE +
'gift-plane.webp" alt="" aria-hidden="true" />'
: [
'<img class="huwaa-cp-score-heart" src="',
ASSET_BASE,
'score-heart.webp" alt="" aria-hidden="true" />',
'<strong class="huwaa-cp-score">',
escapeHTML(row.score),
'</strong>',
].join('');
return [
'<article class="huwaa-cp-row">',
renderAvatar('is-left', row.leftAvatar, row.leftName),
renderAvatar('is-right', row.rightAvatar, row.rightName),
'<span class="huwaa-cp-name is-left">',
escapeHTML(row.leftName),
'</span>',
'<span class="huwaa-cp-name is-right">',
escapeHTML(row.rightName),
'</span>',
'<img class="huwaa-cp-deco-line is-left" src="',
ASSET_BASE,
'deco-line-left.webp" alt="" aria-hidden="true" />',
'<img class="huwaa-cp-deco-line is-right" src="',
ASSET_BASE,
'deco-line-right.webp" alt="" aria-hidden="true" />',
center,
'</article>',
].join('');
}
function rowsOrState(rows, state, retryScope) {
if (rows.length) {
return (
'<div class="huwaa-cp-list">' +
rows.map(renderCPRow).join('') +
'</div>'
);
}
return renderState(
state === 'ready' ? 'empty' : state,
state === 'error' ? retryScope : ''
);
}
function renderPreviousPanel(top, height) {
var body =
renderPanelHeader(
'cp.previousWeekTopTitle',
'Previous Top 3',
'cp.previousWeekTopSubtitle',
'(Top CPs from the previous completed week.)'
) +
rowsOrState(
activityData.previousRows,
activityData.statusState,
'status'
);
return renderPanel({ top: top, height: height, body: body });
}
function renderPlaza() {
var firstBody =
renderPanelHeader(
'cp.huwaaPlazaGiftTitle',
'Gifts exchanged between CPs ≥ 5000 gold will be displayed here',
'cp.plazaGiftSubtitle',
'(Display the last 10 records, sorted in ascending order.)'
) +
rowsOrState(activityData.plazaRows, activityData.plazaState, '');
return [
renderPanel({
top: PANEL_LAYOUT.plazaFirstTop,
height: PANEL_LAYOUT.plazaFirstHeight,
className: 'is-gift-panel',
body: firstBody,
}),
renderPreviousPanel(
PANEL_LAYOUT.plazaSecondTop,
PANEL_LAYOUT.plazaSecondHeight
),
].join('');
}
function renderPodiumPair(item, place) {
if (!item) return '';
var level = place === 1 ? 'gold' : place === 2 ? 'silver' : 'bronze';
var accessibleLabel = [
t('cp.rankPlace', 'Rank'),
String(place),
item.leftName,
item.rightName,
].join(' ');
return [
'<article class="huwaa-cp-podium is-',
level,
'" aria-label="',
escapeHTML(accessibleLabel),
'">',
renderPodiumAvatar('is-left', item.leftAvatar, item.leftName),
renderPodiumAvatar('is-right', item.rightAvatar, item.rightName),
'<img class="huwaa-cp-podium-art" src="',
ASSET_BASE,
PODIUM_ASSETS[level],
'" alt="" aria-hidden="true" />',
'<span class="huwaa-cp-podium-name is-left">',
escapeHTML(item.leftName),
'</span>',
'<span class="huwaa-cp-podium-name is-right">',
escapeHTML(item.rightName),
'</span>',
'</article>',
].join('');
}
function renderRank() {
var podium = '';
if (activityData.rank.length) {
var rankAt = function (place) {
return activityData.rank.find(function (item) {
return item.place === place;
});
};
podium = [
'<section class="huwaa-cp-rank-podium" aria-label="',
escapeHTML(t('cp.rankPodiumLabel', 'Top CP ranking')),
'">',
renderPodiumPair(rankAt(1), 1),
renderPodiumPair(rankAt(2), 2),
renderPodiumPair(rankAt(3), 3),
'</section>',
].join('');
} else {
var rankStateKey =
activityData.statusState === 'error'
? 'cp.error'
: activityData.statusState === 'loading'
? 'cp.loading'
: activityData.statusState === 'disabled'
? 'cp.disabled'
: 'cp.empty';
var rankStateFallback =
activityData.statusState === 'error'
? 'Unable to load activity data.'
: activityData.statusState === 'loading'
? 'Loading...'
: activityData.statusState === 'disabled'
? 'This activity is not available.'
: 'No data yet.';
podium = [
'<p class="huwaa-cp-rank-empty" role="status" aria-live="polite">',
escapeHTML(t(rankStateKey, rankStateFallback)),
'</p>',
].join('');
}
return (
podium +
renderPreviousPanel(
PANEL_LAYOUT.rankPanelTop,
PANEL_LAYOUT.rankPanelHeight
)
);
}
function rewardName(item) {
var resourceName =
item.resourceName ||
item.name ||
item.resource_name ||
localizedResourceType(item.resource_type || item.resourceType) ||
t('cp.reward', 'Reward');
var duration = Number(item.duration_days || item.durationDays || 0);
var quantity = Number(item.quantity || 0);
if (duration > 0) {
return (
resourceName + '*' + duration + ' ' + t('cp.daysLong', 'days')
);
}
if (quantity > 0) return resourceName + '*' + quantity;
return resourceName;
}
function localizedResourceType(value) {
var type = String(value || '')
.trim()
.toLowerCase();
var keys = {
avatar_frame: ['cp.rewardAvatarFrame', 'Avatar frame'],
profile_card: ['cp.rewardProfileCard', 'Profile card'],
coin: ['cp.rewardCoins', 'Coins'],
vehicle: ['cp.rewardVehicle', 'Vehicle'],
chat_bubble: ['cp.rewardChatBubble', 'Chat bubble'],
badge: ['cp.rewardBadge', 'Badge'],
floating_screen: ['cp.rewardFloatingScreen', 'Floating screen'],
gift: ['cp.rewardGift', 'Gift'],
mic_seat_icon: ['cp.rewardMicSeatIcon', 'Mic seat icon'],
mic_seat_animation: [
'cp.rewardMicSeatAnimation',
'Mic seat animation',
],
emoji_pack: ['cp.rewardEmojiPack', 'Emoji pack'],
vip_trial_card: ['cp.rewardVipTrialCard', 'VIP trial card'],
};
return keys[type] ? t(keys[type][0], keys[type][1]) : '';
}
function rewardNote(item) {
return String(item.description || item.note || '').trim();
}
function renderRewardItem(item) {
var cover = safeImageURL(item.coverUrl || item.cover_url);
return [
'<article class="huwaa-cp-reward-card">',
'<div class="huwaa-cp-reward-icon">',
cover
? '<img class="huwaa-cp-reward-cover" src="' +
cover +
'" alt="" loading="lazy" />'
: '',
'<img class="huwaa-cp-reward-frame" src="',
ASSET_BASE,
'reward-frame.webp" alt="" aria-hidden="true" />',
'</div>',
'<div class="huwaa-cp-reward-name">',
escapeHTML(item.displayName || rewardName(item)),
'</div>',
item.note || rewardNote(item)
? '<div class="huwaa-cp-reward-note">' +
escapeHTML(item.note || rewardNote(item)) +
'</div>'
: '',
'</article>',
].join('');
}
function renderRewardPanel(section, index) {
var tierNo = Number(section.rankNo || index + 1);
if (tierNo < 1 || tierNo > 3) tierNo = index + 1;
var top =
PANEL_LAYOUT.rewardFirstTop +
index *
(PANEL_LAYOUT.rewardPanelHeight + PANEL_LAYOUT.rewardPanelGap);
var items = Array.isArray(section.items) ? section.items : [];
var tierLabels = {
1: t('cp.rewardTierGold', 'First-place rewards'),
2: t('cp.rewardTierSilver', 'Second-place rewards'),
3: t('cp.rewardTierBronze', 'Third-place rewards'),
};
var body = [
'<img class="huwaa-cp-reward-crown" src="',
ASSET_BASE,
CROWN_ASSETS[tierNo] || CROWN_ASSETS[3],
'" alt="" aria-hidden="true" />',
items.length
? '<div class="huwaa-cp-reward-grid">' +
items.map(renderRewardItem).join('') +
'</div>'
: renderState('empty', ''),
].join('');
return renderPanel({
top: top,
height: PANEL_LAYOUT.rewardPanelHeight,
className: 'huwaa-cp-reward-panel',
ariaLabel: tierLabels[tierNo],
body: body,
});
}
function renderRewards() {
if (!activityData.rewards.length) {
var state =
activityData.statusState === 'ready'
? 'empty'
: activityData.statusState;
return renderPanel({
top: PANEL_LAYOUT.rewardFirstTop,
height: PANEL_LAYOUT.rewardPanelHeight,
className: 'huwaa-cp-reward-panel',
body: renderState(state, state === 'error' ? 'status' : ''),
});
}
return [1, 2, 3]
.map(function (rankNo) {
return (
activityData.rewards.find(function (section) {
return section.rankNo === rankNo;
}) || { rankNo: rankNo, items: [] }
);
})
.map(renderRewardPanel)
.join('');
}
function formatScore(value) {
var number = Number(value || 0);
if (!Number.isFinite(number)) return '0';
try {
return number.toLocaleString(document.documentElement.lang || 'en');
} catch (_) {
return String(number);
}
}
function normalizePairUser(user) {
var source = user || {};
var userID = source.user_id || source.userId || '';
var displayID = source.display_user_id || source.displayUserId || '';
var name = source.username || source.name || displayID || userID || '';
if (!name && !source.avatar) return null;
return {
name: name || 'CP',
avatar: source.avatar || source.avatar_url || '',
};
}
function normalizeRankEntry(entry, fallbackPlace) {
var source = entry || {};
var userA = normalizePairUser(source.user_a || source.userA);
var userB = normalizePairUser(source.user_b || source.userB);
if (!userA || !userB) return null;
return {
type: 'score',
place: Number(source.rank || fallbackPlace || 0),
score: formatScore(source.score || source.intimacy_value || 0),
leftName: userA.name,
rightName: userB.name,
leftAvatar: userA.avatar,
rightAvatar: userB.avatar,
};
}
function normalizeRewardSection(section, fallbackRankNo) {
var source = section || {};
var group = source.resource_group || source.resourceGroup || {};
var groupItems = Array.isArray(group.items) ? group.items : [];
var items = Array.isArray(source.items) ? source.items : [];
return {
rankNo: Number(
source.rank_no || source.rankNo || fallbackRankNo || 0
),
items: items.map(function (item, index) {
var groupItem = groupItems[index] || {};
var resource = groupItem.resource || {};
return {
resourceName: String(resource.name || '').trim(),
resource_type:
item.resource_type ||
item.resourceType ||
resource.resource_type ||
resource.resourceType ||
'',
duration_days: item.duration_days || item.durationDays || 0,
quantity: item.quantity || groupItem.quantity || 0,
coverUrl:
item.cover_url ||
item.coverUrl ||
resource.preview_url ||
resource.previewUrl ||
resource.asset_url ||
resource.assetUrl ||
'',
};
}),
};
}
function applyActivityStatus(status) {
var source = status || {};
var serverTimeMS = Number(
source.server_time_ms || source.serverTimeMS || 0
);
if (serverTimeMS > 0) {
serverTimeOffsetMS = serverTimeMS - Date.now();
}
activityData.enabled = source.enabled !== false;
if (!activityData.enabled) {
countdownEndAt = 0;
activityData.rank = [];
activityData.previousRows = [];
activityData.rewards = [];
activityData.statusState = 'disabled';
return;
}
var endMS = Number(
(source.period && (source.period.end_ms || source.period.endMS)) ||
source.end_ms ||
source.endMS ||
0
);
countdownEndAt = endMS > nowMS() ? endMS : 0;
activityData.rank = Array.isArray(source.entries)
? source.entries
.map(function (entry, index) {
return normalizeRankEntry(entry, index + 1);
})
.filter(Boolean)
.sort(function (left, right) {
return left.place - right.place;
})
.slice(0, 3)
: [];
activityData.previousRows = Array.isArray(source.previous_entries)
? source.previous_entries
.map(function (entry, index) {
return normalizeRankEntry(entry, index + 1);
})
.filter(Boolean)
.slice(0, 3)
: [];
activityData.rewards = Array.isArray(source.rewards)
? source.rewards
.map(function (section, index) {
return normalizeRewardSection(section, index + 1);
})
.filter(function (section) {
return section.rankNo >= 1 && section.rankNo <= 3;
})
.sort(function (left, right) {
return left.rankNo - right.rankNo;
})
: [];
activityData.statusState = 'ready';
}
function applyMockData() {
var pairs = [
['Avery', 'Noor', 126580],
['Mina', 'Elif', 118420],
['Lina', 'Sofia', 109730],
['Dara', 'Maya', 98760],
];
function pair(row, index, type) {
return {
type: type || 'score',
place: index + 1,
score: formatScore(row[2]),
rawScore: row[2],
leftName: row[0],
rightName: row[1],
leftAvatar: '',
rightAvatar: '',
};
}
activityData.rank = pairs.slice(0, 3).map(function (row, index) {
return pair(row, index, 'score');
});
activityData.previousRows = pairs
.slice(0, 3)
.map(function (row, index) {
return pair(row, index, 'score');
});
activityData.plazaRows = pairs.map(function (row, index) {
return pair(row, index, 'gift');
});
activityData.rewards = ['gold', 'silver', 'bronze'].map(
function (_, tier) {
return {
rankNo: tier + 1,
items: [
{
displayName: t('cp.rewardMedal7', 'Medal*7 days'),
note:
tier === 0
? t(
'cp.rewardMedalNote',
'(Permanent medal after getting 3 times)'
)
: '',
},
{
displayName: t(
'cp.rewardHonorBanner7',
'Honor banner*7 days'
),
},
{
displayName: t(
'cp.rewardHeadwear7',
'Headwear*7days'
),
},
{ displayName: t('cp.rewardMount7', 'Mount*7days') },
{
displayName: t(
'cp.rewardCoins20000',
'Coins*20000'
),
},
{ displayName: t('cp.rewardDuke7', 'Duke*7days') },
],
};
}
);
activityData.statusState = 'ready';
activityData.plazaState = 'ready';
activityData.enabled = true;
serverTimeOffsetMS = 0;
countdownEndAt = Date.now() + 2 * 86400000 + 2 * 3600000 + 2 * 60000;
}
function loadData(scope) {
var generation = ++requestGeneration;
if (isExplicitMockMode()) {
applyMockData();
render();
return Promise.resolve();
}
if (!scope || scope === 'status') {
activityData.statusState = 'loading';
activityData.rank = [];
activityData.previousRows = [];
activityData.rewards = [];
}
// 设计稿的 Plaza 是“单笔 CP 送礼事件流”;当前公开 intimacy-leaderboard 是累计亲密榜,
// 两者排序和业务含义不同。在后端事件 feed 上线前保持明确不可用态,绝不把累计榜伪装成送礼记录。
activityData.plazaState = 'unavailable';
activityData.plazaRows = [];
render();
var statusRequest =
window.HyAppAPI && window.HyAppAPI.cpWeeklyRank
? window.HyAppAPI.cpWeeklyRank.status({ limit: 3 })
: Promise.reject(new Error('cp_weekly_rank_api_unavailable'));
// allSettled 统一承接未来可并行加入的 Plaza 事件 feedgeneration 防止重试后的旧周榜响应覆盖新状态。
return Promise.allSettled([statusRequest]).then(function (results) {
if (generation !== requestGeneration) return;
if (results[0].status === 'fulfilled') {
applyActivityStatus(results[0].value);
} else {
activityData.statusState = 'error';
}
render();
});
}
function applyTabState() {
tabs.forEach(function (tab) {
var isActive = tab.getAttribute('data-tab') === activeTab;
tab.classList.toggle('is-active', isActive);
tab.setAttribute('aria-selected', String(isActive));
tab.setAttribute('tabindex', isActive ? '0' : '-1');
var image = tab.querySelector('img');
if (image) {
image.src =
ASSET_BASE +
(isActive ? 'tab-active.webp' : 'tab-inactive.webp');
}
});
var activeButton = tabs.find(function (tab) {
return tab.getAttribute('data-tab') === activeTab;
});
tabContent.setAttribute(
'aria-labelledby',
activeButton ? activeButton.id : 'huwaaCpTabRank'
);
page.setAttribute('data-tab', activeTab);
}
function getPageWidth() {
var viewportWidth =
window.innerWidth ||
document.documentElement.clientWidth ||
DESIGN_WIDTH;
return Math.min(Math.max(viewportWidth, 1), MAX_PAGE_WIDTH);
}
function updateStageHeight() {
var designHeight = STAGE_HEIGHTS[activeTab] || STAGE_HEIGHTS.rank;
// Rewards 的完整 4782px 高度仅对应三档奖励都存在的设计态。加载失败、活动关闭或确实为空时
// 只有首块状态面板;按面板底部收拢页面,避免把不存在的二、三档内容伪装成长段空白。
if (activeTab === 'rewards' && !activityData.rewards.length) {
designHeight =
PANEL_LAYOUT.rewardFirstTop +
PANEL_LAYOUT.rewardPanelHeight +
160;
}
var pageWidth = getPageWidth();
var designScale = pageWidth / DESIGN_WIDTH;
stage.style.height = designHeight + 'px';
stage.style.minHeight = designHeight + 'px';
document.documentElement.style.setProperty(
'--huwaa-cp-page-width',
pageWidth + 'px'
);
document.documentElement.style.setProperty(
'--huwaa-cp-design-scale',
String(designScale)
);
stageWrap.style.height = Math.ceil(designHeight * designScale) + 'px';
stageWrap.style.minHeight = stageWrap.style.height;
copyright.style.top = designHeight - 77 + 'px';
document.body.style.minHeight = stageWrap.style.height;
viewport.style.minHeight = stageWrap.style.height;
page.style.minHeight = stageWrap.style.height;
}
function render() {
document.title = t('cp.pageTitle', 'Last Weekly CP');
tabContent.innerHTML =
activeTab === 'plaza'
? renderPlaza()
: activeTab === 'rewards'
? renderRewards()
: renderRank();
applyTabState();
updateStageHeight();
updateCountdown();
}
function updateURLTab() {
try {
var nextURL = new URL(window.location.href);
nextURL.searchParams.set('tab', activeTab);
window.history.replaceState(null, '', nextURL.toString());
} catch (_) {
// tab 切换本身不依赖 URLhistory 被宿主禁用时仅放弃可分享状态参数。
}
}
function selectTab(nextTab, shouldFocus) {
if (['plaza', 'rank', 'rewards'].indexOf(nextTab) < 0) return;
activeTab = nextTab;
updateURLTab();
render();
var activeButton = tabs.find(function (tab) {
return tab.getAttribute('data-tab') === activeTab;
});
if (shouldFocus && activeButton) activeButton.focus();
announcement.textContent =
activeButton && activeButton.querySelector('span')
? activeButton.querySelector('span').textContent
: '';
}
function bindInteractions() {
tabs.forEach(function (tab, index) {
tab.addEventListener('click', function () {
selectTab(tab.getAttribute('data-tab') || 'rank', false);
});
tab.addEventListener('keydown', function (event) {
if (event.key !== 'ArrowLeft' && event.key !== 'ArrowRight')
return;
event.preventDefault();
var delta = event.key === 'ArrowRight' ? 1 : -1;
var nextIndex = (index + delta + tabs.length) % tabs.length;
selectTab(tabs[nextIndex].getAttribute('data-tab'), true);
});
});
tabContent.addEventListener('click', function (event) {
var retry = event.target.closest('[data-retry]');
if (retry) loadData(retry.getAttribute('data-retry') || 'status');
});
}
function init() {
countdownEndAt = resolveExplicitEndAt();
bindInteractions();
render();
loadData();
countdownTimer = window.setInterval(updateCountdown, 1000);
window.addEventListener('resize', updateStageHeight);
window.addEventListener('hyapp:i18n-ready', function () {
if (isExplicitMockMode()) applyMockData();
render();
});
}
window.addEventListener('beforeunload', function () {
requestGeneration += 1;
if (countdownTimer) window.clearInterval(countdownTimer);
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
})();

424
activity/invite/huwaa.html Normal file
View File

@ -0,0 +1,424 @@
<!doctype html>
<html lang="en" dir="ltr" data-hy-app-code="huwaa">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title data-i18n="huwaa_invite.page_title">Cash Wheel</title>
<link rel="icon" href="data:," />
<link rel="stylesheet" href="../../common/theme.css?v=20260716" />
<link rel="stylesheet" href="./huwaa/huwaa.css?v=20260716" />
</head>
<body>
<main
class="huwaa-page"
id="huwaaPage"
aria-label="Huwaa cash wheel invite activity"
data-i18n-aria="huwaa_invite.page_aria"
>
<section class="huwaa-stage" id="huwaaStage">
<div class="huwaa-palace" aria-hidden="true">
<img
src="./huwaa/assets/layers/bg-palace.webp"
alt=""
draggable="false"
/>
<img
src="./huwaa/assets/layers/bg-palace.webp"
alt=""
draggable="false"
/>
</div>
<div class="huwaa-status-bar" aria-hidden="true">
<span class="huwaa-status-time">9:41</span>
<img
class="huwaa-status-cellular"
src="./huwaa/assets/layers/status-cellular.svg"
alt=""
/>
<img
class="huwaa-status-wifi"
src="./huwaa/assets/layers/status-wifi.svg"
alt=""
/>
<img
class="huwaa-status-battery"
src="./huwaa/assets/layers/status-battery.svg"
alt=""
/>
</div>
<header class="huwaa-nav">
<button
class="huwaa-nav-back"
id="huwaaBackButton"
type="button"
aria-label="Back"
data-i18n-aria="huwaa_invite.back"
>
<img src="./huwaa/assets/layers/nav-back.svg" alt="" />
</button>
<h1 data-i18n="huwaa_invite.page_title">Cash Wheel</h1>
</header>
<section
class="huwaa-wallet-card"
aria-label="Cash wheel points"
data-i18n-aria="huwaa_invite.wallet_aria"
>
<img
class="huwaa-wallet-skin"
src="./huwaa/assets/layers/panel-gold-emerald.webp"
alt=""
draggable="false"
/>
<div class="huwaa-wallet-texture" aria-hidden="true">
<img
src="./huwaa/assets/layers/wallet-inner-panel.webp"
alt=""
draggable="false"
/>
</div>
<div class="huwaa-wallet-rate" aria-hidden="true">
<img src="./huwaa/assets/layers/points-a.webp" alt="" />
<strong id="huwaaRatePoints">90K</strong>
<img
class="huwaa-rate-exchange"
src="./huwaa/assets/layers/icon-convert.svg"
alt=""
/>
<img
class="huwaa-rate-finance"
src="./huwaa/assets/layers/icon-finance.svg"
alt=""
/>
<img
class="huwaa-rate-infinity"
src="./huwaa/assets/layers/icon-infinity.svg"
alt=""
/>
</div>
<div class="huwaa-wallet-balance">
<img src="./huwaa/assets/layers/points-a.webp" alt="" />
<strong id="huwaaPointBalance">89,094</strong>
</div>
<strong class="huwaa-wallet-cash" id="huwaaCashValue"
>≈$8.9</strong
>
<p class="huwaa-wallet-remaining">
<span data-i18n="huwaa_invite.points_left"
>Points Left To Withdraw:</span
>
<strong id="huwaaPointsLeft">1,000</strong>
</p>
<button
class="huwaa-withdraw-button"
id="huwaaWithdrawButton"
type="button"
>
<img
src="./huwaa/assets/layers/button-withdraw.webp"
alt=""
draggable="false"
/>
<span data-i18n="huwaa_invite.withdraw"
>WITHDRAW TO WALLET</span
>
</button>
</section>
<section
class="huwaa-wheel-region"
aria-label="Cash wheel"
data-i18n-aria="huwaa_invite.wheel_aria"
>
<img
class="huwaa-wheel-base"
src="./huwaa/assets/layers/wheel-base.webp"
alt=""
draggable="false"
/>
<button
class="huwaa-reward-entry"
id="huwaaRewardEntry"
type="button"
aria-label="Estimated rewards"
data-i18n-aria="huwaa_invite.estimated_rewards"
>
<img
src="./huwaa/assets/layers/chest-red.webp"
alt=""
/>
<img
src="./huwaa/assets/layers/chest-red.webp"
alt=""
/>
</button>
<div class="huwaa-wheel-prizes" id="huwaaWheelPrizes">
<button
class="huwaa-prize"
type="button"
data-index="0"
>
<img
src="./huwaa/assets/layers/chest-orange.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="1"
>
<img
src="./huwaa/assets/layers/chest-green.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="2"
>
<img
src="./huwaa/assets/layers/chest-white-gold.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="3"
>
<img
src="./huwaa/assets/layers/chest-blue.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="4"
>
<img
src="./huwaa/assets/layers/chest-purple.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="5"
>
<img
src="./huwaa/assets/layers/chest-red.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
<button
class="huwaa-prize"
type="button"
data-index="6"
>
<img
src="./huwaa/assets/layers/chest-gold.webp"
alt=""
/>
<span data-i18n="huwaa_invite.medium">Medium</span>
</button>
</div>
<button
class="huwaa-spin-button"
id="huwaaSpinButton"
type="button"
aria-label="Spin"
data-i18n-aria="huwaa_invite.spin"
>
<img
src="./huwaa/assets/layers/wheel-spin-center.webp"
alt=""
draggable="false"
/>
<span
class="huwaa-spin-label"
data-i18n="huwaa_invite.spin"
>Spin</span
>
<span class="huwaa-spin-cost" id="huwaaSpinCost"
>*5</span
>
</button>
</section>
<button
class="huwaa-invite-button"
id="huwaaInviteButton"
type="button"
>
<img
src="./huwaa/assets/layers/button-invite.webp"
alt=""
draggable="false"
/>
<span data-i18n="huwaa_invite.invite_now"
>Invite Friends Now</span
>
</button>
<section
class="huwaa-task-panel huwaa-extendable-panel"
aria-label="Invitation tasks"
data-i18n-aria="huwaa_invite.tasks_aria"
>
<div class="huwaa-extendable-skin" aria-hidden="true">
<span class="huwaa-skin-top"></span>
<span class="huwaa-skin-middle"></span>
<span class="huwaa-skin-bottom"></span>
</div>
<div class="huwaa-task-list" id="huwaaTaskList"></div>
</section>
<section
class="huwaa-win-modal"
id="huwaaWinModal"
role="dialog"
aria-modal="true"
aria-labelledby="huwaaWinTitle"
hidden
>
<button
class="huwaa-modal-scrim"
type="button"
aria-label="Close"
data-i18n-aria="huwaa_invite.close"
data-close-win
></button>
<article class="huwaa-win-dialog" aria-live="polite">
<img
class="huwaa-win-skin"
src="./huwaa/assets/layers/reward-modal-panel.webp"
alt=""
draggable="false"
/>
<div class="huwaa-win-media">
<img id="huwaaWinImage" alt="" />
</div>
<h2
id="huwaaWinTitle"
tabindex="-1"
data-i18n="huwaa_invite.congratulations"
>
CONGRATULATIONS!
</h2>
<p
class="huwaa-win-kicker"
data-i18n="huwaa_invite.you_won"
>
You have won
</p>
<p class="huwaa-win-reward">
<strong id="huwaaWinReward">Medium Reward</strong>
<span id="huwaaWinPoints">(500 Points)</span>
</p>
<button
class="huwaa-awesome-button"
id="huwaaAwesomeButton"
type="button"
>
<img
src="./huwaa/assets/layers/button-awesome.webp"
alt=""
draggable="false"
/>
<span data-i18n="huwaa_invite.awesome"
>AWESOME</span
>
</button>
</article>
</section>
<section
class="huwaa-reward-drawer"
id="huwaaRewardDrawer"
role="dialog"
aria-modal="true"
aria-labelledby="huwaaRewardTitle"
hidden
>
<button
class="huwaa-modal-scrim"
type="button"
aria-label="Close"
data-i18n-aria="huwaa_invite.close"
data-close-rewards
></button>
<article class="huwaa-reward-sheet huwaa-extendable-panel">
<div class="huwaa-extendable-skin" aria-hidden="true">
<span class="huwaa-skin-top"></span>
<span class="huwaa-skin-middle"></span>
</div>
<h2
id="huwaaRewardTitle"
tabindex="-1"
data-i18n="huwaa_invite.estimated_rewards"
>
ESTIMATED REWARDS
</h2>
<div
class="huwaa-estimated-grid"
id="huwaaEstimatedGrid"
></div>
</article>
</section>
</section>
</main>
<template id="huwaaTaskTemplate">
<article class="huwaa-task-card">
<img
class="huwaa-task-icon"
src="./huwaa/assets/layers/reward-calendar-check.webp"
alt=""
/>
<div class="huwaa-task-copy">
<strong data-task-title></strong>
<span data-task-description></span>
</div>
<strong class="huwaa-task-reward" data-task-reward></strong>
<span class="huwaa-task-status" data-task-status></span>
</article>
</template>
<template id="huwaaEstimatedTemplate">
<article class="huwaa-estimated-item">
<span class="huwaa-estimated-icon">
<img alt="" />
</span>
<strong></strong>
</article>
</template>
<script>
window.HyAppDefaultAppCode = 'huwaa';
</script>
<script src="../../common/i18n.js?v=20260716"></script>
<script src="../../common/api.js?v=20260716"></script>
<script src="../../common/params.js?v=20260716"></script>
<script src="../../common/toast.js?v=20260716"></script>
<script src="./huwaa/huwaa.js?v=20260716"></script>
</body>
</html>

View File

@ -0,0 +1,11 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 541 812" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="image 5044" opacity="0.5">
<rect width="541" height="812" fill="url(#paint0_linear_0_18)" style="mix-blend-mode:screen"/>
</g>
<defs>
<linearGradient id="paint0_linear_0_18" x1="270.5" y1="791.5" x2="270.5" y2="812" gradientUnits="userSpaceOnUse">
<stop stop-color="white"/>
<stop offset="1" stop-color="white" stop-opacity="0"/>
</linearGradient>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 542 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.3 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 13 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 408 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 366 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 146 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 140 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 161 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 174 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 159 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 150 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 171 KiB

View File

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 12.5205" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="&#232;&#189;&#172;&#230;&#141;&#162;_&#232;&#189;&#172;&#230;&#141;&#162;_copy 1" clip-path="url(#clip0_0_6)">
<path id="Vector" d="M0.0273838 7.72325L3.28198 12.4413C3.29831 12.4654 3.32031 12.4852 3.34605 12.4989C3.3718 12.5126 3.4005 12.5197 3.42965 12.5197C3.4588 12.5197 3.48751 12.5126 3.51325 12.4989C3.539 12.4852 3.561 12.4654 3.57732 12.4413L6.83413 7.72325C6.85226 7.69708 6.86227 7.66615 6.86293 7.63432C6.86358 7.60249 6.85485 7.57117 6.83781 7.54428C6.82029 7.5169 6.79554 7.49491 6.76629 7.48074C6.73704 7.46657 6.70444 7.46078 6.6721 7.464H4.35949V3.47216C4.35949 3.47216 4.30425 1.32084 5.94591 0.777304C5.35148 0.834823 4.77582 1.0169 4.25644 1.31168C3.73705 1.60646 3.28557 2.00734 2.93141 2.4882C2.64546 2.8983 2.49505 3.38763 2.50129 3.88755V7.43749H0.188678C0.155221 7.43884 0.122642 7.44859 0.0939403 7.46583C0.0652391 7.48308 0.041338 7.50727 0.0244378 7.53618C0.0080584 7.56471 -0.000375733 7.59711 1.28449e-05 7.63001C0.000401422 7.66291 0.00959851 7.69511 0.0266473 7.72325H0.0273838ZM13.9627 4.79566L10.7074 0.0783638C10.6909 0.0542118 10.6688 0.0344501 10.6429 0.020794C10.617 0.00713785 10.5882 0 10.559 0C10.5297 0 10.5009 0.00713785 10.4751 0.020794C10.4492 0.0344501 10.4271 0.0542118 10.4106 0.0783638L7.15745 4.79418C7.1394 4.82035 7.12936 4.85122 7.12857 4.88301C7.12779 4.9148 7.13629 4.94612 7.15303 4.97315C7.18839 5.02913 7.25246 5.0608 7.31875 5.05417H9.63136V9.04601C9.63136 9.04601 9.68439 11.1966 8.04347 11.7409C8.63772 11.6834 9.21321 11.5014 9.73247 11.2067C10.2517 10.9121 10.7031 10.5114 11.0572 10.0307C11.3435 9.61992 11.4939 9.12979 11.4873 8.62914V5.05711H13.8022C13.8685 5.06448 13.9333 5.03281 13.9686 4.9761C13.9858 4.9489 13.9944 4.91722 13.9935 4.88509C13.9926 4.85296 13.9821 4.82183 13.9635 4.79566H13.9627Z" fill="var(--fill-0, white)"/>
</g>
<defs>
<clipPath id="clip0_0_6">
<rect width="14" height="12.5205" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 2.0 KiB

View File

@ -0,0 +1,5 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Finance 1">
<path id="Vector" d="M5.23346 3.26063L4.44437 1.36108C5.02265 1.41856 5.60249 1.26808 6.07983 0.936627C6.20519 0.799172 6.37001 0.696082 6.55646 0.638172C6.74292 0.5809 6.94274 0.570082 7.13555 0.6089C7.23371 0.630091 7.32525 0.674814 7.40229 0.739219C7.47933 0.803624 7.53957 0.885783 7.57783 0.978627C7.60964 1.14535 7.72165 1.29045 7.88201 1.37635C8.22521 1.49318 8.60062 1.47009 8.92692 1.31208C9.08283 1.26372 9.37555 1.17145 9.41374 1.21599C9.3693 1.96612 9.13808 2.69324 8.7411 3.33126C8.20899 3.47896 7.65976 3.55598 7.10755 3.56035C6.47148 3.55355 5.83993 3.45254 5.23346 3.26063ZM12.2182 10.8881C12.1452 11.4216 11.899 11.9163 11.5176 12.2964C11.116 12.6968 10.6058 12.9707 10.0501 13.0842C8.04129 13.5314 5.95873 13.5314 3.94992 13.0842C3.39425 12.9707 2.88406 12.6968 2.48246 12.2964C2.10095 11.9164 1.85479 11.4216 1.78183 10.8881C1.42037 7.25572 4.24137 4.65299 4.99037 4.03063C5.66937 4.23935 6.38019 4.35008 7.09674 4.35772H7.10565C7.76752 4.35317 8.42575 4.2592 9.06246 4.07835C9.8821 4.76563 12.5707 7.31999 12.2188 10.8887H12.2182V10.8881ZM7.18455 8.29363C7.16737 8.29363 7.1591 8.28535 7.14065 8.28535C6.50619 7.99835 6.55901 7.83799 6.61246 7.69481C6.64958 7.60417 6.71411 7.52741 6.79701 7.47526C6.88409 7.42041 6.98491 7.39129 7.08783 7.39126H7.09674C7.19855 7.38657 7.29961 7.41084 7.38819 7.46126C7.47307 7.50929 7.54073 7.58274 7.58165 7.67126C7.67328 7.87426 7.92846 7.97354 8.15437 7.89399C8.26038 7.85818 8.34782 7.78173 8.39746 7.68145C8.42037 7.63379 8.43258 7.5817 8.43324 7.52882C8.4339 7.47595 8.42299 7.42357 8.40128 7.37535C8.25046 7.02535 7.92783 6.75935 7.52883 6.65626V6.34508C7.52819 6.1249 7.33155 5.94672 7.08783 5.94608C6.97265 5.94419 6.86095 5.98551 6.77474 6.0619C6.73462 6.09735 6.70252 6.14094 6.68057 6.18977C6.65863 6.2386 6.64734 6.29155 6.64746 6.34508V6.66454C6.23637 6.78672 5.9131 7.07626 5.77437 7.44726C5.42183 8.41326 6.45337 8.86826 6.7881 9.01208C6.80592 9.01972 6.81483 9.02799 6.83265 9.02799C7.47537 9.31499 7.62555 9.45117 7.56383 9.69044C7.51928 9.85844 7.29019 9.92972 7.10565 9.94626H7.04392C6.91437 9.95771 6.78411 9.93442 6.66655 9.87881C6.55317 9.82572 6.45766 9.74082 6.39164 9.63445C6.29683 9.43272 6.04037 9.33663 5.81637 9.41999C5.59174 9.50272 5.48292 9.73372 5.57201 9.93799C5.76419 10.3421 6.16892 10.6304 6.64746 10.7048V10.8639C6.64725 10.9175 6.65849 10.9706 6.68044 11.0196C6.70239 11.0685 6.73454 11.1122 6.77474 11.1477C6.85746 11.2228 6.97074 11.2648 7.08783 11.2635C7.33092 11.2629 7.52819 11.0847 7.52883 10.8645V10.6724C7.74201 10.6265 7.93801 10.5285 8.09519 10.3892C8.25174 10.2504 8.36374 10.0748 8.4191 9.88199C8.66601 8.94781 7.7401 8.54117 7.18455 8.29363Z" fill="var(--fill-0, #ECC100)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 2.8 KiB

View File

@ -0,0 +1,10 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 14 14" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="&#231;&#172;&#166;&#229;&#143;&#183;_&#230;&#151;&#160;&#233;&#153;&#144; 1" clip-path="url(#clip0_0_4)">
<path id="Vector" d="M10.7713 3.58594C9.47625 3.58594 8.32125 4.32969 7.67375 5.56344C7.51625 5.86094 7.63875 6.23719 7.93625 6.38594C8.23375 6.54344 8.61 6.42094 8.75875 6.12344C9.1875 5.30094 9.93125 4.80219 10.7625 4.80219C11.865 4.80219 12.7663 5.70344 12.7663 6.80594C12.7663 7.90844 11.865 8.80969 10.7625 8.80969C9.7825 8.80969 9.0125 7.98719 8.0325 6.95469C7.86625 6.77969 7.69125 6.59594 7.5075 6.40344C7.07 5.92219 4.8475 3.58594 3.22875 3.58594C1.44375 3.58594 0 5.03844 0 6.81469C0 8.59094 1.44375 10.0434 3.22875 10.0434C4.55 10.0434 5.7225 9.29969 6.37 8.04844C6.5275 7.75094 6.405 7.37469 6.1075 7.22594C5.81 7.06844 5.43375 7.19094 5.285 7.48844C4.8475 8.33719 4.10375 8.81844 3.2375 8.81844C2.12625 8.81844 1.225 7.91719 1.225 6.81469C1.225 5.71219 2.12625 4.81094 3.22875 4.81094C4.03375 4.81094 5.69625 6.23719 6.62375 7.24344L6.64125 7.26094C6.81625 7.44469 6.99125 7.62844 7.1575 7.80344C8.2425 8.95844 9.275 10.0434 10.78 10.0434C12.5562 10.0434 14.0088 8.59969 14.0088 6.81469C14.0088 5.02969 12.5563 3.58594 10.7713 3.58594Z" fill="var(--fill-0, white)"/>
</g>
<defs>
<clipPath id="clip0_0_4">
<rect width="14" height="14" fill="white"/>
</clipPath>
</defs>
</svg>

After

Width:  |  Height:  |  Size: 1.4 KiB

View File

@ -0,0 +1,430 @@
{
"schemaVersion": 1,
"figma": {
"fileKey": "OTOg5cwbhVNWxkImGmzTJl",
"canvas": {
"id": "543:1998",
"name": "Huwaa运营"
},
"entryFrames": {
"default": "850:232",
"win": "850:431",
"estimatedRewards": "850:638"
},
"designSize": {
"width": 375,
"height": 1114
},
"forbiddenAggregateNodes": [
"543:1998",
"850:232",
"850:431",
"850:638",
"850:237",
"850:255",
"850:262",
"850:291",
"850:314",
"850:425",
"850:628",
"850:835"
]
},
"assets": [
{
"file": "bg-palace-mask.svg",
"sourceNode": "850:234",
"sourceName": "image 5044",
"sourceType": "ROUNDED_RECTANGLE",
"purpose": "Bottom fade mask for the palace-only background layer",
"bounds": { "width": 541, "height": 812 },
"original": { "width": 541, "height": 812 },
"exportFormat": "svg",
"bytes": 542,
"sha256": "f99c7cf545fa81ef21174ea786fa32485a2efa0bc0e0b223b04f13e29255c8cf",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "bg-palace.webp",
"sourceNode": "850:235",
"sourceNodes": ["850:235", "850:236"],
"sourceName": "image 5046",
"sourceType": "ROUNDED_RECTANGLE",
"purpose": "Palace-only decorative background reused by both half-opacity Figma layers",
"bounds": { "width": 541, "height": 812 },
"original": { "width": 1024, "height": 1536 },
"exportFormat": "webp-lossless",
"bytes": 2406896,
"sha256": "fb80cada233c7ebcf37aa5a0fa16c92f836db8aa7a05e7b8782426273563ddb6",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "button-awesome.webp",
"sourceNode": "850:636",
"sourceName": "图层 1 9",
"sourceType": "RECTANGLE",
"purpose": "Cropped AWESOME button skin without its DOM label",
"bounds": { "width": 250, "height": 53 },
"original": { "width": 250, "height": 53 },
"exportFormat": "webp-lossless",
"bytes": 13646,
"sha256": "a40f8c77ed9f78921e857dc530e032997d964e396207cff931934e46f9f93888",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "button-invite.webp",
"sourceNode": "850:426",
"sourceName": "图层 0 315",
"sourceType": "RECTANGLE",
"purpose": "Invite CTA skin without its DOM label",
"bounds": { "width": 280, "height": 50 },
"original": { "width": 1990, "height": 360 },
"exportFormat": "webp-lossless",
"bytes": 417966,
"sha256": "538d9833e1e8f8bf7bfbfbf7c6032afa18425a9fff0a661dd83d09f082b1f00b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "button-withdraw.webp",
"sourceNode": "850:312",
"sourceName": "图层 1 7",
"sourceType": "RECTANGLE",
"purpose": "Wallet withdrawal button skin without its DOM label",
"bounds": { "width": 214, "height": 34 },
"original": { "width": 2055, "height": 329 },
"exportFormat": "webp-lossless",
"bytes": 374810,
"sha256": "e566ebfbe19d15b667a3238df600a7cc1225d3e7ec35903443f82008760239f9",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-blue.webp",
"sourceNode": "850:267",
"sourceName": "03 2 1",
"sourceType": "RECTANGLE",
"purpose": "Blue wheel reward chest",
"bounds": { "width": 57, "height": 55 },
"original": { "width": 354, "height": 336 },
"exportFormat": "webp-lossless",
"bytes": 149466,
"sha256": "0551c01bc09d311d20a8a6f1323629b7bb3898d446b3b22c6e947a2c0b04b272",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-gold.webp",
"sourceNode": "850:270",
"sourceName": "01 2 1",
"sourceType": "RECTANGLE",
"purpose": "Gold wheel reward chest",
"bounds": { "width": 57, "height": 51 },
"original": { "width": 355, "height": 316 },
"exportFormat": "webp-lossless",
"bytes": 142820,
"sha256": "0ba668e4312b24377ead80d73b0fb87bd778dcaf8bf444a9a176e703fe88cd36",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-green.webp",
"sourceNode": "850:283",
"sourceName": "04 2 1",
"sourceType": "RECTANGLE",
"purpose": "Green wheel reward chest",
"bounds": { "width": 57, "height": 53 },
"original": { "width": 354, "height": 331 },
"exportFormat": "webp-lossless",
"bytes": 164498,
"sha256": "5b3e74a47979ac5f4459a4de0ceb66473d272fbb0484f9aa84807a0c630b3209",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-orange.webp",
"sourceNode": "850:279",
"sourceName": "07 2 1",
"sourceType": "RECTANGLE",
"purpose": "Orange wheel reward chest",
"bounds": { "width": 57, "height": 53 },
"original": { "width": 354, "height": 325 },
"exportFormat": "webp-lossless",
"bytes": 178720,
"sha256": "3e7ffe20e4309dee33cbde633597794f68a77e3b07beaff4d5e63f830dbb33e9",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-purple.webp",
"sourceNode": "850:276",
"sourceName": "05 2 1",
"sourceType": "RECTANGLE",
"purpose": "Purple wheel reward chest",
"bounds": { "width": 57, "height": 55 },
"original": { "width": 354, "height": 334 },
"exportFormat": "webp-lossless",
"bytes": 162400,
"sha256": "f6f782ee6440d7349fabf6955f81d71345f71a70a9d3151a20417cb40893703e",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-red.webp",
"sourceNode": "850:273",
"sourceNodes": ["850:273", "850:429", "850:430"],
"sourceName": "02 2 1",
"sourceType": "RECTANGLE",
"purpose": "Red wheel reward chest reused by the estimated-rewards entry",
"bounds": { "width": 57, "height": 52 },
"original": { "width": 353, "height": 318 },
"exportFormat": "webp-lossless",
"bytes": 153572,
"sha256": "89fba58f5b4ad0aebf4fe193eabbec83070eeb6246f11f5fa41e580f150e4e39",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "chest-white-gold.webp",
"sourceNode": "850:286",
"sourceName": "图层 1 6",
"sourceType": "RECTANGLE",
"purpose": "White-gold wheel reward chest",
"bounds": { "width": 57, "height": 55 },
"original": { "width": 355, "height": 341 },
"exportFormat": "webp-lossless",
"bytes": 174792,
"sha256": "ed3f81ff4f21ad9a8ca3d32c42a11953b5a2e8bf9bd9ddeb1918d64a4220946b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "icon-convert.svg",
"sourceNode": "850:306",
"sourceName": "转换_转换_copy 1",
"sourceType": "INSTANCE",
"purpose": "Points conversion arrows",
"bounds": { "width": 12.521, "height": 14 },
"original": { "width": 14, "height": 12.5205 },
"exportFormat": "svg",
"bytes": 2095,
"sha256": "df2b2c6f97d7ae52bd61240846a9168281d2406b61bc1a39b5676061a6484ebb",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "icon-finance.svg",
"sourceNode": "850:308",
"sourceName": "Finance 1",
"sourceType": "INSTANCE",
"purpose": "Wallet finance icon",
"bounds": { "width": 14, "height": 14 },
"original": { "width": 14, "height": 14 },
"exportFormat": "svg",
"bytes": 2884,
"sha256": "86e054e618747a8d6ec1c248fc74bc1781eeb96654cceacc4b4274db24c07be4",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "icon-infinity.svg",
"sourceNode": "850:304",
"sourceName": "符号_无限 1",
"sourceType": "INSTANCE",
"purpose": "Infinite points indicator",
"bounds": { "width": 14, "height": 14 },
"original": { "width": 14, "height": 14 },
"exportFormat": "svg",
"bytes": 1472,
"sha256": "58b7569f3a7f018c492bcfb1633d58c73b4fc807cb9e4a86ff5a03845dd2f841",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "nav-back.svg",
"sourceNode": "850:257",
"sourceName": "icon_20_返回",
"sourceType": "INSTANCE",
"purpose": "Navigation back icon",
"bounds": { "width": 24, "height": 24 },
"original": { "width": 24, "height": 24 },
"exportFormat": "svg",
"bytes": 419,
"sha256": "50d06f24adb0019fcdfb3c3f5db012808c230cd8c2f06c61b775fcccdbf525d4",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "panel-gold-emerald.webp",
"sourceNode": "850:292",
"sourceNodes": ["850:292", "850:315", "850:836"],
"sourceName": "图层 0 46",
"sourceType": "RECTANGLE",
"purpose": "Reusable gold-and-emerald panel skin reconstructed without Figma one-pixel slices",
"bounds": { "width": 351, "height": 205 },
"original": { "width": 1502, "height": 879 },
"exportFormat": "webp-lossless",
"bytes": 722510,
"sha256": "6780e103ca19992a5fa3b1e6be5f0d598dab26d803fb9463f9e01fc2fce49789",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "points-a.webp",
"sourceNode": "850:297",
"sourceNodes": ["850:297", "850:302"],
"sourceName": "图层 0 47",
"sourceType": "RECTANGLE",
"purpose": "Huwaa A points icon",
"bounds": { "width": 23, "height": 24 },
"original": { "width": 868, "height": 892 },
"exportFormat": "webp-lossless",
"bytes": 514910,
"sha256": "5e3b941cc0f6224ccb17017fad201a508d96230ff70d3f1268d2765636eb2240",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "reward-calendar-check.webp",
"sourceNode": "850:394",
"sourceNodes": ["850:394", "850:405", "850:416", "850:1085"],
"sourceName": "image 5049",
"sourceType": "RECTANGLE",
"purpose": "Task icon and reward-list fallback icon",
"bounds": { "width": 42, "height": 45 },
"original": { "width": 658, "height": 701 },
"exportFormat": "webp-lossless",
"bytes": 182252,
"sha256": "79b06b912c2e64ff3487bf8d35b848f493df4134583ba16c34d35ce0427ba81b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "reward-modal-panel.webp",
"sourceNode": "850:629",
"sourceName": "图层 0 316",
"sourceType": "RECTANGLE",
"purpose": "Win dialog decorative panel without reward media or text",
"bounds": { "width": 327, "height": 415 },
"original": { "width": 994, "height": 1262 },
"exportFormat": "webp-lossless",
"bytes": 960078,
"sha256": "26ded67cb3b3f917d68e517627a3328dbb02c223f3590f2ac3ad9609e510627f",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "status-battery.svg",
"sourceNode": "850:240",
"sourceName": "Battery",
"sourceType": "INSTANCE",
"purpose": "Design status-bar battery icon",
"bounds": { "width": 24.33, "height": 11.3333 },
"original": { "width": 24.33, "height": 11.3333 },
"exportFormat": "svg",
"bytes": 1005,
"sha256": "62fd408926ad465cc16e1fb57634dc5ff2205b4ab11a6b0edc110440e83f0f2b",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "status-cellular.svg",
"sourceNode": "850:248",
"sourceName": "Cellular Connection",
"sourceType": "INSTANCE",
"purpose": "Design status-bar cellular icon",
"bounds": { "width": 17, "height": 11.3334 },
"original": { "width": 17.0001, "height": 11.3334 },
"exportFormat": "svg",
"bytes": 1147,
"sha256": "112f067ba1b97345f39df94cba9e080a46606a4387fe913344a2b2e7e7aee13e",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "status-wifi.svg",
"sourceNode": "850:244",
"sourceName": "Wifi",
"sourceType": "INSTANCE",
"purpose": "Design status-bar Wi-Fi icon",
"bounds": { "width": 15.3333, "height": 11.01 },
"original": { "width": 15.3333, "height": 11.01 },
"exportFormat": "svg",
"bytes": 1633,
"sha256": "22cf5fae5ae340219fbe1ed699d3a7d1562f6f4993c24df5502c640f907a1ec5",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "wallet-inner-panel.webp",
"sourceNode": "850:293",
"sourceName": "成v下 1",
"sourceType": "RECTANGLE",
"purpose": "Wallet inner texture behind live balance values",
"bounds": { "width": 327, "height": 120 },
"original": { "width": 1464, "height": 622 },
"exportFormat": "webp-lossless",
"bytes": 459290,
"sha256": "e8fe298ba89673bb8058668481dee7023b30e4445f83ae3363d8202796750f1c",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "wheel-base.webp",
"sourceNode": "850:264",
"sourceName": "图层 0 49",
"sourceType": "RECTANGLE",
"purpose": "Empty wheel frame, stage, and pedestal without prizes or controls",
"bounds": { "width": 370, "height": 437.464 },
"original": { "width": 1038, "height": 1227 },
"exportFormat": "webp-lossless",
"bytes": 1118800,
"sha256": "abb8c248b21d09716eb7022aa991fd18c0452ec7a2f832792104dbeb533b58bb",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
},
{
"file": "wheel-spin-center.webp",
"sourceNode": "850:265",
"sourceName": "图层 0 43",
"sourceType": "RECTANGLE",
"purpose": "Wheel center and pointer skin without Spin or cost text",
"bounds": { "width": 106, "height": 117 },
"original": { "width": 756, "height": 840 },
"exportFormat": "webp-lossless",
"bytes": 407758,
"sha256": "09659e624c1cc7979ca847142f00e5c322653ad531f7752878d7fe9cc90c6506",
"containsText": false,
"containsControls": false,
"containsDynamicMedia": false
}
]
}

View File

@ -0,0 +1,9 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24 24" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="icon_20_&#232;&#191;&#148;&#229;&#155;&#158;">
<g id="Vector">
</g>
<g id="Vector_2">
</g>
<path id="Vector_3" d="M16 4L8 12L16 20" stroke="var(--stroke-0, white)" stroke-width="2" stroke-linecap="round" stroke-linejoin="round"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 419 B

Binary file not shown.

After

Width:  |  Height:  |  Size: 706 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 503 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 178 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 938 KiB

View File

@ -0,0 +1,7 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 24.33 11.3333" fill="none" xmlns="http://www.w3.org/2000/svg">
<g id="Battery">
<path id="Border" opacity="0.35" d="M2.66699 0.5H19.333C20.5296 0.5 21.5 1.47038 21.5 2.66699V8.66699C21.4998 9.86346 20.5295 10.833 19.333 10.833H2.66699C1.4705 10.833 0.500175 9.86346 0.5 8.66699V2.66699C0.5 1.47038 1.47039 0.5 2.66699 0.5Z" fill="var(--fill-0, white)" stroke="var(--stroke-0, white)"/>
<path id="Cap" opacity="0.4" fill-rule="evenodd" clip-rule="evenodd" d="M23.002 7.66406C23.8067 7.32529 24.33 6.53719 24.33 5.66406C24.33 4.79093 23.8067 4.00284 23.002 3.66406V7.66406V7.66406Z" fill="var(--fill-0, white)"/>
<path id="Capacity" fill-rule="evenodd" clip-rule="evenodd" d="M18.6667 2C19.403 2 20 2.59696 20 3.33333V8C20 8.73638 19.403 9.33333 18.6667 9.33333H3.33333C2.59695 9.33333 2 8.73638 2 8V3.33333C2 2.59696 2.59695 2 3.33333 2H18.6667V2Z" fill="var(--fill-0, white)"/>
</g>
</svg>

After

Width:  |  Height:  |  Size: 1005 B

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 17.0001 11.3334" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Cellular Connection" fill-rule="evenodd" clip-rule="evenodd" d="M11.3301 3.00002C11.8824 3.00002 12.3301 3.44774 12.3301 4.00002V10.3334C12.3301 10.8856 11.8824 11.3334 11.3301 11.3334H10.3301C9.77779 11.3334 9.33008 10.8856 9.33008 10.3334V4.00002C9.33008 3.44774 9.77779 3.00002 10.3301 3.00002H11.3301V3.00002ZM16.0001 0C16.5524 0 17.0001 0.447712 17.0001 1V9.66667C17.0001 10.219 16.5524 10.6667 16.0001 10.6667H15.0001C14.4479 10.6667 14.0001 10.219 14.0001 9.66667V1C14.0001 0.447712 14.4479 0 15.0001 0V0H16.0001V0ZM2 7.00002C2.55229 7.00002 3 7.44774 3 8.00002V10C3 10.5523 2.55229 11 2 11H0.999999C0.447714 11 0 10.5523 0 10V8.00002C0 7.44774 0.447714 7.00002 0.999999 7.00002H2V7.00002ZM6.66211 5.00002C7.2144 5.00002 7.66211 5.44774 7.66211 6.00003V10C7.66211 10.5523 7.2144 11 6.66211 11H5.66211C5.10982 11 4.66211 10.5523 4.66211 10V6.00003C4.66211 5.44774 5.10982 5.00002 5.66211 5.00002H6.66211V5.00002Z" fill="var(--fill-0, white)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.1 KiB

View File

@ -0,0 +1,3 @@
<svg preserveAspectRatio="none" width="100%" height="100%" overflow="visible" style="display: block;" viewBox="0 0 15.3333 11.01" fill="none" xmlns="http://www.w3.org/2000/svg">
<path id="Wifi" fill-rule="evenodd" clip-rule="evenodd" d="M9.99235 8.6639C9.99057 8.57712 9.95304 8.49492 9.88861 8.4367C8.60795 7.35443 6.7323 7.35443 5.45164 8.4367C5.38717 8.49487 5.34957 8.57705 5.34773 8.66383C5.3459 8.75061 5.37998 8.8343 5.44194 8.89514L7.44757 10.9171C7.50636 10.9766 7.5865 11.01 7.67013 11.01C7.75375 11.01 7.83389 10.9766 7.89267 10.9171L9.89797 8.89514C9.95998 8.83434 9.99412 8.75068 9.99235 8.6639V8.6639ZM13.6438 4.67028C13.7652 4.78852 13.9593 4.78703 14.0789 4.66693L15.2395 3.4966C15.3 3.43569 15.3338 3.35318 15.3333 3.26733C15.3328 3.18148 15.2981 3.09937 15.2368 3.03917C11.0049 -1.01306 4.32858 -1.01306 0.0966678 3.03917C0.0353653 3.09933 0.00057401 3.18141 6.67831e-06 3.26726C-0.000545321 3.35311 0.0331419 3.43564 0.0936471 3.4966L1.25459 4.66693C1.3741 4.78721 1.56831 4.7887 1.68966 4.67028C3.30222 3.13859 5.44213 2.28447 7.66707 2.28448C9.89181 2.28457 12.0315 3.13869 13.6438 4.67028V4.67028ZM10.978 7.36445C11.1006 7.48079 11.2936 7.47827 11.4131 7.35876L12.5724 6.18843C12.6334 6.12705 12.6673 6.04377 12.6664 5.95724C12.6655 5.8707 12.63 5.78813 12.5677 5.728C9.80853 3.16359 5.53562 3.16359 2.77646 5.728C2.71413 5.78813 2.67857 5.87074 2.67775 5.95731C2.67694 6.04387 2.71093 6.12714 2.7721 6.18843L3.93104 7.35876C4.05051 7.47827 4.24356 7.48079 4.36611 7.36445C5.27158 6.54522 6.44936 6.0913 7.6709 6.0908C8.89325 6.09072 10.072 6.54468 10.978 7.36445V7.36445Z" fill="var(--fill-0, white)"/>
</svg>

After

Width:  |  Height:  |  Size: 1.6 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 448 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 398 KiB

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -0,0 +1,171 @@
<!doctype html>
<html lang="en" data-hy-app-code="fami">
<head>
<meta charset="utf-8" />
<meta
name="viewport"
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
/>
<title>Recharge Reward</title>
<link rel="stylesheet" href="../../common/theme.css?v=20260716" />
<link
rel="stylesheet"
href="./fami/fami.css?v=20260716-figma-822-2928"
/>
<link
rel="preload"
as="image"
href="./fami/assets/background-source-844-7068.png"
/>
</head>
<body>
<div class="fami-recharge-app" id="appViewport">
<main class="stage-wrap" id="stageWrap">
<section
class="stage"
id="stage"
data-node-id="822:2928"
aria-label="Recharge Reward activity"
data-i18n-aria="rechargeReward.pageLabel"
aria-busy="true"
>
<div class="activity-background" aria-hidden="true">
<div
class="background-segment background-segment-mid"
data-node-id="844:7094"
>
<img
src="./fami/assets/background-source-844-7068.png"
alt=""
/>
</div>
<div
class="background-segment background-segment-top"
data-node-id="822:2929"
>
<img
src="./fami/assets/background-source-844-7068.png"
alt=""
/>
</div>
<div
class="background-segment background-segment-bottom"
data-node-id="844:7090"
>
<img
src="./fami/assets/background-source-844-7068.png"
alt=""
/>
</div>
</div>
<section
class="countdown"
id="countdown"
data-node-id="822:2953"
aria-label="Activity countdown"
>
<img
class="countdown-frame"
src="./fami/assets/countdown-frame-822-2954.png"
alt=""
data-node-id="822:2954"
/>
<output
class="count-value count-value-days"
data-count="days"
></output>
<output
class="count-value count-value-hours"
data-count="hours"
></output>
<output
class="count-value count-value-minutes"
data-count="minutes"
></output>
<output
class="count-value count-value-seconds"
data-count="seconds"
></output>
<span
class="count-label count-label-days"
data-node-id="822:2955"
data-i18n="rechargeReward.days"
>Days</span
>
<span
class="count-label count-label-hours"
data-node-id="822:2956"
data-i18n="rechargeReward.hours"
>Hours</span
>
<span
class="count-label count-label-minutes"
data-node-id="822:2957"
data-i18n="rechargeReward.minutes"
>Minutes</span
>
<span
class="count-label count-label-seconds"
data-node-id="822:2958"
data-i18n="rechargeReward.seconds"
>Seconds</span
>
</section>
<section
class="profile-card"
data-node-id="822:2959"
aria-label="My recharge status"
>
<img
class="profile-frame"
src="./fami/assets/profile-frame-840-4737.png"
alt=""
data-node-id="840:4737"
/>
<img
class="profile-avatar"
id="profileAvatar"
alt=""
data-node-id="822:2960"
hidden
/>
<h1
class="profile-name"
id="profileName"
data-node-id="822:2962"
></h1>
<p
class="profile-id"
id="profileID"
data-node-id="822:2964"
></p>
<p
class="profile-recharge"
id="profileRecharge"
data-node-id="822:2963"
></p>
</section>
<div class="reward-sections" id="rewardSections"></div>
<p
class="page-status"
id="pageStatus"
role="status"
aria-live="polite"
hidden
></p>
</section>
</main>
</div>
<script>
// Fami 页面即使由旧客户端漏传 app_code也必须稳定请求 Fami 租户;显式 query 仍由 common/api.js 优先处理。
window.HyAppDefaultAppCode = 'fami';
</script>
<script src="../../common/i18n.js?v=20260716"></script>
<script src="../../common/api.js?v=20260716"></script>
<script src="./fami/fami.js?v=20260716-figma-822-2928"></script>
</body>
</html>

Binary file not shown.

After

Width:  |  Height:  |  Size: 3.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 479 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 134 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 47 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 903 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 66 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 89 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 692 KiB

View File

@ -0,0 +1,437 @@
@font-face {
font-family: 'Fami Source Han Sans SC';
src: url('./assets/fonts/source-han-sans-sc-regular-latin.woff2')
format('woff2');
font-style: normal;
font-weight: 400;
font-display: swap;
}
@font-face {
font-family: 'Fami Source Han Sans SC';
src: url('./assets/fonts/source-han-sans-sc-medium-latin.woff2')
format('woff2');
font-style: normal;
font-weight: 500;
font-display: swap;
}
* {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
:root {
--fami-design-width: 1080;
--fami-stage-scale: 0.3472222222;
--fami-stage-height: 3273px;
--fami-background-bottom-top: 2695px;
}
html,
body {
margin: 0;
width: 100%;
min-height: 100%;
overflow-x: hidden;
background: #080116;
color: #f6e0b2;
font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
}
body {
display: flex;
justify-content: center;
}
img {
display: block;
}
.fami-recharge-app {
width: min(100vw, 430px);
min-height: calc(3273px * var(--fami-stage-scale));
overflow: hidden;
background: #080116;
}
.stage-wrap {
position: relative;
width: 100%;
height: calc(3273px * var(--fami-stage-scale));
overflow: hidden;
}
.stage {
position: absolute;
left: 0;
top: 0;
width: 1080px;
height: var(--fami-stage-height);
overflow: hidden;
transform: scale(var(--fami-stage-scale));
transform-origin: top left;
background: #080116;
}
.activity-background,
.reward-sections {
position: absolute;
inset: 0;
}
.activity-background {
z-index: 0;
pointer-events: none;
}
.background-segment {
position: absolute;
left: 0;
width: 1080px;
overflow: hidden;
}
.background-segment img {
position: absolute;
left: 0;
width: 1080px;
height: 1887px;
max-width: none;
pointer-events: none;
}
/* Figma 用同一背景节点的三段蒙版拼接长页;这里保留原始裁切坐标,而不是把根 Frame 当整页图片。 */
.background-segment-mid {
top: 1311px;
height: 405px;
z-index: 0;
}
.background-segment-mid img {
top: -1168px;
}
.background-segment-top {
top: 0;
height: 1588px;
z-index: 1;
}
.background-segment-top img {
top: 0;
}
.background-segment-bottom {
top: var(--fami-background-bottom-top);
height: 578px;
z-index: 2;
}
.background-segment-bottom img {
top: -1333px;
}
.countdown,
.profile-card,
.reward-sections,
.page-status {
position: absolute;
z-index: 3;
}
.countdown {
left: 90px;
top: 696px;
width: 900px;
height: 393px;
}
.countdown-frame {
position: absolute;
inset: 0;
width: 900px;
height: 393px;
pointer-events: none;
}
.count-label,
.count-value {
position: absolute;
margin: 0;
transform: translateX(-50%);
text-align: center;
white-space: nowrap;
}
.count-label {
top: 280px;
width: 180px;
height: 24px;
color: #050300;
font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
font-size: 24px;
font-weight: 400;
line-height: 24px;
text-transform: capitalize;
}
.count-value {
display: none;
top: 142px;
width: 150px;
height: 92px;
color: #f6e0b2;
font-size: 68px;
font-weight: 500;
line-height: 92px;
text-shadow: 0 3px 8px rgba(0, 0, 0, 0.72);
}
.countdown.has-values .count-value {
display: block;
}
.count-label-days,
.count-value-days {
left: 145px;
}
.count-label-hours,
.count-value-hours {
left: 349px;
}
.count-label-minutes,
.count-value-minutes {
left: 553.5px;
}
.count-label-seconds,
.count-value-seconds {
left: 753.5px;
}
.profile-card {
left: 90px;
top: 1089px;
width: 900px;
height: 679px;
}
.profile-frame {
position: absolute;
inset: 0;
width: 900px;
height: 679px;
pointer-events: none;
}
.profile-avatar {
position: absolute;
left: 309px;
top: 134px;
width: 282px;
height: 270px;
border-radius: 50%;
object-fit: cover;
}
.profile-name,
.profile-id,
.profile-recharge {
position: absolute;
left: 100px;
width: 700px;
margin: 0;
overflow: hidden;
color: #f6e0b2;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.profile-name {
top: 425px;
height: 36px;
font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
font-size: 36px;
font-weight: 400;
line-height: 36px;
}
.profile-id {
top: 476px;
height: 36px;
font-family: 'Fami Source Han Sans SC', Arial, sans-serif;
font-size: 24px;
font-weight: 400;
line-height: 36px;
}
.profile-recharge {
top: 520px;
height: 40px;
font-size: 36px;
font-weight: 500;
line-height: 36px;
}
.reward-sections {
width: 1080px;
height: var(--fami-stage-height);
}
.reward-section {
position: absolute;
left: 0;
top: var(--section-top);
width: 1080px;
height: 720px;
}
.reward-panel {
position: absolute;
left: 90px;
top: 0;
width: 900px;
height: 689px;
pointer-events: none;
}
.reward-title {
position: absolute;
left: 529.5px;
top: 95px;
margin: 0;
transform: translateX(-50%);
color: #f6e0b2;
font-size: 40px;
font-weight: 500;
line-height: 40px;
text-align: center;
white-space: nowrap;
}
.reward-section.is-following .reward-title {
left: 559.5px;
top: 99px;
}
.reward-card {
position: absolute;
width: 229px;
height: 258px;
color: #f6e0b2;
text-align: center;
}
.reward-card-frame {
position: absolute;
inset: 0;
width: 229px;
height: 258px;
pointer-events: none;
}
.reward-media {
position: absolute;
left: 18px;
top: 19px;
width: 193px;
height: 193px;
object-fit: contain;
}
.reward-section.is-following .reward-media {
left: 18.5px;
top: 21px;
}
.reward-price {
position: absolute;
left: 0;
top: 202px;
width: 229px;
height: 32px;
overflow: hidden;
color: #f6e0b2;
font-size: 32px;
font-weight: 500;
line-height: 32px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.reward-action {
position: absolute;
left: 341px;
top: 591px;
width: 399px;
height: 129px;
}
.reward-section.is-following .reward-action {
top: 604px;
}
.reward-action-skin {
position: absolute;
inset: 0;
width: 399px;
height: 129px;
pointer-events: none;
}
.reward-action-label {
position: absolute;
left: 0;
top: 45px;
width: 399px;
height: 40px;
overflow: hidden;
color: #f6e0b2;
font-family:
'SF Pro Display',
'SF Pro Text',
-apple-system,
BlinkMacSystemFont,
Arial,
sans-serif;
font-size: 40px;
font-weight: 510;
font-variation-settings:
'wght' 510,
'wdth' 100;
line-height: 40px;
text-align: center;
text-overflow: ellipsis;
white-space: nowrap;
}
.page-status {
left: 190px;
top: 1730px;
width: 700px;
margin: 0;
color: #f6e0b2;
font-size: 28px;
line-height: 40px;
text-align: center;
}
@media (min-width: 431px) {
html,
body {
background: #05000e;
}
}
@media (prefers-reduced-motion: reduce) {
*,
*::before,
*::after {
scroll-behavior: auto !important;
}
}

View File

@ -0,0 +1,495 @@
(function () {
'use strict';
var DESIGN_WIDTH = 1080;
var BASE_STAGE_HEIGHT = 3273;
var SECTION_STEP = 720;
var MAX_VIEWPORT_WIDTH = 430;
var STATUS_PATH = '/api/v1/activities/cumulative-recharge-reward';
var appViewport = document.getElementById('appViewport');
var stageWrap = document.getElementById('stageWrap');
var stage = document.getElementById('stage');
var countdown = document.getElementById('countdown');
var rewardSections = document.getElementById('rewardSections');
var profileName = document.getElementById('profileName');
var profileID = document.getElementById('profileID');
var profileRecharge = document.getElementById('profileRecharge');
var profileAvatar = document.getElementById('profileAvatar');
var pageStatus = document.getElementById('pageStatus');
var countNodes = {
days: document.querySelector('[data-count="days"]'),
hours: document.querySelector('[data-count="hours"]'),
minutes: document.querySelector('[data-count="minutes"]'),
seconds: document.querySelector('[data-count="seconds"]'),
};
var currentData = null;
var designHeight = BASE_STAGE_HEIGHT;
var countdownEndAt = 0;
var timeOffsetMS = 0;
var countdownTimer = 0;
function queryParams() {
return new URLSearchParams(window.location.search || '');
}
function shouldUseMock(params) {
var value = String(params.get('mock') || '').toLowerCase();
// 设计数据只允许显式 mock=1/true生产接口失败时不得回退到看似可用的虚假奖励。
return value === '1' || value === 'true';
}
function t(key, fallback) {
return window.HyAppI18n && typeof window.HyAppI18n.t === 'function'
? window.HyAppI18n.t(key, fallback)
: fallback || key;
}
function escapeHTML(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function firstValue(source, keys, fallback) {
source = source && typeof source === 'object' ? source : {};
for (var index = 0; index < keys.length; index += 1) {
var value = source[keys[index]];
if (value !== undefined && value !== null && value !== '')
return value;
}
return fallback;
}
function numberValue(source, keys, fallback) {
var value = Number(firstValue(source, keys, fallback));
return Number.isFinite(value) ? value : Number(fallback || 0);
}
function wholeUSD(minor) {
var value = Math.max(0, Number(minor || 0)) / 100;
return value % 1 === 0
? String(value)
: value.toFixed(2).replace(/0+$/, '').replace(/\.$/, '');
}
function tierAmount(minor) {
return '$' + wholeUSD(minor);
}
function twoDigits(value) {
return String(Math.max(0, Math.floor(value))).padStart(2, '0');
}
function updateScale() {
var viewportWidth = Math.min(
window.innerWidth || 375,
MAX_VIEWPORT_WIDTH
);
var scale = viewportWidth / DESIGN_WIDTH;
var renderedHeight = designHeight * scale;
document.documentElement.style.setProperty(
'--fami-stage-scale',
String(scale)
);
document.documentElement.style.setProperty(
'--fami-stage-height',
designHeight + 'px'
);
document.documentElement.style.setProperty(
'--fami-background-bottom-top',
designHeight - 578 + 'px'
);
appViewport.style.minHeight = renderedHeight + 'px';
stageWrap.style.height = renderedHeight + 'px';
stage.style.height = designHeight + 'px';
}
function mockData(params) {
var rechargeValue = Number(params.get('recharge') || 5998);
var explicitEnd = Date.parse(
params.get('end_at') || params.get('endAt') || ''
);
var rewardItem = {
item_type: 'resource',
resource: {
name: 'Figma reward fixture',
preview_url: './fami/assets/mock-reward-822-2971.png',
},
};
return {
enabled: true,
user: {
username: params.get('name') || 'nanfangjian',
display_user_id:
params.get('uid') || params.get('id') || '12345678',
avatar:
params.get('avatar') ||
'./fami/assets/mock-avatar-822-2960.png',
},
period: {
// Figma 节点没有倒计时数字;只有显式 end_at 才展示动态数字,避免凭空猜一个验收值。
end_ms: Number.isFinite(explicitEnd) ? explicitEnd : 0,
},
progress: {
total_usd_minor: Math.max(0, Math.round(rechargeValue * 100)),
},
tiers: [
{
tier_id: 1,
threshold_usd_minor: 10000,
reached: true,
resource_group: { items: [rewardItem, rewardItem] },
},
{
tier_id: 2,
threshold_usd_minor: 10000,
reached: true,
resource_group: {
items: [rewardItem, rewardItem, rewardItem],
},
},
],
grants: [],
server_time_ms: Date.now(),
};
}
function resourcePreview(item) {
var resource = (item && item.resource) || {};
// preview_url 是 gateway 明确下发给静态 H5 的封面asset_url 可能是动画,仅作为旧数据兜底。
return String(
firstValue(
resource,
[
'preview_url',
'previewUrl',
'asset_url',
'assetUrl',
'animation_url',
'animationUrl',
],
''
) || ''
);
}
function normalizeData(payload) {
payload = payload && payload.status ? payload.status : payload || {};
var config = payload.config || {};
var rawTiers = Array.isArray(payload.tiers)
? payload.tiers
: Array.isArray(config.tiers)
? config.tiers
: [];
var grants = Array.isArray(payload.grants) ? payload.grants : [];
var grantByTier = {};
grants.forEach(function (grant) {
var tierID = String(firstValue(grant, ['tier_id', 'tierId'], ''));
if (tierID) grantByTier[tierID] = grant;
});
var tiers = rawTiers
.slice()
.sort(function (a, b) {
return (
numberValue(a, ['sort_order', 'sortOrder'], 0) -
numberValue(b, ['sort_order', 'sortOrder'], 0)
);
})
.map(function (tier) {
var group = tier.resource_group || tier.resourceGroup || {};
var items = Array.isArray(group.items) ? group.items : [];
var tierID = String(
firstValue(tier, ['tier_id', 'tierId'], '')
);
var grant = tier.grant || grantByTier[tierID] || null;
return {
tierID: tierID,
thresholdUSDMinor: numberValue(
tier,
['threshold_usd_minor', 'thresholdUsdMinor'],
0
),
reached: Boolean(firstValue(tier, ['reached'], false)),
grant: grant,
cards: items.slice(0, 3).map(function (item) {
return {
name: firstValue(
(item && item.resource) || {},
['name'],
''
),
preview: resourcePreview(item),
};
}),
};
});
return {
enabled:
firstValue(
payload,
['enabled'],
firstValue(config, ['enabled'], true)
) !== false,
user: payload.user || {},
period: payload.period || {},
progress: payload.progress || {},
serverTimeMS: numberValue(
payload,
['server_time_ms', 'serverTimeMs'],
Date.now()
),
tiers: tiers,
};
}
function grantWasIssued(grant) {
if (!grant) return false;
var status = String(firstValue(grant, ['status'], '')).toLowerCase();
return (
['granted', 'success', 'succeeded', 'completed'].indexOf(status) >=
0
);
}
function cardPositions(count) {
if (count <= 1) return [425.5];
if (count === 2) return [201, 650];
return [160.5, 429.5, 698.5];
}
function renderCard(card, section, cardIndex, left) {
var media = card.preview
? '<img class="reward-media" src="' +
escapeHTML(card.preview) +
'" alt="' +
escapeHTML(card.name) +
'" />'
: '';
return [
'<article class="reward-card" style="left:',
left,
'px;top:',
section.cardTop,
'px" data-card-index="',
cardIndex,
'">',
'<img class="reward-card-frame" src="./fami/assets/reward-card-822-2969.png" alt="" data-node-id="822:2969" />',
media,
'<span class="reward-price">',
escapeHTML(tierAmount(section.thresholdUSDMinor)),
'</span>',
'</article>',
].join('');
}
function renderSections(tiers) {
designHeight = Math.max(
BASE_STAGE_HEIGHT,
BASE_STAGE_HEIGHT + Math.max(0, tiers.length - 2) * SECTION_STEP
);
rewardSections.innerHTML = tiers
.map(function (tier, sectionIndex) {
var section = {
thresholdUSDMinor: tier.thresholdUSDMinor,
cardTop: sectionIndex === 0 ? 293 : 278,
};
var positions = cardPositions(tier.cards.length);
var actionKey = grantWasIssued(tier.grant)
? 'rechargeReward.claimed'
: 'rechargeReward.receive';
var actionFallback = grantWasIssued(tier.grant)
? 'Claimed'
: 'Receive';
return [
'<section class="reward-section',
sectionIndex > 0 ? ' is-following' : '',
'" style="--section-top:',
1776 + sectionIndex * SECTION_STEP,
'px" data-tier-id="',
escapeHTML(tier.tierID),
'" data-node-id="',
sectionIndex === 0 ? '822:2965' : '822:2976',
'">',
'<img class="reward-panel" src="./fami/assets/reward-panel-822-2967.png" alt="" data-node-id="822:2967" />',
'<h2 class="reward-title">',
escapeHTML(
t(
'rechargeReward.rechargeTitle',
'Recharge {amount}'
).replace(
'{amount}',
tierAmount(tier.thresholdUSDMinor)
)
),
'</h2>',
tier.cards
.map(function (card, cardIndex) {
return renderCard(
card,
section,
cardIndex,
positions[cardIndex]
);
})
.join(''),
// 累充奖励由 wallet 充值事件自动发放gateway 没有 claim RPC设计中的 Receive 仅显示服务端状态,禁止伪造领取请求。
'<div class="reward-action" role="status" aria-label="',
escapeHTML(t(actionKey, actionFallback)),
'" data-node-id="',
sectionIndex === 0 ? '822:2991' : '822:2990',
'">',
'<img class="reward-action-skin" src="./fami/assets/receive-skin-822-2990.png" alt="" />',
'<span class="reward-action-label">',
escapeHTML(t(actionKey, actionFallback)),
'</span>',
'</div>',
'</section>',
].join('');
})
.join('');
updateScale();
}
function renderProfile(data) {
var user = data.user || {};
var displayID = firstValue(
user,
['display_user_id', 'displayUserId', 'user_id', 'userId'],
''
);
var name = firstValue(user, ['username', 'name'], '');
var avatar = String(
firstValue(user, ['avatar', 'avatar_url', 'avatarUrl'], '') || ''
);
var totalMinor = numberValue(
data.progress || {},
['total_usd_minor', 'totalUsdMinor'],
0
);
profileName.textContent = name;
profileID.textContent = displayID
? t('rechargeReward.idPrefix', 'ID:') + ' ' + displayID
: '';
profileRecharge.textContent =
t('rechargeReward.accumulatedRecharge', 'Accumulated Recharge:') +
wholeUSD(totalMinor);
if (avatar) {
profileAvatar.src = avatar;
profileAvatar.hidden = false;
} else {
profileAvatar.removeAttribute('src');
profileAvatar.hidden = true;
}
}
function updateCountdown() {
if (!countdownEndAt) {
countdown.classList.remove('has-values');
Object.keys(countNodes).forEach(function (key) {
countNodes[key].textContent = '';
});
return;
}
var remaining = Math.max(
0,
countdownEndAt - (Date.now() - timeOffsetMS)
);
var totalSeconds = Math.floor(remaining / 1000);
countNodes.days.textContent = twoDigits(totalSeconds / 86400);
countNodes.hours.textContent = twoDigits((totalSeconds % 86400) / 3600);
countNodes.minutes.textContent = twoDigits((totalSeconds % 3600) / 60);
countNodes.seconds.textContent = twoDigits(totalSeconds % 60);
countdown.classList.add('has-values');
}
function render(data) {
currentData = normalizeData(data);
renderProfile(currentData);
renderSections(currentData.tiers);
timeOffsetMS = Date.now() - currentData.serverTimeMS;
countdownEndAt = numberValue(
currentData.period,
['end_ms', 'endMs'],
0
);
updateCountdown();
if (countdownTimer) window.clearInterval(countdownTimer);
if (countdownEndAt)
countdownTimer = window.setInterval(updateCountdown, 1000);
pageStatus.hidden = true;
pageStatus.textContent = '';
stage.setAttribute('aria-busy', 'false');
}
function loadStatus(params) {
if (shouldUseMock(params)) return Promise.resolve(mockData(params));
if (!window.HyAppAPI || typeof window.HyAppAPI.get !== 'function') {
return Promise.reject(new Error('api_unavailable'));
}
return window.HyAppAPI.get(STATUS_PATH);
}
function showLoadError(error) {
console.warn('[recharge-reward/fami] status load failed', error);
currentData = null;
rewardSections.innerHTML = '';
profileName.textContent = '';
profileID.textContent = '';
profileRecharge.textContent = '';
profileAvatar.hidden = true;
pageStatus.textContent = t('rechargeReward.loadFailed', 'Load failed');
pageStatus.hidden = false;
stage.setAttribute('aria-busy', 'false');
}
function init() {
var params = queryParams();
document.title = t('rechargeReward.pageTitle', 'Recharge Reward');
updateScale();
loadStatus(params).then(render).catch(showLoadError);
window.addEventListener('resize', updateScale);
window.addEventListener('hyapp:i18n-ready', function () {
document.title = t('rechargeReward.pageTitle', 'Recharge Reward');
if (currentData) {
// 语言包只影响 DOM 文案;复用已归一化的业务状态,不能再次按原始 API 结构解析而丢失奖励卡。
renderProfile(currentData);
renderSections(currentData.tiers);
updateCountdown();
} else if (!pageStatus.hidden)
pageStatus.textContent = t(
'rechargeReward.loadFailed',
'Load failed'
);
});
// 远程头像或奖励封面失效时只隐藏业务媒体,装饰框和真实金额状态仍保留,避免回退到伪造素材。
stage.addEventListener(
'error',
function (event) {
if (
event.target === profileAvatar ||
(event.target &&
event.target.classList.contains('reward-media'))
) {
event.target.hidden = true;
}
},
true
);
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init, { once: true });
} else {
init();
}
})();

Some files were not shown because too many files have changed in this diff Show More