Files
web_huangguo2_h5/app/static/js/recharge.js
T
谢宇宁andClaude Opus 5.5 0fbdba5ccf Initial commit
Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-29 11:47:47 +07:00

323 lines
12 KiB
JavaScript

(() => {
const root = document.querySelector('[data-recharge]');
if (!root) return;
const packages = Array.from(root.querySelectorAll('[data-recharge-package]'));
const methodsRoot = root.querySelector('[data-recharge-methods]');
const methodEmpty = root.querySelector('[data-recharge-method-empty]');
const payButton = root.querySelector('[data-recharge-pay]');
const price = root.querySelector('[data-recharge-price]');
const coins = root.querySelector('[data-recharge-coins]');
const status = root.querySelector('[data-recharge-status]');
const rowsRoot = root.querySelector('[data-recharge-rows]');
const filters = Array.from(root.querySelectorAll('[data-recharge-filter]'));
const pagination = root.querySelector('[data-recharge-pagination]');
const pageButtons = root.querySelector('[data-recharge-pages]');
const previous = root.querySelector('[data-recharge-prev]');
const next = root.querySelector('[data-recharge-next]');
const total = root.querySelector('[data-recharge-total]');
const empty = root.querySelector('[data-recharge-history-empty]');
const more = root.querySelector('[data-recharge-more]');
const exportBtn = root.querySelector('[data-recharge-export]');
const mobile = window.matchMedia('(max-width: 768px)');
let selectedPackage = null;
let selectedMethod = '';
let activeType = 0;
let currentPage = Number(pagination.dataset.page) || 1;
let pageSize = Number(pagination.dataset.pageSize) || 5;
let totalCount = Number(pagination.dataset.total) || 0;
let expanded = false;
function showStatus(message, success) {
status.textContent = message;
status.hidden = !message;
status.classList.toggle('is-success', Boolean(success));
}
function choose(button, choices) {
choices.forEach(choice => {
const isSelected = choice === button;
choice.classList.toggle('is-selected', isSelected);
choice.setAttribute('aria-checked', String(isSelected));
});
}
function parseMethods(button) {
try {
const parsed = JSON.parse(button.dataset.payMethods || '[]');
return Array.isArray(parsed) ? parsed : [];
} catch (_) {
return [];
}
}
function renderMethods(items) {
methodsRoot.replaceChildren();
selectedMethod = '';
items.forEach((item, index) => {
const type = String(item.type || '').trim();
if (!type) return;
const button = document.createElement('button');
button.className = 'recharge-method';
button.type = 'button';
button.setAttribute('role', 'radio');
button.dataset.rechargeMethod = type;
button.innerHTML = '<span class="recharge-method__radio"><img src="/static/images/placeholder.png?v=2" data-src="/static/images/recharge/check.svg" alt="" loading="lazy" decoding="async"></span><span><strong></strong><small>支付成功后到账</small></span>';
button.querySelector('strong').textContent = item.typeName || type;
button.addEventListener('click', () => {
const choices = Array.from(methodsRoot.querySelectorAll('[data-recharge-method]'));
choose(button, choices);
selectedMethod = type;
showStatus('');
});
methodsRoot.appendChild(button);
if (index === 0) {
button.classList.add('is-selected');
button.setAttribute('aria-checked', 'true');
selectedMethod = type;
} else {
button.setAttribute('aria-checked', 'false');
}
});
const hasMethod = Boolean(selectedMethod);
methodEmpty.hidden = hasMethod;
payButton.disabled = !selectedPackage || !hasMethod;
if (typeof window.initLazyImages === 'function') {
window.initLazyImages(methodsRoot);
}
}
function selectPackage(button) {
selectedPackage = button;
choose(button, packages);
price.textContent = button.dataset.price || '—';
coins.textContent = button.dataset.coins || '—';
renderMethods(parseMethods(button));
showStatus('');
}
packages.forEach(button => button.addEventListener('click', () => selectPackage(button)));
const initialPackage = packages.find(button => button.classList.contains('is-selected')) || packages[0];
if (initialPackage) selectPackage(initialPackage);
else payButton.disabled = true;
payButton.addEventListener('click', async () => {
if (!selectedPackage || !selectedMethod) {
showStatus('请选择套餐和支付方式');
return;
}
payButton.disabled = true;
showStatus('正在创建支付订单…');
const payload = {
packageId: Number(selectedPackage.dataset.packageId),
payMode: selectedMethod,
};
try {
let result;
if (window.HttpRequest && typeof window.HttpRequest.post === 'function') {
result = await window.HttpRequest.post('/api/coin/buy', payload);
} else {
const response = await fetch('/api/coin/buy', {
method: 'POST',
credentials: 'same-origin',
headers: {'Content-Type': 'application/json'},
body: JSON.stringify(payload),
});
result = await response.json();
}
if (!result || Number(result.code) !== 200 || !result.data || !result.data.payUrl) {
if (result && (Number(result.code) === 401 || Number(result.code) === 1000)) {
const login = document.querySelector('[data-login-open]');
if (login) login.click();
else window.location.href = '/login';
return;
}
throw new Error((result && (result.tip || result.msg)) || '创建支付订单失败');
}
showStatus('订单创建成功,正在前往支付…', true);
openPayInNewWindow(result.data.payUrl);
payButton.disabled = false;
} catch (error) {
showStatus(error.message || '创建支付订单失败,请稍后重试');
payButton.disabled = false;
}
});
function openPayInNewWindow(url) {
url = String(url || '').trim();
if (!url) return;
let opened = null;
try {
opened = window.open(url, '_blank', 'noopener,noreferrer');
} catch (e) {
opened = null;
}
if (opened) {
try {
opened.opener = null;
} catch (e2) {}
showStatus('已在新窗口打开支付页面,请完成支付后返回本页', true);
return;
}
const link = document.createElement('a');
link.href = url;
link.target = '_blank';
link.rel = 'noopener noreferrer';
link.style.display = 'none';
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
showStatus('若未打开支付页,请允许本站弹窗后重试', true);
}
function appendText(parent, tag, className, value) {
const node = document.createElement(tag);
node.className = className;
node.textContent = value || '';
parent.appendChild(node);
return node;
}
function renderRows(items) {
rowsRoot.replaceChildren();
items.forEach(item => {
const row = document.createElement('div');
row.className = 'recharge-history__row';
row.dataset.rechargeRow = '';
row.dataset.category = item.category || '';
appendText(row, 'span', 'recharge-history__kind', item.kind);
const detail = document.createElement('span');
detail.className = 'recharge-history__detail-line';
appendText(detail, 'span', 'recharge-history__description', item.description);
const time = document.createElement('time');
time.className = 'recharge-history__time';
appendText(time, 'span', 'only-desktop', item.time);
appendText(time, 'span', 'only-mobile', item.mobileTime);
detail.appendChild(time);
row.appendChild(detail);
const amount = appendText(row, 'strong', 'recharge-history__amount', item.amount);
amount.classList.toggle('is-credit', Boolean(item.credit));
rowsRoot.appendChild(row);
});
empty.hidden = items.length > 0;
empty.textContent = '暂无记录';
}
function renderPagination() {
const pageCount = Math.ceil(totalCount / pageSize);
currentPage = Math.min(currentPage, Math.max(1, pageCount));
pagination.hidden = totalCount === 0;
more.hidden = totalCount <= 5 && !expanded;
previous.disabled = currentPage <= 1;
next.disabled = currentPage >= pageCount;
total.textContent = '共 ' + totalCount + ' 条';
pageButtons.replaceChildren();
const firstPage = Math.max(1, Math.min(currentPage - 1, Math.max(1, pageCount - 3)));
for (let page = firstPage; page <= Math.min(pageCount, firstPage + 3); page += 1) {
const button = document.createElement('button');
button.type = 'button';
button.textContent = String(page);
button.classList.toggle('is-active', page === currentPage);
if (page === currentPage) button.setAttribute('aria-current', 'page');
button.addEventListener('click', () => loadHistory(page));
pageButtons.appendChild(button);
}
}
async function loadHistory(page) {
empty.hidden = false;
empty.textContent = '加载中…';
try {
const query = new URLSearchParams({
page: String(page),
pageSize: String(pageSize),
type: String(activeType),
});
const url = '/api/coin/trans/list?' + query.toString();
let result;
if (window.HttpRequest && typeof window.HttpRequest.get === 'function') {
result = await window.HttpRequest.get(url);
} else {
const response = await fetch(url, {credentials: 'same-origin'});
result = await response.json();
}
if (!result || Number(result.code) !== 200 || !result.data) {
throw new Error((result && (result.tip || result.msg)) || '交易明细加载失败');
}
currentPage = Number(result.data.pageNum) || page;
totalCount = Number(result.data.totalCount) || 0;
renderRows(Array.isArray(result.data.list) ? result.data.list : []);
renderPagination();
} catch (error) {
rowsRoot.replaceChildren();
empty.hidden = false;
empty.textContent = error.message || '交易明细加载失败';
pagination.hidden = true;
}
}
async function exportHistory() {
if (!exportBtn) return;
exportBtn.disabled = true;
exportBtn.classList.add('is-loading');
try {
const query = new URLSearchParams({type: String(activeType)});
const url = '/api/coin/trans/export?' + query.toString();
const response = await fetch(url, {credentials: 'same-origin'});
const contentType = (response.headers.get('Content-Type') || '').toLowerCase();
if (contentType.includes('application/json')) {
const result = await response.json();
throw new Error((result && (result.tip || result.msg)) || '导出失败');
}
if (!response.ok) {
throw new Error('导出失败');
}
const blob = await response.blob();
const disposition = response.headers.get('Content-Disposition') || '';
let filename = 'coin-trans.csv';
const match = /filename\*?=(?:UTF-8''|")?([^\";]+)/i.exec(disposition);
if (match && match[1]) {
filename = decodeURIComponent(match[1].replace(/"/g, '').trim()) || filename;
}
const objectURL = URL.createObjectURL(blob);
const link = document.createElement('a');
link.href = objectURL;
link.download = filename;
document.body.appendChild(link);
link.click();
link.remove();
URL.revokeObjectURL(objectURL);
showStatus('导出成功', true);
} catch (error) {
showStatus(error.message || '导出失败', false);
} finally {
exportBtn.disabled = false;
exportBtn.classList.remove('is-loading');
}
}
filters.forEach(button => button.addEventListener('click', () => {
activeType = Number(button.dataset.rechargeFilter) || 0;
filters.forEach(option => {
const selected = option === button;
option.classList.toggle('is-active', selected);
option.setAttribute('aria-pressed', String(selected));
});
loadHistory(1);
}));
previous.addEventListener('click', () => { if (currentPage > 1) loadHistory(currentPage - 1); });
next.addEventListener('click', () => loadHistory(currentPage + 1));
if (exportBtn) {
exportBtn.addEventListener('click', () => exportHistory());
}
more.addEventListener('click', () => {
expanded = !expanded;
pageSize = expanded ? 100 : 5;
more.firstChild.textContent = expanded ? '收起 ' : '全部 ';
more.setAttribute('aria-expanded', String(expanded));
loadHistory(1);
});
renderPagination();
})();