- read preset reasons from the real source (ban_preset_reason via UserRuntime::ban_settings); the previous injection read a nonexistent report.categories key, so backend reasons never showed - rebuild the modal on the reference markup contract: edit-header warning header, reason chips toggling the reference .active class (focus color), other-reason textarea, violation link, description, but-average modal-buts footer - add image evidence upload gated by user_report_img_s: new /report/upload-image route reusing the comment upload handler with its own permission, preview/remove UI, up to 3 images appended to the report content - render the backend report notice (user_report_desc, sanitized HTML) inside the modal - widen the dialog to the reference 600px desktop geometry and share the form-upload preview styles with the order review modal - extend the report contract test accordingly
369 lines
14 KiB
JavaScript
369 lines
14 KiB
JavaScript
/**
|
||
* AKSRT Report — 前台统一举报弹窗
|
||
*
|
||
* 监听 [data-aksrt-report] 点击,弹出对齐参考主题的举报弹窗(像素级同构):
|
||
* edit-header 红色警示头 + 举报原因徽章(active 状态走 .active.badg 焦点色)
|
||
* + 违规链接 + 详细描述 + 图片举证(后台开启 user_report_img_s 时)
|
||
* + 举报说明(后台 user_report_desc,支持 HTML)+ but-average modal-buts 底栏。
|
||
* 提交走 REST /aksrt/v1/report/submit(user/comment/post 三类目标统一入口),
|
||
* 举证图片走 /aksrt/v1/report/upload-image(最多 3 张,URL 拼入举报内容)。
|
||
*
|
||
* @package AKSRT
|
||
*/
|
||
|
||
(function () {
|
||
'use strict';
|
||
|
||
var API_ROOT = (function () {
|
||
var d = window.aksrtData || {};
|
||
var raw = d.apiUrl || d.restUrl || d.rest_url || '/index.php?rest_route=/aksrt/v1';
|
||
return String(raw).replace(/\/$/, '');
|
||
})();
|
||
var NONCE = (window.aksrtData && window.aksrtData.restNonce) || '';
|
||
|
||
var TARGET_TITLES = {
|
||
user: '举报用户',
|
||
post: '举报文章',
|
||
comment: '举报评论',
|
||
};
|
||
|
||
var MAX_IMAGES = 3;
|
||
|
||
function getReportCategories() {
|
||
// 预置原因:后台“用户&互动 → 禁封/举报 → 预置原因”(ban_preset_reason,
|
||
// 由 UserRuntime::ban_settings 解析,aksrtData.reportCategories 注入)。
|
||
var list = window.aksrtData && window.aksrtData.reportCategories;
|
||
if (Array.isArray(list)) {
|
||
var items = list
|
||
.map(function (cat) {
|
||
return {
|
||
slug: String((cat && (cat.slug || cat.value)) || ''),
|
||
name: String((cat && (cat.name || cat.label || cat.t)) || ''),
|
||
};
|
||
})
|
||
.filter(function (cat) { return cat.slug !== '' && cat.name !== ''; });
|
||
if (items.length) return items;
|
||
}
|
||
return [
|
||
{ slug: 'spam', name: '垃圾信息' },
|
||
{ slug: 'abuse', name: '人身攻击' },
|
||
{ slug: 'copyright', name: '版权投诉' },
|
||
];
|
||
}
|
||
|
||
function reportDescHtml() {
|
||
// 举报说明:后台富文本(服务端已 wp_kses_post),直接注入。
|
||
var d = window.aksrtData && window.aksrtData.report;
|
||
return (d && typeof d.desc === 'string') ? d.desc : '';
|
||
}
|
||
|
||
function imgUploadEnabled() {
|
||
var d = window.aksrtData && window.aksrtData.report;
|
||
return !!(d && d.imgEnabled);
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.replace(/</g, '<')
|
||
.replace(/>/g, '>')
|
||
.replace(/"/g, '"')
|
||
.replace(/'/g, ''');
|
||
}
|
||
|
||
function buildReportModalHtml(data) {
|
||
var categories = getReportCategories();
|
||
var title = TARGET_TITLES[data.target_type] || '举报不良信息';
|
||
|
||
var html = '';
|
||
|
||
// 弹窗头:参考主题 edit-header 同构(关闭钮 + 红色警示徽标 + 标题)。
|
||
html += '<div class="mb10 edit-header touch">';
|
||
html += '<button type="button" class="close" data-aksrt-modal-close aria-label="关闭"><i class="fa fa-times" aria-hidden="true"></i></button>';
|
||
html += '<b class="flex ac c-red"><span class="toggle-radius mr10 c-red"><i class="fa fa-exclamation-triangle" aria-hidden="true"></i></span>' + escapeHtml(title) + '</b>';
|
||
html += '</div>';
|
||
|
||
html += '<form class="aksrt-report-form dependency-box">';
|
||
html += '<div class="scroll-y mini-scrollbar max-vh7">';
|
||
html += '<div class="mt10 mb20">';
|
||
|
||
// 被举报对象(头像 + 名称),由服务端渲染进 data-aksrt-report。
|
||
if (data.target_avatar || data.target_name) {
|
||
html += '<div class="flex ac mb6">';
|
||
if (data.target_avatar) {
|
||
html += '<img class="avatar-mini mr6" src="' + escapeHtml(data.target_avatar) + '" alt="">';
|
||
}
|
||
html += '<b>' + escapeHtml(data.target_name || '') + '</b>';
|
||
html += '</div>';
|
||
}
|
||
html += '</div>';
|
||
|
||
// 举报原因(徽章单选,active 走参考主题焦点色;含“其它原因”展开自定义文本)。
|
||
html += '<div class="form-radio mb20 form-but-radio">';
|
||
html += '<div class="em09 muted-2-color mb6">举报原因</div>';
|
||
categories.forEach(function (cat) {
|
||
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-report-reason="' + escapeHtml(cat.name) + '" data-reason-slug="' + escapeHtml(cat.slug) + '">' + escapeHtml(cat.name) + '</span>';
|
||
});
|
||
html += '<span class="mr6 mb6 badg p2-10 pointer" data-aksrt-report-reason="other">其它原因</span>';
|
||
html += '<div class="aksrt-report-other mb20" style="display: none;">';
|
||
html += '<textarea class="form-control" name="reason_other" rows="2" placeholder="请填写举报原因"></textarea>';
|
||
html += '</div>';
|
||
html += '</div>';
|
||
|
||
// 违规链接。
|
||
html += '<div class="mb20">';
|
||
html += '<div class="em09 muted-2-color mb6">违规链接</div>';
|
||
html += '<input type="text" class="form-control" name="report_url" placeholder="请输入违规链接">';
|
||
html += '</div>';
|
||
|
||
// 详细描述。
|
||
html += '<div class="mb20">';
|
||
html += '<div class="em09 muted-2-color mb6">详细描述</div>';
|
||
html += '<textarea class="form-control" name="report_desc" rows="2" placeholder="请详细描述违规事项"></textarea>';
|
||
html += '</div>';
|
||
|
||
// 图片举证(后台 user_report_img_s 开启时渲染,最多 3 张)。
|
||
if (imgUploadEnabled()) {
|
||
html += '<div class="form-upload mb10">';
|
||
html += '<div class="em09 muted-2-color mb6">添加图片举证</div>';
|
||
html += '<div class="preview">';
|
||
html += '<div class="add" data-aksrt-report-add role="button" aria-label="添加图片举证"></div>';
|
||
html += '</div>';
|
||
html += '<input class="hide" type="file" accept="image/gif,image/jpeg,image/jpg,image/png" data-aksrt-report-file>';
|
||
html += '</div>';
|
||
}
|
||
|
||
// 举报说明(后台 user_report_desc,支持 HTML)。
|
||
var desc = reportDescHtml();
|
||
if (desc) {
|
||
html += '<div class="em09 padding-h10 muted-box mb10">' + desc + '</div>';
|
||
}
|
||
|
||
html += '</div>';
|
||
|
||
// 底栏:参考主题 but-average modal-buts 双按钮。
|
||
html += '<div class="but-average modal-buts">';
|
||
html += '<button type="button" class="but" data-aksrt-modal-close>取消</button>';
|
||
html += '<button type="button" class="but c-blue" data-aksrt-report-submit><i class="fa fa-check" aria-hidden="true"></i>确认提交</button>';
|
||
html += '</div>';
|
||
html += '</form>';
|
||
|
||
return html;
|
||
}
|
||
|
||
function selectedReason(shell) {
|
||
var active = shell.querySelector('[data-aksrt-report-reason].active');
|
||
if (!active) return { label: '', slug: '' };
|
||
return {
|
||
label: active.getAttribute('data-aksrt-report-reason') || '',
|
||
slug: active.getAttribute('data-reason-slug') || '',
|
||
};
|
||
}
|
||
|
||
function collectImages(shell) {
|
||
return Array.prototype.map.call(
|
||
shell.querySelectorAll('.preview .preview-item img'),
|
||
function (img) { return img.getAttribute('src') || ''; }
|
||
).filter(function (url) { return url !== ''; });
|
||
}
|
||
|
||
function uploadImage(shell, file) {
|
||
var preview = shell.querySelector('.form-upload .preview');
|
||
if (!preview) return Promise.resolve();
|
||
|
||
var existing = shell.querySelectorAll('.preview .preview-item').length;
|
||
if (existing + collectImages(shell).length >= MAX_IMAGES) {
|
||
notyf('最多上传 ' + MAX_IMAGES + ' 张图片', 'danger');
|
||
return Promise.resolve();
|
||
}
|
||
|
||
var body = new FormData();
|
||
body.append('image', file);
|
||
|
||
return window.fetch(API_ROOT + '/report/upload-image', {
|
||
method: 'POST',
|
||
headers: { 'X-WP-Nonce': NONCE },
|
||
body: body,
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (result) {
|
||
var url = result && result.success && result.data && result.data.url;
|
||
if (!url) {
|
||
notyf((result && result.message) || '图片上传失败', 'danger');
|
||
return;
|
||
}
|
||
var item = document.createElement('div');
|
||
item.className = 'preview-item';
|
||
var img = document.createElement('img');
|
||
img.src = url;
|
||
img.alt = '';
|
||
var remove = document.createElement('button');
|
||
remove.type = 'button';
|
||
remove.className = 'remove';
|
||
remove.setAttribute('aria-label', '删除图片');
|
||
remove.innerHTML = '×';
|
||
remove.addEventListener('click', function () { item.remove(); });
|
||
item.appendChild(img);
|
||
item.appendChild(remove);
|
||
preview.insertBefore(item, preview.querySelector('.add'));
|
||
})
|
||
.catch(function () {
|
||
notyf('图片上传失败,请重试', 'danger');
|
||
});
|
||
}
|
||
|
||
function submitReport(data, shell) {
|
||
var form = shell.querySelector('.aksrt-report-form');
|
||
if (!form) return;
|
||
|
||
var submitBtn = form.querySelector('[data-aksrt-report-submit]');
|
||
var descInput = form.querySelector('[name="report_desc"]');
|
||
var otherInput = form.querySelector('[name="reason_other"]');
|
||
var urlInput = form.querySelector('[name="report_url"]');
|
||
|
||
var selected = selectedReason(shell);
|
||
var other = (otherInput && otherInput.value || '').trim();
|
||
|
||
if (!selected.label && !other) {
|
||
notyf('请选择或输入举报原因', 'danger');
|
||
return;
|
||
}
|
||
|
||
// “其它原因”以自定义文本为举报原因;预置徽章取徽章文案(参考主题同语义)。
|
||
var reason = selected.label === 'other' || !selected.label ? other : selected.label;
|
||
var slug = selected.label && selected.label !== 'other' ? selected.slug : '';
|
||
|
||
if (!reason) {
|
||
notyf('请填写举报原因', 'danger');
|
||
return;
|
||
}
|
||
|
||
var link = (urlInput && urlInput.value || '').trim();
|
||
if (link) {
|
||
reason += (reason ? '\n' : '') + '违规链接:' + link;
|
||
}
|
||
var images = collectImages(shell);
|
||
if (images.length) {
|
||
reason += '\n图片举证:';
|
||
images.forEach(function (url) { reason += '\n' + url; });
|
||
}
|
||
|
||
submitBtn.disabled = true;
|
||
notyf('正在提交举报…', 'load', 3000, 'aksrt-report-submit');
|
||
|
||
window.fetch(API_ROOT + '/report/submit', {
|
||
method: 'POST',
|
||
headers: {
|
||
'Content-Type': 'application/json',
|
||
'X-WP-Nonce': NONCE,
|
||
},
|
||
body: JSON.stringify({
|
||
target_type: data.target_type,
|
||
target_id: data.target_id,
|
||
category: slug,
|
||
reason: reason,
|
||
}),
|
||
})
|
||
.then(function (res) { return res.json(); })
|
||
.then(function (result) {
|
||
if (result && result.success) {
|
||
window.AksrtModalRuntime.close('submit');
|
||
notyf(result.message || '举报已提交,我们会尽快处理', 'success');
|
||
} else {
|
||
submitBtn.disabled = false;
|
||
notyf((result && result.message) || '举报提交失败', 'danger');
|
||
}
|
||
})
|
||
.catch(function () {
|
||
submitBtn.disabled = false;
|
||
notyf('网络错误,请重试', 'danger');
|
||
})
|
||
.then(function () {
|
||
var toast = document.getElementById('aksrt-report-submit');
|
||
if (toast) toast.remove();
|
||
});
|
||
}
|
||
|
||
function bindReportModal(shell) {
|
||
shell.addEventListener('click', function (event) {
|
||
// 原因徽章:单选 active(参考主题 .active.badg 焦点色),其它原因展开文本框。
|
||
// 参考主题行为:重复点击已选徽章保持选中(先全清再激活),不做反选。
|
||
var chip = event.target.closest('[data-aksrt-report-reason]');
|
||
if (chip) {
|
||
event.preventDefault();
|
||
shell.querySelectorAll('[data-aksrt-report-reason].active').forEach(function (el) {
|
||
el.classList.remove('active');
|
||
});
|
||
chip.classList.add('active');
|
||
var otherBox = shell.querySelector('.aksrt-report-other');
|
||
if (otherBox) otherBox.style.display = chip.getAttribute('data-aksrt-report-reason') === 'other' ? '' : 'none';
|
||
return;
|
||
}
|
||
|
||
// 图片举证:点击加号或预览补位触发文件选择。
|
||
var addBtn = event.target.closest('[data-aksrt-report-add]');
|
||
if (addBtn) {
|
||
event.preventDefault();
|
||
var fileInput = shell.querySelector('[data-aksrt-report-file]');
|
||
if (fileInput) fileInput.click();
|
||
return;
|
||
}
|
||
|
||
var submitBtn = event.target.closest('[data-aksrt-report-submit]');
|
||
if (submitBtn) {
|
||
event.preventDefault();
|
||
submitReport(shell.getAttribute('data-aksrt-report-target') ? JSON.parse(shell.getAttribute('data-aksrt-report-target')) : {}, shell);
|
||
}
|
||
});
|
||
|
||
shell.addEventListener('change', function (event) {
|
||
var fileInput = event.target.closest('[data-aksrt-report-file]');
|
||
if (!fileInput || !fileInput.files || !fileInput.files.length) return;
|
||
Array.prototype.forEach.call(fileInput.files, function (file) {
|
||
uploadImage(shell, file);
|
||
});
|
||
fileInput.value = '';
|
||
});
|
||
}
|
||
|
||
function openReportModal(data) {
|
||
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.open !== 'function') {
|
||
notyf('举报组件加载失败,请刷新页面后重试', 'danger');
|
||
return;
|
||
}
|
||
|
||
var shell = window.AksrtModalRuntime.open({
|
||
html: buildReportModalHtml(data),
|
||
className: 'aksrt-report-modal',
|
||
kind: 'report',
|
||
mobileBottom: true,
|
||
});
|
||
shell.setAttribute('data-aksrt-report-target', JSON.stringify({
|
||
target_type: data.target_type,
|
||
target_id: data.target_id,
|
||
}));
|
||
bindReportModal(shell);
|
||
}
|
||
|
||
function initReportButtons() {
|
||
if (document.documentElement.dataset.aksrtReportInitialized === '1') return;
|
||
document.documentElement.dataset.aksrtReportInitialized = '1';
|
||
document.addEventListener('click', function (e) {
|
||
var btn = e.target.closest('[data-aksrt-report]');
|
||
if (!btn) return;
|
||
e.preventDefault();
|
||
|
||
try {
|
||
var data = JSON.parse(btn.getAttribute('data-aksrt-report'));
|
||
if (data && data.target_type && data.target_id) {
|
||
openReportModal(data);
|
||
}
|
||
} catch {
|
||
// invalid data attribute
|
||
}
|
||
});
|
||
}
|
||
|
||
jQuery(document).off('auto_fun.aksrt-report').on('auto_fun.aksrt-report', initReportButtons);
|
||
})();
|