From 94562110bbf723fbb97d08d5203108c751b8f7f3 Mon Sep 17 00:00:00 2001 From: zhx Date: Sun, 12 Jul 2026 17:38:53 +0800 Subject: [PATCH] =?UTF-8?q?=E7=A7=AF=E5=88=86=E6=94=B6=E7=9B=8A=E6=94=BF?= =?UTF-8?q?=E7=AD=96h5?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- common/api.js | 72 ++- common/i18n.js | 48 +- guild/ARCHITECTURE.md | 95 +++ guild/fami/README.md | 12 + guild/fami/agency-center/index.html | 422 +++++++++++++ guild/fami/agency-center/script.js | 437 ++++++++++++++ guild/fami/agency-center/style.css | 566 ++++++++++++++++++ guild/fami/bd-center/index.html | 291 +++++++++ guild/fami/bd-center/script.js | 292 +++++++++ guild/fami/bd-center/style.css | 376 ++++++++++++ guild/fami/host-center/index.html | 297 +++++++++ guild/fami/host-center/script.js | 279 +++++++++ guild/fami/host-center/style.css | 409 +++++++++++++ guild/fami/wallet/index.html | 207 +++++++ guild/fami/wallet/script.js | 417 +++++++++++++ guild/fami/wallet/style.css | 396 ++++++++++++ guild/packages/agency-center/agency-center.js | 243 ++++++++ guild/packages/bd-center/bd-center.js | 161 +++++ guild/packages/core/date-range.js | 90 +++ guild/packages/core/product-context.js | 111 ++++ guild/packages/host-center/host-center.js | 196 ++++++ guild/packages/ui/center.css | 48 ++ guild/packages/wallet/wallet.js | 196 ++++++ guild/products/fami/locales/ar.json | 139 +++++ guild/products/fami/locales/en.json | 139 +++++ guild/products/fami/locales/es.json | 139 +++++ guild/products/fami/locales/tr.json | 139 +++++ guild/products/fami/product.js | 33 + package.json | 5 + scripts/test_fami_agency_center.js | 185 ++++++ scripts/test_fami_bd_center.js | 162 +++++ scripts/test_fami_host_center.js | 170 ++++++ scripts/test_fami_wallet.js | 153 +++++ scripts/test_guild_product_architecture.js | 161 +++++ 34 files changed, 7077 insertions(+), 9 deletions(-) create mode 100644 guild/ARCHITECTURE.md create mode 100644 guild/fami/README.md create mode 100644 guild/fami/agency-center/index.html create mode 100644 guild/fami/agency-center/script.js create mode 100644 guild/fami/agency-center/style.css create mode 100644 guild/fami/bd-center/index.html create mode 100644 guild/fami/bd-center/script.js create mode 100644 guild/fami/bd-center/style.css create mode 100644 guild/fami/host-center/index.html create mode 100644 guild/fami/host-center/script.js create mode 100644 guild/fami/host-center/style.css create mode 100644 guild/fami/wallet/index.html create mode 100644 guild/fami/wallet/script.js create mode 100644 guild/fami/wallet/style.css create mode 100644 guild/packages/agency-center/agency-center.js create mode 100644 guild/packages/bd-center/bd-center.js create mode 100644 guild/packages/core/date-range.js create mode 100644 guild/packages/core/product-context.js create mode 100644 guild/packages/host-center/host-center.js create mode 100644 guild/packages/ui/center.css create mode 100644 guild/packages/wallet/wallet.js create mode 100644 guild/products/fami/locales/ar.json create mode 100644 guild/products/fami/locales/en.json create mode 100644 guild/products/fami/locales/es.json create mode 100644 guild/products/fami/locales/tr.json create mode 100644 guild/products/fami/product.js create mode 100644 scripts/test_fami_agency_center.js create mode 100644 scripts/test_fami_bd_center.js create mode 100644 scripts/test_fami_host_center.js create mode 100644 scripts/test_fami_wallet.js create mode 100644 scripts/test_guild_product_architecture.js diff --git a/common/api.js b/common/api.js index 88ddbd2..3f05d17 100644 --- a/common/api.js +++ b/common/api.js @@ -256,7 +256,20 @@ var default_api = 'https://api.global-interaction.com/'; appCode = String(appCode || 'lalu') .trim() .toLowerCase(); - return appCode === 'yumi' || appCode === 'aslan' ? appCode : 'lalu'; + // App 身份统一保留在公共主题根节点;未定义专属变量的 Fami/Huwaa 会自然继承 :root 浅紫主题, + // 后续增加品牌覆盖时无需再改 API/环境解析逻辑。未知 app 仍回退 Lalu,保护旧页面。 + return ['lalu', 'fami', 'huwaa', 'yumi', 'aslan'].indexOf(appCode) >= 0 + ? appCode + : 'lalu'; + } + + function pageDefaultAppCode() { + // 新的多产品 H5 入口会在 common/api.js 执行前声明页面所属 App。页面级默认值只在 URL + // 未显式携带 app_code 时生效,既避免 Fami 页面因客户端漏参串到 Lalu,也不改变历史页面 + // “无参数即 Lalu”的兼容约定。该值不从 localStorage 读取,防止前一次访问污染下一产品。 + return String(window.HyAppDefaultAppCode || '') + .trim() + .toLowerCase(); } function requestAppCode() { @@ -265,7 +278,7 @@ var default_api = 'https://api.global-interaction.com/'; .toLowerCase(); // X-App-Code 只表达本次 URL 显式指定的后端租户,不能复用 getAppCode 的内存或 localStorage 结果。 // 这样没有 app_code 参数的入口会稳定落到 lalu,避免历史主题缓存把后续普通页面继续打到 yumi/aslan。 - return queryAppCode || 'lalu'; + return queryAppCode || pageDefaultAppCode() || 'lalu'; } function applyAppTheme(appCode) { @@ -280,6 +293,7 @@ var default_api = 'https://api.global-interaction.com/'; var queryAppCode = readQuery('app') || readQuery('app_code') || readQuery('appCode'); if (queryAppCode) return queryAppCode; + if (pageDefaultAppCode()) return pageDefaultAppCode(); if (memoryAppCode) return memoryAppCode; if (!shouldPersist()) return 'lalu'; return window.localStorage.getItem(APP_CODE_KEY) || 'lalu'; @@ -295,6 +309,11 @@ var default_api = 'https://api.global-interaction.com/'; applyAppTheme(queryAppCode); return queryAppCode; } + if (pageDefaultAppCode()) { + memoryAppCode = pageDefaultAppCode(); + applyAppTheme(memoryAppCode); + return memoryAppCode; + } if (memoryAppCode) { applyAppTheme(memoryAppCode); return memoryAppCode; @@ -632,6 +651,34 @@ var default_api = 'https://api.global-interaction.com/'; }, }; + // POINT 通用钱包的汇率、手续费和币商白名单全部由服务端快照返回;页面不持有资金参数默认值。 + var pointWalletAPI = { + overview: function () { + return request('/api/v1/point-wallet/overview', { + method: 'GET', + }); + }, + withdrawUSDT: function (payload) { + return request('/api/v1/point-wallet/withdraw', { + method: 'POST', + body: payload || {}, + }); + }, + transferToCoinSeller: function (payload) { + return request('/api/v1/point-wallet/transfer-to-coin-seller', { + method: 'POST', + body: payload || {}, + }); + }, + history: function (pageSize) { + return walletAPI.transactions({ + asset_type: 'POINT', + page: 1, + page_size: pageSize || 30, + }); + }, + }; + // H5 充值 API 只做路径和参数名封装;token、app_code、env、错误 envelope 仍由 request 统一处理。 // 金额、金币数、普通/币商钱包、三方支付参数和 USDT 链上校验都以后端订单快照为准。 function isAslanWebPay() { @@ -1438,6 +1485,13 @@ var default_api = 'https://api.global-interaction.com/'; method: 'GET', }); }, + stats: function (query) { + // 公会统计只接受自然日范围;收益、分成和主播在线数据均由 owner service 聚合。 + return request('/api/v1/agency-center/stats', { + method: 'GET', + query: query || {}, + }); + }, applications: function (status) { return request('/api/v1/agency-center/applications', { method: 'GET', @@ -1480,6 +1534,13 @@ var default_api = 'https://api.global-interaction.com/'; method: 'GET', }); }, + stats: function (query) { + // Fami 新主播中心按任意自然日区间读取服务端聚合;前端只传日期,不上传时长或收益值。 + return request('/api/v1/host-center/stats', { + method: 'GET', + query: query || {}, + }); + }, platformPolicy: function () { return request('/api/v1/host-center/platform-policy', { method: 'GET', @@ -1531,6 +1592,12 @@ var default_api = 'https://api.global-interaction.com/'; query: { page_size: pageSize || 20 }, }); }, + stats: function (query) { + return request('/api/v1/bd-center/stats', { + method: 'GET', + query: query || {}, + }); + }, inviteAgency: function (payload) { return request('/api/v1/bd/invitations/agency', { method: 'POST', @@ -2056,6 +2123,7 @@ var default_api = 'https://api.global-interaction.com/'; vip: vipAPI, user: userAPI, wallet: walletAPI, + pointWallet: pointWalletAPI, recharge: rechargeAPI, salaryWallet: salaryWalletAPI, host: hostAPI, diff --git a/common/i18n.js b/common/i18n.js index e742bb5..dcaa2b9 100644 --- a/common/i18n.js +++ b/common/i18n.js @@ -109,18 +109,52 @@ return DEFAULT_LANG; } - function load(lang) { - var normalized = normalizeLang(lang) || DEFAULT_LANG; + function localeSources() { + var sources = [localeBaseURL]; + var extras = Array.isArray(window.HyAppI18nExtraSources) + ? window.HyAppI18nExtraSources + : []; + extras.forEach(function (source) { + var normalized = String(source || '').trim(); + if (!normalized) return; + if (normalized.charAt(normalized.length - 1) !== '/') { + normalized += '/'; + } + if (sources.indexOf(normalized) < 0) sources.push(normalized); + }); + return sources; + } + + function fetchLocale(source, lang) { return window - .fetch(localeBaseURL + normalized + '.json', { - cache: 'no-store', - }) + .fetch(source + lang + '.json', { cache: 'no-store' }) .then(function (response) { if (!response.ok) throw new Error('locale_not_found'); return response.json(); }) - .then(function (json) { - messages = json || {}; + .catch(function () { + // 产品语言包是可选叠加层;单个包发布延迟不能让 common 基础文案也一起失效。 + return null; + }); + } + + function load(lang) { + var normalized = normalizeLang(lang) || DEFAULT_LANG; + return Promise.all( + localeSources().map(function (source) { + return fetchLocale(source, normalized); + }) + ) + .then(function (localeMessages) { + var found = false; + messages = {}; + localeMessages.forEach(function (sourceMessages) { + if (!sourceMessages) return; + found = true; + // 产品包后加载并覆盖同名 key,允许品牌化文案,但 common 仍提供跨页面兜底。 + Object.assign(messages, sourceMessages); + }); + if (!found) throw new Error('locale_not_found'); currentLang = normalized; storageSet(STORAGE_KEY, currentLang); apply(); diff --git a/guild/ARCHITECTURE.md b/guild/ARCHITECTURE.md new file mode 100644 index 0000000..534d95e --- /dev/null +++ b/guild/ARCHITECTURE.md @@ -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 ` + + + + +``` + +`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.js` and optional assets/theme overrides. +2. Add thin entries under `guild///`. +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. diff --git a/guild/fami/README.md b/guild/fami/README.md new file mode 100644 index 0000000..8a9e41b --- /dev/null +++ b/guild/fami/README.md @@ -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//`; 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. diff --git a/guild/fami/agency-center/index.html b/guild/fami/agency-center/index.html new file mode 100644 index 0000000..6178b81 --- /dev/null +++ b/guild/fami/agency-center/index.html @@ -0,0 +1,422 @@ + + + + + + Agency Center + + + + + + + +
+
+ +

