Files
kate522andClaude Opus 5 35bb96e82f refactor: rename the theme brand and every identifier from AKSRT to Jio
Full-project rename. `git grep -i aksrt` is now empty apart from the
DBMigrator 3.3.0 changelog note that documents the rename itself.

Code identifiers
- PHP namespace AKSRT\ -> Jio\ (composer PSR-4 + 378 declarations)
- global constants AKSRT_* -> JIO_* (VERSION/DIR/URI, PAY_TYPE_*, CAPTCHA_*)
- procedural functions, hooks, options, meta keys, table names,
  cookies, query vars, ajax actions and nonces: aksrt_ -> jio_
- text domain aksrt -> jio; Theme Name -> Jio
- JS globals: aksrtData/aksrtAdminData/... -> jioData/jioAdminData/...,
  window.AKSRTCaptcha & friends -> window.JioCaptcha, Aksrt* -> Jio*
- Vue components Aks*.vue -> Jio*.vue (15 files renamed + imports)
- CSS classes/data-attrs/custom properties: aksrt- -> jio-
- REST namespace aksrt/v1 -> jio/v1 (incl. payment gateway callbacks)

Storage layer
- DB tables {prefix}aksrt_* -> {prefix}jio_*; SCHEMA_VERSION 3.2.0 -> 3.3.0
  so dbDelta creates the new names on install
- widget id_base aksrt_widget_* -> jio_widget_*
- uninstall.php drop list rebuilt from the actual migrator tables
  (dropped 3 stale entries, added the 17 missing ones)

Removed
- hardcoded https://api.aksrt.com/version.json default update endpoint;
  get_endpoint() now reads the 「更新端点」 setting and do_check() no-ops
  when it is empty

Files renamed: inc/helpers/aksrt-*.php -> jio-*.php (54),
assets/admin/src/components/form/Aks*.vue -> Jio*.vue (15).
Build artifacts regenerated: assets/css/build, assets/js/runtime,
assets/admin/build.

Verified: audit-psr4-paths errors=0, audit-admin-contracts errors=0
(warnings=56, unchanged baseline), 813/813 widget-system tests,
44/44 phpunit, php -l clean across all PHP.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-11-01 20:05:04 +08:00

