2026-07-09 18:15:08 +08:00

1869 lines
62 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

(function () {
var SELLER_ASSET_TYPE = 'COIN_SELLER_COIN';
var query = new URLSearchParams(window.location.search);
var mock = {
profile: {
name: 'CoinSeller Mina',
uid: '163009',
region: 'Saudi Arabia',
avatar: avatarData('C', '#dbc8ff', '#7d57c7'),
},
balance: {
coins: 1285000,
},
users: [
{
uid: '2001',
userId: '9002001',
name: 'Alice',
avatar: avatarData('A', '#dbc8ff', '#7d57c7'),
},
{
uid: '2002',
userId: '9002002',
name: 'Bob',
avatar: avatarData('B', '#f4ebff', '#7d57c7'),
},
{
uid: '2003',
userId: '9002003',
name: 'Nora',
avatar: avatarData('N', '#efe4ff', '#7d57c7'),
},
],
quickAmounts: [10000, 50000, 100000],
records: [
{
id: 'tx-001',
type: 'income',
name: 'Stock transfer',
uid: 'System',
avatar: avatarData('S', '#e7f7f3', '#31a88f'),
coins: 300000,
amount: 36,
currency: 'USD',
time: '2026-06-04 10:30',
},
{
id: 'tx-002',
type: 'expenditure',
name: 'Alice',
uid: '2001',
avatar: avatarData('A', '#dbc8ff', '#7d57c7'),
coins: 110000,
amount: 12.5,
currency: 'USD',
time: '2026-06-04 10:10',
},
{
id: 'tx-003',
type: 'expenditure',
name: 'Bob',
uid: '2002',
avatar: avatarData('B', '#f4ebff', '#7d57c7'),
coins: 88000,
amount: 9.25,
currency: 'USD',
time: '2026-06-04 09:20',
},
{
id: 'tx-004',
type: 'income',
name: 'Manager recharge',
uid: 'System',
avatar: avatarData('M', '#e7f7f3', '#31a88f'),
coins: 180000,
amount: 22,
currency: 'USD',
time: '2026-06-03 18:42',
},
],
subSellerPermission: {
visible: true,
canInvite: true,
canTransfer: true,
},
subSellers: [
{
uid: '7001001',
userId: '107001001',
name: 'Sub Seller Sara',
avatar: avatarData('S', '#efe4ff', '#7d57c7'),
status: 'active',
balance: 320000,
},
{
uid: '7001002',
userId: '107001002',
name: 'Sub Seller Omar',
avatar: avatarData('O', '#e7f7f3', '#31a88f'),
status: 'active',
balance: 180000,
},
],
};
var data = {
profile: emptyProfile(),
balance: { coins: 0 },
users: [],
quickAmounts: mock.quickAmounts.slice(),
records: [],
subSellerPermission: emptySubSellerPermission(),
subSellers: [],
};
var state = {
target: null,
coinAmount: '',
historyTab: 'income',
historyQuery: '',
historyLimit: 3,
isMock: query.get('mock') === '1',
loading: query.get('mock') !== '1',
searching: false,
submitting: false,
subSellerLoading: false,
invitingSubSeller: false,
subSellerTransferTarget: null,
subSellerTransferAmount: '',
subSellerTransferSubmitting: 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 avatarData(text, background, foreground) {
var value =
String(text || 'C')
.trim()
.charAt(0) || 'C';
var letter = value.replace(/[<>&"']/g, '');
var svg =
'<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 96 96">' +
'<rect width="96" height="96" rx="48" fill="' +
background +
'"/>' +
'<text x="50%" y="54%" text-anchor="middle" dominant-baseline="middle" fill="' +
foreground +
'" font-family="Arial, sans-serif" font-size="38" font-weight="800">' +
letter +
'</text>' +
'</svg>';
return 'data:image/svg+xml;charset=UTF-8,' + encodeURIComponent(svg);
}
function emptyProfile() {
return {
name: '',
uid: '',
region: '',
avatar: '',
};
}
function emptySubSellerPermission() {
return {
visible: false,
canInvite: false,
canTransfer: false,
};
}
function initial(text) {
return String(text || '')
.trim()
.charAt(0)
.toUpperCase();
}
function count(value) {
return Number(value || 0).toLocaleString('en-US', {
maximumFractionDigits: 0,
});
}
function toNumber(value) {
var number = Number(value);
return Number.isFinite(number) ? number : 0;
}
function positiveNumber(value) {
var number = toNumber(value);
return number > 0 ? number : 0;
}
function booleanValue(value) {
if (value === true || value === 1) return true;
if (value === false || value === 0) return false;
var text = String(value || '')
.trim()
.toLowerCase();
return text === 'true' || text === '1' || text === 'yes';
}
function firstValue(source, keys) {
if (!source) return '';
for (var i = 0; i < keys.length; i += 1) {
var value = source[keys[i]];
if (value !== undefined && value !== null && value !== '') {
return value;
}
}
return '';
}
function unwrapData(payload) {
if (!payload || typeof payload !== 'object') return payload;
return Object.prototype.hasOwnProperty.call(payload, 'data')
? payload.data
: payload;
}
function replaceChildren(node, children) {
node.textContent = '';
children.forEach(function (child) {
node.appendChild(child);
});
}
function toast(message) {
if (!message) return;
if (window.HyAppToast && window.HyAppToast.show) {
window.HyAppToast.show(message);
return;
}
window.alert(message);
}
function setStatus(node, message, type) {
node.hidden = !message;
node.textContent = message || '';
node.className = 'form-status' + (type ? ' ' + type : '');
}
function setSkeleton(element, loading, text) {
if (!element) return;
element.classList.toggle('skeleton-line', !!loading);
if (loading) {
element.textContent = '';
return;
}
element.textContent = text || '';
}
function hasAPI() {
return (
window.HyAppAPI && window.HyAppAPI.user && window.HyAppAPI.wallet
);
}
function hasSubSellerAPI() {
return (
hasAPI() &&
window.HyAppAPI.wallet.getCoinSellerSubSellerPermission &&
window.HyAppAPI.wallet.listCoinSellerSubSellers
);
}
function setImage(image, fallback, src, name) {
if (!image || !fallback) return;
fallback.textContent = initial(name) || 'C';
if (!src) {
image.hidden = true;
image.removeAttribute('src');
fallback.hidden = false;
return;
}
image.onload = function () {
image.hidden = false;
fallback.hidden = true;
};
image.onerror = function () {
image.hidden = true;
fallback.hidden = false;
};
image.src = src;
}
function renderAvatar(container, item) {
container.textContent = '';
if (item.avatar) {
var image = document.createElement('img');
image.src = item.avatar;
image.alt = '';
image.addEventListener('error', function () {
container.textContent = initial(item.name || item.uid) || 'U';
});
container.appendChild(image);
return;
}
container.textContent = initial(item.name || item.uid) || 'U';
}
function normalizeProfile(payload) {
var source = unwrapData(payload) || {};
return {
name:
firstValue(source, [
'username',
'nickname',
'name',
'display_user_id',
'displayUserID',
'user_id',
'userId',
]) || '-',
uid:
firstValue(source, [
'display_user_id',
'displayUserID',
'displayUserId',
'user_id',
'userId',
]) || '-',
region:
firstValue(source, [
'country_display_name',
'countryDisplayName',
'country_name',
'countryName',
'region_name',
'regionName',
'country',
'region',
]) || '',
avatar:
firstValue(source, [
'avatar',
'avatar_url',
'avatarUrl',
'profile_picture_url',
'profilePictureUrl',
]) || '',
};
}
function normalizeBalance(payload) {
var source = unwrapData(payload) || {};
var balances = Array.isArray(source.balances)
? source.balances
: Array.isArray(source)
? source
: [];
var matched =
balances.find(function (item) {
return item && item.asset_type === SELLER_ASSET_TYPE;
}) ||
balances.find(function (item) {
return item && item.assetType === SELLER_ASSET_TYPE;
}) ||
balances[0] ||
{};
return {
coins: toNumber(
firstValue(matched, [
'available_amount',
'availableAmount',
'available',
'amount',
])
),
};
}
function timeLabelFromMS(value) {
var number = Number(value || 0);
if (!Number.isFinite(number) || number <= 0) return '';
var date = new Date(number);
if (Number.isNaN(date.getTime())) return '';
return (
date.getFullYear() +
'-' +
String(date.getMonth() + 1).padStart(2, '0') +
'-' +
String(date.getDate()).padStart(2, '0') +
' ' +
String(date.getHours()).padStart(2, '0') +
':' +
String(date.getMinutes()).padStart(2, '0')
);
}
function normalizeTransferAmount(item) {
// 转账美金来自服务端账务 metadata不同入账来源汇率可能不同前端不能用金币数反推缺字段时保持不展示。
var minor = positiveNumber(
firstValue(item, [
'transfer_usd_minor',
'transferUsdMinor',
'salary_usd_minor',
'salaryUsdMinor',
'recharge_usd_minor',
'rechargeUsdMinor',
'usd_minor_amount',
'usdMinorAmount',
])
);
if (minor > 0) return minor / 100;
var micro = positiveNumber(
firstValue(item, [
'paid_amount_micro',
'paidAmountMicro',
'amount_usdt_micro',
'amountUsdtMicro',
])
);
if (micro > 0) return micro / 1000000;
return positiveNumber(
firstValue(item, [
'amount_usd',
'amountUsd',
'transfer_amount_usd',
'transferAmountUsd',
])
);
}
function normalizeTransferCurrency(item, amount) {
return amount > 0
? String(
firstValue(item, [
'transfer_currency_code',
'transferCurrencyCode',
'recharge_currency_code',
'rechargeCurrencyCode',
'paid_currency_code',
'paidCurrencyCode',
'currency_code',
'currencyCode',
]) || 'USD'
).toUpperCase()
: '';
}
function formatTransferAmount(amount, currency) {
if (!amount || amount <= 0) return '';
var code = String(currency || 'USD').toUpperCase();
var value = Number(amount).toLocaleString('en-US', {
minimumFractionDigits: 2,
maximumFractionDigits: 2,
});
return code + ' ' + value;
}
function normalizeResolvedUser(payload, keyword) {
var source = unwrapData(payload) || {};
var identity = source.identity || source.user_identity || source;
var profile = source.profile || source.user || source;
var displayID =
firstValue(identity, [
'display_user_id',
'displayUserID',
'displayUserId',
'display_id',
'displayId',
]) ||
firstValue(profile, [
'display_user_id',
'displayUserID',
'displayUserId',
'display_id',
'displayId',
]) ||
String(keyword || '').trim();
var userID =
firstValue(identity, ['user_id', 'userId', 'id']) ||
firstValue(profile, ['user_id', 'userId', 'id']) ||
'';
return {
uid: displayID,
userId: userID,
name:
firstValue(profile, ['username', 'nickname', 'name']) ||
firstValue(identity, ['username', 'nickname', 'name']) ||
t('coinseller_center.user_name_template', 'User {id}').replace(
'{id}',
displayID
),
avatar:
firstValue(profile, [
'avatar',
'avatar_url',
'avatarUrl',
'profile_picture_url',
'profilePictureUrl',
]) ||
firstValue(identity, [
'avatar',
'avatar_url',
'avatarUrl',
'profile_picture_url',
'profilePictureUrl',
]) ||
'',
};
}
function normalizeTransactionRecord(item, index) {
var delta = toNumber(
firstValue(item, ['available_delta', 'availableDelta'])
);
var type = delta >= 0 ? 'income' : 'expenditure';
var counterpartyUserID = String(
firstValue(item, ['counterparty_user_id', 'counterpartyUserId']) ||
''
);
var counterpartyDisplayID = String(
firstValue(item, [
'counterparty_display_user_id',
'counterpartyDisplayUserId',
'counterpartyDisplayUserID',
]) || ''
);
var counterpartyName = firstValue(item, [
'counterparty_username',
'counterpartyUsername',
'counterparty_name',
'counterpartyName',
]);
var counterpartyAvatar = firstValue(item, [
'counterparty_avatar',
'counterpartyAvatar',
]);
var counterpartyUID = counterpartyDisplayID || counterpartyUserID;
// 入账流水也可能来自某个用户的工资转币商,标题必须优先展示对方资料;只有后台进货这类没有对方用户的流水才展示库存入账。
var name =
counterpartyName ||
(counterpartyUID
? t(
'coinseller_center.user_name_template',
'User {id}'
).replace('{id}', counterpartyUID)
: type === 'income'
? t('coinseller_center.stock_transfer', 'Stock transfer')
: t('coinseller_center.transfer_out', 'Transfer out'));
var uid =
counterpartyUID ||
(type === 'income' ? t('coinseller_center.system', 'System') : '-');
var transferAmount = normalizeTransferAmount(item);
return {
id:
firstValue(item, ['transaction_id', 'transactionId']) ||
String(firstValue(item, ['entry_id', 'entryId']) || index),
type: type,
name: name,
uid: uid,
avatar:
counterpartyAvatar ||
avatarData(
name,
type === 'income' ? '#e7f7f3' : '#dbc8ff',
type === 'income' ? '#31a88f' : '#7d57c7'
),
coins: Math.abs(delta),
amount: transferAmount,
currency: normalizeTransferCurrency(item, transferAmount),
time: timeLabelFromMS(
firstValue(item, ['created_at_ms', 'createdAtMs'])
),
};
}
function normalizeTransactions(payload) {
var source = unwrapData(payload) || {};
var items = Array.isArray(source.items)
? source.items
: Array.isArray(source.transactions)
? source.transactions
: Array.isArray(source)
? source
: [];
return items.map(function (item, index) {
return normalizeTransactionRecord(item || {}, index);
});
}
function normalizeSubSellerPermission(payload) {
var source = unwrapData(payload) || {};
var canInvite = booleanValue(
firstValue(source, [
'can_invite_sub_seller',
'canInviteSubSeller',
'can_open_sub_seller',
'canOpenSubSeller',
'can_invite',
'canInvite',
])
);
var canTransfer = booleanValue(
firstValue(source, [
'can_transfer_sub_seller',
'canTransferSubSeller',
'can_transfer',
'canTransfer',
])
);
var canList = booleanValue(
firstValue(source, [
'can_list_sub_sellers',
'canListSubSellers',
'can_list',
'canList',
])
);
var explicitVisible = firstValue(source, [
'visible',
'enabled',
'allowed',
'has_permission',
'hasPermission',
]);
return {
visible:
explicitVisible === '' || explicitVisible === undefined
? canInvite || canTransfer || canList
: booleanValue(explicitVisible),
canInvite: canInvite,
canTransfer: canTransfer,
};
}
function normalizeSubSeller(item, index) {
var source = item || {};
var displayID =
firstValue(source, [
'display_user_id',
'displayUserID',
'displayUserId',
'display_id',
'displayId',
]) || '';
var userID = firstValue(source, ['user_id', 'userId', 'id']) || '';
var name =
firstValue(source, ['username', 'nickname', 'name']) ||
t('coinseller_center.user_name_template', 'User {id}').replace(
'{id}',
displayID || userID || index + 1
);
return {
uid: displayID || userID,
userId: userID,
name: name,
avatar:
firstValue(source, [
'avatar',
'avatar_url',
'avatarUrl',
'profile_picture_url',
'profilePictureUrl',
]) || '',
status: firstValue(source, ['status']) || '',
balance: toNumber(
firstValue(source, [
'coin_seller_balance',
'coinSellerBalance',
'available_amount',
'availableAmount',
'balance',
'amount',
])
),
region:
firstValue(source, [
'country_display_name',
'countryDisplayName',
'region_name',
'regionName',
'country_name',
'countryName',
]) || '',
};
}
function normalizeSubSellers(payload) {
var source = unwrapData(payload) || {};
var items = Array.isArray(source.items)
? source.items
: Array.isArray(source.sub_sellers)
? source.sub_sellers
: Array.isArray(source.subSellers)
? source.subSellers
: Array.isArray(source.children)
? source.children
: Array.isArray(source)
? source
: [];
return items
.map(function (item, index) {
return normalizeSubSeller(item, index);
})
.filter(function (item) {
var status = String(item.status || '').toLowerCase();
// 列表接口的服务端契约只返回 active 直属关系;这里再过滤一次,避免异常数据把 pending 申请展示成可转账子币商。
return !status || status === 'active';
});
}
function applyMockData() {
data.profile = Object.assign({}, mock.profile);
data.balance = Object.assign({}, mock.balance);
data.users = mock.users.slice();
data.quickAmounts = mock.quickAmounts.slice();
data.records = mock.records.slice();
data.subSellerPermission = Object.assign({}, mock.subSellerPermission);
data.subSellers = mock.subSellers.slice();
}
function applyEmptyData() {
data.profile = emptyProfile();
data.balance = { coins: 0 };
data.users = [];
data.quickAmounts = mock.quickAmounts.slice();
data.records = [];
data.subSellerPermission = emptySubSellerPermission();
data.subSellers = [];
}
function loadSubSellerCapability() {
if (!hasSubSellerAPI()) {
data.subSellerPermission = emptySubSellerPermission();
data.subSellers = [];
return Promise.resolve();
}
state.subSellerLoading = true;
renderSubSellers();
return window.HyAppAPI.wallet
.getCoinSellerSubSellerPermission()
.then(function (response) {
data.subSellerPermission =
normalizeSubSellerPermission(response);
// 子币商列表属于权限增强区;只有服务端明确给到能力时才拉取,避免普通币商看到空功能入口。
if (!data.subSellerPermission.visible) {
data.subSellers = [];
return null;
}
return window.HyAppAPI.wallet
.listCoinSellerSubSellers({
page: 1,
page_size: 50,
})
.then(function (listResponse) {
data.subSellers = normalizeSubSellers(listResponse);
});
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] sub seller api failed',
error
);
}
data.subSellerPermission = emptySubSellerPermission();
data.subSellers = [];
})
.finally(function () {
state.subSellerLoading = false;
renderSubSellers();
});
}
function loadRealData() {
if (!hasAPI()) {
toast(
t(
'coinseller_center.load_failed',
'Load failed. Try again later.'
)
);
return Promise.resolve();
}
state.loading = true;
render();
return Promise.all([
window.HyAppAPI.user.me(),
window.HyAppAPI.wallet.balances(SELLER_ASSET_TYPE),
window.HyAppAPI.wallet.transactions
? window.HyAppAPI.wallet
.transactions({
asset_type: SELLER_ASSET_TYPE,
page: 1,
page_size: 20,
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] records api failed',
error
);
}
return null;
})
: Promise.resolve(null),
loadSubSellerCapability(),
])
.then(function (results) {
data.profile = normalizeProfile(results[0]);
data.balance = normalizeBalance(results[1]);
data.records = normalizeTransactions(results[2]);
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] api failed',
error
);
}
toast(
t(
'coinseller_center.load_failed',
'Load failed. Try again later.'
)
);
})
.finally(function () {
state.loading = false;
render();
});
}
function renderProfile() {
var image = $('profileAvatar');
var fallback = $('profileAvatarFallback');
var profile = data.profile;
var loading = !!state.loading;
setSkeleton($('profileName'), loading, profile.name || '');
setSkeleton(
$('profileUID'),
loading,
profile.uid
? t('coinseller_center.uid_prefix', 'UID: {id}').replace(
'{id}',
profile.uid
)
: ''
);
fallback.classList.toggle('avatar-skeleton', loading);
if (loading) {
image.hidden = true;
image.removeAttribute('src');
fallback.hidden = false;
fallback.textContent = '';
return;
}
setImage(image, fallback, profile.avatar, profile.name || profile.uid);
}
function renderBalance() {
setSkeleton($('coinBalance'), state.loading, count(data.balance.coins));
var coinIcon = document.querySelector('.balance-value .coin-icon');
if (coinIcon) {
coinIcon.classList.toggle('skeleton-coin', !!state.loading);
}
}
function createSubSellerRow(item) {
var row = document.createElement('article');
var avatar = document.createElement('div');
var main = document.createElement('div');
var title = document.createElement('div');
var meta = document.createElement('div');
var balance = document.createElement('div');
var action = document.createElement('button');
row.className = 'sub-seller-row';
avatar.className = 'record-avatar';
main.className = 'sub-seller-main';
title.className = 'record-title';
meta.className = 'record-meta';
balance.className = 'sub-seller-balance';
action.className = 'sub-seller-transfer';
action.type = 'button';
action.disabled =
!data.subSellerPermission.canTransfer ||
state.subSellerTransferSubmitting;
action.textContent = t('coinseller_center.transfer', 'Transfer');
renderAvatar(avatar, item);
title.textContent = item.name || '-';
meta.textContent = t(
'coinseller_center.uid_prefix',
'UID: {id}'
).replace('{id}', item.uid || '-');
balance.textContent =
t('coinseller_center.sub_seller_wallet', 'Wallet') +
': ' +
count(item.balance) +
' ' +
t('coinseller_center.coins_lower', 'coins');
action.addEventListener('click', function () {
openSubSellerTransfer(item);
});
main.appendChild(title);
main.appendChild(meta);
if (item.balance > 0) main.appendChild(balance);
row.appendChild(avatar);
row.appendChild(main);
if (data.subSellerPermission.canTransfer) row.appendChild(action);
return row;
}
function renderSubSellers() {
var section = $('subSellerSection');
if (!section) return;
var permission = data.subSellerPermission || emptySubSellerPermission();
section.hidden = !permission.visible;
$('inviteSubSellerButton').hidden = !permission.canInvite;
if (!permission.visible) return;
if (state.subSellerLoading) {
setStatus(
$('subSellerStatus'),
t('coinseller_center.loading', 'Loading...'),
''
);
} else {
setStatus(
$('subSellerStatus'),
data.subSellers.length
? ''
: t(
'coinseller_center.no_sub_sellers',
'No sub coin sellers yet.'
),
data.subSellers.length ? '' : 'warning'
);
}
replaceChildren(
$('subSellerList'),
data.subSellers.map(function (item) {
return createSubSellerRow(item);
})
);
}
function renderQuickAmounts() {
replaceChildren(
$('quickAmounts'),
data.quickAmounts.map(function (amount) {
var button = document.createElement('button');
button.className =
'quick-amount' +
(String(amount) === state.coinAmount ? ' is-active' : '');
button.type = 'button';
button.textContent = count(amount);
button.addEventListener('click', function () {
state.coinAmount = String(amount);
$('coinAmountInput').value = state.coinAmount;
setStatus($('rechargeStatus'), '', '');
renderRechargeState();
});
return button;
})
);
}
function renderTarget() {
var card = $('targetCard');
if (!state.target) {
card.hidden = true;
$('rechargeTargetName').textContent = '-';
return;
}
card.hidden = false;
renderAvatar($('targetAvatar'), state.target);
$('targetName').textContent = state.target.name;
$('targetUID').textContent = t(
'coinseller_center.uid_prefix',
'UID: {id}'
).replace('{id}', state.target.uid);
$('rechargeTargetName').textContent =
t('coinseller_center.target_user', 'Target user') +
': ' +
state.target.name;
}
function parsedCoinAmount() {
var input = $('coinAmountInput');
if (input) {
var sanitized = String(input.value || '').replace(/[^\d]/g, '');
if (input.value !== sanitized) input.value = sanitized;
state.coinAmount = sanitized;
}
return Number(state.coinAmount || 0);
}
function renderRechargeState() {
var amount = parsedCoinAmount();
var disabled =
state.loading ||
state.submitting ||
state.searching ||
!state.target ||
amount <= 0;
$('rechargeButton').disabled = disabled;
$('searchButton').disabled = state.searching;
renderQuickAmounts();
}
function recordMatchesQuery(record) {
if (!state.historyQuery) return true;
var keyword = state.historyQuery.toLowerCase();
return (
String(record.name || '')
.toLowerCase()
.indexOf(keyword) >= 0 ||
String(record.uid || '')
.toLowerCase()
.indexOf(keyword) >= 0
);
}
function visibleHistoryRecords() {
return data.records
.filter(function (record) {
return (
record.type === state.historyTab &&
recordMatchesQuery(record)
);
})
.slice(0, state.historyLimit);
}
function createRecordRow(record) {
var row = document.createElement('article');
var avatar = document.createElement('div');
var main = document.createElement('div');
var title = document.createElement('div');
var metaGroup = document.createElement('div');
var meta = document.createElement('div');
var transfer = document.createElement('span');
var side = document.createElement('div');
var amount = document.createElement('strong');
var time = document.createElement('span');
row.className = 'record-row';
avatar.className = 'record-avatar';
main.className = 'record-main';
title.className = 'record-title';
metaGroup.className = 'record-meta-group';
meta.className = 'record-meta';
transfer.className = 'record-transfer';
side.className =
'record-side' + (record.type === 'income' ? ' is-income' : '');
renderAvatar(avatar, record);
title.textContent = record.name || '-';
meta.textContent = t(
'coinseller_center.uid_prefix',
'UID: {id}'
).replace('{id}', record.uid || '-');
transfer.textContent = formatTransferAmount(
record.amount,
record.currency
);
amount.textContent =
(record.type === 'income' ? '+' : '-') +
count(record.coins) +
' ' +
t('coinseller_center.coins_lower', 'coins');
time.textContent = record.time || '';
main.appendChild(title);
metaGroup.appendChild(meta);
if (transfer.textContent) metaGroup.appendChild(transfer);
main.appendChild(metaGroup);
side.appendChild(amount);
side.appendChild(time);
row.appendChild(avatar);
row.appendChild(main);
row.appendChild(side);
return row;
}
function renderRecent() {
replaceChildren(
$('recentList'),
data.records.slice(0, 3).map(function (record) {
return createRecordRow(record);
})
);
}
function renderHistory() {
document.querySelectorAll('[data-history-tab]').forEach(function (tab) {
var active =
tab.getAttribute('data-history-tab') === state.historyTab;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', String(active));
});
var all = data.records.filter(function (record) {
return (
record.type === state.historyTab && recordMatchesQuery(record)
);
});
var records = visibleHistoryRecords();
$('loadMoreButton').hidden = records.length >= all.length;
setStatus(
$('historyStatus'),
all.length
? ''
: t(
'coinseller_center.no_transaction_records',
'No transaction records'
),
all.length ? '' : 'warning'
);
replaceChildren(
$('historyList'),
records.map(function (record) {
return createRecordRow(record);
})
);
}
function findMockUser(keyword) {
var normalized = String(keyword || '')
.trim()
.toLowerCase();
return data.users.find(function (user) {
return (
String(user.uid || '')
.toLowerCase()
.indexOf(normalized) >= 0 ||
String(user.name || '')
.toLowerCase()
.indexOf(normalized) >= 0
);
});
}
function findUser(keyword) {
if (state.isMock) {
return Promise.resolve(findMockUser(keyword) || null);
}
if (!hasAPI() || !window.HyAppAPI.user.resolveDisplayUserID) {
return Promise.reject(new Error('api_not_ready'));
}
return window.HyAppAPI.user
.resolveDisplayUserID(keyword)
.then(function (payload) {
return normalizeResolvedUser(payload, keyword);
});
}
function handleSearch(value) {
state.searching = true;
setStatus(
$('searchStatus'),
t('coinseller_center.loading', 'Loading...'),
''
);
renderRechargeState();
findUser(value)
.then(function (user) {
if (!user) {
state.target = null;
renderTarget();
renderRechargeState();
setStatus(
$('searchStatus'),
t(
'coinseller_center.user_not_found',
'User not found.'
),
'warning'
);
return;
}
state.target = user;
renderTarget();
renderRechargeState();
setStatus($('searchStatus'), '', '');
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] user search failed',
error
);
}
state.target = null;
renderTarget();
renderRechargeState();
setStatus(
$('searchStatus'),
t('coinseller_center.user_not_found', 'User not found.'),
'warning'
);
})
.finally(function () {
state.searching = false;
renderRechargeState();
});
}
function commandID() {
return [
'coin-seller-transfer',
Date.now(),
Math.random().toString(36).slice(2, 8),
].join('-');
}
function subSellerCommandID(action) {
return [
'coin-seller-sub-seller',
action || 'action',
Date.now(),
Math.random().toString(36).slice(2, 8),
].join('-');
}
function buildTransferPayload(amount) {
return {
command_id: commandID(),
target_display_user_id: String(state.target.uid || '').trim(),
amount: amount,
reason: 'player recharge',
};
}
function appendTransferRecord(amount, response) {
var payload = unwrapData(response) || {};
var coins = toNumber(
firstValue(payload, ['amount', 'coin_amount', 'coinAmount'])
);
var transferAmount = normalizeTransferAmount(payload);
data.records.unshift({
id:
firstValue(payload, ['transaction_id', 'transactionId']) ||
'tx-' + Date.now(),
type: 'expenditure',
name: state.target.name,
uid: state.target.uid,
avatar: state.target.avatar,
coins: coins || amount,
amount: transferAmount,
currency: normalizeTransferCurrency(payload, transferAmount),
time: currentTimeLabel(),
});
}
function appendSubSellerTransferRecord(amount, response) {
var target = state.subSellerTransferTarget;
if (!target) return;
var payload = unwrapData(response) || {};
var coins = toNumber(
firstValue(payload, ['amount', 'coin_amount', 'coinAmount'])
);
data.records.unshift({
id:
firstValue(payload, ['transaction_id', 'transactionId']) ||
'sub-seller-tx-' + Date.now(),
type: 'expenditure',
name: target.name,
uid: target.uid,
avatar: target.avatar,
coins: coins || amount,
amount: 0,
currency: '',
time: currentTimeLabel(),
});
}
function refreshSubSellerFromResponse(target, payload, amount) {
if (!target) return;
var balanceAfter = firstValue(payload, [
'sub_seller_balance_after',
'subSellerBalanceAfter',
'child_balance_after',
'childBalanceAfter',
'target_balance_after',
'targetBalanceAfter',
]);
data.subSellers = data.subSellers.map(function (item) {
if (String(item.uid) !== String(target.uid)) return item;
return Object.assign({}, item, {
balance:
balanceAfter !== '' && balanceAfter !== undefined
? toNumber(balanceAfter)
: item.balance + amount,
});
});
}
function inviteSubSeller() {
var input = $('inviteSubSellerInput');
var displayID = String(input.value || '').trim();
if (!data.subSellerPermission.canInvite) {
setStatus(
$('inviteSubSellerStatus'),
t('coinseller_center.invite_failed', 'Invite failed.'),
'warning'
);
return;
}
if (!displayID) {
setStatus(
$('inviteSubSellerStatus'),
t('coinseller_center.enter_user_id', 'Enter user ID'),
'warning'
);
return;
}
if (state.isMock) {
input.value = '';
closeModal('inviteSubSellerModal');
toast(
t(
'coinseller_center.invite_success',
'Official will review your invitation within 24 hours.'
)
);
return;
}
if (!hasAPI() || !window.HyAppAPI.wallet.inviteCoinSellerSubSeller) {
setStatus(
$('inviteSubSellerStatus'),
t('coinseller_center.invite_failed', 'Invite failed.'),
'warning'
);
return;
}
state.invitingSubSeller = true;
$('submitInviteSubSellerButton').disabled = true;
setStatus(
$('inviteSubSellerStatus'),
t('coinseller_center.loading', 'Loading...'),
''
);
// 邀请子币商现在只创建待审核申请command_id 保证移动端重试不会重复生成多个 pending 申请。
window.HyAppAPI.wallet
.inviteCoinSellerSubSeller({
command_id: subSellerCommandID('invite'),
target_display_user_id: displayID,
})
.then(function (response) {
input.value = '';
setStatus(
$('inviteSubSellerStatus'),
t(
'coinseller_center.invite_success',
'Official will review your invitation within 24 hours.'
),
'success'
);
// 申请提交成功不代表父子关系已经生效,列表必须继续以服务端 active 关系为准。
toast(
t(
'coinseller_center.invite_success',
'Official will review your invitation within 24 hours.'
)
);
closeModal('inviteSubSellerModal');
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] invite sub seller failed',
error
);
}
setStatus(
$('inviteSubSellerStatus'),
error && error.message
? error.message
: t(
'coinseller_center.invite_failed',
'Invite failed.'
),
'warning'
);
})
.finally(function () {
state.invitingSubSeller = false;
$('submitInviteSubSellerButton').disabled = false;
});
}
function openSubSellerTransfer(item) {
state.subSellerTransferTarget = item;
state.subSellerTransferAmount = '';
$('subSellerTransferAmountInput').value = '';
$('subSellerTransferTargetName').textContent =
t(
'coinseller_center.transfer_to_sub_seller',
'Transfer to sub seller'
) +
': ' +
item.name;
setStatus($('subSellerTransferStatus'), '', '');
renderSubSellerTransferState();
openModal('subSellerTransferModal');
window.setTimeout(function () {
$('subSellerTransferAmountInput').focus();
}, 80);
}
function parsedSubSellerTransferAmount() {
var input = $('subSellerTransferAmountInput');
if (input) {
var sanitized = String(input.value || '').replace(/[^\d]/g, '');
if (input.value !== sanitized) input.value = sanitized;
state.subSellerTransferAmount = sanitized;
}
return Number(state.subSellerTransferAmount || 0);
}
function renderSubSellerTransferState() {
var amount = parsedSubSellerTransferAmount();
$('submitSubSellerTransferButton').disabled =
state.loading ||
state.subSellerTransferSubmitting ||
!state.subSellerTransferTarget ||
amount <= 0;
}
function transferToSubSeller() {
var amount = parsedSubSellerTransferAmount();
var target = state.subSellerTransferTarget;
if (!target) {
setStatus(
$('subSellerTransferStatus'),
t(
'coinseller_center.select_sub_seller_first',
'Select a sub coin seller first.'
),
'warning'
);
return;
}
if (amount <= 0) {
setStatus(
$('subSellerTransferStatus'),
t(
'coinseller_center.valid_amount_required',
'Please enter a valid amount.'
),
'warning'
);
return;
}
if (amount > data.balance.coins) {
setStatus(
$('subSellerTransferStatus'),
t(
'coinseller_center.amount_exceeds_stock',
'Amount exceeds stock.'
),
'warning'
);
return;
}
if (state.isMock) {
data.balance.coins -= amount;
refreshSubSellerFromResponse(target, {}, amount);
appendSubSellerTransferRecord(amount, {});
finishSubSellerTransfer();
return;
}
if (
!hasAPI() ||
!window.HyAppAPI.wallet.transferCoinSellerToSubSeller
) {
setStatus($('subSellerTransferStatus'), '', '');
toast(t('coinseller_center.transfer_failed', 'Transfer failed.'));
return;
}
state.subSellerTransferSubmitting = true;
setStatus(
$('subSellerTransferStatus'),
t('coinseller_center.loading', 'Loading...'),
''
);
renderSubSellerTransferState();
// 子币商转账目标是对方的币商专用钱包不是玩家普通金币payload 单独命名,避免误走老的玩家充值接口。
window.HyAppAPI.wallet
.transferCoinSellerToSubSeller({
command_id: subSellerCommandID('transfer'),
target_display_user_id: String(target.uid || '').trim(),
amount: amount,
reason: 'sub coin seller stock transfer',
})
.then(function (response) {
var payload = unwrapData(response) || {};
var balanceAfter = firstValue(payload, [
'parent_balance_after',
'parentBalanceAfter',
'seller_balance_after',
'sellerBalanceAfter',
]);
data.balance.coins =
balanceAfter !== '' && balanceAfter !== undefined
? toNumber(balanceAfter)
: data.balance.coins - amount;
refreshSubSellerFromResponse(target, payload, amount);
appendSubSellerTransferRecord(amount, response);
finishSubSellerTransfer();
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] sub seller transfer failed',
error
);
}
setStatus($('subSellerTransferStatus'), '', '');
toast(
error && error.message
? error.message
: t(
'coinseller_center.transfer_failed',
'Transfer failed.'
)
);
})
.finally(function () {
state.subSellerTransferSubmitting = false;
renderSubSellerTransferState();
renderSubSellers();
});
}
function rechargeNow() {
var amount = parsedCoinAmount();
if (!state.target) {
setStatus(
$('rechargeStatus'),
t(
'coinseller_center.select_user_first',
'Select a user first.'
),
'warning'
);
return;
}
if (amount <= 0) {
setStatus(
$('rechargeStatus'),
t(
'coinseller_center.valid_amount_required',
'Please enter a valid amount.'
),
'warning'
);
return;
}
if (amount > data.balance.coins) {
setStatus(
$('rechargeStatus'),
t(
'coinseller_center.amount_exceeds_stock',
'Amount exceeds stock.'
),
'warning'
);
return;
}
if (state.isMock) {
data.balance.coins -= amount;
appendTransferRecord(amount, {});
finishRecharge();
return;
}
if (!hasAPI() || !window.HyAppAPI.wallet.transferCoinFromSeller) {
setStatus($('rechargeStatus'), '', '');
toast(t('coinseller_center.transfer_failed', 'Transfer failed.'));
return;
}
state.submitting = true;
setStatus(
$('rechargeStatus'),
t('coinseller_center.loading', 'Loading...'),
''
);
renderRechargeState();
window.HyAppAPI.wallet
.transferCoinFromSeller(buildTransferPayload(amount))
.then(function (response) {
var payload = unwrapData(response) || {};
var balanceAfter = toNumber(
firstValue(payload, [
'seller_balance_after',
'sellerBalanceAfter',
])
);
if (balanceAfter > 0 || payload.seller_balance_after === 0) {
data.balance.coins = balanceAfter;
} else {
data.balance.coins -= amount;
}
appendTransferRecord(amount, response);
finishRecharge();
})
.catch(function (error) {
if (window.console && window.console.warn) {
window.console.warn(
'[coinseller-center] transfer failed',
error
);
}
setStatus($('rechargeStatus'), '', '');
toast(
error && error.message
? error.message
: t(
'coinseller_center.transfer_failed',
'Transfer failed.'
)
);
})
.finally(function () {
state.submitting = false;
renderRechargeState();
});
}
function finishRecharge() {
state.coinAmount = '';
$('coinAmountInput').value = '';
setStatus(
$('rechargeStatus'),
t('coinseller_center.recharge_success', 'Recharge success.'),
'success'
);
renderBalance();
renderRecent();
renderRechargeState();
if (!$('historyModal').hidden) renderHistory();
closeModal('rechargeModal');
toast(t('coinseller_center.recharge_success', 'Recharge success.'));
}
function finishSubSellerTransfer() {
state.subSellerTransferAmount = '';
$('subSellerTransferAmountInput').value = '';
setStatus(
$('subSellerTransferStatus'),
t('coinseller_center.transfer_success', 'Transfer success.'),
'success'
);
renderBalance();
renderSubSellers();
renderRecent();
renderSubSellerTransferState();
if (!$('historyModal').hidden) renderHistory();
closeModal('subSellerTransferModal');
toast(t('coinseller_center.transfer_success', 'Transfer success.'));
}
function currentTimeLabel() {
var now = new Date();
return (
now.getFullYear() +
'-' +
String(now.getMonth() + 1).padStart(2, '0') +
'-' +
String(now.getDate()).padStart(2, '0') +
' ' +
String(now.getHours()).padStart(2, '0') +
':' +
String(now.getMinutes()).padStart(2, '0')
);
}
function openModal(id) {
var modal = $(id);
if (!modal) return;
modal.hidden = false;
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('modal-open');
}
function closeModal(id) {
var modal = $(id);
if (!modal) return;
modal.hidden = true;
modal.setAttribute('aria-hidden', 'true');
var hasOpen = Array.prototype.some.call(
document.querySelectorAll('.sheet-modal'),
function (node) {
return !node.hidden;
}
);
document.body.classList.toggle('modal-open', hasOpen);
}
function bindEvents() {
$('backButton').addEventListener('click', function () {
if (window.HyAppBridge) window.HyAppBridge.back();
});
$('historyButton').addEventListener('click', function () {
state.historyLimit = 3;
renderHistory();
openModal('historyModal');
});
$('viewAllHistoryButton').addEventListener('click', function () {
state.historyLimit = 3;
renderHistory();
openModal('historyModal');
});
$('helpButton').addEventListener('click', function () {
openModal('helpModal');
});
$('inviteSubSellerButton').addEventListener('click', function () {
$('inviteSubSellerInput').value = '';
setStatus($('inviteSubSellerStatus'), '', '');
openModal('inviteSubSellerModal');
window.setTimeout(function () {
$('inviteSubSellerInput').focus();
}, 80);
});
document
.querySelectorAll('[data-close-modal]')
.forEach(function (node) {
node.addEventListener('click', function () {
closeModal(node.getAttribute('data-close-modal'));
});
});
$('searchForm').addEventListener('submit', function (event) {
event.preventDefault();
var value = $('userSearchInput').value.trim();
if (!value) {
setStatus(
$('searchStatus'),
t('coinseller_center.enter_user_id', 'Enter user ID'),
'warning'
);
return;
}
handleSearch(value);
});
$('openRechargeButton').addEventListener('click', function () {
if (!state.target) return;
state.coinAmount = '';
$('coinAmountInput').value = '';
setStatus($('rechargeStatus'), '', '');
renderRechargeState();
openModal('rechargeModal');
window.setTimeout(function () {
$('coinAmountInput').focus();
}, 80);
});
$('coinAmountInput').addEventListener('input', function () {
state.coinAmount = $('coinAmountInput').value.replace(/[^\d]/g, '');
$('coinAmountInput').value = state.coinAmount;
setStatus($('rechargeStatus'), '', '');
renderRechargeState();
});
$('rechargeForm').addEventListener('submit', function (event) {
event.preventDefault();
rechargeNow();
});
$('inviteSubSellerForm').addEventListener('submit', function (event) {
event.preventDefault();
inviteSubSeller();
});
$('subSellerTransferAmountInput').addEventListener(
'input',
function () {
state.subSellerTransferAmount = $(
'subSellerTransferAmountInput'
).value.replace(/[^\d]/g, '');
$('subSellerTransferAmountInput').value =
state.subSellerTransferAmount;
setStatus($('subSellerTransferStatus'), '', '');
renderSubSellerTransferState();
}
);
$('subSellerTransferForm').addEventListener('submit', function (event) {
event.preventDefault();
transferToSubSeller();
});
document.querySelectorAll('[data-history-tab]').forEach(function (tab) {
tab.addEventListener('click', function () {
state.historyTab = tab.getAttribute('data-history-tab');
state.historyLimit = 3;
renderHistory();
});
});
$('historySearchForm').addEventListener('submit', function (event) {
event.preventDefault();
state.historyQuery = $('historySearchInput').value.trim();
state.historyLimit = 3;
renderHistory();
});
$('historyClearButton').addEventListener('click', function () {
state.historyQuery = '';
$('historySearchInput').value = '';
state.historyLimit = 3;
renderHistory();
});
$('loadMoreButton').addEventListener('click', function () {
state.historyLimit += 3;
renderHistory();
});
document.addEventListener('keydown', function (event) {
if (event.key !== 'Escape') return;
closeModal('historyModal');
closeModal('helpModal');
closeModal('inviteSubSellerModal');
closeModal('subSellerTransferModal');
});
}
function render() {
renderProfile();
renderBalance();
renderQuickAmounts();
renderTarget();
renderRechargeState();
renderSubSellers();
renderSubSellerTransferState();
renderRecent();
}
function init() {
if (state.isMock) {
applyMockData();
} else {
applyEmptyData();
}
render();
bindEvents();
if (window.HyAppBridge) {
window.HyAppBridge.ready({
page: 'coinseller-center',
mock: state.isMock,
});
}
if (!state.isMock) loadRealData();
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', init);
} else {
init();
}
window.addEventListener('hyapp:i18n-ready', function () {
render();
if (!$('historyModal').hidden) renderHistory();
});
})();