Files
wordpress-theme-aksrt/assets/js/admin-withdrawals.js
T
kate522 dcd306b428 feat(withdrawal): make payout channels a configurable sortable list
The payout channel was a two-item checkbox (alipay / wechat). Sites that pay
out through a bank transfer, USDT or any other route had no way to express it,
and every surface behind it — the withdrawal modal, the server-side check, the
admin review list — hard-coded those two names.

affiliate_payout_channels is now a repeater on both the PHP and Vue schemas,
using the same draggable component as the post filters and gateway config. Each
row carries a stable key, a display label, an account placeholder, an optional
note and an enabled flag. Disabled channels disappear from the front end but
stay resolvable, so historical withdrawal records still render their original
name.

PaymentRuntime::payout_channels() normalises the stored structure: it drops rows
with an empty key or label, de-duplicates keys, and falls back to the built-in
two channels when nothing is configured. enabled_payout_channels() feeds the
front end and payout_channel() resolves a single key for record display.
CommissionLedger's validation now compares against the configured, enabled keys
instead of a literal allowlist.

The withdrawal modal renders the configured list and swaps the account
placeholder and note when the channel changes; credits.js drives that. The
admin review page reads the same settings for its method labels and filter, and
the review dialog gained a "渠道说明" row so the operator sees the payout
instruction, hidden when empty.

Default channels live in SchemaPayment as plain data — the runtime imports them
rather than the reverse, because the schema layer must not depend on a runtime
settings class (doing it the other way made the schema harness fatal).

Verification: drove payout_channels() through default / custom / dirty / duplicate
inputs and confirmed enabled filtering, note lookup and disabled-channel
resolution. Added payment-withdrawal-channels-contract.test.cjs covering the
schema shape, runtime helpers, front-end wiring and admin read path. 873 tests
pass, PSR-4 audit clean, admin SPA builds.
2026-11-04 19:16:39 +08:00

159 lines
5.6 KiB
JavaScript

/** 提现审核:共享处理弹窗(开始处理 / 确认打款 / 拒绝)、复制收款信息、防重复提交。仅本页入队。 */
(function () {
'use strict';
var root = document.querySelector('.jio-wd-page');
if (!root) return;
var dialog = root.querySelector('#jio-wd-dialog');
var form = dialog ? dialog.querySelector('[data-jio-wd-form]') : null;
var note = form ? form.querySelector('[data-jio-wd-note]') : null;
var toast = root.querySelector('[data-jio-wd-toast]');
var toastTimer;
var opener = null;
function notify(key) {
if (!toast) return;
clearTimeout(toastTimer);
toast.textContent = toast.getAttribute('data-' + key);
toast.classList.add('is-visible');
toastTimer = setTimeout(function () { toast.classList.remove('is-visible'); }, 3000);
}
function lockScroll() {
document.documentElement.classList.toggle('jio-wd-modal-open', !!(dialog && dialog.open));
}
/** data-mode 可列多个状态(空格分隔):只显示与当前目标状态匹配的说明、标题与提交按钮。 */
function applyMode(mode) {
form.closest('dialog').querySelectorAll('[data-mode]').forEach(function (el) {
el.hidden = el.getAttribute('data-mode').split(' ').indexOf(mode) === -1;
});
if (note) note.required = mode === 'rejected';
}
function resetSubmits() {
delete form.dataset.submitting;
form.querySelectorAll('[data-jio-wd-submit]').forEach(function (button) {
button.disabled = false;
if (button.dataset.idleLabel) button.textContent = button.dataset.idleLabel;
});
}
function openFor(button) {
var mode = button.getAttribute('data-jio-wd-open');
['id', 'nonce', 'key'].forEach(function (field) {
form.querySelector('[data-field="' + field + '"]').value = button.dataset[field] || '';
});
form.querySelector('[data-field="status"]').value = mode;
dialog.querySelectorAll('[data-summary]').forEach(function (dd) {
var raw = button.dataset[dd.getAttribute('data-summary')] || '';
// 渠道说明是可选的,留空时整行隐藏,不留一条破折号。
var row = dd.parentElement;
if (dd.getAttribute('data-summary') === 'method_note' && raw === '') {
if (row) row.hidden = true;
dd.textContent = '';
return;
}
if (row) row.hidden = false;
dd.textContent = raw || '—';
});
if (note) note.value = '';
resetSubmits();
applyMode(mode);
opener = button;
dialog.showModal();
lockScroll();
if (mode === 'rejected' && note) note.focus();
}
if (dialog && form && typeof dialog.showModal === 'function') {
var startedOutside = false;
function outside(event) {
var rect = dialog.getBoundingClientRect();
return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
}
function requestClose() {
if (!form.dataset.submitting) dialog.close();
}
root.addEventListener('click', function (event) {
var button = event.target.closest('[data-jio-wd-open]');
if (button) openFor(button);
});
dialog.querySelectorAll('[data-jio-wd-close]').forEach(function (button) {
button.addEventListener('click', requestClose);
});
dialog.addEventListener('pointerdown', function (event) {
startedOutside = event.target === dialog && outside(event);
});
dialog.addEventListener('click', function (event) {
if (startedOutside && event.target === dialog && outside(event)) requestClose();
startedOutside = false;
});
dialog.addEventListener('cancel', function (event) {
if (form.dataset.submitting) event.preventDefault();
});
dialog.addEventListener('close', function () {
lockScroll();
if (opener && opener.isConnected) opener.focus();
opener = null;
});
form.addEventListener('submit', function (event) {
if (form.dataset.submitting) {
event.preventDefault();
return;
}
if (note && note.required && note.value.trim() === '') {
event.preventDefault();
note.value = '';
note.reportValidity();
return;
}
form.dataset.submitting = '1';
var submit = form.querySelector('[data-jio-wd-submit]:not([hidden])');
if (submit) {
submit.dataset.idleLabel = submit.textContent;
submit.textContent = form.getAttribute('data-busy-label');
// 延后禁用:同步禁用 submit 按钮会让部分浏览器丢掉这次提交。
setTimeout(function () { submit.disabled = true; }, 0);
}
});
// bfcache 返回时恢复按钮,避免弹窗停在提交中状态。
window.addEventListener('pageshow', function (event) {
if (event.persisted) resetSubmits();
});
}
root.addEventListener('click', async function (event) {
var button = event.target.closest('[data-clipboard-text]');
if (!button) return;
var value = button.getAttribute('data-clipboard-text');
try {
if (navigator.clipboard && window.isSecureContext) {
await navigator.clipboard.writeText(value);
} else {
var input = document.createElement('textarea');
input.value = value;
input.className = 'screen-reader-text';
root.appendChild(input);
input.select();
var copied = false;
try {
copied = document.execCommand('copy');
} finally {
input.remove();
button.focus();
}
if (!copied) throw new Error('copy');
}
notify('copied');
} catch (error) {
notify('copy-failed');
}
});
})();