Files
mce/templates/admin/static/js/audit.js
Victor_Jay 7d944cfa35 fix: 修复更名退款逻辑、审核列表显示及更名消耗确认提示
- 首次更名审核被拒不再退款,首次免费未扣费不产生退款
- 审核列表提交者列改用 UID,当前用户名列显示当前用户名
- 更名消耗提示改为确认弹窗,首次/非首次动态显示不同文案
2026-06-02 22:09:41 +08:00

306 lines
11 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

/* =====================================================
MetaLab 管理面板 - 审核管理 JS
单一职责:审核列表 TAB 切换、分页、审批操作
===================================================== */
let currentTab = 'username';
const pageSize = 20;
const pages = {
username: 1,
avatar: 1,
bio: 1,
history: 1
};
let pendingRejectId = null;
// 审核操作按钮事件委托(避免 inline onclick
function handleAuditClick(e) {
const btn = e.target.closest('[data-audit-action]');
if (!btn) return;
const action = btn.dataset.auditAction;
const id = parseInt(btn.dataset.auditId);
if (action === 'approve') approveAudit(id);
else if (action === 'reject') openRejectModal(id);
}
// 分页按钮事件委托
function handlePaginationClick(e) {
const btn = e.target.closest('[data-go-page]');
if (!btn) return;
goPage(parseInt(btn.dataset.goPage));
}
// 为动态插入的图片绑定 error 处理
function attachImageErrorHandlers(container) {
if (!container) return;
container.querySelectorAll('img[data-fallback]').forEach(img => {
img.addEventListener('error', function handler() {
this.src = 'data:image/svg+xml,' + encodeURIComponent('<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 80 80"><rect fill="#eee" width="80" height="80"/><text x="40" y="45" text-anchor="middle" fill="#999" font-size="12">加载失败</text></svg>');
this.removeEventListener('error', handler);
});
});
}
document.addEventListener('DOMContentLoaded', () => {
// TAB 切换
document.querySelectorAll('.audit-tab').forEach(tab => {
tab.addEventListener('click', () => {
document.querySelectorAll('.audit-tab').forEach(t => t.classList.remove('active'));
tab.classList.add('active');
currentTab = tab.dataset.tab;
document.querySelectorAll('.audit-panel').forEach(p => p.classList.remove('active'));
document.getElementById('panel-' + currentTab).classList.add('active');
loadCurrentTab();
});
});
// 全局事件委托:审核操作 & 分页
document.addEventListener('click', handleAuditClick);
document.addEventListener('click', handlePaginationClick);
// 初始加载
loadCurrentTab();
});
/* =========================================
数据加载
========================================= */
function loadCurrentTab() {
if (currentTab === 'history') {
loadHistory();
} else if (currentTab === 'avatar') {
loadAvatarAudits();
} else {
loadTableAudits();
}
}
function loadTableAudits() {
const page = pages[currentTab];
api.get(`/api/admin/audits?type=${currentTab}&page=${page}&page_size=${pageSize}`)
.then(res => {
if (res.success) {
renderTable(res.data.items, currentTab + 'TableBody');
renderPagination(res.data.total, res.data.page, currentTab + 'Pagination');
} else {
showToast(res.message || '加载失败', 'error');
}
})
.catch((e) => {
console.error('loadTableAudits error:', e);
showToast('加载失败: ' + (e.message || '网络错误'), 'error');
});
}
function loadAvatarAudits() {
const page = pages['avatar'];
api.get(`/api/admin/audits?type=avatar&page=${page}&page_size=${pageSize}`)
.then(res => {
if (res.success) {
renderAvatarCards(res.data.items);
renderPagination(res.data.total, res.data.page, 'avatarPagination');
} else {
showToast(res.message || '加载失败', 'error');
}
})
.catch((e) => {
console.error('loadAvatarAudits error:', e);
showToast('加载失败: ' + (e.message || '网络错误'), 'error');
});
}
function loadHistory() {
const page = pages['history'];
api.get(`/api/admin/audits?page=${page}&page_size=${pageSize}&status=`)
.then(res => {
if (res.success) {
renderHistoryTable(res.data.items);
renderPagination(res.data.total, res.data.page, 'historyPagination');
} else {
showToast(res.message || '加载失败', 'error');
}
})
.catch((e) => {
console.error('loadHistory error:', e);
showToast('加载失败: ' + (e.message || '网络错误'), 'error');
});
}
/* =========================================
表格渲染(用户名/签名)
========================================= */
function renderTable(items, bodyId) {
const tbody = document.getElementById(bodyId);
if (!items || items.length === 0) {
const cols = currentTab === 'bio' ? 4 : 5;
tbody.innerHTML = `<tr><td colspan="${cols}" style="text-align:center;padding:40px;color:#636e72;">没有待审核记录</td></tr>`;
return;
}
tbody.innerHTML = items.map(item => {
if (currentTab === 'bio') {
return renderBioRow(item);
}
return renderUsernameRow(item);
}).join('');
}
function renderUsernameRow(item) {
return `
<tr>
<td>${item.user_id || '—'}</td>
<td>${escapeHtml(item.current_username || '—')}</td>
<td><strong>${escapeHtml(item.new_value)}</strong></td>
<td>${formatTime(item.created_at)}</td>
<td class="col-actions">
<button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</td>
</tr>`;
}
function renderBioRow(item) {
const text = item.new_value || '';
const display = text.length > 40 ? text.substring(0, 40) + '...' : text;
return `
<tr>
<td>${item.user_id || '—'}</td>
<td title="${escapeHtml(text)}">${escapeHtml(display)}</td>
<td>${formatTime(item.created_at)}</td>
<td class="col-actions">
<button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</td>
</tr>`;
}
/* =========================================
头像卡片渲染
========================================= */
function renderAvatarCards(items) {
const grid = document.getElementById('avatarGrid');
if (!items || items.length === 0) {
grid.innerHTML = '<div class="table-loading-full">没有待审核头像</div>';
return;
}
grid.innerHTML = items.map(item => `
<div class="avatar-audit-card">
<div class="card-header">
<span>提交者: <strong>${item.user_id || '—'}</strong></span>
<span>${formatTime(item.created_at)}</span>
</div>
<div class="avatar-compare">
<span>新头像 →</span>
<img src="${escapeHtml(item.new_value)}" alt="新头像" data-fallback="true">
</div>
<div class="card-actions">
<button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</div>
</div>
`).join('');
// 为动态插入的图片绑定 error 处理器CSP 不允许 inline onerror
attachImageErrorHandlers(grid);
}
/* =========================================
审核记录渲染
========================================= */
function renderHistoryTable(items) {
const tbody = document.getElementById('historyTableBody');
if (!items || items.length === 0) {
tbody.innerHTML = '<tr><td colspan="6" style="text-align:center;padding:40px;color:#636e72;">没有审核记录</td></tr>';
return;
}
tbody.innerHTML = items.map(item => {
const typeLabel = window.__AUDIT_TYPES[item.audit_type] || item.audit_type;
const statusLabel = window.__AUDIT_STATUS[item.status] || item.status;
const statusClass = item.status;
const content = item.new_value.length > 40 ? item.new_value.substring(0, 40) + '...' : item.new_value;
const reviewer = item.reviewer_name || '—';
return `
<tr>
<td>${typeLabel}</td>
<td>${item.user_id || '—'}</td>
<td>${escapeHtml(content)}</td>
<td><span class="audit-status-tag ${statusClass}">${statusLabel}</span></td>
<td>${escapeHtml(reviewer)}</td>
<td>${formatTime(item.created_at)}</td>
</tr>`;
}).join('');
}
/* =========================================
审核操作
========================================= */
async function approveAudit(id) {
const ok = await showConfirm('通过审核', '确定要通过此审核吗?通过后将立即生效。');
if (!ok) return;
const res = await api.post('/api/admin/audits/' + id + '/approve');
if (res.success) {
showToast(res.message, 'success');
loadCurrentTab();
} else {
showToast(res.message || '操作失败', 'error');
}
}
function openRejectModal(id) {
pendingRejectId = id;
document.getElementById('rejectReason').value = '';
document.getElementById('rejectModal').style.display = 'flex';
}
function closeRejectModal() {
pendingRejectId = null;
document.getElementById('rejectModal').style.display = 'none';
}
document.getElementById('closeRejectModalBtn').addEventListener('click', closeRejectModal);
document.getElementById('confirmRejectBtn').addEventListener('click', async () => {
const reason = document.getElementById('rejectReason').value.trim();
if (!reason) {
showToast('请填写拒绝理由', 'error');
return;
}
const res = await api.post('/api/admin/audits/' + pendingRejectId + '/reject', { reason });
if (res.success) {
showToast(res.message, 'success');
closeRejectModal();
loadCurrentTab();
} else {
showToast(res.message || '操作失败', 'error');
}
});
// 点击遮罩关闭弹窗
document.getElementById('rejectModal').addEventListener('click', (e) => {
if (e.target === e.currentTarget) closeRejectModal();
});
/* =========================================
分页
========================================= */
function renderPagination(total, page, barId) {
const totalPages = Math.ceil(total / pageSize);
const bar = document.getElementById(barId);
if (totalPages <= 1) { bar.innerHTML = ''; return; }
let html = `<button data-go-page="${page - 1}" ${page <= 1 ? 'disabled' : ''}>上一页</button>`;
html += `<span class="page-current">第 ${page} / ${totalPages} 页(共 ${total} 条)</span>`;
html += `<button data-go-page="${page + 1}" ${page >= totalPages ? 'disabled' : ''}>下一页</button>`;
bar.innerHTML = html;
}
function goPage(p) {
pages[currentTab] = p;
loadCurrentTab();
window.scrollTo({ top: 0 });
}