积分收益政策h5
This commit is contained in:
parent
f457803881
commit
94562110bb
@ -256,7 +256,20 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
appCode = String(appCode || 'lalu')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
return appCode === 'yumi' || appCode === 'aslan' ? appCode : 'lalu';
|
||||
// App 身份统一保留在公共主题根节点;未定义专属变量的 Fami/Huwaa 会自然继承 :root 浅紫主题,
|
||||
// 后续增加品牌覆盖时无需再改 API/环境解析逻辑。未知 app 仍回退 Lalu,保护旧页面。
|
||||
return ['lalu', 'fami', 'huwaa', 'yumi', 'aslan'].indexOf(appCode) >= 0
|
||||
? appCode
|
||||
: 'lalu';
|
||||
}
|
||||
|
||||
function pageDefaultAppCode() {
|
||||
// 新的多产品 H5 入口会在 common/api.js 执行前声明页面所属 App。页面级默认值只在 URL
|
||||
// 未显式携带 app_code 时生效,既避免 Fami 页面因客户端漏参串到 Lalu,也不改变历史页面
|
||||
// “无参数即 Lalu”的兼容约定。该值不从 localStorage 读取,防止前一次访问污染下一产品。
|
||||
return String(window.HyAppDefaultAppCode || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
}
|
||||
|
||||
function requestAppCode() {
|
||||
@ -265,7 +278,7 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
.toLowerCase();
|
||||
// X-App-Code 只表达本次 URL 显式指定的后端租户,不能复用 getAppCode 的内存或 localStorage 结果。
|
||||
// 这样没有 app_code 参数的入口会稳定落到 lalu,避免历史主题缓存把后续普通页面继续打到 yumi/aslan。
|
||||
return queryAppCode || 'lalu';
|
||||
return queryAppCode || pageDefaultAppCode() || 'lalu';
|
||||
}
|
||||
|
||||
function applyAppTheme(appCode) {
|
||||
@ -280,6 +293,7 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
var queryAppCode =
|
||||
readQuery('app') || readQuery('app_code') || readQuery('appCode');
|
||||
if (queryAppCode) return queryAppCode;
|
||||
if (pageDefaultAppCode()) return pageDefaultAppCode();
|
||||
if (memoryAppCode) return memoryAppCode;
|
||||
if (!shouldPersist()) return 'lalu';
|
||||
return window.localStorage.getItem(APP_CODE_KEY) || 'lalu';
|
||||
@ -295,6 +309,11 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
applyAppTheme(queryAppCode);
|
||||
return queryAppCode;
|
||||
}
|
||||
if (pageDefaultAppCode()) {
|
||||
memoryAppCode = pageDefaultAppCode();
|
||||
applyAppTheme(memoryAppCode);
|
||||
return memoryAppCode;
|
||||
}
|
||||
if (memoryAppCode) {
|
||||
applyAppTheme(memoryAppCode);
|
||||
return memoryAppCode;
|
||||
@ -632,6 +651,34 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
},
|
||||
};
|
||||
|
||||
// POINT 通用钱包的汇率、手续费和币商白名单全部由服务端快照返回;页面不持有资金参数默认值。
|
||||
var pointWalletAPI = {
|
||||
overview: function () {
|
||||
return request('/api/v1/point-wallet/overview', {
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
withdrawUSDT: function (payload) {
|
||||
return request('/api/v1/point-wallet/withdraw', {
|
||||
method: 'POST',
|
||||
body: payload || {},
|
||||
});
|
||||
},
|
||||
transferToCoinSeller: function (payload) {
|
||||
return request('/api/v1/point-wallet/transfer-to-coin-seller', {
|
||||
method: 'POST',
|
||||
body: payload || {},
|
||||
});
|
||||
},
|
||||
history: function (pageSize) {
|
||||
return walletAPI.transactions({
|
||||
asset_type: 'POINT',
|
||||
page: 1,
|
||||
page_size: pageSize || 30,
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
// H5 充值 API 只做路径和参数名封装;token、app_code、env、错误 envelope 仍由 request 统一处理。
|
||||
// 金额、金币数、普通/币商钱包、三方支付参数和 USDT 链上校验都以后端订单快照为准。
|
||||
function isAslanWebPay() {
|
||||
@ -1438,6 +1485,13 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
stats: function (query) {
|
||||
// 公会统计只接受自然日范围;收益、分成和主播在线数据均由 owner service 聚合。
|
||||
return request('/api/v1/agency-center/stats', {
|
||||
method: 'GET',
|
||||
query: query || {},
|
||||
});
|
||||
},
|
||||
applications: function (status) {
|
||||
return request('/api/v1/agency-center/applications', {
|
||||
method: 'GET',
|
||||
@ -1480,6 +1534,13 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
method: 'GET',
|
||||
});
|
||||
},
|
||||
stats: function (query) {
|
||||
// Fami 新主播中心按任意自然日区间读取服务端聚合;前端只传日期,不上传时长或收益值。
|
||||
return request('/api/v1/host-center/stats', {
|
||||
method: 'GET',
|
||||
query: query || {},
|
||||
});
|
||||
},
|
||||
platformPolicy: function () {
|
||||
return request('/api/v1/host-center/platform-policy', {
|
||||
method: 'GET',
|
||||
@ -1531,6 +1592,12 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
query: { page_size: pageSize || 20 },
|
||||
});
|
||||
},
|
||||
stats: function (query) {
|
||||
return request('/api/v1/bd-center/stats', {
|
||||
method: 'GET',
|
||||
query: query || {},
|
||||
});
|
||||
},
|
||||
inviteAgency: function (payload) {
|
||||
return request('/api/v1/bd/invitations/agency', {
|
||||
method: 'POST',
|
||||
@ -2056,6 +2123,7 @@ var default_api = 'https://api.global-interaction.com/';
|
||||
vip: vipAPI,
|
||||
user: userAPI,
|
||||
wallet: walletAPI,
|
||||
pointWallet: pointWalletAPI,
|
||||
recharge: rechargeAPI,
|
||||
salaryWallet: salaryWalletAPI,
|
||||
host: hostAPI,
|
||||
|
||||
@ -109,18 +109,52 @@
|
||||
return DEFAULT_LANG;
|
||||
}
|
||||
|
||||
function load(lang) {
|
||||
var normalized = normalizeLang(lang) || DEFAULT_LANG;
|
||||
function localeSources() {
|
||||
var sources = [localeBaseURL];
|
||||
var extras = Array.isArray(window.HyAppI18nExtraSources)
|
||||
? window.HyAppI18nExtraSources
|
||||
: [];
|
||||
extras.forEach(function (source) {
|
||||
var normalized = String(source || '').trim();
|
||||
if (!normalized) return;
|
||||
if (normalized.charAt(normalized.length - 1) !== '/') {
|
||||
normalized += '/';
|
||||
}
|
||||
if (sources.indexOf(normalized) < 0) sources.push(normalized);
|
||||
});
|
||||
return sources;
|
||||
}
|
||||
|
||||
function fetchLocale(source, lang) {
|
||||
return window
|
||||
.fetch(localeBaseURL + normalized + '.json', {
|
||||
cache: 'no-store',
|
||||
})
|
||||
.fetch(source + lang + '.json', { cache: 'no-store' })
|
||||
.then(function (response) {
|
||||
if (!response.ok) throw new Error('locale_not_found');
|
||||
return response.json();
|
||||
})
|
||||
.then(function (json) {
|
||||
messages = json || {};
|
||||
.catch(function () {
|
||||
// 产品语言包是可选叠加层;单个包发布延迟不能让 common 基础文案也一起失效。
|
||||
return null;
|
||||
});
|
||||
}
|
||||
|
||||
function load(lang) {
|
||||
var normalized = normalizeLang(lang) || DEFAULT_LANG;
|
||||
return Promise.all(
|
||||
localeSources().map(function (source) {
|
||||
return fetchLocale(source, normalized);
|
||||
})
|
||||
)
|
||||
.then(function (localeMessages) {
|
||||
var found = false;
|
||||
messages = {};
|
||||
localeMessages.forEach(function (sourceMessages) {
|
||||
if (!sourceMessages) return;
|
||||
found = true;
|
||||
// 产品包后加载并覆盖同名 key,允许品牌化文案,但 common 仍提供跨页面兜底。
|
||||
Object.assign(messages, sourceMessages);
|
||||
});
|
||||
if (!found) throw new Error('locale_not_found');
|
||||
currentLang = normalized;
|
||||
storageSet(STORAGE_KEY, currentLang);
|
||||
apply();
|
||||
|
||||
95
guild/ARCHITECTURE.md
Normal file
95
guild/ARCHITECTURE.md
Normal file
@ -0,0 +1,95 @@
|
||||
# Guild H5 architecture
|
||||
|
||||
## Decision
|
||||
|
||||
The existing `gonghui/` tree is the Lalu legacy product and remains independently deployable. New guild products use a zero-build, static multi-package architecture under `guild/`:
|
||||
|
||||
```text
|
||||
guild/
|
||||
├── packages/
|
||||
│ ├── core/ # Product context, date ranges, errors, state machines
|
||||
│ ├── ui/ # Product-neutral center layouts and components
|
||||
│ ├── host-center/ # Host feature package (added with the first host screen)
|
||||
│ ├── agency-center/ # Agency feature package
|
||||
│ ├── bd-center/ # BD feature package
|
||||
│ └── wallet/ # Shared coin-seller and USDT withdrawal flow
|
||||
├── products/
|
||||
│ └── fami/ # Fami manifest, assets and visual overrides
|
||||
└── fami/
|
||||
├── host-center/ # Thin deployable page entry
|
||||
├── agency-center/
|
||||
├── bd-center/
|
||||
└── wallet/
|
||||
```
|
||||
|
||||
Each page entry composes packages with ordered classic `<script>` and `<link>` tags. There is deliberately no bundling step: the current repository is deployed as static files, so a build-system failure must not take down existing H5 pages.
|
||||
|
||||
## Package boundaries
|
||||
|
||||
| Package | Owns | Must not own |
|
||||
| -------------------------- | ------------------------------------------------------------------------------------- | -------------------------------------------- |
|
||||
| `common/` | Existing cross-H5 API transport, environment selection, i18n and base theme | Guild role semantics or Fami page state |
|
||||
| `guild/packages/core` | Product context, shared time range model, request DTO mapping, async state primitives | DOM layout and product colors |
|
||||
| `guild/packages/ui` | Responsive cards, filters, tables, empty/loading/error presentation | API paths or role permissions |
|
||||
| `guild/packages/<feature>` | One role/flow's state, API adapter and render controller | Fami-only text, assets or hardcoded app code |
|
||||
| `guild/products/fami` | `app_code=fami`, feature flags, Fami assets and permitted visual overrides | Shared business calculations |
|
||||
| `guild/fami/<feature>` | HTML composition and page-only wiring | Reusable business logic |
|
||||
|
||||
## Runtime composition
|
||||
|
||||
Every Fami page must load scripts in this order:
|
||||
|
||||
```html
|
||||
<script src="../../packages/core/product-context.js"></script>
|
||||
<script src="../../products/fami/product.js"></script>
|
||||
<script src="../../../common/api.js"></script>
|
||||
<script src="../../../common/params.js"></script>
|
||||
<script src="../../../common/i18n.js"></script>
|
||||
```
|
||||
|
||||
`product.js` runs before `common/api.js`, making Fami the page-level default tenant even when an older client omits `app_code`. An explicit URL `app_code` still wins for diagnostics. Pages under the old `gonghui/` tree load none of these files, so their default remains Lalu.
|
||||
|
||||
## Shared domain rules from the initial specification
|
||||
|
||||
- Host, agency and BD statistics consume one date-range model: today, yesterday, last 7 days, last 30 days, this month, last month and custom range.
|
||||
- Metrics are response-driven. The frontend renders values and definitions but does not recompute diamonds, gift senders, online duration, valid live duration/days, private-message users or new followers.
|
||||
- Agency and BD list rows use stable IDs as keys. Removing a host or agency updates the view only after the server confirms the command.
|
||||
- Wallet is a separate feature package because host, agency and BD reuse it. Coin-seller withdrawal and USDT withdrawal are separate state machines with separate validation, idempotency command IDs and histories.
|
||||
- Permissions and feature switches come from backend capabilities. Functional switches default to enabled; only external addresses, secrets and third-party credentials may require environment configuration.
|
||||
|
||||
## Backend/admin contract direction
|
||||
|
||||
New endpoints should be app-scoped through `X-App-Code: fami` and return server-calculated metrics. Prefer additive `/api/v1/guild/...` contracts or additive fields on existing center endpoints; do not change Lalu response meaning in place.
|
||||
|
||||
The admin coin-seller withdrawal whitelist belongs to backend/admin storage. The H5 only consumes eligible coin sellers and must never treat a locally cached list as authorization.
|
||||
|
||||
### Implemented host statistics contract
|
||||
|
||||
`GET /api/v1/host-center/stats?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` returns an inclusive UTC natural-day range and these server-owned metrics:
|
||||
|
||||
- `diamond_earnings`, `diamond_exchanged`, `gift_senders` from wallet-service POINT ledger entries;
|
||||
- `online_duration_ms` from App session heartbeat spans, merged per day so overlapping sessions are not double-counted;
|
||||
- `valid_mic_duration_ms`, `valid_mic_days` from user-service microphone daily facts;
|
||||
- `private_message_senders` from authenticated Tencent IM `C2C.CallbackAfterSendMsg` events;
|
||||
- `new_followers` from follow/unfollow delta events.
|
||||
|
||||
The Tencent C2C callback URL must include the matching product context, for example `app_code=fami`. The callback stores only event ID, sender, receiver and event time; message content is never written to the statistics table.
|
||||
|
||||
### Implemented Agency contract and revenue policy
|
||||
|
||||
`GET /api/v1/agency-center/stats?start_date=YYYY-MM-DD&end_date=YYYY-MM-DD` returns the Agency summary and current host rows for the same inclusive UTC date range. Host earnings come from wallet POINT entries; engagement metrics come from user-service facts.
|
||||
|
||||
Agency share is configured in the shared Admin revenue-policy template JSON as `agency.point_ratio_percent`. The common legacy template defaults to `0`; the seeded `fami_guild_revenue_policy` template defaults to `20`. Publishing an App policy compiles the value into `wallet_policy_instances.agency_point_ratio_ppm`. Each successful gift atomically credits the owner POINT account and writes `agency_point_share_entries`, including the host, owner, ratio and policy instance snapshots. Historical earnings therefore do not move when a host later changes Agency.
|
||||
|
||||
### Implemented BD and wallet contracts
|
||||
|
||||
`GET /api/v1/bd-center/stats` aggregates the current BD's direct Agencies, active host counts and host gift POINT income without including Agency-share POINT. The page reuses the same date-range model and invitation acceptance flow as the existing organization domain.
|
||||
|
||||
The Fami wallet is a shared `guild/packages/wallet` feature mounted at `guild/fami/wallet/`. `GET /api/v1/point-wallet/overview` returns the POINT balance, published policy exchange/fee snapshot and the country-filtered coin-seller whitelist. POINT-to-seller transfers never accept an exchange ratio from H5: wallet-service locks `point_withdrawal_coin_seller_configs` and atomically debits `POINT` while crediting `COIN_SELLER_COIN`. Admin edits the same app-scoped table from `/host/point-withdrawal-config`; switching the selected App supports Fami, Huwaa, Lalu and future products without adding boolean columns.
|
||||
|
||||
## Adding another guild product
|
||||
|
||||
1. Add `guild/products/<product>/product.js` and optional assets/theme overrides.
|
||||
2. Add thin entries under `guild/<product>/<feature>/`.
|
||||
3. Reuse a feature package when its API/state semantics match; add a product adapter when DTOs differ.
|
||||
4. Run `npm run test:guild-architecture` to prove the new default app code does not change legacy Lalu requests.
|
||||
12
guild/fami/README.md
Normal file
12
guild/fami/README.md
Normal file
@ -0,0 +1,12 @@
|
||||
# Fami guild page entries
|
||||
|
||||
This product will expose four independent static entries:
|
||||
|
||||
- `host-center/`
|
||||
- `agency-center/`
|
||||
- `bd-center/`
|
||||
- `wallet/`
|
||||
|
||||
Entries are intentionally thin. Shared business logic belongs in `guild/packages/<feature>/`; Fami-specific configuration and visual overrides belong in `guild/products/fami/`.
|
||||
|
||||
Do not copy scripts from `gonghui/` wholesale. Reuse existing backend semantics through a feature adapter and keep any Fami response differences additive and explicit.
|
||||
422
guild/fami/agency-center/index.html
Normal file
422
guild/fami/agency-center/index.html
Normal file
@ -0,0 +1,422 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Agency Center</title>
|
||||
<link rel="stylesheet" href="../../../common/theme.css" />
|
||||
<link rel="stylesheet" href="../../packages/ui/center.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="../../packages/core/product-context.js"></script>
|
||||
<script src="../../products/fami/product.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="guild-center fami-agency-center">
|
||||
<header class="center-header">
|
||||
<button
|
||||
class="icon-button"
|
||||
id="backButton"
|
||||
type="button"
|
||||
data-i18n-aria="fami_common.back"
|
||||
aria-label="Back"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
</button>
|
||||
<h1 data-i18n="fami_agency.title">Agency Center</h1>
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
type="button"
|
||||
data-language-toggle
|
||||
data-current-lang
|
||||
aria-expanded="false"
|
||||
data-i18n-aria="fami_common.change_language"
|
||||
aria-label="Change language"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<div class="language-menu" data-language-menu hidden>
|
||||
<button type="button" data-lang-option="en">EN</button>
|
||||
<button type="button" data-lang-option="ar">AR</button>
|
||||
<button type="button" data-lang-option="tr">TR</button>
|
||||
<button type="button" data-lang-option="es">ES</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="guild-center__content" aria-live="polite">
|
||||
<section class="guild-card agency-profile-card">
|
||||
<div class="agency-avatar">
|
||||
<img id="agencyAvatar" alt="" hidden />
|
||||
<span id="agencyInitial" aria-hidden="true">A</span>
|
||||
</div>
|
||||
<div class="agency-profile-copy">
|
||||
<strong id="agencyName">—</strong>
|
||||
<span id="agencyID">Agency ID: —</span>
|
||||
<span id="createdAt">Created: —</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guild-card contact-card">
|
||||
<span class="contact-icon" aria-hidden="true">☎</span>
|
||||
<div>
|
||||
<strong data-i18n="fami_agency.contact">WhatsApp</strong
|
||||
><span id="contactValue">—</span>
|
||||
</div>
|
||||
<button
|
||||
id="editContactButton"
|
||||
type="button"
|
||||
data-i18n="fami_common.edit"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="guild-card balance-card">
|
||||
<div>
|
||||
<span
|
||||
class="card-label"
|
||||
data-i18n="fami_common.diamond_balance"
|
||||
>Diamond balance</span
|
||||
>
|
||||
<strong class="balance-value"
|
||||
><span aria-hidden="true">💎</span
|
||||
><span id="diamondBalance">—</span></strong
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="primary-action"
|
||||
id="withdrawButton"
|
||||
type="button"
|
||||
data-i18n="fami_common.withdraw"
|
||||
>
|
||||
Withdraw
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="data-section" aria-labelledby="dataTitle">
|
||||
<div class="section-title-row">
|
||||
<h2 id="dataTitle" data-i18n="fami_agency.data_title">
|
||||
Agency data
|
||||
</h2>
|
||||
<span id="rangeCaption"></span>
|
||||
</div>
|
||||
<div class="data-tabs" role="tablist">
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-tab="agency"
|
||||
aria-selected="true"
|
||||
data-i18n="fami_agency.tab_agency"
|
||||
>
|
||||
Agency data
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
role="tab"
|
||||
data-tab="hosts"
|
||||
aria-selected="false"
|
||||
data-i18n="fami_agency.tab_hosts"
|
||||
>
|
||||
Host data
|
||||
</button>
|
||||
</div>
|
||||
<div
|
||||
class="guild-filter-bar"
|
||||
id="rangeFilters"
|
||||
role="group"
|
||||
data-i18n-aria="fami_range.label"
|
||||
aria-label="Date range"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-range="today"
|
||||
data-i18n="fami_range.today"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="yesterday"
|
||||
data-i18n="fami_range.yesterday"
|
||||
>
|
||||
Yesterday
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_7_days"
|
||||
data-i18n="fami_range.last_7_days"
|
||||
>
|
||||
Last 7 days
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_30_days"
|
||||
data-i18n="fami_range.last_30_days"
|
||||
>
|
||||
Last 30 days
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="this_month"
|
||||
data-i18n="fami_range.this_month"
|
||||
>
|
||||
This month
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_month"
|
||||
data-i18n="fami_range.last_month"
|
||||
>
|
||||
Last month
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="custom"
|
||||
data-i18n="fami_range.custom"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats-status" id="statsStatus" hidden></div>
|
||||
|
||||
<div id="agencyPanel" role="tabpanel">
|
||||
<div class="guild-card summary-grid">
|
||||
<article>
|
||||
<strong id="totalEarnings">—</strong
|
||||
><span data-i18n="fami_agency.total_earnings"
|
||||
>Total earnings</span
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<strong id="giftIncome">—</strong
|
||||
><span data-i18n="fami_agency.gift_income"
|
||||
>Gift income</span
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<strong id="shareIncome">—</strong
|
||||
><span data-i18n="fami_agency.share_income"
|
||||
>Share income</span
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<strong id="totalHosts">—</strong
|
||||
><span data-i18n="fami_agency.total_hosts"
|
||||
>Total hosts</span
|
||||
>
|
||||
</article>
|
||||
<article>
|
||||
<strong id="giftedHosts">—</strong
|
||||
><span data-i18n="fami_agency.gifted_hosts"
|
||||
>Gifted hosts</span
|
||||
>
|
||||
</article>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div id="hostsPanel" role="tabpanel" hidden>
|
||||
<div class="host-panel-toolbar">
|
||||
<span id="hostCountLabel"></span>
|
||||
<button
|
||||
class="primary-action compact"
|
||||
id="addHostButton"
|
||||
type="button"
|
||||
>
|
||||
<span aria-hidden="true">+</span
|
||||
><span data-i18n="fami_agency.add_host"
|
||||
>Add host</span
|
||||
>
|
||||
</button>
|
||||
</div>
|
||||
<div class="guild-card host-table-shell">
|
||||
<table class="host-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="fami_agency.host">
|
||||
Host
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.diamond_earnings"
|
||||
>
|
||||
Diamond earnings
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.diamond_exchanged"
|
||||
>
|
||||
Diamonds exchanged
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.gift_senders"
|
||||
>
|
||||
Gift senders
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.online_duration"
|
||||
>
|
||||
Online duration
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.valid_mic_duration"
|
||||
>
|
||||
Valid mic duration
|
||||
</th>
|
||||
<th
|
||||
data-i18n="fami_metric.valid_mic_days"
|
||||
>
|
||||
Valid mic days
|
||||
</th>
|
||||
<th data-i18n="fami_agency.joined_at">
|
||||
Joined
|
||||
</th>
|
||||
<th data-i18n="fami_agency.action">
|
||||
Action
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="hostTableBody"></tbody>
|
||||
</table>
|
||||
<div
|
||||
class="empty-state"
|
||||
id="hostEmpty"
|
||||
hidden
|
||||
data-i18n="fami_agency.no_hosts"
|
||||
>
|
||||
No hosts yet.
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog class="edit-dialog" id="contactDialog">
|
||||
<form method="dialog" id="contactForm">
|
||||
<h2 data-i18n="fami_agency.edit_contact">Edit WhatsApp</h2>
|
||||
<label
|
||||
><span data-i18n="fami_agency.contact_number"
|
||||
>Contact number</span
|
||||
><input
|
||||
id="contactInput"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="tel"
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelContactButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_common.save"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="edit-dialog" id="rangeDialog">
|
||||
<form method="dialog" id="rangeForm">
|
||||
<h2 data-i18n="fami_range.custom_title">
|
||||
Custom date range
|
||||
</h2>
|
||||
<label
|
||||
><span data-i18n="fami_range.start">Start date</span
|
||||
><input id="rangeStart" type="date" required
|
||||
/></label>
|
||||
<label
|
||||
><span data-i18n="fami_range.end">End date</span
|
||||
><input id="rangeEnd" type="date" required
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelRangeButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_common.apply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="edit-dialog" id="inviteDialog">
|
||||
<form method="dialog" id="inviteForm">
|
||||
<h2 data-i18n="fami_agency.add_host">Add host</h2>
|
||||
<p data-i18n="fami_agency.invite_hint">
|
||||
Enter the host display ID. The host joins after
|
||||
accepting the invitation.
|
||||
</p>
|
||||
<label
|
||||
><span data-i18n="fami_agency.host_id">Host ID</span
|
||||
><input
|
||||
id="inviteHostID"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="32"
|
||||
required
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelInviteButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_agency.send_invite"
|
||||
>
|
||||
Send invite
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="edit-dialog" id="removeDialog">
|
||||
<form method="dialog" id="removeForm">
|
||||
<h2 data-i18n="fami_agency.remove_host">Remove host</h2>
|
||||
<p id="removeMessage"></p>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelRemoveButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="danger-action"
|
||||
type="submit"
|
||||
data-i18n="fami_agency.remove"
|
||||
>
|
||||
Remove
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script src="../../../common/api.js"></script>
|
||||
<script src="../../../common/params.js"></script>
|
||||
<script src="../../../common/i18n.js"></script>
|
||||
<script src="../../../common/jsbridge.js"></script>
|
||||
<script src="../../../common/toast.js"></script>
|
||||
<script src="../../packages/core/date-range.js"></script>
|
||||
<script src="../../packages/agency-center/agency-center.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
437
guild/fami/agency-center/script.js
Normal file
437
guild/fami/agency-center/script.js
Normal file
@ -0,0 +1,437 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var state = {
|
||||
range: null,
|
||||
data: null,
|
||||
tab: 'agency',
|
||||
loading: false,
|
||||
mutating: false,
|
||||
removingHost: null,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function t(key, fallback) {
|
||||
return window.HyAppI18n && window.HyAppI18n.t
|
||||
? window.HyAppI18n.t(key, fallback)
|
||||
: fallback || key;
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||||
}
|
||||
|
||||
function count(value) {
|
||||
if (value === null || value === undefined || !Number.isFinite(value)) {
|
||||
return '—';
|
||||
}
|
||||
return Number(value).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function duration(value) {
|
||||
if (!Number.isFinite(value)) return '—';
|
||||
var minutes = Math.max(0, Math.floor(Number(value) / 60000));
|
||||
var hours = Math.floor(minutes / 60);
|
||||
var rest = minutes % 60;
|
||||
if (!hours) return rest + t('fami_unit.minute_short', 'm');
|
||||
return (
|
||||
hours +
|
||||
t('fami_unit.hour_short', 'h') +
|
||||
(rest ? ' ' + rest + t('fami_unit.minute_short', 'm') : '')
|
||||
);
|
||||
}
|
||||
|
||||
function dateTime(value) {
|
||||
if (!Number(value)) return '—';
|
||||
return new Date(Number(value)).toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function renderIdentity() {
|
||||
if (!state.data) return;
|
||||
var agency = state.data.agency || {};
|
||||
$('agencyName').textContent = agency.name || '—';
|
||||
$('agencyID').textContent =
|
||||
t('fami_agency.id_prefix', 'Agency ID:') +
|
||||
' ' +
|
||||
(agency.shortId || agency.id || '—');
|
||||
$('createdAt').textContent =
|
||||
t('fami_agency.created_prefix', 'Created:') +
|
||||
' ' +
|
||||
dateTime(agency.createdAtMS);
|
||||
$('agencyInitial').textContent = String(agency.name || 'A')
|
||||
.trim()
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
$('agencyAvatar').hidden = !agency.avatar;
|
||||
$('agencyInitial').hidden = !!agency.avatar;
|
||||
if (agency.avatar) $('agencyAvatar').src = agency.avatar;
|
||||
$('contactValue').textContent = state.data.contact || '—';
|
||||
$('diamondBalance').textContent = count(state.data.pointBalance);
|
||||
}
|
||||
|
||||
function renderSummary() {
|
||||
var summary =
|
||||
state.data && state.data.stats
|
||||
? state.data.stats.summary || {}
|
||||
: {};
|
||||
$('totalEarnings').textContent = count(summary.total_earnings);
|
||||
$('giftIncome').textContent = count(summary.gift_income);
|
||||
$('shareIncome').textContent = count(summary.share_income);
|
||||
$('totalHosts').textContent = count(summary.total_hosts);
|
||||
$('giftedHosts').textContent = count(summary.gifted_host_count);
|
||||
var statsError = state.data && state.data.errors.stats;
|
||||
$('statsStatus').hidden = !statsError;
|
||||
if (statsError) {
|
||||
$('statsStatus').textContent = t(
|
||||
'fami_common.stats_unavailable',
|
||||
'Statistics are temporarily unavailable.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function hostAvatar(host) {
|
||||
var shell = document.createElement('span');
|
||||
shell.className = 'host-avatar';
|
||||
if (host.avatar) {
|
||||
var image = document.createElement('img');
|
||||
image.src = host.avatar;
|
||||
image.alt = '';
|
||||
shell.appendChild(image);
|
||||
} else {
|
||||
shell.textContent = String(host.name || 'H')
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
}
|
||||
return shell;
|
||||
}
|
||||
|
||||
function textCell(row, value) {
|
||||
var cell = document.createElement('td');
|
||||
cell.textContent = value;
|
||||
row.appendChild(cell);
|
||||
}
|
||||
|
||||
function renderHosts() {
|
||||
var hosts =
|
||||
state.data && state.data.stats ? state.data.stats.hosts || [] : [];
|
||||
var body = $('hostTableBody');
|
||||
body.textContent = '';
|
||||
hosts.forEach(function (host) {
|
||||
var row = document.createElement('tr');
|
||||
var identityCell = document.createElement('td');
|
||||
var identity = document.createElement('div');
|
||||
identity.className = 'host-identity';
|
||||
identity.appendChild(hostAvatar(host));
|
||||
var copy = document.createElement('span');
|
||||
copy.className = 'host-copy';
|
||||
var name = document.createElement('strong');
|
||||
name.textContent = host.name || '—';
|
||||
var id = document.createElement('span');
|
||||
id.textContent =
|
||||
t('fami_common.uid_prefix', 'UID:') +
|
||||
' ' +
|
||||
(host.displayUserId || host.userId || '—');
|
||||
copy.appendChild(name);
|
||||
copy.appendChild(id);
|
||||
identity.appendChild(copy);
|
||||
identityCell.appendChild(identity);
|
||||
row.appendChild(identityCell);
|
||||
textCell(row, count(host.diamondEarnings));
|
||||
textCell(row, count(host.diamondExchanged));
|
||||
textCell(row, count(host.giftSenders));
|
||||
textCell(row, duration(host.onlineDurationMS));
|
||||
textCell(row, duration(host.validMicDurationMS));
|
||||
textCell(row, count(host.validMicDays));
|
||||
textCell(row, dateTime(host.joinedAtMS));
|
||||
var actionCell = document.createElement('td');
|
||||
var remove = document.createElement('button');
|
||||
remove.type = 'button';
|
||||
remove.className = 'remove-button';
|
||||
if (host.removable) {
|
||||
remove.setAttribute('data-remove-host', host.userId);
|
||||
remove.textContent = t('fami_agency.remove', 'Remove');
|
||||
} else {
|
||||
remove.disabled = true;
|
||||
remove.textContent = t('fami_agency.owner', 'Owner');
|
||||
}
|
||||
actionCell.appendChild(remove);
|
||||
row.appendChild(actionCell);
|
||||
body.appendChild(row);
|
||||
});
|
||||
$('hostEmpty').hidden = hosts.length > 0;
|
||||
$('hostCountLabel').textContent = t(
|
||||
'fami_agency.host_count',
|
||||
'{count} hosts'
|
||||
).replace('{count}', count(hosts.length));
|
||||
}
|
||||
|
||||
function renderRange() {
|
||||
document.querySelectorAll('[data-range]').forEach(function (button) {
|
||||
button.setAttribute(
|
||||
'aria-pressed',
|
||||
button.getAttribute('data-range') === state.range.preset
|
||||
? 'true'
|
||||
: 'false'
|
||||
);
|
||||
button.disabled = state.loading;
|
||||
});
|
||||
$('rangeCaption').textContent =
|
||||
state.range.startDate === state.range.endDate
|
||||
? state.range.startDate
|
||||
: state.range.startDate + ' – ' + state.range.endDate;
|
||||
}
|
||||
|
||||
function renderTabs() {
|
||||
document.querySelectorAll('[data-tab]').forEach(function (button) {
|
||||
button.setAttribute(
|
||||
'aria-selected',
|
||||
button.getAttribute('data-tab') === state.tab ? 'true' : 'false'
|
||||
);
|
||||
});
|
||||
$('agencyPanel').hidden = state.tab !== 'agency';
|
||||
$('hostsPanel').hidden = state.tab !== 'hosts';
|
||||
$('dataTitle').textContent =
|
||||
state.tab === 'agency'
|
||||
? t('fami_agency.tab_agency', 'Agency data')
|
||||
: t('fami_agency.tab_hosts', 'Host data');
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderIdentity();
|
||||
renderSummary();
|
||||
renderHosts();
|
||||
renderRange();
|
||||
renderTabs();
|
||||
}
|
||||
|
||||
function setLoading(value) {
|
||||
state.loading = value;
|
||||
document.body.classList.toggle('is-loading', value);
|
||||
renderRange();
|
||||
}
|
||||
|
||||
function setMutating(value) {
|
||||
state.mutating = value;
|
||||
document.body.classList.toggle('is-mutating', value);
|
||||
}
|
||||
|
||||
function load() {
|
||||
setLoading(true);
|
||||
return window.HyGuild.agencyCenter
|
||||
.load(state.range)
|
||||
.then(function (data) {
|
||||
state.data = data;
|
||||
renderAll();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_agency.load_failed',
|
||||
'Unable to load Agency Center.'
|
||||
)
|
||||
);
|
||||
$('statsStatus').hidden = false;
|
||||
$('statsStatus').textContent = t(
|
||||
'fami_agency.load_failed',
|
||||
'Unable to load Agency Center.'
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function selectRange(preset) {
|
||||
if (preset === 'custom') {
|
||||
$('rangeStart').value = state.range.startDate;
|
||||
$('rangeEnd').value = state.range.endDate;
|
||||
$('rangeDialog').showModal();
|
||||
return;
|
||||
}
|
||||
state.range = window.HyGuild.dateRange.create(preset);
|
||||
renderRange();
|
||||
load();
|
||||
}
|
||||
|
||||
function applyCustomRange(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
state.range = window.HyGuild.dateRange.create('custom', {
|
||||
start: $('rangeStart').value,
|
||||
end: $('rangeEnd').value,
|
||||
});
|
||||
} catch (_) {
|
||||
toast(t('fami_range.invalid', 'Choose a valid date range.'));
|
||||
return;
|
||||
}
|
||||
$('rangeDialog').close();
|
||||
load();
|
||||
}
|
||||
|
||||
function saveContact(event) {
|
||||
event.preventDefault();
|
||||
if (state.mutating) return;
|
||||
var value = String($('contactInput').value || '').trim();
|
||||
if (!value) {
|
||||
toast(t('fami_agency.contact_required', 'Enter a contact number.'));
|
||||
return;
|
||||
}
|
||||
setMutating(true);
|
||||
window.HyGuild.agencyCenter
|
||||
.saveContact(value)
|
||||
.then(function () {
|
||||
state.data.contact = value;
|
||||
$('contactDialog').close();
|
||||
renderIdentity();
|
||||
toast(t('fami_agency.contact_saved', 'Contact saved.'));
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_agency.contact_save_failed',
|
||||
'Unable to save contact.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setMutating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function inviteHost(event) {
|
||||
event.preventDefault();
|
||||
if (state.mutating) return;
|
||||
var displayID = String($('inviteHostID').value || '').trim();
|
||||
if (!displayID) return;
|
||||
setMutating(true);
|
||||
window.HyGuild.agencyCenter
|
||||
.inviteHost(displayID)
|
||||
.then(function () {
|
||||
$('inviteDialog').close();
|
||||
toast(
|
||||
t(
|
||||
'fami_agency.invite_sent',
|
||||
'Invitation sent. The host must accept it.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(function () {
|
||||
toast(t('fami_agency.invite_failed', 'Unable to invite host.'));
|
||||
})
|
||||
.finally(function () {
|
||||
setMutating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function openRemove(hostUserID) {
|
||||
var hosts = state.data.stats.hosts || [];
|
||||
state.removingHost = hosts.find(function (host) {
|
||||
return host.userId === hostUserID;
|
||||
});
|
||||
if (!state.removingHost) return;
|
||||
$('removeMessage').textContent = t(
|
||||
'fami_agency.remove_confirm',
|
||||
'Remove {name} from this agency? Historical earnings will remain unchanged.'
|
||||
).replace('{name}', state.removingHost.name || hostUserID);
|
||||
$('removeDialog').showModal();
|
||||
}
|
||||
|
||||
function removeHost(event) {
|
||||
event.preventDefault();
|
||||
if (state.mutating || !state.removingHost) return;
|
||||
var target = state.removingHost;
|
||||
setMutating(true);
|
||||
window.HyGuild.agencyCenter
|
||||
.removeHost(target.userId)
|
||||
.then(function () {
|
||||
// 成员关系由后端原子结束;成功后重拉统计,不能只在本地删行伪造 total_hosts。
|
||||
$('removeDialog').close();
|
||||
state.removingHost = null;
|
||||
toast(t('fami_agency.host_removed', 'Host removed.'));
|
||||
return load();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(t('fami_agency.remove_failed', 'Unable to remove host.'));
|
||||
})
|
||||
.finally(function () {
|
||||
setMutating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function bind() {
|
||||
$('backButton').addEventListener('click', function () {
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else if (window.HyAppBridge) window.HyAppBridge.back();
|
||||
});
|
||||
$('withdrawButton').addEventListener('click', function () {
|
||||
window.location.href =
|
||||
window.HyGuild.product.buildURL('../wallet/');
|
||||
});
|
||||
$('editContactButton').addEventListener('click', function () {
|
||||
$('contactInput').value = (state.data && state.data.contact) || '';
|
||||
$('contactDialog').showModal();
|
||||
});
|
||||
$('cancelContactButton').addEventListener('click', function () {
|
||||
$('contactDialog').close();
|
||||
});
|
||||
$('contactForm').addEventListener('submit', saveContact);
|
||||
document
|
||||
.querySelector('.data-tabs')
|
||||
.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-tab]');
|
||||
if (!button) return;
|
||||
state.tab = button.getAttribute('data-tab');
|
||||
renderTabs();
|
||||
});
|
||||
$('rangeFilters').addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-range]');
|
||||
if (button && !state.loading)
|
||||
selectRange(button.getAttribute('data-range'));
|
||||
});
|
||||
$('cancelRangeButton').addEventListener('click', function () {
|
||||
$('rangeDialog').close();
|
||||
});
|
||||
$('rangeForm').addEventListener('submit', applyCustomRange);
|
||||
$('addHostButton').addEventListener('click', function () {
|
||||
$('inviteHostID').value = '';
|
||||
$('inviteDialog').showModal();
|
||||
});
|
||||
$('cancelInviteButton').addEventListener('click', function () {
|
||||
$('inviteDialog').close();
|
||||
});
|
||||
$('inviteForm').addEventListener('submit', inviteHost);
|
||||
$('hostTableBody').addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-remove-host]');
|
||||
if (button) openRemove(button.getAttribute('data-remove-host'));
|
||||
});
|
||||
$('cancelRemoveButton').addEventListener('click', function () {
|
||||
$('removeDialog').close();
|
||||
});
|
||||
$('removeForm').addEventListener('submit', removeHost);
|
||||
window.addEventListener('hyapp:i18n-ready', renderAll);
|
||||
}
|
||||
|
||||
function init() {
|
||||
state.range = window.HyGuild.dateRange.create('today');
|
||||
bind();
|
||||
renderRange();
|
||||
load();
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
566
guild/fami/agency-center/style.css
Normal file
566
guild/fami/agency-center/style.css
Normal file
@ -0,0 +1,566 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
background: var(--hy-theme-bg);
|
||||
color: var(--hy-theme-text);
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.center-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 48px;
|
||||
align-items: center;
|
||||
min-height: calc(64px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) 12px 0;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
background: color-mix(in srgb, var(--hy-theme-bg) 94%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
|
||||
.center-header h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.language-button {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-language);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
|
||||
.icon-button span {
|
||||
font-size: 38px;
|
||||
font-weight: 300;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.language-menu {
|
||||
position: absolute;
|
||||
top: 46px;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-small);
|
||||
background: var(--hy-theme-surface);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
|
||||
[dir='rtl'] .language-menu {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.language-menu button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.language-menu button.is-active {
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.agency-profile-card,
|
||||
.contact-card,
|
||||
.balance-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.agency-avatar {
|
||||
display: grid;
|
||||
flex: 0 0 76px;
|
||||
overflow: hidden;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
place-items: center;
|
||||
border: 3px solid var(--hy-theme-primary);
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.agency-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.agency-profile-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-inline-start: 16px;
|
||||
}
|
||||
|
||||
.agency-profile-copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.agency-profile-copy span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--hy-theme-success) 13%,
|
||||
var(--hy-theme-surface)
|
||||
);
|
||||
color: var(--hy-theme-success);
|
||||
}
|
||||
|
||||
.contact-card > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.contact-card > div span {
|
||||
overflow: hidden;
|
||||
color: var(--hy-theme-muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.contact-card > button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.balance-card > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-label,
|
||||
.section-title-row span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: clamp(20px, 6vw, 30px);
|
||||
}
|
||||
|
||||
.primary-action,
|
||||
.danger-action {
|
||||
min-width: 94px;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
color: #fff;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
background: var(--hy-theme-button);
|
||||
box-shadow: var(--hy-theme-button-shadow);
|
||||
}
|
||||
|
||||
.danger-action {
|
||||
background: var(--hy-theme-danger);
|
||||
}
|
||||
|
||||
.primary-action.compact {
|
||||
display: inline-flex;
|
||||
min-width: 0;
|
||||
min-height: 38px;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
padding: 0 14px;
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.section-title-row,
|
||||
.host-panel-toolbar {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title-row h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.data-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: 1fr 1fr;
|
||||
margin-bottom: 12px;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
}
|
||||
|
||||
.data-tabs button {
|
||||
position: relative;
|
||||
min-height: 44px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-muted);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.data-tabs button[aria-selected='true'] {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.data-tabs button[aria-selected='true']::after {
|
||||
position: absolute;
|
||||
right: 18%;
|
||||
bottom: -1px;
|
||||
left: 18%;
|
||||
height: 3px;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-primary-strong);
|
||||
content: '';
|
||||
}
|
||||
|
||||
.guild-filter-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guild-filter-bar button {
|
||||
padding: 5px 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.summary-grid article {
|
||||
display: flex;
|
||||
min-height: 108px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 8px;
|
||||
border-right: 1px solid var(--hy-theme-line);
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.summary-grid article:nth-child(3n) {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
|
||||
.summary-grid article:nth-last-child(-n + 2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.summary-grid article strong {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: clamp(18px, 5vw, 25px);
|
||||
}
|
||||
|
||||
.summary-grid article span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.host-panel-toolbar {
|
||||
margin-bottom: 10px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.host-table-shell {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
overflow-x: auto;
|
||||
}
|
||||
|
||||
.data-section,
|
||||
#hostsPanel {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
|
||||
.host-table {
|
||||
width: 100%;
|
||||
min-width: 1060px;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.host-table th,
|
||||
.host-table td {
|
||||
padding: 13px 12px;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.host-table th {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 11px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.host-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.host-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
|
||||
.host-avatar {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
width: 38px;
|
||||
height: 38px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.host-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.host-copy {
|
||||
display: flex;
|
||||
max-width: 150px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
|
||||
.host-copy strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
|
||||
.host-copy span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.remove-button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-danger);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.empty-state,
|
||||
.stats-status {
|
||||
padding: 24px;
|
||||
color: var(--hy-theme-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.stats-status {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
}
|
||||
|
||||
.edit-dialog {
|
||||
width: min(calc(100% - 32px), 420px);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-modal);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
|
||||
.edit-dialog::backdrop {
|
||||
background: rgba(31, 23, 45, 0.42);
|
||||
}
|
||||
|
||||
.edit-dialog form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
|
||||
.edit-dialog h2,
|
||||
.edit-dialog p {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.edit-dialog p {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
|
||||
.edit-dialog label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.edit-dialog input {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
outline: none;
|
||||
background: var(--hy-theme-surface-soft);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.edit-dialog input:focus {
|
||||
border-color: var(--hy-theme-primary-strong);
|
||||
box-shadow: 0 0 0 4px var(--hy-theme-focus-ring);
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dialog-actions > button:not(.primary-action, .danger-action) {
|
||||
min-height: 42px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.is-loading .data-section,
|
||||
.is-mutating button {
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
|
||||
@media (max-width: 520px) {
|
||||
.guild-center__content {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
|
||||
.summary-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.summary-grid article:nth-child(3n) {
|
||||
border-inline-end: 1px solid var(--hy-theme-line);
|
||||
}
|
||||
|
||||
.summary-grid article:nth-child(2n) {
|
||||
border-inline-end: 0;
|
||||
}
|
||||
|
||||
.summary-grid article:nth-last-child(-n + 2) {
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
}
|
||||
|
||||
.summary-grid article:last-child {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 380px) {
|
||||
.agency-avatar {
|
||||
flex-basis: 64px;
|
||||
width: 64px;
|
||||
height: 64px;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
align-items: flex-start;
|
||||
flex-direction: column;
|
||||
}
|
||||
|
||||
.balance-card .primary-action {
|
||||
width: 100%;
|
||||
}
|
||||
}
|
||||
291
guild/fami/bd-center/index.html
Normal file
291
guild/fami/bd-center/index.html
Normal file
@ -0,0 +1,291 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>BD Center</title>
|
||||
<link rel="stylesheet" href="../../../common/theme.css" />
|
||||
<link rel="stylesheet" href="../../packages/ui/center.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="../../packages/core/product-context.js"></script>
|
||||
<script src="../../products/fami/product.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="guild-center fami-bd-center">
|
||||
<header class="center-header">
|
||||
<button
|
||||
class="icon-button"
|
||||
id="backButton"
|
||||
type="button"
|
||||
data-i18n-aria="fami_common.back"
|
||||
aria-label="Back"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
</button>
|
||||
<h1 data-i18n="fami_bd.title">BD Center</h1>
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
type="button"
|
||||
data-language-toggle
|
||||
data-current-lang
|
||||
aria-expanded="false"
|
||||
data-i18n-aria="fami_common.change_language"
|
||||
aria-label="Change language"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<div class="language-menu" data-language-menu hidden>
|
||||
<button type="button" data-lang-option="en">EN</button
|
||||
><button type="button" data-lang-option="ar">AR</button
|
||||
><button type="button" data-lang-option="tr">TR</button
|
||||
><button type="button" data-lang-option="es">ES</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="guild-center__content" aria-live="polite">
|
||||
<section class="guild-card profile-card">
|
||||
<div class="profile-avatar">
|
||||
<img id="profileAvatar" alt="" hidden /><span
|
||||
id="profileInitial"
|
||||
aria-hidden="true"
|
||||
>B</span
|
||||
>
|
||||
</div>
|
||||
<div class="profile-copy">
|
||||
<span class="role-pill">BD</span
|
||||
><strong id="profileName">—</strong
|
||||
><span id="profileUID">UID: —</span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guild-card contact-card">
|
||||
<span class="contact-icon" aria-hidden="true">☎</span>
|
||||
<div>
|
||||
<strong data-i18n="fami_bd.contact">WhatsApp</strong
|
||||
><span id="contactValue">—</span>
|
||||
</div>
|
||||
<button
|
||||
id="editContactButton"
|
||||
type="button"
|
||||
data-i18n="fami_common.edit"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<button
|
||||
class="add-agency-card"
|
||||
id="addAgencyButton"
|
||||
type="button"
|
||||
>
|
||||
<span aria-hidden="true">+</span
|
||||
><span data-i18n="fami_bd.add_agency">Add agency</span>
|
||||
</button>
|
||||
|
||||
<section class="data-section" aria-labelledby="dataTitle">
|
||||
<div class="section-title-row">
|
||||
<h2 id="dataTitle" data-i18n="fami_bd.agency_data">
|
||||
Agency data
|
||||
</h2>
|
||||
<span id="rangeCaption"></span>
|
||||
</div>
|
||||
<div
|
||||
class="guild-filter-bar"
|
||||
id="rangeFilters"
|
||||
role="group"
|
||||
data-i18n-aria="fami_range.label"
|
||||
aria-label="Date range"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-range="today"
|
||||
data-i18n="fami_range.today"
|
||||
>
|
||||
Today</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="yesterday"
|
||||
data-i18n="fami_range.yesterday"
|
||||
>
|
||||
Yesterday</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="last_7_days"
|
||||
data-i18n="fami_range.last_7_days"
|
||||
>
|
||||
Last 7 days</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="last_30_days"
|
||||
data-i18n="fami_range.last_30_days"
|
||||
>
|
||||
Last 30 days</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="this_month"
|
||||
data-i18n="fami_range.this_month"
|
||||
>
|
||||
This month</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="last_month"
|
||||
data-i18n="fami_range.last_month"
|
||||
>
|
||||
Last month</button
|
||||
><button
|
||||
type="button"
|
||||
data-range="custom"
|
||||
data-i18n="fami_range.custom"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
<div class="stats-status" id="statsStatus" hidden></div>
|
||||
<div class="guild-card agency-table-shell">
|
||||
<table class="agency-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th data-i18n="fami_bd.agency">Agency</th>
|
||||
<th data-i18n="fami_bd.owner_id">
|
||||
Owner ID
|
||||
</th>
|
||||
<th data-i18n="fami_bd.country">Country</th>
|
||||
<th data-i18n="fami_agency.total_hosts">
|
||||
Total hosts
|
||||
</th>
|
||||
<th data-i18n="fami_agency.gifted_hosts">
|
||||
Gifted hosts
|
||||
</th>
|
||||
<th data-i18n="fami_bd.diamond_earnings">
|
||||
Agency diamond earnings
|
||||
</th>
|
||||
<th data-i18n="fami_bd.joined_at">
|
||||
Joined
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody id="agencyTableBody"></tbody>
|
||||
</table>
|
||||
<div
|
||||
class="empty-state"
|
||||
id="agencyEmpty"
|
||||
hidden
|
||||
data-i18n="fami_bd.no_agencies"
|
||||
>
|
||||
No agencies yet.
|
||||
</div>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog class="edit-dialog" id="contactDialog">
|
||||
<form method="dialog" id="contactForm">
|
||||
<h2 data-i18n="fami_bd.edit_contact">Edit WhatsApp</h2>
|
||||
<label
|
||||
><span data-i18n="fami_agency.contact_number"
|
||||
>Contact number</span
|
||||
><input
|
||||
id="contactInput"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="tel"
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelContactButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_common.save"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog class="edit-dialog" id="rangeDialog">
|
||||
<form method="dialog" id="rangeForm">
|
||||
<h2 data-i18n="fami_range.custom_title">
|
||||
Custom date range
|
||||
</h2>
|
||||
<label
|
||||
><span data-i18n="fami_range.start">Start date</span
|
||||
><input id="rangeStart" type="date" required /></label
|
||||
><label
|
||||
><span data-i18n="fami_range.end">End date</span
|
||||
><input id="rangeEnd" type="date" required
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelRangeButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_common.apply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
<dialog class="edit-dialog" id="inviteDialog">
|
||||
<form method="dialog" id="inviteForm">
|
||||
<h2 data-i18n="fami_bd.add_agency">Add agency</h2>
|
||||
<p data-i18n="fami_bd.invite_hint">
|
||||
Enter the future agency owner's display ID. The agency
|
||||
is created after the invitation is accepted.
|
||||
</p>
|
||||
<label
|
||||
><span data-i18n="fami_bd.owner_id">Owner ID</span
|
||||
><input
|
||||
id="ownerDisplayID"
|
||||
type="text"
|
||||
inputmode="numeric"
|
||||
maxlength="32"
|
||||
required /></label
|
||||
><label
|
||||
><span data-i18n="fami_bd.agency_name">Agency name</span
|
||||
><input id="agencyNameInput" type="text" maxlength="64"
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
type="button"
|
||||
id="cancelInviteButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel</button
|
||||
><button
|
||||
class="primary-action"
|
||||
type="submit"
|
||||
data-i18n="fami_agency.send_invite"
|
||||
>
|
||||
Send invite
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script src="../../../common/api.js"></script>
|
||||
<script src="../../../common/params.js"></script>
|
||||
<script src="../../../common/i18n.js"></script>
|
||||
<script src="../../../common/jsbridge.js"></script>
|
||||
<script src="../../../common/toast.js"></script>
|
||||
<script src="../../packages/core/date-range.js"></script>
|
||||
<script src="../../packages/bd-center/bd-center.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
292
guild/fami/bd-center/script.js
Normal file
292
guild/fami/bd-center/script.js
Normal file
@ -0,0 +1,292 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var state = { range: null, data: null, loading: false, mutating: false };
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
function t(key, fallback) {
|
||||
return window.HyAppI18n && window.HyAppI18n.t
|
||||
? window.HyAppI18n.t(key, fallback)
|
||||
: fallback || key;
|
||||
}
|
||||
function toast(message) {
|
||||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||||
}
|
||||
function count(value) {
|
||||
return Number.isFinite(value)
|
||||
? Number(value).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
})
|
||||
: '—';
|
||||
}
|
||||
function dateTime(value) {
|
||||
return Number(value)
|
||||
? new Date(Number(value)).toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
})
|
||||
: '—';
|
||||
}
|
||||
|
||||
function renderProfile() {
|
||||
if (!state.data) return;
|
||||
var profile = state.data.profile || {};
|
||||
$('profileName').textContent = profile.name || '—';
|
||||
$('profileUID').textContent =
|
||||
t('fami_common.uid_prefix', 'UID:') +
|
||||
' ' +
|
||||
(profile.displayUserId || profile.userId || '—');
|
||||
$('profileInitial').textContent = String(profile.name || 'B')
|
||||
.trim()
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
$('profileAvatar').hidden = !profile.avatar;
|
||||
$('profileInitial').hidden = !!profile.avatar;
|
||||
if (profile.avatar) $('profileAvatar').src = profile.avatar;
|
||||
$('contactValue').textContent = profile.contact || '—';
|
||||
}
|
||||
|
||||
function agencyAvatar(item) {
|
||||
var shell = document.createElement('span');
|
||||
shell.className = 'agency-avatar';
|
||||
if (item.avatar) {
|
||||
var image = document.createElement('img');
|
||||
image.src = item.avatar;
|
||||
image.alt = '';
|
||||
shell.appendChild(image);
|
||||
} else {
|
||||
shell.textContent = String(item.name || 'A')
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
}
|
||||
return shell;
|
||||
}
|
||||
|
||||
function textCell(row, value) {
|
||||
var cell = document.createElement('td');
|
||||
cell.textContent = value;
|
||||
row.appendChild(cell);
|
||||
}
|
||||
|
||||
function renderAgencies() {
|
||||
var agencies =
|
||||
state.data && state.data.stats
|
||||
? state.data.stats.agencies || []
|
||||
: [];
|
||||
var body = $('agencyTableBody');
|
||||
body.textContent = '';
|
||||
agencies.forEach(function (item) {
|
||||
var row = document.createElement('tr');
|
||||
var identityCell = document.createElement('td');
|
||||
var identity = document.createElement('div');
|
||||
identity.className = 'agency-identity';
|
||||
identity.appendChild(agencyAvatar(item));
|
||||
var copy = document.createElement('span');
|
||||
copy.className = 'agency-copy';
|
||||
var name = document.createElement('strong');
|
||||
name.textContent = item.name || '—';
|
||||
var id = document.createElement('span');
|
||||
id.textContent =
|
||||
t('fami_agency.id_prefix', 'Agency ID:') +
|
||||
' ' +
|
||||
(item.shortId || item.id || '—');
|
||||
copy.appendChild(name);
|
||||
copy.appendChild(id);
|
||||
identity.appendChild(copy);
|
||||
identityCell.appendChild(identity);
|
||||
row.appendChild(identityCell);
|
||||
textCell(row, item.ownerId || '—');
|
||||
textCell(row, item.country || '—');
|
||||
textCell(row, count(item.totalHosts));
|
||||
textCell(row, count(item.giftedHostCount));
|
||||
textCell(row, count(item.diamondEarnings));
|
||||
textCell(row, dateTime(item.joinedAtMS));
|
||||
body.appendChild(row);
|
||||
});
|
||||
$('agencyEmpty').hidden = agencies.length > 0;
|
||||
}
|
||||
|
||||
function renderRange() {
|
||||
document.querySelectorAll('[data-range]').forEach(function (button) {
|
||||
button.setAttribute(
|
||||
'aria-pressed',
|
||||
button.getAttribute('data-range') === state.range.preset
|
||||
? 'true'
|
||||
: 'false'
|
||||
);
|
||||
button.disabled = state.loading;
|
||||
});
|
||||
$('rangeCaption').textContent =
|
||||
state.range.startDate === state.range.endDate
|
||||
? state.range.startDate
|
||||
: state.range.startDate + ' – ' + state.range.endDate;
|
||||
}
|
||||
|
||||
function renderAll() {
|
||||
renderProfile();
|
||||
renderAgencies();
|
||||
renderRange();
|
||||
}
|
||||
function setLoading(value) {
|
||||
state.loading = value;
|
||||
document.body.classList.toggle('is-loading', value);
|
||||
renderRange();
|
||||
}
|
||||
function setMutating(value) {
|
||||
state.mutating = value;
|
||||
document.body.classList.toggle('is-mutating', value);
|
||||
}
|
||||
|
||||
function load() {
|
||||
setLoading(true);
|
||||
return window.HyGuild.bdCenter
|
||||
.load(state.range)
|
||||
.then(function (data) {
|
||||
state.data = data;
|
||||
$('statsStatus').hidden = true;
|
||||
renderAll();
|
||||
})
|
||||
.catch(function () {
|
||||
$('statsStatus').hidden = false;
|
||||
$('statsStatus').textContent = t(
|
||||
'fami_bd.load_failed',
|
||||
'Unable to load BD Center.'
|
||||
);
|
||||
toast($('statsStatus').textContent);
|
||||
})
|
||||
.finally(function () {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function selectRange(preset) {
|
||||
if (preset === 'custom') {
|
||||
$('rangeStart').value = state.range.startDate;
|
||||
$('rangeEnd').value = state.range.endDate;
|
||||
$('rangeDialog').showModal();
|
||||
return;
|
||||
}
|
||||
state.range = window.HyGuild.dateRange.create(preset);
|
||||
load();
|
||||
}
|
||||
|
||||
function applyCustomRange(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
state.range = window.HyGuild.dateRange.create('custom', {
|
||||
start: $('rangeStart').value,
|
||||
end: $('rangeEnd').value,
|
||||
});
|
||||
} catch (_) {
|
||||
toast(t('fami_range.invalid', 'Choose a valid date range.'));
|
||||
return;
|
||||
}
|
||||
$('rangeDialog').close();
|
||||
load();
|
||||
}
|
||||
|
||||
function saveContact(event) {
|
||||
event.preventDefault();
|
||||
if (state.mutating) return;
|
||||
var value = String($('contactInput').value || '').trim();
|
||||
if (!value) {
|
||||
toast(t('fami_agency.contact_required', 'Enter a contact number.'));
|
||||
return;
|
||||
}
|
||||
setMutating(true);
|
||||
window.HyGuild.bdCenter
|
||||
.saveContact(value)
|
||||
.then(function () {
|
||||
state.data.profile.contact = value;
|
||||
$('contactDialog').close();
|
||||
renderProfile();
|
||||
toast(t('fami_agency.contact_saved', 'Contact saved.'));
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_agency.contact_save_failed',
|
||||
'Unable to save contact.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setMutating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function inviteAgency(event) {
|
||||
event.preventDefault();
|
||||
if (state.mutating) return;
|
||||
var ownerID = String($('ownerDisplayID').value || '').trim();
|
||||
if (!ownerID) return;
|
||||
setMutating(true);
|
||||
window.HyGuild.bdCenter
|
||||
.inviteAgency(ownerID, $('agencyNameInput').value)
|
||||
.then(function () {
|
||||
$('inviteDialog').close();
|
||||
toast(
|
||||
t(
|
||||
'fami_bd.invite_sent',
|
||||
'Invitation sent. The agency is created after acceptance.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.catch(function () {
|
||||
toast(t('fami_bd.invite_failed', 'Unable to invite agency.'));
|
||||
})
|
||||
.finally(function () {
|
||||
setMutating(false);
|
||||
});
|
||||
}
|
||||
|
||||
function bind() {
|
||||
$('backButton').addEventListener('click', function () {
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else if (window.HyAppBridge) window.HyAppBridge.back();
|
||||
});
|
||||
$('editContactButton').addEventListener('click', function () {
|
||||
$('contactInput').value =
|
||||
(state.data && state.data.profile.contact) || '';
|
||||
$('contactDialog').showModal();
|
||||
});
|
||||
$('cancelContactButton').addEventListener('click', function () {
|
||||
$('contactDialog').close();
|
||||
});
|
||||
$('contactForm').addEventListener('submit', saveContact);
|
||||
$('addAgencyButton').addEventListener('click', function () {
|
||||
$('ownerDisplayID').value = '';
|
||||
$('agencyNameInput').value = '';
|
||||
$('inviteDialog').showModal();
|
||||
});
|
||||
$('cancelInviteButton').addEventListener('click', function () {
|
||||
$('inviteDialog').close();
|
||||
});
|
||||
$('inviteForm').addEventListener('submit', inviteAgency);
|
||||
$('rangeFilters').addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-range]');
|
||||
if (button && !state.loading)
|
||||
selectRange(button.getAttribute('data-range'));
|
||||
});
|
||||
$('cancelRangeButton').addEventListener('click', function () {
|
||||
$('rangeDialog').close();
|
||||
});
|
||||
$('rangeForm').addEventListener('submit', applyCustomRange);
|
||||
window.addEventListener('hyapp:i18n-ready', renderAll);
|
||||
}
|
||||
|
||||
function init() {
|
||||
state.range = window.HyGuild.dateRange.create('today');
|
||||
bind();
|
||||
renderRange();
|
||||
load();
|
||||
}
|
||||
if (document.readyState === 'loading')
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
else init();
|
||||
})();
|
||||
376
guild/fami/bd-center/style.css
Normal file
376
guild/fami/bd-center/style.css
Normal file
@ -0,0 +1,376 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
background: var(--hy-theme-bg);
|
||||
color: var(--hy-theme-text);
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
}
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
.center-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 48px;
|
||||
align-items: center;
|
||||
min-height: calc(64px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) 12px 0;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
background: color-mix(in srgb, var(--hy-theme-bg) 94%, transparent);
|
||||
backdrop-filter: blur(16px);
|
||||
}
|
||||
.center-header h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
.icon-button,
|
||||
.language-button {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-language);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
.icon-button span {
|
||||
font-size: 38px;
|
||||
font-weight: 300;
|
||||
line-height: 30px;
|
||||
}
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
}
|
||||
.language-button {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.language-menu {
|
||||
position: absolute;
|
||||
top: 46px;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-small);
|
||||
background: var(--hy-theme-surface);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
[dir='rtl'] .language-menu {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
.language-menu button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
.language-menu button.is-active {
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
.profile-card,
|
||||
.contact-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
}
|
||||
.profile-avatar {
|
||||
display: grid;
|
||||
flex: 0 0 76px;
|
||||
overflow: hidden;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
place-items: center;
|
||||
border: 3px solid var(--hy-theme-primary);
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.profile-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.profile-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 4px;
|
||||
margin-inline-start: 16px;
|
||||
}
|
||||
.profile-copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.profile-copy > span:not(.role-pill) {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.role-pill {
|
||||
align-self: flex-start;
|
||||
padding: 4px 11px;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 12px;
|
||||
font-weight: 800;
|
||||
}
|
||||
.contact-card {
|
||||
gap: 14px;
|
||||
}
|
||||
.contact-icon {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--hy-theme-success) 13%,
|
||||
var(--hy-theme-surface)
|
||||
);
|
||||
color: var(--hy-theme-success);
|
||||
}
|
||||
.contact-card > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
.contact-card > div span {
|
||||
overflow: hidden;
|
||||
color: var(--hy-theme-muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.contact-card > button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 750;
|
||||
}
|
||||
.add-agency-card {
|
||||
display: flex;
|
||||
min-height: 82px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 9px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
font-weight: 800;
|
||||
}
|
||||
.data-section,
|
||||
.agency-table-shell {
|
||||
min-width: 0;
|
||||
max-width: 100%;
|
||||
}
|
||||
.add-agency-card span:first-child {
|
||||
display: grid;
|
||||
width: 30px;
|
||||
height: 30px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-surface);
|
||||
font-size: 20px;
|
||||
}
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.section-title-row h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
.section-title-row span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.guild-filter-bar {
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
.guild-filter-bar button {
|
||||
padding: 5px 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
.agency-table-shell {
|
||||
overflow-x: auto;
|
||||
}
|
||||
.agency-table {
|
||||
width: 100%;
|
||||
min-width: 900px;
|
||||
border-collapse: collapse;
|
||||
font-size: 12px;
|
||||
}
|
||||
.agency-table th,
|
||||
.agency-table td {
|
||||
padding: 13px 12px;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
text-align: start;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.agency-table th {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.agency-table tr:last-child td {
|
||||
border-bottom: 0;
|
||||
}
|
||||
.agency-identity {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 9px;
|
||||
}
|
||||
.agency-avatar {
|
||||
display: grid;
|
||||
overflow: hidden;
|
||||
width: 40px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
.agency-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.agency-copy {
|
||||
display: flex;
|
||||
max-width: 170px;
|
||||
flex-direction: column;
|
||||
gap: 2px;
|
||||
}
|
||||
.agency-copy strong {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
}
|
||||
.agency-copy span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 11px;
|
||||
}
|
||||
.empty-state,
|
||||
.stats-status {
|
||||
padding: 24px;
|
||||
color: var(--hy-theme-muted);
|
||||
text-align: center;
|
||||
}
|
||||
.stats-status {
|
||||
margin-bottom: 12px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
}
|
||||
.primary-action {
|
||||
min-width: 94px;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-button);
|
||||
color: #fff;
|
||||
box-shadow: var(--hy-theme-button-shadow);
|
||||
font-weight: 750;
|
||||
}
|
||||
.edit-dialog {
|
||||
width: min(calc(100% - 32px), 420px);
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-modal);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
.edit-dialog::backdrop {
|
||||
background: rgba(31, 23, 45, 0.42);
|
||||
}
|
||||
.edit-dialog form {
|
||||
display: grid;
|
||||
gap: 16px;
|
||||
padding: 24px;
|
||||
}
|
||||
.edit-dialog h2,
|
||||
.edit-dialog p {
|
||||
margin: 0;
|
||||
}
|
||||
.edit-dialog p {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
line-height: 1.55;
|
||||
}
|
||||
.edit-dialog label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
.edit-dialog input {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 0 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
outline: none;
|
||||
background: var(--hy-theme-surface-soft);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
.edit-dialog input:focus {
|
||||
border-color: var(--hy-theme-primary-strong);
|
||||
box-shadow: 0 0 0 4px var(--hy-theme-focus-ring);
|
||||
}
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
.dialog-actions > button:not(.primary-action) {
|
||||
min-height: 42px;
|
||||
padding: 0 16px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
.is-loading .data-section,
|
||||
.is-mutating button {
|
||||
pointer-events: none;
|
||||
opacity: 0.65;
|
||||
}
|
||||
@media (max-width: 520px) {
|
||||
.guild-center__content {
|
||||
padding-inline: 12px;
|
||||
}
|
||||
}
|
||||
297
guild/fami/host-center/index.html
Normal file
297
guild/fami/host-center/index.html
Normal file
@ -0,0 +1,297 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Host Center</title>
|
||||
<link rel="stylesheet" href="../../../common/theme.css" />
|
||||
<link rel="stylesheet" href="../../packages/ui/center.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="../../packages/core/product-context.js"></script>
|
||||
<script src="../../products/fami/product.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="guild-center fami-host-center">
|
||||
<header class="center-header">
|
||||
<button
|
||||
class="icon-button"
|
||||
id="backButton"
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
data-i18n-aria="fami_host.back"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
</button>
|
||||
<h1 data-i18n="fami_host.title">Host Center</h1>
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
type="button"
|
||||
data-language-toggle
|
||||
data-current-lang
|
||||
aria-expanded="false"
|
||||
aria-label="Change language"
|
||||
data-i18n-aria="fami_host.change_language"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<div class="language-menu" data-language-menu hidden>
|
||||
<button type="button" data-lang-option="en">EN</button>
|
||||
<button type="button" data-lang-option="ar">AR</button>
|
||||
<button type="button" data-lang-option="tr">TR</button>
|
||||
<button type="button" data-lang-option="es">ES</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="guild-center__content" aria-live="polite">
|
||||
<section class="guild-card profile-card">
|
||||
<div class="avatar-shell">
|
||||
<img id="profileAvatar" alt="" hidden />
|
||||
<span id="profileInitial" aria-hidden="true">H</span>
|
||||
</div>
|
||||
<div class="profile-copy">
|
||||
<strong id="profileName">—</strong>
|
||||
<span id="profileUID">UID: —</span>
|
||||
<span class="agency-pill" id="agencyPill" hidden></span>
|
||||
<span class="join-time" id="joinTime" hidden></span>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section class="guild-card contact-card">
|
||||
<span class="contact-icon" aria-hidden="true">☎</span>
|
||||
<div>
|
||||
<strong data-i18n="fami_host.contact">WhatsApp</strong>
|
||||
<span id="contactValue">—</span>
|
||||
</div>
|
||||
<button
|
||||
id="editContactButton"
|
||||
type="button"
|
||||
data-i18n="fami_host.edit"
|
||||
>
|
||||
Edit
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="guild-card balance-card">
|
||||
<div>
|
||||
<span
|
||||
class="card-label"
|
||||
data-i18n="fami_host.diamond_balance"
|
||||
>Diamond balance</span
|
||||
>
|
||||
<strong class="balance-value"
|
||||
><span aria-hidden="true">💎</span
|
||||
><span id="diamondBalance">—</span></strong
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="primary-action"
|
||||
id="withdrawButton"
|
||||
type="button"
|
||||
data-i18n="fami_host.withdraw"
|
||||
>
|
||||
Withdraw
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="stats-section" aria-labelledby="statsTitle">
|
||||
<div class="section-title-row">
|
||||
<h2 id="statsTitle" data-i18n="fami_host.data_details">
|
||||
Data details
|
||||
</h2>
|
||||
<span id="rangeCaption"></span>
|
||||
</div>
|
||||
<div
|
||||
class="guild-filter-bar"
|
||||
id="rangeFilters"
|
||||
role="group"
|
||||
aria-label="Date range"
|
||||
>
|
||||
<button
|
||||
type="button"
|
||||
data-range="today"
|
||||
data-i18n="fami_range.today"
|
||||
>
|
||||
Today
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="yesterday"
|
||||
data-i18n="fami_range.yesterday"
|
||||
>
|
||||
Yesterday
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_7_days"
|
||||
data-i18n="fami_range.last_7_days"
|
||||
>
|
||||
Last 7 days
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_30_days"
|
||||
data-i18n="fami_range.last_30_days"
|
||||
>
|
||||
Last 30 days
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="this_month"
|
||||
data-i18n="fami_range.this_month"
|
||||
>
|
||||
This month
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="last_month"
|
||||
data-i18n="fami_range.last_month"
|
||||
>
|
||||
Last month
|
||||
</button>
|
||||
<button
|
||||
type="button"
|
||||
data-range="custom"
|
||||
data-i18n="fami_range.custom"
|
||||
>
|
||||
Custom
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<div class="stats-status" id="statsStatus" hidden></div>
|
||||
<div class="guild-card metrics-grid" id="metricsGrid">
|
||||
<article data-metric="diamond_earnings">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.diamond_earnings"
|
||||
>Diamond earnings</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="diamond_exchanged">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.diamond_exchanged"
|
||||
>Diamonds exchanged</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="gift_senders">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.gift_senders"
|
||||
>Gift senders</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="online_duration_ms">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.online_duration"
|
||||
>Online duration</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="valid_mic_duration_ms">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.valid_mic_duration"
|
||||
>Valid mic duration</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="valid_mic_days">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.valid_mic_days"
|
||||
>Valid mic days</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="private_message_senders">
|
||||
<strong>—</strong
|
||||
><span
|
||||
data-i18n="fami_metric.private_message_senders"
|
||||
>Private message users</span
|
||||
>
|
||||
</article>
|
||||
<article data-metric="new_followers">
|
||||
<strong>—</strong
|
||||
><span data-i18n="fami_metric.new_followers"
|
||||
>New followers</span
|
||||
>
|
||||
</article>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
|
||||
<dialog class="edit-dialog" id="contactDialog">
|
||||
<form method="dialog" id="contactForm">
|
||||
<h2 data-i18n="fami_host.edit_contact">Edit WhatsApp</h2>
|
||||
<label
|
||||
><span data-i18n="fami_host.contact_number"
|
||||
>Contact number</span
|
||||
><input
|
||||
id="contactInput"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="tel"
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
value="cancel"
|
||||
type="button"
|
||||
id="cancelContactButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="primary-action"
|
||||
value="save"
|
||||
type="submit"
|
||||
data-i18n="fami_common.save"
|
||||
>
|
||||
Save
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
|
||||
<dialog class="edit-dialog" id="rangeDialog">
|
||||
<form method="dialog" id="rangeForm">
|
||||
<h2 data-i18n="fami_range.custom_title">
|
||||
Custom date range
|
||||
</h2>
|
||||
<label
|
||||
><span data-i18n="fami_range.start">Start date</span
|
||||
><input id="rangeStart" type="date" required
|
||||
/></label>
|
||||
<label
|
||||
><span data-i18n="fami_range.end">End date</span
|
||||
><input id="rangeEnd" type="date" required
|
||||
/></label>
|
||||
<div class="dialog-actions">
|
||||
<button
|
||||
value="cancel"
|
||||
type="button"
|
||||
id="cancelRangeButton"
|
||||
data-i18n="fami_common.cancel"
|
||||
>
|
||||
Cancel
|
||||
</button>
|
||||
<button
|
||||
class="primary-action"
|
||||
value="apply"
|
||||
type="submit"
|
||||
data-i18n="fami_common.apply"
|
||||
>
|
||||
Apply
|
||||
</button>
|
||||
</div>
|
||||
</form>
|
||||
</dialog>
|
||||
</div>
|
||||
|
||||
<script src="../../../common/api.js"></script>
|
||||
<script src="../../../common/params.js"></script>
|
||||
<script src="../../../common/i18n.js"></script>
|
||||
<script src="../../../common/jsbridge.js"></script>
|
||||
<script src="../../../common/toast.js"></script>
|
||||
<script src="../../packages/core/date-range.js"></script>
|
||||
<script src="../../packages/host-center/host-center.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
279
guild/fami/host-center/script.js
Normal file
279
guild/fami/host-center/script.js
Normal file
@ -0,0 +1,279 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var state = {
|
||||
range: null,
|
||||
data: null,
|
||||
loading: false,
|
||||
savingContact: false,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function t(key, fallback) {
|
||||
return window.HyAppI18n && window.HyAppI18n.t
|
||||
? window.HyAppI18n.t(key, fallback)
|
||||
: fallback || key;
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||||
}
|
||||
|
||||
function count(value) {
|
||||
if (value === null || value === undefined || !Number.isFinite(value)) {
|
||||
return '—';
|
||||
}
|
||||
return Number(value).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function duration(value) {
|
||||
if (value === null || value === undefined || !Number.isFinite(value)) {
|
||||
return '—';
|
||||
}
|
||||
var minutes = Math.max(0, Math.floor(Number(value) / 60000));
|
||||
var days = Math.floor(minutes / 1440);
|
||||
var hours = Math.floor((minutes % 1440) / 60);
|
||||
var rest = minutes % 60;
|
||||
var parts = [];
|
||||
if (days) parts.push(days + t('fami_unit.day_short', 'd'));
|
||||
if (hours) parts.push(hours + t('fami_unit.hour_short', 'h'));
|
||||
if (!days && rest) parts.push(rest + t('fami_unit.minute_short', 'm'));
|
||||
return parts.length
|
||||
? parts.join(' ')
|
||||
: '0' + t('fami_unit.minute_short', 'm');
|
||||
}
|
||||
|
||||
function dateTime(value) {
|
||||
if (!Number(value)) return '';
|
||||
return new Date(Number(value)).toLocaleString(undefined, {
|
||||
year: 'numeric',
|
||||
month: '2-digit',
|
||||
day: '2-digit',
|
||||
hour: '2-digit',
|
||||
minute: '2-digit',
|
||||
});
|
||||
}
|
||||
|
||||
function renderProfile() {
|
||||
if (!state.data) return;
|
||||
var profile = state.data.profile || {};
|
||||
var agency = state.data.agency;
|
||||
$('profileName').textContent = profile.name || '—';
|
||||
$('profileUID').textContent =
|
||||
t('fami_host.uid_prefix', 'UID:') +
|
||||
' ' +
|
||||
(profile.displayUserId || profile.userId || '—');
|
||||
$('profileInitial').textContent = String(profile.name || 'H')
|
||||
.trim()
|
||||
.charAt(0)
|
||||
.toUpperCase();
|
||||
$('profileAvatar').hidden = !profile.avatar;
|
||||
$('profileInitial').hidden = !!profile.avatar;
|
||||
if (profile.avatar) $('profileAvatar').src = profile.avatar;
|
||||
|
||||
$('agencyPill').hidden = !agency;
|
||||
$('joinTime').hidden = !agency || !agency.joinedAtMS;
|
||||
if (agency) {
|
||||
$('agencyPill').textContent =
|
||||
t('fami_host.agency_id_prefix', 'Agency ID:') +
|
||||
' ' +
|
||||
(agency.shortId || agency.id || '—');
|
||||
if (agency.joinedAtMS) {
|
||||
$('joinTime').textContent =
|
||||
t('fami_host.joined_at_prefix', 'Joined:') +
|
||||
' ' +
|
||||
dateTime(agency.joinedAtMS);
|
||||
}
|
||||
}
|
||||
$('contactValue').textContent = profile.contact || '—';
|
||||
$('diamondBalance').textContent = count(state.data.pointBalance);
|
||||
}
|
||||
|
||||
function renderMetrics() {
|
||||
var metrics =
|
||||
state.data && state.data.stats ? state.data.stats.metrics : {};
|
||||
window.HyGuild.hostCenter.metrics.forEach(function (definition) {
|
||||
var node = document.querySelector(
|
||||
'[data-metric="' + definition.key + '"] strong'
|
||||
);
|
||||
if (!node) return;
|
||||
node.textContent =
|
||||
definition.format === 'duration'
|
||||
? duration(metrics[definition.key])
|
||||
: count(metrics[definition.key]);
|
||||
});
|
||||
var statsError =
|
||||
state.data && state.data.errors ? state.data.errors.stats : null;
|
||||
$('statsStatus').hidden = !statsError;
|
||||
if (statsError) {
|
||||
$('statsStatus').textContent = t(
|
||||
'fami_host.stats_unavailable',
|
||||
'Statistics are temporarily unavailable.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function renderRange() {
|
||||
document.querySelectorAll('[data-range]').forEach(function (button) {
|
||||
button.setAttribute(
|
||||
'aria-pressed',
|
||||
button.getAttribute('data-range') === state.range.preset
|
||||
? 'true'
|
||||
: 'false'
|
||||
);
|
||||
button.disabled = state.loading;
|
||||
});
|
||||
$('rangeCaption').textContent =
|
||||
state.range.startDate === state.range.endDate
|
||||
? state.range.startDate
|
||||
: state.range.startDate + ' – ' + state.range.endDate;
|
||||
}
|
||||
|
||||
function setLoading(loading) {
|
||||
state.loading = loading;
|
||||
document.body.classList.toggle('is-loading', loading);
|
||||
renderRange();
|
||||
}
|
||||
|
||||
function load() {
|
||||
setLoading(true);
|
||||
return window.HyGuild.hostCenter
|
||||
.load(state.range)
|
||||
.then(function (data) {
|
||||
state.data = data;
|
||||
renderProfile();
|
||||
renderMetrics();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t('fami_host.load_failed', 'Unable to load Host Center.')
|
||||
);
|
||||
$('statsStatus').hidden = false;
|
||||
$('statsStatus').textContent = t(
|
||||
'fami_host.load_failed',
|
||||
'Unable to load Host Center.'
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function selectRange(preset) {
|
||||
if (preset === 'custom') {
|
||||
$('rangeStart').value = state.range.startDate;
|
||||
$('rangeEnd').value = state.range.endDate;
|
||||
$('rangeDialog').showModal();
|
||||
return;
|
||||
}
|
||||
state.range = window.HyGuild.dateRange.create(preset);
|
||||
renderRange();
|
||||
load();
|
||||
}
|
||||
|
||||
function saveContact(event) {
|
||||
event.preventDefault();
|
||||
if (state.savingContact) return;
|
||||
var value = String($('contactInput').value || '').trim();
|
||||
if (!value) {
|
||||
toast(t('fami_host.contact_required', 'Enter a contact number.'));
|
||||
return;
|
||||
}
|
||||
state.savingContact = true;
|
||||
window.HyGuild.hostCenter
|
||||
.saveContact(value)
|
||||
.then(function () {
|
||||
state.data.profile.contact = value;
|
||||
$('contactValue').textContent = value;
|
||||
$('contactDialog').close();
|
||||
toast(t('fami_host.contact_saved', 'Contact saved.'));
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_host.contact_save_failed',
|
||||
'Unable to save contact.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
state.savingContact = false;
|
||||
});
|
||||
}
|
||||
|
||||
function applyCustomRange(event) {
|
||||
event.preventDefault();
|
||||
try {
|
||||
state.range = window.HyGuild.dateRange.create('custom', {
|
||||
start: $('rangeStart').value,
|
||||
end: $('rangeEnd').value,
|
||||
});
|
||||
} catch (_) {
|
||||
toast(t('fami_range.invalid', 'Choose a valid date range.'));
|
||||
return;
|
||||
}
|
||||
$('rangeDialog').close();
|
||||
renderRange();
|
||||
load();
|
||||
}
|
||||
|
||||
function bind() {
|
||||
$('backButton').addEventListener('click', function () {
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else if (window.HyAppBridge) window.HyAppBridge.back();
|
||||
});
|
||||
$('withdrawButton').addEventListener('click', function () {
|
||||
// 钱包是独立页面包;统一 URL builder 继承 token/env/lang 并强制 app_code=fami。
|
||||
window.location.href =
|
||||
window.HyGuild.product.buildURL('../wallet/');
|
||||
});
|
||||
$('editContactButton').addEventListener('click', function () {
|
||||
$('contactInput').value =
|
||||
(state.data && state.data.profile.contact) || '';
|
||||
$('contactDialog').showModal();
|
||||
});
|
||||
$('cancelContactButton').addEventListener('click', function () {
|
||||
$('contactDialog').close();
|
||||
});
|
||||
$('contactForm').addEventListener('submit', saveContact);
|
||||
$('rangeFilters').addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-range]');
|
||||
if (button && !state.loading) {
|
||||
selectRange(button.getAttribute('data-range'));
|
||||
}
|
||||
});
|
||||
$('cancelRangeButton').addEventListener('click', function () {
|
||||
$('rangeDialog').close();
|
||||
});
|
||||
$('rangeForm').addEventListener('submit', applyCustomRange);
|
||||
window.addEventListener('hyapp:i18n-ready', function () {
|
||||
renderProfile();
|
||||
renderMetrics();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
state.range = window.HyGuild.dateRange.create('today');
|
||||
bind();
|
||||
renderRange();
|
||||
load().finally(function () {
|
||||
if (window.HyAppBridge) {
|
||||
window.HyAppBridge.ready({
|
||||
page: 'fami-host-center',
|
||||
app_code: 'fami',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
409
guild/fami/host-center/style.css
Normal file
409
guild/fami/host-center/style.css
Normal file
@ -0,0 +1,409 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
background: var(--hy-theme-bg);
|
||||
color: var(--hy-theme-text);
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.fami-host-center {
|
||||
background:
|
||||
radial-gradient(
|
||||
circle at 16% 5%,
|
||||
var(--hy-theme-primary-soft),
|
||||
transparent 32%
|
||||
),
|
||||
var(--hy-theme-bg);
|
||||
}
|
||||
|
||||
.center-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 48px;
|
||||
align-items: center;
|
||||
min-height: calc(64px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) 12px 0;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
background: color-mix(in srgb, var(--hy-theme-bg) 88%, transparent);
|
||||
backdrop-filter: blur(18px);
|
||||
}
|
||||
|
||||
.center-header h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
line-height: 1.2;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.language-button {
|
||||
display: grid;
|
||||
place-items: center;
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-language);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
|
||||
.icon-button span {
|
||||
font-size: 38px;
|
||||
font-weight: 300;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.language-menu {
|
||||
position: absolute;
|
||||
top: 46px;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-small);
|
||||
background: var(--hy-theme-surface);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
|
||||
.language-menu button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.language-menu button.is-active {
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.profile-card,
|
||||
.contact-card,
|
||||
.balance-card {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.avatar-shell {
|
||||
display: grid;
|
||||
flex: 0 0 76px;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
width: 76px;
|
||||
height: 76px;
|
||||
border: 3px solid var(--hy-theme-primary);
|
||||
border-radius: 50%;
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 30px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.avatar-shell img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.profile-copy {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 5px;
|
||||
margin-inline-start: 16px;
|
||||
}
|
||||
|
||||
.profile-copy strong {
|
||||
overflow: hidden;
|
||||
font-size: 21px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.profile-copy > span:not(.agency-pill) {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.agency-pill {
|
||||
align-self: flex-start;
|
||||
padding: 5px 10px;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 12px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.contact-card {
|
||||
gap: 14px;
|
||||
}
|
||||
|
||||
.contact-icon {
|
||||
display: grid;
|
||||
width: 48px;
|
||||
height: 48px;
|
||||
place-items: center;
|
||||
border-radius: 50%;
|
||||
background: color-mix(
|
||||
in srgb,
|
||||
var(--hy-theme-success) 14%,
|
||||
var(--hy-theme-surface)
|
||||
);
|
||||
color: var(--hy-theme-success);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.contact-card > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex: 1;
|
||||
flex-direction: column;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.contact-card > div span {
|
||||
overflow: hidden;
|
||||
color: var(--hy-theme-muted);
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.contact-card > button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.balance-card {
|
||||
justify-content: space-between;
|
||||
gap: 18px;
|
||||
}
|
||||
|
||||
.balance-card > div {
|
||||
display: flex;
|
||||
min-width: 0;
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.card-label,
|
||||
.section-title-row span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.balance-value {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
overflow-wrap: anywhere;
|
||||
font-size: clamp(20px, 6vw, 30px);
|
||||
}
|
||||
|
||||
.primary-action {
|
||||
min-width: 94px;
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-button);
|
||||
color: white;
|
||||
box-shadow: var(--hy-theme-button-shadow);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.section-title-row {
|
||||
display: flex;
|
||||
align-items: end;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.section-title-row h2 {
|
||||
margin: 0;
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.guild-filter-bar {
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
margin-bottom: 12px;
|
||||
}
|
||||
|
||||
.guild-filter-bar button {
|
||||
padding: 5px 4px;
|
||||
font-size: 11px;
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.metrics-grid article {
|
||||
display: flex;
|
||||
min-height: 104px;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
padding: 14px 8px;
|
||||
border-right: 1px solid var(--hy-theme-line);
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
flex-direction: column;
|
||||
gap: 8px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.metrics-grid article:nth-child(3n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.metrics-grid article:nth-last-child(-n + 2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
|
||||
.metrics-grid article strong {
|
||||
color: var(--hy-theme-text);
|
||||
font-size: 19px;
|
||||
}
|
||||
|
||||
.metrics-grid article span {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.35;
|
||||
}
|
||||
|
||||
.stats-status {
|
||||
margin-bottom: 10px;
|
||||
padding: 11px 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-small);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.edit-dialog {
|
||||
width: min(420px, calc(100vw - 32px));
|
||||
padding: 0;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-modal);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
|
||||
.edit-dialog::backdrop {
|
||||
background: rgb(25 18 38 / 45%);
|
||||
}
|
||||
|
||||
.edit-dialog form {
|
||||
display: grid;
|
||||
gap: 18px;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.edit-dialog h2 {
|
||||
margin: 0;
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.edit-dialog label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.edit-dialog input {
|
||||
width: 100%;
|
||||
min-height: 46px;
|
||||
padding: 0 13px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
outline: none;
|
||||
background: var(--hy-theme-surface-soft);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.edit-dialog input:focus {
|
||||
border-color: var(--hy-theme-primary-strong);
|
||||
box-shadow: 0 0 0 4px var(--hy-theme-focus-ring);
|
||||
}
|
||||
|
||||
.dialog-actions {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.dialog-actions button:not(.primary-action) {
|
||||
min-height: 42px;
|
||||
padding: 0 18px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-pill);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
[dir='rtl'] .language-menu {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.guild-filter-bar {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metrics-grid {
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
}
|
||||
|
||||
.metrics-grid article:nth-child(3n) {
|
||||
border-right: 1px solid var(--hy-theme-line);
|
||||
}
|
||||
|
||||
.metrics-grid article:nth-child(2n) {
|
||||
border-right: 0;
|
||||
}
|
||||
|
||||
.metrics-grid article:nth-last-child(-n + 2) {
|
||||
border-bottom: 0;
|
||||
}
|
||||
}
|
||||
207
guild/fami/wallet/index.html
Normal file
207
guild/fami/wallet/index.html
Normal file
@ -0,0 +1,207 @@
|
||||
<!doctype html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8" />
|
||||
<meta
|
||||
name="viewport"
|
||||
content="width=device-width, initial-scale=1, maximum-scale=1, user-scalable=no, viewport-fit=cover"
|
||||
/>
|
||||
<title>Wallet</title>
|
||||
<link rel="stylesheet" href="../../../common/theme.css" />
|
||||
<link rel="stylesheet" href="../../packages/ui/center.css" />
|
||||
<link rel="stylesheet" href="./style.css" />
|
||||
<script src="../../packages/core/product-context.js"></script>
|
||||
<script src="../../products/fami/product.js"></script>
|
||||
</head>
|
||||
<body>
|
||||
<div class="guild-center fami-wallet">
|
||||
<header class="center-header">
|
||||
<button
|
||||
class="icon-button"
|
||||
id="backButton"
|
||||
type="button"
|
||||
aria-label="Back"
|
||||
data-i18n-aria="fami_common.back"
|
||||
>
|
||||
<span aria-hidden="true">‹</span>
|
||||
</button>
|
||||
<h1 data-i18n="fami_wallet.title">Withdraw</h1>
|
||||
<div class="language-switcher">
|
||||
<button
|
||||
class="language-button"
|
||||
type="button"
|
||||
data-language-toggle
|
||||
data-current-lang
|
||||
aria-expanded="false"
|
||||
aria-label="Change language"
|
||||
data-i18n-aria="fami_common.change_language"
|
||||
>
|
||||
EN
|
||||
</button>
|
||||
<div class="language-menu" data-language-menu hidden>
|
||||
<button type="button" data-lang-option="en">EN</button>
|
||||
<button type="button" data-lang-option="ar">AR</button>
|
||||
<button type="button" data-lang-option="tr">TR</button>
|
||||
<button type="button" data-lang-option="es">ES</button>
|
||||
</div>
|
||||
</div>
|
||||
</header>
|
||||
|
||||
<main class="guild-center__content wallet-content">
|
||||
<div class="wallet-tabs" role="tablist">
|
||||
<button
|
||||
id="sellerTab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected="true"
|
||||
data-mode="seller"
|
||||
data-i18n="fami_wallet.seller_tab"
|
||||
>
|
||||
Coin seller
|
||||
</button>
|
||||
<button
|
||||
id="usdtTab"
|
||||
type="button"
|
||||
role="tab"
|
||||
aria-selected="false"
|
||||
data-mode="usdt"
|
||||
data-i18n="fami_wallet.usdt_tab"
|
||||
>
|
||||
USDT
|
||||
</button>
|
||||
</div>
|
||||
|
||||
<section class="guild-card wallet-balance-card">
|
||||
<span data-i18n="fami_wallet.point_balance"
|
||||
>POINT balance</span
|
||||
>
|
||||
<strong id="pointBalance">—</strong>
|
||||
<small id="balanceUSD"></small>
|
||||
</section>
|
||||
|
||||
<section class="guild-card amount-card">
|
||||
<label for="pointAmount" data-i18n="fami_wallet.amount"
|
||||
>Withdrawal amount</label
|
||||
>
|
||||
<div class="amount-input-row">
|
||||
<span aria-hidden="true">◆</span>
|
||||
<input
|
||||
id="pointAmount"
|
||||
type="number"
|
||||
inputmode="numeric"
|
||||
min="1"
|
||||
step="1"
|
||||
data-i18n-placeholder="fami_wallet.amount_placeholder"
|
||||
placeholder="Enter POINT amount"
|
||||
/>
|
||||
<button
|
||||
id="allButton"
|
||||
type="button"
|
||||
data-i18n="fami_wallet.all"
|
||||
>
|
||||
All
|
||||
</button>
|
||||
</div>
|
||||
<div class="conversion-row">
|
||||
<span data-i18n="fami_wallet.estimated_receive"
|
||||
>Estimated receive</span
|
||||
>
|
||||
<strong id="estimatedReceive">—</strong>
|
||||
</div>
|
||||
<small id="policyCaption"></small>
|
||||
</section>
|
||||
|
||||
<section id="sellerPanel" role="tabpanel">
|
||||
<div class="section-heading">
|
||||
<h2 data-i18n="fami_wallet.choose_seller">
|
||||
Choose coin seller
|
||||
</h2>
|
||||
</div>
|
||||
<div class="seller-list" id="sellerList"></div>
|
||||
<div class="empty-state" id="sellerEmpty" hidden>
|
||||
<strong data-i18n="fami_wallet.no_sellers"
|
||||
>No eligible coin sellers</strong
|
||||
>
|
||||
<span data-i18n="fami_wallet.no_sellers_hint"
|
||||
>Availability depends on your country and the Admin
|
||||
whitelist.</span
|
||||
>
|
||||
</div>
|
||||
<button
|
||||
class="wallet-submit"
|
||||
id="sellerSubmit"
|
||||
type="button"
|
||||
data-i18n="fami_wallet.withdraw_now"
|
||||
>
|
||||
Withdraw now
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section id="usdtPanel" role="tabpanel" hidden>
|
||||
<div class="section-heading">
|
||||
<h2 data-i18n="fami_wallet.usdt_address">
|
||||
USDT TRC20 address
|
||||
</h2>
|
||||
<p data-i18n="fami_wallet.usdt_warning">
|
||||
Withdrawals are sent only to your saved TRC20
|
||||
address.
|
||||
</p>
|
||||
</div>
|
||||
<div class="address-status" id="addressStatus"></div>
|
||||
<label class="address-label">
|
||||
<span data-i18n="fami_wallet.trc20_address"
|
||||
>TRC20 address</span
|
||||
>
|
||||
<input
|
||||
id="addressInput"
|
||||
type="text"
|
||||
maxlength="64"
|
||||
autocomplete="off"
|
||||
spellcheck="false"
|
||||
data-i18n-placeholder="fami_wallet.address_placeholder"
|
||||
placeholder="T..."
|
||||
/>
|
||||
</label>
|
||||
<button
|
||||
class="secondary-submit"
|
||||
id="saveAddressButton"
|
||||
type="button"
|
||||
data-i18n="fami_wallet.save_address"
|
||||
>
|
||||
Save address
|
||||
</button>
|
||||
<button
|
||||
class="wallet-submit"
|
||||
id="usdtSubmit"
|
||||
type="button"
|
||||
data-i18n="fami_wallet.withdraw_now"
|
||||
>
|
||||
Withdraw now
|
||||
</button>
|
||||
</section>
|
||||
|
||||
<section class="history-section">
|
||||
<div class="section-heading">
|
||||
<h2 data-i18n="fami_wallet.history">
|
||||
Withdrawal history
|
||||
</h2>
|
||||
</div>
|
||||
<div class="history-list" id="historyList"></div>
|
||||
<div class="empty-state" id="historyEmpty" hidden>
|
||||
<span data-i18n="fami_wallet.no_history"
|
||||
>No withdrawal records</span
|
||||
>
|
||||
</div>
|
||||
</section>
|
||||
</main>
|
||||
</div>
|
||||
|
||||
<script src="../../../common/api.js"></script>
|
||||
<script src="../../../common/params.js"></script>
|
||||
<script src="../../../common/i18n.js"></script>
|
||||
<script src="../../../common/jsbridge.js"></script>
|
||||
<script src="../../../common/toast.js"></script>
|
||||
<script src="../../packages/wallet/wallet.js"></script>
|
||||
<script src="./script.js"></script>
|
||||
</body>
|
||||
</html>
|
||||
417
guild/fami/wallet/script.js
Normal file
417
guild/fami/wallet/script.js
Normal file
@ -0,0 +1,417 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var state = {
|
||||
data: null,
|
||||
mode: 'seller',
|
||||
selectedSellerID: '',
|
||||
busy: false,
|
||||
};
|
||||
|
||||
function $(id) {
|
||||
return document.getElementById(id);
|
||||
}
|
||||
|
||||
function t(key, fallback) {
|
||||
return window.HyAppI18n && window.HyAppI18n.t
|
||||
? window.HyAppI18n.t(key, fallback)
|
||||
: fallback || key;
|
||||
}
|
||||
|
||||
function toast(message) {
|
||||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||||
}
|
||||
|
||||
function number(value) {
|
||||
var normalized = value || 0;
|
||||
if (
|
||||
typeof BigInt === 'function' &&
|
||||
typeof normalized === 'string' &&
|
||||
/^-?\d+$/.test(normalized)
|
||||
) {
|
||||
return new Intl.NumberFormat().format(BigInt(normalized));
|
||||
}
|
||||
return Number(normalized).toLocaleString(undefined, {
|
||||
maximumFractionDigits: 0,
|
||||
});
|
||||
}
|
||||
|
||||
function amount() {
|
||||
var value = Number($('pointAmount').value || 0);
|
||||
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
||||
}
|
||||
|
||||
function selectedSeller() {
|
||||
if (!state.data) return null;
|
||||
return state.data.sellers.find(function (seller) {
|
||||
return seller.userId === state.selectedSellerID;
|
||||
});
|
||||
}
|
||||
|
||||
function setBusy(value) {
|
||||
state.busy = value;
|
||||
document.body.classList.toggle('is-busy', value);
|
||||
$('sellerSubmit').disabled = value || !selectedSeller();
|
||||
$('usdtSubmit').disabled = value;
|
||||
$('saveAddressButton').disabled = value;
|
||||
}
|
||||
|
||||
function renderOverview() {
|
||||
if (!state.data) return;
|
||||
$('pointBalance').textContent = number(state.data.balance.available);
|
||||
$('balanceUSD').textContent =
|
||||
'≈ ' + state.data.balance.displayUSD + ' USDT';
|
||||
$('addressInput').value = state.data.address || '';
|
||||
$('addressStatus').classList.toggle(
|
||||
'has-address',
|
||||
Boolean(state.data.address)
|
||||
);
|
||||
$('addressStatus').textContent = state.data.address
|
||||
? t('fami_wallet.saved_address', 'Saved address:') +
|
||||
' ' +
|
||||
state.data.address
|
||||
: t('fami_wallet.no_address', 'No USDT address saved.');
|
||||
$('policyCaption').textContent =
|
||||
t('fami_wallet.policy_caption', 'Rate and fee are set by Admin.') +
|
||||
' · 1 USDT = ' +
|
||||
number(state.data.pointsPerUSD) +
|
||||
' POINT · ' +
|
||||
(state.data.feeBPS / 100).toFixed(2) +
|
||||
'%';
|
||||
if (!$('pointAmount').value && state.data.minimumPoints > 0) {
|
||||
$('pointAmount').value = String(state.data.minimumPoints);
|
||||
}
|
||||
renderConversion();
|
||||
}
|
||||
|
||||
function renderSellers() {
|
||||
var sellers = state.data ? state.data.sellers : [];
|
||||
if (
|
||||
sellers.length &&
|
||||
!sellers.some(function (seller) {
|
||||
return seller.userId === state.selectedSellerID;
|
||||
})
|
||||
) {
|
||||
state.selectedSellerID = sellers[0].userId;
|
||||
}
|
||||
$('sellerList').innerHTML = sellers
|
||||
.map(function (seller) {
|
||||
var selected = seller.userId === state.selectedSellerID;
|
||||
var avatar = seller.avatar
|
||||
? '<img src="' + escapeHTML(seller.avatar) + '" alt="" />'
|
||||
: escapeHTML(
|
||||
(seller.nickname || 'C').charAt(0).toUpperCase()
|
||||
);
|
||||
return (
|
||||
'<button class="seller-card" type="button" data-seller-id="' +
|
||||
escapeHTML(seller.userId) +
|
||||
'" aria-pressed="' +
|
||||
selected +
|
||||
'"><span class="seller-avatar">' +
|
||||
avatar +
|
||||
'</span><span class="seller-copy"><strong>' +
|
||||
escapeHTML(seller.nickname || '—') +
|
||||
'</strong><span>' +
|
||||
t('fami_common.uid_prefix', 'UID:') +
|
||||
' ' +
|
||||
escapeHTML(seller.displayUserId || seller.userId) +
|
||||
'</span><span class="seller-ratio">' +
|
||||
number(seller.pointAmount) +
|
||||
' : ' +
|
||||
number(seller.sellerCoinAmount) +
|
||||
'</span></span><span class="seller-check" aria-hidden="true">' +
|
||||
(selected ? '✓' : '○') +
|
||||
'</span></button>'
|
||||
);
|
||||
})
|
||||
.join('');
|
||||
$('sellerEmpty').hidden = sellers.length > 0;
|
||||
setBusy(state.busy);
|
||||
}
|
||||
|
||||
function renderHistory() {
|
||||
var items = state.data ? state.data.history || [] : [];
|
||||
$('historyList').innerHTML = items
|
||||
.map(function (item) {
|
||||
var seller = item.counterpartyUserId
|
||||
? ' · UID ' + escapeHTML(item.counterpartyUserId)
|
||||
: '';
|
||||
return (
|
||||
'<article class="history-item"><strong>' +
|
||||
escapeHTML(historyLabel(item.bizType)) +
|
||||
seller +
|
||||
'</strong><span class="history-amount">' +
|
||||
(item.availableDelta > 0 ? '+' : '') +
|
||||
number(item.availableDelta) +
|
||||
'</span><small>' +
|
||||
dateTime(item.createdAtMS) +
|
||||
'</small></article>'
|
||||
);
|
||||
})
|
||||
.join('');
|
||||
$('historyEmpty').textContent = t(
|
||||
'fami_wallet.no_history',
|
||||
'No withdrawal records'
|
||||
);
|
||||
$('historyEmpty').hidden = items.length > 0;
|
||||
if (state.data && state.data.historyUnavailable) {
|
||||
$('historyEmpty').hidden = false;
|
||||
$('historyEmpty').textContent = t(
|
||||
'fami_wallet.history_unavailable',
|
||||
'History is temporarily unavailable.'
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
function historyLabel(bizType) {
|
||||
if (bizType === 'point_transfer_to_coin_seller') {
|
||||
return t('fami_wallet.seller_withdrawal', 'Coin seller withdrawal');
|
||||
}
|
||||
if (String(bizType).indexOf('withdrawal') >= 0) {
|
||||
return t('fami_wallet.usdt_withdrawal', 'USDT withdrawal');
|
||||
}
|
||||
return t('fami_wallet.point_transaction', 'POINT transaction');
|
||||
}
|
||||
|
||||
function renderConversion() {
|
||||
var points = amount();
|
||||
if (!state.data || !points) {
|
||||
$('estimatedReceive').textContent = '—';
|
||||
return;
|
||||
}
|
||||
if (state.mode === 'seller') {
|
||||
var seller = selectedSeller();
|
||||
var receive = seller ? ratioAmount(points, seller) : '0';
|
||||
$('estimatedReceive').textContent =
|
||||
number(receive) +
|
||||
' ' +
|
||||
t('fami_wallet.seller_coin', 'seller coins');
|
||||
return;
|
||||
}
|
||||
var fee = Math.floor((points * state.data.feeBPS) / 10000);
|
||||
var net = points - fee;
|
||||
$('estimatedReceive').textContent =
|
||||
(net / state.data.pointsPerUSD).toFixed(2) + ' USDT';
|
||||
}
|
||||
|
||||
function ratioAmount(points, seller) {
|
||||
// 余额可达到万亿级,Number 中间乘法会超过安全整数;BigInt 只用于前端预计值,最终到账仍由 wallet-service 计算。
|
||||
if (typeof BigInt === 'function') {
|
||||
return (
|
||||
(BigInt(points) * BigInt(seller.sellerCoinAmount)) /
|
||||
BigInt(seller.pointAmount)
|
||||
).toString();
|
||||
}
|
||||
return String(
|
||||
Math.floor((points * seller.sellerCoinAmount) / seller.pointAmount)
|
||||
);
|
||||
}
|
||||
|
||||
function selectMode(mode) {
|
||||
state.mode = mode === 'usdt' ? 'usdt' : 'seller';
|
||||
$('sellerPanel').hidden = state.mode !== 'seller';
|
||||
$('usdtPanel').hidden = state.mode !== 'usdt';
|
||||
document.querySelectorAll('[data-mode]').forEach(function (button) {
|
||||
button.setAttribute(
|
||||
'aria-selected',
|
||||
button.getAttribute('data-mode') === state.mode
|
||||
? 'true'
|
||||
: 'false'
|
||||
);
|
||||
});
|
||||
renderConversion();
|
||||
}
|
||||
|
||||
function load() {
|
||||
setBusy(true);
|
||||
return window.HyGuild.wallet
|
||||
.load()
|
||||
.then(function (data) {
|
||||
state.data = data;
|
||||
renderOverview();
|
||||
renderSellers();
|
||||
renderHistory();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(t('fami_wallet.load_failed', 'Unable to load wallet.'));
|
||||
})
|
||||
.finally(function () {
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
function submitSeller() {
|
||||
var seller = selectedSeller();
|
||||
var points = amount();
|
||||
if (!seller || !validateAmount(points, state.data.minimumPoints))
|
||||
return;
|
||||
setBusy(true);
|
||||
window.HyGuild.wallet
|
||||
.transferToCoinSeller(seller.userId, points)
|
||||
.then(function () {
|
||||
toast(t('fami_wallet.submitted', 'Withdrawal submitted.'));
|
||||
return load();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_wallet.submit_failed',
|
||||
'Unable to submit withdrawal.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
function submitUSDT() {
|
||||
var points = amount();
|
||||
if (!validateAmount(points, state.data.minimumPoints)) return;
|
||||
var address = String($('addressInput').value || '').trim();
|
||||
if (!validTRC20(address)) {
|
||||
toast(
|
||||
t('fami_wallet.invalid_address', 'Enter a valid TRC20 address.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
window.HyGuild.wallet
|
||||
.withdrawUSDT(points, address)
|
||||
.then(function () {
|
||||
toast(t('fami_wallet.submitted', 'Withdrawal submitted.'));
|
||||
return load();
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_wallet.submit_failed',
|
||||
'Unable to submit withdrawal.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
function saveAddress() {
|
||||
var address = String($('addressInput').value || '').trim();
|
||||
if (!validTRC20(address)) {
|
||||
toast(
|
||||
t('fami_wallet.invalid_address', 'Enter a valid TRC20 address.')
|
||||
);
|
||||
return;
|
||||
}
|
||||
setBusy(true);
|
||||
window.HyGuild.wallet
|
||||
.saveAddress(address)
|
||||
.then(function () {
|
||||
state.data.address = address;
|
||||
renderOverview();
|
||||
toast(t('fami_wallet.address_saved', 'Address saved.'));
|
||||
})
|
||||
.catch(function () {
|
||||
toast(
|
||||
t(
|
||||
'fami_wallet.address_save_failed',
|
||||
'Unable to save address.'
|
||||
)
|
||||
);
|
||||
})
|
||||
.finally(function () {
|
||||
setBusy(false);
|
||||
});
|
||||
}
|
||||
|
||||
function validateAmount(points, minimum) {
|
||||
if (!points || !state.data || points > state.data.balance.available) {
|
||||
toast(
|
||||
t(
|
||||
'fami_wallet.invalid_amount',
|
||||
'Enter an available POINT amount.'
|
||||
)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
if (points < minimum) {
|
||||
toast(
|
||||
t('fami_wallet.minimum_amount', 'Minimum POINT amount:') +
|
||||
' ' +
|
||||
number(minimum)
|
||||
);
|
||||
return false;
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
function validTRC20(value) {
|
||||
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(value);
|
||||
}
|
||||
|
||||
function escapeHTML(value) {
|
||||
return String(value || '')
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
function dateTime(value) {
|
||||
if (!Number(value)) return '—';
|
||||
return new Date(Number(value)).toLocaleString();
|
||||
}
|
||||
|
||||
function bind() {
|
||||
$('backButton').addEventListener('click', function () {
|
||||
if (window.history.length > 1) window.history.back();
|
||||
else if (window.HyAppBridge) window.HyAppBridge.back();
|
||||
});
|
||||
document
|
||||
.querySelector('.wallet-tabs')
|
||||
.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-mode]');
|
||||
if (button) selectMode(button.getAttribute('data-mode'));
|
||||
});
|
||||
$('sellerList').addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-seller-id]');
|
||||
if (!button || state.busy) return;
|
||||
state.selectedSellerID = button.getAttribute('data-seller-id');
|
||||
renderSellers();
|
||||
renderConversion();
|
||||
});
|
||||
$('pointAmount').addEventListener('input', renderConversion);
|
||||
$('allButton').addEventListener('click', function () {
|
||||
if (!state.data) return;
|
||||
$('pointAmount').value = String(state.data.balance.available);
|
||||
renderConversion();
|
||||
});
|
||||
$('sellerSubmit').addEventListener('click', submitSeller);
|
||||
$('usdtSubmit').addEventListener('click', submitUSDT);
|
||||
$('saveAddressButton').addEventListener('click', saveAddress);
|
||||
window.addEventListener('hyapp:i18n-ready', function () {
|
||||
renderOverview();
|
||||
renderHistory();
|
||||
});
|
||||
}
|
||||
|
||||
function init() {
|
||||
bind();
|
||||
selectMode('seller');
|
||||
load().finally(function () {
|
||||
if (window.HyAppBridge) {
|
||||
window.HyAppBridge.ready({
|
||||
page: 'fami-wallet',
|
||||
app_code: 'fami',
|
||||
});
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
396
guild/fami/wallet/style.css
Normal file
396
guild/fami/wallet/style.css
Normal file
@ -0,0 +1,396 @@
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
min-height: 100%;
|
||||
margin: 0;
|
||||
background: var(--hy-theme-bg);
|
||||
color: var(--hy-theme-text);
|
||||
font-family:
|
||||
Inter,
|
||||
-apple-system,
|
||||
BlinkMacSystemFont,
|
||||
'Segoe UI',
|
||||
sans-serif;
|
||||
}
|
||||
|
||||
button,
|
||||
input {
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
cursor: pointer;
|
||||
}
|
||||
|
||||
.center-header {
|
||||
position: sticky;
|
||||
z-index: 20;
|
||||
top: 0;
|
||||
display: grid;
|
||||
grid-template-columns: 48px 1fr 48px;
|
||||
align-items: center;
|
||||
min-height: calc(64px + env(safe-area-inset-top));
|
||||
padding: env(safe-area-inset-top) 12px 0;
|
||||
border-bottom: 1px solid var(--hy-theme-line);
|
||||
background: var(--hy-theme-bg);
|
||||
}
|
||||
|
||||
.center-header h1 {
|
||||
margin: 0;
|
||||
font-size: 22px;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.icon-button,
|
||||
.language-button {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 40px;
|
||||
place-items: center;
|
||||
border: 0;
|
||||
border-radius: var(--hy-theme-radius-language);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
|
||||
.icon-button span {
|
||||
font-size: 38px;
|
||||
font-weight: 300;
|
||||
line-height: 30px;
|
||||
}
|
||||
|
||||
.language-switcher {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.language-button {
|
||||
font-size: 13px;
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.language-menu {
|
||||
position: absolute;
|
||||
top: 46px;
|
||||
right: 0;
|
||||
overflow: hidden;
|
||||
width: 64px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-small);
|
||||
background: var(--hy-theme-surface);
|
||||
box-shadow: var(--hy-theme-modal-shadow);
|
||||
}
|
||||
|
||||
[dir='rtl'] .language-menu {
|
||||
right: auto;
|
||||
left: 0;
|
||||
}
|
||||
|
||||
.language-menu button {
|
||||
width: 100%;
|
||||
padding: 10px;
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.wallet-content {
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
.wallet-tabs {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(2, minmax(0, 1fr));
|
||||
padding: 4px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-surface);
|
||||
}
|
||||
|
||||
.wallet-tabs button {
|
||||
min-height: 40px;
|
||||
border: 0;
|
||||
border-radius: calc(var(--hy-theme-radius-control) - 4px);
|
||||
background: transparent;
|
||||
color: var(--hy-theme-muted);
|
||||
font-weight: 700;
|
||||
}
|
||||
|
||||
.wallet-tabs button[aria-selected='true'] {
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.wallet-balance-card,
|
||||
.amount-card {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
padding: 18px;
|
||||
}
|
||||
|
||||
.wallet-balance-card > span,
|
||||
.amount-card > label,
|
||||
.amount-card > small {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.wallet-balance-card strong {
|
||||
overflow-wrap: anywhere;
|
||||
color: var(--hy-theme-text);
|
||||
font-size: clamp(27px, 8vw, 38px);
|
||||
line-height: 1.1;
|
||||
}
|
||||
|
||||
.wallet-balance-card small {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.amount-input-row {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 8px;
|
||||
min-height: 48px;
|
||||
padding: 0 12px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-surface-soft);
|
||||
}
|
||||
|
||||
.amount-input-row > span {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.amount-input-row input,
|
||||
.address-label input {
|
||||
min-width: 0;
|
||||
border: 0;
|
||||
outline: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-text);
|
||||
font: inherit;
|
||||
}
|
||||
|
||||
.amount-input-row input {
|
||||
width: 100%;
|
||||
font-size: 18px;
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.amount-input-row button {
|
||||
border: 0;
|
||||
background: transparent;
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 750;
|
||||
}
|
||||
|
||||
.conversion-row {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
}
|
||||
|
||||
.conversion-row strong {
|
||||
color: var(--hy-theme-text);
|
||||
font-size: 15px;
|
||||
}
|
||||
|
||||
#sellerPanel,
|
||||
#usdtPanel,
|
||||
.history-section {
|
||||
display: grid;
|
||||
gap: 12px;
|
||||
}
|
||||
|
||||
#sellerPanel[hidden],
|
||||
#usdtPanel[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
.section-heading h2 {
|
||||
margin: 2px 0;
|
||||
color: var(--hy-theme-text);
|
||||
font-size: 16px;
|
||||
}
|
||||
|
||||
.section-heading p {
|
||||
margin: 4px 0 0;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 12px;
|
||||
line-height: 1.5;
|
||||
}
|
||||
|
||||
.seller-list,
|
||||
.history-list {
|
||||
display: grid;
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
.seller-card {
|
||||
display: grid;
|
||||
grid-template-columns: auto minmax(0, 1fr) auto;
|
||||
align-items: center;
|
||||
gap: 12px;
|
||||
width: 100%;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
text-align: start;
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
|
||||
.seller-card[aria-pressed='true'] {
|
||||
border-color: var(--hy-theme-primary-strong);
|
||||
box-shadow: 0 0 0 2px var(--hy-theme-focus-ring);
|
||||
}
|
||||
|
||||
.seller-avatar {
|
||||
display: grid;
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
place-items: center;
|
||||
overflow: hidden;
|
||||
border-radius: var(--hy-theme-radius-circle);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.seller-avatar img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
|
||||
.seller-copy {
|
||||
display: grid;
|
||||
min-width: 0;
|
||||
gap: 3px;
|
||||
}
|
||||
|
||||
.seller-copy strong,
|
||||
.seller-copy span {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.seller-copy span,
|
||||
.seller-ratio {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.seller-check {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
font-size: 20px;
|
||||
}
|
||||
|
||||
.wallet-submit,
|
||||
.secondary-submit {
|
||||
min-height: 48px;
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.wallet-submit {
|
||||
border: 0;
|
||||
background: var(--hy-theme-button);
|
||||
color: white;
|
||||
box-shadow: var(--hy-theme-button-shadow);
|
||||
}
|
||||
|
||||
.secondary-submit {
|
||||
border: 1px solid var(--hy-theme-primary-strong);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.wallet-submit:disabled,
|
||||
.secondary-submit:disabled {
|
||||
background: var(--hy-theme-button-disabled);
|
||||
color: var(--hy-theme-button-disabled-text);
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.address-status,
|
||||
.address-label {
|
||||
padding: 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-surface);
|
||||
}
|
||||
|
||||
.address-status {
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 13px;
|
||||
overflow-wrap: anywhere;
|
||||
}
|
||||
|
||||
.address-status.has-address {
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
.address-label {
|
||||
display: grid;
|
||||
gap: 8px;
|
||||
color: var(--hy-theme-muted);
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.address-label input {
|
||||
padding: 6px 0;
|
||||
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
||||
}
|
||||
|
||||
.history-item {
|
||||
display: grid;
|
||||
grid-template-columns: minmax(0, 1fr) auto;
|
||||
gap: 6px 12px;
|
||||
padding: 14px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-surface);
|
||||
}
|
||||
|
||||
.history-item strong {
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.history-item .history-amount {
|
||||
color: var(--hy-theme-danger);
|
||||
font-weight: 800;
|
||||
}
|
||||
|
||||
.history-item small {
|
||||
grid-column: 1 / -1;
|
||||
color: var(--hy-theme-muted);
|
||||
}
|
||||
|
||||
.empty-state {
|
||||
display: grid;
|
||||
gap: 5px;
|
||||
padding: 20px;
|
||||
border: 1px dashed var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
color: var(--hy-theme-muted);
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.empty-state[hidden] {
|
||||
display: none;
|
||||
}
|
||||
|
||||
body.is-busy button,
|
||||
body.is-busy input {
|
||||
pointer-events: none;
|
||||
}
|
||||
243
guild/packages/agency-center/agency-center.js
Normal file
243
guild/packages/agency-center/agency-center.js
Normal file
@ -0,0 +1,243 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function params() {
|
||||
return new URLSearchParams(window.location.search || '');
|
||||
}
|
||||
|
||||
function settled(promise) {
|
||||
return Promise.resolve(promise).then(
|
||||
function (value) {
|
||||
return { ok: true, value: value };
|
||||
},
|
||||
function (error) {
|
||||
return { ok: false, error: error };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function firstBalance(payload, assetType) {
|
||||
var items = Array.isArray(payload)
|
||||
? payload
|
||||
: (payload && (payload.balances || payload.items)) || [];
|
||||
return (
|
||||
items.find(function (item) {
|
||||
return (
|
||||
String(
|
||||
item.asset_type || item.assetType || ''
|
||||
).toUpperCase() === assetType
|
||||
);
|
||||
}) || null
|
||||
);
|
||||
}
|
||||
|
||||
function normalizeProfile(payload) {
|
||||
var profile =
|
||||
(payload && (payload.profile || payload.user)) || payload || {};
|
||||
return {
|
||||
contact: String(profile.contact_info || profile.contactInfo || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOverview(payload) {
|
||||
var agency = (payload && payload.agency) || {};
|
||||
return {
|
||||
agency: {
|
||||
id: String(agency.agency_id || agency.agencyId || ''),
|
||||
shortId: String(
|
||||
agency.short_id ||
|
||||
agency.shortId ||
|
||||
agency.agency_id ||
|
||||
agency.agencyId ||
|
||||
''
|
||||
),
|
||||
name: String(agency.name || ''),
|
||||
avatar: String(agency.avatar || agency.avatar_url || ''),
|
||||
createdAtMS: Number(
|
||||
agency.created_at_ms || agency.createdAtMs || 0
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStats(payload, range) {
|
||||
var source = payload || {};
|
||||
var summary = source.summary || {};
|
||||
return {
|
||||
range: source.range || range,
|
||||
summary: {
|
||||
total_earnings: Number(summary.total_earnings || 0),
|
||||
gift_income: Number(summary.gift_income || 0),
|
||||
share_income: Number(summary.share_income || 0),
|
||||
total_hosts: Number(summary.total_hosts || 0),
|
||||
gifted_host_count: Number(summary.gifted_host_count || 0),
|
||||
},
|
||||
hosts: Array.isArray(source.hosts)
|
||||
? source.hosts.map(function (host) {
|
||||
return {
|
||||
membershipId: String(host.membership_id || ''),
|
||||
userId: String(host.host_user_id || ''),
|
||||
displayUserId: String(
|
||||
host.display_user_id || host.host_user_id || ''
|
||||
),
|
||||
name: String(host.username || ''),
|
||||
avatar: String(host.avatar || ''),
|
||||
joinedAtMS: Number(host.joined_at_ms || 0),
|
||||
diamondEarnings: Number(host.diamond_earnings || 0),
|
||||
diamondExchanged: Number(host.diamond_exchanged || 0),
|
||||
giftSenders: Number(host.gift_senders || 0),
|
||||
onlineDurationMS: Number(
|
||||
host.online_duration_ms || 0
|
||||
),
|
||||
validMicDurationMS: Number(
|
||||
host.valid_mic_duration_ms || 0
|
||||
),
|
||||
validMicDays: Number(host.valid_mic_days || 0),
|
||||
removable: host.removable !== false,
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mockData(range) {
|
||||
return {
|
||||
agency: {
|
||||
id: '343434',
|
||||
shortId: '343434',
|
||||
name: 'Fami Stars Agency',
|
||||
avatar: '',
|
||||
createdAtMS: new Date(2025, 9, 12, 15, 0).getTime(),
|
||||
},
|
||||
contact: '+20 1524151515',
|
||||
pointBalance: 5152051511212,
|
||||
stats: normalizeStats(
|
||||
{
|
||||
summary: {
|
||||
total_earnings: 10000,
|
||||
gift_income: 10000,
|
||||
share_income: 10000,
|
||||
total_hosts: 1,
|
||||
gifted_host_count: 1,
|
||||
},
|
||||
hosts: [
|
||||
{
|
||||
membership_id: '8001',
|
||||
host_user_id: '3534',
|
||||
display_user_id: '3534',
|
||||
username: 'Host dada',
|
||||
diamond_earnings: 343434,
|
||||
diamond_exchanged: 343434,
|
||||
gift_senders: 343434,
|
||||
online_duration_ms: 165 * 60 * 1000,
|
||||
valid_mic_duration_ms: 152 * 60 * 1000,
|
||||
valid_mic_days: 12,
|
||||
joined_at_ms: new Date(
|
||||
2025,
|
||||
4,
|
||||
31,
|
||||
15,
|
||||
0
|
||||
).getTime(),
|
||||
},
|
||||
],
|
||||
},
|
||||
range
|
||||
),
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
|
||||
function load(range) {
|
||||
if (params().get('mock') === '1')
|
||||
return Promise.resolve(mockData(range));
|
||||
var api = window.HyAppAPI || {};
|
||||
if (!api.user || !api.agencyCenter || !api.wallet) {
|
||||
return Promise.reject(new Error('guild_api_unavailable'));
|
||||
}
|
||||
return Promise.all([
|
||||
settled(api.user.me()),
|
||||
settled(api.agencyCenter.overview()),
|
||||
settled(api.wallet.balances(['POINT'])),
|
||||
settled(
|
||||
api.agencyCenter.stats({
|
||||
start_date: range.startDate,
|
||||
end_date: range.endDate,
|
||||
})
|
||||
),
|
||||
]).then(function (results) {
|
||||
// owner 身份和 agency 资料是页面最小事实;余额/统计允许独立降级,避免聚合服务故障遮住管理入口。
|
||||
if (!results[0].ok) throw results[0].error;
|
||||
if (!results[1].ok) throw results[1].error;
|
||||
var overview = normalizeOverview(results[1].value);
|
||||
var profile = normalizeProfile(results[0].value);
|
||||
var point = results[2].ok
|
||||
? firstBalance(results[2].value, 'POINT')
|
||||
: null;
|
||||
return {
|
||||
agency: overview.agency,
|
||||
contact: profile.contact,
|
||||
pointBalance: point
|
||||
? Number(
|
||||
point.available_amount || point.availableAmount || 0
|
||||
)
|
||||
: null,
|
||||
stats: results[3].ok
|
||||
? normalizeStats(results[3].value, range)
|
||||
: normalizeStats({}, range),
|
||||
errors: {
|
||||
balance: results[2].ok ? null : results[2].error,
|
||||
stats: results[3].ok ? null : results[3].error,
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function commandID(prefix) {
|
||||
var random =
|
||||
window.crypto && window.crypto.randomUUID
|
||||
? window.crypto.randomUUID()
|
||||
: Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||
return prefix + ':' + random;
|
||||
}
|
||||
|
||||
function saveContact(value) {
|
||||
if (params().get('mock') === '1') return Promise.resolve({});
|
||||
return window.HyAppAPI.user.updateContact({ contact_info: value });
|
||||
}
|
||||
|
||||
function inviteHost(displayUserID) {
|
||||
if (params().get('mock') === '1') return Promise.resolve({});
|
||||
var api = window.HyAppAPI;
|
||||
// 邀请写接口只接受内部 user_id;先通过公共解析接口把用户输入的展示 ID 转换,禁止前端猜测两种 ID 相同。
|
||||
return api.user
|
||||
.resolveDisplayUserID(displayUserID)
|
||||
.then(function (payload) {
|
||||
var user =
|
||||
(payload && (payload.user || payload.profile)) ||
|
||||
payload ||
|
||||
{};
|
||||
var userID = user.user_id || user.userId;
|
||||
if (!userID) throw new Error('host_not_found');
|
||||
return api.agencyCenter.inviteHost({
|
||||
command_id: commandID('fami-agency-invite-host'),
|
||||
target_user_id: String(userID),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function removeHost(hostUserID) {
|
||||
if (params().get('mock') === '1') return Promise.resolve({});
|
||||
return window.HyAppAPI.agencyCenter.removeHost(hostUserID, {
|
||||
command_id: commandID('fami-agency-remove-host'),
|
||||
});
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.agencyCenter = {
|
||||
load: load,
|
||||
saveContact: saveContact,
|
||||
inviteHost: inviteHost,
|
||||
removeHost: removeHost,
|
||||
};
|
||||
})();
|
||||
161
guild/packages/bd-center/bd-center.js
Normal file
161
guild/packages/bd-center/bd-center.js
Normal file
@ -0,0 +1,161 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function params() {
|
||||
return new URLSearchParams(window.location.search || '');
|
||||
}
|
||||
|
||||
function normalizeOverview(payload) {
|
||||
var profile = (payload && payload.profile) || {};
|
||||
var role = (payload && payload.bd_profile) || {};
|
||||
return {
|
||||
profile: {
|
||||
userId: String(profile.user_id || role.user_id || ''),
|
||||
displayUserId: String(
|
||||
profile.display_user_id ||
|
||||
profile.user_id ||
|
||||
role.user_id ||
|
||||
''
|
||||
),
|
||||
name: String(profile.username || ''),
|
||||
avatar: String(profile.avatar || ''),
|
||||
contact: String(
|
||||
profile.contact_info || profile.contactInfo || ''
|
||||
),
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStats(payload, range) {
|
||||
return {
|
||||
range: (payload && payload.range) || range,
|
||||
agencies: Array.isArray(payload && payload.items)
|
||||
? payload.items.map(function (item) {
|
||||
var agency = item.agency || {};
|
||||
var owner = item.owner || {};
|
||||
return {
|
||||
id: String(agency.agency_id || ''),
|
||||
shortId: String(
|
||||
agency.short_id || agency.agency_id || ''
|
||||
),
|
||||
name: String(agency.name || ''),
|
||||
avatar: String(agency.avatar || ''),
|
||||
ownerId: String(
|
||||
owner.display_user_id || owner.user_id || ''
|
||||
),
|
||||
ownerName: String(owner.username || ''),
|
||||
country: String(item.country || owner.country || ''),
|
||||
totalHosts: Number(item.total_hosts || 0),
|
||||
giftedHostCount: Number(item.gifted_host_count || 0),
|
||||
diamondEarnings: Number(item.diamond_earnings || 0),
|
||||
joinedAtMS: Number(item.joined_at_ms || 0),
|
||||
};
|
||||
})
|
||||
: [],
|
||||
};
|
||||
}
|
||||
|
||||
function mockData(range) {
|
||||
return {
|
||||
profile: {
|
||||
userId: '163028',
|
||||
displayUserId: '163028',
|
||||
name: 'dada',
|
||||
avatar: '',
|
||||
contact: '1586668885',
|
||||
},
|
||||
stats: normalizeStats(
|
||||
{
|
||||
items: [
|
||||
{
|
||||
agency: {
|
||||
agency_id: '343434',
|
||||
short_id: '343434',
|
||||
name: 'Fami Stars',
|
||||
},
|
||||
owner: {
|
||||
display_user_id: '3534',
|
||||
username: 'Agency owner',
|
||||
},
|
||||
country: 'Saudi Arabia',
|
||||
total_hosts: 12,
|
||||
gifted_host_count: 8,
|
||||
diamond_earnings: 5152051,
|
||||
joined_at_ms: new Date(
|
||||
2025,
|
||||
4,
|
||||
31,
|
||||
15,
|
||||
0
|
||||
).getTime(),
|
||||
},
|
||||
],
|
||||
},
|
||||
range
|
||||
),
|
||||
};
|
||||
}
|
||||
|
||||
function load(range) {
|
||||
if (params().get('mock') === '1')
|
||||
return Promise.resolve(mockData(range));
|
||||
var api = window.HyAppAPI || {};
|
||||
if (!api.bdCenter || !api.user) {
|
||||
return Promise.reject(new Error('guild_api_unavailable'));
|
||||
}
|
||||
return Promise.all([
|
||||
api.bdCenter.overview(),
|
||||
api.bdCenter.stats({
|
||||
start_date: range.startDate,
|
||||
end_date: range.endDate,
|
||||
}),
|
||||
]).then(function (results) {
|
||||
var overview = normalizeOverview(results[0]);
|
||||
return {
|
||||
profile: overview.profile,
|
||||
stats: normalizeStats(results[1], range),
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function commandID() {
|
||||
var random =
|
||||
window.crypto && window.crypto.randomUUID
|
||||
? window.crypto.randomUUID()
|
||||
: Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||
return 'fami-bd-invite-agency:' + random;
|
||||
}
|
||||
|
||||
function saveContact(value) {
|
||||
if (params().get('mock') === '1') return Promise.resolve({});
|
||||
return window.HyAppAPI.user.updateContact({ contact_info: value });
|
||||
}
|
||||
|
||||
function inviteAgency(displayUserID, agencyName) {
|
||||
if (params().get('mock') === '1') return Promise.resolve({});
|
||||
var api = window.HyAppAPI;
|
||||
// BD 写接口使用内部 user_id;展示 ID 解析由 gateway 完成,避免把短号误当主键。
|
||||
return api.user
|
||||
.resolveDisplayUserID(displayUserID)
|
||||
.then(function (payload) {
|
||||
var user =
|
||||
(payload && (payload.user || payload.profile)) ||
|
||||
payload ||
|
||||
{};
|
||||
var userID = user.user_id || user.userId;
|
||||
if (!userID) throw new Error('agency_owner_not_found');
|
||||
return api.bdCenter.inviteAgency({
|
||||
command_id: commandID(),
|
||||
target_user_id: String(userID),
|
||||
agency_name: String(agencyName || '').trim(),
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.bdCenter = {
|
||||
load: load,
|
||||
saveContact: saveContact,
|
||||
inviteAgency: inviteAgency,
|
||||
};
|
||||
})();
|
||||
90
guild/packages/core/date-range.js
Normal file
90
guild/packages/core/date-range.js
Normal file
@ -0,0 +1,90 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var PRESETS = [
|
||||
'today',
|
||||
'yesterday',
|
||||
'last_7_days',
|
||||
'last_30_days',
|
||||
'this_month',
|
||||
'last_month',
|
||||
'custom',
|
||||
];
|
||||
|
||||
function pad(value) {
|
||||
return String(value).padStart(2, '0');
|
||||
}
|
||||
|
||||
function dateText(date) {
|
||||
return (
|
||||
date.getFullYear() +
|
||||
'-' +
|
||||
pad(date.getMonth() + 1) +
|
||||
'-' +
|
||||
pad(date.getDate())
|
||||
);
|
||||
}
|
||||
|
||||
function localDate(value) {
|
||||
if (value instanceof Date) {
|
||||
return new Date(
|
||||
value.getFullYear(),
|
||||
value.getMonth(),
|
||||
value.getDate()
|
||||
);
|
||||
}
|
||||
var match = String(value || '').match(/^(\d{4})-(\d{2})-(\d{2})$/);
|
||||
if (!match) return null;
|
||||
var date = new Date(
|
||||
Number(match[1]),
|
||||
Number(match[2]) - 1,
|
||||
Number(match[3])
|
||||
);
|
||||
return Number.isNaN(date.getTime()) ? null : date;
|
||||
}
|
||||
|
||||
function addDays(date, count) {
|
||||
return new Date(
|
||||
date.getFullYear(),
|
||||
date.getMonth(),
|
||||
date.getDate() + count
|
||||
);
|
||||
}
|
||||
|
||||
function createRange(preset, options) {
|
||||
var name = PRESETS.indexOf(preset) >= 0 ? preset : 'today';
|
||||
var opts = options || {};
|
||||
var now = localDate(opts.now || new Date());
|
||||
var start = now;
|
||||
var end = now;
|
||||
|
||||
if (name === 'yesterday') start = end = addDays(now, -1);
|
||||
else if (name === 'last_7_days') start = addDays(now, -6);
|
||||
else if (name === 'last_30_days') start = addDays(now, -29);
|
||||
else if (name === 'this_month')
|
||||
start = new Date(now.getFullYear(), now.getMonth(), 1);
|
||||
else if (name === 'last_month') {
|
||||
start = new Date(now.getFullYear(), now.getMonth() - 1, 1);
|
||||
end = new Date(now.getFullYear(), now.getMonth(), 0);
|
||||
} else if (name === 'custom') {
|
||||
start = localDate(opts.start);
|
||||
end = localDate(opts.end);
|
||||
if (!start || !end || start.getTime() > end.getTime()) {
|
||||
throw new Error('Invalid custom date range');
|
||||
}
|
||||
}
|
||||
|
||||
// UI 和 API 都使用包含首尾的自然日;在线时长等跨日指标由服务端按业务时区切日,前端不自行累计秒数。
|
||||
return Object.freeze({
|
||||
preset: name,
|
||||
startDate: dateText(start),
|
||||
endDate: dateText(end),
|
||||
});
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.dateRange = {
|
||||
presets: Object.freeze(PRESETS.slice()),
|
||||
create: createRange,
|
||||
};
|
||||
})();
|
||||
111
guild/packages/core/product-context.js
Normal file
111
guild/packages/core/product-context.js
Normal file
@ -0,0 +1,111 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var CONTEXT_QUERY_KEYS = [
|
||||
'token',
|
||||
'access_token',
|
||||
'accessToken',
|
||||
'env',
|
||||
'language',
|
||||
'lang',
|
||||
'locale',
|
||||
'app_lang',
|
||||
'appLanguage',
|
||||
'mock',
|
||||
];
|
||||
var product = null;
|
||||
|
||||
function normalizeSlug(value, field) {
|
||||
var slug = String(value || '')
|
||||
.trim()
|
||||
.toLowerCase();
|
||||
if (!/^[a-z][a-z0-9_-]*$/.test(slug)) {
|
||||
throw new Error('Invalid guild product ' + field + ': ' + slug);
|
||||
}
|
||||
return slug;
|
||||
}
|
||||
|
||||
function freezeArray(value) {
|
||||
return Object.freeze(Array.isArray(value) ? value.slice() : []);
|
||||
}
|
||||
|
||||
function defineProduct(config) {
|
||||
var next = config || {};
|
||||
var normalized = Object.freeze({
|
||||
key: normalizeSlug(next.key, 'key'),
|
||||
appCode: normalizeSlug(next.appCode, 'appCode'),
|
||||
languages: freezeArray(next.languages),
|
||||
features: Object.freeze(
|
||||
Object.assign(
|
||||
{
|
||||
hostCenter: true,
|
||||
agencyCenter: true,
|
||||
bdCenter: true,
|
||||
wallet: true,
|
||||
},
|
||||
next.features || {}
|
||||
)
|
||||
),
|
||||
});
|
||||
|
||||
// 同一页面只能属于一个产品;静默覆盖会让后加载的品牌包改变请求租户,属于高风险串数据。
|
||||
if (product && product.key !== normalized.key) {
|
||||
throw new Error('Guild product already defined as ' + product.key);
|
||||
}
|
||||
product = normalized;
|
||||
window.HyAppDefaultAppCode = normalized.appCode;
|
||||
window.HyAppI18nSupported = normalized.languages;
|
||||
document.documentElement.setAttribute(
|
||||
'data-hy-product',
|
||||
normalized.key
|
||||
);
|
||||
return product;
|
||||
}
|
||||
|
||||
function getProduct() {
|
||||
if (!product) throw new Error('Guild product is not defined');
|
||||
return product;
|
||||
}
|
||||
|
||||
function currentParams() {
|
||||
var params = new URLSearchParams(window.location.search || '');
|
||||
var hash = window.location.hash || '';
|
||||
if (hash.indexOf('?') >= 0) {
|
||||
new URLSearchParams(hash.split('?').slice(1).join('?')).forEach(
|
||||
function (value, key) {
|
||||
if (!params.has(key)) params.set(key, value);
|
||||
}
|
||||
);
|
||||
}
|
||||
return params;
|
||||
}
|
||||
|
||||
function buildURL(path, extra) {
|
||||
var target = new URL(path, window.location.href);
|
||||
var source = currentParams();
|
||||
|
||||
// 页面跳转只继承认证、环境和语言上下文,不复制筛选、分页等页面状态,避免不同中心解释同名参数。
|
||||
CONTEXT_QUERY_KEYS.forEach(function (key) {
|
||||
if (source.has(key) && !target.searchParams.has(key)) {
|
||||
target.searchParams.set(key, source.get(key));
|
||||
}
|
||||
});
|
||||
target.searchParams.set('app_code', getProduct().appCode);
|
||||
Object.keys(extra || {}).forEach(function (key) {
|
||||
var value = extra[key];
|
||||
if (value === undefined || value === null || value === '') {
|
||||
target.searchParams.delete(key);
|
||||
return;
|
||||
}
|
||||
target.searchParams.set(key, String(value));
|
||||
});
|
||||
return target.toString();
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.product = {
|
||||
define: defineProduct,
|
||||
get: getProduct,
|
||||
buildURL: buildURL,
|
||||
};
|
||||
})();
|
||||
196
guild/packages/host-center/host-center.js
Normal file
196
guild/packages/host-center/host-center.js
Normal file
@ -0,0 +1,196 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var METRICS = [
|
||||
{ key: 'diamond_earnings', format: 'count' },
|
||||
{ key: 'diamond_exchanged', format: 'count' },
|
||||
{ key: 'gift_senders', format: 'count' },
|
||||
{ key: 'online_duration_ms', format: 'duration' },
|
||||
{ key: 'valid_mic_duration_ms', format: 'duration' },
|
||||
{ key: 'valid_mic_days', format: 'count' },
|
||||
{ key: 'private_message_senders', format: 'count' },
|
||||
{ key: 'new_followers', format: 'count' },
|
||||
];
|
||||
|
||||
function params() {
|
||||
return new URLSearchParams(window.location.search || '');
|
||||
}
|
||||
|
||||
function settled(promise) {
|
||||
return Promise.resolve(promise).then(
|
||||
function (value) {
|
||||
return { ok: true, value: value };
|
||||
},
|
||||
function (error) {
|
||||
return { ok: false, error: error };
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function firstBalance(payload, assetType) {
|
||||
var items = Array.isArray(payload)
|
||||
? payload
|
||||
: (payload && (payload.balances || payload.items)) || [];
|
||||
for (var i = 0; i < items.length; i += 1) {
|
||||
var item = items[i] || {};
|
||||
if (
|
||||
String(
|
||||
item.asset_type || item.assetType || ''
|
||||
).toUpperCase() === assetType
|
||||
) {
|
||||
return item;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function normalizeProfile(payload) {
|
||||
var profile =
|
||||
(payload && (payload.profile || payload.user)) || payload || {};
|
||||
return {
|
||||
userId: String(profile.user_id || profile.userId || ''),
|
||||
displayUserId: String(
|
||||
profile.display_user_id ||
|
||||
profile.displayUserId ||
|
||||
profile.user_id ||
|
||||
''
|
||||
),
|
||||
name: String(profile.username || profile.name || ''),
|
||||
avatar: String(profile.avatar || profile.avatar_url || ''),
|
||||
contact: String(profile.contact_info || profile.contactInfo || ''),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeAgency(payload) {
|
||||
var agency =
|
||||
payload && Object.prototype.hasOwnProperty.call(payload, 'agency')
|
||||
? payload.agency
|
||||
: payload;
|
||||
if (!agency) return null;
|
||||
return {
|
||||
id: String(agency.agency_id || agency.agencyId || ''),
|
||||
shortId: String(
|
||||
agency.short_id ||
|
||||
agency.shortId ||
|
||||
agency.agency_id ||
|
||||
agency.agencyId ||
|
||||
''
|
||||
),
|
||||
name: String(agency.name || ''),
|
||||
joinedAtMS: Number(agency.joined_at_ms || agency.joinedAtMs || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeStats(payload, range) {
|
||||
var source =
|
||||
(payload && (payload.metrics || payload.stats)) || payload || {};
|
||||
var metrics = {};
|
||||
METRICS.forEach(function (definition) {
|
||||
var value = source[definition.key];
|
||||
metrics[definition.key] =
|
||||
value === undefined || value === null ? null : Number(value);
|
||||
});
|
||||
return {
|
||||
range: (payload && payload.range) || range,
|
||||
metrics: metrics,
|
||||
};
|
||||
}
|
||||
|
||||
function mockData(range) {
|
||||
return {
|
||||
profile: {
|
||||
userId: '163028',
|
||||
displayUserId: '163028',
|
||||
name: 'dada',
|
||||
avatar: '',
|
||||
contact: '1586668885',
|
||||
},
|
||||
agency: {
|
||||
id: '163030',
|
||||
shortId: '163030',
|
||||
name: 'Fami Stars',
|
||||
joinedAtMS: new Date(2025, 5, 12, 16, 0).getTime(),
|
||||
},
|
||||
pointBalance: 5152051511212,
|
||||
stats: normalizeStats(
|
||||
{
|
||||
diamond_earnings: 50000,
|
||||
diamond_exchanged: 50000,
|
||||
gift_senders: 50000,
|
||||
online_duration_ms: 53 * 60 * 1000,
|
||||
valid_mic_duration_ms: 53 * 60 * 1000,
|
||||
valid_mic_days: 12,
|
||||
private_message_senders: 50000,
|
||||
new_followers: 50000,
|
||||
},
|
||||
range
|
||||
),
|
||||
errors: {},
|
||||
};
|
||||
}
|
||||
|
||||
function load(range) {
|
||||
if (params().get('mock') === '1') {
|
||||
return Promise.resolve(mockData(range));
|
||||
}
|
||||
var api = window.HyAppAPI || {};
|
||||
if (!api.user || !api.hostCenter || !api.wallet) {
|
||||
return Promise.reject(new Error('guild_api_unavailable'));
|
||||
}
|
||||
|
||||
return Promise.all([
|
||||
settled(api.user.me()),
|
||||
settled(api.hostCenter.agency()),
|
||||
settled(api.wallet.balances(['POINT'])),
|
||||
settled(
|
||||
api.hostCenter.stats({
|
||||
start_date: range.startDate,
|
||||
end_date: range.endDate,
|
||||
})
|
||||
),
|
||||
]).then(function (results) {
|
||||
// 资料是页面身份校验的最低要求;其余读模型可以分别降级,不能因统计服务晚发布让联系方式和余额也不可见。
|
||||
if (!results[0].ok) throw results[0].error;
|
||||
var errors = {};
|
||||
if (!results[1].ok) errors.agency = results[1].error;
|
||||
if (!results[2].ok) errors.balance = results[2].error;
|
||||
if (!results[3].ok) errors.stats = results[3].error;
|
||||
var point = results[2].ok
|
||||
? firstBalance(results[2].value, 'POINT')
|
||||
: null;
|
||||
return {
|
||||
profile: normalizeProfile(results[0].value),
|
||||
agency: results[1].ok
|
||||
? normalizeAgency(results[1].value)
|
||||
: null,
|
||||
pointBalance: point
|
||||
? Number(
|
||||
point.available_amount || point.availableAmount || 0
|
||||
)
|
||||
: null,
|
||||
stats: results[3].ok
|
||||
? normalizeStats(results[3].value, range)
|
||||
: normalizeStats({}, range),
|
||||
errors: errors,
|
||||
};
|
||||
});
|
||||
}
|
||||
|
||||
function saveContact(value) {
|
||||
if (params().get('mock') === '1') {
|
||||
return Promise.resolve({ contact_info: value });
|
||||
}
|
||||
var api = window.HyAppAPI || {};
|
||||
if (!api.user || !api.user.updateContact) {
|
||||
return Promise.reject(new Error('guild_api_unavailable'));
|
||||
}
|
||||
return api.user.updateContact({ contact_info: value });
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.hostCenter = {
|
||||
metrics: Object.freeze(METRICS.slice()),
|
||||
load: load,
|
||||
saveContact: saveContact,
|
||||
};
|
||||
})();
|
||||
48
guild/packages/ui/center.css
Normal file
48
guild/packages/ui/center.css
Normal file
@ -0,0 +1,48 @@
|
||||
.guild-center {
|
||||
min-height: 100vh;
|
||||
background: var(--hy-theme-bg);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.guild-center__content {
|
||||
display: grid;
|
||||
gap: 14px;
|
||||
width: min(100%, 720px);
|
||||
margin: 0 auto;
|
||||
padding: 16px max(16px, env(safe-area-inset-right))
|
||||
max(28px, env(safe-area-inset-bottom))
|
||||
max(16px, env(safe-area-inset-left));
|
||||
}
|
||||
|
||||
.guild-card {
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-card);
|
||||
background: var(--hy-theme-surface);
|
||||
box-shadow: var(--hy-theme-shadow);
|
||||
}
|
||||
|
||||
.guild-filter-bar {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.guild-filter-bar button {
|
||||
min-height: 38px;
|
||||
border: 1px solid var(--hy-theme-line);
|
||||
border-radius: var(--hy-theme-radius-control);
|
||||
background: var(--hy-theme-surface);
|
||||
color: var(--hy-theme-text);
|
||||
}
|
||||
|
||||
.guild-filter-bar button[aria-pressed='true'] {
|
||||
border-color: var(--hy-theme-primary-strong);
|
||||
background: var(--hy-theme-primary-soft);
|
||||
color: var(--hy-theme-primary-deep);
|
||||
}
|
||||
|
||||
@media (max-width: 420px) {
|
||||
.guild-filter-bar {
|
||||
grid-template-columns: repeat(3, minmax(0, 1fr));
|
||||
}
|
||||
}
|
||||
196
guild/packages/wallet/wallet.js
Normal file
196
guild/packages/wallet/wallet.js
Normal file
@ -0,0 +1,196 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
function query() {
|
||||
return new URLSearchParams(window.location.search || '');
|
||||
}
|
||||
|
||||
function commandID(prefix) {
|
||||
var random =
|
||||
window.crypto && window.crypto.randomUUID
|
||||
? window.crypto.randomUUID()
|
||||
: Date.now() + '-' + Math.random().toString(16).slice(2);
|
||||
return prefix + ':' + random;
|
||||
}
|
||||
|
||||
function normalizeBalance(value) {
|
||||
value = value || {};
|
||||
return {
|
||||
available: Number(
|
||||
value.available_amount || value.availableAmount || 0
|
||||
),
|
||||
frozen: Number(value.frozen_amount || value.frozenAmount || 0),
|
||||
displayUSD: String(value.display_usd || value.displayUSD || '0.00'),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeSeller(value) {
|
||||
value = value || {};
|
||||
return {
|
||||
userId: String(value.user_id || value.userId || ''),
|
||||
displayUserId: String(
|
||||
value.display_user_id || value.displayUserId || ''
|
||||
),
|
||||
nickname: String(value.nickname || ''),
|
||||
avatar: String(value.avatar || ''),
|
||||
sortOrder: Number(value.sort_order || value.sortOrder || 0),
|
||||
pointAmount: Number(value.point_amount || value.pointAmount || 0),
|
||||
sellerCoinAmount: Number(
|
||||
value.seller_coin_amount || value.sellerCoinAmount || 0
|
||||
),
|
||||
serviceCountries:
|
||||
value.service_country_codes || value.serviceCountryCodes || [],
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeTransaction(value) {
|
||||
value = value || {};
|
||||
return {
|
||||
id: String(value.transaction_id || value.transactionId || ''),
|
||||
bizType: String(value.biz_type || value.bizType || ''),
|
||||
availableDelta: Number(
|
||||
value.available_delta || value.availableDelta || 0
|
||||
),
|
||||
counterpartyUserId: String(
|
||||
value.counterparty_user_id || value.counterpartyUserId || ''
|
||||
),
|
||||
createdAtMS: Number(value.created_at_ms || value.createdAtMs || 0),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeOverview(value) {
|
||||
value = value || {};
|
||||
var address = value.withdraw_address || value.withdrawAddress || {};
|
||||
return {
|
||||
balance: normalizeBalance(value.balance),
|
||||
pointsPerUSD: Number(
|
||||
value.points_per_usd || value.pointsPerUSD || 0
|
||||
),
|
||||
feeBPS: Number(value.fee_bps || value.feeBPS || 0),
|
||||
minimumPoints: Number(
|
||||
value.minimum_points || value.minimumPoints || 0
|
||||
),
|
||||
policyInstance: String(
|
||||
value.policy_instance_code || value.policyInstance || ''
|
||||
),
|
||||
address: String(
|
||||
address.usdt_trc20_address || address.usdtTRC20Address || ''
|
||||
),
|
||||
sellers: (value.coin_sellers || value.coinSellers || [])
|
||||
.map(normalizeSeller)
|
||||
.filter(function (item) {
|
||||
return (
|
||||
item.userId &&
|
||||
item.pointAmount > 0 &&
|
||||
item.sellerCoinAmount > 0
|
||||
);
|
||||
}),
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeHistory(value) {
|
||||
var items = Array.isArray(value)
|
||||
? value
|
||||
: (value && (value.transactions || value.items)) || [];
|
||||
return items.map(normalizeTransaction);
|
||||
}
|
||||
|
||||
function mockData() {
|
||||
return {
|
||||
balance: {
|
||||
available: 5152051511212,
|
||||
frozen: 0,
|
||||
displayUSD: '51520515.11',
|
||||
},
|
||||
pointsPerUSD: 100000,
|
||||
feeBPS: 500,
|
||||
minimumPoints: 1000000,
|
||||
policyInstance: 'fami-guild-demo',
|
||||
address: '',
|
||||
sellers: [
|
||||
{
|
||||
userId: '34343',
|
||||
displayUserId: '34343',
|
||||
nickname: 'Coin Seller One',
|
||||
avatar: '',
|
||||
sortOrder: 1,
|
||||
pointAmount: 100000,
|
||||
sellerCoinAmount: 92000,
|
||||
serviceCountries: ['SA', 'TR'],
|
||||
},
|
||||
{
|
||||
userId: '34344',
|
||||
displayUserId: '34344',
|
||||
nickname: 'Coin Seller Two',
|
||||
avatar: '',
|
||||
sortOrder: 2,
|
||||
pointAmount: 100000,
|
||||
sellerCoinAmount: 90000,
|
||||
serviceCountries: [],
|
||||
},
|
||||
],
|
||||
history: [
|
||||
{
|
||||
id: 'demo-1',
|
||||
bizType: 'point_transfer_to_coin_seller',
|
||||
availableDelta: -1000000,
|
||||
counterpartyUserId: '34343',
|
||||
createdAtMS: Date.now() - 3600000,
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
function load() {
|
||||
if (query().get('mock') === '1') return Promise.resolve(mockData());
|
||||
var api = window.HyAppAPI && window.HyAppAPI.pointWallet;
|
||||
if (!api)
|
||||
return Promise.reject(new Error('point_wallet_api_unavailable'));
|
||||
return Promise.all([
|
||||
api.overview(),
|
||||
api.history(30).catch(function () {
|
||||
// 流水故障不应遮住余额和提现入口;页面会单独提示历史暂不可用。
|
||||
return null;
|
||||
}),
|
||||
]).then(function (results) {
|
||||
var data = normalizeOverview(results[0]);
|
||||
data.history = normalizeHistory(results[1]);
|
||||
data.historyUnavailable = results[1] === null;
|
||||
return data;
|
||||
});
|
||||
}
|
||||
|
||||
function transferToCoinSeller(sellerUserID, pointAmount) {
|
||||
if (query().get('mock') === '1') return Promise.resolve({});
|
||||
return window.HyAppAPI.pointWallet.transferToCoinSeller({
|
||||
command_id: commandID('fami-point-coin-seller'),
|
||||
seller_user_id: String(sellerUserID || ''),
|
||||
point_amount: Number(pointAmount || 0),
|
||||
});
|
||||
}
|
||||
|
||||
function withdrawUSDT(pointAmount, address) {
|
||||
if (query().get('mock') === '1') return Promise.resolve({});
|
||||
return window.HyAppAPI.pointWallet.withdrawUSDT({
|
||||
command_id: commandID('fami-point-usdt'),
|
||||
gross_point_amount: Number(pointAmount || 0),
|
||||
usdt_trc20_address: String(address || '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
function saveAddress(address) {
|
||||
if (query().get('mock') === '1') return Promise.resolve({});
|
||||
// 地址写入继续复用通用工资钱包用户资料接口;资金冻结仍只由 POINT 提现接口完成。
|
||||
return window.HyAppAPI.salaryWallet.saveWithdrawAddress({
|
||||
usdt_trc20_address: String(address || '').trim(),
|
||||
});
|
||||
}
|
||||
|
||||
window.HyGuild = window.HyGuild || {};
|
||||
window.HyGuild.wallet = {
|
||||
load: load,
|
||||
saveAddress: saveAddress,
|
||||
transferToCoinSeller: transferToCoinSeller,
|
||||
withdrawUSDT: withdrawUSDT,
|
||||
};
|
||||
})();
|
||||
139
guild/products/fami/locales/ar.json
Normal file
139
guild/products/fami/locales/ar.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"fami_common.apply": "تطبيق",
|
||||
"fami_common.back": "رجوع",
|
||||
"fami_common.cancel": "إلغاء",
|
||||
"fami_common.change_language": "تغيير اللغة",
|
||||
"fami_common.diamond_balance": "رصيد الألماس",
|
||||
"fami_common.edit": "تعديل",
|
||||
"fami_common.save": "حفظ",
|
||||
"fami_common.stats_unavailable": "الإحصاءات غير متاحة مؤقتًا.",
|
||||
"fami_common.uid_prefix": "UID:",
|
||||
"fami_common.withdraw": "سحب",
|
||||
"fami_agency.action": "الإجراء",
|
||||
"fami_agency.add_host": "إضافة مضيف",
|
||||
"fami_agency.contact": "واتساب",
|
||||
"fami_agency.contact_number": "رقم التواصل",
|
||||
"fami_agency.contact_required": "أدخل رقم التواصل.",
|
||||
"fami_agency.contact_save_failed": "تعذر حفظ جهة الاتصال.",
|
||||
"fami_agency.contact_saved": "تم حفظ جهة الاتصال.",
|
||||
"fami_agency.created_prefix": "تاريخ الإنشاء:",
|
||||
"fami_agency.data_title": "بيانات الوكالة",
|
||||
"fami_agency.edit_contact": "تعديل واتساب",
|
||||
"fami_agency.gift_income": "دخل الهدايا",
|
||||
"fami_agency.gifted_hosts": "المضيفون المتلقون للهدايا",
|
||||
"fami_agency.host": "المضيف",
|
||||
"fami_agency.host_count": "{count} مضيف",
|
||||
"fami_agency.host_id": "معرّف المضيف",
|
||||
"fami_agency.host_removed": "تمت إزالة المضيف.",
|
||||
"fami_agency.id_prefix": "معرّف الوكالة:",
|
||||
"fami_agency.invite_failed": "تعذر دعوة المضيف.",
|
||||
"fami_agency.invite_hint": "أدخل معرّف العرض للمضيف. ينضم المضيف بعد قبول الدعوة.",
|
||||
"fami_agency.invite_sent": "تم إرسال الدعوة. يجب على المضيف قبولها.",
|
||||
"fami_agency.joined_at": "تاريخ الانضمام",
|
||||
"fami_agency.load_failed": "تعذر تحميل مركز الوكالة.",
|
||||
"fami_agency.no_hosts": "لا يوجد مضيفون بعد.",
|
||||
"fami_agency.owner": "المالك",
|
||||
"fami_agency.remove": "إزالة",
|
||||
"fami_agency.remove_confirm": "إزالة {name} من هذه الوكالة؟ ستبقى الأرباح السابقة دون تغيير.",
|
||||
"fami_agency.remove_failed": "تعذرت إزالة المضيف.",
|
||||
"fami_agency.remove_host": "إزالة المضيف",
|
||||
"fami_agency.send_invite": "إرسال الدعوة",
|
||||
"fami_agency.share_income": "دخل الحصة",
|
||||
"fami_agency.tab_agency": "بيانات الوكالة",
|
||||
"fami_agency.tab_hosts": "بيانات المضيفين",
|
||||
"fami_agency.title": "مركز الوكالة",
|
||||
"fami_agency.total_earnings": "إجمالي الأرباح",
|
||||
"fami_agency.total_hosts": "إجمالي المضيفين",
|
||||
"fami_bd.add_agency": "إضافة وكالة",
|
||||
"fami_bd.agency": "الوكالة",
|
||||
"fami_bd.agency_data": "بيانات الوكالات",
|
||||
"fami_bd.agency_name": "اسم الوكالة",
|
||||
"fami_bd.contact": "واتساب",
|
||||
"fami_bd.country": "الدولة",
|
||||
"fami_bd.diamond_earnings": "إجمالي أرباح ألماس الوكالة",
|
||||
"fami_bd.edit_contact": "تعديل واتساب",
|
||||
"fami_bd.invite_failed": "تعذرت دعوة الوكالة.",
|
||||
"fami_bd.invite_hint": "أدخل معرّف العرض لمالك الوكالة المستقبلي. تُنشأ الوكالة بعد قبول الدعوة.",
|
||||
"fami_bd.invite_sent": "تم إرسال الدعوة. تُنشأ الوكالة بعد القبول.",
|
||||
"fami_bd.joined_at": "تاريخ الانضمام",
|
||||
"fami_bd.load_failed": "تعذر تحميل مركز BD.",
|
||||
"fami_bd.no_agencies": "لا توجد وكالات بعد.",
|
||||
"fami_bd.owner_id": "معرّف المالك",
|
||||
"fami_bd.title": "مركز BD",
|
||||
"fami_host.agency_id_prefix": "معرّف الوكالة:",
|
||||
"fami_host.back": "رجوع",
|
||||
"fami_host.change_language": "تغيير اللغة",
|
||||
"fami_host.contact": "واتساب",
|
||||
"fami_host.contact_number": "رقم التواصل",
|
||||
"fami_host.contact_required": "أدخل رقم التواصل.",
|
||||
"fami_host.contact_save_failed": "تعذر حفظ جهة الاتصال.",
|
||||
"fami_host.contact_saved": "تم حفظ جهة الاتصال.",
|
||||
"fami_host.data_details": "تفاصيل البيانات",
|
||||
"fami_host.diamond_balance": "رصيد الألماس",
|
||||
"fami_host.edit": "تعديل",
|
||||
"fami_host.edit_contact": "تعديل واتساب",
|
||||
"fami_host.joined_at_prefix": "تاريخ الانضمام:",
|
||||
"fami_host.load_failed": "تعذر تحميل مركز المضيف.",
|
||||
"fami_host.stats_unavailable": "الإحصاءات غير متاحة مؤقتًا.",
|
||||
"fami_host.title": "مركز المضيف",
|
||||
"fami_host.uid_prefix": "UID:",
|
||||
"fami_host.withdraw": "سحب",
|
||||
"fami_metric.diamond_earnings": "أرباح الألماس",
|
||||
"fami_metric.diamond_exchanged": "الألماس المحوّل",
|
||||
"fami_metric.gift_senders": "مرسلو الهدايا",
|
||||
"fami_metric.new_followers": "متابعون جدد",
|
||||
"fami_metric.online_duration": "مدة الاتصال",
|
||||
"fami_metric.private_message_senders": "مرسلو الرسائل الخاصة",
|
||||
"fami_metric.valid_mic_days": "أيام الميكروفون الفعالة",
|
||||
"fami_metric.valid_mic_duration": "مدة الميكروفون الفعالة",
|
||||
"fami_range.custom": "مخصص",
|
||||
"fami_range.custom_title": "نطاق تاريخ مخصص",
|
||||
"fami_range.end": "تاريخ النهاية",
|
||||
"fami_range.invalid": "اختر نطاق تاريخ صالحًا.",
|
||||
"fami_range.label": "نطاق التاريخ",
|
||||
"fami_range.last_30_days": "آخر 30 يومًا",
|
||||
"fami_range.last_7_days": "آخر 7 أيام",
|
||||
"fami_range.last_month": "الشهر الماضي",
|
||||
"fami_range.start": "تاريخ البداية",
|
||||
"fami_range.this_month": "هذا الشهر",
|
||||
"fami_range.today": "اليوم",
|
||||
"fami_range.yesterday": "أمس",
|
||||
"fami_unit.day_short": "ي",
|
||||
"fami_unit.hour_short": "س",
|
||||
"fami_unit.minute_short": "د",
|
||||
"fami_wallet.address_placeholder": "T...",
|
||||
"fami_wallet.address_save_failed": "تعذر حفظ العنوان.",
|
||||
"fami_wallet.address_saved": "تم حفظ العنوان.",
|
||||
"fami_wallet.all": "الكل",
|
||||
"fami_wallet.amount": "مبلغ السحب",
|
||||
"fami_wallet.amount_placeholder": "أدخل مبلغ POINT",
|
||||
"fami_wallet.choose_seller": "اختر تاجر العملات",
|
||||
"fami_wallet.estimated_receive": "الاستلام المتوقع",
|
||||
"fami_wallet.history": "سجل السحب",
|
||||
"fami_wallet.history_unavailable": "السجل غير متاح مؤقتًا.",
|
||||
"fami_wallet.invalid_address": "أدخل عنوان TRC20 صالحًا.",
|
||||
"fami_wallet.invalid_amount": "أدخل مبلغ POINT متاحًا.",
|
||||
"fami_wallet.load_failed": "تعذر تحميل المحفظة.",
|
||||
"fami_wallet.minimum_amount": "الحد الأدنى من POINT:",
|
||||
"fami_wallet.no_address": "لم يتم حفظ عنوان USDT.",
|
||||
"fami_wallet.no_history": "لا توجد سجلات سحب",
|
||||
"fami_wallet.no_sellers": "لا يوجد تجار مؤهلون",
|
||||
"fami_wallet.no_sellers_hint": "يعتمد التوفر على بلدك والقائمة البيضاء في لوحة الإدارة.",
|
||||
"fami_wallet.point_balance": "رصيد POINT",
|
||||
"fami_wallet.point_transaction": "معاملة POINT",
|
||||
"fami_wallet.policy_caption": "يتم تعيين السعر والرسوم من لوحة الإدارة.",
|
||||
"fami_wallet.save_address": "حفظ العنوان",
|
||||
"fami_wallet.saved_address": "العنوان المحفوظ:",
|
||||
"fami_wallet.seller_coin": "عملات التاجر",
|
||||
"fami_wallet.seller_tab": "تاجر العملات",
|
||||
"fami_wallet.seller_withdrawal": "سحب عبر تاجر العملات",
|
||||
"fami_wallet.submit_failed": "تعذر إرسال طلب السحب.",
|
||||
"fami_wallet.submitted": "تم إرسال طلب السحب.",
|
||||
"fami_wallet.title": "سحب",
|
||||
"fami_wallet.trc20_address": "عنوان TRC20",
|
||||
"fami_wallet.usdt_address": "عنوان USDT TRC20",
|
||||
"fami_wallet.usdt_tab": "USDT",
|
||||
"fami_wallet.usdt_warning": "يتم إرسال السحب فقط إلى عنوان TRC20 المحفوظ.",
|
||||
"fami_wallet.usdt_withdrawal": "سحب USDT",
|
||||
"fami_wallet.withdraw_now": "اسحب الآن"
|
||||
}
|
||||
139
guild/products/fami/locales/en.json
Normal file
139
guild/products/fami/locales/en.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"fami_common.apply": "Apply",
|
||||
"fami_common.back": "Back",
|
||||
"fami_common.cancel": "Cancel",
|
||||
"fami_common.change_language": "Change language",
|
||||
"fami_common.diamond_balance": "Diamond balance",
|
||||
"fami_common.edit": "Edit",
|
||||
"fami_common.save": "Save",
|
||||
"fami_common.stats_unavailable": "Statistics are temporarily unavailable.",
|
||||
"fami_common.uid_prefix": "UID:",
|
||||
"fami_common.withdraw": "Withdraw",
|
||||
"fami_agency.action": "Action",
|
||||
"fami_agency.add_host": "Add host",
|
||||
"fami_agency.contact": "WhatsApp",
|
||||
"fami_agency.contact_number": "Contact number",
|
||||
"fami_agency.contact_required": "Enter a contact number.",
|
||||
"fami_agency.contact_save_failed": "Unable to save contact.",
|
||||
"fami_agency.contact_saved": "Contact saved.",
|
||||
"fami_agency.created_prefix": "Created:",
|
||||
"fami_agency.data_title": "Agency data",
|
||||
"fami_agency.edit_contact": "Edit WhatsApp",
|
||||
"fami_agency.gift_income": "Gift income",
|
||||
"fami_agency.gifted_hosts": "Gifted hosts",
|
||||
"fami_agency.host": "Host",
|
||||
"fami_agency.host_count": "{count} hosts",
|
||||
"fami_agency.host_id": "Host ID",
|
||||
"fami_agency.host_removed": "Host removed.",
|
||||
"fami_agency.id_prefix": "Agency ID:",
|
||||
"fami_agency.invite_failed": "Unable to invite host.",
|
||||
"fami_agency.invite_hint": "Enter the host display ID. The host joins after accepting the invitation.",
|
||||
"fami_agency.invite_sent": "Invitation sent. The host must accept it.",
|
||||
"fami_agency.joined_at": "Joined",
|
||||
"fami_agency.load_failed": "Unable to load Agency Center.",
|
||||
"fami_agency.no_hosts": "No hosts yet.",
|
||||
"fami_agency.owner": "Owner",
|
||||
"fami_agency.remove": "Remove",
|
||||
"fami_agency.remove_confirm": "Remove {name} from this agency? Historical earnings will remain unchanged.",
|
||||
"fami_agency.remove_failed": "Unable to remove host.",
|
||||
"fami_agency.remove_host": "Remove host",
|
||||
"fami_agency.send_invite": "Send invite",
|
||||
"fami_agency.share_income": "Share income",
|
||||
"fami_agency.tab_agency": "Agency data",
|
||||
"fami_agency.tab_hosts": "Host data",
|
||||
"fami_agency.title": "Agency Center",
|
||||
"fami_agency.total_earnings": "Total earnings",
|
||||
"fami_agency.total_hosts": "Total hosts",
|
||||
"fami_bd.add_agency": "Add agency",
|
||||
"fami_bd.agency": "Agency",
|
||||
"fami_bd.agency_data": "Agency data",
|
||||
"fami_bd.agency_name": "Agency name",
|
||||
"fami_bd.contact": "WhatsApp",
|
||||
"fami_bd.country": "Country",
|
||||
"fami_bd.diamond_earnings": "Agency diamond earnings",
|
||||
"fami_bd.edit_contact": "Edit WhatsApp",
|
||||
"fami_bd.invite_failed": "Unable to invite agency.",
|
||||
"fami_bd.invite_hint": "Enter the future agency owner's display ID. The agency is created after the invitation is accepted.",
|
||||
"fami_bd.invite_sent": "Invitation sent. The agency is created after acceptance.",
|
||||
"fami_bd.joined_at": "Joined",
|
||||
"fami_bd.load_failed": "Unable to load BD Center.",
|
||||
"fami_bd.no_agencies": "No agencies yet.",
|
||||
"fami_bd.owner_id": "Owner ID",
|
||||
"fami_bd.title": "BD Center",
|
||||
"fami_host.agency_id_prefix": "Agency ID:",
|
||||
"fami_host.back": "Back",
|
||||
"fami_host.change_language": "Change language",
|
||||
"fami_host.contact": "WhatsApp",
|
||||
"fami_host.contact_number": "Contact number",
|
||||
"fami_host.contact_required": "Enter a contact number.",
|
||||
"fami_host.contact_save_failed": "Unable to save contact.",
|
||||
"fami_host.contact_saved": "Contact saved.",
|
||||
"fami_host.data_details": "Data details",
|
||||
"fami_host.diamond_balance": "Diamond balance",
|
||||
"fami_host.edit": "Edit",
|
||||
"fami_host.edit_contact": "Edit WhatsApp",
|
||||
"fami_host.joined_at_prefix": "Joined:",
|
||||
"fami_host.load_failed": "Unable to load Host Center.",
|
||||
"fami_host.stats_unavailable": "Statistics are temporarily unavailable.",
|
||||
"fami_host.title": "Host Center",
|
||||
"fami_host.uid_prefix": "UID:",
|
||||
"fami_host.withdraw": "Withdraw",
|
||||
"fami_metric.diamond_earnings": "Diamond earnings",
|
||||
"fami_metric.diamond_exchanged": "Diamonds exchanged",
|
||||
"fami_metric.gift_senders": "Gift senders",
|
||||
"fami_metric.new_followers": "New followers",
|
||||
"fami_metric.online_duration": "Online duration",
|
||||
"fami_metric.private_message_senders": "Private message users",
|
||||
"fami_metric.valid_mic_days": "Valid mic days",
|
||||
"fami_metric.valid_mic_duration": "Valid mic duration",
|
||||
"fami_range.custom": "Custom",
|
||||
"fami_range.custom_title": "Custom date range",
|
||||
"fami_range.end": "End date",
|
||||
"fami_range.invalid": "Choose a valid date range.",
|
||||
"fami_range.label": "Date range",
|
||||
"fami_range.last_30_days": "Last 30 days",
|
||||
"fami_range.last_7_days": "Last 7 days",
|
||||
"fami_range.last_month": "Last month",
|
||||
"fami_range.start": "Start date",
|
||||
"fami_range.this_month": "This month",
|
||||
"fami_range.today": "Today",
|
||||
"fami_range.yesterday": "Yesterday",
|
||||
"fami_unit.day_short": "d",
|
||||
"fami_unit.hour_short": "h",
|
||||
"fami_unit.minute_short": "m",
|
||||
"fami_wallet.address_placeholder": "T...",
|
||||
"fami_wallet.address_save_failed": "Unable to save address.",
|
||||
"fami_wallet.address_saved": "Address saved.",
|
||||
"fami_wallet.all": "All",
|
||||
"fami_wallet.amount": "Withdrawal amount",
|
||||
"fami_wallet.amount_placeholder": "Enter POINT amount",
|
||||
"fami_wallet.choose_seller": "Choose coin seller",
|
||||
"fami_wallet.estimated_receive": "Estimated receive",
|
||||
"fami_wallet.history": "Withdrawal history",
|
||||
"fami_wallet.history_unavailable": "History is temporarily unavailable.",
|
||||
"fami_wallet.invalid_address": "Enter a valid TRC20 address.",
|
||||
"fami_wallet.invalid_amount": "Enter an available POINT amount.",
|
||||
"fami_wallet.load_failed": "Unable to load wallet.",
|
||||
"fami_wallet.minimum_amount": "Minimum POINT amount:",
|
||||
"fami_wallet.no_address": "No USDT address saved.",
|
||||
"fami_wallet.no_history": "No withdrawal records",
|
||||
"fami_wallet.no_sellers": "No eligible coin sellers",
|
||||
"fami_wallet.no_sellers_hint": "Availability depends on your country and the Admin whitelist.",
|
||||
"fami_wallet.point_balance": "POINT balance",
|
||||
"fami_wallet.point_transaction": "POINT transaction",
|
||||
"fami_wallet.policy_caption": "Rate and fee are set by Admin.",
|
||||
"fami_wallet.save_address": "Save address",
|
||||
"fami_wallet.saved_address": "Saved address:",
|
||||
"fami_wallet.seller_coin": "seller coins",
|
||||
"fami_wallet.seller_tab": "Coin seller",
|
||||
"fami_wallet.seller_withdrawal": "Coin seller withdrawal",
|
||||
"fami_wallet.submit_failed": "Unable to submit withdrawal.",
|
||||
"fami_wallet.submitted": "Withdrawal submitted.",
|
||||
"fami_wallet.title": "Withdraw",
|
||||
"fami_wallet.trc20_address": "TRC20 address",
|
||||
"fami_wallet.usdt_address": "USDT TRC20 address",
|
||||
"fami_wallet.usdt_tab": "USDT",
|
||||
"fami_wallet.usdt_warning": "Withdrawals are sent only to your saved TRC20 address.",
|
||||
"fami_wallet.usdt_withdrawal": "USDT withdrawal",
|
||||
"fami_wallet.withdraw_now": "Withdraw now"
|
||||
}
|
||||
139
guild/products/fami/locales/es.json
Normal file
139
guild/products/fami/locales/es.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"fami_common.apply": "Aplicar",
|
||||
"fami_common.back": "Volver",
|
||||
"fami_common.cancel": "Cancelar",
|
||||
"fami_common.change_language": "Cambiar idioma",
|
||||
"fami_common.diamond_balance": "Saldo de diamantes",
|
||||
"fami_common.edit": "Editar",
|
||||
"fami_common.save": "Guardar",
|
||||
"fami_common.stats_unavailable": "Las estadísticas no están disponibles temporalmente.",
|
||||
"fami_common.uid_prefix": "UID:",
|
||||
"fami_common.withdraw": "Retirar",
|
||||
"fami_agency.action": "Acción",
|
||||
"fami_agency.add_host": "Añadir anfitrión",
|
||||
"fami_agency.contact": "WhatsApp",
|
||||
"fami_agency.contact_number": "Número de contacto",
|
||||
"fami_agency.contact_required": "Introduce un número de contacto.",
|
||||
"fami_agency.contact_save_failed": "No se pudo guardar el contacto.",
|
||||
"fami_agency.contact_saved": "Contacto guardado.",
|
||||
"fami_agency.created_prefix": "Creada:",
|
||||
"fami_agency.data_title": "Datos de la agencia",
|
||||
"fami_agency.edit_contact": "Editar WhatsApp",
|
||||
"fami_agency.gift_income": "Ingresos por regalos",
|
||||
"fami_agency.gifted_hosts": "Anfitriones con regalos",
|
||||
"fami_agency.host": "Anfitrión",
|
||||
"fami_agency.host_count": "{count} anfitriones",
|
||||
"fami_agency.host_id": "ID del anfitrión",
|
||||
"fami_agency.host_removed": "Anfitrión eliminado.",
|
||||
"fami_agency.id_prefix": "ID de agencia:",
|
||||
"fami_agency.invite_failed": "No se pudo invitar al anfitrión.",
|
||||
"fami_agency.invite_hint": "Introduce el ID visible del anfitrión. Se unirá después de aceptar la invitación.",
|
||||
"fami_agency.invite_sent": "Invitación enviada. El anfitrión debe aceptarla.",
|
||||
"fami_agency.joined_at": "Ingreso",
|
||||
"fami_agency.load_failed": "No se pudo cargar el Centro de agencia.",
|
||||
"fami_agency.no_hosts": "Aún no hay anfitriones.",
|
||||
"fami_agency.owner": "Propietario",
|
||||
"fami_agency.remove": "Eliminar",
|
||||
"fami_agency.remove_confirm": "¿Eliminar a {name} de esta agencia? Los ingresos históricos no cambiarán.",
|
||||
"fami_agency.remove_failed": "No se pudo eliminar al anfitrión.",
|
||||
"fami_agency.remove_host": "Eliminar anfitrión",
|
||||
"fami_agency.send_invite": "Enviar invitación",
|
||||
"fami_agency.share_income": "Ingresos compartidos",
|
||||
"fami_agency.tab_agency": "Datos de la agencia",
|
||||
"fami_agency.tab_hosts": "Datos de anfitriones",
|
||||
"fami_agency.title": "Centro de agencia",
|
||||
"fami_agency.total_earnings": "Ganancias totales",
|
||||
"fami_agency.total_hosts": "Total de anfitriones",
|
||||
"fami_bd.add_agency": "Añadir agencia",
|
||||
"fami_bd.agency": "Agencia",
|
||||
"fami_bd.agency_data": "Datos de agencias",
|
||||
"fami_bd.agency_name": "Nombre de agencia",
|
||||
"fami_bd.contact": "WhatsApp",
|
||||
"fami_bd.country": "País",
|
||||
"fami_bd.diamond_earnings": "Ganancias de diamantes de la agencia",
|
||||
"fami_bd.edit_contact": "Editar WhatsApp",
|
||||
"fami_bd.invite_failed": "No se pudo invitar a la agencia.",
|
||||
"fami_bd.invite_hint": "Introduce el ID visible del futuro propietario. La agencia se crea después de aceptar la invitación.",
|
||||
"fami_bd.invite_sent": "Invitación enviada. La agencia se crea después de aceptarla.",
|
||||
"fami_bd.joined_at": "Ingreso",
|
||||
"fami_bd.load_failed": "No se pudo cargar el Centro BD.",
|
||||
"fami_bd.no_agencies": "Aún no hay agencias.",
|
||||
"fami_bd.owner_id": "ID del propietario",
|
||||
"fami_bd.title": "Centro BD",
|
||||
"fami_host.agency_id_prefix": "ID de agencia:",
|
||||
"fami_host.back": "Volver",
|
||||
"fami_host.change_language": "Cambiar idioma",
|
||||
"fami_host.contact": "WhatsApp",
|
||||
"fami_host.contact_number": "Número de contacto",
|
||||
"fami_host.contact_required": "Introduce un número de contacto.",
|
||||
"fami_host.contact_save_failed": "No se pudo guardar el contacto.",
|
||||
"fami_host.contact_saved": "Contacto guardado.",
|
||||
"fami_host.data_details": "Detalles de datos",
|
||||
"fami_host.diamond_balance": "Saldo de diamantes",
|
||||
"fami_host.edit": "Editar",
|
||||
"fami_host.edit_contact": "Editar WhatsApp",
|
||||
"fami_host.joined_at_prefix": "Ingreso:",
|
||||
"fami_host.load_failed": "No se pudo cargar el Centro de anfitrión.",
|
||||
"fami_host.stats_unavailable": "Las estadísticas no están disponibles temporalmente.",
|
||||
"fami_host.title": "Centro de anfitrión",
|
||||
"fami_host.uid_prefix": "UID:",
|
||||
"fami_host.withdraw": "Retirar",
|
||||
"fami_metric.diamond_earnings": "Ganancias de diamantes",
|
||||
"fami_metric.diamond_exchanged": "Diamantes canjeados",
|
||||
"fami_metric.gift_senders": "Remitentes de regalos",
|
||||
"fami_metric.new_followers": "Nuevos seguidores",
|
||||
"fami_metric.online_duration": "Tiempo en línea",
|
||||
"fami_metric.private_message_senders": "Usuarios de mensajes privados",
|
||||
"fami_metric.valid_mic_days": "Días de micrófono válidos",
|
||||
"fami_metric.valid_mic_duration": "Tiempo de micrófono válido",
|
||||
"fami_range.custom": "Personalizado",
|
||||
"fami_range.custom_title": "Rango de fechas personalizado",
|
||||
"fami_range.end": "Fecha final",
|
||||
"fami_range.invalid": "Elige un rango de fechas válido.",
|
||||
"fami_range.label": "Rango de fechas",
|
||||
"fami_range.last_30_days": "Últimos 30 días",
|
||||
"fami_range.last_7_days": "Últimos 7 días",
|
||||
"fami_range.last_month": "Mes pasado",
|
||||
"fami_range.start": "Fecha inicial",
|
||||
"fami_range.this_month": "Este mes",
|
||||
"fami_range.today": "Hoy",
|
||||
"fami_range.yesterday": "Ayer",
|
||||
"fami_unit.day_short": "d",
|
||||
"fami_unit.hour_short": "h",
|
||||
"fami_unit.minute_short": "min",
|
||||
"fami_wallet.address_placeholder": "T...",
|
||||
"fami_wallet.address_save_failed": "No se pudo guardar la dirección.",
|
||||
"fami_wallet.address_saved": "Dirección guardada.",
|
||||
"fami_wallet.all": "Todo",
|
||||
"fami_wallet.amount": "Importe de retiro",
|
||||
"fami_wallet.amount_placeholder": "Introduce el importe en POINT",
|
||||
"fami_wallet.choose_seller": "Elige un vendedor de monedas",
|
||||
"fami_wallet.estimated_receive": "Recepción estimada",
|
||||
"fami_wallet.history": "Historial de retiros",
|
||||
"fami_wallet.history_unavailable": "El historial no está disponible temporalmente.",
|
||||
"fami_wallet.invalid_address": "Introduce una dirección TRC20 válida.",
|
||||
"fami_wallet.invalid_amount": "Introduce un importe POINT disponible.",
|
||||
"fami_wallet.load_failed": "No se pudo cargar la cartera.",
|
||||
"fami_wallet.minimum_amount": "Importe mínimo de POINT:",
|
||||
"fami_wallet.no_address": "No hay dirección USDT guardada.",
|
||||
"fami_wallet.no_history": "No hay registros de retiro",
|
||||
"fami_wallet.no_sellers": "No hay vendedores elegibles",
|
||||
"fami_wallet.no_sellers_hint": "La disponibilidad depende de tu país y de la lista blanca de Admin.",
|
||||
"fami_wallet.point_balance": "Saldo POINT",
|
||||
"fami_wallet.point_transaction": "Transacción POINT",
|
||||
"fami_wallet.policy_caption": "La tasa y la comisión se configuran en Admin.",
|
||||
"fami_wallet.save_address": "Guardar dirección",
|
||||
"fami_wallet.saved_address": "Dirección guardada:",
|
||||
"fami_wallet.seller_coin": "monedas del vendedor",
|
||||
"fami_wallet.seller_tab": "Vendedor de monedas",
|
||||
"fami_wallet.seller_withdrawal": "Retiro con vendedor",
|
||||
"fami_wallet.submit_failed": "No se pudo enviar el retiro.",
|
||||
"fami_wallet.submitted": "Retiro enviado.",
|
||||
"fami_wallet.title": "Retirar",
|
||||
"fami_wallet.trc20_address": "Dirección TRC20",
|
||||
"fami_wallet.usdt_address": "Dirección USDT TRC20",
|
||||
"fami_wallet.usdt_tab": "USDT",
|
||||
"fami_wallet.usdt_warning": "Los retiros se envían solo a tu dirección TRC20 guardada.",
|
||||
"fami_wallet.usdt_withdrawal": "Retiro USDT",
|
||||
"fami_wallet.withdraw_now": "Retirar ahora"
|
||||
}
|
||||
139
guild/products/fami/locales/tr.json
Normal file
139
guild/products/fami/locales/tr.json
Normal file
@ -0,0 +1,139 @@
|
||||
{
|
||||
"fami_common.apply": "Uygula",
|
||||
"fami_common.back": "Geri",
|
||||
"fami_common.cancel": "İptal",
|
||||
"fami_common.change_language": "Dili değiştir",
|
||||
"fami_common.diamond_balance": "Elmas bakiyesi",
|
||||
"fami_common.edit": "Düzenle",
|
||||
"fami_common.save": "Kaydet",
|
||||
"fami_common.stats_unavailable": "İstatistikler geçici olarak kullanılamıyor.",
|
||||
"fami_common.uid_prefix": "UID:",
|
||||
"fami_common.withdraw": "Çek",
|
||||
"fami_agency.action": "İşlem",
|
||||
"fami_agency.add_host": "Host ekle",
|
||||
"fami_agency.contact": "WhatsApp",
|
||||
"fami_agency.contact_number": "İletişim numarası",
|
||||
"fami_agency.contact_required": "Bir iletişim numarası girin.",
|
||||
"fami_agency.contact_save_failed": "İletişim bilgisi kaydedilemedi.",
|
||||
"fami_agency.contact_saved": "İletişim bilgisi kaydedildi.",
|
||||
"fami_agency.created_prefix": "Oluşturulma:",
|
||||
"fami_agency.data_title": "Ajans verileri",
|
||||
"fami_agency.edit_contact": "WhatsApp'ı düzenle",
|
||||
"fami_agency.gift_income": "Hediye geliri",
|
||||
"fami_agency.gifted_hosts": "Hediye alan hostlar",
|
||||
"fami_agency.host": "Host",
|
||||
"fami_agency.host_count": "{count} host",
|
||||
"fami_agency.host_id": "Host ID",
|
||||
"fami_agency.host_removed": "Host kaldırıldı.",
|
||||
"fami_agency.id_prefix": "Ajans ID:",
|
||||
"fami_agency.invite_failed": "Host davet edilemedi.",
|
||||
"fami_agency.invite_hint": "Hostun görünen ID'sini girin. Host daveti kabul ettikten sonra katılır.",
|
||||
"fami_agency.invite_sent": "Davet gönderildi. Hostun kabul etmesi gerekir.",
|
||||
"fami_agency.joined_at": "Katılım",
|
||||
"fami_agency.load_failed": "Agency Center yüklenemedi.",
|
||||
"fami_agency.no_hosts": "Henüz host yok.",
|
||||
"fami_agency.owner": "Sahip",
|
||||
"fami_agency.remove": "Kaldır",
|
||||
"fami_agency.remove_confirm": "{name} bu ajanstan kaldırılsın mı? Geçmiş kazançlar değişmeyecek.",
|
||||
"fami_agency.remove_failed": "Host kaldırılamadı.",
|
||||
"fami_agency.remove_host": "Hostu kaldır",
|
||||
"fami_agency.send_invite": "Davet gönder",
|
||||
"fami_agency.share_income": "Pay geliri",
|
||||
"fami_agency.tab_agency": "Ajans verileri",
|
||||
"fami_agency.tab_hosts": "Host verileri",
|
||||
"fami_agency.title": "Agency Center",
|
||||
"fami_agency.total_earnings": "Toplam kazanç",
|
||||
"fami_agency.total_hosts": "Toplam host",
|
||||
"fami_bd.add_agency": "Ajans ekle",
|
||||
"fami_bd.agency": "Ajans",
|
||||
"fami_bd.agency_data": "Ajans verileri",
|
||||
"fami_bd.agency_name": "Ajans adı",
|
||||
"fami_bd.contact": "WhatsApp",
|
||||
"fami_bd.country": "Ülke",
|
||||
"fami_bd.diamond_earnings": "Ajans elmas kazancı",
|
||||
"fami_bd.edit_contact": "WhatsApp'ı düzenle",
|
||||
"fami_bd.invite_failed": "Ajans davet edilemedi.",
|
||||
"fami_bd.invite_hint": "Gelecekteki ajans sahibinin görünen ID'sini girin. Ajans davet kabul edilince oluşturulur.",
|
||||
"fami_bd.invite_sent": "Davet gönderildi. Ajans kabulden sonra oluşturulur.",
|
||||
"fami_bd.joined_at": "Katılım",
|
||||
"fami_bd.load_failed": "BD Center yüklenemedi.",
|
||||
"fami_bd.no_agencies": "Henüz ajans yok.",
|
||||
"fami_bd.owner_id": "Sahip ID",
|
||||
"fami_bd.title": "BD Center",
|
||||
"fami_host.agency_id_prefix": "Ajans ID:",
|
||||
"fami_host.back": "Geri",
|
||||
"fami_host.change_language": "Dili değiştir",
|
||||
"fami_host.contact": "WhatsApp",
|
||||
"fami_host.contact_number": "İletişim numarası",
|
||||
"fami_host.contact_required": "Bir iletişim numarası girin.",
|
||||
"fami_host.contact_save_failed": "İletişim bilgisi kaydedilemedi.",
|
||||
"fami_host.contact_saved": "İletişim bilgisi kaydedildi.",
|
||||
"fami_host.data_details": "Veri ayrıntıları",
|
||||
"fami_host.diamond_balance": "Elmas bakiyesi",
|
||||
"fami_host.edit": "Düzenle",
|
||||
"fami_host.edit_contact": "WhatsApp'ı düzenle",
|
||||
"fami_host.joined_at_prefix": "Katılım:",
|
||||
"fami_host.load_failed": "Host Center yüklenemedi.",
|
||||
"fami_host.stats_unavailable": "İstatistikler geçici olarak kullanılamıyor.",
|
||||
"fami_host.title": "Host Center",
|
||||
"fami_host.uid_prefix": "UID:",
|
||||
"fami_host.withdraw": "Çek",
|
||||
"fami_metric.diamond_earnings": "Elmas kazancı",
|
||||
"fami_metric.diamond_exchanged": "Çevrilen elmas",
|
||||
"fami_metric.gift_senders": "Hediye gönderenler",
|
||||
"fami_metric.new_followers": "Yeni takipçiler",
|
||||
"fami_metric.online_duration": "Çevrimiçi süre",
|
||||
"fami_metric.private_message_senders": "Özel mesaj kullanıcıları",
|
||||
"fami_metric.valid_mic_days": "Geçerli mikrofon günleri",
|
||||
"fami_metric.valid_mic_duration": "Geçerli mikrofon süresi",
|
||||
"fami_range.custom": "Özel",
|
||||
"fami_range.custom_title": "Özel tarih aralığı",
|
||||
"fami_range.end": "Bitiş tarihi",
|
||||
"fami_range.invalid": "Geçerli bir tarih aralığı seçin.",
|
||||
"fami_range.label": "Tarih aralığı",
|
||||
"fami_range.last_30_days": "Son 30 gün",
|
||||
"fami_range.last_7_days": "Son 7 gün",
|
||||
"fami_range.last_month": "Geçen ay",
|
||||
"fami_range.start": "Başlangıç tarihi",
|
||||
"fami_range.this_month": "Bu ay",
|
||||
"fami_range.today": "Bugün",
|
||||
"fami_range.yesterday": "Dün",
|
||||
"fami_unit.day_short": "g",
|
||||
"fami_unit.hour_short": "sa",
|
||||
"fami_unit.minute_short": "dk",
|
||||
"fami_wallet.address_placeholder": "T...",
|
||||
"fami_wallet.address_save_failed": "Adres kaydedilemedi.",
|
||||
"fami_wallet.address_saved": "Adres kaydedildi.",
|
||||
"fami_wallet.all": "Tümü",
|
||||
"fami_wallet.amount": "Çekim tutarı",
|
||||
"fami_wallet.amount_placeholder": "POINT tutarını girin",
|
||||
"fami_wallet.choose_seller": "Coin satıcısı seçin",
|
||||
"fami_wallet.estimated_receive": "Tahmini alınacak",
|
||||
"fami_wallet.history": "Çekim geçmişi",
|
||||
"fami_wallet.history_unavailable": "Geçmiş geçici olarak kullanılamıyor.",
|
||||
"fami_wallet.invalid_address": "Geçerli bir TRC20 adresi girin.",
|
||||
"fami_wallet.invalid_amount": "Kullanılabilir bir POINT tutarı girin.",
|
||||
"fami_wallet.load_failed": "Cüzdan yüklenemedi.",
|
||||
"fami_wallet.minimum_amount": "Minimum POINT tutarı:",
|
||||
"fami_wallet.no_address": "Kayıtlı USDT adresi yok.",
|
||||
"fami_wallet.no_history": "Çekim kaydı yok",
|
||||
"fami_wallet.no_sellers": "Uygun coin satıcısı yok",
|
||||
"fami_wallet.no_sellers_hint": "Kullanılabilirlik ülkenize ve Admin beyaz listesine bağlıdır.",
|
||||
"fami_wallet.point_balance": "POINT bakiyesi",
|
||||
"fami_wallet.point_transaction": "POINT işlemi",
|
||||
"fami_wallet.policy_caption": "Kur ve ücret Admin tarafından ayarlanır.",
|
||||
"fami_wallet.save_address": "Adresi kaydet",
|
||||
"fami_wallet.saved_address": "Kayıtlı adres:",
|
||||
"fami_wallet.seller_coin": "satıcı coini",
|
||||
"fami_wallet.seller_tab": "Coin satıcısı",
|
||||
"fami_wallet.seller_withdrawal": "Coin satıcısına çekim",
|
||||
"fami_wallet.submit_failed": "Çekim gönderilemedi.",
|
||||
"fami_wallet.submitted": "Çekim gönderildi.",
|
||||
"fami_wallet.title": "Çekim",
|
||||
"fami_wallet.trc20_address": "TRC20 adresi",
|
||||
"fami_wallet.usdt_address": "USDT TRC20 adresi",
|
||||
"fami_wallet.usdt_tab": "USDT",
|
||||
"fami_wallet.usdt_warning": "Çekimler yalnızca kayıtlı TRC20 adresinize gönderilir.",
|
||||
"fami_wallet.usdt_withdrawal": "USDT çekimi",
|
||||
"fami_wallet.withdraw_now": "Şimdi çek"
|
||||
}
|
||||
33
guild/products/fami/product.js
Normal file
33
guild/products/fami/product.js
Normal file
@ -0,0 +1,33 @@
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var productScriptURL =
|
||||
document.currentScript && document.currentScript.src
|
||||
? document.currentScript.src
|
||||
: window.location.href;
|
||||
|
||||
if (!window.HyGuild || !window.HyGuild.product) {
|
||||
throw new Error('Guild product context must load before Fami');
|
||||
}
|
||||
|
||||
// 产品清单是 Fami 页面唯一的租户默认值来源;功能默认全开,权限是否可用仍以后端 capability 为准。
|
||||
window.HyGuild.product.define({
|
||||
key: 'fami',
|
||||
appCode: 'fami',
|
||||
languages: ['en', 'ar', 'tr', 'es'],
|
||||
features: {
|
||||
hostCenter: true,
|
||||
agencyCenter: true,
|
||||
bdCenter: true,
|
||||
wallet: true,
|
||||
coinSellerWithdraw: true,
|
||||
usdtWithdraw: true,
|
||||
},
|
||||
});
|
||||
|
||||
// 产品语言包和 common/locales 分开发布,后续新增品牌无需把整套页面文案塞回全站公共 JSON。
|
||||
window.HyAppI18nExtraSources = window.HyAppI18nExtraSources || [];
|
||||
window.HyAppI18nExtraSources.push(
|
||||
new URL('locales/', productScriptURL).toString()
|
||||
);
|
||||
})();
|
||||
@ -8,6 +8,11 @@
|
||||
"aslan:webview": "node scripts/aslan_flutter_webview.js",
|
||||
"test:manager-center": "node scripts/test_manager_center_resource_pagination.js",
|
||||
"test:recharge-v5pay": "node scripts/test_recharge_v5pay.js",
|
||||
"test:guild-architecture": "node scripts/test_guild_product_architecture.js",
|
||||
"test:fami-host-center": "node scripts/test_fami_host_center.js",
|
||||
"test:fami-agency-center": "node scripts/test_fami_agency_center.js",
|
||||
"test:fami-bd-center": "node scripts/test_fami_bd_center.js",
|
||||
"test:fami-wallet": "node scripts/test_fami_wallet.js",
|
||||
"format": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||
},
|
||||
|
||||
185
scripts/test_fami_agency_center.js
Normal file
185
scripts/test_fami_agency_center.js
Normal file
@ -0,0 +1,185 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function apiResponse(data) {
|
||||
return {
|
||||
headers: { get: () => '' },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: () => Promise.resolve(JSON.stringify({ code: 'OK', data })),
|
||||
};
|
||||
}
|
||||
|
||||
function harness(search = '') {
|
||||
const requests = [];
|
||||
const location = {
|
||||
hash: '',
|
||||
hostname: 'h5.global-interaction.com',
|
||||
href: `https://h5.global-interaction.com/guild/fami/agency-center/${search}`,
|
||||
origin: 'https://h5.global-interaction.com',
|
||||
pathname: '/guild/fami/agency-center/',
|
||||
search,
|
||||
};
|
||||
const storage = new Map();
|
||||
const window = {
|
||||
atob: (value) => Buffer.from(value, 'base64').toString('binary'),
|
||||
crypto: { randomUUID: () => 'fixed-uuid' },
|
||||
document: {
|
||||
currentScript: null,
|
||||
documentElement: { setAttribute: () => {} },
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
fetch: (rawURL, options = {}) => {
|
||||
const url = new URL(rawURL);
|
||||
requests.push({ url, options });
|
||||
if (url.pathname === '/api/v1/users/me') {
|
||||
return Promise.resolve(
|
||||
apiResponse({ contact_info: '+20 100 200 300' })
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/agency-center/overview') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
agency: {
|
||||
agency_id: '9',
|
||||
short_id: '90009',
|
||||
name: 'Fami Stars',
|
||||
created_at_ms: 1_750_000_000_000,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/wallet/me/balances') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
balances: [
|
||||
{ asset_type: 'POINT', available_amount: 8800 },
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/agency-center/stats') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
summary: {
|
||||
total_earnings: 1200,
|
||||
gift_income: 700,
|
||||
share_income: 500,
|
||||
total_hosts: 1,
|
||||
gifted_host_count: 1,
|
||||
},
|
||||
hosts: [
|
||||
{
|
||||
membership_id: '77',
|
||||
host_user_id: '42',
|
||||
display_user_id: '100042',
|
||||
username: 'Fami Host',
|
||||
diamond_earnings: 700,
|
||||
valid_mic_days: 3,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/users/by-display-user-id/100099') {
|
||||
return Promise.resolve(apiResponse({ user_id: '99' }));
|
||||
}
|
||||
return Promise.resolve(apiResponse({}));
|
||||
},
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) || null,
|
||||
removeItem: (key) => storage.delete(key),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
location,
|
||||
};
|
||||
window.window = window;
|
||||
const context = vm.createContext({
|
||||
Buffer,
|
||||
CustomEvent: function CustomEvent() {},
|
||||
Date,
|
||||
Error,
|
||||
FormData,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
Promise,
|
||||
String,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
console,
|
||||
document: window.document,
|
||||
window,
|
||||
});
|
||||
return { context, requests, window };
|
||||
}
|
||||
|
||||
function run(context, file) {
|
||||
vm.runInContext(fs.readFileSync(path.join(root, file), 'utf8'), context, {
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = harness('?token=test-token&env=test');
|
||||
run(browser.context, 'guild/packages/core/product-context.js');
|
||||
run(browser.context, 'guild/products/fami/product.js');
|
||||
run(browser.context, 'common/api.js');
|
||||
run(browser.context, 'guild/packages/core/date-range.js');
|
||||
run(browser.context, 'guild/packages/agency-center/agency-center.js');
|
||||
|
||||
const range = browser.window.HyGuild.dateRange.create('last_7_days', {
|
||||
now: new Date(2026, 6, 12),
|
||||
});
|
||||
const data = await browser.window.HyGuild.agencyCenter.load(range);
|
||||
assert.equal(data.agency.shortId, '90009');
|
||||
assert.equal(data.contact, '+20 100 200 300');
|
||||
assert.equal(data.pointBalance, 8800);
|
||||
assert.equal(data.stats.summary.share_income, 500);
|
||||
assert.equal(data.stats.hosts[0].displayUserId, '100042');
|
||||
|
||||
const statsRequest = browser.requests.find(
|
||||
({ url }) => url.pathname === '/api/v1/agency-center/stats'
|
||||
);
|
||||
assert.equal(statsRequest.url.searchParams.get('start_date'), '2026-07-06');
|
||||
assert.equal(statsRequest.url.searchParams.get('end_date'), '2026-07-12');
|
||||
|
||||
await browser.window.HyGuild.agencyCenter.inviteHost('100099');
|
||||
const invite = browser.requests.find(
|
||||
({ url }) => url.pathname === '/api/v1/agency-center/invitations/host'
|
||||
);
|
||||
assert.ok(invite, 'resolved display ID must be sent to invite endpoint');
|
||||
assert.deepEqual(JSON.parse(invite.options.body), {
|
||||
command_id: 'fami-agency-invite-host:fixed-uuid',
|
||||
target_user_id: '99',
|
||||
});
|
||||
browser.requests.forEach(({ options }) => {
|
||||
assert.equal(options.headers['X-App-Code'], 'fami');
|
||||
});
|
||||
|
||||
const mockBrowser = harness('?mock=1');
|
||||
run(mockBrowser.context, 'guild/packages/core/product-context.js');
|
||||
run(mockBrowser.context, 'guild/products/fami/product.js');
|
||||
run(mockBrowser.context, 'guild/packages/core/date-range.js');
|
||||
run(mockBrowser.context, 'guild/packages/agency-center/agency-center.js');
|
||||
const mock = await mockBrowser.window.HyGuild.agencyCenter.load(
|
||||
mockBrowser.window.HyGuild.dateRange.create('today', {
|
||||
now: new Date(2026, 6, 12),
|
||||
})
|
||||
);
|
||||
assert.equal(mock.stats.hosts.length, 1);
|
||||
assert.equal(mockBrowser.requests.length, 0);
|
||||
|
||||
console.log('Fami agency center tests passed');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
162
scripts/test_fami_bd_center.js
Normal file
162
scripts/test_fami_bd_center.js
Normal file
@ -0,0 +1,162 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
function apiResponse(data) {
|
||||
return {
|
||||
headers: { get: () => '' },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: () => Promise.resolve(JSON.stringify({ code: 'OK', data })),
|
||||
};
|
||||
}
|
||||
function harness(search = '') {
|
||||
const requests = [];
|
||||
const location = {
|
||||
hash: '',
|
||||
hostname: 'h5.global-interaction.com',
|
||||
href: `https://h5.global-interaction.com/guild/fami/bd-center/${search}`,
|
||||
origin: 'https://h5.global-interaction.com',
|
||||
pathname: '/guild/fami/bd-center/',
|
||||
search,
|
||||
};
|
||||
const storage = new Map();
|
||||
const window = {
|
||||
atob: (value) => Buffer.from(value, 'base64').toString('binary'),
|
||||
crypto: { randomUUID: () => 'fixed-uuid' },
|
||||
document: {
|
||||
currentScript: null,
|
||||
documentElement: { setAttribute: () => {} },
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
fetch: (rawURL, options = {}) => {
|
||||
const url = new URL(rawURL);
|
||||
requests.push({ url, options });
|
||||
if (url.pathname === '/api/v1/bd-center/overview')
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
profile: {
|
||||
user_id: '7',
|
||||
display_user_id: '100007',
|
||||
username: 'Fami BD',
|
||||
contact_info: '+90 555',
|
||||
},
|
||||
bd_profile: { user_id: '7' },
|
||||
})
|
||||
);
|
||||
if (url.pathname === '/api/v1/bd-center/stats')
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
items: [
|
||||
{
|
||||
agency: {
|
||||
agency_id: '9',
|
||||
short_id: '90009',
|
||||
name: 'Fami Stars',
|
||||
},
|
||||
owner: {
|
||||
display_user_id: '100042',
|
||||
username: 'Owner',
|
||||
},
|
||||
country: 'Türkiye',
|
||||
total_hosts: 4,
|
||||
gifted_host_count: 3,
|
||||
diamond_earnings: 7700,
|
||||
joined_at_ms: 1750000000000,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
if (url.pathname === '/api/v1/users/by-display-user-id/100099')
|
||||
return Promise.resolve(apiResponse({ user_id: '99' }));
|
||||
return Promise.resolve(apiResponse({}));
|
||||
},
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) || null,
|
||||
removeItem: (key) => storage.delete(key),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
location,
|
||||
};
|
||||
window.window = window;
|
||||
return {
|
||||
context: vm.createContext({
|
||||
Buffer,
|
||||
CustomEvent: function CustomEvent() {},
|
||||
Date,
|
||||
Error,
|
||||
FormData,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
Promise,
|
||||
String,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
console,
|
||||
document: window.document,
|
||||
window,
|
||||
}),
|
||||
requests,
|
||||
window,
|
||||
};
|
||||
}
|
||||
function run(context, file) {
|
||||
vm.runInContext(fs.readFileSync(path.join(root, file), 'utf8'), context, {
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = harness('?token=test-token&env=test');
|
||||
run(browser.context, 'guild/packages/core/product-context.js');
|
||||
run(browser.context, 'guild/products/fami/product.js');
|
||||
run(browser.context, 'common/api.js');
|
||||
run(browser.context, 'guild/packages/core/date-range.js');
|
||||
run(browser.context, 'guild/packages/bd-center/bd-center.js');
|
||||
const range = browser.window.HyGuild.dateRange.create('last_7_days', {
|
||||
now: new Date(2026, 6, 12),
|
||||
});
|
||||
const data = await browser.window.HyGuild.bdCenter.load(range);
|
||||
assert.equal(data.profile.displayUserId, '100007');
|
||||
assert.equal(data.stats.agencies[0].diamondEarnings, 7700);
|
||||
assert.equal(data.stats.agencies[0].country, 'Türkiye');
|
||||
await browser.window.HyGuild.bdCenter.inviteAgency('100099', 'New Stars');
|
||||
const invite = browser.requests.find(
|
||||
({ url }) => url.pathname === '/api/v1/bd/invitations/agency'
|
||||
);
|
||||
assert.deepEqual(JSON.parse(invite.options.body), {
|
||||
command_id: 'fami-bd-invite-agency:fixed-uuid',
|
||||
target_user_id: '99',
|
||||
agency_name: 'New Stars',
|
||||
});
|
||||
const stats = browser.requests.find(
|
||||
({ url }) => url.pathname === '/api/v1/bd-center/stats'
|
||||
);
|
||||
assert.equal(stats.url.searchParams.get('start_date'), '2026-07-06');
|
||||
assert.equal(stats.url.searchParams.get('end_date'), '2026-07-12');
|
||||
browser.requests.forEach(({ options }) =>
|
||||
assert.equal(options.headers['X-App-Code'], 'fami')
|
||||
);
|
||||
const mockBrowser = harness('?mock=1');
|
||||
run(mockBrowser.context, 'guild/packages/core/product-context.js');
|
||||
run(mockBrowser.context, 'guild/products/fami/product.js');
|
||||
run(mockBrowser.context, 'guild/packages/core/date-range.js');
|
||||
run(mockBrowser.context, 'guild/packages/bd-center/bd-center.js');
|
||||
const mock = await mockBrowser.window.HyGuild.bdCenter.load(
|
||||
mockBrowser.window.HyGuild.dateRange.create('today', {
|
||||
now: new Date(2026, 6, 12),
|
||||
})
|
||||
);
|
||||
assert.equal(mock.stats.agencies.length, 1);
|
||||
assert.equal(mockBrowser.requests.length, 0);
|
||||
console.log('Fami BD center tests passed');
|
||||
}
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
170
scripts/test_fami_host_center.js
Normal file
170
scripts/test_fami_host_center.js
Normal file
@ -0,0 +1,170 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function apiResponse(data) {
|
||||
return {
|
||||
headers: { get: () => '' },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: () => Promise.resolve(JSON.stringify({ code: 'OK', data })),
|
||||
};
|
||||
}
|
||||
|
||||
function harness(search = '') {
|
||||
const requests = [];
|
||||
const location = {
|
||||
hash: '',
|
||||
hostname: 'h5.global-interaction.com',
|
||||
href: `https://h5.global-interaction.com/guild/fami/host-center/${search}`,
|
||||
origin: 'https://h5.global-interaction.com',
|
||||
pathname: '/guild/fami/host-center/',
|
||||
search,
|
||||
};
|
||||
const storage = new Map();
|
||||
const window = {
|
||||
atob: (value) => Buffer.from(value, 'base64').toString('binary'),
|
||||
document: {
|
||||
currentScript: null,
|
||||
documentElement: { setAttribute: () => {} },
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
fetch: (rawURL, options = {}) => {
|
||||
const url = new URL(rawURL);
|
||||
requests.push({ url, options });
|
||||
if (url.pathname === '/api/v1/users/me') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
user_id: '42',
|
||||
display_user_id: '100042',
|
||||
username: 'Fami Host',
|
||||
contact_info: '+90 555 0100',
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/host-center/agency') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
agency: {
|
||||
agency_id: '9',
|
||||
short_id: '90009',
|
||||
name: 'Fami Agency',
|
||||
joined_at_ms: 1_750_000_000_000,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/wallet/me/balances') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
balances: [
|
||||
{ asset_type: 'POINT', available_amount: 8800 },
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/host-center/stats') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
metrics: {
|
||||
diamond_earnings: 700,
|
||||
diamond_exchanged: 200,
|
||||
gift_senders: 3,
|
||||
online_duration_ms: 7200000,
|
||||
valid_mic_duration_ms: 3600000,
|
||||
valid_mic_days: 1,
|
||||
private_message_senders: 4,
|
||||
new_followers: 5,
|
||||
},
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve(apiResponse({}));
|
||||
},
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) || null,
|
||||
removeItem: (key) => storage.delete(key),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
location,
|
||||
};
|
||||
window.window = window;
|
||||
const context = vm.createContext({
|
||||
Buffer,
|
||||
CustomEvent: function CustomEvent() {},
|
||||
Date,
|
||||
Error,
|
||||
FormData,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
Promise,
|
||||
String,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
console,
|
||||
document: window.document,
|
||||
window,
|
||||
});
|
||||
return { context, requests, window };
|
||||
}
|
||||
|
||||
function run(context, file) {
|
||||
vm.runInContext(fs.readFileSync(path.join(root, file), 'utf8'), context, {
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = harness('?token=test-token&env=test');
|
||||
run(browser.context, 'guild/packages/core/product-context.js');
|
||||
run(browser.context, 'guild/products/fami/product.js');
|
||||
run(browser.context, 'common/api.js');
|
||||
run(browser.context, 'guild/packages/core/date-range.js');
|
||||
run(browser.context, 'guild/packages/host-center/host-center.js');
|
||||
|
||||
const range = browser.window.HyGuild.dateRange.create('last_7_days', {
|
||||
now: new Date(2026, 6, 12),
|
||||
});
|
||||
const data = await browser.window.HyGuild.hostCenter.load(range);
|
||||
assert.equal(data.profile.displayUserId, '100042');
|
||||
assert.equal(data.agency.joinedAtMS, 1_750_000_000_000);
|
||||
assert.equal(data.pointBalance, 8800);
|
||||
assert.equal(data.stats.metrics.private_message_senders, 4);
|
||||
assert.deepEqual(Object.keys(data.errors), []);
|
||||
|
||||
const statsRequest = browser.requests.find(
|
||||
({ url }) => url.pathname === '/api/v1/host-center/stats'
|
||||
);
|
||||
assert.ok(statsRequest, 'host stats endpoint must be requested');
|
||||
assert.equal(statsRequest.url.searchParams.get('start_date'), '2026-07-06');
|
||||
assert.equal(statsRequest.url.searchParams.get('end_date'), '2026-07-12');
|
||||
browser.requests.forEach(({ options }) => {
|
||||
assert.equal(options.headers['X-App-Code'], 'fami');
|
||||
});
|
||||
|
||||
const mockBrowser = harness('?mock=1');
|
||||
run(mockBrowser.context, 'guild/packages/core/product-context.js');
|
||||
run(mockBrowser.context, 'guild/products/fami/product.js');
|
||||
run(mockBrowser.context, 'guild/packages/core/date-range.js');
|
||||
run(mockBrowser.context, 'guild/packages/host-center/host-center.js');
|
||||
const mock = await mockBrowser.window.HyGuild.hostCenter.load(
|
||||
mockBrowser.window.HyGuild.dateRange.create('today', {
|
||||
now: new Date(2026, 6, 12),
|
||||
})
|
||||
);
|
||||
assert.equal(mock.pointBalance, 5152051511212);
|
||||
assert.equal(mockBrowser.requests.length, 0);
|
||||
|
||||
console.log('Fami host center tests passed');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
153
scripts/test_fami_wallet.js
Normal file
153
scripts/test_fami_wallet.js
Normal file
@ -0,0 +1,153 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
function apiResponse(data) {
|
||||
return {
|
||||
headers: { get: () => '' },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: () => Promise.resolve(JSON.stringify({ code: 'OK', data })),
|
||||
};
|
||||
}
|
||||
function harness(search = '') {
|
||||
const requests = [];
|
||||
const storage = new Map();
|
||||
const location = {
|
||||
hash: '',
|
||||
hostname: 'h5.global-interaction.com',
|
||||
href: `https://h5.global-interaction.com/guild/fami/wallet/${search}`,
|
||||
origin: 'https://h5.global-interaction.com',
|
||||
pathname: '/guild/fami/wallet/',
|
||||
search,
|
||||
};
|
||||
const window = {
|
||||
atob: (value) => Buffer.from(value, 'base64').toString('binary'),
|
||||
crypto: { randomUUID: () => 'fixed-uuid' },
|
||||
document: {
|
||||
currentScript: null,
|
||||
documentElement: { setAttribute: () => {} },
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
fetch: (rawURL, options = {}) => {
|
||||
const url = new URL(rawURL);
|
||||
requests.push({ url, options });
|
||||
if (url.pathname === '/api/v1/point-wallet/overview') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
balance: {
|
||||
available_amount: 2000000,
|
||||
display_usd: '20.00',
|
||||
},
|
||||
points_per_usd: 100000,
|
||||
fee_bps: 500,
|
||||
minimum_points: 1000000,
|
||||
coin_sellers: [
|
||||
{
|
||||
user_id: '9007199254740001',
|
||||
display_user_id: '443344',
|
||||
nickname: 'Seller',
|
||||
point_amount: 100000,
|
||||
seller_coin_amount: 92000,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
if (url.pathname === '/api/v1/wallet/transactions') {
|
||||
return Promise.resolve(
|
||||
apiResponse({
|
||||
transactions: [
|
||||
{
|
||||
transaction_id: 'tx-1',
|
||||
biz_type: 'point_transfer_to_coin_seller',
|
||||
available_delta: -1000000,
|
||||
},
|
||||
],
|
||||
})
|
||||
);
|
||||
}
|
||||
return Promise.resolve(apiResponse({}));
|
||||
},
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) || null,
|
||||
removeItem: (key) => storage.delete(key),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
location,
|
||||
};
|
||||
window.window = window;
|
||||
return {
|
||||
context: vm.createContext({
|
||||
Buffer,
|
||||
CustomEvent: function CustomEvent() {},
|
||||
Date,
|
||||
Error,
|
||||
FormData,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
Promise,
|
||||
String,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
console,
|
||||
document: window.document,
|
||||
window,
|
||||
}),
|
||||
requests,
|
||||
window,
|
||||
};
|
||||
}
|
||||
function run(context, file) {
|
||||
vm.runInContext(fs.readFileSync(path.join(root, file), 'utf8'), context, {
|
||||
filename: file,
|
||||
});
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const browser = harness('?token=test-token&env=test');
|
||||
run(browser.context, 'guild/packages/core/product-context.js');
|
||||
run(browser.context, 'guild/products/fami/product.js');
|
||||
run(browser.context, 'common/api.js');
|
||||
run(browser.context, 'guild/packages/wallet/wallet.js');
|
||||
const data = await browser.window.HyGuild.wallet.load();
|
||||
assert.equal(data.balance.available, 2000000);
|
||||
assert.equal(data.sellers[0].userId, '9007199254740001');
|
||||
assert.equal(data.sellers[0].sellerCoinAmount, 92000);
|
||||
assert.equal(data.history[0].bizType, 'point_transfer_to_coin_seller');
|
||||
await browser.window.HyGuild.wallet.transferToCoinSeller(
|
||||
data.sellers[0].userId,
|
||||
1000000
|
||||
);
|
||||
const transfer = browser.requests.find(
|
||||
({ url }) =>
|
||||
url.pathname === '/api/v1/point-wallet/transfer-to-coin-seller'
|
||||
);
|
||||
assert.deepEqual(JSON.parse(transfer.options.body), {
|
||||
command_id: 'fami-point-coin-seller:fixed-uuid',
|
||||
seller_user_id: '9007199254740001',
|
||||
point_amount: 1000000,
|
||||
});
|
||||
browser.requests.forEach(({ options }) =>
|
||||
assert.equal(options.headers['X-App-Code'], 'fami')
|
||||
);
|
||||
|
||||
const mockBrowser = harness('?mock=1');
|
||||
run(mockBrowser.context, 'guild/packages/core/product-context.js');
|
||||
run(mockBrowser.context, 'guild/products/fami/product.js');
|
||||
run(mockBrowser.context, 'guild/packages/wallet/wallet.js');
|
||||
const mock = await mockBrowser.window.HyGuild.wallet.load();
|
||||
assert.equal(mock.sellers.length, 2);
|
||||
assert.equal(mockBrowser.requests.length, 0);
|
||||
console.log('Fami wallet tests passed');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
161
scripts/test_guild_product_architecture.js
Normal file
161
scripts/test_guild_product_architecture.js
Normal file
@ -0,0 +1,161 @@
|
||||
const assert = require('node:assert/strict');
|
||||
const fs = require('node:fs');
|
||||
const path = require('node:path');
|
||||
const vm = require('node:vm');
|
||||
|
||||
const root = path.join(__dirname, '..');
|
||||
|
||||
function response(data) {
|
||||
return {
|
||||
headers: { get: () => '' },
|
||||
ok: true,
|
||||
status: 200,
|
||||
statusText: 'OK',
|
||||
text: () => Promise.resolve(JSON.stringify({ code: 'OK', data })),
|
||||
};
|
||||
}
|
||||
|
||||
function browser(search = '') {
|
||||
const requests = [];
|
||||
const attributes = new Map();
|
||||
const storage = new Map();
|
||||
const location = {
|
||||
hash: '',
|
||||
hostname: 'h5.global-interaction.com',
|
||||
href: `https://h5.global-interaction.com/guild/fami/host-center/${search}`,
|
||||
origin: 'https://h5.global-interaction.com',
|
||||
pathname: '/guild/fami/host-center/',
|
||||
search,
|
||||
};
|
||||
const window = {
|
||||
atob: (value) => Buffer.from(value, 'base64').toString('binary'),
|
||||
document: {
|
||||
documentElement: {
|
||||
setAttribute: (key, value) => attributes.set(key, value),
|
||||
},
|
||||
},
|
||||
dispatchEvent: () => {},
|
||||
fetch: (url, options = {}) => {
|
||||
requests.push({ url, options });
|
||||
return Promise.resolve(response({ ok: true }));
|
||||
},
|
||||
localStorage: {
|
||||
getItem: (key) => storage.get(key) || null,
|
||||
removeItem: (key) => storage.delete(key),
|
||||
setItem: (key, value) => storage.set(key, String(value)),
|
||||
},
|
||||
location,
|
||||
};
|
||||
window.window = window;
|
||||
window.URL = URL;
|
||||
window.URLSearchParams = URLSearchParams;
|
||||
const context = vm.createContext({
|
||||
Buffer,
|
||||
CustomEvent: function CustomEvent() {},
|
||||
Date,
|
||||
Error,
|
||||
FormData,
|
||||
JSON,
|
||||
Math,
|
||||
Number,
|
||||
Object,
|
||||
Promise,
|
||||
String,
|
||||
URL,
|
||||
URLSearchParams,
|
||||
console,
|
||||
document: window.document,
|
||||
window,
|
||||
});
|
||||
return { attributes, context, requests, window };
|
||||
}
|
||||
|
||||
function runScript(context, relativePath) {
|
||||
const source = fs.readFileSync(path.join(root, relativePath), 'utf8');
|
||||
vm.runInContext(source, context, { filename: relativePath });
|
||||
}
|
||||
|
||||
async function requestAppCode({ product, search = '' }) {
|
||||
const harness = browser(search);
|
||||
if (product) {
|
||||
runScript(harness.context, 'guild/packages/core/product-context.js');
|
||||
runScript(harness.context, `guild/products/${product}/product.js`);
|
||||
}
|
||||
runScript(harness.context, 'common/api.js');
|
||||
await harness.window.HyAppAPI.get('/api/v1/test');
|
||||
return {
|
||||
harness,
|
||||
appCode: harness.requests[0].options.headers['X-App-Code'],
|
||||
};
|
||||
}
|
||||
|
||||
async function main() {
|
||||
const legacy = await requestAppCode({ product: '' });
|
||||
assert.equal(legacy.appCode, 'lalu', 'legacy pages must stay on Lalu');
|
||||
|
||||
const fami = await requestAppCode({ product: 'fami' });
|
||||
assert.equal(fami.appCode, 'fami', 'Fami page default must be isolated');
|
||||
assert.equal(fami.harness.window.HyAppAPI.getAppCode(), 'fami');
|
||||
assert.equal(fami.harness.attributes.get('data-hy-product'), 'fami');
|
||||
|
||||
const explicit = await requestAppCode({
|
||||
product: 'fami',
|
||||
search: '?app_code=lalu&env=test',
|
||||
});
|
||||
assert.equal(
|
||||
explicit.appCode,
|
||||
'lalu',
|
||||
'explicit diagnostic tenant must win over the page default'
|
||||
);
|
||||
|
||||
const contextHarness = browser('?token=secret&env=test&language=ar&page=8');
|
||||
runScript(contextHarness.context, 'guild/packages/core/product-context.js');
|
||||
runScript(contextHarness.context, 'guild/products/fami/product.js');
|
||||
const linked = new URL(
|
||||
contextHarness.window.HyGuild.product.buildURL('../wallet/', {
|
||||
mode: 'usdt',
|
||||
})
|
||||
);
|
||||
assert.equal(linked.searchParams.get('app_code'), 'fami');
|
||||
assert.equal(linked.searchParams.get('token'), 'secret');
|
||||
assert.equal(linked.searchParams.get('env'), 'test');
|
||||
assert.equal(linked.searchParams.get('language'), 'ar');
|
||||
assert.equal(linked.searchParams.get('mode'), 'usdt');
|
||||
assert.equal(linked.searchParams.has('page'), false);
|
||||
|
||||
const rangeHarness = browser();
|
||||
runScript(rangeHarness.context, 'guild/packages/core/date-range.js');
|
||||
const ranges = rangeHarness.window.HyGuild.dateRange;
|
||||
assert.equal(
|
||||
JSON.stringify(
|
||||
ranges.create('last_7_days', { now: new Date(2026, 6, 12) })
|
||||
),
|
||||
JSON.stringify({
|
||||
preset: 'last_7_days',
|
||||
startDate: '2026-07-06',
|
||||
endDate: '2026-07-12',
|
||||
})
|
||||
);
|
||||
assert.equal(
|
||||
JSON.stringify(
|
||||
ranges.create('last_month', { now: new Date(2026, 0, 15) })
|
||||
),
|
||||
JSON.stringify({
|
||||
preset: 'last_month',
|
||||
startDate: '2025-12-01',
|
||||
endDate: '2025-12-31',
|
||||
})
|
||||
);
|
||||
assert.throws(
|
||||
() =>
|
||||
ranges.create('custom', { start: '2026-07-12', end: '2026-07-01' }),
|
||||
/Invalid custom date range/
|
||||
);
|
||||
|
||||
console.log('guild product architecture tests passed');
|
||||
}
|
||||
|
||||
main().catch((error) => {
|
||||
console.error(error);
|
||||
process.exitCode = 1;
|
||||
});
|
||||
Loading…
x
Reference in New Issue
Block a user