fix: 修复审核管理页面 CSP 违规 — inline onclick/onerror 移入事件委托

- 审核操作按钮(通过/拒绝)从 onclick 替换为 data-audit-action 属性 + document 级事件委托
- 分页按钮从 onclick 替换为 data-go-page 属性 + 事件委托
- 头像加载失败处理从 inline onerror 替换为 data-fallback 标记 + addEventListener 绑定
This commit is contained in:
2026-06-02 21:42:20 +08:00
parent 543325105c
commit fa3b20d1bc

View File

@ -13,6 +13,34 @@ const pages = {
}; };
let pendingRejectId = null; 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', () => { document.addEventListener('DOMContentLoaded', () => {
// TAB 切换 // TAB 切换
document.querySelectorAll('.audit-tab').forEach(tab => { document.querySelectorAll('.audit-tab').forEach(tab => {
@ -26,6 +54,10 @@ document.addEventListener('DOMContentLoaded', () => {
}); });
}); });
// 全局事件委托:审核操作 & 分页
document.addEventListener('click', handleAuditClick);
document.addEventListener('click', handlePaginationClick);
// 初始加载 // 初始加载
loadCurrentTab(); loadCurrentTab();
}); });
@ -122,8 +154,8 @@ function renderUsernameRow(item) {
<td><strong>${escapeHtml(item.new_value)}</strong></td> <td><strong>${escapeHtml(item.new_value)}</strong></td>
<td>${formatTime(item.created_at)}</td> <td>${formatTime(item.created_at)}</td>
<td class="col-actions"> <td class="col-actions">
<button class="btn btn-primary btn-sm" onclick="approveAudit(${item.id})">通过</button> <button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" onclick="openRejectModal(${item.id})">拒绝</button> <button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</td> </td>
</tr>`; </tr>`;
} }
@ -137,8 +169,8 @@ function renderBioRow(item) {
<td title="${escapeHtml(text)}">${escapeHtml(display)}</td> <td title="${escapeHtml(text)}">${escapeHtml(display)}</td>
<td>${formatTime(item.created_at)}</td> <td>${formatTime(item.created_at)}</td>
<td class="col-actions"> <td class="col-actions">
<button class="btn btn-primary btn-sm" onclick="approveAudit(${item.id})">通过</button> <button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" onclick="openRejectModal(${item.id})">拒绝</button> <button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</td> </td>
</tr>`; </tr>`;
} }
@ -161,14 +193,16 @@ function renderAvatarCards(items) {
</div> </div>
<div class="avatar-compare"> <div class="avatar-compare">
<span>新头像 →</span> <span>新头像 →</span>
<img src="${escapeHtml(item.new_value)}" alt="新头像" onerror="this.src='data:image/svg+xml,<svg xmlns=%22http://www.w3.org/2000/svg%22 viewBox=%220 0 80 80%22><rect fill=%22%23eee%22 width=%2280%22 height=%2280%22/><text x=%2240%22 y=%2245%22 text-anchor=%22middle%22 fill=%22%23999%22 font-size=%2212%22>加载失败</text></svg>'"> <img src="${escapeHtml(item.new_value)}" alt="新头像" data-fallback="true">
</div> </div>
<div class="card-actions"> <div class="card-actions">
<button class="btn btn-primary btn-sm" onclick="approveAudit(${item.id})">通过</button> <button class="btn btn-primary btn-sm" data-audit-action="approve" data-audit-id="${item.id}">通过</button>
<button class="btn btn-outline btn-sm" onclick="openRejectModal(${item.id})">拒绝</button> <button class="btn btn-outline btn-sm" data-audit-action="reject" data-audit-id="${item.id}">拒绝</button>
</div> </div>
</div> </div>
`).join(''); `).join('');
// 为动态插入的图片绑定 error 处理器CSP 不允许 inline onerror
attachImageErrorHandlers(grid);
} }
/* ========================================= /* =========================================
@ -258,9 +292,9 @@ function renderPagination(total, page, barId) {
const bar = document.getElementById(barId); const bar = document.getElementById(barId);
if (totalPages <= 1) { bar.innerHTML = ''; return; } if (totalPages <= 1) { bar.innerHTML = ''; return; }
let html = `<button onclick="goPage(${page - 1})" ${page <= 1 ? 'disabled' : ''}>上一页</button>`; let html = `<button data-go-page="${page - 1}" ${page <= 1 ? 'disabled' : ''}>上一页</button>`;
html += `<span class="page-current">第 ${page} / ${totalPages} 页(共 ${total} 条)</span>`; html += `<span class="page-current">第 ${page} / ${totalPages} 页(共 ${total} 条)</span>`;
html += `<button onclick="goPage(${page + 1})" ${page >= totalPages ? 'disabled' : ''}>下一页</button>`; html += `<button data-go-page="${page + 1}" ${page >= totalPages ? 'disabled' : ''}>下一页</button>`;
bar.innerHTML = html; bar.innerHTML = html;
} }