418 lines
14 KiB
JavaScript
418 lines
14 KiB
JavaScript
(function () {
|
||
'use strict';
|
||
|
||
var state = {
|
||
data: null,
|
||
mode: 'seller',
|
||
selectedSellerID: '',
|
||
busy: false,
|
||
};
|
||
|
||
function $(id) {
|
||
return document.getElementById(id);
|
||
}
|
||
|
||
function t(key, fallback) {
|
||
return window.HyAppI18n && window.HyAppI18n.t
|
||
? window.HyAppI18n.t(key, fallback)
|
||
: fallback || key;
|
||
}
|
||
|
||
function toast(message) {
|
||
if (window.HyAppToast) window.HyAppToast.show(message);
|
||
}
|
||
|
||
function number(value) {
|
||
var normalized = value || 0;
|
||
if (
|
||
typeof BigInt === 'function' &&
|
||
typeof normalized === 'string' &&
|
||
/^-?\d+$/.test(normalized)
|
||
) {
|
||
return new Intl.NumberFormat().format(BigInt(normalized));
|
||
}
|
||
return Number(normalized).toLocaleString(undefined, {
|
||
maximumFractionDigits: 0,
|
||
});
|
||
}
|
||
|
||
function amount() {
|
||
var value = Number($('pointAmount').value || 0);
|
||
return Number.isSafeInteger(value) && value > 0 ? value : 0;
|
||
}
|
||
|
||
function selectedSeller() {
|
||
if (!state.data) return null;
|
||
return state.data.sellers.find(function (seller) {
|
||
return seller.userId === state.selectedSellerID;
|
||
});
|
||
}
|
||
|
||
function setBusy(value) {
|
||
state.busy = value;
|
||
document.body.classList.toggle('is-busy', value);
|
||
$('sellerSubmit').disabled = value || !selectedSeller();
|
||
$('usdtSubmit').disabled = value;
|
||
$('saveAddressButton').disabled = value;
|
||
}
|
||
|
||
function renderOverview() {
|
||
if (!state.data) return;
|
||
$('pointBalance').textContent = number(state.data.balance.available);
|
||
$('balanceUSD').textContent =
|
||
'≈ ' + state.data.balance.displayUSD + ' USDT';
|
||
$('addressInput').value = state.data.address || '';
|
||
$('addressStatus').classList.toggle(
|
||
'has-address',
|
||
Boolean(state.data.address)
|
||
);
|
||
$('addressStatus').textContent = state.data.address
|
||
? t('fami_wallet.saved_address', 'Saved address:') +
|
||
' ' +
|
||
state.data.address
|
||
: t('fami_wallet.no_address', 'No USDT address saved.');
|
||
$('policyCaption').textContent =
|
||
t('fami_wallet.policy_caption', 'Rate and fee are set by Admin.') +
|
||
' · 1 USDT = ' +
|
||
number(state.data.pointsPerUSD) +
|
||
' POINT · ' +
|
||
(state.data.feeBPS / 100).toFixed(2) +
|
||
'%';
|
||
if (!$('pointAmount').value && state.data.minimumPoints > 0) {
|
||
$('pointAmount').value = String(state.data.minimumPoints);
|
||
}
|
||
renderConversion();
|
||
}
|
||
|
||
function renderSellers() {
|
||
var sellers = state.data ? state.data.sellers : [];
|
||
if (
|
||
sellers.length &&
|
||
!sellers.some(function (seller) {
|
||
return seller.userId === state.selectedSellerID;
|
||
})
|
||
) {
|
||
state.selectedSellerID = sellers[0].userId;
|
||
}
|
||
$('sellerList').innerHTML = sellers
|
||
.map(function (seller) {
|
||
var selected = seller.userId === state.selectedSellerID;
|
||
var avatar = seller.avatar
|
||
? '<img src="' + escapeHTML(seller.avatar) + '" alt="" />'
|
||
: escapeHTML(
|
||
(seller.nickname || 'C').charAt(0).toUpperCase()
|
||
);
|
||
return (
|
||
'<button class="seller-card" type="button" data-seller-id="' +
|
||
escapeHTML(seller.userId) +
|
||
'" aria-pressed="' +
|
||
selected +
|
||
'"><span class="seller-avatar">' +
|
||
avatar +
|
||
'</span><span class="seller-copy"><strong>' +
|
||
escapeHTML(seller.nickname || '—') +
|
||
'</strong><span>' +
|
||
t('fami_common.uid_prefix', 'UID:') +
|
||
' ' +
|
||
escapeHTML(seller.displayUserId || seller.userId) +
|
||
'</span><span class="seller-ratio">' +
|
||
number(seller.pointAmount) +
|
||
' : ' +
|
||
number(seller.sellerCoinAmount) +
|
||
'</span></span><span class="seller-check" aria-hidden="true">' +
|
||
(selected ? '✓' : '○') +
|
||
'</span></button>'
|
||
);
|
||
})
|
||
.join('');
|
||
$('sellerEmpty').hidden = sellers.length > 0;
|
||
setBusy(state.busy);
|
||
}
|
||
|
||
function renderHistory() {
|
||
var items = state.data ? state.data.history || [] : [];
|
||
$('historyList').innerHTML = items
|
||
.map(function (item) {
|
||
var seller = item.counterpartyUserId
|
||
? ' · UID ' + escapeHTML(item.counterpartyUserId)
|
||
: '';
|
||
return (
|
||
'<article class="history-item"><strong>' +
|
||
escapeHTML(historyLabel(item.bizType)) +
|
||
seller +
|
||
'</strong><span class="history-amount">' +
|
||
(item.availableDelta > 0 ? '+' : '') +
|
||
number(item.availableDelta) +
|
||
'</span><small>' +
|
||
dateTime(item.createdAtMS) +
|
||
'</small></article>'
|
||
);
|
||
})
|
||
.join('');
|
||
$('historyEmpty').textContent = t(
|
||
'fami_wallet.no_history',
|
||
'No withdrawal records'
|
||
);
|
||
$('historyEmpty').hidden = items.length > 0;
|
||
if (state.data && state.data.historyUnavailable) {
|
||
$('historyEmpty').hidden = false;
|
||
$('historyEmpty').textContent = t(
|
||
'fami_wallet.history_unavailable',
|
||
'History is temporarily unavailable.'
|
||
);
|
||
}
|
||
}
|
||
|
||
function historyLabel(bizType) {
|
||
if (bizType === 'point_transfer_to_coin_seller') {
|
||
return t('fami_wallet.seller_withdrawal', 'Coin seller withdrawal');
|
||
}
|
||
if (String(bizType).indexOf('withdrawal') >= 0) {
|
||
return t('fami_wallet.usdt_withdrawal', 'USDT withdrawal');
|
||
}
|
||
return t('fami_wallet.point_transaction', 'POINT transaction');
|
||
}
|
||
|
||
function renderConversion() {
|
||
var points = amount();
|
||
if (!state.data || !points) {
|
||
$('estimatedReceive').textContent = '—';
|
||
return;
|
||
}
|
||
if (state.mode === 'seller') {
|
||
var seller = selectedSeller();
|
||
var receive = seller ? ratioAmount(points, seller) : '0';
|
||
$('estimatedReceive').textContent =
|
||
number(receive) +
|
||
' ' +
|
||
t('fami_wallet.seller_coin', 'seller coins');
|
||
return;
|
||
}
|
||
var fee = Math.floor((points * state.data.feeBPS) / 10000);
|
||
var net = points - fee;
|
||
$('estimatedReceive').textContent =
|
||
(net / state.data.pointsPerUSD).toFixed(2) + ' USDT';
|
||
}
|
||
|
||
function ratioAmount(points, seller) {
|
||
// 余额可达到万亿级,Number 中间乘法会超过安全整数;BigInt 只用于前端预计值,最终到账仍由 wallet-service 计算。
|
||
if (typeof BigInt === 'function') {
|
||
return (
|
||
(BigInt(points) * BigInt(seller.sellerCoinAmount)) /
|
||
BigInt(seller.pointAmount)
|
||
).toString();
|
||
}
|
||
return String(
|
||
Math.floor((points * seller.sellerCoinAmount) / seller.pointAmount)
|
||
);
|
||
}
|
||
|
||
function selectMode(mode) {
|
||
state.mode = mode === 'usdt' ? 'usdt' : 'seller';
|
||
$('sellerPanel').hidden = state.mode !== 'seller';
|
||
$('usdtPanel').hidden = state.mode !== 'usdt';
|
||
document.querySelectorAll('[data-mode]').forEach(function (button) {
|
||
button.setAttribute(
|
||
'aria-selected',
|
||
button.getAttribute('data-mode') === state.mode
|
||
? 'true'
|
||
: 'false'
|
||
);
|
||
});
|
||
renderConversion();
|
||
}
|
||
|
||
function load() {
|
||
setBusy(true);
|
||
return window.HyGuild.wallet
|
||
.load()
|
||
.then(function (data) {
|
||
state.data = data;
|
||
renderOverview();
|
||
renderSellers();
|
||
renderHistory();
|
||
})
|
||
.catch(function () {
|
||
toast(t('fami_wallet.load_failed', 'Unable to load wallet.'));
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function submitSeller() {
|
||
var seller = selectedSeller();
|
||
var points = amount();
|
||
if (!seller || !validateAmount(points, state.data.minimumPoints))
|
||
return;
|
||
setBusy(true);
|
||
window.HyGuild.wallet
|
||
.transferToCoinSeller(seller.userId, points)
|
||
.then(function () {
|
||
toast(t('fami_wallet.submitted', 'Withdrawal submitted.'));
|
||
return load();
|
||
})
|
||
.catch(function () {
|
||
toast(
|
||
t(
|
||
'fami_wallet.submit_failed',
|
||
'Unable to submit withdrawal.'
|
||
)
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function submitUSDT() {
|
||
var points = amount();
|
||
if (!validateAmount(points, state.data.minimumPoints)) return;
|
||
var address = String($('addressInput').value || '').trim();
|
||
if (!validTRC20(address)) {
|
||
toast(
|
||
t('fami_wallet.invalid_address', 'Enter a valid TRC20 address.')
|
||
);
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
window.HyGuild.wallet
|
||
.withdrawUSDT(points, address)
|
||
.then(function () {
|
||
toast(t('fami_wallet.submitted', 'Withdrawal submitted.'));
|
||
return load();
|
||
})
|
||
.catch(function () {
|
||
toast(
|
||
t(
|
||
'fami_wallet.submit_failed',
|
||
'Unable to submit withdrawal.'
|
||
)
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function saveAddress() {
|
||
var address = String($('addressInput').value || '').trim();
|
||
if (!validTRC20(address)) {
|
||
toast(
|
||
t('fami_wallet.invalid_address', 'Enter a valid TRC20 address.')
|
||
);
|
||
return;
|
||
}
|
||
setBusy(true);
|
||
window.HyGuild.wallet
|
||
.saveAddress(address)
|
||
.then(function () {
|
||
state.data.address = address;
|
||
renderOverview();
|
||
toast(t('fami_wallet.address_saved', 'Address saved.'));
|
||
})
|
||
.catch(function () {
|
||
toast(
|
||
t(
|
||
'fami_wallet.address_save_failed',
|
||
'Unable to save address.'
|
||
)
|
||
);
|
||
})
|
||
.finally(function () {
|
||
setBusy(false);
|
||
});
|
||
}
|
||
|
||
function validateAmount(points, minimum) {
|
||
if (!points || !state.data || points > state.data.balance.available) {
|
||
toast(
|
||
t(
|
||
'fami_wallet.invalid_amount',
|
||
'Enter an available POINT amount.'
|
||
)
|
||
);
|
||
return false;
|
||
}
|
||
if (points < minimum) {
|
||
toast(
|
||
t('fami_wallet.minimum_amount', 'Minimum POINT amount:') +
|
||
' ' +
|
||
number(minimum)
|
||
);
|
||
return false;
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function validTRC20(value) {
|
||
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(value);
|
||
}
|
||
|
||
function escapeHTML(value) {
|
||
return String(value || '')
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function dateTime(value) {
|
||
if (!Number(value)) return '—';
|
||
return new Date(Number(value)).toLocaleString();
|
||
}
|
||
|
||
function bind() {
|
||
$('backButton').addEventListener('click', function () {
|
||
if (window.history.length > 1) window.history.back();
|
||
else if (window.HyAppBridge) window.HyAppBridge.back();
|
||
});
|
||
document
|
||
.querySelector('.wallet-tabs')
|
||
.addEventListener('click', function (event) {
|
||
var button = event.target.closest('[data-mode]');
|
||
if (button) selectMode(button.getAttribute('data-mode'));
|
||
});
|
||
$('sellerList').addEventListener('click', function (event) {
|
||
var button = event.target.closest('[data-seller-id]');
|
||
if (!button || state.busy) return;
|
||
state.selectedSellerID = button.getAttribute('data-seller-id');
|
||
renderSellers();
|
||
renderConversion();
|
||
});
|
||
$('pointAmount').addEventListener('input', renderConversion);
|
||
$('allButton').addEventListener('click', function () {
|
||
if (!state.data) return;
|
||
$('pointAmount').value = String(state.data.balance.available);
|
||
renderConversion();
|
||
});
|
||
$('sellerSubmit').addEventListener('click', submitSeller);
|
||
$('usdtSubmit').addEventListener('click', submitUSDT);
|
||
$('saveAddressButton').addEventListener('click', saveAddress);
|
||
window.addEventListener('hyapp:i18n-ready', function () {
|
||
renderOverview();
|
||
renderHistory();
|
||
});
|
||
}
|
||
|
||
function init() {
|
||
bind();
|
||
selectMode('seller');
|
||
load().finally(function () {
|
||
if (window.HyAppBridge) {
|
||
window.HyAppBridge.ready({
|
||
page: 'fami-wallet',
|
||
app_code: 'fami',
|
||
});
|
||
}
|
||
});
|
||
}
|
||
|
||
if (document.readyState === 'loading') {
|
||
document.addEventListener('DOMContentLoaded', init);
|
||
} else {
|
||
init();
|
||
}
|
||
})();
|