feat(admin): redesign VIP membership page with modal editing
- Replace per-row inline <details> forms and the separate bulk form with a single <dialog> shared by single-user and bulk adjustments - Add stats row, styled tabs, date range filter with reset, sortable expiry - Show fallback tier, remaining time, and disable unavailable operations - History tab: labelled operation/status badges, avatars, local time, batch marker and error message - Move inline script/onclick to assets/js/admin-vip.js; add vip.css - Surface the service error message when every user fails Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
1 parent
18951b01d6
commit
1be52d75a0
5 files changed
+789
-126
No files matched your search
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* 用户会员后台页(users.php?page=aksrt_user_vip)专属样式:列表细节 + 调整弹窗。
|
||||
* 依赖 native-ui.css 的 --an-* 令牌;标记由 src/Admin/Native/UsersMenusVipView.php 输出。
|
||||
*/
|
||||
|
||||
/* ---------- 筛选 ---------- */
|
||||
.aksrt-vip .aksrt-vip-filters .small-text { width: 96px; }
|
||||
.aksrt-vip .aksrt-vip-filters label > span[aria-hidden] { color: var(--an-muted); }
|
||||
|
||||
/* ---------- 列表 ---------- */
|
||||
.aksrt-vip .aksrt-native-card__actions .button .dashicons,
|
||||
.aksrt-vip .column-actions .button .dashicons {
|
||||
width: 16px;
|
||||
height: 16px;
|
||||
margin: 0 4px 0 0;
|
||||
font-size: 16px;
|
||||
line-height: inherit;
|
||||
vertical-align: text-bottom;
|
||||
}
|
||||
.aksrt-vip-selection {
|
||||
padding: 2px 10px;
|
||||
color: var(--an-primary);
|
||||
font-size: 12px;
|
||||
background: var(--an-primary-soft);
|
||||
border-radius: 999px;
|
||||
}
|
||||
.aksrt-vip-table .column-user { width: 32%; }
|
||||
.aksrt-vip-table .column-actions { width: 96px; text-align: right; }
|
||||
.aksrt-vip-login,
|
||||
.aksrt-vip-note,
|
||||
.aksrt-vip-remaining { display: block; margin-top: 4px; font-size: 12px; }
|
||||
.aksrt-vip-expiry { font-variant-numeric: tabular-nums; }
|
||||
.aksrt-vip-sort {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 2px;
|
||||
color: inherit;
|
||||
text-decoration: none;
|
||||
}
|
||||
.aksrt-vip-sort .dashicons { width: 16px; height: 16px; font-size: 14px; line-height: 16px; color: var(--an-muted); }
|
||||
.aksrt-vip-sort:hover,
|
||||
.aksrt-vip-sort.is-active,
|
||||
.aksrt-vip-sort.is-active .dashicons { color: var(--an-primary); }
|
||||
.aksrt-vip-history .column-time { width: 140px; }
|
||||
.aksrt-vip-history .column-user { width: 22%; }
|
||||
.aksrt-vip-history .column-reason { width: 26%; word-break: break-word; }
|
||||
|
||||
/* ---------- 弹窗 ---------- */
|
||||
.aksrt-vip-dialog {
|
||||
width: min(560px, calc(100vw - 32px));
|
||||
max-height: calc(100vh - 64px);
|
||||
padding: 0;
|
||||
color: var(--an-text-2);
|
||||
background: var(--an-card);
|
||||
border: 0;
|
||||
border-radius: var(--an-radius);
|
||||
box-shadow: 0 12px 48px rgba(15, 23, 42, .18);
|
||||
}
|
||||
.aksrt-vip-dialog::backdrop { background: rgba(15, 23, 42, .45); }
|
||||
.aksrt-vip-dialog__form { display: flex; flex-direction: column; max-height: inherit; margin: 0; }
|
||||
.aksrt-vip-dialog__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: space-between;
|
||||
gap: 12px;
|
||||
padding: 16px 20px;
|
||||
border-bottom: 1px solid var(--an-border-soft);
|
||||
}
|
||||
.aksrt-vip-dialog .aksrt-vip-dialog__title { margin: 0; color: var(--an-text); font-size: 16px; font-weight: 600; line-height: 1.4; }
|
||||
.aksrt-vip-dialog__close {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 32px;
|
||||
height: 32px;
|
||||
padding: 0;
|
||||
color: var(--an-muted);
|
||||
background: transparent;
|
||||
border: 0;
|
||||
border-radius: var(--an-radius-sm);
|
||||
cursor: pointer;
|
||||
}
|
||||
.aksrt-vip-dialog__close:hover,
|
||||
.aksrt-vip-dialog__close:focus-visible { color: var(--an-text); background: var(--an-bg-soft); }
|
||||
.aksrt-vip-dialog__body { display: flex; flex-direction: column; gap: 16px; padding: 18px 20px; overflow-y: auto; }
|
||||
.aksrt-vip-dialog__body > * { margin: 0; }
|
||||
.aksrt-vip-dialog__target {
|
||||
padding: 10px 12px;
|
||||
color: var(--an-text);
|
||||
background: var(--an-bg-soft);
|
||||
border-radius: var(--an-radius-sm);
|
||||
word-break: break-word;
|
||||
}
|
||||
.aksrt-vip-dialog__foot {
|
||||
display: flex;
|
||||
justify-content: flex-end;
|
||||
gap: 8px;
|
||||
padding: 14px 20px;
|
||||
border-top: 1px solid var(--an-border-soft);
|
||||
}
|
||||
.aksrt-vip-dialog .button.is-danger { color: #fff; background: var(--an-danger); border-color: var(--an-danger); }
|
||||
.aksrt-vip-dialog .button.is-danger:hover,
|
||||
.aksrt-vip-dialog .button.is-danger:focus { color: #fff; background: #b02a45; border-color: #b02a45; }
|
||||
|
||||
/* 操作分段选择 */
|
||||
.aksrt-vip-ops {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(4, minmax(0, 1fr));
|
||||
gap: 8px;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
}
|
||||
.aksrt-vip-ops legend { margin-bottom: 8px; padding: 0; }
|
||||
.aksrt-vip-op { position: relative; display: block; cursor: pointer; }
|
||||
.aksrt-vip-op input { position: absolute; opacity: 0; pointer-events: none; }
|
||||
.aksrt-vip-op > span {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 10px 6px;
|
||||
color: var(--an-text-2);
|
||||
font-size: 13px;
|
||||
text-align: center;
|
||||
border: 1px solid var(--an-border);
|
||||
border-radius: var(--an-radius-sm);
|
||||
transition: border-color .15s, color .15s, background .15s;
|
||||
}
|
||||
.aksrt-vip-op .dashicons { width: 20px; height: 20px; font-size: 20px; }
|
||||
.aksrt-vip-op:hover > span { border-color: var(--an-primary); }
|
||||
.aksrt-vip-op input:checked + span { color: var(--an-primary); background: var(--an-primary-soft); border-color: var(--an-primary); font-weight: 600; }
|
||||
.aksrt-vip-op input:focus-visible + span { outline: 2px solid color-mix(in srgb, var(--an-primary) 40%, transparent); outline-offset: 1px; }
|
||||
.aksrt-vip-op.is-danger:hover > span { border-color: var(--an-danger); }
|
||||
.aksrt-vip-op.is-danger input:checked + span { color: var(--an-danger); background: #fdf0f2; border-color: var(--an-danger); }
|
||||
.aksrt-vip-op.is-disabled { cursor: not-allowed; }
|
||||
.aksrt-vip-op.is-disabled > span,
|
||||
.aksrt-vip-op.is-disabled:hover > span { color: #c9cdd4; background: var(--an-bg-soft); border-color: var(--an-border-soft); }
|
||||
.aksrt-vip-op-hint { font-size: 12px; }
|
||||
|
||||
.aksrt-vip-pane[hidden] { display: none; }
|
||||
.aksrt-vip-dialog .aksrt-native-callout { margin: 0; }
|
||||
.aksrt-vip-switch { display: inline-flex; align-items: center; gap: 6px; color: var(--an-text); grid-column: 1 / -1; }
|
||||
.aksrt-vip-duration { display: flex; gap: 8px; }
|
||||
.aksrt-vip-duration input { flex: 1 1 auto; }
|
||||
.aksrt-vip-duration select { flex: 0 0 96px; }
|
||||
.aksrt-vip-bulk-note { font-size: 12px; }
|
||||
|
||||
@media (max-width: 600px) {
|
||||
.aksrt-vip-ops { grid-template-columns: repeat(2, minmax(0, 1fr)); }
|
||||
.aksrt-vip-table .column-user { width: auto; }
|
||||
}
|
||||
@@ -0,0 +1,161 @@
|
||||
/**
|
||||
* 用户会员后台页(users.php?page=aksrt_user_vip)的调整弹窗。
|
||||
* - 行内「调整」:单人打开弹窗;工具栏「批量调整」:对勾选用户打开弹窗;
|
||||
* - 按操作类型切换字段,隐藏区域的输入被禁用,不参与校验与提交;
|
||||
* - 单人时按用户状态禁用不可用的操作(延长 / 恢复);
|
||||
* - 提交后禁用按钮防重复提交。
|
||||
* 标记由 src/Admin/Native/UsersMenusVipView.php 输出。
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var dialog = document.getElementById('aksrt-vip-dialog');
|
||||
if (!dialog || typeof dialog.showModal !== 'function') return;
|
||||
|
||||
var form = dialog.querySelector('form');
|
||||
var ids = dialog.querySelector('[data-aksrt-vip-ids]');
|
||||
var title = dialog.querySelector('[data-aksrt-vip-title]');
|
||||
var target = dialog.querySelector('[data-aksrt-vip-target]');
|
||||
var opHint = dialog.querySelector('[data-aksrt-vip-op-hint]');
|
||||
var bulkNote = dialog.querySelector('[data-aksrt-vip-bulk-note]');
|
||||
var submit = dialog.querySelector('[data-aksrt-vip-submit]');
|
||||
var permanent = dialog.querySelector('[data-aksrt-vip-permanent]');
|
||||
var dateInput = dialog.querySelector('[data-aksrt-vip-date]');
|
||||
var levelSelect = dialog.querySelector('[data-aksrt-vip-level]');
|
||||
var radios = dialog.querySelectorAll('input[name="vip_operation"]');
|
||||
var panes = dialog.querySelectorAll('[data-aksrt-vip-pane]');
|
||||
var bulkButton = document.querySelector('[data-aksrt-vip-bulk]');
|
||||
var selection = document.querySelector('[data-aksrt-vip-selection]');
|
||||
var checkAll = document.querySelector('[data-aksrt-vip-check-all]');
|
||||
var checks = document.querySelectorAll('.aksrt-vip-check');
|
||||
|
||||
function t(key, value) {
|
||||
var text = dialog.getAttribute('data-i18n-' + key) || '';
|
||||
return value === undefined ? text : text.replace(/%[sd]/, String(value));
|
||||
}
|
||||
|
||||
function operation() {
|
||||
var checked = dialog.querySelector('input[name="vip_operation"]:checked');
|
||||
return checked ? checked.value : 'set';
|
||||
}
|
||||
|
||||
function sync() {
|
||||
var op = operation();
|
||||
panes.forEach(function (pane) {
|
||||
var on = pane.getAttribute('data-aksrt-vip-pane') === op;
|
||||
pane.hidden = !on;
|
||||
pane.querySelectorAll('input, select').forEach(function (field) { field.disabled = !on; });
|
||||
});
|
||||
if (op === 'set' && dateInput) {
|
||||
dateInput.disabled = permanent.checked;
|
||||
dateInput.required = !permanent.checked;
|
||||
}
|
||||
submit.classList.toggle('is-danger', op === 'disable');
|
||||
submit.classList.toggle('button-primary', op !== 'disable');
|
||||
}
|
||||
|
||||
function setOperation(value) {
|
||||
radios.forEach(function (radio) { radio.checked = radio.value === value; });
|
||||
sync();
|
||||
}
|
||||
|
||||
function setAvailability(row) {
|
||||
var hints = [];
|
||||
radios.forEach(function (radio) {
|
||||
var off = false;
|
||||
if (row && radio.value === 'extend' && row.getAttribute('data-can-extend') !== '1') {
|
||||
off = true;
|
||||
hints.push(t('extend-off'));
|
||||
}
|
||||
if (row && radio.value === 'restore' && row.getAttribute('data-can-restore') !== '1') {
|
||||
off = true;
|
||||
hints.push(t('restore-off'));
|
||||
}
|
||||
radio.disabled = off;
|
||||
radio.closest('label').classList.toggle('is-disabled', off);
|
||||
});
|
||||
opHint.textContent = hints.join(' ');
|
||||
opHint.hidden = hints.length === 0;
|
||||
}
|
||||
|
||||
function open(rows, bulk) {
|
||||
ids.textContent = '';
|
||||
rows.forEach(function (row) {
|
||||
var input = document.createElement('input');
|
||||
input.type = 'hidden';
|
||||
input.name = 'user_ids[]';
|
||||
input.value = row.getAttribute('data-user-id');
|
||||
ids.appendChild(input);
|
||||
});
|
||||
form.reset();
|
||||
if (bulk) {
|
||||
title.textContent = t('bulk', rows.length);
|
||||
target.textContent = rows.map(function (row) { return row.getAttribute('data-user-name'); }).slice(0, 8).join('、') + (rows.length > 8 ? ' …' : '');
|
||||
setAvailability(null);
|
||||
bulkNote.textContent = t('bulk-note');
|
||||
bulkNote.hidden = false;
|
||||
} else {
|
||||
var row = rows[0];
|
||||
title.textContent = t('single', row.getAttribute('data-user-name'));
|
||||
target.textContent = '#' + row.getAttribute('data-user-id') + ' · ' + row.getAttribute('data-current');
|
||||
if (levelSelect) levelSelect.value = row.getAttribute('data-level') || '1';
|
||||
setAvailability(row);
|
||||
bulkNote.hidden = true;
|
||||
}
|
||||
setOperation('set');
|
||||
submit.disabled = false;
|
||||
dialog.showModal();
|
||||
var reason = form.querySelector('input[name="reason"]');
|
||||
if (reason) reason.focus();
|
||||
}
|
||||
|
||||
function selected() {
|
||||
return Array.prototype.filter.call(checks, function (check) { return check.checked; });
|
||||
}
|
||||
|
||||
function syncSelection() {
|
||||
var count = selected().length;
|
||||
if (bulkButton) bulkButton.disabled = count === 0;
|
||||
if (selection) {
|
||||
selection.hidden = count === 0;
|
||||
selection.textContent = t('selected', count);
|
||||
}
|
||||
if (checkAll) {
|
||||
checkAll.checked = count > 0 && count === checks.length;
|
||||
checkAll.indeterminate = count > 0 && count < checks.length;
|
||||
}
|
||||
}
|
||||
|
||||
document.querySelectorAll('[data-aksrt-vip-edit]').forEach(function (button) {
|
||||
button.addEventListener('click', function () { open([button], false); });
|
||||
});
|
||||
if (bulkButton) {
|
||||
bulkButton.addEventListener('click', function () {
|
||||
var rows = selected();
|
||||
if (rows.length) open(rows, true);
|
||||
});
|
||||
}
|
||||
if (checkAll) {
|
||||
checkAll.addEventListener('change', function () {
|
||||
checks.forEach(function (check) { check.checked = checkAll.checked; });
|
||||
syncSelection();
|
||||
});
|
||||
}
|
||||
checks.forEach(function (check) { check.addEventListener('change', syncSelection); });
|
||||
|
||||
radios.forEach(function (radio) { radio.addEventListener('change', sync); });
|
||||
if (permanent) permanent.addEventListener('change', sync);
|
||||
|
||||
dialog.querySelectorAll('[data-aksrt-vip-close]').forEach(function (button) {
|
||||
button.addEventListener('click', function () { dialog.close(); });
|
||||
});
|
||||
// 点击遮罩(dialog 自身而非内容区)关闭。
|
||||
dialog.addEventListener('click', function (event) {
|
||||
if (event.target === dialog) dialog.close();
|
||||
});
|
||||
form.addEventListener('submit', function () {
|
||||
submit.disabled = true;
|
||||
});
|
||||
|
||||
syncSelection();
|
||||
})();
|
||||
Reference in new issue
Block a user