Files
kate522 5201f2ab6f fix(ban): hide scrollbars entirely in ban modals
申诉弹窗等内容区的细滚动条(mini-scrollbar 悬停显现的 3px 滚动条)也不予保留:
三个禁封弹窗(禁封/解封 max-vh7、申诉 max-vh5、禁封记录 max-vh5)的滚动容器
统一改用 no-scrollbar(scrollbar-width:none + ::-webkit-scrollbar{display:none}),
滚动能力保留但任何状态下都不显示滚动条。契约测试锁定:ban 相关helper 不得
再出现 mini-scrollbar。
2026-09-22 19:53:03 +08:00

775 lines
41 KiB
PHP
Raw Permalink 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.
<?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(禁封 / 解封弹窗)
* zib_get_ban_info_modal → aksrt_get_user_ban_info_modal(查看禁封状态)
* zib_get_user_ban_appeal_modal → aksrt_get_user_ban_appeal_modal(禁封申诉)
* zib_get_ban_log_modal → aksrt_get_user_ban_log_modal(禁封记录)
* zib_user_ban_restrict_login → aksrt_render_user_ban_block_page(登录后禁封页)
*
* 数据源唯一:\AKSRT\User\Restriction(wp_aksrt_user_restriction 表)。
* 弹窗内容经 REST /user-ban/*-modal 输出(AksrtModalRuntime.openRemote 拉取,
* 与参考主题 refresh_modal 每次刷新同语义),提交走 REST /user-ban/set 与
* admin-ajax aksrt_ban_appeal_submit。
*
* @package AKSRT
*/
defined('ABSPATH') || exit;
/**
* 用户治理展示态(禁封 / 小黑屋 / 禁言),含请求级缓存。
*
* 单一数据源为 \AKSRT\User\Restriction::get_display_state() / state_badges()
* (颜色 / 文案 / 图标统一管理);本 helper 供身份行徽章、用户中心元信息、
* 评论拒绝态等位置复用,避免重复查库。
*
* 对齐参考主题:
* zib_user_is_ban → aksrt_get_user_ban_state(状态判定)
* zib_ban_author_header_identity_filter → 资料卡/用户中心身份行徽章
*
* @return array{state:string,label:string,tone:string,icon:string}|array{} state 为空串表示无治理状态。
*/
function aksrt_get_user_ban_state(int $user_id): array
{
static $cache = [];
if ($user_id <= 0) {
return [];
}
if (isset($cache[$user_id])) {
return $cache[$user_id];
}
$state = '';
if (class_exists('\AKSRT\User\Restriction')) {
$state = (string) \AKSRT\User\Restriction::get_instance()->get_display_state($user_id);
}
if ($state === '') {
return $cache[$user_id] = [];
}
$badges = class_exists('\AKSRT\User\Restriction') ? \AKSRT\User\Restriction::state_badges() : [];
$badge = is_array($badges[$state] ?? null) ? $badges[$state] : [];
return $cache[$user_id] = [
'state' => $state,
'label' => (string) ($badge['label'] ?? ''),
'tone' => (string) ($badge['tone'] ?? 'c-gray'),
'icon' => (string) ($badge['icon'] ?? 'ban'),
];
}
/**
* 目标是否处于封禁/小黑屋状态(举报入口隐藏判定,参考主题
* zib_get_report_link 中 zib_user_is_ban($user_id) 的排除语义;
* 禁言为轻量评论域限制,不隐藏举报入口)。
*/
function aksrt_is_user_ban_restricted(int $user_id): bool
{
$state = aksrt_get_user_ban_state($user_id);
return in_array((string) ($state['state'] ?? ''), ['banned', 'restricted'], true);
}
/**
* 治理状态徽章(占据身份行元信息位,参考主题 author_header_identity 钩子 /
* zib_ban_author_header_identity_filter:badg + 图标 + 文案,红=封号、
* 黄=小黑屋、灰=禁言,字段与颜色由 Restriction::state_badges() 统一管理)。
*/
function aksrt_get_user_ban_identity_badge(int $user_id, string $class = ''): string
{
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$classes = trim('badg ' . (string) $state['tone'] . ($class !== '' ? ' ' . $class : ''));
return '<span class="' . esc_attr($classes) . '" title="' . esc_attr((string) $state['label']) . '">'
. '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . ' mr6" aria-hidden="true"></i>'
. esc_html((string) $state['label']) . '</span>';
}
/**
* 「查看禁封状态」入口(用户中心元信息位,参考主题 user_page_header_desc
* 钩子 / zib_get_user_ban_info_link:but 按钮 = 内层 badg 徽章(带色)+
* 右箭头,外层 but 为默认按钮色——不在内外两层重复着色)。
* 点击经 data-aksrt-ban-info 委托打开禁封信息弹窗。
*/
function aksrt_get_user_ban_info_link(int $user_id, string $class = ''): string
{
if (!aksrt_user_ban_enabled()) {
return '';
}
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$badge = aksrt_get_user_ban_identity_badge($user_id);
if ($badge === '') {
return '';
}
// 外层 but 不重复状态色:颜色仅由内层 badg 承载(1:1 参考主题)。
$classes = trim('but p2-10' . ($class !== '' ? ' ' . $class : ''));
return '<a class="' . esc_attr($classes) . '" href="javascript:;" role="button"'
. ' data-aksrt-ban-info="' . esc_attr((string) $user_id) . '"'
. ' title="' . esc_attr__('查看禁封状态', 'aksrt') . '">' . $badge
. '<i class="fa fas fa-angle-right em12 ml6" aria-hidden="true"></i></a>';
}
/**
* 禁封信息弹窗(1:1 复刻参考主题 zib_get_ban_info_modal:多彩头 c-gray +
* 禁封信息行 + 申诉入口;本人或管理员可见)。
*
* @return string 弹窗 HTML(空串 = 无权查看或用户不存在)
*/
function aksrt_get_user_ban_info_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user) {
return '';
}
$viewer_id = get_current_user_id();
$can_view = $viewer_id > 0 && ($viewer_id === $user_id || aksrt_current_user_can_ban($user_id));
if (!$can_view) {
return '';
}
$info = class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
$html = '<div class="aksrt-user-ban-info-modal">';
// 弹窗头(参考主题 zib_get_modal_colorful_header('c-gray', ban 图标, 标题))。
$html .= '<div class="modal-colorful-header colorful-bg c-gray"><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__('小黑屋禁封中', 'aksrt') . '</div></div></div>';
if (empty($info)) {
$html .= '<div class="text-center c-yellow" style="padding: 50px 0;">' . esc_html__('您已解封,请刷新页面', 'aksrt') . '</div></div>';
return $html;
}
$set_row = static function (string $label, string $value): string {
if ($value === '') {
return '';
}
return '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html($label) . '</div><div class="author-set-right mt6">' . esc_html($value) . '</div></div>';
};
$html .= '<div class="mb20">';
$html .= $set_row(__('用户名', 'aksrt'), $user->display_name);
$html .= $set_row(__('状态', 'aksrt'), (string) $info['status']);
$html .= $set_row(__('开始时间', 'aksrt'), (string) $info['start']);
$html .= $set_row(__('结束时间', 'aksrt'), (string) $info['end']);
$html .= $set_row(__('原因', 'aksrt'), (string) $info['reason']);
$html .= $set_row(__('说明', 'aksrt'), (string) $info['desc']);
$html .= '</div>';
if (!empty($info['no_appeal'])) {
$html .= '<div class="muted-box em09"><div class="c-yellow">' . esc_html__('您当前已无法进行申诉', 'aksrt') . '</div>';
if ((string) $info['no_appeal_desc'] !== '') {
$html .= '<div class="mt6">' . esc_html((string) $info['no_appeal_desc']) . '</div>';
}
$html .= '</div>';
$html .= '<div class="mt20 but-average modal-buts"><button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button></div>';
} else {
$appeal_link = $viewer_id === $user_id ? aksrt_get_user_ban_appeal_link($user_id, 'but c-blue') : '';
$html .= '<div class="mt20 but-average modal-buts">';
$html .= '<button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>';
$html .= $appeal_link !== '' ? $appeal_link : '';
$html .= '</div>';
}
$html .= '</div>';
return $html;
}
/**
* 「申诉」入口(参考主题 zib_get_user_ban_appeal_link:禁封申诉 modal-mini,
* 仅本人可发起;点击经 data-aksrt-ban-appeal 委托打开申诉弹窗)。
*/
function aksrt_get_user_ban_appeal_link(int $user_id, string $class = ''): string
{
if ($user_id <= 0 || get_current_user_id() !== $user_id) {
return '';
}
$classes = trim((string) ($class !== '' ? $class : 'but c-blue'));
return '<a class="' . esc_attr($classes) . '" href="javascript:;" role="button"'
. ' data-aksrt-ban-appeal="' . esc_attr((string) $user_id) . '">'
. '<i class="fa fas fa-gavel mr6" aria-hidden="true"></i>'
. esc_html__('申诉', 'aksrt') . '</a>';
}
/**
* 禁封申诉弹窗(1:1 复刻参考主题 zib_get_user_ban_appeal_modal:
* edit-header(gavel / c-blue / 禁封申诉)+ 禁封信息行 + 申诉说明 + 资料项
* 输入 + 审核中态 + 禁止申诉态;仅本人可见可提交)。
*
* @return string 弹窗 HTML(空串 = 非本人或用户不存在)
*/
function aksrt_get_user_ban_appeal_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user || get_current_user_id() !== $user_id) {
return '';
}
$info = class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
$settings = class_exists('\AKSRT\User\BanAppeal') ? \AKSRT\User\BanAppeal::settings() : ['enabled' => true, 'desc' => '', 'keys' => []];
$status = class_exists('\AKSRT\User\BanAppeal') ? \AKSRT\User\BanAppeal::get_status($user_id) : '';
$payload = class_exists('\AKSRT\User\BanAppeal') ? \AKSRT\User\BanAppeal::get_payload($user_id) : [];
// 弹窗头(参考主题 edit-header:关闭钮 + gavel 徽标 + 标题居左)。
$html = '<div class="aksrt-user-ban-appeal-modal">';
$html .= '<div class="mb10 edit-header touch"><button type="button" class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">' . aksrt_user_ban_close_svg() . '</button><b class="flex ac c-blue"><span class="toggle-radius mr10 c-blue"><i class="fa fas fa-gavel" aria-hidden="true"></i></span>' . esc_html__('禁封申诉', 'aksrt') . '</b></div>';
if (empty($info)) {
$html .= '<div class="text-center c-yellow" style="padding: 50px 0;">' . esc_html__('您已解封,请刷新页面', 'aksrt') . '</div></div>';
return $html;
}
$set_row = static function (string $label, string $value): string {
if ($value === '') {
return '';
}
return '<div class="flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html($label) . '</div><div class="author-set-right mt6">' . esc_html($value) . '</div></div>';
};
// 禁封信息行(参考主题申诉弹窗内嵌信息框:用户名/状态/起止时间/原因/说明)。
$info_box = '<div class="mb10 muted-box padding-10 em09">'
. $set_row(__('用户名', 'aksrt'), $user->display_name)
. $set_row(__('状态', 'aksrt'), (string) $info['status'])
. $set_row(__('开始时间', 'aksrt'), (string) $info['start'])
. $set_row(__('结束时间', 'aksrt'), (string) $info['end'])
. $set_row(__('原因', 'aksrt'), (string) $info['reason'])
. $set_row(__('说明', 'aksrt'), (string) $info['desc'])
. '</div>';
if (!empty($info['no_appeal'])) {
// 禁止申诉态(参考主题:c-yellow box-body + 禁止申诉说明)。
$html .= '<div><div class="c-yellow box-body">' . esc_html__('抱歉!您当前已无法进行申诉', 'aksrt') . '</div>';
if ((string) $info['no_appeal_desc'] !== '') {
$html .= '<div class="muted-box mt10">' . esc_html((string) $info['no_appeal_desc']) . '</div>';
}
$html .= '</div></div>';
return $html;
}
if (empty($settings['enabled'])) {
$html .= '<div class="c-yellow box-body">' . esc_html__('申诉功能暂未开启', 'aksrt') . '</div></div>';
return $html;
}
$input = '';
$footer = '';
if ($status === 'pending') {
// 审核中态(参考主题:黄色 badg + 已提交资料行 + 提交时间)。
$input = '<div class="c-yellow badg block mb6">' . esc_html__('您的申诉正在审核中,请耐心等待', 'aksrt') . '</div>';
$data = is_array($payload['data'] ?? null) ? $payload['data'] : [];
if (!empty($data)) {
foreach ($data as $k => $v) {
$input .= $set_row((string) $k, (string) $v);
}
} elseif (!empty($payload['reason'])) {
$input .= $set_row(__('申诉说明', 'aksrt'), (string) $payload['reason']);
}
$input .= $set_row(__('提交时间', 'aksrt'), (string) ($payload['time'] ?? ''));
} else {
// 申诉表单(参考主题:资料项逐项输入 + 申诉说明 textarea + 双按钮)。
foreach ((array) $settings['keys'] as $key) {
$key = (string) $key;
if ($key === '') {
continue;
}
$input .= '<div class="mb10"><div class="muted-2-color mb6">' . esc_html($key) . '</div>'
. '<input class="form-control" type="input" name="data[' . esc_attr($key) . ']" value="' . esc_attr((string) ($payload['data'][$key] ?? '')) . '"></div>';
}
$input .= '<div class="mb10"><div class="muted-2-color mb6">' . esc_html__('请填写申诉说明', 'aksrt') . '</div>'
. '<textarea class="form-control mb20" name="data[' . esc_attr__('申诉说明', 'aksrt') . ']" tabindex="2" placeholder="' . esc_attr__('详细的说明更容易通过审核喔', 'aksrt') . '" rows="3">' . esc_textarea((string) ($payload['data'][__('申诉说明', 'aksrt')] ?? ($payload['reason'] ?? ''))) . '</textarea></div>';
$footer = '<div class="but-average modal-buts">'
. '<input type="hidden" name="action" value="aksrt_ban_appeal_submit">'
. wp_nonce_field('aksrt_ban_appeal', '_wpnonce', false, false)
. '<button type="button" class="but" data-aksrt-modal-close>' . esc_html__('取消', 'aksrt') . '</button>'
. '<button type="submit" class="but c-blue" data-aksrt-appeal-submit>' . esc_html__('提交申诉', 'aksrt') . '</button>'
. '</div>';
}
$desc_html = (string) $settings['desc'] !== '' ? '<div class="mb10 em09">' . wp_kses_post(nl2br(esc_html((string) $settings['desc']))) . '</div>' : '';
// 滚动容器:scroll-y + no-scrollbar(完全隐藏滚动条,含细滚动条)+ max-vh5(50vh,
// 内层滚动、外层对话框不滚动);说明框保持 rows=3 自然高度,不做 autoHeight 压缩。
$html .= '<form class="aksrt-user-ban-appeal-form" data-aksrt-appeal-form>'
. '<div class="scroll-y no-scrollbar max-vh5">' . $info_box . $input . $desc_html . '</div>'
. $footer . '</form>';
$html .= '</div>';
return $html;
}
/**
* 「查看禁封记录」链接(参考主题 zib_get_user_ban_log_link:em09 c-yellow
* 链接触发禁封记录弹窗;仅具备禁封权限的管理员可见)。
*/
function aksrt_get_user_ban_log_link(int $user_id, string $class = '', string $con = ''): string
{
if ($user_id <= 0 || !aksrt_current_user_can_ban($user_id)) {
return '';
}
if ($con === '') {
$con = '<i class="fa fas fa-ban mr6" aria-hidden="true"></i>' . esc_html__('查看禁封记录', 'aksrt');
}
$classes = trim('em09 c-yellow pointer ' . $class);
return '<a class="' . esc_attr($classes) . '" href="javascript:;" role="button"'
. ' data-aksrt-ban-log="' . esc_attr((string) $user_id) . '">' . $con . '</a>';
}
/**
* 禁封记录弹窗(1:1 复刻参考主题 zib_get_ban_log_modal:多彩头 c-yellow +
* 用户行 + 计数 + 逐条记录卡(类型徽章 / 结束时间 / 原因 / 说明 / 禁止申诉);
* 仅具备禁封权限的管理员可见)。
*
* @return string 弹窗 HTML(空串 = 无权查看)
*/
function aksrt_get_user_ban_log_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user || !aksrt_current_user_can_ban($user_id)) {
return '';
}
$logs = class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->get_user_restrictions($user_id, false)
: [];
$logs = array_values(array_filter((array) $logs, static function ($r): bool {
return in_array((string) ($r['type'] ?? ''), ['ban', 'suspend'], true);
}));
// 弹窗头(参考主题 zib_get_modal_colorful_header('c-yellow', ban 图标, 标题))。
$html = '<div class="aksrt-user-ban-log-modal">';
$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__('禁封记录', 'aksrt') . '</div></div></div>';
$user_name = function_exists('aksrt_get_user_name')
? aksrt_get_user_name(['id' => $user_id, 'vip' => false, 'medal' => false, 'auth' => false, 'level' => false])
: '<b>' . esc_html($user->display_name) . '</b>';
$avatar_url = get_avatar_url($user_id, ['size' => 40]);
$html .= '<div class="flex ac mb6"><img class="avatar-mini mr6" src="' . esc_url((string) $avatar_url) . '" alt="">' . $user_name . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- aksrt_get_user_name escapes internally.
if (empty($logs)) {
$html .= '<div class="text-center c-yellow" style="padding: 50px 0;">' . esc_html__('该用户暂无禁封记录', 'aksrt') . '</div></div>';
return $html;
}
$html .= '<div class="c-yellow mb10">' . esc_html(sprintf(__('该用户共有%d次禁封记录', 'aksrt'), count($logs))) . '</div>';
$html .= '<div class="scroll-y no-scrollbar max-vh5">';
$i = 1;
foreach ($logs as $log) {
$type = (string) ($log['type'] ?? '');
$is_ban = $type === 'ban';
$type_badge = $is_ban ? __('禁封', 'aksrt') : __('小黑屋', 'aksrt');
$end_time = !empty($log['expires_at']) ? (string) $log['expires_at'] : __('永久', 'aksrt');
$no_appeal = get_user_meta($user_id, '_aksrt_no_appeal', true);
$raw = (string) ($log['reason'] ?? '');
$reason = $raw;
$desc = '';
$marker = "\n" . __('禁封说明:', 'aksrt');
$pos = strpos($raw, $marker);
if (false !== $pos) {
$reason = substr($raw, 0, $pos);
$desc = trim(substr($raw, $pos + strlen($marker)));
}
$no_appeal_html = '';
if ($i === 1 && is_array($no_appeal) && trim((string) ($no_appeal['desc'] ?? '')) !== '') {
$no_appeal_html = '<div class="flex"><div class="author-set-left" style="min-width: 80px;"><span class="c-red">' . esc_html__('禁止申诉', 'aksrt') . '</span></div><div class="author-set-right mt6">' . esc_html((string) $no_appeal['desc']) . '</div></div>';
}
$html .= '<div class="em09 muted-box muted-2-color mb6 padding-10">'
. '<div class="border-bottom flex jsb p-b6"><div class="muted-color"><badge class="mr6 c-blue">' . $i . '</badge>' . esc_html((string) ($log['created_at'] ?? '')) . '</div>'
. '<span class="badg badg-sm ' . ($is_ban ? 'c-red' : 'c-yellow') . '">' . esc_html($type_badge) . '</span></div>'
. '<div class="flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('结束时间', 'aksrt') . '</div><div class="author-set-right mt6">' . esc_html($end_time) . '</div></div>'
. '<div class="flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('原因', 'aksrt') . '</div><div class="author-set-right mt6">' . esc_html(trim($reason)) . '</div></div>'
. ($desc !== '' ? '<div class="flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('说明', 'aksrt') . '</div><div class="author-set-right mt6">' . esc_html($desc) . '</div></div>' : '')
. $no_appeal_html
. '</div>';
$i++;
}
$html .= '</div></div>';
return $html;
}
/**
* 受限位置治理提示(评论拒绝态,参考主题 zib_get_user_ban_nocan_info 语义:
* 状态徽章 +「暂时无法操作」+ 查看禁封状态入口)。
*
* @param string $msg 具体受阻的操作描述,如「发表评论」。
*/
function aksrt_get_user_ban_nocan_html(int $user_id, string $msg = ''): string
{
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$text = (string) $state['label'] . ($msg !== '' ? ',' . $msg : '');
$inner = '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . ' mr6" aria-hidden="true"></i>'
. '<span class="' . esc_attr((string) $state['tone']) . '">' . esc_html($text) . '</span>';
if (aksrt_current_user_can_ban($user_id) || get_current_user_id() === $user_id) {
$inner .= ' <a class="c-blue" href="javascript:;" data-aksrt-ban-info="' . esc_attr((string) $user_id) . '">'
. esc_html__('查看禁封状态', 'aksrt')
. '<i class="fa fas fa-angle-right em09 ml3" aria-hidden="true"></i></a>';
}
return '<p class="comments-permission-denied aksrt-user-ban-nocan">' . $inner . '</p>';
}
/**
* 昵称旁禁封徽章(参考主题 user_name_badge 钩子语义:占据 VIP 图标位,
* 头像角标 user_avatar_badge 为可选项,见 aksrt_get_user_ban_avatar_badge)。
*/
function aksrt_get_user_ban_name_badge(int $user_id): string
{
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
return '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . ' ' . esc_attr((string) $state['tone']) . ' ml3 aksrt-user-ban-badge"'
. ' title="' . esc_attr((string) $state['label']) . '" aria-hidden="true"></i>';
}
/**
* 头像禁封角标(参考主题 user_avatar_badge 钩子语义:覆盖 VIP 角标位)。
*/
function aksrt_get_user_ban_avatar_badge(int $user_id): string
{
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$tone = 'banned' === $state['state'] ? 'is-banned' : 'is-restricted';
return '<span class="avatar-badge aksrt-avatar-ban-badge ' . $tone . '" title="' . esc_attr((string) $state['label']) . '">'
. '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . '" aria-hidden="true"></i></span>';
}
/**
* 登录后禁封状态页(1:1 复刻参考主题 zib_user_ban_restrict_login /
* zib_die_page:红盒提示 + 禁封信息行 + 申诉权限 + 退出登录/申诉按钮)。
* 由 Restriction::restrict_frontend_access() 在 template_redirect 调用。
*/
function aksrt_render_user_ban_block_page(int $user_id): void
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user) {
return;
}
$info = class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
// 禁封页可出现在任何前台页面:确保弹窗运行时与禁封脚本就位(申诉入口)。
if (function_exists('wp_style_is') && !wp_script_is('aksrt-user-ban', 'enqueued')) {
$ban_js = get_template_directory() . '/assets/js/user-ban.js';
if (file_exists($ban_js)) {
wp_enqueue_script(
'aksrt-user-ban',
get_template_directory_uri() . '/assets/js/user-ban.js',
['loader_js'],
filemtime($ban_js) ?: '1.0.0',
true
);
}
}
if (!headers_sent()) {
header('Content-Type: text/html; charset=' . get_bloginfo('charset'));
}
get_header();
get_template_part('templates/parts/user/ban-block', null, [
'user' => $user,
'info' => $info,
]);
get_footer();
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');
// 用户名徽章(参考主题 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>';
// 禁封信息行(与查看禁封状态弹窗 / 申诉弹窗 / 禁封页共用的单一数据源)。
$info = class_exists('\AKSRT\User\Restriction')
? \AKSRT\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
$set_row = static function (string $label, string $value, bool $raw = false): string {
if ($value === '') {
return '';
}
$html_value = $raw ? $value : nl2br(esc_html($value)); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- raw callers pass internally-escaped HTML.
return '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html($label) . '</div><div class="author-set-right mt6">' . $html_value . '</div></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_link = aksrt_get_user_ban_log_link($user_id, 'em09 c-yellow', sprintf(__('当前用户已有%d次禁封记录,请谨慎操作', 'aksrt'), $log_count));
$log_view = ($log_count > 1 && $log_link !== '')
? '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('禁封记录', 'aksrt') . '</div><div class="author-set-right mt6">' . $log_link . '</div></div>'
: '';
// 弹窗头(参考主题 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, true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- aksrt_get_user_name escapes internally.
$html .= $set_row(__('状态', 'aksrt'), (string) ($info['status'] ?? ''));
$html .= $set_row(__('开始时间', 'aksrt'), (string) ($info['start'] ?? ''));
$html .= $set_row(__('结束时间', 'aksrt'), (string) ($info['end'] ?? ''));
$html .= $set_row(__('原因', 'aksrt'), (string) ($info['reason'] ?? ''));
$html .= $set_row(__('说明', 'aksrt'), (string) ($info['desc'] ?? ''));
$html .= $log_view;
$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:1 参考主题 zib_get_set_user_ban_modal:>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_link = aksrt_get_user_ban_log_link($user_id, '', sprintf(__('当前用户已有%d次禁封记录,请谨慎操作', 'aksrt'), $log_count));
$log_view = ($log_count > 1 && $log_link !== '')
? '<div class="mb10 flex"><div class="author-set-left" style="min-width: 80px;">' . esc_html__('禁封记录', 'aksrt') . '</div><div class="author-set-right mt6">' . $log_link . '</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 no-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;
}