diff --git a/assets/css/build/public.css b/assets/css/build/public.css index 50256f65..3aadba99 100644 --- a/assets/css/build/public.css +++ b/assets/css/build/public.css @@ -2504,6 +2504,11 @@ main.container, --bs-gutter-y: 0; } +/* Header multifunc icon cards must align with the main content edges: + * the global .container is padding-free at >=992px (dual-column float + * math), so the card container gets no extra gutter either. The + * <=991.98px media rule below restores 15px for every .container. */ + /* Header multifunc icon cards: pin typography baseline so cards render * identically on all pages. .px12-sm is only defined globally in * legacy-utilities.css, which is @import'd by the single-post aggregator; diff --git a/assets/js/report.js b/assets/js/report.js index 4bb3d6d9..8030d6d2 100644 --- a/assets/js/report.js +++ b/assets/js/report.js @@ -1,7 +1,9 @@ /** - * AKSRT Report — 前台举报提交 + * AKSRT Report — 前台统一举报弹窗 * - * 监听 [data-aksrt-report] 按钮点击,弹出举报表单并提交。 + * 监听 [data-aksrt-report] 点击,弹出对齐参考主题格式的举报弹窗: + * 红色警示头 + 举报原因徽章(含“其它原因”)+ 违规链接 + 详细描述 + 取消/确认提交。 + * 提交走 REST /aksrt/v1/report/submit(user/comment/post 三类目标统一入口)。 * * @package AKSRT */ @@ -14,130 +16,218 @@ 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 NONCE = (window.aksrtData && window.aksrtData.restNonce) || ''; + + var TARGET_TITLES = { + user: '举报用户', + post: '举报文章', + comment: '举报评论', + }; function getReportCategories() { - // Default categories; can be overridden by server-rendered config - if (window.aksrtReportCategories && Array.isArray(window.aksrtReportCategories)) { - return window.aksrtReportCategories; + // 预置原因:后台“用户&互动 → 禁封/举报”配置(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)) || ''), + label: String((cat && (cat.label || cat.name)) || ''), + }; + }) + .filter(function (cat) { return cat.slug !== '' && cat.label !== ''; }); + if (items.length) return items; } return [ - { value: 'spam', label: '垃圾信息' }, - { value: 'abuse', label: '人身攻击' }, - { value: 'copyright', label: '版权投诉' }, - { value: 'other', label: '其他' }, + { slug: 'spam', label: '垃圾信息' }, + { slug: 'abuse', label: '人身攻击' }, + { slug: 'copyright', label: '版权投诉' }, ]; } - function createReportModal(data) { - // Remove existing modal if any - var existing = document.getElementById('aksrt-report-modal'); - if (existing) existing.remove(); + function escapeHtml(value) { + return String(value == null ? '' : value) + .replace(/&/g, '&') + .replace(//g, '>') + .replace(/"/g, '"') + .replace(/'/g, '''); + } + function buildReportModalHtml(data) { var categories = getReportCategories(); + var title = TARGET_TITLES[data.target_type] || '举报不良信息'; - var overlay = document.createElement('div'); - overlay.id = 'aksrt-report-modal'; - overlay.style.cssText = 'position:fixed;top:0;left:0;width:100%;height:100%;background:rgba(0,0,0,0.5);z-index:9999;display:flex;align-items:center;justify-content:center;'; + var html = ''; - var modal = document.createElement('div'); - modal.style.cssText = 'background:#fff;border-radius:8px;padding:24px;max-width:420px;width:90%;box-shadow:0 4px 20px rgba(0,0,0,0.15);'; + // 弹窗头:参考主题红色警示样式。 + html += '
'; + html += '' + escapeHtml(title) + ''; + html += ''; + html += '
'; - var title = document.createElement('h3'); - title.textContent = '提交举报'; - title.style.cssText = 'margin:0 0 16px;font-size:16px;font-weight:600;'; - modal.appendChild(title); + html += '
'; + html += '
'; - // Category select - var catLabel = document.createElement('label'); - catLabel.textContent = '举报分类'; - catLabel.style.cssText = 'display:block;margin-bottom:4px;font-size:13px;color:#555;'; - modal.appendChild(catLabel); + // 被举报对象(头像 + 名称),由服务端渲染进 data-aksrt-report。 + if (data.target_avatar || data.target_name) { + html += '
'; + if (data.target_avatar) { + html += ''; + } + html += '' + escapeHtml(data.target_name || '') + ''; + html += '
'; + } - var catSelect = document.createElement('select'); - catSelect.style.cssText = 'width:100%;padding:8px;border:1px solid #ddd;border-radius:4px;margin-bottom:12px;'; + // 举报原因(徽章单选,含“其它原因”)。 + html += '
'; + html += '
举报原因
'; + html += '
'; categories.forEach(function (cat) { - var opt = document.createElement('option'); - opt.value = cat.value; - opt.textContent = cat.label; - catSelect.appendChild(opt); + html += ''; }); - modal.appendChild(catSelect); + html += ''; + html += '
'; + html += ''; + html += '
'; - // Reason textarea - var reasonLabel = document.createElement('label'); - reasonLabel.textContent = '举报原因'; - reasonLabel.style.cssText = 'display:block;margin-bottom:4px;font-size:13px;color:#555;'; - modal.appendChild(reasonLabel); + // 违规链接。 + html += '
'; + html += '
违规链接
'; + html += ''; + html += '
'; - var reasonInput = document.createElement('textarea'); - reasonInput.placeholder = '请详细描述举报原因…'; - reasonInput.style.cssText = 'width:100%;height:80px;padding:8px;border:1px solid #ddd;border-radius:4px;resize:vertical;margin-bottom:16px;box-sizing:border-box;'; - modal.appendChild(reasonInput); + // 详细描述。 + html += '
'; + html += '
详细描述
'; + html += ''; + html += '
'; - // Buttons - var btnRow = document.createElement('div'); - btnRow.style.cssText = 'display:flex;gap:8px;justify-content:flex-end;'; + html += '
'; - var cancelBtn = document.createElement('button'); - cancelBtn.type = 'button'; - cancelBtn.textContent = '取消'; - cancelBtn.style.cssText = 'padding:8px 16px;border:1px solid #ddd;border-radius:4px;background:#fff;cursor:pointer;'; - cancelBtn.onclick = function () { overlay.remove(); }; - btnRow.appendChild(cancelBtn); + // 底栏:参考主题 but-average modal-buts 双按钮。 + html += ''; + html += '
'; - var submitBtn = document.createElement('button'); - submitBtn.type = 'button'; - submitBtn.textContent = '提交举报'; - submitBtn.style.cssText = 'padding:8px 16px;border:none;border-radius:4px;background:#e74c3c;color:#fff;cursor:pointer;'; - submitBtn.onclick = function () { - var reason = reasonInput.value.trim(); - if (!reason) { - reasonInput.style.borderColor = '#e74c3c'; - reasonInput.focus(); + return html; + } + + function selectedReason(shell) { + var active = shell.querySelector('[data-report-reason].is-active'); + return active ? active.getAttribute('data-report-reason') : ''; + } + + function submitReport(session, 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 slug = selectedReason(shell); + var reason = (descInput && descInput.value || '').trim(); + var other = (otherInput && otherInput.value || '').trim(); + + if (!slug && !reason) { + notyf('请选择或输入举报原因', 'danger'); + return; + } + + // “其它原因”时以自定义文本为举报原因。 + if (slug === 'other') { + slug = ''; + reason = other || reason; + } + if (!reason) { + // 徽章已选但没有描述时,将徽章文案作为举报原因(参考主题同款语义)。 + var activeBtn = shell.querySelector('[data-report-reason].is-active'); + reason = activeBtn ? (activeBtn.textContent || '').trim() : ''; + } + var link = (urlInput && urlInput.value || '').trim(); + if (link) { + reason += (reason ? '\n' : '') + '违规链接:' + link; + } + + if (!reason) { + notyf('请选择或输入举报原因', 'danger'); + return; + } + + 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 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 modal-mini', + kind: 'report', + mobileBottom: true, + }); + + shell.addEventListener('click', function (event) { + var chip = event.target.closest('[data-report-reason]'); + if (chip) { + event.preventDefault(); + var wasActive = chip.classList.contains('is-active'); + shell.querySelectorAll('[data-report-reason].is-active').forEach(function (el) { + el.classList.remove('is-active'); + }); + if (!wasActive) chip.classList.add('is-active'); + var otherBox = shell.querySelector('.aksrt-report-other'); + if (otherBox) otherBox.hidden = chip.getAttribute('data-report-reason') !== 'other' || wasActive; return; } - submitBtn.disabled = true; - submitBtn.textContent = '提交中…'; - - 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: catSelect.value, - reason: reason, - }), - }) - .then(function (res) { return res.json(); }) - .then(function (result) { - overlay.remove(); - if (result.success) { - notyf(result.message || '举报已提交'); - } else { - notyf(result.message || '提交失败', 'danger'); - } - }) - .catch(function () { - submitBtn.disabled = false; - submitBtn.textContent = '提交举报'; - notyf('网络错误,请重试', 'danger'); - }); - }; - btnRow.appendChild(submitBtn); - modal.appendChild(btnRow); - - overlay.appendChild(modal); - overlay.addEventListener('click', function (e) { - if (e.target === overlay) overlay.remove(); + var submitBtn = event.target.closest('[data-aksrt-report-submit]'); + if (submitBtn) { + event.preventDefault(); + submitReport({ target_type: data.target_type, target_id: data.target_id }, data, shell); + } }); - - document.body.appendChild(overlay); - reasonInput.focus(); } function initReportButtons() { @@ -151,7 +241,7 @@ try { var data = JSON.parse(btn.getAttribute('data-aksrt-report')); if (data && data.target_type && data.target_id) { - createReportModal(data); + openReportModal(data); } } catch { // invalid data attribute diff --git a/inc/helpers/aksrt-single-components.php b/inc/helpers/aksrt-single-components.php index ffa74da5..27ef0954 100644 --- a/inc/helpers/aksrt-single-components.php +++ b/inc/helpers/aksrt-single-components.php @@ -137,7 +137,12 @@ if (!function_exists('aksrt_get_single_meta_box')) : } $more .= ''; if (is_user_logged_in()) { - $more .= ''; + $report_payload = wp_json_encode([ + 'target_type' => 'post', + 'target_id' => (int) $post_id, + 'target_name' => get_the_title($post_id), + ]); + $more .= ''; } else { $more .= '' . esc_html__('登录后举报', 'aksrt') . ''; } diff --git a/inc/helpers/comments/item.php b/inc/helpers/comments/item.php index f9497456..53382cc8 100644 --- a/inc/helpers/comments/item.php +++ b/inc/helpers/comments/item.php @@ -147,6 +147,8 @@ if (!function_exists('aksrt_get_comment_report_html')) : /** * 评论举报入口 — 登录用户可举报他人评论(前台走 report.js 的 data-aksrt-report 契约)。 * + * 举报位于“回复”右侧三点下拉菜单内(参考主题同位置)。 + * * @param WP_Comment $comment Comment. * @return string */ @@ -169,7 +171,14 @@ if (!function_exists('aksrt_get_comment_report_html')) : return ''; } - return '
  • comment_ID . '}\'>' . esc_html__('举报', 'aksrt') . '
  • '; + $report_payload = wp_json_encode([ + 'target_type' => 'comment', + 'target_id' => (int) $comment->comment_ID, + 'target_name' => get_comment_author($comment), + 'target_avatar' => get_avatar_url((int) $comment->user_id > 0 ? (int) $comment->user_id : 0, ['size' => 40]) ?: '', + ]); + + return '
  • ' . esc_html__('举报', 'aksrt') . '
  • '; } endif; diff --git a/src/Core/ScriptsPart1b.php b/src/Core/ScriptsPart1b.php index 5ac8321e..b1061e83 100644 --- a/src/Core/ScriptsPart1b.php +++ b/src/Core/ScriptsPart1b.php @@ -277,9 +277,9 @@ trait ScriptsPart1b { // 评论区样式由上方 is_singular 统一入口按 needs_comments 加载。 } - // 用户中心 / 公开主页:路由专属 CSS - if ($is_profile_page) { - // 举报脚本(用户主页和个人中心均需要) + // 举报脚本:举报入口渲染于文章页 meta 菜单、评论区与用户主页/个人中心, + // 因此 singular 页面与 profile 路由都需要加载(文章页举报按钮此前无脚本支撑)。 + if (is_singular() || $is_profile_page) { $report_js_path = get_template_directory() . '/assets/js/report.js'; if (file_exists($report_js_path)) { wp_enqueue_script( @@ -290,7 +290,10 @@ trait ScriptsPart1b { true ); } + } + // 用户中心 / 公开主页:路由专属 CSS + if ($is_profile_page) { // 关注脚本已在全局栈注册(ScriptsPart1c),data-aksrt-follow 按钮 // 也渲染于文章页作者栏与各处用户卡片,不能仅限本路由加载。 diff --git a/src/Core/ScriptsPart1c.php b/src/Core/ScriptsPart1c.php index 47478f2d..737c1758 100644 --- a/src/Core/ScriptsPart1c.php +++ b/src/Core/ScriptsPart1c.php @@ -487,6 +487,14 @@ trait ScriptsPart1c { 'restUrl' => esc_url_raw(rest_url('aksrt/v1')), // Alias used by modal-auth / follow / report (same value as restUrl). 'apiUrl' => esc_url_raw(rest_url('aksrt/v1')), + // 举报弹窗预置原因(report.js 徽章选择数据源)。 + 'reportCategories' => (function () { + if (!class_exists('\AKSRT\Settings\Settings')) { + return []; + } + $report = \AKSRT\Settings\Settings::get_user_identity_settings()['report'] ?? []; + return is_array($report['categories'] ?? null) ? array_values($report['categories']) : []; + })(), 'stickyEnabled' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s']) : true, 'stickyPages' => class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_post_list_style_settings()['sticky_s'] : ['home', 'cat'], 'badgeShow' => class_exists('\AKSRT\Settings\Settings') ? !empty(\AKSRT\Settings\Settings::get_post_list_style_settings()['list_badge_show']) : true, diff --git a/src/Settings/UserRuntime.php b/src/Settings/UserRuntime.php index d2f893a4..aef2d9c4 100644 --- a/src/Settings/UserRuntime.php +++ b/src/Settings/UserRuntime.php @@ -173,7 +173,8 @@ class UserRuntime { }, $medal['items_by_slug'])), ], 'report' => [ - 'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? false), + // 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。 + 'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true), 'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']), 'public_notice' => !isset($settings['user_report_public_notice']) || !empty($settings['user_report_public_notice']), 'ban_notice_text' => $ban_notice_text !== '' ? $ban_notice_text : __('当前账号处于限制状态,部分互动能力已关闭。', 'aksrt'), @@ -252,7 +253,8 @@ class UserRuntime { public static function report_settings(): array { $settings = Settings::get_all(); return [ - 'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? false), + // 默认开启:对齐后台 schema default true 与参考主题 _pz('user_report_s', true)。 + 'enabled' => (bool) ($settings['user_report_s'] ?? $settings['user_report_enabled'] ?? true), 'public_entry' => !isset($settings['user_report_public_entry']) || !empty($settings['user_report_public_entry']), 'public_notice' => !isset($settings['user_report_public_notice']) || !empty($settings['user_report_public_notice']), 'ban_notice_text' => sanitize_text_field((string) ($settings['user_report_ban_notice_text'] ?? '')), diff --git a/templates/parts/user/author-hero.php b/templates/parts/user/author-hero.php index 725fa803..7e92045d 100644 --- a/templates/parts/user/author-hero.php +++ b/templates/parts/user/author-hero.php @@ -80,7 +80,16 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0 diff --git a/tests/widget-system/report-ban-contract.test.cjs b/tests/widget-system/report-ban-contract.test.cjs index 4ff47f95..101402d4 100644 --- a/tests/widget-system/report-ban-contract.test.cjs +++ b/tests/widget-system/report-ban-contract.test.cjs @@ -170,10 +170,16 @@ test('native ban page mirrors the reference theme layout', () => { }) test('comments expose a report entry for other users when reporting is enabled', () => { - assert.match(commentItem, /data-aksrt-report=\\?'\{"target_type":"comment","target_id":/) + // 举报位于“回复”右侧三点下拉菜单内(参考主题同位置),载荷走 wp_json_encode。 + assert.match(commentItem, /data-aksrt-report="/) assert.match(commentItem, /function aksrt_get_comment_report_html/) + assert.match(commentItem, /comment-report-link/) assert.match(commentItem, /report'\]\['enabled'\]/) assert.match(commentItem, /\$author_id === \$reporter_id/) + // 下拉菜单(置顶/编辑/批准/删除 + 举报)包含举报项。 + assert.match(commentItem, /function aksrt_get_comments_action_dropdown_html[\s\S]*?\$lists \.= aksrt_get_comment_report_html/) + // 举报不作为 comt-meta 独立按钮直接渲染。 + assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/) }) test('uninstall cleans up every ban/report meta key written by the theme', () => {