2026-07-12 17:38:53 +08:00

418 lines
14 KiB
JavaScript
Raw 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 () {
'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, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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();
}
})();