Agency Center

+
+ + +
+
+ +
+
+
+ + +
+
+ + Agency ID: — + Created: — +
+
+ +
+ +
+ WhatsApp +
+ +
+ +
+
+ Diamond balance + +
+ +
+ +
+
+

+ Agency data +

+ +
+
+ + +
+
+ + + + + + + +
+ + +
+
+
+ Total earnings +
+
+ Gift income +
+
+ Share income +
+
+ Total hosts +
+
+ Gifted hosts +
+
+
+ + +
+
+ + +
+

Edit WhatsApp

+ +
+ +
+
+
+ + +
+

+ Custom date range +

+ + +
+ +
+
+
+ + +
+

Add host

+

+ Enter the host display ID. The host joins after + accepting the invitation. +

+ +
+ +
+
+
+ + +
+

Remove host

+

+
+ +
+
+
+
+ + + + + + + + + + + diff --git a/guild/fami/agency-center/script.js b/guild/fami/agency-center/script.js new file mode 100644 index 0000000..f8185b8 --- /dev/null +++ b/guild/fami/agency-center/script.js @@ -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(); + } +})(); diff --git a/guild/fami/agency-center/style.css b/guild/fami/agency-center/style.css new file mode 100644 index 0000000..28152ba --- /dev/null +++ b/guild/fami/agency-center/style.css @@ -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%; + } +} diff --git a/guild/fami/bd-center/index.html b/guild/fami/bd-center/index.html new file mode 100644 index 0000000..85cd449 --- /dev/null +++ b/guild/fami/bd-center/index.html @@ -0,0 +1,291 @@ + + + + + + BD Center + + + + + + + +
+
+ +

