Files
wordpress-theme-aksrt/assets/js/report.js
T
kate522 0df6e760e3 feat: full parity pass - suspend enforcement, report modal polish, wechat notify
- Restriction: add is_suspended/get_active_suspend; comment block now covers
  suspend (小黑屋) in addition to mute, with reason in the deny message
- FrontSubmitSave::persist: block post/draft/edit submission for suspended
  users (reference-theme semantics: 小黑屋 loses all publishing rights)
- report.js: close icon switched from fa-times to the SVG sprite ic-close
  (#icon-close), same as reference theme; textareas gain autoHeight growth
  (hidden 'other reason' box skips the first measurement)
- WeChatNotify: NOTIFY_TYPES now includes report/system so report results
  and account restriction notices fan out to WeChat template messages;
  generic title/description fallback for template values
- contract tests: +3 tests (suspend enforcement, modal polish, wechat types)
2026-09-20 16:09:12 +08:00

386 lines
15 KiB
JavaScript
Raw 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.
/**
* 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.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, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
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="关闭"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></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 = '&times;';
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 bindAutoHeight(textarea) {
// 对齐参考主题 autoHeight 行为:随内容自动增高,最小保持初始行数高度。
// 隐藏中的 textarea(如“其它原因”)跳过首次计算,待可见后按实际高度增长。
if (!textarea || textarea.dataset.aksrtAutoHeight === '1') return;
textarea.dataset.aksrtAutoHeight = '1';
var minHeight = 0;
var grow = function () {
if (!minHeight) minHeight = textarea.offsetHeight || 0;
textarea.style.height = 'auto';
textarea.style.height = Math.max(textarea.scrollHeight, minHeight) + 'px';
};
textarea.addEventListener('input', grow);
if (textarea.offsetHeight > 0) grow();
}
function bindReportModal(shell) {
shell.querySelectorAll('textarea.form-control').forEach(bindAutoHeight);
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);
})();