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,84 @@
|
||||
/* 付费文章(aksrt_pay_products):列表 + 快速设置弹窗。复用 native-ui.css 的 --an-* 令牌,规则限定在本页类名下。 */
|
||||
.aksrt-products-page [hidden],
|
||||
.aksrt-product-dialog [hidden] { display: none !important; }
|
||||
|
||||
/* ---------- 工具栏:类型视图 + 搜索 ---------- */
|
||||
.aksrt-products-toolbar { display: flex; flex-wrap: wrap; align-items: center; justify-content: space-between; gap: 10px 18px; padding: 14px 20px; border-bottom: 1px solid var(--an-border-soft); }
|
||||
.aksrt-products-toolbar .subsubsub { margin: 0; padding: 0; box-shadow: none; }
|
||||
.aksrt-products-toolbar .aksrt-native-filters { margin: 0; }
|
||||
|
||||
/* ---------- 商品表 ---------- */
|
||||
.aksrt-products-page .aksrt-products-table { min-width: 960px; table-layout: fixed; }
|
||||
.aksrt-products-table .column-product { width: 32%; }
|
||||
.aksrt-products-table .column-type { width: 13%; }
|
||||
.aksrt-products-table .column-price { width: 17%; }
|
||||
.aksrt-products-table .column-sales { width: 11%; }
|
||||
.aksrt-products-table .column-revenue { width: 12%; }
|
||||
.aksrt-products-table .column-actions { width: 15%; }
|
||||
.aksrt-products-page .aksrt-products-table td { vertical-align: top; overflow-wrap: anywhere; }
|
||||
.aksrt-products-table .row-title { font-weight: 600; }
|
||||
.aksrt-products-sub { margin-top: 4px; color: var(--an-muted); font-size: 12px; line-height: 1.5; }
|
||||
.aksrt-products-links { display: flex; flex-wrap: wrap; gap: 4px 12px; margin-top: 8px; font-size: 12px; }
|
||||
.aksrt-products-links a { text-decoration: none; }
|
||||
.aksrt-products-table tr.is-highlight > td { background: var(--an-primary-soft); }
|
||||
|
||||
/* ---------- 快速设置弹窗 ---------- */
|
||||
html.aksrt-products-modal-open { overflow: hidden; }
|
||||
.aksrt-product-dialog { width: min(760px, calc(100vw - 32px)); max-width: none; max-height: calc(100vh - 48px); max-height: calc(100dvh - 48px); margin: auto; padding: 0; overflow: hidden; color: var(--an-text-2); background: var(--an-card); border: 0; border-radius: var(--an-radius); box-shadow: 0 16px 64px rgba(15, 23, 42, .2); }
|
||||
.aksrt-product-dialog::backdrop { background: rgba(15, 23, 42, .45); }
|
||||
.aksrt-product-dialog__frame { display: flex; flex-direction: column; max-height: inherit; margin: 0; }
|
||||
.aksrt-product-dialog__head { display: flex; flex: none; align-items: flex-start; justify-content: space-between; gap: 16px; padding: 20px 24px 18px; border-bottom: 1px solid var(--an-border-soft); }
|
||||
.aksrt-product-dialog .aksrt-product-dialog__head h2 { margin: 0; padding: 0; color: var(--an-text); font-size: 18px; font-weight: 600; line-height: 1.5; }
|
||||
.aksrt-product-dialog__head p { margin: 5px 0 0; font-size: 13px; line-height: 1.6; overflow-wrap: anywhere; }
|
||||
.aksrt-product-dialog__close { display: inline-flex; flex: none; align-items: center; justify-content: center; width: 32px; height: 32px; padding: 0; color: var(--an-text-2); background: transparent; border: 0; border-radius: var(--an-radius-sm); cursor: pointer; }
|
||||
.aksrt-product-dialog__close:hover { color: var(--an-text); background: var(--an-bg-soft); }
|
||||
.aksrt-product-dialog__body { min-height: 0; padding: 4px 24px 20px; overflow-y: auto; overscroll-behavior: contain; }
|
||||
.aksrt-product-dialog__foot { display: flex; flex: none; justify-content: flex-end; gap: 8px; padding: 14px 24px; background: var(--an-card); border-top: 1px solid var(--an-border-soft); }
|
||||
.aksrt-product-dialog__foot .button { min-width: 110px; }
|
||||
.aksrt-product-dialog .button:disabled { opacity: .65; cursor: not-allowed; }
|
||||
.aksrt-product-dialog :is(a, button, input, select, textarea):focus-visible { outline: 2px solid var(--an-primary); outline-offset: 2px; }
|
||||
|
||||
.aksrt-product-section { padding: 18px 0; border-bottom: 1px solid var(--an-border-soft); }
|
||||
.aksrt-product-section:last-child { border-bottom: 0; }
|
||||
.aksrt-product-dialog .aksrt-product-section h3 { margin: 0 0 12px; color: var(--an-text); font-size: 14px; font-weight: 600; line-height: 1.5; }
|
||||
.aksrt-product-section__hint { margin: -6px 0 12px; color: var(--an-muted); font-size: 12px; }
|
||||
.aksrt-product-section .aksrt-native-callout { margin: 14px 0 0; }
|
||||
.aksrt-product-dialog .aksrt-native-form-grid { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.aksrt-product-pane { min-width: 0; margin: 0; padding: 0; border: 0; }
|
||||
.aksrt-product-pane + .aksrt-product-pane { margin-top: 0; }
|
||||
.aksrt-product-dialog textarea { resize: vertical; }
|
||||
|
||||
.aksrt-product-radios { margin: 0; padding: 0; border: 0; }
|
||||
.aksrt-product-radios legend { margin-bottom: 6px; padding: 0; }
|
||||
.aksrt-product-radios > div { display: flex; flex-wrap: wrap; gap: 8px; }
|
||||
.aksrt-product-radios label { position: relative; cursor: pointer; }
|
||||
.aksrt-product-radios input[type="radio"] { position: absolute; width: 1px; height: 1px; margin: -1px; overflow: hidden; clip-path: inset(50%); }
|
||||
.aksrt-product-radios label > span { display: inline-flex; align-items: center; min-height: 32px; padding: 4px 12px; color: var(--an-text-2); background: var(--an-card); border: 1px solid var(--an-border); border-radius: var(--an-radius-sm); }
|
||||
.aksrt-product-radios label:hover > span { border-color: var(--an-primary); }
|
||||
.aksrt-product-radios input:checked + span { color: var(--an-primary); background: var(--an-primary-soft); border-color: var(--an-primary); font-weight: 600; }
|
||||
.aksrt-product-radios input:focus-visible + span { outline: 2px solid var(--an-primary); outline-offset: 2px; }
|
||||
.aksrt-product-section > [data-aksrt-product-pane="enabled"] { margin-top: 14px; }
|
||||
|
||||
.aksrt-product-inline { display: flex; align-items: center; gap: 6px; }
|
||||
.aksrt-product-inline input { flex: 1 1 auto; min-width: 0; }
|
||||
.aksrt-product-inline select { flex: none; width: auto; }
|
||||
.aksrt-product-unit { flex: none; color: var(--an-muted); }
|
||||
.aksrt-product-switches { justify-content: flex-end; gap: 8px; }
|
||||
.aksrt-product-check { display: inline-flex; align-items: center; gap: 6px; color: var(--an-text); }
|
||||
.aksrt-product-check input { margin: 0; }
|
||||
.aksrt-product-expire { max-width: 50%; margin-top: 14px; }
|
||||
.aksrt-product-note { margin: 4px 0 0; font-size: 12px; }
|
||||
|
||||
@media (max-width: 782px) {
|
||||
.aksrt-products-toolbar { padding: 12px 14px; }
|
||||
.aksrt-products-toolbar .aksrt-native-filters { width: 100%; }
|
||||
}
|
||||
@media (max-width: 600px) {
|
||||
.aksrt-product-dialog { width: calc(100vw - 24px); max-height: calc(100dvh - 24px); }
|
||||
.aksrt-product-dialog__head,
|
||||
.aksrt-product-dialog__body,
|
||||
.aksrt-product-dialog__foot { padding-left: 16px; padding-right: 16px; }
|
||||
.aksrt-product-dialog .aksrt-native-form-grid { grid-template-columns: 1fr; }
|
||||
.aksrt-product-expire { max-width: none; }
|
||||
.aksrt-product-dialog__foot .button { flex: 1; min-width: 0; }
|
||||
}
|
||||
@@ -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