积分收益政策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')
|
appCode = String(appCode || 'lalu')
|
||||||
.trim()
|
.trim()
|
||||||
.toLowerCase();
|
.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() {
|
function requestAppCode() {
|
||||||
@ -265,7 +278,7 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
.toLowerCase();
|
.toLowerCase();
|
||||||
// X-App-Code 只表达本次 URL 显式指定的后端租户,不能复用 getAppCode 的内存或 localStorage 结果。
|
// X-App-Code 只表达本次 URL 显式指定的后端租户,不能复用 getAppCode 的内存或 localStorage 结果。
|
||||||
// 这样没有 app_code 参数的入口会稳定落到 lalu,避免历史主题缓存把后续普通页面继续打到 yumi/aslan。
|
// 这样没有 app_code 参数的入口会稳定落到 lalu,避免历史主题缓存把后续普通页面继续打到 yumi/aslan。
|
||||||
return queryAppCode || 'lalu';
|
return queryAppCode || pageDefaultAppCode() || 'lalu';
|
||||||
}
|
}
|
||||||
|
|
||||||
function applyAppTheme(appCode) {
|
function applyAppTheme(appCode) {
|
||||||
@ -280,6 +293,7 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
var queryAppCode =
|
var queryAppCode =
|
||||||
readQuery('app') || readQuery('app_code') || readQuery('appCode');
|
readQuery('app') || readQuery('app_code') || readQuery('appCode');
|
||||||
if (queryAppCode) return queryAppCode;
|
if (queryAppCode) return queryAppCode;
|
||||||
|
if (pageDefaultAppCode()) return pageDefaultAppCode();
|
||||||
if (memoryAppCode) return memoryAppCode;
|
if (memoryAppCode) return memoryAppCode;
|
||||||
if (!shouldPersist()) return 'lalu';
|
if (!shouldPersist()) return 'lalu';
|
||||||
return window.localStorage.getItem(APP_CODE_KEY) || 'lalu';
|
return window.localStorage.getItem(APP_CODE_KEY) || 'lalu';
|
||||||
@ -295,6 +309,11 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
applyAppTheme(queryAppCode);
|
applyAppTheme(queryAppCode);
|
||||||
return queryAppCode;
|
return queryAppCode;
|
||||||
}
|
}
|
||||||
|
if (pageDefaultAppCode()) {
|
||||||
|
memoryAppCode = pageDefaultAppCode();
|
||||||
|
applyAppTheme(memoryAppCode);
|
||||||
|
return memoryAppCode;
|
||||||
|
}
|
||||||
if (memoryAppCode) {
|
if (memoryAppCode) {
|
||||||
applyAppTheme(memoryAppCode);
|
applyAppTheme(memoryAppCode);
|
||||||
return 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 统一处理。
|
// H5 充值 API 只做路径和参数名封装;token、app_code、env、错误 envelope 仍由 request 统一处理。
|
||||||
// 金额、金币数、普通/币商钱包、三方支付参数和 USDT 链上校验都以后端订单快照为准。
|
// 金额、金币数、普通/币商钱包、三方支付参数和 USDT 链上校验都以后端订单快照为准。
|
||||||
function isAslanWebPay() {
|
function isAslanWebPay() {
|
||||||
@ -1438,6 +1485,13 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
stats: function (query) {
|
||||||
|
// 公会统计只接受自然日范围;收益、分成和主播在线数据均由 owner service 聚合。
|
||||||
|
return request('/api/v1/agency-center/stats', {
|
||||||
|
method: 'GET',
|
||||||
|
query: query || {},
|
||||||
|
});
|
||||||
|
},
|
||||||
applications: function (status) {
|
applications: function (status) {
|
||||||
return request('/api/v1/agency-center/applications', {
|
return request('/api/v1/agency-center/applications', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -1480,6 +1534,13 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
method: 'GET',
|
method: 'GET',
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
stats: function (query) {
|
||||||
|
// Fami 新主播中心按任意自然日区间读取服务端聚合;前端只传日期,不上传时长或收益值。
|
||||||
|
return request('/api/v1/host-center/stats', {
|
||||||
|
method: 'GET',
|
||||||
|
query: query || {},
|
||||||
|
});
|
||||||
|
},
|
||||||
platformPolicy: function () {
|
platformPolicy: function () {
|
||||||
return request('/api/v1/host-center/platform-policy', {
|
return request('/api/v1/host-center/platform-policy', {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
@ -1531,6 +1592,12 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
query: { page_size: pageSize || 20 },
|
query: { page_size: pageSize || 20 },
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
stats: function (query) {
|
||||||
|
return request('/api/v1/bd-center/stats', {
|
||||||
|
method: 'GET',
|
||||||
|
query: query || {},
|
||||||
|
});
|
||||||
|
},
|
||||||
inviteAgency: function (payload) {
|
inviteAgency: function (payload) {
|
||||||
return request('/api/v1/bd/invitations/agency', {
|
return request('/api/v1/bd/invitations/agency', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@ -2056,6 +2123,7 @@ var default_api = 'https://api.global-interaction.com/';
|
|||||||
vip: vipAPI,
|
vip: vipAPI,
|
||||||
user: userAPI,
|
user: userAPI,
|
||||||
wallet: walletAPI,
|
wallet: walletAPI,
|
||||||
|
pointWallet: pointWalletAPI,
|
||||||
recharge: rechargeAPI,
|
recharge: rechargeAPI,
|
||||||
salaryWallet: salaryWalletAPI,
|
salaryWallet: salaryWalletAPI,
|
||||||
host: hostAPI,
|
host: hostAPI,
|
||||||
|
|||||||
@ -109,18 +109,52 @@
|
|||||||
return DEFAULT_LANG;
|
return DEFAULT_LANG;
|
||||||
}
|
}
|
||||||
|
|
||||||
function load(lang) {
|
function localeSources() {
|
||||||
var normalized = normalizeLang(lang) || DEFAULT_LANG;
|
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
|
return window
|
||||||
.fetch(localeBaseURL + normalized + '.json', {
|
.fetch(source + lang + '.json', { cache: 'no-store' })
|
||||||
cache: 'no-store',
|
|
||||||
})
|
|
||||||
.then(function (response) {
|
.then(function (response) {
|
||||||
if (!response.ok) throw new Error('locale_not_found');
|
if (!response.ok) throw new Error('locale_not_found');
|
||||||
return response.json();
|
return response.json();
|
||||||
})
|
})
|
||||||
.then(function (json) {
|
.catch(function () {
|
||||||
messages = json || {};
|
// 产品语言包是可选叠加层;单个包发布延迟不能让 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;
|
currentLang = normalized;
|
||||||
storageSet(STORAGE_KEY, currentLang);
|
storageSet(STORAGE_KEY, currentLang);
|
||||||
apply();
|
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",
|
"aslan:webview": "node scripts/aslan_flutter_webview.js",
|
||||||
"test:manager-center": "node scripts/test_manager_center_resource_pagination.js",
|
"test:manager-center": "node scripts/test_manager_center_resource_pagination.js",
|
||||||
"test:recharge-v5pay": "node scripts/test_recharge_v5pay.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": "prettier --write \"**/*.{js,html,css,json,md}\"",
|
||||||
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
"format:check": "prettier --check \"**/*.{js,html,css,json,md}\""
|
||||||
},
|
},
|
||||||
|
|||||||
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