feat(user-ban): frontend ban modal replaces the backend restrictions tab

1:1 replicate the reference theme user-ban flow on the frontend:

- Ban/unban modal rendered by inc/helpers/user-ban.php, opened via
  AksrtModalRuntime.openRemote (600px desktop dialog, 360px initial
  height, mobile bottom drawer, c-yellow colorful unban header).
- Three entry points wired: user homepage cover three-dot menu,
  comment section menu, article author card menu (ban item takes
  priority over report, matching the reference hook order).
- REST endpoints /user-ban/modal and /user-ban/set under aksrt/v1,
  guarded by check_permission, persisted through User\Restriction.
- Submit semantics match zib_ajax_set_user_ban: ban reloads the page,
  unban only closes the modal.

Cleanup (no dual-track): the backend ban-restrictions tab, its form
handlers and flash keys are removed; ticket processing links to the
frontend user homepage as the reference theme does; admin/report and
appeal notification URLs move to type/status query args.

Verification: 337/337 widget-system contract tests, 41/41 admin and
front-submit tests, php -l clean on all changed PHP files.
This commit is contained in:
2026-09-20 18:42:53 +08:00
parent a1eb545bef
commit 7597972eea
18 changed files with 1080 additions and 374 deletions
+135
View File
@@ -7224,6 +7224,141 @@ body.aksrt-modal-runtime-open {
user-select: none;
}
/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)----
参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认
.modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始
高度 360 后动画至内容高度;mobile-bottom → <769px 底部抽屉。
此处以 min-height:360px 复刻初始高度契约,桌面宽取 600px。 */
.aksrt-user-ban-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(600px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-user-ban-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* edit-header:与禁封表单头部同构(关闭钮靠右,红色警示徽标 + 标题居左)。 */
.aksrt-user-ban-modal .edit-header {
position: relative;
padding-right: 32px;
}
.aksrt-user-ban-modal .edit-header .close {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
float: none;
font-size: 20px;
line-height: 1;
cursor: pointer;
background: none;
border: 0;
padding: 2px 6px;
}
/* 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...)):
120px 高绝对定位横幅 + 旋转彩斑,后续表单整体下移 120px。 */
.aksrt-user-ban-modal .modal-colorful-header,
.aksrt-user-ban-modal .colorful-bg {
position: relative;
overflow: hidden;
color: var(--this-color);
background: var(--this-bg);
background-size: 120%;
}
.aksrt-user-ban-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-user-ban-modal .modal-colorful-header + * {
margin-top: 120px;
}
.aksrt-user-ban-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
color: var(--this-color);
font-size: 18px;
opacity: 0.6;
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make,
.aksrt-user-ban-modal .colorful-bg > .colorful-make::before,
.aksrt-user-ban-modal .colorful-bg > .colorful-make::after {
position: absolute;
color: var(--this-color);
background: var(--this-bg);
border-radius: 100%;
content: '';
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: aksrt-ban-colorful 120s linear infinite;
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
opacity: 0.6;
transform: rotate(341deg);
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
opacity: 0.4;
transform: rotate(326deg);
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make + * {
position: relative;
}
/* 单选徽章行(参考主题 .form-but-radio 契约):input 由 main.css 隐藏,
label 可点;选中态由 main.css 的 input:checked + .but-radio 驱动。 */
.aksrt-user-ban-modal .form-but-radio label {
cursor: pointer;
}
.aksrt-user-ban-modal .form-but-radio .but-radio {
user-select: none;
}
/* 开关行(删除全部内容 / 禁止用户申诉):原生 checkbox 隐藏,
视觉态由 main.css 的 input:checked ~ .form-switch 驱动。 */
.aksrt-user-ban-modal .aksrt-user-ban-form label {
user-select: none;
}
@keyframes aksrt-ban-colorful {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ---- order review modal content ---- */
.order-comment-from .shop-score-box {
display: flex;
+1 -1
View File
File diff suppressed because one or more lines are too long
+135
View File
@@ -307,6 +307,141 @@ body.aksrt-modal-runtime-open {
user-select: none;
}
/* ---- 禁封用户弹窗(参考主题 set_user_ban 模态框几何 1:1)----
参考主题 zib_get_edit_user_ban_link 未传 data-class → 走 Bootstrap 默认
.modal-dialog(>=768px 宽 600px);data-height=360 → .modal-content 初始
高度 360 后动画至内容高度;mobile-bottom → <769px 底部抽屉。
此处以 min-height:360px 复刻初始高度契约,桌面宽取 600px。 */
.aksrt-user-ban-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(600px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-user-ban-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* edit-header:与禁封表单头部同构(关闭钮靠右,红色警示徽标 + 标题居左)。 */
.aksrt-user-ban-modal .edit-header {
position: relative;
padding-right: 32px;
}
.aksrt-user-ban-modal .edit-header .close {
position: absolute;
right: 0;
top: 50%;
transform: translateY(-50%);
float: none;
font-size: 20px;
line-height: 1;
cursor: pointer;
background: none;
border: 0;
padding: 2px 6px;
}
/* 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...)):
120px 高绝对定位横幅 + 旋转彩斑,后续表单整体下移 120px。 */
.aksrt-user-ban-modal .modal-colorful-header,
.aksrt-user-ban-modal .colorful-bg {
position: relative;
overflow: hidden;
color: var(--this-color);
background: var(--this-bg);
background-size: 120%;
}
.aksrt-user-ban-modal .modal-colorful-header {
position: absolute;
top: 0;
right: 0;
left: 0;
display: flex;
align-items: center;
justify-content: center;
height: 120px;
border-radius: var(--main-radius, 8px) var(--main-radius, 8px) 0 0;
}
.aksrt-user-ban-modal .modal-colorful-header + * {
margin-top: 120px;
}
.aksrt-user-ban-modal .modal-colorful-header > .close {
position: absolute;
top: 10px;
right: 10px;
z-index: 9;
color: var(--this-color);
font-size: 18px;
opacity: 0.6;
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make,
.aksrt-user-ban-modal .colorful-bg > .colorful-make::before,
.aksrt-user-ban-modal .colorful-bg > .colorful-make::after {
position: absolute;
color: var(--this-color);
background: var(--this-bg);
border-radius: 100%;
content: '';
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make {
top: 60%;
bottom: 0;
width: 323px;
height: 290px;
animation: aksrt-ban-colorful 120s linear infinite;
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make::before {
top: -325px;
right: -558px;
bottom: 0;
width: 677px;
height: 300px;
opacity: 0.6;
transform: rotate(341deg);
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make::after {
top: -170px;
bottom: 0;
left: -262px;
width: 402px;
height: 214px;
opacity: 0.4;
transform: rotate(326deg);
}
.aksrt-user-ban-modal .colorful-bg > .colorful-make + * {
position: relative;
}
/* 单选徽章行(参考主题 .form-but-radio 契约):input 由 main.css 隐藏,
label 可点;选中态由 main.css 的 input:checked + .but-radio 驱动。 */
.aksrt-user-ban-modal .form-but-radio label {
cursor: pointer;
}
.aksrt-user-ban-modal .form-but-radio .but-radio {
user-select: none;
}
/* 开关行(删除全部内容 / 禁止用户申诉):原生 checkbox 隐藏,
视觉态由 main.css 的 input:checked ~ .form-switch 驱动。 */
.aksrt-user-ban-modal .aksrt-user-ban-form label {
user-select: none;
}
@keyframes aksrt-ban-colorful {
0% { transform: rotate(0deg); }
100% { transform: rotate(360deg); }
}
/* ---- order review modal content ---- */
.order-comment-from .shop-score-box {
display: flex;
+262
View File
@@ -0,0 +1,262 @@
/**
* AKSRT User Ban — 前台“禁封/解封用户”弹窗
*
* 1:1 复刻参考主题 Zibll 前台禁封能力:
* - zib_get_edit_user_ban_link → [data-aksrt-ban] 委托入口(三点菜单内)
* - zib_get_set_user_ban_modal → REST /user-ban/modal 拉取弹窗内容
* - zib_ajax_set_user_ban → REST /user-ban/set 提交
*
* 弹窗经 AksrtModalRuntime.openRemote 打开(与参考主题 refresh_modal 每次刷新
* 同语义):桌面 600px 宽(参考主题未传 data-class → Bootstrap .modal-dialog
* 默认宽 600px)/ min-height 360px(data-height=360 → .modal-content 初始高度),
* 移动端底部抽屉(mobileBottom)。提交成功语义与参考主题 zib_ajax_set_user_ban
* 返回值一致:禁封 → reload:true(整页刷新),解封 → hide_modal:true(仅关闭弹窗)。
*
* @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 MODAL_HEIGHT = 360;
function openBanModal(userId) {
if (!window.AksrtModalRuntime || typeof window.AksrtModalRuntime.openRemote !== 'function') {
notyf('禁封组件加载失败,请刷新页面后重试', 'danger');
return;
}
window.AksrtModalRuntime.openRemote({
url: API_ROOT + '/user-ban/modal',
method: 'GET',
query: { user_id: userId },
cache: false,
height: MODAL_HEIGHT,
mobileBottom: true,
kind: 'user-ban',
className: 'aksrt-user-ban-modal',
onContent: function (html, session) {
if (session && session.shell) bindBanModal(session.shell);
},
});
}
function collectPayload(shell, userId, unban) {
var payload = { user_id: userId, ban: unban ? 0 : 1 };
if (unban) {
var descInput = shell.querySelector('[data-aksrt-ban-form] [name="desc"]');
payload.desc = (descInput && descInput.value || '').trim();
return payload;
}
var form = shell.querySelector('[data-aksrt-ban-form]');
if (!form) return payload;
var banInput = form.querySelector('input[name="ban"]:checked');
payload.ban = banInput ? parseInt(banInput.value, 10) : 1;
var timeInput = form.querySelector('input[name="time_day"]:checked');
payload.time_day = timeInput ? parseInt(timeInput.value, 10) : 7;
var reasonInput = form.querySelector('input[name="reason"]:checked');
payload.reason = reasonInput ? reasonInput.value : '';
var otherInput = form.querySelector('[name="reason_other"]');
payload.reason_other = (otherInput && otherInput.value || '').trim();
var desc = form.querySelector('[name="desc"]');
payload.desc = (desc && desc.value || '').trim();
payload.delete_content = !!(form.querySelector('[name="delete_content"]') || {}).checked;
payload.no_appeal = !!(form.querySelector('[name="no_appeal"]') || {}).checked;
var noAppealDesc = form.querySelector('[name="no_appeal_desc"]');
payload.no_appeal_desc = (noAppealDesc && noAppealDesc.value || '').trim();
return payload;
}
function submitBan(shell, userId, unban) {
var payload = collectPayload(shell, userId, unban);
if (!unban) {
var reason = payload.reason === 'other' || !payload.reason ? payload.reason_other : payload.reason;
if (!reason) {
notyf('请选择或输入禁封原因', 'danger');
return;
}
if (payload.no_appeal && !payload.no_appeal_desc) {
notyf('请输入禁止用户申诉的说明或原因', 'danger');
return;
}
}
var submitBtn = shell.querySelector(unban ? '[data-aksrt-unban-submit]' : '[data-aksrt-ban-submit]');
if (submitBtn) submitBtn.disabled = true;
notyf(unban ? '正在解封…' : '正在提交禁封…', 'load', 3000, 'aksrt-ban-submit');
window
.fetch(API_ROOT + '/user-ban/set', {
method: 'POST',
headers: { 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE },
body: JSON.stringify(payload),
})
.then(function (res) { return res.json(); })
.then(function (result) {
if (result && result.success) {
notyf(result.message || '操作成功', 'success');
// 参考主题 zib_ajax_set_user_ban 返回值语义:
// 禁封 → reload:true(整页刷新,入口按钮状态随页面重渲染);
// 解封 → hide_modal:true(仅关闭弹窗)。
if (window.AksrtModalRuntime && typeof window.AksrtModalRuntime.close === 'function') {
window.AksrtModalRuntime.close('submit');
}
if (!unban) window.location.reload();
} else {
if (submitBtn) submitBtn.disabled = false;
notyf((result && result.message) || '操作失败', 'danger');
}
})
.catch(function () {
if (submitBtn) submitBtn.disabled = false;
notyf('网络错误,请重试', 'danger');
})
.then(function () {
var toast = document.getElementById('aksrt-ban-submit');
if (toast) toast.remove();
});
}
function bindAutoHeight(textarea) {
// 1:1 复刻参考主题 textarea[autoHeight] 行为(input/focus 触发):
// 高度 = scrollHeight + 上下边框;超过 maxHeight 属性则锁定 maxHeight 并
// 打开自身滚动条(参考主题 main.js autoHeight 同语义)。
if (!textarea || textarea.dataset.aksrtAutoHeight === '1') return;
textarea.dataset.aksrtAutoHeight = '1';
var grow = function () {
var style = getComputedStyle(textarea);
var border = (parseInt(style.getPropertyValue('border-top-width'), 10) || 0)
+ (parseInt(style.getPropertyValue('border-bottom-width'), 10) || 0);
var max = parseInt(textarea.getAttribute('maxHeight'), 10) || 0;
textarea.style.height = 'auto';
if (max > textarea.scrollHeight) {
textarea.style.height = textarea.scrollHeight + border + 'px';
textarea.style.overflow = 'hidden';
} else {
textarea.style.height = max + 'px';
textarea.style.overflow = 'auto';
}
};
textarea.addEventListener('input', grow);
textarea.addEventListener('focus', grow);
if (textarea.offsetHeight > 0) grow();
}
function syncBanForm(shell) {
var form = shell.querySelector('[data-aksrt-ban-form]');
if (!form || form.getAttribute('data-aksrt-ban-mode') !== 'ban') return;
var banInput = form.querySelector('input[name="ban"]:checked');
var ban = banInput ? banInput.value : '1';
Array.prototype.forEach.call(form.querySelectorAll('[data-aksrt-ban-desc]'), function (el) {
el.style.display = el.getAttribute('data-aksrt-ban-desc') === ban ? '' : 'none';
});
var reasonInput = form.querySelector('input[name="reason"]:checked');
var otherBox = form.querySelector('[data-aksrt-ban-reason-other]');
if (otherBox) otherBox.style.display = reasonInput && reasonInput.value === 'other' ? '' : 'none';
var noAppealBox = form.querySelector('[data-aksrt-ban-noappeal-desc]');
var noAppealInput = form.querySelector('[name="no_appeal"]');
if (noAppealBox) noAppealBox.style.display = noAppealInput && noAppealInput.checked ? '' : 'none';
}
function bindBanModal(shell) {
Array.prototype.forEach.call(shell.querySelectorAll('textarea.form-control'), bindAutoHeight);
syncBanForm(shell);
}
/**
* 收起封面三点菜单面板(参考主题 author_header_drop_lists 菜单项点击后
* 不自动收起,需手动关闭;评论区与文章作者下拉由各自脚本点击项后自动收起)。
*/
function closeCoverMenu(trigger) {
var menu = trigger && trigger.closest ? trigger.closest('[data-aksrt-menu]') : null;
if (!menu) return;
var panel = menu.querySelector('.aksrt-cover-menu__panel');
if (panel) panel.hidden = true;
var menuTrigger = menu.querySelector('.aksrt-cover-menu__trigger');
if (menuTrigger) menuTrigger.setAttribute('aria-expanded', 'false');
}
function initBanBindings() {
if (document.documentElement.dataset.aksrtBanInitialized === '1') return;
document.documentElement.dataset.aksrtBanInitialized = '1';
document.addEventListener('click', function (event) {
if (!event.target || !event.target.closest) return;
// 禁封 / 解封入口(封面菜单按钮 / 评论区菜单项 / 文章作者菜单项)。
var banBtn = event.target.closest('[data-aksrt-ban]');
if (banBtn) {
event.preventDefault();
var userId = parseInt(banBtn.getAttribute('data-aksrt-ban'), 10);
if (userId > 0) {
closeCoverMenu(banBtn);
openBanModal(userId);
}
return;
}
// 弹窗内交互(事件委托到 document,弹窗壳由 runtime portal 到 body)。
var active = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive
? window.AksrtModalRuntime.getActive()
: null;
if (!active || !active.contains(event.target)) return;
var shell = active;
var form = shell.querySelector('[data-aksrt-ban-form]');
var modalUser = form ? parseInt(form.getAttribute('data-aksrt-ban-user'), 10) : 0;
var submitBtn = event.target.closest('[data-aksrt-ban-submit]');
if (submitBtn) {
event.preventDefault();
submitBtn.disabled = true;
submitBan(shell, modalUser, false);
return;
}
var unbanBtn = event.target.closest('[data-aksrt-unban-submit]');
if (unbanBtn) {
event.preventDefault();
unbanBtn.disabled = true;
submitBan(shell, modalUser, true);
return;
}
});
document.addEventListener('change', function (event) {
var active = window.AksrtModalRuntime && window.AksrtModalRuntime.getActive
? window.AksrtModalRuntime.getActive()
: null;
if (!active || !event.target || !active.contains(event.target)) return;
syncBanForm(active);
});
// 弹窗内容注入后由 onContent 绑定;bs.modal 展示契约下对 user-ban 弹窗做同步。
jQuery(document).on('loaded.bs.modal show.bs.modal', function (event) {
var shell = event.target && event.target.classList && event.target.classList.contains('aksrt-user-ban-modal')
? event.target
: null;
if (shell) syncBanForm(shell);
});
}
jQuery(document)
.off('auto_fun.aksrt-ban')
.on('auto_fun.aksrt-ban', initBanBindings);
})();
+1
View File
@@ -51,6 +51,7 @@ require_once AKSRT_DIR . '/inc/helpers/aksrt-pay.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-coupon-order.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-credits.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-null.php';
require_once AKSRT_DIR . '/inc/helpers/user-ban.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-ajax-list.php';
require_once AKSRT_DIR . '/inc/helpers/aksrt-links.php';
require_once AKSRT_DIR . '/inc/helpers/search/aksrt-search-posts.php';
+8 -2
View File
@@ -132,18 +132,24 @@ if (!function_exists('aksrt_get_single_meta_box')) :
$more .= '<button type="button" class="but cir post-drop-meta__toggle" aria-haspopup="true" aria-controls="' . esc_attr($menu_id) . '" aria-expanded="false" aria-label="' . esc_attr__('更多', 'aksrt') . '">';
$more .= '<i class="fa fas fa-ellipsis-h" aria-hidden="true"></i></button>';
$more .= '<div id="' . esc_attr($menu_id) . '" class="post-drop-meta__menu" aria-hidden="true">';
// 首项(参考主题 zib_get_post_more_dropdown 顺序):管理员 → 禁封用户;
// 无禁封权限时不占位,由下方举报项兜底(「禁封优先、否则举报」互斥语义)。
$ban_item = function_exists('aksrt_get_post_author_ban_item')
? aksrt_get_post_author_ban_item((int) get_post_field('post_author', $post_id), (string) $permalink)
: '';
$more .= $ban_item; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper escapes internally.
if ($edit_link) {
$more .= '<a href="' . esc_url($edit_link) . '" class="post-drop-meta__item"><i class="fa fas fa-edit" aria-hidden="true"></i><span>' . esc_html__('编辑文章', 'aksrt') . '</span></a>';
}
$more .= '<button type="button" class="post-drop-meta__item" data-share-copy data-share-url="' . esc_url((string) $permalink) . '"><i class="fa fas fa-link" aria-hidden="true"></i><span>' . esc_html__('复制链接', 'aksrt') . '</span></button>';
if (is_user_logged_in()) {
if ($ban_item === '' && is_user_logged_in()) {
$report_payload = wp_json_encode([
'target_type' => 'post',
'target_id' => (int) $post_id,
'target_name' => get_the_title($post_id),
]);
$more .= '<button type="button" class="post-drop-meta__item" data-aksrt-report="' . esc_attr((string) $report_payload) . '"><i class="fa fas fa-flag" aria-hidden="true"></i><span>' . esc_html__('举报', 'aksrt') . '</span></button>';
} else {
} elseif ($ban_item === '') {
$more .= '<a href="' . esc_url($login_url) . '" class="post-drop-meta__item"><i class="fa fas fa-flag" aria-hidden="true"></i><span>' . esc_html__('登录后举报', 'aksrt') . '</span></a>';
}
$more .= '</div></div>';
+3 -1
View File
@@ -217,7 +217,9 @@ if (!function_exists('aksrt_get_comments_action_dropdown_html')) :
if (!empty($perms['can_delete'])) {
$lists .= '<li><a class="comment-trash-link c-red" data-comment-action="delete" data-commentid="' . (int) $comment->comment_ID . '" data-postid="' . (int) $comment->comment_post_ID . '" href="javascript:;"><i class="fa far fa-trash-alt mr10 fa-fw" aria-hidden="true"></i>' . esc_html__('删除', 'aksrt') . '</a></li>';
}
$lists .= aksrt_get_comment_report_html($comment);
// 末项(参考主题 comments_action_lists 钩子顺序):管理员 → 禁封用户;
// 否则 → 举报(登录且非本人且举报开启时,既有评论举报契约)。
$lists .= aksrt_get_comment_ban_or_report_item($comment);
if ($lists === '') {
return '';
+285
View File
@@ -0,0 +1,285 @@
<?php
/**
* 前台用户禁封(小黑屋 / 封号)— 1:1 复刻参考主题 Zibll 前台禁封能力:
* zib_get_edit_user_ban_link → aksrt_get_edit_user_ban_link(三点菜单入口)
* zib_get_set_user_ban_modal → aksrt_get_user_ban_modal(禁封 / 解封弹窗)
*
* 数据源唯一:\AKSRT\User\Restriction(wp_aksrt_user_restriction 表)。
* 弹窗内容经 REST /user-ban/modal 输出(AksrtModalRuntime.openRemote 拉取,
* 与参考主题 refresh_modal 每次刷新同语义),提交走 REST /user-ban/set。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 禁封功能开关(后台“用户&互动 → 禁封/举报 → 禁封功能”,
* 对齐参考主题 _pz('user_ban_s', true):缺键默认开启)。
*/
function aksrt_user_ban_enabled(): bool
{
$settings = class_exists('\AKSRT\Admin\SchemaProvider')
? \AKSRT\Settings\Settings::read_domain('users', 'report')
: (class_exists('\AKSRT\Settings\Settings') ? \AKSRT\Settings\Settings::get_all() : []);
$settings = is_array($settings) ? $settings : [];
return !isset($settings['user_ban_s']) || !empty($settings['user_ban_s']);
}
/**
* 当前用户能否禁封目标用户(对齐参考主题 zib_current_user_can('set_user_ban') 语义)。
*/
function aksrt_current_user_can_ban(int $user_id): bool
{
if ($user_id <= 0 || !aksrt_user_ban_enabled()) {
return false;
}
$current_id = get_current_user_id();
if ($current_id <= 0 || $current_id === $user_id) {
return false;
}
if (!current_user_can('manage_options')) {
return false;
}
if (function_exists('is_super_admin') && is_super_admin($user_id)) {
return false;
}
return true;
}
/**
* 取用户当前生效中的禁封记录(ban 优先于 suspend,对齐后台单一状态语义)。
*/
function aksrt_get_user_current_ban(int $user_id): ?array
{
if ($user_id <= 0 || !class_exists('\AKSRT\User\Restriction')) {
return null;
}
$svc = \AKSRT\User\Restriction::get_instance();
return $svc->get_active_ban($user_id) ?: $svc->get_active_suspend($user_id);
}
/**
* 禁封 / 解封触发器(参考主题 zib_get_edit_user_ban_link)。
* 输出 data-aksrt-ban 委托入口,由 assets/js/user-ban.js 打开禁封弹窗。
*/
function aksrt_get_edit_user_ban_link(int $user_id = 0, string $class = '', string $con = ''): string
{
if (!aksrt_current_user_can_ban($user_id)) {
return '';
}
if ($con === '') {
$con = aksrt_get_user_current_ban($user_id)
? '<i class="fa fas fa-ban mr6" aria-hidden="true"></i>' . __('解封此用户', 'aksrt')
: '<i class="fa fas fa-ban mr6" aria-hidden="true"></i>' . __('禁封此用户', 'aksrt');
}
return '<a href="javascript:;" class="' . esc_attr($class) . '" data-aksrt-ban="' . esc_attr((string) $user_id) . '">' . $con . '</a>';
}
/**
* 用户主页封面三点菜单项(参考主题 author_header_drop_lists 钩子语义):
* 有禁封权限 → 禁封入口;否则(开启举报且非本人)→ 举报入口。
*/
function aksrt_get_user_cover_menu_action_html(int $user_id): string
{
if (aksrt_current_user_can_ban($user_id)) {
$ban = aksrt_get_user_current_ban($user_id);
$label = $ban ? __('解封此用户', 'aksrt') : __('禁封此用户', 'aksrt');
return '<button type="button" role="menuitem" data-aksrt-ban="' . esc_attr((string) $user_id) . '"><i class="fa fas fa-ban" aria-hidden="true"></i>' . esc_html($label) . '</button>';
}
return '';
}
/**
* 评论三点菜单项(参考主题 comments_action_lists 钩子语义):
* 管理员 → 禁封用户(固定文案,与参考主题 zib_ban_comments_action_lists_filter
* 一致——解封态由弹窗内容呈现);否则 → 举报( Aksrt 既有评论举报契约)。
*/
function aksrt_get_comment_ban_or_report_item(\WP_Comment $comment): string
{
$user_id = (int) $comment->user_id;
if ($user_id > 0 && aksrt_current_user_can_ban($user_id)) {
return '<li><a class="comment-ban-link" href="javascript:;" data-aksrt-ban="' . esc_attr((string) $user_id) . '"><i class="fa fas fa-ban mr6 fa-fw c-red" aria-hidden="true"></i>' . esc_html__('禁封用户', 'aksrt') . '</a></li>';
}
if (function_exists('aksrt_get_comment_report_html')) {
return aksrt_get_comment_report_html($comment);
}
return '';
}
/**
* 文章作者资料卡三点菜单项(参考主题 zib_get_post_more_dropdown 语义,菜单首项):
* 管理员 → 禁封用户(固定文案,解封态由弹窗内容呈现);否则 → 空(由既有
* 文章举报项兜底,与参考主题「禁封优先、否则举报」互斥语义一致)。
*/
function aksrt_get_post_author_ban_item(int $author_id, string $post_url = ''): string
{
if ($author_id <= 0 || get_current_user_id() === $author_id) {
return '';
}
if (aksrt_current_user_can_ban($author_id)) {
return '<a href="javascript:;" class="post-drop-meta__item" data-aksrt-ban="' . esc_attr((string) $author_id) . '"><i class="fa fas fa-ban" aria-hidden="true"></i><span>' . esc_html__('禁封用户', 'aksrt') . '</span></a>';
}
return '';
}
/**
* 弹窗关闭图标(与 report.js 同款 SVG 雪碧图契约)。
*/
function aksrt_user_ban_close_svg(): string
{
return '<svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg>';
}
/**
* 禁封 / 解封弹窗内容 — 1:1 复刻参考主题 zib_get_set_user_ban_modal:
* 未禁封 → 禁封表单(禁封方式 / 禁封时间 / 预置原因徽章 / 其它原因 /
* 禁封说明 / 删除全部内容 / 禁止用户申诉(含说明));
* 已禁封 → 解封视图(状态信息 + 解封留言表单)。
*
* @return string 弹窗 HTML(空串 = 无权操作或用户不存在)
*/
function aksrt_get_user_ban_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user || !aksrt_current_user_can_ban($user_id)) {
return '';
}
$ban = aksrt_get_user_current_ban($user_id);
// 用户行(头像 + 昵称),与参考主题弹窗头部同构。
$avatar_url = get_avatar_url($user_id, ['size' => 40]);
$user_name = function_exists('aksrt_get_user_name')
? aksrt_get_user_name(['id' => $user_id, 'vip' => false, 'medal' => false])
: esc_html($user->display_name);
$user_row = '<div class="flex ac mb6">'
. '<img class="avatar-mini mr6" src="' . esc_url((string) $avatar_url) . '" alt="">'
. '<b>' . $user_name . '</b></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- aksrt_get_user_name escapes internally.
$html = '<div class="aksrt-user-ban-modal">';
if ($ban) {
// ── 已禁封视图:解封(参考主题解封弹窗同构) ──
$is_ban = (string) ($ban['type'] ?? '') === 'ban';
$unban_title = $is_ban ? __('解封用户', 'aksrt') : __('移出小黑屋', 'aksrt');
$status_text = $is_ban ? __('禁封中(禁止登录)', 'aksrt') : __('小黑屋', 'aksrt');
$end_time = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt');
// AKSRT 单一 reason 列:禁封说明以“禁封说明:”后缀并入(set_user_ban 同语义),
// 解封视图拆回参考主题的「原因 / 说明」两行。
$raw_reason = (string) ($ban['reason'] ?? '');
$reason = $raw_reason;
$ban_desc = '';
$desc_marker = __('禁封说明:', 'aksrt');
$marker_pos = strpos($raw_reason, "\n" . $desc_marker);
if (false !== $marker_pos) {
$reason = substr($raw_reason, 0, $marker_pos);
$ban_desc = substr($raw_reason, $marker_pos + strlen("\n" . $desc_marker));
}
// 用户名徽章(参考主题 zib_get_user_name(...&class=inflex ac badg p2-10 em09))。
$user_badge = function_exists('aksrt_get_user_name')
? aksrt_get_user_name(['id' => $user_id, 'class' => 'inflex ac badg p2-10 em09', 'vip' => false, 'medal' => false, 'auth' => false, 'level' => false])
: '<b>' . esc_html($user->display_name) . '</b>';
$set_row = static function (string $label, string $value): string {
return '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html($label) . '</div><div class="author-set-right mt6">' . $value . '</div></div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- callers pass escaped HTML.
};
// 弹窗头(参考主题 zib_get_modal_colorful_header('c-yellow', ban 图标, 标题):
// 120px 多彩横幅 + 大图标 + 居中标题,与禁封视图的 edit-header 头对应)。
$html .= '<div class="modal-colorful-header colorful-bg c-yellow"><button type="button" class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">' . aksrt_user_ban_close_svg() . '</button><div class="colorful-make"></div><div class="text-center"><div class="em2x"><i class="fa fas fa-ban" aria-hidden="true"></i></div><div class="mt10 em12 padding-w10">' . esc_html($unban_title) . '</div></div></div>';
$html .= '<form class="aksrt-user-ban-form" data-aksrt-ban-form data-aksrt-ban-mode="unban" data-aksrt-ban-user="' . esc_attr((string) $user_id) . '">';
$html .= '<div class="mb20">';
$html .= $set_row(__('用户', 'aksrt'), $user_badge); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
$html .= $set_row(__('状态', 'aksrt'), esc_html($status_text));
$html .= $set_row(__('开始时间', 'aksrt'), esc_html((string) ($ban['created_at'] ?? '')));
$html .= $set_row(__('结束时间', 'aksrt'), esc_html($end_time));
$html .= $set_row(__('原因', 'aksrt'), nl2br(esc_html($reason)));
$html .= $set_row(__('说明', 'aksrt'), nl2br(esc_html($ban_desc)));
$html .= '</div>';
$html .= '<div class="mb20"><textarea class="form-control" name="desc" tabindex="2" placeholder="' . esc_attr__('给用户的留言', 'aksrt') . '" rows="2" autoHeight="true" maxHeight="110"></textarea></div>';
$html .= '<div class="mt20 but-average modal-buts">';
$html .= '<button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>';
$html .= '<button type="button" class="but c-yellow" data-aksrt-unban-submit>' . esc_html(sprintf(__('确认%s', 'aksrt'), $unban_title)) . '</button>';
$html .= '</div></form></div>';
return $html;
}
// ── 未禁封视图:禁封表单(参考主题禁封弹窗同构) ──
$close_svg = aksrt_user_ban_close_svg();
$html .= '<div class="mb10 edit-header touch"><button type="button" class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">' . $close_svg . '</button><b class="flex ac c-red"><span class="toggle-radius mr10 c-red"><i class="fa fas fa-ban" aria-hidden="true"></i></span>' . esc_html__('禁封用户', 'aksrt') . '</b></div>';
// 历史禁封次数提示(参考主题:>1 次时展示禁封记录链接,此处展示计数提示)。
$log_count = 0;
if (class_exists('\AKSRT\User\Restriction')) {
$all = \AKSRT\User\Restriction::get_instance()->get_user_restrictions($user_id, false);
foreach ((array) $all as $r) {
if (in_array((string) ($r['type'] ?? ''), ['ban', 'suspend'], true)) {
$log_count++;
}
}
}
$log_view = $log_count > 1
? '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('禁封记录', 'aksrt') . '</div><div class="author-set-right mt6"><span class="em09 c-yellow">' . esc_html(sprintf(__('当前用户已有%d次禁封记录,请谨慎操作', 'aksrt'), $log_count)) . '</span></div></div>'
: '';
$html .= '<form class="aksrt-user-ban-form" data-aksrt-ban-form data-aksrt-ban-mode="ban" data-aksrt-ban-user="' . esc_attr((string) $user_id) . '">';
$html .= '<div class="scroll-y mini-scrollbar max-vh7">';
$html .= '<div class="mt10 mb20">' . $user_row . $log_view . '</div>';
// 禁封方式(1=拉入小黑屋 suspend / 2=禁封用户 ban)。
$html .= '<div class="mb6 muted-2-color">' . esc_html__('禁封方式', 'aksrt') . '</div>';
$html .= '<div class="form-radio form-but-radio">';
$html .= '<label><input type="radio" name="ban" value="1" checked="checked"><span class="p2-10 mr6 but but-radio">' . esc_html__('拉入小黑屋', 'aksrt') . '</span></label>';
$html .= '<label><input type="radio" name="ban" value="2"><span class="p2-10 mr6 but but-radio">' . esc_html__('禁封用户', 'aksrt') . '</span></label>';
$html .= '</div>';
$html .= '<div class="em09 c-red opacity8 mb10">';
$html .= '<div data-aksrt-ban-desc="1">' . esc_html__('将用户拉入小黑屋后,用户将失去所有的发布权限及大部分的操作权限,例如发帖、评论等', 'aksrt') . '</div>';
$html .= '<div data-aksrt-ban-desc="2" style="display:none;">' . esc_html__('禁封的用户将不能登录', 'aksrt') . '</div>';
$html .= '</div>';
// 禁封时间(1/7/14/30/100/永久,默认 7 天,对齐参考主题 checked(7, ...))。
$html .= '<div class="muted-2-color mb6">' . esc_html__('禁封时间', 'aksrt') . '</div>';
$html .= '<div class="form-radio mb10 form-but-radio">';
foreach ([1 => __('1天', 'aksrt'), 7 => __('7天', 'aksrt'), 14 => __('14天', 'aksrt'), 30 => __('30天', 'aksrt'), 100 => __('100天', 'aksrt'), 0 => __('永久', 'aksrt')] as $days => $label) {
$checked = 7 === $days ? ' checked="checked"' : '';
$html .= '<label><input type="radio" name="time_day" value="' . esc_attr((string) $days) . '"' . $checked . '><span class="p2-10 mr6 but but-radio">' . esc_html($label) . '</span></label>';
}
$html .= '</div>';
// 选择禁封原因(预置徽章 + 其它原因)。
$html .= '<div class="muted-2-color mb6">' . esc_html__('选择禁封原因', 'aksrt') . '</div>';
$html .= '<div class="form-radio mb10 form-but-radio">';
$reasons = class_exists('\AKSRT\Settings\UserRuntime') ? (\AKSRT\Settings\UserRuntime::ban_settings()['reasons'] ?? []) : [];
foreach ($reasons as $reason) {
$html .= '<label><input type="radio" name="reason" value="' . esc_attr((string) $reason['name']) . '"><span class="p2-10 mr6 but but-radio">' . esc_html((string) $reason['name']) . '</span></label>';
}
$html .= '<label><input type="radio" name="reason" value="other"><span class="p2-10 mr6 but but-radio">' . esc_html__('其它原因', 'aksrt') . '</span></label>';
$html .= '</div>';
$html .= '<div class="mb20" data-aksrt-ban-reason-other="1" style="display:none;"><input type="text" class="form-control" name="reason_other" tabindex="1" placeholder="' . esc_attr__('请填写禁封原因', 'aksrt') . '"></div>';
// 禁封说明。
$html .= '<div class="muted-2-color mb6">' . esc_html__('禁封说明', 'aksrt') . '</div>';
$html .= '<textarea class="form-control mb20" name="desc" tabindex="2" placeholder="' . esc_attr__('请输入禁封说明或给用户的留言', 'aksrt') . '" rows="2" autoHeight="true" maxHeight="110"></textarea>';
// 删除全部内容。
$html .= '<div class="mb20"><label class="flex jsb ac" style="font-weight:normal;"><input class="hide" name="delete_content" type="checkbox"><div class="flex1 mr20"><div class="muted-2-color mb3">' . esc_html__('删除全部内容', 'aksrt') . '</div><div class="muted-3-color px12">' . esc_html__('将该用户发布的全部文章、帖子、评论移至回收站', 'aksrt') . '</div></div><div class="form-switch flex0"></div></label></div>';
// 禁止用户申诉(含说明联动)。
$html .= '<div class="mb20">';
$html .= '<div class="mb6"><label class="flex jsb ac" style="font-weight:normal;"><input class="hide" name="no_appeal" type="checkbox"><div class="flex1 mr20"><div class="muted-2-color mb3">' . esc_html__('禁止用户申诉', 'aksrt') . '</div><div class="muted-3-color px12">' . esc_html__('开启后用户将无法进行申诉', 'aksrt') . '</div></div><div class="form-switch flex0"></div></label></div>';
$html .= '<div data-aksrt-ban-noappeal-desc style="display:none;"><textarea class="form-control" name="no_appeal_desc" tabindex="1" placeholder="' . esc_attr__('请填写禁止申诉说明', 'aksrt') . '" rows="2"></textarea></div>';
$html .= '</div>';
$html .= '</div>';
$html .= '<div class="mt20 but-average modal-buts">';
$html .= '<button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>';
$html .= '<button type="button" class="but c-red" data-aksrt-ban-submit><i class="fa fas fa-ban" aria-hidden="true"></i>' . esc_html__('确认禁封', 'aksrt') . '</button>';
$html .= '</div>';
$html .= '</form></div>';
return $html;
}
+4 -340
View File
@@ -36,8 +36,6 @@ class UsersMenus {
add_action('admin_post_aksrt_users_review_verify', [$this, 'handle_review_verify']);
add_action('admin_post_aksrt_users_delete_verify', [$this, 'handle_delete_verify']);
add_action('admin_post_aksrt_users_update_report', [$this, 'handle_update_report']);
add_action('admin_post_aksrt_users_add_restriction', [$this, 'handle_add_restriction']);
add_action('admin_post_aksrt_users_remove_restriction', [$this, 'handle_remove_restriction']);
add_action('admin_post_aksrt_users_process_appeal', [$this, 'handle_process_appeal']);
add_action('admin_post_aksrt_users_mutate_vip', [$this, 'handle_mutate_vip']);
}
@@ -159,9 +157,6 @@ class UsersMenus {
'verify_failed' => __('认证审核失败。', 'aksrt'),
'report_updated' => __('举报状态已更新。', 'aksrt'),
'report_failed' => __('举报更新失败。', 'aksrt'),
'restriction_added' => __('限制已添加。', 'aksrt'),
'restriction_removed' => __('限制已移除。', 'aksrt'),
'restriction_failed' => __('限制操作失败。', 'aksrt'),
'appeal_approved' => __('申诉已通过,限制已解除。', 'aksrt'),
'appeal_rejected' => __('申诉已驳回。', 'aksrt'),
'appeal_failed' => __('申诉处理失败。', 'aksrt'),
@@ -287,247 +282,13 @@ class UsersMenus {
echo '<div class="wrap"><h2>' . esc_html__('用户禁封申诉及用户举报管理', 'aksrt') . '</h2>';
$this->flash();
$tab = isset($_GET['tab']) ? sanitize_key((string) wp_unslash($_GET['tab'])) : 'tickets';
if (!in_array($tab, ['tickets', 'restrictions'], true)) {
$tab = 'tickets';
}
echo '<h2 class="nav-tab-wrapper">';
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'tickets' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban')), esc_html__('申诉&举报', 'aksrt'));
printf('<a class="nav-tab%s" href="%s">%s</a>', $tab === 'restrictions' ? ' nav-tab-active' : '', esc_url(admin_url('users.php?page=aksrt_user_ban&tab=restrictions')), esc_html__('禁封限制', 'aksrt'));
echo '</h2>';
if ($tab === 'restrictions') {
$this->render_restrictions_tab();
} else {
$this->render_tickets_tab();
}
// 与参考主题一致:后台仅承载「申诉&举报」工单处理;禁封/解封操作
// 全部在前台完成(用户主页封面 / 评论区 / 文章作者栏三点菜单 →
// 封禁弹窗,REST /user-ban/modal + /user-ban/set)。
$this->render_tickets_tab();
echo '</div>';
}
/**
* 禁封限制页 — 1:1 对齐参考主题 zib_get_set_user_ban_modal:
* 禁封方式(拉入小黑屋/禁封用户)+ 禁封时间(1/7/14/30/100/永久)+
* 选择禁封原因(预置徽章 + 其它原因)+ 禁封说明 + 删除全部内容 +
* 禁止用户申诉(含说明);已禁封用户展示状态信息与解封留言表单。
* 参考主题无 scope 维度,禁封一律全局。
*/
private function render_restrictions_tab(): void {
if (!class_exists('\AKSRT\User\Restriction')) {
echo '<div class="notice notice-error"><p>' . esc_html__('限制模块未初始化。', 'aksrt') . '</p></div>';
return;
}
$svc = \AKSRT\User\Restriction::get_instance();
$page_url = admin_url('users.php?page=aksrt_user_ban&tab=restrictions');
$target_id = isset($_GET['user_id']) ? max(0, (int) $_GET['user_id']) : 0;
$unban = isset($_GET['action']) && sanitize_key((string) wp_unslash($_GET['action'])) === 'unban';
$target = $target_id > 0 ? get_userdata($target_id) : null;
// 页面级样式(wp-admin 不加载前端 kit,就地声明参考主题同款外观)
echo '<style>
.aksrt-ban-modal .muted-2-color{color:#777;}
.aksrt-ban-modal .author-set-left{min-width:80px;flex:0 0 auto;color:#777;}
.aksrt-ban-modal .author-set-right{margin-top:0;}
.aksrt-ban-modal .form-radio label{display:inline-block;margin:0 6px 6px 0;}
.aksrt-ban-modal .form-radio input[type=radio]{display:none;}
.aksrt-ban-modal .form-radio input[type=radio]+span{display:inline-block;padding:4px 10px;border:1px solid #dcdcde;border-radius:4px;background:#f6f7f7;cursor:pointer;}
.aksrt-ban-modal .form-radio input[type=radio]:checked+span{background:#2271b1;border-color:#2271b1;color:#fff;}
.aksrt-ban-modal .but-average.modal-buts{display:grid;grid-template-columns:1fr 1fr;gap:10px;margin-top:16px;max-width:480px;}
.aksrt-ban-modal .but-average.modal-buts .but{min-height:40px;text-align:center;border-radius:10px;border:1px solid #dcdcde;background:#f6f7f7;font-weight:600;cursor:pointer;display:inline-flex;align-items:center;justify-content:center;}
.aksrt-ban-modal .but-average.modal-buts .but.c-blue{background:#2271b1;border-color:#2271b1;color:#fff;}
.aksrt-ban-modal .but-average.modal-buts .but.c-yellow{background:#dba617;border-color:#dba617;color:#fff;}
.aksrt-ban-modal .switch-row{display:flex;align-items:center;margin:0 0 14px;font-weight:400;}
.aksrt-ban-modal .switch-row .switch-title{color:#777;margin-bottom:3px;}
.aksrt-ban-modal .switch-row .switch-help{color:#a7aaad;font-size:12px;}
.aksrt-ban-modal textarea,.aksrt-ban-modal input[type=text]{width:100%;max-width:560px;}
</style>';
// 当前生效中的全局禁封(ban 优先于 suspend,对齐参考主题单一状态语义)
$current = null;
if ($target_id > 0) {
$restrictions = $svc->get_user_restrictions($target_id, true);
foreach ($restrictions as $r) {
if (($r['scope'] ?? '') !== 'global' || !in_array($r['type'], ['ban', 'suspend'], true)) {
continue;
}
if ($current === null || ($r['type'] === 'ban' && $current['type'] !== 'ban')) {
$current = $r;
}
}
// 禁言解除视图:记录列表“解除禁言”进入(无更重的 ban/suspend 时生效)。
if ($unban && $current === null) {
foreach ($restrictions as $r) {
if (($r['type'] ?? '') === 'mute') {
$current = $r;
break;
}
}
}
}
echo '<div class="table-box aksrt-ban-modal" style="max-width:760px;padding:12px 16px;">';
if (!$target) {
echo '<p>' . esc_html__('请先通过“用户 ID”定位要禁封或解封的用户(也可从工单列表的“点击此处”进入)。', 'aksrt') . '</p>';
} elseif ($current !== null) {
// —— 已禁封视图(参考主题解封弹窗同构):状态信息 + 解封留言表单 ——
$unban_titles = [
'ban' => __('解封用户', 'aksrt'),
'suspend' => __('移出小黑屋', 'aksrt'),
'mute' => __('解除禁言', 'aksrt'),
];
$status_texts = [
'ban' => __('禁封中(禁止登录)', 'aksrt'),
'suspend' => __('小黑屋禁封中', 'aksrt'),
'mute' => __('禁言中', 'aksrt'),
];
$cur_type = (string) ($current['type'] ?? 'ban');
$unban_title = $unban_titles[$cur_type] ?? $unban_titles['ban'];
$status_text = $status_texts[$cur_type] ?? $status_texts['ban'];
$end_time = !empty($current['expires_at']) ? (string) $current['expires_at'] : __('永久', 'aksrt');
echo '<h2 style="margin-top:0;">' . esc_html($unban_title) . '</h2>';
echo '<div class="mb10 flex"><div class="author-set-left">' . esc_html__('用户', 'aksrt') . '</div><div class="author-set-right">' . esc_html($target->display_name) . '</div></div>';
echo '<div class="mb10 flex"><div class="author-set-left">' . esc_html__('状态', 'aksrt') . '</div><div class="author-set-right">' . esc_html($status_text) . '</div></div>';
echo '<div class="mb10 flex"><div class="author-set-left">' . esc_html__('开始时间', 'aksrt') . '</div><div class="author-set-right">' . esc_html((string) ($current['created_at'] ?? '')) . '</div></div>';
echo '<div class="mb10 flex"><div class="author-set-left">' . esc_html__('结束时间', 'aksrt') . '</div><div class="author-set-right">' . esc_html($end_time) . '</div></div>';
echo '<div class="mb10 flex"><div class="author-set-left">' . esc_html__('原因', 'aksrt') . '</div><div class="author-set-right">' . nl2br(esc_html((string) ($current['reason'] ?? ''))) . '</div></div>';
echo '<form method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_users_remove_restriction" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $target_id) . '" />';
wp_nonce_field('aksrt_users_unban_' . $target_id);
echo '<p><textarea class="large-text" name="desc" rows="2" placeholder="' . esc_attr__('给用户的留言', 'aksrt') . '"></textarea></p>';
echo '<div class="but-average modal-buts">';
echo '<a class="but" href="' . esc_url($page_url) . '">' . esc_html__('取消', 'aksrt') . '</a>';
echo '<button type="submit" class="but c-yellow">' . esc_html(sprintf(__('确认%s', 'aksrt'), $unban_title)) . '</button>';
echo '</div>';
echo '</form>';
} else {
// —— 禁封表单(参考主题禁封弹窗同构) ——
echo '<h2 style="margin-top:0;">' . esc_html__('禁封用户', 'aksrt') . '</h2>';
echo '<form class="aksrt-ban-form" method="post" action="' . esc_url(admin_url('admin-post.php')) . '">';
echo '<input type="hidden" name="action" value="aksrt_users_add_restriction" />';
echo '<input type="hidden" name="user_id" value="' . esc_attr((string) $target_id) . '" />';
wp_nonce_field('aksrt_users_add_restriction');
echo '<div class="mt10 mb20 flex ac mb6">' . get_avatar($target_id, 40, '', '', ['class' => 'avatar-mini mr6']) . '<b>' . esc_html($target->display_name) . '</b></div>';
// 禁封方式
echo '<div class="mb6 muted-2-color">' . esc_html__('禁封方式', 'aksrt') . '</div>';
echo '<div class="form-radio form-but-radio">';
echo '<label><input type="radio" name="ban" value="1" checked="checked"><span class="p2-10 mr6">' . esc_html__('拉入小黑屋', 'aksrt') . '</span></label>';
echo '<label><input type="radio" name="ban" value="2"><span class="p2-10 mr6">' . esc_html__('禁封用户', 'aksrt') . '</span></label>';
echo '</div>';
echo '<div class="em09 c-red opacity8 mb10">';
echo '<div data-aksrt-ban-desc="1">' . esc_html__('将用户拉入小黑屋后,用户将失去所有的发布权限及大部分的操作权限,例如发帖、评论等', 'aksrt') . '</div>';
echo '<div data-aksrt-ban-desc="2" style="display:none;">' . esc_html__('禁封的用户将不能登录', 'aksrt') . '</div>';
echo '</div>';
// 禁封时间
echo '<div class="mb6 muted-2-color">' . esc_html__('禁封时间', 'aksrt') . '</div>';
echo '<div class="form-radio mb10 form-but-radio">';
foreach ([1 => __('1天', 'aksrt'), 7 => __('7天', 'aksrt'), 14 => __('14天', 'aksrt'), 30 => __('30天', 'aksrt'), 100 => __('100天', 'aksrt'), 0 => __('永久', 'aksrt')] as $days => $label) {
echo '<label><input type="radio" name="time_day" value="' . esc_attr((string) $days) . '"' . checked(7, $days, false) . '><span class="p2-10 mr6">' . esc_html($label) . '</span></label>';
}
echo '</div>';
// 选择禁封原因(预置原因 + 其它原因)
echo '<div class="mb6 muted-2-color">' . esc_html__('选择禁封原因', 'aksrt') . '</div>';
echo '<div class="form-radio mb10 form-but-radio">';
foreach (\AKSRT\Settings\UserRuntime::ban_settings()['reasons'] ?? [] as $reason) {
echo '<label><input type="radio" name="reason" value="' . esc_attr($reason['name']) . '"><span class="p2-10 mr6">' . esc_html($reason['name']) . '</span></label>';
}
echo '<label><input type="radio" name="reason" value="other"><span class="p2-10 mr6">' . esc_html__('其它原因', 'aksrt') . '</span></label>';
echo '</div>';
echo '<div class="aksrt-reason-other mb20" style="display:none;"><input type="text" class="regular-text" name="reason_other" placeholder="' . esc_attr__('请填写禁封原因', 'aksrt') . '"></div>';
// 禁封说明
echo '<div class="mb6 muted-2-color">' . esc_html__('禁封说明', 'aksrt') . '</div>';
echo '<p><textarea class="large-text" name="desc" rows="2" placeholder="' . esc_attr__('请输入禁封说明或给用户的留言', 'aksrt') . '"></textarea></p>';
// 删除全部内容
echo '<div class="switch-row"><label><input type="checkbox" name="delete_content" value="1"> <span class="flex1 mr20"><span class="switch-title">' . esc_html__('删除全部内容', 'aksrt') . '</span><br><span class="switch-help">' . esc_html__('将该用户发布的全部文章、帖子、评论移至回收站', 'aksrt') . '</span></span></label></div>';
// 禁止用户申诉
echo '<div class="switch-row"><label><input type="checkbox" name="no_appeal" value="1"> <span class="flex1 mr20"><span class="switch-title">' . esc_html__('禁止用户申诉', 'aksrt') . '</span><br><span class="switch-help">' . esc_html__('开启后用户将无法进行申诉', 'aksrt') . '</span></span></label></div>';
echo '<div class="aksrt-no-appeal-desc mb20" style="display:none;"><textarea class="large-text" name="no_appeal_desc" rows="2" placeholder="' . esc_attr__('请填写禁止申诉说明', 'aksrt') . '"></textarea></div>';
echo '<div class="but-average modal-buts">';
echo '<a class="but" href="' . esc_url($page_url) . '">' . esc_html__('取消', 'aksrt') . '</a>';
echo '<button type="submit" class="but c-blue">' . esc_html__('确认提交', 'aksrt') . '</button>';
echo '</div>';
echo '</form>';
}
echo '</div>';
// —— 限制记录列表(参考主题列语义:状态/开始/结束/原因) ——
global $wpdb;
$table = class_exists('\AKSRT\Core\DBMigrator') ? \AKSRT\Core\DBMigrator::table('user_restriction') : '';
if ($table === '') {
echo '<div class="notice notice-warning"><p>' . esc_html__('限制表不可用。', 'aksrt') . '</p></div>';
return;
}
$items = $wpdb->get_results("SELECT * FROM {$table} ORDER BY id DESC LIMIT 100", ARRAY_A);
$now = gmdate('Y-m-d H:i:s');
echo '<h2>' . esc_html__('限制记录', 'aksrt') . '</h2>';
echo '<table class="wp-list-table widefat fixed striped"><thead><tr>';
echo '<th>' . esc_html__('用户', 'aksrt') . '</th><th>' . esc_html__('禁封状态', 'aksrt') . '</th><th>' . esc_html__('开始时间', 'aksrt') . '</th><th>' . esc_html__('结束时间', 'aksrt') . '</th><th>' . esc_html__('原因', 'aksrt') . '</th><th>' . esc_html__('操作', 'aksrt') . '</th>';
echo '</tr></thead><tbody>';
if (!$items) {
echo '<tr><td colspan="6">' . esc_html__('暂无限制记录', 'aksrt') . '</td></tr>';
}
foreach ((array) $items as $r) {
$type = (string) ($r['type'] ?? '');
$expired = !empty($r['expires_at']) && (string) $r['expires_at'] <= $now;
$is_global = ($r['scope'] ?? '') === 'global' && in_array($type, ['ban', 'suspend'], true);
if ($expired) {
$status_html = '<span style="color:#a7aaad;">' . esc_html__('已过期', 'aksrt') . '</span>';
} elseif ($type === 'ban') {
$status_html = '<span style="color:#fb4444;">' . esc_html__('禁封中(禁止登录)', 'aksrt') . '</span>';
} elseif ($type === 'suspend') {
$status_html = '<span style="color:#fd6a11;">' . esc_html__('小黑屋禁封中', 'aksrt') . '</span>';
} elseif ($type === 'mute') {
$status_html = '<span style="color:#fd6a11;">' . esc_html__('禁言中', 'aksrt') . '</span>';
} else {
$status_html = '<span>' . esc_html__('警告', 'aksrt') . '</span>';
}
$r_user_id = (int) ($r['user_id'] ?? 0);
$r_user = $r_user_id > 0 ? get_userdata($r_user_id) : null;
echo '<tr>';
echo '<td>' . ($r_user ? esc_html($r_user->display_name) : esc_html((string) $r_user_id)) . '</td>';
echo '<td>' . $status_html . '</td>';
echo '<td>' . esc_html((string) ($r['created_at'] ?? '')) . '</td>';
echo '<td>' . esc_html(!empty($r['expires_at']) ? (string) $r['expires_at'] : __('永久', 'aksrt')) . '</td>';
echo '<td>' . nl2br(esc_html((string) ($r['reason'] ?? ''))) . '</td>';
echo '<td>';
if (!$expired && $is_global) {
echo '<a class="button" href="' . esc_url(add_query_arg(['action' => 'unban', 'user_id' => $r_user_id], $page_url)) . '">' . esc_html(($type === 'ban' ? __('解封', 'aksrt') : __('移出小黑屋', 'aksrt'))) . '</a>';
} elseif (!$expired && $type === 'mute') {
echo '<a class="button" href="' . esc_url(add_query_arg(['action' => 'unban', 'user_id' => $r_user_id], $page_url)) . '">' . esc_html__('解除禁言', 'aksrt') . '</a>';
}
echo '</td></tr>';
}
echo '</tbody></table>';
// 依赖联动(参考主题 dependency-box 同语义):禁封方式说明/其它原因/禁止申诉说明
echo '<script>
(function ($) {
function syncBanForm() {
$(".aksrt-ban-modal").each(function () {
var $box = $(this);
var ban = $box.find("input[name=\\"ban\\"]:checked").val();
$box.find("[data-aksrt-ban-desc]").each(function () {
$(this).toggle($(this).attr("data-aksrt-ban-desc") === ban);
});
$box.find(".aksrt-reason-other").toggle($box.find("input[name=\\"reason\\"]:checked").val() === "other");
$box.find(".aksrt-no-appeal-desc").toggle($box.find("input[name=\\"no_appeal\\"]").is(":checked"));
});
}
$(document).on("change", ".aksrt-ban-modal input", syncBanForm);
$(document).ready(syncBanForm);
})(jQuery);
</script>';
}
public function handle_update_report(): void {
if (!current_user_can(self::CAPABILITY)) {
@@ -541,103 +302,6 @@ class UsersMenus {
$this->redirect('aksrt_user_ban', $ok ? 'report_updated' : 'report_failed', ['tab' => 'tickets']);
}
public function handle_add_restriction(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
check_admin_referer('aksrt_users_add_restriction');
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
if ($user_id <= 0 || !get_userdata($user_id)) {
$this->redirect('aksrt_user_ban', 'restriction_failed', ['tab' => 'restrictions']);
}
// 禁封方式(参考主题语义):1=拉入小黑屋(suspend) / 2=禁封用户(ban,禁止登录)
$ban = isset($_POST['ban']) ? (int) $_POST['ban'] : 1;
$type = $ban === 2 ? 'ban' : 'suspend';
$time_day = isset($_POST['time_day']) ? (int) $_POST['time_day'] : 0;
$desc = isset($_POST['desc']) ? sanitize_textarea_field((string) wp_unslash($_POST['desc'])) : '';
// 原因:预置徽章值或“其它原因”自定义文本
$reason = isset($_POST['reason']) ? sanitize_text_field((string) wp_unslash($_POST['reason'])) : '';
if ($reason === '' || $reason === 'other') {
$reason = isset($_POST['reason_other']) ? sanitize_text_field((string) wp_unslash($_POST['reason_other'])) : '';
}
if ($reason === '') {
$this->redirect('aksrt_user_ban', 'restriction_failed', ['tab' => 'restrictions', 'user_id' => $user_id]);
}
if ($desc !== '') {
$reason .= "\n" . sprintf(__('禁封说明:%s', 'aksrt'), $desc);
}
$no_appeal = !empty($_POST['no_appeal']);
$no_appeal_desc = isset($_POST['no_appeal_desc']) ? sanitize_textarea_field((string) wp_unslash($_POST['no_appeal_desc'])) : '';
if ($no_appeal && $no_appeal_desc === '') {
// 参考主题同校验:勾选禁止申诉必须填写说明
$this->redirect('aksrt_user_ban', 'restriction_failed', ['tab' => 'restrictions', 'user_id' => $user_id]);
}
$expires = $time_day > 0 ? date('Y-m-d H:i:s', strtotime('+' . $time_day . ' day', current_time('timestamp'))) : '';
$result = \AKSRT\User\Restriction::get_instance()->restrict($user_id, $type, $reason, 'global', $expires, get_current_user_id());
$ok = !empty($result['success']);
if ($ok) {
// 禁封状态变更站内信(参考主题 zib_updata_user_ban_msg 同语义)
$msg = $type === 'ban' ? __('您的账号已被禁封,将无法登录。', 'aksrt') : __('您已被拉入小黑屋,发布等相关权限已被限制。', 'aksrt');
if ($time_day > 0) {
$msg .= ' ' . sprintf(__('期限:%1$d 天,%2$s 起自动解封。', 'aksrt'), $time_day, $expires);
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'system', $msg, ['title' => __('账号限制', 'aksrt')]);
if ($no_appeal) {
update_user_meta($user_id, '_aksrt_no_appeal', [
'desc' => $no_appeal_desc,
'at' => current_time('mysql'),
]);
}
// 删除全部内容:文章/帖子/评论移至回收站(参考主题同行为)
if (!empty($_POST['delete_content'])) {
$post_ids = get_posts([
'author' => $user_id,
'post_type' => get_post_types(['public' => true]),
'post_status' => 'publish',
'numberposts' => -1,
'fields' => 'ids',
]);
foreach ($post_ids as $pid) {
wp_trash_post((int) $pid);
}
foreach (get_comments(['user_id' => $user_id, 'status' => 'approve', 'number' => 500]) as $comment) {
wp_trash_comment((int) $comment->comment_ID);
}
}
}
$this->redirect('aksrt_user_ban', $ok ? 'restriction_added' : 'restriction_failed', ['tab' => 'restrictions', 'user_id' => $user_id]);
}
public function handle_remove_restriction(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
}
$user_id = isset($_POST['user_id']) ? (int) $_POST['user_id'] : 0;
check_admin_referer('aksrt_users_unban_' . $user_id);
$desc = isset($_POST['desc']) ? sanitize_textarea_field((string) wp_unslash($_POST['desc'])) : '';
$removed = $user_id > 0 && class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->lift_user_restriction($user_id)
: 0;
delete_user_meta($user_id, '_aksrt_no_appeal');
// 解封站内信(含给用户的留言)
$msg = __('您的账号限制已解除。', 'aksrt');
if ($desc !== '') {
$msg .= ' ' . sprintf(__('留言:%s', 'aksrt'), $desc);
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'system', $msg, ['title' => __('限制解除', 'aksrt')]);
$this->redirect('aksrt_user_ban', $removed > 0 ? 'restriction_removed' : 'restriction_failed', ['tab' => 'restrictions', 'user_id' => $user_id]);
}
public function handle_process_appeal(): void {
if (!current_user_can(self::CAPABILITY)) {
wp_die(esc_html__('权限不足', 'aksrt'));
+3 -1
View File
@@ -477,7 +477,9 @@ trait UsersMenusTickets {
if (class_exists('\AKSRT\User\Restriction') && \AKSRT\User\Restriction::get_instance()->is_restricted($target_id)) {
echo $this->ticket_ban_info_html($target_id);
} else {
$ban_url = admin_url('users.php?page=aksrt_user_ban&tab=restrictions&user_id=' . $target_id);
// 与参考主题一致:跳转到前台该用户公开主页,经封面右下角三点
// 菜单的「禁封此用户」打开封禁弹窗完成处理(后台不提供禁封表单)。
$ban_url = function_exists('aksrt_get_user_home_url') ? aksrt_get_user_home_url($target_id) : get_author_posts_url($target_id);
echo '<div style="color:#16a60a;">' . esc_html__('用户状态正常', 'aksrt') . '</div>';
echo '<p class="description">' . esc_html__('请查阅相关资料以及举证,如举报事实清楚,可以', 'aksrt');
echo ' <a target="_blank" href="' . esc_url($ban_url) . '">' . esc_html__('点击此处', 'aksrt') . '</a> ';
+15
View File
@@ -310,6 +310,21 @@ class UserController extends ControllerBase {
'callback' => [$this, 'get_restriction_stats'],
'permission_callback' => [$this, 'check_permission'],
]);
// 前台禁封弹窗(1:1 复刻参考主题 set_user_ban_modal / zib_get_set_user_ban_modal)。
// modal 内容每次拉取(restriction 状态可能已变),不缓存。
register_rest_route(self::API_NAMESPACE, '/user-ban/modal', [
'methods' => 'GET',
'callback' => [$this, 'get_user_ban_modal'],
'permission_callback' => [$this, 'check_permission'],
]);
// 前台禁封 / 解封提交(对齐参考主题 wp_ajax_set_user_ban)。
register_rest_route(self::API_NAMESPACE, '/user-ban/set', [
'methods' => 'POST',
'callback' => [$this, 'set_user_ban'],
'permission_callback' => [$this, 'check_permission'],
]);
}
@@ -79,4 +79,135 @@ trait UserControllerRestrictionOperations {
$stats = \AKSRT\User\Restriction::get_instance()->get_stats();
return $this->success(['data' => $stats]);
}
/**
* 前台禁封弹窗内容(1:1 复刻参考主题 zib_get_set_user_ban_modal,
* 经 aksrt_get_user_ban_modal 渲染;参考主题 refresh_modal 每次刷新同语义)。
*/
public function get_user_ban_modal(\WP_REST_Request $request): \WP_REST_Response {
$user_id = (int) $request->get_param('user_id');
if ($user_id <= 0 || !get_userdata($user_id)) {
return $this->error('用户不存在', 404);
}
if (!function_exists('aksrt_get_user_ban_modal')) {
return $this->error('禁封组件未加载', 501);
}
$html = aksrt_get_user_ban_modal($user_id);
if ($html === '') {
return $this->error('抱歉,您暂时没有此权限', 403);
}
return $this->success(['data' => ['html' => $html]]);
}
/**
* 前台禁封 / 解封提交(1:1 复刻参考主题 zib_ajax_set_user_ban 语义):
* ban=0 解封(留言站内信);ban=1 拉入小黑屋 / ban=2 禁封用户,
* 支持禁封时长、预置/其它原因、禁封说明、删除全部内容、禁止申诉(含说明)。
*/
public function set_user_ban(\WP_REST_Request $request): \WP_REST_Response {
$params = $request->get_json_params() ?? [];
$user_id = (int) ($params['user_id'] ?? 0);
if ($user_id <= 0 || !get_userdata($user_id)) {
return $this->error('用户不存在或参数传入错误', 404);
}
if (!function_exists('aksrt_current_user_can_ban') || !aksrt_current_user_can_ban($user_id)) {
return $this->error('抱歉,您暂时没有此权限', 403);
}
if (!class_exists('\AKSRT\User\Restriction')) {
return $this->error('限制系统未启用', 501);
}
$svc = \AKSRT\User\Restriction::get_instance();
$ban = (int) ($params['ban'] ?? 0);
$desc = isset($params['desc']) ? sanitize_textarea_field((string) wp_unslash($params['desc'])) : '';
// ── 解封 ──
if (0 === $ban) {
$svc->lift_user_restriction($user_id);
delete_user_meta($user_id, '_aksrt_no_appeal');
$this->send_ban_notice($user_id, __('您的账号限制已解除。', 'aksrt'), __('限制解除', 'aksrt'), $desc);
return $this->success(['message' => __('已解封此用户', 'aksrt'), 'data' => ['banned' => false]]);
}
// ── 禁封 ──
$type = 2 === $ban ? \AKSRT\User\Restriction::TYPE_BAN : \AKSRT\User\Restriction::TYPE_SUSPEND;
$reason = isset($params['reason']) ? sanitize_text_field((string) wp_unslash($params['reason'])) : '';
if ($reason === '' || $reason === 'other') {
$reason = isset($params['reason_other']) ? sanitize_text_field((string) wp_unslash($params['reason_other'])) : '';
}
if ($reason === '') {
return $this->error(__('请输入禁封原因', 'aksrt'));
}
$time_day = isset($params['time_day']) ? (int) $params['time_day'] : 0;
$expires = $time_day > 0 ? date('Y-m-d H:i:s', strtotime('+' . $time_day . ' day', current_time('timestamp'))) : '';
$no_appeal = !empty($params['no_appeal']);
$no_appeal_desc = isset($params['no_appeal_desc']) ? sanitize_textarea_field((string) wp_unslash($params['no_appeal_desc'])) : '';
if ($no_appeal && $no_appeal_desc === '') {
return $this->error(__('请输入禁止用户申诉的说明或原因', 'aksrt'));
}
// 禁封说明并入原因列(与后台工单禁封表单同存储语义)。
if ($desc !== '') {
$reason .= "\n" . sprintf(__('禁封说明:%s', 'aksrt'), $desc);
}
$result = $svc->restrict($user_id, $type, $reason, \AKSRT\User\Restriction::SCOPE_GLOBAL, $expires, get_current_user_id());
if (empty($result['success'])) {
return $this->error((string) ($result['message'] ?? __('操作失败', 'aksrt')));
}
if ($no_appeal) {
update_user_meta($user_id, '_aksrt_no_appeal', [
'desc' => $no_appeal_desc,
'at' => current_time('mysql'),
]);
}
// 站内信(后台禁封表单 / 参考主题 zib_updata_user_ban_msg 同语义)。
$notice = $type === \AKSRT\User\Restriction::TYPE_BAN
? __('您的账号已被禁封,将无法登录。', 'aksrt')
: __('您已被拉入小黑屋,发布等相关权限已被限制。', 'aksrt');
if ($time_day > 0) {
$notice .= ' ' . sprintf(__('期限:%1$d 天,%2$s 起自动解封。', 'aksrt'), $time_day, $expires);
}
$this->send_ban_notice($user_id, $notice, __('账号限制', 'aksrt'), $desc);
// 删除全部内容:全部公开内容与已审核评论移至回收站(参考主题同行为)。
if (!empty($params['delete_content'])) {
$post_ids = get_posts([
'author' => $user_id,
'post_type' => get_post_types(['public' => true]),
'post_status' => 'publish',
'numberposts' => -1,
'fields' => 'ids',
]);
foreach ($post_ids as $pid) {
wp_trash_post((int) $pid);
}
foreach (get_comments(['user_id' => $user_id, 'status' => 'approve', 'number' => 500]) as $comment) {
wp_trash_comment((int) $comment->comment_ID);
}
}
$send_msg = 2 === $ban ? __('已禁封此用户', 'aksrt') : __('已将此用户拉入小黑屋', 'aksrt');
return $this->success(['message' => $send_msg, 'data' => ['banned' => true]]);
}
/**
* 禁封 / 解封站内信(留言拼附在正文之后)。
*/
private function send_ban_notice(int $user_id, string $notice, string $title, string $desc = ''): void
{
if (!class_exists('\AKSRT\Messages\Message')) {
return;
}
$msg = $notice;
if ($desc !== '') {
$msg .= ' ' . sprintf(__('留言:%s', 'aksrt'), $desc);
}
\AKSRT\Messages\Message::get_instance()->send($user_id, 'system', $msg, ['title' => $title]);
}
}
+12
View File
@@ -279,6 +279,8 @@ trait ScriptsPart1b {
// 举报脚本:举报入口渲染于文章页 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)) {
@@ -290,6 +292,16 @@ trait ScriptsPart1b {
true
);
}
$user_ban_js_path = get_template_directory() . '/assets/js/user-ban.js';
if (file_exists($user_ban_js_path)) {
wp_enqueue_script(
'aksrt-user-ban',
$base_uri . '/assets/js/user-ban.js',
['loader_js'],
filemtime($user_ban_js_path) ?: $version,
true
);
}
}
// 用户中心 / 公开主页:路由专属 CSS
+2 -1
View File
@@ -87,7 +87,8 @@ class ReportCenter {
$content,
[
'title' => __('用户举报', 'aksrt'),
'target_url' => admin_url('users.php?page=aksrt_user_ban&tab=tickets'),
// 直达待处理举报队列(参考主题 admin.php?page=user_ban&type=report&status=0 同语义)。
'target_url' => admin_url('users.php?page=aksrt_user_ban&type=report&status=pending'),
]
);
}
+1 -1
View File
@@ -147,7 +147,7 @@ final class MailNotifications {
public static function ban_appeal_submitted(int $user_id, array $payload): void {
$user = get_userdata($user_id);
if ($user instanceof \WP_User) self::send_admin('ban_appeal_admin', ['username' => $user->display_name, 'url' => admin_url('users.php?page=aksrt_user_ban&tab=appeals')]);
if ($user instanceof \WP_User) self::send_admin('ban_appeal_admin', ['username' => $user->display_name, 'url' => admin_url('users.php?page=aksrt_user_ban&type=appeal&status=pending')]);
}
public static function ban_appeal_processed(int $user_id, string $decision, string $note): void {
+2 -1
View File
@@ -141,7 +141,8 @@ class BanAppeal {
$summary,
[
'title' => __('禁封申诉', 'aksrt'),
'target_url' => admin_url('users.php?page=aksrt_user_ban&tab=appeals'),
// 直达待处理申诉队列(与工单页 type=appeal&status=pending 过滤同参)。
'target_url' => admin_url('users.php?page=aksrt_user_ban&type=appeal&status=pending'),
]
);
}
+13 -2
View File
@@ -61,6 +61,13 @@ $show_header_notification = !empty($notification_settings['enabled'])
&& in_array('user_home_header', (array) ($notification_settings['icon_show'] ?? []), true);
$messages_url = $show_header_notification && function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '';
$unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0;
// 封面三点菜单末项:管理员 → 禁封/解封(参考主题 author_header_drop_lists 钩子语义,
// 禁封权限优先于举报);否则 → 举报用户(保持既有契约)。
$cover_ban_html = function_exists('aksrt_get_user_cover_menu_action_html')
? aksrt_get_user_cover_menu_action_html($user_id)
: '';
$show_cover_report = $cover_ban_html === '' && $show_report && !$is_own;
$show_cover_menu = $has_more_details || $show_cover_report || $cover_ban_html !== '';
?><div class="aksrt-author-hero<?php echo $show_cover ? '' : ' aksrt-author-hero--no-cover'; ?>">
<?php if ($show_cover) : ?>
<div class="aksrt-author-hero__cover">
@@ -74,12 +81,12 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0
</span>
</div>
<?php endif; ?>
<?php if ($has_more_details || ($show_report && !$is_own)) : ?>
<?php if ($show_cover_menu) : ?>
<div class="aksrt-cover-menu aksrt-cover-menu--author" data-aksrt-menu>
<button type="button" class="aksrt-cover-menu__trigger" aria-haspopup="true" aria-expanded="false" aria-label="<?php esc_attr_e('个人主页操作', 'aksrt'); ?>"><i class="fa fas fa-ellipsis-h" aria-hidden="true"></i></button>
<div class="aksrt-cover-menu__panel" role="menu" hidden>
<?php if ($has_more_details) : ?><button type="button" role="menuitem" data-aksrt-profile-details><i class="fa fas fa-address-card" aria-hidden="true"></i><?php esc_html_e('更多资料', 'aksrt'); ?></button><?php endif; ?>
<?php if ($show_report && !$is_own) : ?>
<?php if ($show_cover_report) : ?>
<?php
// 统一举报契约:report.js 弹窗展示被举报对象(头像 + 名称)。
$report_payload = [
@@ -92,6 +99,10 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0
<?php if (is_user_logged_in()) : ?><button type="button" role="menuitem" data-aksrt-report="<?php echo esc_attr(wp_json_encode($report_payload)); ?>"><i class="fa fas fa-flag" aria-hidden="true"></i><?php esc_html_e('举报用户', 'aksrt'); ?></button>
<?php else : ?><a role="menuitem" href="<?php echo esc_url(aksrt_get_login_url($home_url)); ?>"><i class="fa fas fa-flag" aria-hidden="true"></i><?php esc_html_e('举报用户', 'aksrt'); ?></a><?php endif; ?>
<?php endif; ?>
<?php
// 禁封/解封入口(user-ban.js 的 [data-aksrt-ban] 委托)。
echo $cover_ban_html; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper escapes internally.
?>
</div>
</div>
<?php endif; ?>
@@ -17,6 +17,8 @@ const home = read('src/User/Home.php')
const commentItem = read('inc/helpers/comments/item.php')
const functions = read('functions.php')
const uninstall = read('uninstall.php')
const userBanHelper = read('inc/helpers/user-ban.php')
const userBanJs = read('assets/js/user-ban.js')
test('report REST surface is canonical: submit + users/reports only', () => {
assert.match(userController, /'\/report\/submit'/)
@@ -147,7 +149,7 @@ test('admin report notification links to the real page', () => {
assert.match(reportCenter, /notify_admins_new_report\(/)
assert.match(reportCenter, /notify_reporter_status_changed\(/)
assert.match(reportCenter, /Message::get_instance\(\)->send\(/)
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&tab=tickets/)
assert.match(reportCenter, /users\.php\?page=aksrt_user_ban&type=report&status=pending/)
})
test('native ban page mirrors the reference theme layout', () => {
@@ -201,10 +203,18 @@ test('native ban page mirrors the reference theme layout', () => {
// 旧的三 tab 结构已收敛;举报处理表单不包含处罚下拉(对齐参考主题)
assert.ok(!usersMenus.includes('tab=reports'), 'old reports tab is gone')
assert.ok(!usersMenus.includes('tab=appeals'), 'old appeals tab is gone')
// 禁封限制 Tab 已移除(参考主题无此后台 Tab):禁封改走前台封禁弹窗。
assert.ok(!usersMenus.includes('tab=restrictions'), 'restrictions tab is gone')
assert.ok(!usersMenus.includes('render_restrictions_tab'), 'restrictions tab renderer is gone')
assert.ok(!usersMenus.includes('handle_add_restriction'), 'backend ban form handler is gone')
assert.ok(!usersMenus.includes('handle_remove_restriction'), 'backend unban form handler is gone')
assert.ok(!usersMenus.includes('render_reports_tab'))
assert.ok(!usersMenus.includes('render_appeals_tab'))
assert.match(usersMenus, /use UsersMenusTickets;/)
assert.ok(!tickets.includes('name="action_taken"'), 'report process form should be status-only like the reference theme')
// 工单处理页「点击此处」跳转前台用户公开主页(封面三点菜单 → 封禁弹窗)。
assert.match(tickets, /aksrt_get_user_home_url\(\$target_id\)/)
assert.ok(!tickets.includes('tab=restrictions'), 'ticket ban link must not point at the removed backend tab')
})
test('report modal mirrors the reference theme: preset reasons, image evidence, notice, geometry', () => {
@@ -245,38 +255,71 @@ test('comments expose a report entry for other users when reporting is enabled',
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/)
// 下拉菜单(置顶/编辑/批准/删除 + 禁封或举报)末项走禁封优先、否则举报。
assert.match(commentItem, /function aksrt_get_comments_action_dropdown_html[\s\S]*?\$lists \.= aksrt_get_comment_ban_or_report_item/)
// 禁封项(管理员可见):参考主题 comments_action_lists 钩子语义,渲染于 user-ban.php。
assert.match(userBanHelper, /comment-ban-link/)
assert.match(userBanHelper, /data-aksrt-ban="/)
// 举报不作为 comt-meta 独立按钮直接渲染。
assert.doesNotMatch(commentItem, /\$report\s*=\s*aksrt_get_comment_report_html\(\$comment\)/)
})
test('restrictions tab mirrors the reference ban modal 1:1', () => {
// 禁封方式/禁封时间/预置原因徽章/禁封说明/删除全部内容/禁止用户申诉,
// 以及已禁封用户的状态信息与解封留言表单(zib_get_set_user_ban_modal 同构)。
for (const label of ['拉入小黑屋', '禁封用户', '禁封方式', '禁封时间', '选择禁封原因', '其它原因', '禁封说明', '删除全部内容', '禁止用户申诉', '请填写禁止申诉说明', '解封用户', '移出小黑屋', '解除禁言', '小黑屋禁封中', '禁封中(禁止登录)', '禁言中', '给用户的留言', '开始时间', '结束时间']) {
assert.ok(usersMenus.includes(label), `restrictions tab missing reference label: ${label}`)
test('frontend ban modal mirrors the reference theme zib_get_set_user_ban_modal 1:1', () => {
// 禁封方式/禁封时间/预置原因徽章/其它原因/禁封说明/删除全部内容/禁止用户申诉,
// 以及已禁封用户的状态信息与解封留言表单(弹窗经 REST /user-ban/modal 输出)。
for (const label of ['拉入小黑屋', '禁封用户', '禁封方式', '禁封时间', '选择禁封原因', '其它原因', '禁封说明', '删除全部内容', '禁止用户申诉', '请填写禁止申诉说明', '解封用户', '移出小黑屋', '禁封中(禁止登录)', '小黑屋', '给用户的留言', '开始时间', '结束时间', '原因', '说明', '禁封记录', '确认禁封']) {
assert.ok(userBanHelper.includes(label), `ban modal missing reference label: ${label}`)
}
// 禁言(mute)记录在操作列提供“解除禁言”入口,且解除视图能选中 mute 记录
// (此前操作列对 mute 为空、顶部视图只认 ban/suspend,禁言无法从后台解除)。
assert.match(usersMenus, /\$type === 'mute'/)
assert.match(usersMenus, /'mute'\s*=>\s*__\('解除禁言', 'aksrt'\)/)
assert.match(usersMenus, /\$unban && \$current === null/)
// 时长选项 1/7/14/30/100/永久(默认 7 天);类型映射:小黑屋→suspend,禁封→ban。
assert.match(usersMenus, /1 => __\('1天', 'aksrt'\)/)
assert.match(usersMenus, /7 => __\('7天', 'aksrt'\)/)
assert.match(usersMenus, /100 => __\('100天', 'aksrt'\)/)
assert.match(usersMenus, /checked\(7, \$days, false\)/)
assert.match(usersMenus, /\$type\s*=\s*\$ban === 2 \? 'ban' : 'suspend'/)
assert.match(userBanHelper, /1 => __\('1天', 'aksrt'\)/)
assert.match(userBanHelper, /7 => __\('7天', 'aksrt'\)/)
assert.match(userBanHelper, /100 => __\('100天', 'aksrt'\)/)
assert.match(userBanHelper, /7 === \$days \? ' checked="checked"' : ''/)
// 预置原因来自 UserRuntime::ban_settings(与后台同一数据源)。
assert.match(userBanHelper, /UserRuntime::ban_settings\(\)\['reasons'\]/)
// 勾选禁止申诉必须填写说明(参考主题同校验),落用户 meta 供申诉入口拦截。
assert.match(usersMenus, /_aksrt_no_appeal/)
assert.match(usersMenus, /请填写禁止申诉说明/)
assert.match(userBanHelper, /_aksrt_no_appeal|no_appeal/)
const appeal = read('src/User/BanAppeal.php')
assert.match(appeal, /_aksrt_no_appeal/)
// 解封流程:按用户解封 + 留言站内信(旧 restriction_id 通道退役)。
assert.match(usersMenus, /wp_nonce_field\('aksrt_users_unban_' \. \$target_id\)/)
assert.match(usersMenus, /lift_user_restriction\(\$user_id\)/)
assert.ok(!usersMenus.includes('name="restriction_id"'), 'per-record removal superseded by per-user unban')
// 弹窗入口按钮契约:[data-aksrt-ban] 委托(封面按钮 / 评论菜单项 / 文章作者菜单项)。
assert.match(userBanHelper, /data-aksrt-ban="/)
assert.match(userBanHelper, /function aksrt_get_user_cover_menu_action_html/)
assert.match(userBanHelper, /function aksrt_get_comment_ban_or_report_item/)
assert.match(userBanHelper, /function aksrt_get_post_author_ban_item/)
// 权限模型(参考主题 zib_current_user_can('set_user_ban') 语义):
// manage_options + 非本人 + 非超级管理员 + 功能开关。
assert.match(userBanHelper, /current_user_can\('manage_options'\)/)
assert.match(userBanHelper, /\$current_id === \$user_id/)
assert.match(userBanHelper, /is_super_admin\(\$user_id\)/)
assert.match(userBanHelper, /user_ban_s/)
// REST 路由:modal 拉取 + set 提交(permission_callback 校验)。
assert.match(userController, /'\/user-ban\/modal'/)
assert.match(userController, /'\/user-ban\/set'/)
// 解封视图多彩横幅(参考主题 zib_get_modal_colorful_header('c-yellow', ...))。
assert.match(userBanHelper, /modal-colorful-header colorful-bg c-yellow/)
// 提交后整页刷新(禁封 reload:true)/ 仅关闭弹窗(解封 hide_modal:true)。
assert.match(userBanJs, /window\.location\.reload\(\)/)
assert.match(userBanJs, /AksrtModalRuntime\.close\('submit'\)/)
assert.match(userBanJs, /data-aksrt-ban-submit/)
assert.match(userBanJs, /data-aksrt-unban-submit/)
// 桌面 600px(参考主题未传 data-class → Bootstrap .modal-dialog 默认宽)
// + min-height 360px(data-height=360);移动端底部抽屉。
assert.match(userBanJs, /MODAL_HEIGHT = 360/)
assert.match(userBanJs, /mobileBottom: true/)
const css = read('assets/css/frontend/modal-runtime.css')
assert.match(css, /\.aksrt-user-ban-modal:not\(\.aksrt-modal-runtime--bottom\) \.aksrt-modal-runtime__dialog/)
assert.match(css, /600px/)
// textarea autoHeight 1:1(参考主题 maxHeight 锁高 + 边框补偿)。
assert.match(userBanJs, /getAttribute\('maxHeight'\)/)
// 三个入口模板接线:封面三点菜单 / 评论区三点菜单 / 文章作者 metabox 三点菜单。
const authorHero = read('templates/parts/user/author-hero.php')
assert.match(authorHero, /aksrt_get_user_cover_menu_action_html\(\$user_id\)/)
assert.match(commentItem, /aksrt_get_comment_ban_or_report_item\(\$comment\)/)
const singleComponents = read('inc/helpers/aksrt-single-components.php')
assert.match(singleComponents, /aksrt_get_post_author_ban_item\(/)
// 脚本入队(singular + profile,与 report.js 同组)。
const scripts = read('src/Core/ScriptsPart1b.php')
assert.match(scripts, /'aksrt-user-ban'/)
})
test('uninstall cleans up every ban/report meta key written by the theme', () => {