2026-07-17 11:54:43 +08:00

545 lines
19 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 () {
var ui = window.FamiCenterUI;
var identity = String(ui.params.get('identity') || 'HOST').toUpperCase();
var explicitChannel = ui.params.get('channel');
var state = {
channel: explicitChannel === 'address' ? 'address' : 'dealer',
diamondBalance: 0,
ratio: 1000000,
minimumPoints: 0,
feeBPS: 0,
dealers: [],
selectedDealerId: '',
address: '',
profileAvatar: '',
contact: '',
submitting: false,
savingAddress: false,
};
var contactController = null;
var mock = {
overview: {
asset_type: 'POINT',
balance: { available_amount: 1000000000 },
withdraw_address: { withdraw_usdt_trc20_address: '' },
points_per_usd: 1000000,
fee_bps: 0,
minimum_points: 1000000,
coin_sellers: Array.from({ length: 3 }, function (_, index) {
return {
user_id: 'seller-' + (index + 1),
display_user_id: '123456789',
nickname: 'konghuimingc',
avatar: '../../products/fami/centers/assets/fami/agency-avatar.png',
};
}),
},
user: {
avatar: '../../products/fami/centers/assets/fami/agency-avatar.png',
contact_info: '+65 1234 5678',
},
};
function normalizeIdentity(value) {
var normalized = String(value || '').toUpperCase();
if (normalized === 'AGENCY' || normalized === 'BD') {
return normalized;
}
return 'HOST';
}
function paymentAddress(payload) {
var data = ui.unwrap(payload) || {};
var source = data.withdraw_address || data;
return String(
source.withdraw_usdt_trc20_address ||
source.usdt_trc20_address ||
''
).trim();
}
function normalizeDealer(item) {
item = item || {};
return {
id: String(item.user_id || ''),
displayId: String(item.display_user_id || ''),
name: String(item.nickname || 'Dealer'),
avatar: String(item.avatar || ''),
};
}
function amountUSD() {
return ui.number(String(ui.$('withdrawAmount').value || '').trim());
}
function amountPoints() {
var text = String(ui.$('withdrawAmount').value || '').trim();
if (!/^\d+(?:\.\d{0,2})?$/.test(text)) return 0;
var parts = text.split('.');
var usdMinor =
ui.number(parts[0]) * 100 +
ui.number((parts[1] || '').padEnd(2, '0'));
// 输入框按 Figma 使用美元符号,账本却只接收整数 POINT用分为单位换算可避免浮点金额跨端不一致。
return Math.round((usdMinor * state.ratio) / 100);
}
function validTrc20(value) {
return /^T[1-9A-HJ-NP-Za-km-z]{33}$/.test(String(value || '').trim());
}
function applyOverview(payload) {
var data = ui.unwrap(payload) || {};
var balance = data.balance || {};
state.diamondBalance = ui.number(balance.available_amount);
state.ratio = ui.number(data.points_per_usd) || 1000000;
state.minimumPoints = ui.number(data.minimum_points);
state.feeBPS = ui.number(data.fee_bps);
state.dealers = Array.isArray(data.coin_sellers)
? data.coin_sellers.map(normalizeDealer).filter(function (item) {
return item.id && item.displayId;
})
: [];
if (
!state.dealers.some(function (dealer) {
return dealer.id === state.selectedDealerId;
})
) {
state.selectedDealerId = state.dealers.length
? state.dealers[0].id
: '';
}
state.address = paymentAddress(data);
if (!explicitChannel) {
state.channel = state.dealers.length ? 'dealer' : 'address';
}
}
function renderTabs() {
document.querySelectorAll('.withdraw-tab').forEach(function (tab) {
var active = tab.getAttribute('data-mode') === 'usdt';
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', String(active));
});
}
function renderDealerList() {
var list = ui.$('dealerList');
list.replaceChildren();
state.dealers.slice(0, 3).forEach(function (dealer) {
var card = document.createElement('button');
card.className = 'dealer-card';
card.type = 'button';
card.setAttribute(
'aria-pressed',
String(dealer.id === state.selectedDealerId)
);
var avatar = document.createElement('img');
avatar.className = 'dealer-card__avatar';
avatar.alt = '';
ui.setAvatar(
avatar,
dealer.avatar,
'../../products/fami/centers/assets/fami/agency-avatar.png'
);
var copy = document.createElement('span');
copy.className = 'dealer-card__copy';
var name = document.createElement('strong');
name.textContent = dealer.name;
var id = document.createElement('span');
id.textContent = 'ID:' + dealer.displayId;
var radio = document.createElement('img');
radio.className = 'dealer-card__radio';
radio.alt = '';
radio.src =
dealer.id === state.selectedDealerId
? '../../products/fami/centers/assets/fami/radio-selected.svg'
: '../../products/fami/centers/assets/fami/radio-unselected.svg';
copy.appendChild(name);
copy.appendChild(id);
card.appendChild(avatar);
card.appendChild(copy);
card.appendChild(radio);
card.addEventListener('click', function () {
state.selectedDealerId = dealer.id;
renderDealerList();
updateButtons();
});
list.appendChild(card);
});
if (!state.dealers.length) {
var empty = document.createElement('p');
empty.className = 'fami-empty';
empty.textContent = ui.t(
'fami.no_currency_dealer',
'No currency dealer'
);
list.appendChild(empty);
}
}
function renderAddress() {
ui.setAvatar(
ui.$('addressAvatar'),
state.profileAvatar,
'../../products/fami/centers/assets/fami/agency-avatar.png'
);
ui.$('addressTitle').textContent = state.address
? 'USDT TRC20'
: ui.t('withdraw_exchange.no_payment_info', 'No USDT address');
ui.$('addressText').textContent = state.address
? state.address
: ui.t(
'fami.no_address_copy',
'Fill in a TRC20 USDT address before withdrawing.'
);
ui.$('addressAddButton').hidden = !!state.address;
ui.$('addressAddButton').setAttribute(
'aria-label',
ui.t('withdraw_exchange.save_address', 'Save address')
);
}
function updateButtons() {
var points = amountPoints();
var commonDisabled =
state.submitting ||
points <= 0 ||
points > state.diamondBalance ||
(state.minimumPoints > 0 && points < state.minimumPoints);
// Dealer 参考稿在空金额时仍为主色;点击后由 validateSubmit 解释原因,地址真实提交则保持不可用直到金额和地址都有效。
ui.$('dealerSubmitButton').disabled =
state.submitting || !state.selectedDealerId;
ui.$('addressSubmitButton').disabled = commonDisabled || !state.address;
var text = state.submitting
? ui.t('withdraw_exchange.submitting', 'Submitting...')
: ui.t('fami.withdrawal', 'Withdrawal');
ui.$('dealerSubmitButton').textContent = text;
ui.$('addressSubmitButton').textContent = text;
}
function renderViews() {
ui.$('dealerView').hidden = state.channel !== 'dealer';
ui.$('addressView').hidden = state.channel !== 'address';
ui.$('goldView').hidden = true;
}
function render() {
renderTabs();
ui.$('diamondBalance').textContent = ui.formatNumber(
state.diamondBalance
);
ui.$('withdrawRatio').textContent = Math.round(state.ratio) + '=$1';
renderViews();
renderDealerList();
renderAddress();
updateButtons();
if (contactController) contactController.setContact(state.contact);
}
function load() {
if (ui.isMock()) {
applyOverview(mock.overview);
state.profileAvatar = ui.profileFrom(mock.user, 'User').avatar;
state.contact = ui.contactFrom(mock.user);
if (explicitChannel === 'address') state.channel = 'address';
render();
return;
}
var api = window.HyAppAPI;
if (!api || !api.pointWallet || !api.salaryWallet) {
render();
ui.toast(
ui.t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
return;
}
// POINT 总览一次返回真实余额、政策比例、最低门槛、地址和白名单币商;联系方式单独读取用户资料,不能再用工资 USD 推导钻石。
Promise.all([
ui.safe('point-wallet-overview', function () {
return api.pointWallet.overview();
}),
ui.safe('withdraw-contact', function () {
return api.user.me();
}),
]).then(function (results) {
if (results[0]) applyOverview(results[0]);
var profile = results[1]
? ui.profileFrom(results[1], 'User')
: null;
state.profileAvatar = profile ? profile.avatar : '';
state.contact = profile ? profile.contact : '';
render();
if (!results.some(Boolean)) {
ui.toast(
ui.t(
'withdraw_exchange.load_failed',
'Load failed. Try again later.'
)
);
}
});
}
function normalizeAmount() {
var input = ui.$('withdrawAmount');
var value = String(input.value || '')
.replace(/[^0-9.]/g, '')
.replace(/(\..*)\./g, '$1');
var parts = value.split('.');
if (parts.length > 1) value = parts[0] + '.' + parts[1].slice(0, 2);
input.value = value;
updateButtons();
}
function setAddressStatus(message, success) {
var node = ui.$('addressStatus');
node.textContent = message || '';
node.hidden = !message;
node.classList.toggle('is-success', !!success);
}
function openAddressEditor() {
ui.$('addressInput').value = state.address;
setAddressStatus('');
ui.openModal('addressModal');
window.setTimeout(function () {
ui.$('addressInput').focus();
}, 0);
}
function saveAddress(event) {
event.preventDefault();
var value = String(ui.$('addressInput').value || '').trim();
if (!validTrc20(value)) {
setAddressStatus(
ui.t(
'withdraw_exchange.invalid_address',
'Please enter a valid TRC20 USDT address'
)
);
return;
}
if (ui.isMock()) {
state.address = value;
ui.closeModal('addressModal');
render();
ui.toast(ui.t('withdraw_exchange.address_saved', 'Address saved'));
return;
}
var api = window.HyAppAPI && window.HyAppAPI.salaryWallet;
if (!api || !api.saveWithdrawAddress || state.savingAddress) return;
state.savingAddress = true;
ui.$('addressSaveButton').disabled = true;
ui.$('addressSaveButton').textContent = ui.t(
'withdraw_exchange.saving',
'Saving...'
);
// 提现地址由用户资料服务持久化;只在服务端确认后更新页面,失败时保留用户输入供修正。
api.saveWithdrawAddress({ usdt_trc20_address: value })
.then(function (response) {
state.address = paymentAddress(response) || value;
ui.closeModal('addressModal');
render();
ui.toast(
ui.t('withdraw_exchange.address_saved', 'Address saved')
);
})
.catch(function (error) {
setAddressStatus(
(error && error.message) ||
ui.t(
'withdraw_exchange.submit_failed',
'Submit failed.'
)
);
})
.finally(function () {
state.savingAddress = false;
ui.$('addressSaveButton').disabled = false;
ui.$('addressSaveButton').textContent = ui.t(
'withdraw_exchange.save_address',
'Save address'
);
});
}
function selectedDealer() {
return state.dealers.find(function (dealer) {
return dealer.id === state.selectedDealerId;
});
}
function validateSubmit() {
var usd = amountUSD();
var points = amountPoints();
if (usd <= 0 || points <= 0) {
return ui.t(
'withdraw_exchange.invalid_amount',
'Please enter a valid amount'
);
}
if (points > state.diamondBalance) {
return ui.t(
'withdraw_exchange.amount_exceeds_balance',
'Amount exceeds balance'
);
}
if (state.minimumPoints > 0 && points < state.minimumPoints) {
return ui
.t(
'withdraw_exchange.minimum_points',
'Minimum withdrawal is {amount} diamonds'
)
.replace('{amount}', ui.formatNumber(state.minimumPoints));
}
if (state.channel === 'dealer' && !selectedDealer()) {
return ui.t(
'fami.select_currency_dealer',
'Select a Currency Dealer'
);
}
if (state.channel === 'address' && !state.address) {
return ui.t(
'withdraw_exchange.missing_payment_info',
'Please add a TRC20 USDT address'
);
}
return '';
}
function requestSubmit(points) {
var api = window.HyAppAPI.pointWallet;
if (state.channel === 'dealer') {
return api.transferToCoinSeller({
command_id: ui.commandId('point-transfer'),
seller_user_id: selectedDealer().id,
point_amount: points,
});
}
return api.withdrawUSDT({
command_id: ui.commandId('point-withdraw'),
gross_point_amount: points,
usdt_trc20_address: state.address,
});
}
function submit() {
var error = validateSubmit();
if (error) {
ui.toast(error);
return;
}
if (!state.contact) {
ui.$('contactCard').click();
return;
}
var points = amountPoints();
if (ui.isMock()) {
state.diamondBalance = Math.max(0, state.diamondBalance - points);
ui.$('withdrawAmount').value = '';
render();
ui.toast(
ui.t(
'withdraw_exchange.withdraw_success',
'Withdrawal submitted'
)
);
return;
}
if (
!window.HyAppAPI ||
!window.HyAppAPI.pointWallet ||
state.submitting
)
return;
state.submitting = true;
updateButtons();
// 两条资金链返回余额字段不同:币商转账返回 source_point_balance_after链上提现返回 balance_after 快照;缺失时才本地扣减展示。
requestSubmit(points)
.then(function (result) {
var after =
state.channel === 'dealer'
? result && result.source_point_balance_after
: result &&
result.balance_after &&
result.balance_after.available_amount;
state.diamondBalance =
after === undefined || after === null
? Math.max(0, state.diamondBalance - points)
: ui.number(after);
ui.$('withdrawAmount').value = '';
ui.toast(
ui.t(
'withdraw_exchange.withdraw_success',
'Withdrawal submitted'
)
);
})
.catch(function (submitError) {
ui.toast(
(submitError && submitError.message) ||
ui.t(
'withdraw_exchange.submit_failed',
'Submit failed.'
)
);
})
.finally(function () {
state.submitting = false;
render();
});
}
function fallbackURL() {
var fallback = '../host-center/';
if (identity === 'AGENCY') fallback = '../agency-center/';
if (identity === 'BD') fallback = '../bd-center/';
// Wallet 回跳只继承产品上下文,不回传金额、渠道等资金页状态。
return window.HyGuild.product.buildURL(fallback);
}
function init() {
identity = normalizeIdentity(identity);
contactController = ui.bindContact({
onSaved: function (value) {
state.contact = value;
},
});
ui.$('backButton').addEventListener('click', function () {
ui.goBack(fallbackURL());
});
ui.$('withdrawAmount').addEventListener('input', normalizeAmount);
ui.$('dealerSubmitButton').addEventListener('click', submit);
ui.$('addressSubmitButton').addEventListener('click', submit);
ui.$('addressAddButton').addEventListener('click', openAddressEditor);
ui.$('addressCard').addEventListener('click', openAddressEditor);
ui.$('addressForm').addEventListener('submit', saveAddress);
document
.querySelectorAll('[data-address-close]')
.forEach(function (node) {
node.addEventListener('click', function () {
ui.closeModal('addressModal');
});
});
window.addEventListener('hyapp:i18n-ready', render);
render();
load();
if (window.HyAppBridge) {
window.HyAppBridge.ready({
page: 'fami-wallet',
app_code: 'fami',
mock: ui.isMock(),
});
}
}
document.addEventListener('DOMContentLoaded', init);
})();