BD Center

+
+ + +
+
+ +
+
+
+ +
+
+ BDUID: — +
+
+ +
+ +
+ WhatsApp +
+ +
+ + + +
+
+

+ Agency data +

+ +
+
+ +
+ +
+ + + + + + + + + + + + + +
Agency + Owner ID + Country + Total hosts + + Gifted hosts + + Agency diamond earnings + + Joined +
+ +
+
+
+ + +
+

Edit WhatsApp

+ +
+ +
+
+
+ +
+

+ Custom date range +

+ +
+ +
+
+
+ +
+

Add agency

+

+ Enter the future agency owner's display ID. The agency + is created after the invitation is accepted. +

+ +
+ +
+
+
+
+ + + + + + + + + + + diff --git a/guild/fami/bd-center/script.js b/guild/fami/bd-center/script.js new file mode 100644 index 0000000..e5083ed --- /dev/null +++ b/guild/fami/bd-center/script.js @@ -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(); +})(); diff --git a/guild/fami/bd-center/style.css b/guild/fami/bd-center/style.css new file mode 100644 index 0000000..8082216 --- /dev/null +++ b/guild/fami/bd-center/style.css @@ -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; + } +} diff --git a/guild/fami/host-center/index.html b/guild/fami/host-center/index.html new file mode 100644 index 0000000..0a219f8 --- /dev/null +++ b/guild/fami/host-center/index.html @@ -0,0 +1,297 @@ + + + + + + Host Center + + + + + + + +
+
+ +

