get_display_state($user_id);
}
if ($state === '') {
return $cache[$user_id] = [];
}
$badges = class_exists('\Jio\User\Restriction') ? \Jio\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'),
];
}
/**
* 目标是否处于封禁/小黑屋状态(举报入口隐藏判定:封号 / 小黑屋用户不展示
* 举报入口;禁言为轻量评论域限制,不隐藏举报入口)。
*/
function jio_is_user_ban_restricted(int $user_id): bool
{
$state = jio_get_user_ban_state($user_id);
return in_array((string) ($state['state'] ?? ''), ['banned', 'restricted'], true);
}
/**
* 治理状态徽章(占据身份行元信息位:badg + 图标 + 文案,红=封号、
* 黄=小黑屋、灰=禁言,字段与颜色由 Restriction::state_badges() 统一管理)。
*/
function jio_get_user_ban_identity_badge(int $user_id, string $class = ''): string
{
$state = jio_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$classes = trim('badg ' . (string) $state['tone'] . ($class !== '' ? ' ' . $class : ''));
return ''
. ''
. esc_html((string) $state['label']) . '';
}
/**
* 「查看禁封状态」入口(用户中心元信息位:but 按钮 = 内层 badg 徽章(带色)+
* 右箭头,外层 but 为默认按钮色——不在内外两层重复着色)。
* 点击经 data-jio-ban-info 委托打开禁封信息弹窗。
*/
function jio_get_user_ban_info_link(int $user_id, string $class = ''): string
{
if (!jio_user_ban_enabled()) {
return '';
}
$state = jio_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$badge = jio_get_user_ban_identity_badge($user_id);
if ($badge === '') {
return '';
}
// 外层 but 不重复状态色:颜色仅由内层 badg 承载。
$classes = trim('but p2-10' . ($class !== '' ? ' ' . $class : ''));
return '' . $badge
. '';
}
/**
* 禁封信息弹窗(多彩头 c-gray + 禁封信息行 + 申诉入口;本人或管理员可见)。
*
* @return string 弹窗 HTML(空串 = 无权查看或用户不存在)
*/
function jio_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 || jio_current_user_can_ban($user_id));
if (!$can_view) {
return '';
}
$info = class_exists('\Jio\User\Restriction')
? \Jio\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
$html = '
';
// 弹窗头:灰色 colorful header,ban 图标 + 标题。
$html .= '';
if (empty($info)) {
$html .= '
' . esc_html__('您已解封,请刷新页面', 'jio') . '
';
return $html;
}
$set_row = static function (string $label, string $value): string {
if ($value === '') {
return '';
}
return '' . esc_html($label) . '
' . esc_html($value) . '
';
};
$html .= '';
$html .= $set_row(__('用户名', 'jio'), $user->display_name);
$html .= $set_row(__('状态', 'jio'), (string) $info['status']);
$html .= $set_row(__('开始时间', 'jio'), (string) $info['start']);
$html .= $set_row(__('结束时间', 'jio'), (string) $info['end']);
$html .= $set_row(__('原因', 'jio'), (string) $info['reason']);
$html .= $set_row(__('说明', 'jio'), (string) $info['desc']);
$html .= '
';
if (!empty($info['no_appeal'])) {
$html .= '' . esc_html__('您当前已无法进行申诉', 'jio') . '
';
if ((string) $info['no_appeal_desc'] !== '') {
$html .= '
' . esc_html((string) $info['no_appeal_desc']) . '
';
}
$html .= '
';
$html .= '';
} else {
$appeal_link = $viewer_id === $user_id ? jio_get_user_ban_appeal_link($user_id, 'but c-blue') : '';
$html .= '';
$html .= '';
$html .= $appeal_link !== '' ? $appeal_link : '';
$html .= '
';
}
$html .= '';
return $html;
}
/**
* 「申诉」入口(禁封申诉 modal-mini,仅本人可发起;
* 点击经 data-jio-ban-appeal 委托打开申诉弹窗)。
*/
function jio_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 ''
. ''
. esc_html__('申诉', 'jio') . '';
}
/**
* 禁封申诉弹窗(edit-header(gavel / c-blue / 禁封申诉)+ 禁封信息行 +
* 申诉说明 + 资料项输入 + 审核中态 + 禁止申诉态;仅本人可见可提交)。
*
* @return string 弹窗 HTML(空串 = 非本人或用户不存在)
*/
function jio_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('\Jio\User\Restriction')
? \Jio\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
$settings = class_exists('\Jio\User\BanAppeal') ? \Jio\User\BanAppeal::settings() : ['enabled' => true, 'desc' => '', 'keys' => []];
$status = class_exists('\Jio\User\BanAppeal') ? \Jio\User\BanAppeal::get_status($user_id) : '';
$payload = class_exists('\Jio\User\BanAppeal') ? \Jio\User\BanAppeal::get_payload($user_id) : [];
// 弹窗头:关闭钮 + gavel 徽标 + 标题居左。
$html = '';
$html .= '';
if (empty($info)) {
$html .= '
' . esc_html__('您已解封,请刷新页面', 'jio') . '
';
return $html;
}
$set_row = static function (string $label, string $value): string {
if ($value === '') {
return '';
}
return '' . esc_html($label) . '
' . esc_html($value) . '
';
};
// 禁封信息行:用户名/状态/起止时间/原因/说明。
$info_box = ''
. $set_row(__('用户名', 'jio'), $user->display_name)
. $set_row(__('状态', 'jio'), (string) $info['status'])
. $set_row(__('开始时间', 'jio'), (string) $info['start'])
. $set_row(__('结束时间', 'jio'), (string) $info['end'])
. $set_row(__('原因', 'jio'), (string) $info['reason'])
. $set_row(__('说明', 'jio'), (string) $info['desc'])
. '
';
if (!empty($info['no_appeal'])) {
// 禁止申诉态:c-yellow box-body + 禁止申诉说明。
$html .= '' . esc_html__('抱歉!您当前已无法进行申诉', 'jio') . '
';
if ((string) $info['no_appeal_desc'] !== '') {
$html .= '
' . esc_html((string) $info['no_appeal_desc']) . '
';
}
$html .= '
';
return $html;
}
if (empty($settings['enabled'])) {
$html .= '' . esc_html__('申诉功能暂未开启', 'jio') . '
';
return $html;
}
$input = '';
$footer = '';
if ($status === 'pending') {
// 审核中态:黄色 badg + 已提交资料行 + 提交时间。
$input = '' . esc_html__('您的申诉正在审核中,请耐心等待', 'jio') . '
';
$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(__('申诉说明', 'jio'), (string) $payload['reason']);
}
$input .= $set_row(__('提交时间', 'jio'), (string) ($payload['time'] ?? ''));
} else {
// 申诉表单:资料项逐项输入 + 申诉说明 textarea + 双按钮。
foreach ((array) $settings['keys'] as $key) {
$key = (string) $key;
if ($key === '') {
continue;
}
$input .= '';
}
$input .= '' . esc_html__('请填写申诉说明', 'jio') . '
'
. '
';
$footer = ''
. ''
. wp_nonce_field('jio_ban_appeal', '_wpnonce', false, false)
. ''
. ''
. '
';
}
$desc_html = (string) $settings['desc'] !== '' ? '' . wp_kses_post(nl2br(esc_html((string) $settings['desc']))) . '
' : '';
// 滚动容器:scroll-y + no-scrollbar(完全隐藏滚动条,含细滚动条)+ max-vh5(50vh,
// 内层滚动、外层对话框不滚动);说明框保持 rows=3 自然高度,不做 autoHeight 压缩。
$html .= '';
$html .= '';
return $html;
}
/**
* 「查看禁封记录」链接(em09 c-yellow 链接触发禁封记录弹窗;
* 仅具备禁封权限的管理员可见)。
*/
function jio_get_user_ban_log_link(int $user_id, string $class = '', string $con = ''): string
{
if ($user_id <= 0 || !jio_current_user_can_ban($user_id)) {
return '';
}
if ($con === '') {
$con = '' . esc_html__('查看禁封记录', 'jio');
}
$classes = trim('em09 c-yellow pointer ' . $class);
return '' . $con . '';
}
/**
* 禁封记录弹窗(多彩头 c-yellow + 用户行 + 计数 + 逐条记录卡
* (类型徽章 / 结束时间 / 原因 / 说明 / 禁止申诉);仅具备禁封权限的管理员可见)。
*
* @return string 弹窗 HTML(空串 = 无权查看)
*/
function jio_get_user_ban_log_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user || !jio_current_user_can_ban($user_id)) {
return '';
}
$logs = class_exists('\Jio\User\Restriction')
? \Jio\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);
}));
// 弹窗头:黄色 colorful header,ban 图标 + 标题。
$html = '';
$html .= '';
$user_name = function_exists('jio_get_user_name')
? jio_get_user_name(['id' => $user_id, 'vip' => false, 'medal' => false, 'auth' => false, 'level' => false])
: '
' . esc_html($user->display_name) . '';
$avatar_url = get_avatar_url($user_id, ['size' => 40]);
$html .= '
 $avatar_url) . ')
