feat(pay): 统一收银台四种模式(站内二维码/当前页跳转/表单提交/JSAPI)

- PaymentOrchestrator 统一返回 mode + code_url/payment_url/form_html/jsapi_params
- 支付宝:当面付二维码 / 电脑网站支付 / H5;微信:Native / H5 / JSAPI(WechatOpenid 静默授权)
- 易支付:PC mapi 二维码,移动端自动提交表单
- 删除旧键 alipay_payment_type / wechat_payment_type / epay_device 与 redirect/qr_content 字段
- 新增 pay-cashier.js,移除 window.open,订单状态接口改为 /pay/order/{n}
- 拆分 credits.js(affiliate / withdraw-history)至 1000 行以内

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-29 14:01:28 +08:00
1 parent b340b77d5a
commit 1df2822f01
28 files changed
+1255 -836

No files matched your search

@@ -54,13 +54,15 @@ export const gatewaySchema = {
pay_sdk_order: {},
alipay_appid: '',
alipay_privatekey: '',
alipay_payment_type: 'page',
alipay_pc_mode: 'facepay',
alipay_h5: false,
alipay_app_public_cert_path: '',
alipay_public_cert_path: '',
alipay_root_cert_path: '',
wechat_merchantid: '',
wechat_appid: '',
wechat_payment_type: 'native',
wechat_h5: false,
wechat_jsapi: false,
wechat_api_v3_key: '',
wechat_private_key_path: '',
wechat_cert_path: '',
@@ -78,7 +80,7 @@ export const gatewaySchema = {
epay_apiurl: '',
epay_pid: '',
epay_key: '',
epay_device: 'auto',
epay_qrcode: true,
},
groups: [
{
@@ -206,20 +208,22 @@ export const gatewaySchema = {
label: '',
type: 'content',
ui_only: true,
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>同时填写了企业支付以及当面付参数,则优先使用当面付</li></ul>',
content: '<ul><li>支付宝公钥为必填项目(公钥错误可以成功付款,但会回调失败)</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/alipay/{notify|return}</li><li>电脑端按「电脑端收款」选择站内二维码或跳转;手机端开启 H5 时跳转,否则显示二维码</li></ul>',
},
{ key: 'alipay_appid', label: '当面付:APPID', type: 'input', default: '' },
{ key: 'alipay_privatekey', label: '当面付:应用私钥', type: 'textarea', sanitize_type: 'code', default: '', rows: 4, secret: true },
{
key: 'alipay_payment_type',
label: '支付方式',
key: 'alipay_pc_mode',
label: '电脑端收款',
type: 'select',
default: 'page',
default: 'facepay',
options: [
{ label: '电脑网站支付(web)', value: 'page' },
{ label: '手机网站支付(wap)', value: 'wap' },
{ label: '当面付(站内二维码)', value: 'facepay' },
{ label: '电脑网站支付(跳转支付宝)', value: 'web' },
],
help: '当面付在站内弹出二维码;电脑网站支付在当前页跳转到支付宝收银台',
},
{ key: 'alipay_h5', label: '移动端 H5 跳转', type: 'switch', default: false, help: '需签约手机网站支付。开启后手机浏览器跳转支付宝 App/收银台付款;关闭则手机端也显示二维码(附唤起链接)' },
{ key: 'alipay_app_public_cert_path', label: '应用公钥证书路径', type: 'input', default: '', help: 'appCertPublicKey.crt 绝对路径。' },
{ key: 'alipay_public_cert_path', label: '支付宝公钥证书路径', type: 'input', default: '', help: 'alipayCertPublicKey_RSA2.crt 绝对路径。' },
{ key: 'alipay_root_cert_path', label: '支付宝根证书路径', type: 'input', default: '', help: 'alipayRootCert.crt 绝对路径。' },
@@ -239,20 +243,12 @@ export const gatewaySchema = {
label: '',
type: 'content',
ui_only: true,
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li></ul>',
content: '<ul><li>微信企业支付:官方接口,商家可申请,有年费,申请较为复杂</li><li>回调地址固定为 /wp-json/aksrt/v1/payment/wechatpay/{notify|return},请配置 Native 支付回调链接</li><li>电脑端显示站内二维码;手机端按下方开关选择 H5 跳转或微信内 JSAPI,未开启时同样显示二维码</li></ul>',
},
{ key: 'wechat_merchantid', label: '商户号 PartnerID', type: 'input', default: '' },
{ key: 'wechat_appid', label: '授权绑定的AppID', type: 'input', default: '' },
{
key: 'wechat_payment_type',
label: '支付方式',
type: 'select',
default: 'native',
options: [
{ label: '扫码支付(Native)', value: 'native' },
{ label: 'H5支付', value: 'h5' },
],
},
{ key: 'wechat_h5', label: '移动端 H5 跳转', type: 'switch', default: false, help: '需开通 H5 支付。开启后手机浏览器(非微信内)跳转微信付款;关闭则显示站内二维码' },
{ key: 'wechat_jsapi', label: '微信内 JSAPI 支付', type: 'switch', default: false, help: '需开通 JSAPI 支付并在商户平台配置支付授权目录。微信内打开时直接调起支付;用户 openid 通过「用户→微信公众号」的 AppID/AppSecret 静默授权获取,公众号 AppID 必须与上方 AppID 一致' },
{ key: 'wechat_api_v3_key', label: '支付APIv3密钥', type: 'input', inputType: 'password', secret: true, default: '', help: '微信商户平台设置的 APIv3 密钥(32位)。' },
{ key: 'wechat_private_key_path', label: '商户私钥路径', type: 'input', default: '', help: 'apiclient_key.pem 绝对路径,用于签名。' },
{ key: 'wechat_cert_path', label: '商户API证书路径', type: 'input', default: '', help: 'apiclient_cert.pem 绝对路径。' },
@@ -329,17 +325,7 @@ export const gatewaySchema = {
{ key: 'epay_apiurl', label: 'API接口网址', type: 'input', default: '', help: '请填写接口地址,例如:https://pay.xxxxxxx.cn/' },
{ key: 'epay_pid', label: '商户号', type: 'input', default: '' },
{ key: 'epay_key', label: '商户秘钥', type: 'input', inputType: 'password', secret: true, default: '', help: 'MD5 通信密钥。' },
{
key: 'epay_device',
label: '下单终端',
type: 'select',
default: 'auto',
options: [
{ label: '自动(按 UA)', value: 'auto' },
{ label: '电脑端 submit', value: 'pc' },
{ label: '移动端 mapi', value: 'mobile' },
],
},
{ key: 'epay_qrcode', label: '电脑端站内二维码', type: 'switch', default: true, help: '开启后电脑端通过 mapi 接口获取二维码在站内展示(接口未返回二维码时跳转支付页);关闭则跳转易支付收银台。移动端始终跳转' },
],
},
],
+14 -13
View File
@@ -281,7 +281,7 @@
const maxTries = 40;
const timer = setInterval(function () {
tries += 1;
fetch(REST_BASE + '/pay/content/order/' + encodeURIComponent(orderNumber), {
fetch(REST_BASE + '/pay/order/' + encodeURIComponent(orderNumber), {
method: 'GET',
credentials: 'same-origin',
headers: restHeaders(),
@@ -673,13 +673,17 @@
}
const data = (r.json && r.json.data) ? r.json.data : r.json;
delete button.dataset.aksrtOperationKey;
if (data && data.redirect && (data.payment_url || data.qr_content)) {
if (data.payment_url) {
window.open(data.payment_url, '_blank');
}
renderScanPay(data, returnFocusEl);
if (data && data.mode) {
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
if (data.order_number) pollOrderPaid(data.order_number, returnFocusEl);
if (data.mode === 'qrcode') {
renderScanPay(data, returnFocusEl);
} else if (window.AksrtPayCashier) {
close();
window.AksrtPayCashier.handle(data, {});
}
if ((data.mode === 'qrcode' || data.mode === 'jsapi') && data.order_number) {
pollOrderPaid(data.order_number, returnFocusEl);
}
return;
}
window.location.reload();
@@ -693,12 +697,9 @@
function renderScanPay(data, returnFocusEl) {
data = data || {};
let body = '';
if (data.qr_content) {
body += '<div class="aksrt-dl-modal__code-card"><div class="aksrt-dl-modal__code-caption">凭证内容</div>' +
'<code class="aksrt-dl-modal__code aksrt-dl-modal__code--wrap">' + escapeHtml(String(data.qr_content)) + '</code></div>';
}
body += '<p class="aksrt-dl-modal__msg">' + escapeHtml(data.message || '请使用手机扫码完成支付,支付后刷新页面。') + '</p>';
const body = window.AksrtPayCashier
? window.AksrtPayCashier.qrHtml(data)
: '<p class="aksrt-dl-modal__msg">' + escapeHtml('收银台组件未加载,请刷新页面重试。') + '</p>';
open(
dlView('pay', {
subtitle: '支付完成后页面会自动刷新',
+342
View File
@@ -0,0 +1,342 @@
/**
* AKSRT affiliate revenue center + referral poster (split from credits.js).
* Exposes window.AKSRTAffiliateRevenueInit for the user-center router.
*
* @package AKSRT
*/
(function () {
"use strict";
function api(path, options) {
return window.AksrtRequest.request(path, options || {}).then(function (response) {
return { ok: response.ok, status: response.status, json: response.json || {}, data: response.data };
}).catch(function (error) {
var response = error && error.response ? error.response : {};
return { ok: false, status: (error && error.status) || response.status || 0, json: (error && error.json) || response.json || {} };
});
}
// ===== Forward-only affiliate revenue center =====
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
* 海报配置与佣金比例,取失败也不影响复制。 */
function initAffiliateRevenue() {
var root = document.querySelector('[data-aksrt-revenue-page]');
if (!root || root.dataset.affiliateBound) return;
root.dataset.affiliateBound = '1';
var input = root.querySelector('[data-affiliate-link]');
var copy = root.querySelector('[data-affiliate-copy]');
var url = input ? input.value : '';
if (copy) copy.disabled = !url;
api('/revenue/me').then(function (r) {
if (!r.ok) return;
var j = r.json || {};
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
if (copy) copy.addEventListener('click', function () {
if (!input || !input.value) return;
var done = function () { notyf('推广链接已复制', 'success', 3, 'aksrt-affiliate-copy-toast'); };
var fail = function () { input.focus(); input.select(); notyf('复制失败,请手动复制', 'danger', 3, 'aksrt-affiliate-copy-toast'); };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(fail);
else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { fail(); } }
});
}
// ===== Referral poster =====
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
var button = root.querySelector('[data-affiliate-poster]');
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
var content = modal ? modal.querySelector('.modal-content') : null;
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
if (!button || !modal) return;
if (!url || !poster || poster.enabled !== true) return;
button.hidden = false;
var backdrop = null;
var canvas = null;
function openPosterModal() {
if (modal.classList.contains('in')) return;
modal.style.display = 'flex';
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
modal.classList.add('in');
modal.setAttribute('aria-hidden', 'false');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade in';
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
document.body.appendChild(backdrop);
}
document.body.classList.add('modal-open');
}
function closePosterModal() {
if (!modal.classList.contains('in')) return;
modal.classList.remove('in');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
backdrop = null;
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
window.setTimeout(function () {
if (!modal.classList.contains('in')) modal.style.display = '';
}, 300);
}
modal.addEventListener('click', function (event) {
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
closePosterModal();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
});
if (download) {
download.addEventListener('click', function () {
if (!canvas) return;
try {
var link = document.createElement('a');
link.download = 'referral-poster.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
link.remove();
} catch (err) {
/* Canvas is same-origin generated; download can only fail if the browser blocks it. */
}
});
}
button.addEventListener('click', function () {
if (typeof window.qrcode !== 'function') {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
openPosterModal();
return;
}
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
if (loading) loading.style.display = '';
if (download) download.classList.add('hide');
openPosterModal();
button.disabled = true;
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
canvas = built;
if (!stage) return;
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.alt = '推广海报';
img.className = 'loaded-img';
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
img.width = 768 * 0.7;
img.height = 1168 * 0.7;
stage.appendChild(img);
stage.classList.add('loaded');
if (download) {
download.href = img.src;
download.classList.remove('hide');
}
if (loading) loading.style.display = 'none';
if (content) {
content.style.height = stage.offsetHeight + 'px';
content.style.transition = '.2s';
window.setTimeout(function () {
content.style.height = '';
content.style.overflow = '';
content.style.transition = '';
}, 200);
}
}).catch(function () {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
}).finally(function () {
button.disabled = false;
});
});
}
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
function loadPosterImage(sources) {
var list = (sources || []).filter(function (src) { return !!src; });
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = list[index];
}
next();
});
}
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
dHeight = (srcH * dWidth) / srcW;
dy = sy + (boxH - dHeight) / 2;
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
dWidth = (srcW * dHeight) / srcH;
dx = sx + (boxW - dWidth) / 2;
}
return { x: dx, y: dy, width: dWidth, height: dHeight };
}
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
// 返回实际测量宽度,供价格/标签层做横向排版。
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
var rows = [];
var temp = '';
var limit = maxWidth - 120;
x = x + 38;
text = String(text || '').replace(/[\r\n]/g, '');
ctx.fillStyle = color;
ctx.font = font;
var chars = text.split('');
var measured = 0;
if (ctx.measureText(text).width <= limit) {
rows.push(text);
} else {
for (var i = 0; i < chars.length; i++) {
if (rows.length === maxRow && i < chars.length - 1) {
rows[rows.length - 1] += '...';
break;
}
if (ctx.measureText(temp).width < limit) {
temp += chars[i];
if (i === chars.length - 1) rows.push(temp);
} else {
i--; // 参考主题:回退一格防止字符丢失
rows.push(temp);
temp = '';
}
}
}
for (var r = 0; r < rows.length; r++) {
measured = Math.max(measured, ctx.measureText(rows[r]).width);
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
}
return { width: measured, height: rows.length * lineHeight };
}
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
function buildPosterCanvas(url, poster, siteName, referrerName) {
var width = 768, height = 1168;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
var bottom = String(poster.bottom || '扫描二维码阅读全文');
var title = String(poster.title || siteName || '');
var description = String(poster.description || '');
return Promise.all([
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
]).then(function (assets) {
var banner = assets[0], logo = assets[1];
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// 顶部主图(cover 裁切)。
var rect = coverRect(width, imgHeight, banner.width, banner.height);
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
// 左上角日期(白字,覆盖在主图上)。
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.font = '35px Arial';
ctx.fillText(weekLabel, 30, 37);
ctx.font = '28px Arial';
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
// 标题竖蓝线。
ctx.strokeStyle = '#00a1ff';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(40, imgHeight + 38);
ctx.lineTo(40, imgHeight + 74);
ctx.stroke();
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
// drawTextOverflow 内部会 +38,所以这里传 2。
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
if (tags) {
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
}
// 简介层(灰):$content 置于 (0, img_height+220)。
if (description) {
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
}
// 底部带边框盒。
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(30, height - 230, width - 60, 200);
ctx.stroke();
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
if (logo && logo.width && logo.height) {
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
// 二维码(右下角)。
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
var count = qr.getModuleCount();
var qrBox = 120;
var cell = qrBox / count;
var qrX = width - 200, qrY = height - 200;
ctx.fillStyle = '#0f172a';
for (var r = 0; r < count; r++) {
for (var c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect(qrX + Math.floor(c * cell), qrY + Math.floor(r * cell), Math.ceil(cell), Math.ceil(cell));
}
}
}
// 底部文案(居中,盒下方)。
ctx.textAlign = 'center';
ctx.fillStyle = '#b4b4b4';
ctx.font = '22px Arial';
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
ctx.textAlign = 'left';
return canvas;
});
}
// cover 裁切:把源图按短边铺满目标框并居中裁切。
function coverRect(boxW, boxH, srcW, srcH) {
var sx = 0, sy = 0, sWidth = srcW, sHeight = srcH;
if (srcW / srcH > boxW / boxH) {
sWidth = (boxW * srcH) / boxH;
sx = (srcW - sWidth) / 2;
} else {
sHeight = (boxH * srcW) / boxW;
sy = (srcH - sHeight) / 2;
}
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
}
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
jQuery(document).off("auto_fun.aksrt-credits-affiliate").on("auto_fun.aksrt-credits-affiliate", initAffiliateRevenue);
})();
+163
View File
@@ -0,0 +1,163 @@
/**
* AKSRT withdraw history modal (split from credits.js).
*
* @package AKSRT
*/
(function () {
"use strict";
function initWithdrawHistory() {
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
var list = modal.querySelector('[data-aksrt-withdraw-history-list]');
var pagination = modal.querySelector('[data-aksrt-withdraw-history-pagination]');
var statusLabels = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
rejected: '已拒绝'
};
var currentPage = 1;
var loading = false;
function text(value) {
return value === null || value === undefined ? '' : String(value);
}
function appendText(parent, value, className) {
var node = document.createElement('span');
if (className) node.className = className;
node.textContent = text(value);
parent.appendChild(node);
return node;
}
function formatGrossAmount(value) {
return text(value).replace(/\.00$/, '');
}
function formatNetAmount(value) {
return text(value);
}
function renderItems(items) {
if (!list) return;
list.textContent = '';
if (!Array.isArray(items) || !items.length) {
var empty = document.createElement('div');
empty.className = 'uc-transactions__empty';
var emptyText = document.createElement('p');
emptyText.textContent = '暂无提现记录';
empty.appendChild(emptyText);
list.appendChild(empty);
return;
}
items.forEach(function (item) {
var status = text(item && item.status).toLowerCase();
var row = document.createElement('div');
row.className = 'uc-withdraw-history__item';
var badge = appendText(row, statusLabels[status] || status, 'badg badg-sm uc-withdraw-history__status');
badge.classList.add(status === 'rejected' ? 'c-red' : (status === 'completed' ? 'c-green' : 'c-blue'));
var gross = text(item && (item.gross_amount || item.amount));
var net = text(item && item.amount);
var amount = document.createElement('span');
amount.className = 'uc-withdraw-history__amount em12';
amount.appendChild(document.createTextNode(formatGrossAmount(gross)));
if (net !== '') {
var netText = document.createElement('span');
netText.className = 'uc-withdraw-history__net';
netText.textContent = '(实际到账:' + formatNetAmount(net) + '元)';
amount.appendChild(netText);
}
row.appendChild(amount);
var number = document.createElement('span');
number.className = 'uc-withdraw-history__number muted-2-color em09';
var numberLabel = document.createElement('span');
numberLabel.textContent = '订单号:';
number.appendChild(numberLabel);
var code = document.createElement('code');
code.textContent = text(item && item.withdrawal_number);
number.appendChild(code);
row.appendChild(number);
appendText(row, text(item && item.created_at), 'uc-withdraw-history__meta muted-2-color em09');
list.appendChild(row);
});
}
function renderPagination(page, maxPages) {
if (!pagination) return;
pagination.textContent = '';
if (maxPages <= 1) return;
function addButton(label, target, disabled, current) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'uc-withdraw-history__page' + (current ? ' is-current' : '');
button.textContent = label;
button.disabled = !!disabled;
if (current) button.setAttribute('aria-current', 'page');
button.dataset.aksrtWithdrawHistoryPage = String(target);
pagination.appendChild(button);
}
addButton('上一页', page - 1, page <= 1, false);
for (var index = 1; index <= maxPages; index += 1) addButton(String(index), index, false, index === page);
addButton('下一页', page + 1, page >= maxPages, false);
}
function loadPage(page) {
if (loading || !list) return;
loading = true;
list.textContent = '加载中…';
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || '';
if (!rest) {
list.textContent = '记录加载失败,请重试';
loading = false;
return;
}
var url = rest.replace(/\/$/, '') + '/earnings/withdrawals?page=' + encodeURIComponent(Math.max(1, page)) + '&per_page=10';
fetch(url, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' }
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok || !payload || !Array.isArray(payload.items)) throw new Error('withdraw-history-load-failed');
currentPage = Math.max(1, parseInt(payload.page, 10) || 1);
var maxPages = Math.max(1, parseInt(payload.max_pages, 10) || 1);
renderItems(payload.items);
renderPagination(currentPage, maxPages);
});
}).catch(function () {
list.textContent = '记录加载失败,请重试';
if (pagination) pagination.textContent = '';
}).finally(function () {
loading = false;
});
}
function open() {
modal.hidden = false;
document.body.classList.add('uc-modal-open');
loadPage(1);
}
function close() {
modal.hidden = true;
document.body.classList.remove('uc-modal-open');
}
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
});
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
});
if (pagination) {
pagination.addEventListener('click', function (e) {
var button = e.target.closest('[data-aksrt-withdraw-history-page]');
if (!button || button.disabled) return;
e.preventDefault();
loadPage(parseInt(button.dataset.aksrtWithdrawHistoryPage || '1', 10) || 1);
});
}
}
jQuery(document).off("auto_fun.aksrt-withdraw-history").on("auto_fun.aksrt-withdraw-history", initWithdrawHistory);
})();
+14 -480
View File
@@ -124,327 +124,6 @@
});
}
// ===== Forward-only affiliate revenue center =====
/* 推广链接由 PHP 直出,首屏即可见、可复制;这里只补异步才拿得到的
* 海报配置与佣金比例,取失败也不影响复制。 */
function initAffiliateRevenue() {
var root = document.querySelector('[data-aksrt-revenue-page]');
if (!root || root.dataset.affiliateBound) return;
root.dataset.affiliateBound = '1';
var input = root.querySelector('[data-affiliate-link]');
var copy = root.querySelector('[data-affiliate-copy]');
var url = input ? input.value : '';
if (copy) copy.disabled = !url;
api('/revenue/me').then(function (r) {
if (!r.ok) return;
var j = r.json || {};
var ratio = root.querySelector('[data-affiliate-ratio]'); if (ratio) ratio.textContent = String(j.current_ratio || 0) + '%';
initAffiliatePoster(root, url || j.url || '', j.poster || {}, j.site_name || document.title, j.referrer_name || '');
}).catch(function () { /* 海报与比例为增强项,静默失败即可。 */ });
if (copy) copy.addEventListener('click', function () {
if (!input || !input.value) return;
var done = function () { notyf('推广链接已复制', 'success', 3, 'aksrt-affiliate-copy-toast'); };
var fail = function () { input.focus(); input.select(); notyf('复制失败,请手动复制', 'danger', 3, 'aksrt-affiliate-copy-toast'); };
if (navigator.clipboard && navigator.clipboard.writeText) navigator.clipboard.writeText(input.value).then(done).catch(fail);
else { input.focus(); input.select(); try { document.execCommand('copy'); done(); } catch (e) { fail(); } }
});
}
// ===== Referral poster =====
/* 布局 1:1 复刻参考主题 poster-share.js 的 rebate 分支(非 product):
* 768×1168,顶部主图占上半屏、左上角日期、标题竖线 + 标题、红色推荐人标签层、
* 简介,底部带边框盒内左侧 contain 居中 logo 图 + 右下二维码,最底部居中文案。
* 顶部主图按参考主题的三级回退加载(banner → banner_default → banner_spare)。 */
function initAffiliatePoster(root, url, poster, siteName, referrerName) {
var button = root.querySelector('[data-affiliate-poster]');
var modal = document.querySelector('[data-aksrt-affiliate-poster-modal]');
var stage = modal ? modal.querySelector('[data-affiliate-poster-stage]') : null;
var loading = modal ? modal.querySelector('[data-affiliate-poster-loading]') : null;
var content = modal ? modal.querySelector('.modal-content') : null;
var download = modal ? modal.querySelector('[data-affiliate-poster-download]') : null;
if (!button || !modal) return;
if (!url || !poster || poster.enabled !== true) return;
button.hidden = false;
var backdrop = null;
var canvas = null;
function openPosterModal() {
if (modal.classList.contains('in')) return;
modal.style.display = 'flex';
void modal.offsetWidth; // 强制 reflow,让 .fade 过渡从 scale(0.9) 起手
modal.classList.add('in');
modal.setAttribute('aria-hidden', 'false');
if (!backdrop) {
backdrop = document.createElement('div');
backdrop.className = 'modal-backdrop fade in';
backdrop.setAttribute('data-affiliate-poster-backdrop', '1');
document.body.appendChild(backdrop);
}
document.body.classList.add('modal-open');
}
function closePosterModal() {
if (!modal.classList.contains('in')) return;
modal.classList.remove('in');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('modal-open');
if (backdrop && backdrop.parentNode) backdrop.parentNode.removeChild(backdrop);
backdrop = null;
// 等过渡结束再撤 display,复刻 Bootstrap 的 fade-out 收尾
window.setTimeout(function () {
if (!modal.classList.contains('in')) modal.style.display = '';
}, 300);
}
modal.addEventListener('click', function (event) {
// 点遮罩层(.modal 自身)或关闭按钮(data-dismiss="modal")
if (event.target === modal || (event.target.closest && event.target.closest('[data-dismiss="modal"]'))) {
closePosterModal();
}
});
document.addEventListener('keydown', function (event) {
if (event.key === 'Escape' && modal.classList.contains('in')) closePosterModal();
});
if (download) {
download.addEventListener('click', function () {
if (!canvas) return;
try {
var link = document.createElement('a');
link.download = 'referral-poster.png';
link.href = canvas.toDataURL('image/png');
document.body.appendChild(link);
link.click();
link.remove();
} catch (err) {
/* Canvas is same-origin generated; download can only fail if the browser blocks it. */
}
});
}
button.addEventListener('click', function () {
if (typeof window.qrcode !== 'function') {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '二维码组件未加载。' + '</p>'; }
openPosterModal();
return;
}
if (stage) { stage.innerHTML = ''; stage.classList.remove('loaded'); }
if (loading) loading.style.display = '';
if (download) download.classList.add('hide');
openPosterModal();
button.disabled = true;
buildPosterCanvas(url, poster, siteName, referrerName).then(function (built) {
canvas = built;
if (!stage) return;
// 参考主题:img.loaded-img 直接进 .poster-imgbox,遮罩淡出,
// 内容块高度从 220px 过渡到海报实际高度后清掉内联样式。
var img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
img.alt = '推广海报';
img.className = 'loaded-img';
// 参考主题按 0.7 比例预设尺寸;.poster-imgbox > img 的 max-width:360px
// 会在窄屏把它压回弹窗宽度,height:100% 让 offsetHeight 反映真实渲染高度。
img.width = 768 * 0.7;
img.height = 1168 * 0.7;
stage.appendChild(img);
stage.classList.add('loaded');
if (download) {
download.href = img.src;
download.classList.remove('hide');
}
if (loading) loading.style.display = 'none';
if (content) {
content.style.height = stage.offsetHeight + 'px';
content.style.transition = '.2s';
window.setTimeout(function () {
content.style.height = '';
content.style.overflow = '';
content.style.transition = '';
}, 200);
}
}).catch(function () {
if (stage) { stage.innerHTML = '<p class="muted-2-color em09">' + '海报生成失败,请重试。' + '</p>'; }
}).finally(function () {
button.disabled = false;
});
});
}
// 三级回退加载图片:任一源成功即返回,全部失败才 reject(参考主题 loadCount 0→1→2)。
function loadPosterImage(sources) {
var list = (sources || []).filter(function (src) { return !!src; });
return new Promise(function (resolve, reject) {
var index = 0;
function next() {
if (index >= list.length) { reject(new Error('poster image unavailable')); return; }
var image = new Image();
image.crossOrigin = 'anonymous';
image.onload = function () { resolve(image); };
image.onerror = function () { index += 1; next(); };
image.src = list[index];
}
next();
});
}
// contain 裁切:参考主题 containImg(),等比缩放塞进盒子并居中,不裁切。
function containRect(sx, sy, boxW, boxH, srcW, srcH) {
var dx = sx, dy = sy, dWidth = boxW, dHeight = boxH;
if (srcW > srcH || (srcW === srcH && boxW < boxH)) {
dHeight = (srcH * dWidth) / srcW;
dy = sy + (boxH - dHeight) / 2;
} else if (srcW < srcH || (srcW === srcH && boxW > boxH)) {
dWidth = (srcW * dHeight) / srcH;
dx = sx + (boxW - dWidth) / 2;
}
return { x: dx, y: dy, width: dWidth, height: dHeight };
}
// 参考主题 drawTextOverflow():按 maxWidth-120 换行、x 偏移 38,最多 maxRow 行,溢出补 ...。
// 返回实际测量宽度,供价格/标签层做横向排版。
function drawTextOverflow(ctx, text, maxWidth, maxRow, font, color, lineHeight, x, y) {
var rows = [];
var temp = '';
var limit = maxWidth - 120;
x = x + 38;
text = String(text || '').replace(/[\r\n]/g, '');
ctx.fillStyle = color;
ctx.font = font;
var chars = text.split('');
var measured = 0;
if (ctx.measureText(text).width <= limit) {
rows.push(text);
} else {
for (var i = 0; i < chars.length; i++) {
if (rows.length === maxRow && i < chars.length - 1) {
rows[rows.length - 1] += '...';
break;
}
if (ctx.measureText(temp).width < limit) {
temp += chars[i];
if (i === chars.length - 1) rows.push(temp);
} else {
i--; // 参考主题:回退一格防止字符丢失
rows.push(temp);
temp = '';
}
}
}
for (var r = 0; r < rows.length; r++) {
measured = Math.max(measured, ctx.measureText(rows[r]).width);
ctx.fillText(rows[r], x, y + r * lineHeight, limit);
}
return { width: measured, height: rows.length * lineHeight };
}
// 参考主题 buildPosterCanvas rebate 分支:768×1168,顶部主图占上半屏,左上角日期,
// 标题竖蓝线 + 标题,推荐人标签(红),简介,底部盒(logo + 二维码 + 底部文案)。
function buildPosterCanvas(url, poster, siteName, referrerName) {
var width = 768, height = 1168;
var canvas = document.createElement('canvas');
canvas.width = width;
canvas.height = height;
var ctx = canvas.getContext('2d');
var imgHeight = height / 2; // rebate 分支:顶部主图占上半屏
// 站点名走「周」前缀,与参考主题 zib__('week_prefix') 一致。
var weekLabel = '周' + '日一二三四五六'.charAt(new Date().getDay());
// 推荐人标签:参考主题 tags = `推荐人: {昵称}`,缺昵称时整层不画。
var tags = referrerName ? '推荐人: ' + String(referrerName) : '';
// 底部文案:优先推广海报配置,回退到分享设置 share_desc,再回退默认文案。
var bottom = String(poster.bottom || '扫描二维码阅读全文');
var title = String(poster.title || siteName || '');
var description = String(poster.description || '');
return Promise.all([
loadPosterImage([poster.image, poster.banner_default, poster.banner_spare]),
poster.logo ? loadPosterImage([poster.logo]).catch(function () { return null; }) : Promise.resolve(null)
]).then(function (assets) {
var banner = assets[0], logo = assets[1];
ctx.fillStyle = '#ffffff';
ctx.fillRect(0, 0, width, height);
// 顶部主图(cover 裁切)。
var rect = coverRect(width, imgHeight, banner.width, banner.height);
ctx.drawImage(banner, rect.sx, rect.sy, rect.sWidth, rect.sHeight, 0, 0, width, imgHeight);
// 左上角日期(白字,覆盖在主图上)。
ctx.textBaseline = 'top';
ctx.textAlign = 'left';
ctx.fillStyle = 'rgba(255,255,255,1)';
ctx.font = '35px Arial';
ctx.fillText(weekLabel, 30, 37);
ctx.font = '28px Arial';
ctx.fillText((new Date().getMonth() + 1) + ' / ' + new Date().getDate(), 30, 87);
// 标题竖蓝线。
ctx.strokeStyle = '#00a1ff';
ctx.lineWidth = 7;
ctx.beginPath();
ctx.lineCap = 'round';
ctx.moveTo(40, imgHeight + 38);
ctx.lineTo(40, imgHeight + 74);
ctx.stroke();
// 标题层(粗体)。参考 rebate 分支把 $title 子画布置于 (20, img_height+40),
// 子画布内左对齐 x=40、文字基线偏移 2,故绝对坐标 (40, img_height+42);
// drawTextOverflow 内部会 +38,所以这里传 2。
drawTextOverflow(ctx, title, width, 2, 'bold 35px Arial', '#424242', 52, 2, imgHeight + 42);
// 推荐人标签层(红,最多 1 行):$tags 置于 (0, img_height+160)。
if (tags) {
drawTextOverflow(ctx, tags, width, 1, '24px Arial', '#fc4e1f', 36, 2, imgHeight + 162);
}
// 简介层(灰):$content 置于 (0, img_height+220)。
if (description) {
drawTextOverflow(ctx, description, width, 4, '25px Arial', 'rgba(88,88,88,1)', 39, 2, imgHeight + 222);
}
// 底部带边框盒。
ctx.strokeStyle = 'rgba(122,122,122,0.2)';
ctx.lineWidth = 2;
ctx.beginPath();
ctx.rect(30, height - 230, width - 60, 200);
ctx.stroke();
// logo 图:contain 居中,缺图时整块不画(参考主题 logo.naturalWidth 守卫)。
if (logo && logo.width && logo.height) {
var logoRect = containRect(80, height - 190, 300, 100, logo.width, logo.height);
ctx.drawImage(logo, logoRect.x, logoRect.y, logoRect.width, logoRect.height);
}
// 二维码(右下角)。
var qr = window.qrcode(0, 'M');
qr.addData(url);
qr.make();
var count = qr.getModuleCount();
var qrBox = 120;
var cell = qrBox / count;
var qrX = width - 200, qrY = height - 200;
ctx.fillStyle = '#0f172a';
for (var r = 0; r < count; r++) {
for (var c = 0; c < count; c++) {
if (qr.isDark(r, c)) {
ctx.fillRect(qrX + Math.floor(c * cell), qrY + Math.floor(r * cell), Math.ceil(cell), Math.ceil(cell));
}
}
}
// 底部文案(居中,盒下方)。
ctx.textAlign = 'center';
ctx.fillStyle = '#b4b4b4';
ctx.font = '22px Arial';
ctx.fillText(String(bottom).slice(0, 46), width / 2, height - 58);
ctx.textAlign = 'left';
return canvas;
});
}
// cover 裁切:把源图按短边铺满目标框并居中裁切。
function coverRect(boxW, boxH, srcW, srcH) {
var sx = 0, sy = 0, sWidth = srcW, sHeight = srcH;
if (srcW / srcH > boxW / boxH) {
sWidth = (boxW * srcH) / boxH;
sx = (srcW - sWidth) / 2;
} else {
sHeight = (boxH * srcW) / boxW;
sy = (srcH - sHeight) / 2;
}
return { sx: sx, sy: sy, sWidth: sWidth, sHeight: sHeight };
}
window.AKSRTAffiliateRevenueInit = initAffiliateRevenue;
// ===== VIP 开通 =====
function initVipShop() {
@@ -488,7 +167,7 @@
busy = false;
btn.disabled = false;
btn.classList.remove('is-loading');
if (data.payment_url) window.open(data.payment_url, '_blank');
if (window.AksrtPayCashier) window.AksrtPayCashier.handle(data, {});
setTip(data.message || '请完成支付,支付成功后将自动刷新。');
},
onSuccess: function (result) {
@@ -573,6 +252,7 @@
tags: tags || ['credits', 'orders'],
onTerminal: function (result) {
activePoll = null;
if (window.AksrtPayCashier) window.AksrtPayCashier.close();
if (result.success) {
notyf('支付成功,正在刷新…', 'success', 5, 'aksrt-balance-charge-payment-toast');
refreshAssetBalances();
@@ -767,9 +447,17 @@
}
var data = (r.json && r.json.data) ? r.json.data : r.json;
resetOperationKey(form);
if (data && data.redirect && (data.payment_url || data.qr_content)) {
if (data.payment_url) window.open(data.payment_url, '_blank');
if (data && data.mode) {
if (window.AksrtModalRuntime) window.AksrtModalRuntime.invalidate(['orders']);
if (window.AksrtPayCashier) {
window.AksrtPayCashier.handle(data, {
onClose: function () {
if (activePoll && typeof activePoll.abort === 'function') activePoll.abort();
activePoll = null;
notyf('已停止查询支付状态', 'warning', 3, 'aksrt-balance-charge-payment-toast');
}
});
}
notyf(data.message || '请完成支付,支付成功后将自动刷新。', 'info', 5, 'aksrt-balance-charge-toast');
if (data.order_number) activePoll = pollPaidOrder(data.order_number, ['credits', 'orders']);
return;
@@ -1244,158 +932,6 @@
}
function initWithdrawHistory() {
var modal = document.querySelector('[data-aksrt-withdraw-history-modal]');
if (!modal || modal.dataset.aksrtWithdrawHistoryInitialized === '1') return;
modal.dataset.aksrtWithdrawHistoryInitialized = '1';
var list = modal.querySelector('[data-aksrt-withdraw-history-list]');
var pagination = modal.querySelector('[data-aksrt-withdraw-history-pagination]');
var statusLabels = {
pending: '待处理',
processing: '处理中',
completed: '已完成',
rejected: '已拒绝'
};
var currentPage = 1;
var loading = false;
function text(value) {
return value === null || value === undefined ? '' : String(value);
}
function appendText(parent, value, className) {
var node = document.createElement('span');
if (className) node.className = className;
node.textContent = text(value);
parent.appendChild(node);
return node;
}
function formatGrossAmount(value) {
return text(value).replace(/\.00$/, '');
}
function formatNetAmount(value) {
return text(value);
}
function renderItems(items) {
if (!list) return;
list.textContent = '';
if (!Array.isArray(items) || !items.length) {
var empty = document.createElement('div');
empty.className = 'uc-transactions__empty';
var emptyText = document.createElement('p');
emptyText.textContent = '暂无提现记录';
empty.appendChild(emptyText);
list.appendChild(empty);
return;
}
items.forEach(function (item) {
var status = text(item && item.status).toLowerCase();
var row = document.createElement('div');
row.className = 'uc-withdraw-history__item';
var badge = appendText(row, statusLabels[status] || status, 'badg badg-sm uc-withdraw-history__status');
badge.classList.add(status === 'rejected' ? 'c-red' : (status === 'completed' ? 'c-green' : 'c-blue'));
var gross = text(item && (item.gross_amount || item.amount));
var net = text(item && item.amount);
var amount = document.createElement('span');
amount.className = 'uc-withdraw-history__amount em12';
amount.appendChild(document.createTextNode(formatGrossAmount(gross)));
if (net !== '') {
var netText = document.createElement('span');
netText.className = 'uc-withdraw-history__net';
netText.textContent = '(实际到账:' + formatNetAmount(net) + '元)';
amount.appendChild(netText);
}
row.appendChild(amount);
var number = document.createElement('span');
number.className = 'uc-withdraw-history__number muted-2-color em09';
var numberLabel = document.createElement('span');
numberLabel.textContent = '订单号:';
number.appendChild(numberLabel);
var code = document.createElement('code');
code.textContent = text(item && item.withdrawal_number);
number.appendChild(code);
row.appendChild(number);
appendText(row, text(item && item.created_at), 'uc-withdraw-history__meta muted-2-color em09');
list.appendChild(row);
});
}
function renderPagination(page, maxPages) {
if (!pagination) return;
pagination.textContent = '';
if (maxPages <= 1) return;
function addButton(label, target, disabled, current) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'uc-withdraw-history__page' + (current ? ' is-current' : '');
button.textContent = label;
button.disabled = !!disabled;
if (current) button.setAttribute('aria-current', 'page');
button.dataset.aksrtWithdrawHistoryPage = String(target);
pagination.appendChild(button);
}
addButton('上一页', page - 1, page <= 1, false);
for (var index = 1; index <= maxPages; index += 1) addButton(String(index), index, false, index === page);
addButton('下一页', page + 1, page >= maxPages, false);
}
function loadPage(page) {
if (loading || !list) return;
loading = true;
list.textContent = '加载中…';
var rest = (window.aksrtData && (window.aksrtData.restUrl || window.aksrtData.apiUrl)) || '';
if (!rest) {
list.textContent = '记录加载失败,请重试';
loading = false;
return;
}
var url = rest.replace(/\/$/, '') + '/earnings/withdrawals?page=' + encodeURIComponent(Math.max(1, page)) + '&per_page=10';
fetch(url, {
credentials: 'same-origin',
headers: { 'X-WP-Nonce': (window.aksrtData && (window.aksrtData.restNonce || window.aksrtData.nonce)) || '' }
}).then(function (response) {
return response.json().then(function (payload) {
if (!response.ok || !payload || !Array.isArray(payload.items)) throw new Error('withdraw-history-load-failed');
currentPage = Math.max(1, parseInt(payload.page, 10) || 1);
var maxPages = Math.max(1, parseInt(payload.max_pages, 10) || 1);
renderItems(payload.items);
renderPagination(currentPage, maxPages);
});
}).catch(function () {
list.textContent = '记录加载失败,请重试';
if (pagination) pagination.textContent = '';
}).finally(function () {
loading = false;
});
}
function open() {
modal.hidden = false;
document.body.classList.add('uc-modal-open');
loadPage(1);
}
function close() {
modal.hidden = true;
document.body.classList.remove('uc-modal-open');
}
document.querySelectorAll('[data-aksrt-withdraw-history-open]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); open(); });
});
modal.querySelectorAll('[data-aksrt-withdraw-history-close]').forEach(function (btn) {
btn.addEventListener('click', function (e) { e.preventDefault(); close(); });
});
if (pagination) {
pagination.addEventListener('click', function (e) {
var button = e.target.closest('[data-aksrt-withdraw-history-page]');
if (!button || button.disabled) return;
e.preventDefault();
loadPage(parseInt(button.dataset.aksrtWithdrawHistoryPage || '1', 10) || 1);
});
}
}
window.AKSRTWithdrawInit = initWithdraw;
window.AKSRTAssetBalances = {
@@ -1405,14 +941,12 @@
function bootAssets() {
refreshAssetBalances();
initSigninGoto();
initAffiliateRevenue();
initSigninGoto();
initVipShop();
initBalanceCharge();
initTransfer();
initPointsPay();
initWithdraw();
initWithdrawHistory();
initWithdraw();
initAssetTabs();
}
+26 -2
View File
@@ -386,6 +386,23 @@
return loadView(state);
}
function pollOrder(state, number) {
var coordinator = window.AksrtVipPurchaseCoordinator;
if (!coordinator || typeof coordinator.poll !== "function") return null;
return coordinator.poll(number, {
tags: ["orders"],
onTerminal: function (result) {
if (window.AksrtPayCashier) window.AksrtPayCashier.close();
notyf(result.success ? "支付成功" : "支付未完成,请重试", result.success ? "success" : "danger");
invalidateOrder(number, null);
if (activeDetail(state, number)) openDetail(state, number);
},
onTimeout: function () {
notyf("支付状态暂未确认,请稍后刷新订单", "danger");
}
});
}
function startPayment(state, button) {
var number = button.getAttribute("data-aksrt-order-pay") || "";
var method = button.getAttribute("data-aksrt-order-method") || "";
@@ -397,8 +414,15 @@
delete state.operationKeys[actionKey];
var data = result.data || {};
invalidateOrder(number, data.order);
if (data.redirect && data.payment_url) {
window.location.assign(data.payment_url);
if (data.mode) {
button.disabled = false;
if (!window.AksrtPayCashier) return;
var handled = window.AksrtPayCashier.handle(data, {
onClose: function () { if (poll) poll.abort(); }
});
var poll = handled === "qrcode" || handled === "jsapi"
? pollOrder(state, number)
: null;
return;
}
notyf(data.message || "支付成功", "success");
+162
View File
@@ -0,0 +1,162 @@
/**
* Unified cashier: renders the gateway result returned by PaymentOrchestrator::pay().
*
* mode=qrcode on-site QR modal (+ optional mobile deep link)
* mode=redirect same-tab navigation to the gateway page
* mode=form auto-submit the server-built POST form
* mode=jsapi WeChat in-app WeixinJSBridge payment
*
* Polling stays with the caller (AksrtVipPurchaseCoordinator.poll); this module
* only presents the payment step and reports when the QR modal is dismissed.
*
* @package AKSRT
*/
(function (window, document) {
"use strict";
var KIND = "pay-cashier";
var sequence = 0;
var listeners = Object.create(null);
function escapeHtml(value) {
return String(value == null ? "" : value).replace(/[&<>"']/g, function (ch) {
return { "&": "&amp;", "<": "&lt;", ">": "&gt;", '"': "&quot;", "'": "&#39;" }[ch];
});
}
function isMobile() {
return /Android|iPhone|iPad|iPod|Mobile/i.test(window.navigator.userAgent || "");
}
function qrDataUrl(text) {
if (typeof window.qrcode !== "function" || !text) return "";
try {
var qr = window.qrcode(0, "M");
qr.addData(String(text));
qr.make();
return qr.createDataURL(6, 12);
} catch (error) {
return "";
}
}
function amountText(data) {
if (data.amount === undefined || data.amount === null || data.amount === "") return "";
var mark = (window._win && window._win.pay_mark) || "¥";
return mark + String(data.amount);
}
/** Inner QR markup; reusable by callers that own their own modal. */
function qrHtml(data) {
var src = qrDataUrl(data.code_url);
var html = '<div class="aksrt-pay-cashier">';
var amount = amountText(data);
if (amount) html += '<div class="aksrt-pay-cashier__amount">' + escapeHtml(amount) + "</div>";
html += src
? '<img class="aksrt-pay-cashier__qr" src="' + src + '" width="200" height="200" alt="支付二维码">'
: '<p class="aksrt-pay-cashier__error">二维码组件未加载,请刷新页面重试。</p>';
html += '<p class="aksrt-pay-cashier__tip">' + escapeHtml(data.message || "请使用手机扫码完成支付") + "</p>";
if (data.payment_url && isMobile()) {
html += '<a class="but jb-blue aksrt-pay-cashier__open" href="' + escapeHtml(data.payment_url) + '">打开支付应用</a>';
}
if (data.order_number) {
html += '<p class="aksrt-pay-cashier__order muted-2-color em09">订单号:' + escapeHtml(data.order_number) + "</p>";
}
return html + "</div>";
}
function openQr(data, options) {
var runtime = window.AksrtModalRuntime;
if (!runtime) return false;
var id = String(++sequence);
if (typeof options.onClose === "function") listeners[id] = options.onClose;
runtime.open({
kind: KIND,
cashierId: id,
title: options.title || "扫码支付",
className: "aksrt-pay-cashier-modal",
telemetry: false,
html: qrHtml(data),
});
return true;
}
function submitForm(html) {
var host = document.createElement("div");
host.hidden = true;
host.innerHTML = String(html || "");
var form = host.querySelector("form");
if (!form) return false;
document.body.appendChild(host);
form.submit();
return true;
}
function invokeJsapi(params, options) {
var run = function () {
window.WeixinJSBridge.invoke("getBrandWCPayRequest", params, function (res) {
var msg = res && res.err_msg ? String(res.err_msg) : "";
if (msg === "get_brand_wcpay_request:ok") return;
if (typeof options.onClose === "function") {
options.onClose(msg === "get_brand_wcpay_request:cancel" ? "cancel" : "error");
}
});
};
if (window.WeixinJSBridge && typeof window.WeixinJSBridge.invoke === "function") {
run();
} else {
document.addEventListener("WeixinJSBridgeReady", run, { once: true });
}
return true;
}
/**
* Present a pending payment. Returns the handled mode, or "" when the
* payload carries no cashier step (synchronous gateways).
*
* options.onClose(reason) fires when the QR modal is dismissed or the
* JSAPI sheet is cancelled; callers stop polling there.
*/
function handle(data, options) {
data = data || {};
options = options || {};
switch (data.mode) {
case "qrcode":
return data.code_url && openQr(data, options) ? "qrcode" : "";
case "redirect":
if (!data.payment_url) return "";
window.location.assign(data.payment_url);
return "redirect";
case "form":
return submitForm(data.form_html) ? "form" : "";
case "jsapi":
return data.jsapi_params ? invokeJsapi(data.jsapi_params, options) && "jsapi" : "";
default:
return "";
}
}
/** Close the cashier QR modal after a terminal poll result. */
function close() {
var runtime = window.AksrtModalRuntime;
var shell = runtime ? runtime.getActive() : null;
if (shell && shell.classList.contains("aksrt-pay-cashier-modal")) runtime.close("paid");
}
document.addEventListener("aksrt:modal:close", function (event) {
var detail = event && event.detail ? event.detail : {};
var opts = detail.session && detail.session.options ? detail.session.options : {};
if (opts.kind !== KIND || !listeners[opts.cashierId]) return;
var fn = listeners[opts.cashierId];
delete listeners[opts.cashierId];
if (detail.reason !== "paid") fn(detail.reason || "close");
});
window.AksrtPayCashier = {
KIND: KIND,
handle: handle,
close: close,
qrHtml: qrHtml,
isPending: function (data) { return !!(data && data.mode); },
};
})(window, document);
+13 -5
View File
@@ -577,7 +577,9 @@
tags: ["vip", "orders"],
onPending: function (data) {
waitingPayment = true;
if (data.payment_url) window.open(data.payment_url, "_blank");
if (window.AksrtPayCashier) {
window.AksrtPayCashier.handle(data, { onClose: cancelActivePurchase });
}
notyf(data.message || "请完成支付,支付成功后将自动刷新。", "info", 5, activePurchaseToastId);
activePurchaseToastId = "aksrt-vip-payment-toast";
notyf("等待支付结果…", "load", "", activePurchaseToastId);
@@ -831,16 +833,22 @@
vipPay(trigger);
}
document.addEventListener("aksrt:modal:close", function (event) {
var detail = event && event.detail ? event.detail : {};
var options = detail.session && detail.session.options ? detail.session.options : {};
if (options.kind !== "vip" || !activePurchase) return;
function cancelActivePurchase() {
if (!activePurchase) return;
if (typeof activePurchase.abort === "function") activePurchase.abort();
activePurchase = null;
if (activePurchaseToastId) {
notyf("已取消会员购买", "warning", 3, activePurchaseToastId);
activePurchaseToastId = "";
}
}
// VIP 弹窗被收银二维码弹窗替换(reason=replace)时保持轮询;用户主动关闭才取消。
document.addEventListener("aksrt:modal:close", function (event) {
var detail = event && event.detail ? event.detail : {};
var options = detail.session && detail.session.options ? detail.session.options : {};
if (options.kind !== "vip" || detail.reason === "replace") return;
cancelActivePurchase();
});
// 积分兑换入口(data-aksrt-vip-points-open)文档级委托:一处绑定同时覆盖
+2 -2
View File
@@ -10,7 +10,7 @@
(function (window, document) {
"use strict";
var ORDER_PATH = "/pay/content/order/";
var ORDER_PATH = "/pay/order/";
var DEFAULT_MAX_ATTEMPTS = 12;
var DEFAULT_MAX_ELAPSED = 120000;
var MAX_DELAY = 15000;
@@ -374,7 +374,7 @@
callback(options, "onError", response);
return response;
}
if (data.redirect && (data.payment_url || data.qr_content)) {
if (data.mode) {
invalidate(["orders"]);
callback(options, "onPending", data);
if (!data.order_number) {