Host Center

+
+ + +
+
+ +
+
+
+ + +
+
+ + UID: — + + +
+
+ +
+ +
+ WhatsApp + +
+ +
+ +
+
+ Diamond balance + +
+ +
+ +
+
+

+ Data details +

+ +
+
+ + + + + + + +
+ + +
+
+ Diamond earnings +
+
+ Diamonds exchanged +
+
+ Gift senders +
+
+ Online duration +
+
+ Valid mic duration +
+
+ Valid mic days +
+
+ Private message users +
+
+ New followers +
+
+
+
+ + +
+

Edit WhatsApp

+ +
+ + +
+
+
+ + +
+

+ Custom date range +

+ + +
+ + +
+
+
+
+ + + + + + + + + + + diff --git a/guild/fami/host-center/script.js b/guild/fami/host-center/script.js new file mode 100644 index 0000000..1c512c4 --- /dev/null +++ b/guild/fami/host-center/script.js @@ -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(); + } +})(); diff --git a/guild/fami/host-center/style.css b/guild/fami/host-center/style.css new file mode 100644 index 0000000..b0add14 --- /dev/null +++ b/guild/fami/host-center/style.css @@ -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; + } +} diff --git a/guild/fami/wallet/index.html b/guild/fami/wallet/index.html new file mode 100644 index 0000000..31d3ea7 --- /dev/null +++ b/guild/fami/wallet/index.html @@ -0,0 +1,207 @@ + + + + + + Wallet + + + + + + + +
+
+ +

Withdraw

+
+ + +
+
+ +
+
+ + +
+ +
+ POINT balance + + +
+ +
+ +
+ + + +
+
+ Estimated receive + +
+ +
+ +
+
+

+ Choose coin seller +

+
+
+ + +
+ + + +
+
+

+ Withdrawal history +

