feat(admin): upgrade invite code management with dialogs

Replace standalone add and export views with accessible dialogs, refine list interactions, and remove obsolete paths and unused counts. Fix filtered totals and reject oversized imports without truncation, with regression coverage.

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
This commit is contained in:
kate522andClaude Fable 5 committed 2026-10-30 18:53:01 +08:00
1 parent 1cd19434d1
commit a6776ffe0a
8 files changed
+884 -400

No files matched your search

+158
View File
@@ -0,0 +1,158 @@
/** 邀请码管理:弹窗、条件表单、复制与批量选择。仅本页入队。 */
(function () {
'use strict';
var root = document.querySelector('.aksrt-inv-page');
if (!root) return;
var toast = root.querySelector('[data-aksrt-inv-toast]');
var toastTimer;
var add = root.querySelector('#aksrt-inv-add');
var addForm = add.querySelector('form');
var addSubmit = add.querySelector('[data-aksrt-inv-add-submit]');
var importing = addForm.elements.import_data;
function notify(key) {
clearTimeout(toastTimer);
toast.textContent = toast.getAttribute('data-' + key);
toast.classList.add('is-visible');
toastTimer = setTimeout(function () { toast.classList.remove('is-visible'); }, 4000);
}
function syncForm(form) {
form.querySelectorAll('[data-aksrt-inv-dep]').forEach(function (pane) {
var parts = pane.getAttribute('data-aksrt-inv-dep').split('=');
var field = form.querySelector('[name="' + parts[0] + '"]:checked') || form.querySelector('[name="' + parts[0] + '"]');
var value = field && field.type === 'checkbox' ? (field.checked ? field.value : '') : field && field.value;
var active = !!field && !field.matches(':disabled') && parts[1].split('|').includes(value);
pane.hidden = !active;
pane.disabled = !active;
});
if (form === addForm && !form.dataset.submitting) {
addSubmit.textContent = addSubmit.getAttribute(form.elements.add_type.value === 'import' ? 'data-import-label' : 'data-auto-label');
}
}
root.querySelectorAll('.aksrt-inv-dialog').forEach(function (dialog) {
var opener;
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;
}
root.querySelectorAll('[data-aksrt-dialog-open="' + dialog.id + '"]').forEach(function (button) {
button.addEventListener('click', function () {
opener = button;
syncForm(dialog.querySelector('form'));
dialog.showModal();
document.documentElement.classList.add('aksrt-inv-modal-open');
});
});
dialog.querySelectorAll('[data-aksrt-dialog-close]').forEach(function (button) {
button.addEventListener('click', function () { dialog.close(); });
});
dialog.addEventListener('pointerdown', function (event) {
startedOutside = event.target === dialog && outside(event);
});
dialog.addEventListener('click', function (event) {
if (startedOutside && event.target === dialog && outside(event)) dialog.close();
startedOutside = false;
});
dialog.addEventListener('close', function () {
document.documentElement.classList.toggle('aksrt-inv-modal-open', !!root.querySelector('dialog[open]'));
if (opener && opener.isConnected) opener.focus();
});
var form = dialog.querySelector('form');
form.addEventListener('change', function () { syncForm(form); });
syncForm(form);
});
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) {
var range = document.createRange();
range.selectNodeContents(button);
var selection = window.getSelection();
selection.removeAllRanges();
selection.addRange(range);
notify('copy-failed');
}
});
addForm.addEventListener('submit', function (event) {
if (addForm.dataset.submitting) {
event.preventDefault();
return;
}
addForm.dataset.submitting = '1';
addSubmit.disabled = true;
addSubmit.textContent = addSubmit.getAttribute('data-busy-label');
});
importing.addEventListener('input', function () {
var count = importing.value.split(/\r\n|\r|\n/).filter(function (line) { return line.trim() !== ''; }).length;
importing.setCustomValidity(count > 1000 ? importing.getAttribute('data-limit-error') : '');
});
root.querySelector('#aksrt-inv-export form').addEventListener('submit', function () {
root.querySelector('#aksrt-inv-export').close();
notify('exported');
});
var list = root.querySelector('[data-aksrt-inv-list]');
var checks = Array.from(list.querySelectorAll('[name="action_id[]"]'));
var checkAll = list.querySelector('[data-aksrt-inv-check-all]');
var selection = list.querySelector('[data-aksrt-inv-selection]');
var bulk = list.querySelector('[data-aksrt-inv-bulk]');
function syncSelection() {
var count = checks.filter(function (check) { return check.checked; }).length;
selection.textContent = selection.getAttribute('data-label').replace('%d', String(count));
checkAll.checked = count > 0 && count === checks.length;
checkAll.indeterminate = count > 0 && count < checks.length;
checkAll.disabled = checks.length === 0;
bulk.disabled = !!list.dataset.submitting || count === 0 || list.elements.bulk_action.value !== 'delete';
checks.forEach(function (check) { check.closest('tr').classList.toggle('is-selected', check.checked); });
return count;
}
checkAll.addEventListener('change', function () {
checks.forEach(function (check) { check.checked = checkAll.checked; });
syncSelection();
});
list.addEventListener('change', syncSelection);
list.addEventListener('submit', function (event) {
var count = syncSelection();
if (list.dataset.submitting || bulk.disabled || !window.confirm(list.getAttribute('data-confirm').replace('%d', String(count)))) {
event.preventDefault();
return;
}
list.dataset.submitting = '1';
bulk.disabled = true;
});
window.addEventListener('pageshow', function () {
delete addForm.dataset.submitting;
delete list.dataset.submitting;
addSubmit.disabled = false;
syncForm(addForm);
syncSelection();
document.documentElement.classList.toggle('aksrt-inv-modal-open', !!root.querySelector('dialog[open]'));
});
syncSelection();
})();