' . $user_name . '
'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- jio_get_user_name escapes internally.
if (empty($logs)) {
$html .= '
' . esc_html__('该用户暂无禁封记录', 'jio') . '
';
return $html;
}
$html .= '' . esc_html(sprintf(__('该用户共有%d次禁封记录', 'jio'), count($logs))) . '
';
$html .= '';
return $html;
}
/**
* 受限位置治理提示(评论拒绝态:状态徽章 +「暂时无法操作」+ 查看禁封状态入口)。
*
* @param string $msg 具体受阻的操作描述,如「发表评论」。
*/
function jio_get_user_ban_nocan_html(int $user_id, string $msg = ''): string
{
$state = jio_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$text = (string) $state['label'] . ($msg !== '' ? ',' . $msg : '');
$inner = ''
. '' . esc_html($text) . '';
if (jio_current_user_can_ban($user_id) || get_current_user_id() === $user_id) {
$inner .= ' '
. esc_html__('查看禁封状态', 'jio')
. '';
}
return '';
}
/**
* 昵称旁禁封徽章(占据 VIP 图标位,
* 头像角标 user_avatar_badge 为可选项,见 jio_get_user_ban_avatar_badge)。
*/
function jio_get_user_ban_name_badge(int $user_id): string
{
$state = jio_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
return '';
}
/**
* 头像禁封角标(覆盖 VIP 角标位)。
*/
function jio_get_user_ban_avatar_badge(int $user_id): string
{
$state = jio_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$tone = 'banned' === $state['state'] ? 'is-banned' : 'is-restricted';
return ''
. '';
}
/**
* 登录后禁封状态页(红盒提示 + 禁封信息行 + 申诉权限 + 退出登录/申诉按钮)。
* 由 Restriction::restrict_frontend_access() 在 template_redirect 调用。
*/
function jio_render_user_ban_block_page(int $user_id): void
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user) {
return;
}
$info = class_exists('\Jio\User\Restriction')
? \Jio\User\Restriction::get_instance()->get_ban_info($user_id)
: [];
// 禁封页可出现在任何前台页面:确保弹窗运行时与禁封脚本就位(申诉入口)。
if (function_exists('wp_style_is') && !wp_script_is('jio-user-ban', 'enqueued')) {
$ban_js = get_template_directory() . '/assets/js/user-ban.js';
if (file_exists($ban_js)) {
wp_enqueue_script(
'jio-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;
}
/**
* 禁封功能开关(后台“用户&互动 → 禁封/举报 → 禁封功能”,缺键默认开启)。
*/
function jio_user_ban_enabled(): bool
{
$settings = class_exists('\Jio\Admin\SchemaProvider')
? \Jio\Settings\Settings::read_domain('users', 'report')
: (class_exists('\Jio\Settings\Settings') ? \Jio\Settings\Settings::get_all() : []);
$settings = is_array($settings) ? $settings : [];
return !isset($settings['user_ban_s']) || !empty($settings['user_ban_s']);
}
/**
* 当前用户能否禁封目标用户(需 manage_options 权限,且不能禁封自己 / 超级管理员)。
*/
function jio_current_user_can_ban(int $user_id): bool
{
if ($user_id <= 0 || !jio_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 jio_get_user_current_ban(int $user_id): ?array
{
if ($user_id <= 0 || !class_exists('\Jio\User\Restriction')) {
return null;
}
$svc = \Jio\User\Restriction::get_instance();
return $svc->get_active_ban($user_id) ?: $svc->get_active_suspend($user_id);
}
/**
* 禁封 / 解封触发器。
* 输出 data-jio-ban 委托入口,由 assets/js/user-ban.js 打开禁封弹窗。
*/
function jio_get_edit_user_ban_link(int $user_id = 0, string $class = '', string $con = ''): string
{
if (!jio_current_user_can_ban($user_id)) {
return '';
}
if ($con === '') {
$con = jio_get_user_current_ban($user_id)
? '' . __('解封此用户', 'jio')
: '' . __('禁封此用户', 'jio');
}
return '' . $con . '';
}
/**
* 用户主页封面三点菜单项:
* 有禁封权限 → 禁封入口;否则(开启举报且非本人)→ 举报入口。
*/
function jio_get_user_cover_menu_action_html(int $user_id): string
{
if (jio_current_user_can_ban($user_id)) {
$ban = jio_get_user_current_ban($user_id);
$label = $ban ? __('解封此用户', 'jio') : __('禁封此用户', 'jio');
return '';
}
return '';
}
/**
* 评论三点菜单项:
* 管理员 → 禁封用户(固定文案,解封态由弹窗内容呈现);否则 → 举报( Jio 既有评论举报契约)。
*/
function jio_get_comment_ban_or_report_item(\WP_Comment $comment): string
{
$user_id = (int) $comment->user_id;
if ($user_id > 0 && jio_current_user_can_ban($user_id)) {
return '';
}
if (function_exists('jio_get_comment_report_html')) {
return jio_get_comment_report_html($comment);
}
return '';
}
/**
* 文章作者资料卡三点菜单项(菜单首项):
* 管理员 → 禁封用户(固定文案,解封态由弹窗内容呈现);否则 → 空(由既有
* 文章举报项兜底,与「禁封优先、否则举报」互斥语义一致)。
*/
function jio_get_post_author_ban_item(int $author_id, string $post_url = ''): string
{
if ($author_id <= 0 || get_current_user_id() === $author_id) {
return '';
}
if (jio_current_user_can_ban($author_id)) {
return '' . esc_html__('禁封用户', 'jio') . '';
}
return '';
}
/**
* 弹窗关闭图标(与 report.js 同款 SVG 雪碧图契约)。
*/
function jio_user_ban_close_svg(): string
{
return '';
}
/**
* 禁封 / 解封弹窗内容:
* 未禁封 → 禁封表单(禁封方式 / 禁封时间 / 预置原因徽章 / 其它原因 /
* 禁封说明 / 删除全部内容 / 禁止用户申诉(含说明));
* 已禁封 → 解封视图(状态信息 + 解封留言表单)。
*
* @return string 弹窗 HTML(空串 = 无权操作或用户不存在)
*/
function jio_get_user_ban_modal(int $user_id): string
{
$user = $user_id > 0 ? get_userdata($user_id) : null;
if (!$user || !jio_current_user_can_ban($user_id)) {
return '';
}
$ban = jio_get_user_current_ban($user_id);
// 用户行(头像 + 昵称)。
$avatar_url = get_avatar_url($user_id, ['size' => 40]);
$user_name = function_exists('jio_get_user_name')
? jio_get_user_name(['id' => $user_id, 'vip' => false, 'medal' => false])
: esc_html($user->display_name);
$user_row = ''
. '
 $avatar_url) . ')
'
. '
' . $user_name . ' '; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- jio_get_user_name escapes internally.
$html = '';
if ($ban) {
// ── 已禁封视图:解封 ──
$is_ban = (string) ($ban['type'] ?? '') === 'ban';
$unban_title = $is_ban ? __('解封用户', 'jio') : __('移出小黑屋', 'jio');
// 用户名徽章(inflex ac badg p2-10 em09)。
$user_badge = function_exists('jio_get_user_name')
? jio_get_user_name(['id' => $user_id, 'class' => 'inflex ac badg p2-10 em09', 'vip' => false, 'medal' => false, 'auth' => false, 'level' => false])
: '
' . esc_html($user->display_name) . '';
// 禁封信息行(与查看禁封状态弹窗 / 申诉弹窗 / 禁封页共用的单一数据源)。
$info = class_exists('\Jio\User\Restriction')
? \Jio\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 '
' . esc_html($label) . '
' . $html_value . '
';
};
// 禁封记录入口:>1 次时展示可点击记录链接,打开禁封记录弹窗。
$log_count = 0;
if (class_exists('\Jio\User\Restriction')) {
$all = \Jio\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 = jio_get_user_ban_log_link($user_id, 'em09 c-yellow', sprintf(__('当前用户已有%d次禁封记录,请谨慎操作', 'jio'), $log_count));
$log_view = ($log_count > 1 && $log_link !== '')
? '
' . esc_html__('禁封记录', 'jio') . '
' . $log_link . '
'
: '';
// 弹窗头:黄色 colorful header(120px 多彩横幅 + 大图标 + 居中标题,
// 与禁封视图的 edit-header 头对应)。
$html .= '';
$html .= '
';
return $html;
}
// ── 未禁封视图:禁封表单 ──
$close_svg = jio_user_ban_close_svg();
$html .= '';
// 历史禁封次数提示:>1 次时展示「禁封记录」行 + 可点击的黄链(打开禁封记录弹窗)。
$log_count = 0;
if (class_exists('\Jio\User\Restriction')) {
$all = \Jio\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 = jio_get_user_ban_log_link($user_id, '', sprintf(__('当前用户已有%d次禁封记录,请谨慎操作', 'jio'), $log_count));
$log_view = ($log_count > 1 && $log_link !== '')
? '' . esc_html__('禁封记录', 'jio') . '
' . $log_link . '
'
: '';
$html .= '';
return $html;
}