feat(admin): quick-edit paid post settings in a dialog on the pay products page
- Rebuild aksrt_pay_products as list + in-page quick settings dialog (pay type, limit, cash/points prices, sales float, coupon, order expiry, copy) - Query by _aksrt_pay_type with real pagination; drop order-summary fallback and the duplicate "Meta 销量" column - Centralize pay meta writes in DomainModel::persist_pay_meta(); stop writing unread _aksrt_pay_modo / _aksrt_pay_limit keys Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
4083613dbe
commit
dc6dbc05d8
11 files changed
+983
-349
No files matched your search
@@ -0,0 +1,196 @@
|
||||
/** 付费文章:快速设置弹窗(填充、条件显隐、会员价校验、未保存提醒、防重复提交)。仅本页入队。 */
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var root = document.querySelector('.aksrt-products-page');
|
||||
var dialog = document.getElementById('aksrt-product-editor');
|
||||
if (!root || !dialog || typeof dialog.showModal !== 'function') return;
|
||||
|
||||
var form = dialog.querySelector('[data-aksrt-product-form]');
|
||||
var submit = form.querySelector('[data-aksrt-product-submit]');
|
||||
var subject = form.querySelector('[data-aksrt-product-subject]');
|
||||
var editLink = form.querySelector('[data-aksrt-product-edit-link]');
|
||||
var submitLabel = submit.textContent;
|
||||
var opener = null;
|
||||
var snapshot = '';
|
||||
var pointerStartedOutside = false;
|
||||
|
||||
function field(key) {
|
||||
return form.querySelectorAll('[name="aksrt_pay[' + key + ']"]');
|
||||
}
|
||||
|
||||
function value(key) {
|
||||
var nodes = field(key);
|
||||
for (var i = 0; i < nodes.length; i += 1) {
|
||||
var node = nodes[i];
|
||||
if (node.type === 'radio') {
|
||||
if (node.checked) return node.value;
|
||||
} else if (node.type === 'checkbox') {
|
||||
return node.checked ? node.value : '';
|
||||
} else {
|
||||
return node.value;
|
||||
}
|
||||
}
|
||||
return '';
|
||||
}
|
||||
|
||||
function paneActive(name) {
|
||||
switch (name) {
|
||||
case 'enabled': return value('pay_type') !== 'no';
|
||||
case 'disabled': return value('pay_type') === 'no';
|
||||
case 'cash': return value('pay_modo') !== 'points';
|
||||
case 'points': return value('pay_modo') === 'points';
|
||||
case 'expire': return value('order_expired_s') === '1';
|
||||
default: return true;
|
||||
}
|
||||
}
|
||||
|
||||
/** 隐藏的分区连同其中的控件一起禁用:既不提交,也不参与必填校验。 */
|
||||
function syncPanes() {
|
||||
form.querySelectorAll('[data-aksrt-product-pane]').forEach(function (pane) {
|
||||
var active = paneActive(pane.getAttribute('data-aksrt-product-pane'));
|
||||
pane.hidden = !active;
|
||||
if (pane.tagName === 'FIELDSET') pane.disabled = !active;
|
||||
});
|
||||
form.querySelectorAll('input:not([type="hidden"]), select, textarea').forEach(function (control) {
|
||||
var node = control.parentElement;
|
||||
var disabled = false;
|
||||
while (node && node !== form) {
|
||||
if (node.hasAttribute('data-aksrt-product-pane') && node.hidden) {
|
||||
disabled = true;
|
||||
break;
|
||||
}
|
||||
node = node.parentElement;
|
||||
}
|
||||
control.disabled = disabled;
|
||||
});
|
||||
syncVip();
|
||||
}
|
||||
|
||||
/** 现金按「分」比较,避免浮点误差。 */
|
||||
function amount(raw, mode) {
|
||||
var text = String(raw || '').trim();
|
||||
if (mode === 'points') return /^\d+$/.test(text) ? Number(text) : NaN;
|
||||
var match = text.match(/^(\d+)(?:\.(\d{1,2}))?$/);
|
||||
return match ? Number(match[1]) * 100 + Number((match[2] || '').padEnd(2, '0')) : NaN;
|
||||
}
|
||||
|
||||
function syncVip() {
|
||||
['cash', 'points'].forEach(function (mode) {
|
||||
var base = form.querySelector('[data-aksrt-product-base="' + mode + '"]');
|
||||
var message = form.getAttribute(mode === 'points' ? 'data-vip-points-error' : 'data-vip-error');
|
||||
form.querySelectorAll('[data-aksrt-product-vip="' + mode + '"]').forEach(function (input) {
|
||||
var vip = amount(input.value, mode);
|
||||
var limit = amount(base && base.value, mode);
|
||||
input.setCustomValidity(!input.disabled && vip > limit ? message : '');
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function serialize() {
|
||||
return new URLSearchParams(new FormData(form)).toString();
|
||||
}
|
||||
|
||||
function fill(payload) {
|
||||
var fields = payload.fields || {};
|
||||
form.reset();
|
||||
form.elements.post_id.value = String(payload.id || '');
|
||||
Object.keys(fields).forEach(function (key) {
|
||||
var current = String(fields[key]);
|
||||
field(key).forEach(function (node) {
|
||||
if (node.type === 'radio') {
|
||||
node.checked = node.value === current;
|
||||
} else if (node.type === 'checkbox') {
|
||||
node.checked = current === '1';
|
||||
} else {
|
||||
node.value = key === 'order_expired_time' && current === '0' ? '' : current;
|
||||
}
|
||||
});
|
||||
});
|
||||
subject.textContent = (payload.title || '') + ' · #' + String(payload.id || '');
|
||||
editLink.hidden = !payload.editUrl;
|
||||
editLink.href = payload.editUrl || '#';
|
||||
syncPanes();
|
||||
}
|
||||
|
||||
function lockScroll() {
|
||||
document.documentElement.classList.toggle('aksrt-products-modal-open', dialog.open);
|
||||
}
|
||||
|
||||
function requestClose() {
|
||||
if (form.dataset.submitting) return;
|
||||
if (serialize() !== snapshot && !window.confirm(form.getAttribute('data-discard'))) return;
|
||||
dialog.close();
|
||||
}
|
||||
|
||||
root.addEventListener('click', function (event) {
|
||||
var button = event.target.closest('[data-aksrt-product-edit]');
|
||||
if (!button || dialog.open) return;
|
||||
var payload;
|
||||
try {
|
||||
payload = JSON.parse(button.getAttribute('data-aksrt-product-edit'));
|
||||
} catch (error) {
|
||||
return;
|
||||
}
|
||||
opener = button;
|
||||
fill(payload);
|
||||
snapshot = serialize();
|
||||
dialog.showModal();
|
||||
lockScroll();
|
||||
});
|
||||
|
||||
form.addEventListener('change', syncPanes);
|
||||
form.addEventListener('input', syncVip);
|
||||
|
||||
function isOutside(event) {
|
||||
var rect = dialog.getBoundingClientRect();
|
||||
return event.clientX < rect.left || event.clientX > rect.right || event.clientY < rect.top || event.clientY > rect.bottom;
|
||||
}
|
||||
|
||||
dialog.addEventListener('pointerdown', function (event) {
|
||||
pointerStartedOutside = event.target === dialog && isOutside(event);
|
||||
});
|
||||
dialog.addEventListener('click', function (event) {
|
||||
if (event.target.closest('[data-aksrt-dialog-close]')
|
||||
|| (pointerStartedOutside && event.target === dialog && isOutside(event))) {
|
||||
event.preventDefault();
|
||||
requestClose();
|
||||
}
|
||||
pointerStartedOutside = false;
|
||||
});
|
||||
dialog.addEventListener('cancel', function (event) {
|
||||
event.preventDefault();
|
||||
requestClose();
|
||||
});
|
||||
dialog.addEventListener('close', function () {
|
||||
lockScroll();
|
||||
if (opener && opener.isConnected) opener.focus();
|
||||
opener = null;
|
||||
});
|
||||
|
||||
form.addEventListener('submit', function (event) {
|
||||
syncVip();
|
||||
if (form.dataset.submitting || !form.checkValidity()) {
|
||||
event.preventDefault();
|
||||
form.reportValidity();
|
||||
return;
|
||||
}
|
||||
form.dataset.submitting = '1';
|
||||
submit.textContent = form.getAttribute('data-busy-label');
|
||||
// 延后禁用:同步禁用 submit 按钮会让部分浏览器丢掉这次提交。
|
||||
setTimeout(function () { submit.disabled = true; }, 0);
|
||||
});
|
||||
|
||||
// bfcache 返回时恢复按钮,避免弹窗停在保存中状态。
|
||||
window.addEventListener('pageshow', function () {
|
||||
delete form.dataset.submitting;
|
||||
submit.disabled = false;
|
||||
submit.textContent = submitLabel;
|
||||
lockScroll();
|
||||
});
|
||||
|
||||
var highlighted = root.querySelector('.aksrt-products-table tr.is-highlight');
|
||||
if (highlighted && typeof highlighted.scrollIntoView === 'function') {
|
||||
highlighted.scrollIntoView({ block: 'center' });
|
||||
}
|
||||
})();
|
||||
Reference in new issue
Block a user