751 lines
29 KiB
JavaScript
Raw Permalink 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.
/**
* Jio CMS Download
*
* 功能:
* - 监听购买入口(data-jio-pay-content / data-action="jio-need-purchase")
* - 内容购买收银台、扫码支付与错误提示弹窗
* - 下载已改为直连:按钮为源地址锚点,不再经过本站中转请求
*
* 弹窗视觉走主题 design token(见 assets/css/frontend/cms-download.css),
* 付费收银台沿用参考实现的 colorful header 结构。
*
* 依赖:
* - window.jioData.{ ajaxUrl, nonce, siteUrl }
* - 主题已加载 fetch (现代浏览器)
*/
(function () {
'use strict';
if (!window.jioData) return;
const REST_BASE = window.jioData.siteUrl ? window.jioData.siteUrl.replace(/\/$/, '') + '/wp-json/jio/v1' : '/wp-json/jio/v1';
const PAY_CONTENT_PATH = '/pay/content/';
// 自定义货币名称(后台「积分配置 → 自定义货币名称」)由服务端注入。
// 余额不足提示在客户端拼装,必须跟随它。
function currencyName() {
return (window.jioData && window.jioData.currencyName) || '积分';
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#039;');
}
function escapeAttr(value) {
return escapeHtml(value);
}
// ===== Focus trap state =====
let lastFocusedElement = null;
function getFocusableElements(container) {
if (!container) return [];
return Array.prototype.slice.call(container.querySelectorAll(
'a[href], area[href], button:not([disabled]), input:not([disabled]):not([type="hidden"]),' +
' select:not([disabled]), textarea:not([disabled]), [tabindex]:not([tabindex="-1"])'
)).filter(function (el) {
return !!(el.offsetWidth || el.offsetHeight || el.getClientRects().length);
});
}
function trapFocus(e, modal) {
if (e.key !== 'Tab') return;
const focusables = getFocusableElements(modal);
if (!focusables.length) {
e.preventDefault();
return;
}
const first = focusables[0];
const last = focusables[focusables.length - 1];
if (e.shiftKey && document.activeElement === first) {
e.preventDefault();
last.focus();
} else if (!e.shiftKey && document.activeElement === last) {
e.preventDefault();
first.focus();
}
}
// ===== Modal helper =====
function buildModal() {
let modal = document.getElementById('jio-dl-modal');
if (modal) return modal;
modal = document.createElement('div');
modal.id = 'jio-dl-modal';
modal.className = 'jio-dl-modal';
modal.setAttribute('role', 'dialog');
modal.setAttribute('aria-modal', 'true');
modal.setAttribute('aria-hidden', 'true');
modal.setAttribute('aria-labelledby', 'jio-dl-modal-title');
modal.innerHTML =
'<div class="jio-dl-modal__backdrop" data-close></div>' +
'<div class="jio-dl-modal__dialog modal-mini">' +
' <div class="jio-dl-modal__panel modal-content" role="document" tabindex="-1">' +
' <button type="button" class="jio-dl-modal__close" aria-label="关闭" data-close>×</button>' +
' <div class="jio-dl-modal__body" data-body></div>' +
' </div>' +
'</div>';
document.body.appendChild(modal);
modal.addEventListener('click', function (e) {
if (e.target.closest('[data-close]')) close();
});
modal.addEventListener('keydown', function (e) {
if (e.key === 'Escape' && modal.classList.contains('is-open')) {
e.preventDefault();
close();
} else if (e.key === 'Tab' && modal.classList.contains('is-open')) {
trapFocus(e, modal);
}
});
return modal;
}
function open(html, returnFocusEl, variant) {
// 记录调用方按钮以便关闭后回焦
lastFocusedElement = returnFocusEl || document.activeElement;
const modal = buildModal();
const body = modal.querySelector('[data-body]');
modal.classList.toggle('is-cashier', variant === 'cashier');
body.innerHTML = html;
const title = body.querySelector('.jio-dl-modal__title, .jio-cashier__title');
if (title) title.id = 'jio-dl-modal-title';
modal.classList.add('is-open');
modal.setAttribute('aria-hidden', 'false');
document.body.classList.add('jio-dl-modal-open');
// 进入后将焦点放在第一个可聚焦元素,否则放 panel
setTimeout(function () {
const focusables = getFocusableElements(modal);
if (focusables.length) {
focusables[0].focus();
} else {
modal.querySelector('.jio-dl-modal__panel').focus();
}
}, 0);
}
function close() {
const modal = document.getElementById('jio-dl-modal');
if (!modal) return;
modal.classList.remove('is-open');
modal.setAttribute('aria-hidden', 'true');
document.body.classList.remove('jio-dl-modal-open');
// 焦点回到触发按钮
if (lastFocusedElement && typeof lastFocusedElement.focus === 'function') {
try { lastFocusedElement.focus(); } catch (_) { /* ignore */ }
}
lastFocusedElement = null;
}
// ===== 统一视图壳(下载 / 错误 / 扫码共用主题化外观) =====
// 头部与按钮统一继承运行时主题色(--focus-color),状态用图标 + 标题区分。
const DL_VIEWS = {
login: { icon: 'fa-user-lock', title: '请先登录' },
purchase: { icon: 'fa-shopping-cart', title: '请先购买' },
vip: { icon: 'fa-gem', title: '会员专属' },
credits: { icon: 'fa-coins', title: '积分不足' },
quota: { icon: 'fa-hourglass-half', title: '今日免费额度已用完' },
captcha: { icon: 'fa-shield-alt', title: '验证失败' },
error: { icon: 'fa-exclamation-circle', title: '下载失败' },
pay: { icon: 'fa-qrcode', title: '扫码支付' },
};
function dlActionButton(action) {
if (!action) return '';
const tag = action.kind === 'link' ? 'a' : 'button';
const cls = 'jio-dl-modal__btn' + (action.cls ? ' ' + action.cls : '');
const attrs = [
'class="' + cls + '"',
action.url ? 'href="' + escapeAttr(action.url) + '"' : '',
action.attrs || '',
].filter(Boolean).join(' ');
return '<' + tag + ' ' + attrs + '>' +
'<i class="fa fas ' + (action.icon || 'fa-arrow-circle-right') + '" aria-hidden="true"></i>' +
'<span>' + escapeHtml(action.label) + '</span>' +
'</' + tag + '>';
}
function dlView(viewKey, options) {
const meta = DL_VIEWS[viewKey] || DL_VIEWS.error;
options = options || {};
let html = '<div class="jio-dl-view">';
html += '<div class="jio-dl-modal__hero">';
html += '<div class="jio-dl-modal__hero-icon"><i class="fa fas ' + meta.icon + '" aria-hidden="true"></i></div>';
html += '<h3 class="jio-dl-modal__title">' + (options.title || meta.title) + '</h3>';
if (options.subtitle) {
html += '<p class="jio-dl-modal__subtitle">' + escapeHtml(options.subtitle) + '</p>';
}
html += '</div>';
html += '<div class="jio-dl-modal__content">';
html += options.body || '';
if (options.actions && options.actions.length) {
html += '<div class="jio-dl-modal__actions">' +
options.actions.map(dlActionButton).join('') +
'</div>';
}
html += '</div></div>';
return html;
}
// ===== 错误提示渲染 =====
function renderError(msg, data, returnFocusEl, code) {
let viewKey = 'error';
let subtitle = '';
let action = null;
// credits 视图标题跟随自定义货币名称;其余视图沿用静态文案。
let unit = '';
data = data || {};
msg = msg || '';
if (code === 'jio_need_login') {
viewKey = 'login';
msg = data.login_url ? '登录后即可继续下载,访客不可使用此资源。' : (msg || '无下载权限。');
if (data.login_url) action = { kind: 'link', url: data.login_url, label: '前往登录', icon: 'fa-sign-in-alt' };
} else if (code === 'jio_need_purchase') {
viewKey = 'purchase';
msg = msg || '请先完成购买后再下载该资源。';
if (data.post_id) {
action = { kind: 'button', label: '去购买', icon: 'fa-shopping-cart', attrs: 'type="button" data-jio-open-cashier="' + escapeAttr(data.post_id) + '"' };
}
} else if (code === 'jio_need_vip') {
viewKey = 'vip';
msg = msg || '该资源仅限会员购买/下载。';
subtitle = '开通会员即可解锁本资源';
if (data.vip_url) action = { kind: 'link', url: data.vip_url, label: '开通会员', icon: 'fa-gem' };
} else if (code === 'jio_points_insufficient' || code === 'jio_balance_insufficient') {
viewKey = 'credits';
const balance = typeof data.balance === 'number' ? data.balance : 0;
const cost = typeof data.cost === 'number' ? data.cost : 0;
unit = currencyName();
msg = '当前余额 ' + balance + ' ' + unit + ',需要 ' + cost + ' ' + unit + '。可通过签到、阅读、邀请好友等方式获取' + unit + '。';
if (data.credits_url) action = { kind: 'link', url: data.credits_url, label: '我的' + unit, icon: 'fa-coins' };
} else if (code === 'jio_download_limit') {
viewKey = 'quota';
msg = msg || '您今日下载免费资源个数已超限,请明日再下载。';
if (data.allow_limit_over && data.post_id) {
action = { kind: 'button', label: '立即购买', icon: 'fa-shopping-cart', attrs: 'type="button" data-jio-open-cashier="' + escapeAttr(data.post_id) + '" data-price-type="download_limit_over"' };
}
} else if (code === 'jio_captcha_failed') {
viewKey = 'captcha';
msg = msg || '人机验证失败,请重试。';
} else if (!action && data.login_url) {
action = { kind: 'link', url: data.login_url, label: '前往登录', icon: 'fa-sign-in-alt' };
}
open(
dlView(viewKey, {
title: viewKey === 'credits' && unit ? unit + '不足' : undefined,
subtitle: subtitle,
body: msg ? '<p class="jio-dl-modal__msg">' + escapeHtml(msg) + '</p>' : '',
actions: action ? [action] : [],
}),
returnFocusEl
);
const openCashierBtn = document.querySelector('[data-jio-open-cashier]');
if (openCashierBtn) {
openCashierBtn.addEventListener('click', function () {
const pid = parseInt(openCashierBtn.getAttribute('data-jio-open-cashier'), 10) || 0;
const pt = openCashierBtn.getAttribute('data-price-type') || '';
close();
if (pid) openCashier(pid, returnFocusEl, pt);
});
}
}
function cashierHeader() {
return '<div class="modal-colorful-header colorful-bg jb-blue">' +
'<button type="button" class="close jio-cashier__close" aria-label="关闭" data-close>×</button>' +
'<div class="colorful-make"></div>' +
'<div class="text-center">' +
' <div class="em2x"><i class="fa fas fa-cart-plus" aria-hidden="true"></i></div>' +
' <div class="jio-cashier__title mt10 em12 padding-w10">确认购买</div>' +
'</div>' +
'</div>';
}
function openCashierContent(content, returnFocusEl) {
open(
cashierHeader() + '<div class="modal-pay-body box-body">' + content + '</div>',
returnFocusEl,
'cashier'
);
}
function pollOrderPaid(orderNumber, returnFocusEl) {
if (!orderNumber) return;
let tries = 0;
const maxTries = 40;
const timer = setInterval(function () {
tries += 1;
fetch(REST_BASE + '/pay/order/' + encodeURIComponent(orderNumber), {
method: 'GET',
credentials: 'same-origin',
headers: restHeaders(),
})
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, json: j }; }); })
.then(function (r) {
if (r.ok && r.json && r.json.paid) {
clearInterval(timer);
window.location.reload();
}
})
.catch(function () { /* ignore */ });
if (tries >= maxTries) clearInterval(timer);
}, 3000);
}
// ===== 收银台(内容购买) =====
function restHeaders(idempotencyKey) {
const headers = {
'Content-Type': 'application/json',
'X-WP-Nonce': window.jioData.restNonce || '',
};
if (idempotencyKey) headers['Idempotency-Key'] = idempotencyKey;
return headers;
}
function buildPreviewUrl(postId, priceType, couponCode) {
var previewUrl = REST_BASE + PAY_CONTENT_PATH + postId;
var qs = [];
if (priceType) qs.push('price_type=' + encodeURIComponent(priceType));
if (couponCode) qs.push('coupon_code=' + encodeURIComponent(couponCode));
if (qs.length) previewUrl += '?' + qs.join('&');
return previewUrl;
}
function fetchPreview(postId, priceType, couponCode) {
return fetch(buildPreviewUrl(postId, priceType, couponCode), {
method: 'GET',
credentials: 'same-origin',
headers: restHeaders(),
}).then(function (resp) {
return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; });
});
}
function openCashier(postId, returnFocusEl, priceType, couponCode) {
if (!postId) return;
priceType = priceType || '';
couponCode = couponCode || '';
openCashierContent(
'<p class="jio-cashier__loading muted-2-color">加载中…</p>',
returnFocusEl
);
fetchPreview(postId, priceType, couponCode)
.then(function (r) {
if (!r.ok || (r.json && r.json.code)) {
const code = r.json && r.json.code ? r.json.code : '';
const data = (r.json && r.json.data) || {};
if (code === 'jio_need_login' || r.status === 401) {
renderError(r.json.message || '', data, returnFocusEl, 'jio_need_login');
return;
}
if (code === 'jio_need_vip') {
renderError(r.json.message || '', data, returnFocusEl, 'jio_need_vip');
return;
}
if (code === 'jio_already_paid') {
window.location.reload();
return;
}
renderError((r.json && r.json.message) || '请稍后重试', data, returnFocusEl, code);
return;
}
var prev = r.json || {};
if (priceType) prev._price_type = priceType;
if (couponCode) prev._coupon_code = couponCode;
renderCashier(prev, returnFocusEl);
})
.catch(function () {
renderError('网络错误,请稍后重试', {}, returnFocusEl);
});
}
function formatAssetLabel(preview, value) {
// preview.currency 由服务端解析(积分价走自定义货币名称),缺省时回退同一来源。
const currency = escapeHtml(preview.currency || (preview.pay_modo === 'points' ? currencyName() : '¥'));
const amount = escapeHtml(value == null ? '' : value);
return preview.pay_modo === 'points' ? amount + currency : currency + amount;
}
function paymentMethodLogo(methodId) {
const logoNames = {
balance: 'pay-balance-logo.svg',
alipay: 'pay-alipay-logo.svg',
epay_alipay: 'pay-alipay-logo.svg',
wechatpay: 'pay-wechat-logo.svg',
epay_wxpay: 'pay-wechat-logo.svg',
paypal: 'pay-paypal-logo.svg',
stripe: 'pay-stripe-logo.svg',
};
const logo = logoNames[methodId];
if (!logo) return '';
const themeUrl = String(window.jioData.themeUrl || '').replace(/\/$/, '');
return '<img src="' + escapeAttr(themeUrl + '/assets/img/' + logo) + '" alt="">';
}
function cashierPriceRow(label, value, className) {
return '<div class="flex jsb ab jio-cashier__price-row">' +
'<span class="muted-2-color">' + label + '</span>' +
'<div class="' + (className || '') + '">' + value + '</div>' +
'</div>';
}
function renderCashier(preview, returnFocusEl) {
if (!preview) return;
if (preview.need_login && !preview.allow_guest) {
renderError('', { login_url: preview.login_url }, returnFocusEl, 'jio_need_login');
return;
}
if (preview.need_vip) {
renderError('', { vip_url: preview.vip_url }, returnFocusEl, 'jio_need_vip');
return;
}
if (preview.has_paid_access) {
window.location.reload();
return;
}
// Content free: reload.
if (preview.is_free) {
window.location.reload();
return;
}
const isPoints = preview.pay_modo === 'points';
const priceType = (preview._price_type || preview.price_type || '');
let amountLabel = escapeHtml(preview.amount_label || '');
let baseLabel = escapeHtml(preview.base_label || '');
const listLabel = escapeHtml(preview.list_amount_label || preview.amount_label || amountLabel);
if (priceType === 'download_limit_over' && preview.download_limit_over_label && !preview.discounts) {
amountLabel = escapeHtml(preview.download_limit_over_label);
baseLabel = amountLabel;
preview.amount = preview.download_limit_over_price;
preview.base = preview.download_limit_over_price;
}
const title = escapeHtml(preview.title || '');
const typeLabel = escapeHtml(preview.pay_type_label || '');
const balance = isPoints && Number.isSafeInteger(preview.balance) ? preview.balance : 0;
const methods = Array.isArray(preview.payment_methods) ? preview.payment_methods : [];
const couponAllowed = !isPoints && !!preview.coupon_allowed;
const couponCodeVal = escapeAttr(preview.coupon_code || preview._coupon_code || '');
let priceRows = '';
if (!preview.pay_limit) {
let regularPrice = '<span class="em14">' + baseLabel + '</span>';
if (!isPoints && Number(preview.original) > Number(preview.base)) {
regularPrice = '<span class="original-price">' + formatAssetLabel(preview, preview.original) + '</span>' + regularPrice;
}
priceRows += cashierPriceRow('价格', regularPrice, 'jio-cashier__regular-price');
}
if (preview.list_amount !== preview.base) {
priceRows += cashierPriceRow(
'会员价<span class="jio-cashier__vip-mark">VIP' + escapeHtml(preview.vip_level || '') + '</span>',
'<span class="em14">' + listLabel + '</span>',
'c-red'
);
} else if (preview.amount !== preview.base && !preview.rebate_discount && !preview.coupon_discount) {
priceRows += cashierPriceRow(
'会员价<span class="jio-cashier__vip-mark">VIP' + escapeHtml(preview.vip_level || '') + '</span>',
'<span class="em14">' + amountLabel + '</span>',
'c-red'
);
}
if (preview.rebate_discount > 0) {
priceRows += cashierPriceRow(
'推广折扣',
'<span class="em14">' + escapeHtml(preview.rebate_discount_label || ('-' + preview.rebate_discount)) + '</span>'
);
}
if (preview.coupon_discount > 0) {
priceRows += cashierPriceRow(
'优惠码',
'<span class="em14">' + escapeHtml(preview.coupon_discount_label || ('-' + preview.coupon_discount)) + '</span>'
);
}
if (preview.amount !== preview.base || preview.rebate_discount > 0 || preview.coupon_discount > 0) {
priceRows += cashierPriceRow(
'支付金额',
'<span class="em14">' + amountLabel + '</span>',
'font-bold c-red'
);
}
let body = '';
body += '<div class="mb10 order-type-' + escapeAttr(preview.pay_type || '') + '">' +
'<span class="pay-tag badg badg-sm mr6">' + typeLabel + '</span><span>' + title + '</span>' +
'</div>';
body += '<div class="mb10 muted-box padding-h6 line-16">' + priceRows + '</div>';
if (isPoints) {
const unit = currencyName();
body += '<div class="mb10 muted-box padding-h10">' +
'<div class="flex jsb ac"><span class="muted-2-color">' +
'<i class="fa fas fa-database jio-cashier__asset-icon" aria-hidden="true"></i>我的' + unit +
'</span><div class="c-green"><span class="em14">' + balance + '</span></div></div></div>';
if (!preview.balance_enough) {
body += '<div class="badg c-red btn-block mb20 jio-cashier__warn">抱歉,您的' + unit + '不足,暂时无法支付</div>';
body += '<div class="modal-buts but-average">';
if (preview.credits_url) {
body += '<a class="but padding-lg" href="' + escapeAttr(preview.credits_url) + '">我的' + unit + '</a>';
}
body += '</div>';
openCashierContent(body, returnFocusEl);
return;
}
body += '<button type="button" class="mt6 but jb-red initiate-pay btn-block radius" data-jio-pay-submit data-mode="points" data-price-type="' + escapeAttr(priceType) + '">' +
'立即支付<span class="pay-price-text ml10">' + amountLabel + '</span></button>';
} else {
if (couponAllowed) {
body += '<div class="mb10 coupon-input-box">';
body += '<div class="muted-2-color em09 mb6">使用优惠码</div>';
body += '<div class="relative">';
body += '<input type="text" class="form-control coupon-input" name="jio_coupon_code" value="' + couponCodeVal + '" placeholder="请输入优惠码" autocomplete="off">';
body += '<span class="abs-right"><button type="button" class="but c-blue coupon-submit" data-jio-check-coupon>检查优惠码</button></span>';
body += '</div>';
if (preview.coupon_desc) {
body += '<div class="muted-3-color px12 mt6 jio-cashier__coupon-desc">' + escapeHtml(preview.coupon_desc) + '</div>';
}
if (preview.coupon_error) {
body += '<div class="jio-cashier__warn">' + escapeHtml(preview.coupon_error) + '</div>';
} else if (preview.coupon_discount > 0) {
body += '<div class="jio-cashier__coupon-ok">优惠码可用</div>';
}
body += '</div>';
}
const walletBal = typeof preview.wallet_balance === 'string'
? preview.wallet_balance
: (typeof preview.balance === 'string' ? preview.balance : '0.00');
const hasBalanceMethod = methods.some(function (m) { return (m && m.id) === 'balance'; });
let methodHtml = '';
methods.forEach(function (m, idx) {
const rawId = String((m && m.id) || '');
const id = escapeAttr(rawId);
const name = escapeHtml((m && m.name) || rawId);
methodHtml += '<div class="flex jc hh payment-method-radio hollow-radio flex-auto pointer' + (idx === 0 ? ' active' : '') + '"' +
' role="radio" aria-checked="' + (idx === 0 ? 'true' : 'false') + '" tabindex="0" data-jio-payment-method="' + id + '">' +
paymentMethodLogo(rawId) + '<div>' + name + '</div></div>';
});
if (methods.length > 1) {
body += '<div class="muted-2-color em09 mb6">请选择支付方式</div>' +
'<div class="flex mb10 jio-cashier__methods" role="radiogroup">' + methodHtml + '</div>';
}
if (methods.length) {
body += '<input type="hidden" name="jio_pay_method" value="' + escapeAttr(methods[0].id || '') + '">';
}
if (hasBalanceMethod) {
const balanceSelected = methods[0] && methods[0].id === 'balance';
body += '<div class="mb10 muted-box padding-h10 jio-cashier__balance" data-jio-balance-box' + (balanceSelected ? '' : ' hidden') + '>' +
'<div class="flex jsb ac"><span class="muted-2-color">' +
'<i class="fa fas fa-wallet jio-cashier__asset-icon" aria-hidden="true"></i>我的余额' +
'</span><div class="c-blue-2"><span class="em14">' + escapeHtml(walletBal) + '</span></div></div>';
if (preview.balance_enough === false) {
if (preview.credits_url) {
body += '<a class="but c-red block mt6 jio-cashier__recharge" href="' + escapeAttr(preview.credits_url) + '">' +
'抱歉!余额不足,请先充值 <i class="ml6 fa fas fa-angle-right em12" aria-hidden="true"></i></a>';
} else {
body += '<div class="c-red mt6 jio-cashier__recharge">抱歉!余额不足,请先充值</div>';
}
}
body += '</div>';
}
if (methods.length) {
body += '<button type="button" class="mt6 but jb-red initiate-pay btn-block radius" data-jio-pay-submit data-mode="cash" data-price-type="' + escapeAttr(priceType) + '">' +
'立即支付<span class="pay-price-text ml10">' + amountLabel + '</span></button>';
} else {
body += '<div class="badg c-red btn-block jio-cashier__warn">暂无可用支付方式,请联系站长配置收款接口</div>';
}
}
openCashierContent(body, returnFocusEl);
const modal = document.getElementById('jio-dl-modal');
const checkBtn = modal && modal.querySelector('[data-jio-check-coupon]');
if (checkBtn) {
checkBtn.addEventListener('click', function () {
const input = modal.querySelector('input[name="jio_coupon_code"]');
const code = input ? String(input.value || '').trim() : '';
if (!code) {
if (input) input.focus();
return;
}
checkBtn.disabled = true;
const old = checkBtn.innerHTML;
checkBtn.innerHTML = '检查中…';
fetchPreview(preview.post_id, priceType, code)
.then(function (r) {
checkBtn.disabled = false;
checkBtn.innerHTML = old;
if (!r.ok || (r.json && r.json.code && !r.json.amount && r.json.amount !== 0)) {
renderError((r.json && r.json.message) || '请稍后重试', (r.json && r.json.data) || {}, returnFocusEl, (r.json && r.json.code) || '');
return;
}
var prev = r.json || {};
if (priceType) prev._price_type = priceType;
prev._coupon_code = code;
renderCashier(prev, returnFocusEl);
})
.catch(function () {
checkBtn.disabled = false;
checkBtn.innerHTML = old;
renderError('网络错误,请稍后重试', {}, returnFocusEl);
});
});
}
if (modal) {
const methodCards = modal.querySelectorAll('[data-jio-payment-method]');
const methodInput = modal.querySelector('input[name="jio_pay_method"]');
const balanceBox = modal.querySelector('[data-jio-balance-box]');
const selectMethod = function (card) {
const method = card.getAttribute('data-jio-payment-method') || '';
methodCards.forEach(function (item) {
const active = item === card;
item.classList.toggle('active', active);
item.setAttribute('aria-checked', active ? 'true' : 'false');
});
if (methodInput) methodInput.value = method;
if (balanceBox) balanceBox.hidden = method !== 'balance';
};
methodCards.forEach(function (card) {
card.addEventListener('click', function () { selectMethod(card); });
card.addEventListener('keydown', function (event) {
if (event.key === 'Enter' || event.key === ' ') {
event.preventDefault();
selectMethod(card);
}
});
});
}
const submitBtn = modal && modal.querySelector('[data-jio-pay-submit]');
if (submitBtn) {
submitBtn.addEventListener('click', function () {
const mode = submitBtn.getAttribute('data-mode') || (isPoints ? 'points' : 'cash');
let paymentMethod = 'balance';
const selected = modal.querySelector('input[name="jio_pay_method"]');
if (selected) paymentMethod = selected.value;
const input = modal.querySelector('input[name="jio_coupon_code"]');
const code = input ? String(input.value || '').trim() : (preview.coupon_code || preview._coupon_code || '');
submitPurchase(preview.post_id, mode, paymentMethod, submitBtn, returnFocusEl, code);
});
}
}
function submitPurchase(postId, mode, paymentMethod, button, returnFocusEl, couponCode) {
if (!postId) return;
const original = button.innerHTML;
button.disabled = true;
button.innerHTML = '处理中…';
const payload = { mode: mode };
if (mode === 'cash') payload.payment_method = paymentMethod || 'balance';
const pt = button.getAttribute('data-price-type') || '';
if (pt) payload.price_type = pt;
if (mode === 'cash' && couponCode) payload.coupon_code = couponCode;
if (!button.dataset.jioOperationKey) {
button.dataset.jioOperationKey = window.JioRequest.createIdempotencyKey();
}
fetch(REST_BASE + PAY_CONTENT_PATH + postId, {
method: 'POST',
credentials: 'same-origin',
headers: restHeaders(button.dataset.jioOperationKey),
body: JSON.stringify(payload),
})
.then(function (resp) { return resp.json().then(function (j) { return { ok: resp.ok, status: resp.status, json: j }; }); })
.then(function (r) {
button.disabled = false;
button.innerHTML = original;
if (!r.ok || (r.json && r.json.code && !r.json.success)) {
const code = r.json && r.json.code ? r.json.code : '';
const data = (r.json && r.json.data) || {};
if (code === 'jio_already_paid') {
window.location.reload();
return;
}
renderError((r.json && r.json.message) || '请稍后重试', data, returnFocusEl, code);
return;
}
const data = (r.json && r.json.data) ? r.json.data : r.json;
delete button.dataset.jioOperationKey;
if (data && data.mode) {
if (window.JioModalRuntime) window.JioModalRuntime.invalidate(['orders']);
if (data.mode === 'qrcode') {
renderScanPay(data, returnFocusEl);
} else if (window.JioPayCashier) {
close();
window.JioPayCashier.handle(data, {});
}
if ((data.mode === 'qrcode' || data.mode === 'jsapi') && data.order_number) {
pollOrderPaid(data.order_number, returnFocusEl);
}
return;
}
window.location.reload();
})
.catch(function () {
button.disabled = false;
button.innerHTML = original;
renderError('网络错误,请稍后重试', {}, returnFocusEl);
});
}
function renderScanPay(data, returnFocusEl) {
data = data || {};
const body = window.JioPayCashier
? window.JioPayCashier.qrHtml(data)
: '<p class="jio-dl-modal__msg">' + escapeHtml('收银台组件未加载,请刷新页面重试。') + '</p>';
open(
dlView('pay', {
subtitle: '支付完成后页面会自动刷新',
body: body,
actions: [{ kind: 'button', label: '我已支付', icon: 'fa-sync-alt', attrs: 'type="button" data-jio-reload' }],
}),
returnFocusEl
);
const reloadBtn = document.querySelector('[data-jio-reload]');
if (reloadBtn) reloadBtn.addEventListener('click', function () { window.location.reload(); });
}
// ===== 事件绑定 =====
document.addEventListener('click', function (e) {
const buyBtn = e.target.closest('[data-action="jio-need-purchase"], [data-jio-pay-content]');
if (buyBtn) {
e.preventDefault();
const postId = parseInt(
buyBtn.getAttribute('data-jio-pay-content') ||
buyBtn.getAttribute('data-post-id') ||
(buyBtn.closest('[data-post-id]') && buyBtn.closest('[data-post-id]').getAttribute('data-post-id')) ||
'0',
10
);
const priceType = buyBtn.getAttribute('data-price-type') || '';
if (postId) openCashier(postId, buyBtn, priceType);
return;
}
});
// Public cashier bridge (content pay).
window.jioOpenCashier = function (postId, returnFocusEl, priceType, couponCode) {
openCashier(postId, returnFocusEl || null, priceType || '', couponCode || '');
};
})();