+
+
+ +
+
+
+ + + + + + + + + + diff --git a/guild/fami/wallet/script.js b/guild/fami/wallet/script.js new file mode 100644 index 0000000..379894f --- /dev/null +++ b/guild/fami/wallet/script.js @@ -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 + ? '' + : escapeHTML( + (seller.nickname || 'C').charAt(0).toUpperCase() + ); + return ( + '' + ); + }) + .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 ( + '
' + + escapeHTML(historyLabel(item.bizType)) + + seller + + '' + + (item.availableDelta > 0 ? '+' : '') + + number(item.availableDelta) + + '' + + dateTime(item.createdAtMS) + + '
' + ); + }) + .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, '''); + } + + 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(); + } +})(); diff --git a/guild/fami/wallet/style.css b/guild/fami/wallet/style.css new file mode 100644 index 0000000..37cf370 --- /dev/null +++ b/guild/fami/wallet/style.css @@ -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; +} diff --git a/guild/packages/agency-center/agency-center.js b/guild/packages/agency-center/agency-center.js new file mode 100644 index 0000000..c895776 --- /dev/null +++ b/guild/packages/agency-center/agency-center.js @@ -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, + }; +})(); diff --git a/guild/packages/bd-center/bd-center.js b/guild/packages/bd-center/bd-center.js new file mode 100644 index 0000000..307c97b --- /dev/null +++ b/guild/packages/bd-center/bd-center.js @@ -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, + }; +})(); diff --git a/guild/packages/core/date-range.js b/guild/packages/core/date-range.js new file mode 100644 index 0000000..c90f889 --- /dev/null +++ b/guild/packages/core/date-range.js @@ -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, + }; +})(); diff --git a/guild/packages/core/product-context.js b/guild/packages/core/product-context.js new file mode 100644 index 0000000..ce2b5b7 --- /dev/null +++ b/guild/packages/core/product-context.js @@ -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, + }; +})(); diff --git a/guild/packages/host-center/host-center.js b/guild/packages/host-center/host-center.js new file mode 100644 index 0000000..56ec234 --- /dev/null +++ b/guild/packages/host-center/host-center.js @@ -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, + }; +})(); diff --git a/guild/packages/ui/center.css b/guild/packages/ui/center.css new file mode 100644 index 0000000..63cce9d --- /dev/null +++ b/guild/packages/ui/center.css @@ -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)); + } +} diff --git a/guild/packages/wallet/wallet.js b/guild/packages/wallet/wallet.js new file mode 100644 index 0000000..5b5c237 --- /dev/null +++ b/guild/packages/wallet/wallet.js @@ -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, + }; +})(); diff --git a/guild/products/fami/locales/ar.json b/guild/products/fami/locales/ar.json new file mode 100644 index 0000000..89df9e0 --- /dev/null +++ b/guild/products/fami/locales/ar.json @@ -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": "اسحب الآن" +} diff --git a/guild/products/fami/locales/en.json b/guild/products/fami/locales/en.json new file mode 100644 index 0000000..f52bf74 --- /dev/null +++ b/guild/products/fami/locales/en.json @@ -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" +} diff --git a/guild/products/fami/locales/es.json b/guild/products/fami/locales/es.json new file mode 100644 index 0000000..9768738 --- /dev/null +++ b/guild/products/fami/locales/es.json @@ -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" +} diff --git a/guild/products/fami/locales/tr.json b/guild/products/fami/locales/tr.json new file mode 100644 index 0000000..dfd9031 --- /dev/null +++ b/guild/products/fami/locales/tr.json @@ -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" +} diff --git a/guild/products/fami/product.js b/guild/products/fami/product.js new file mode 100644 index 0000000..fdfa3bd --- /dev/null +++ b/guild/products/fami/product.js @@ -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() + ); +})(); diff --git a/package.json b/package.json index a35086c..8204768 100644 --- a/package.json +++ b/package.json @@ -8,6 +8,11 @@ "aslan:webview": "node scripts/aslan_flutter_webview.js", "test:manager-center": "node scripts/test_manager_center_resource_pagination.js", "test:recharge-v5pay": "node scripts/test_recharge_v5pay.js", + "test:guild-architecture": "node scripts/test_guild_product_architecture.js", + "test:fami-host-center": "node scripts/test_fami_host_center.js", + "test:fami-agency-center": "node scripts/test_fami_agency_center.js", + "test:fami-bd-center": "node scripts/test_fami_bd_center.js", + "test:fami-wallet": "node scripts/test_fami_wallet.js", "format": "prettier --write \"**/*.{js,html,css,json,md}\"", "format:check": "prettier --check \"**/*.{js,html,css,json,md}\"" }, diff --git a/scripts/test_fami_agency_center.js b/scripts/test_fami_agency_center.js new file mode 100644 index 0000000..ec0ff8c --- /dev/null +++ b/scripts/test_fami_agency_center.js @@ -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; +}); diff --git a/scripts/test_fami_bd_center.js b/scripts/test_fami_bd_center.js new file mode 100644 index 0000000..0b8bd53 --- /dev/null +++ b/scripts/test_fami_bd_center.js @@ -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; +}); diff --git a/scripts/test_fami_host_center.js b/scripts/test_fami_host_center.js new file mode 100644 index 0000000..e293d26 --- /dev/null +++ b/scripts/test_fami_host_center.js @@ -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; +}); diff --git a/scripts/test_fami_wallet.js b/scripts/test_fami_wallet.js new file mode 100644 index 0000000..1dd5239 --- /dev/null +++ b/scripts/test_fami_wallet.js @@ -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; +}); diff --git a/scripts/test_guild_product_architecture.js b/scripts/test_guild_product_architecture.js new file mode 100644 index 0000000..852345e --- /dev/null +++ b/scripts/test_guild_product_architecture.js @@ -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; +});