feat(ban): mirror reference theme ban/restriction prompt surface

修复公开主页禁封提示框为空的问题,并对齐参考主题(Zibll)全部前台禁封提示入口:
- fix(profile): 资料卡下方治理提示框渲染为空——模板读取的 tone/message 键与
  数据层实际输出的 state/label/text 契约不匹配;现按禁封类型展示
  「违规已封号/小黑屋禁封中/禁言中」+ 可配置说明文案
- feat(ban): 用户中心页头展示治理状态徽章 + 查看详情入口,直达禁封申诉页
  (参考主题 user_page_header_desc / zib_get_user_ban_info_link)
- feat(ban): 评论拒绝态区分禁封/小黑屋/禁言,附状态徽章与申诉链接
  (参考主题 zib_get_user_ban_nocan_info,原为笼统的无权限文案)
- feat(ban): 禁封申诉页补全状态信息卡(状态/开始时间/结束时间/原因/说明/
  申诉权限),对齐 zib_get_ban_info_modal;此前仅有「禁封原因」一行且无入口
- feat(ban): 昵称旁禁封徽章 + 头像禁封角标(覆盖 VIP 位,参考主题
  user_name_badge / user_avatar_badge 钩子语义),main.css 与入队的
  main.min.css 同步新增角标样式
- fix(ban): 举报入口对已封禁/小黑屋目标隐藏(评论、文章下拉、封面菜单三处,
  对齐 zib_get_report_link 排除语义;禁言为轻量限制不隐藏)
- refactor(ban): 治理展示态收敛为单一数据源 Restriction::get_display_state()
  + state_labels(),资料卡/用户中心/评论拒绝态/昵称徽章共用
- feat(ban): 小黑屋用户开放申诉(对齐参考主题 zib_user_is_ban 双类型语义),
  禁封站内信附加申诉页链接
- test: report-ban 契约测试 +3(展示态单一数据源、提示面入口矩阵、举报排除),
  45 个测试文件全部通过
This commit is contained in:
2026-09-22 13:28:52 +08:00
parent 8d9cca2fe5
commit 7f520803ff
17 changed files with 457 additions and 46 deletions
+19
View File
@@ -1536,6 +1536,25 @@ svg.loading-circle.zts circle,
bottom: 0;
}
/* 头像禁封角标(禁封红 / 小黑屋-禁言黄,覆盖 VIP 角标位) */
.aksrt-avatar-ban-badge {
display: flex;
align-items: center;
justify-content: center;
border-radius: 50%;
color: #fff;
font-size: 9px;
line-height: 1;
}
.aksrt-avatar-ban-badge.is-banned {
background: #ef4444;
}
.aksrt-avatar-ban-badge.is-restricted {
background: #f59e0b;
}
.blur * {
border-radius: 0;
}
+1 -1
View File
File diff suppressed because one or more lines are too long
+9 -1
View File
@@ -237,7 +237,14 @@ if (!function_exists('aksrt_get_avatar_box')) {
$img = aksrt_get_avatar_html($subject, 80, ['class' => 'avatar']);
$badge = '';
if ($vip && $user_id > 0 && function_exists('aksrt_get_user_vip_level') && aksrt_get_user_vip_level($user_id) > 0) {
if ($vip && $user_id > 0) {
// 禁封角标覆盖 VIP 角标位(参考主题 user_avatar_badge 过滤器覆盖语义)。
$ban_badge = function_exists('aksrt_get_user_ban_avatar_badge')
? aksrt_get_user_ban_avatar_badge($user_id)
: '';
if ($ban_badge !== '') {
$badge = $ban_badge;
} elseif (function_exists('aksrt_get_user_vip_level') && aksrt_get_user_vip_level($user_id) > 0) {
$icon_url = '';
if (class_exists('\AKSRT\Settings\Settings')) {
$vip_settings = \AKSRT\Settings\Settings::get_payment_vip_settings();
@@ -249,6 +256,7 @@ if (!function_exists('aksrt_get_avatar_box')) {
$badge = '<img class="avatar-badge" src="' . $icon_url . '" title="' . esc_attr($badge_text) . '" alt="' . esc_attr($badge_text) . '">';
}
}
}
$html = '<span class="' . esc_attr($class) . '">' . $img . $badge . '</span>';
if ($link && $user_id > 0) {
+9
View File
@@ -458,9 +458,18 @@ if (!function_exists('aksrt_comments_render_form')) :
if (!$can_comment) {
if (!$is_logged_in) {
aksrt_comments_render_signarea($ctx);
} else {
// 治理状态区分提示(参考主题 zib_get_user_ban_nocan_info 语义:
// 禁封中/小黑屋/禁言显示状态徽章与查看入口,而非笼统的无权限文案)。
$ban_nocan = function_exists('aksrt_get_user_ban_nocan_html')
? aksrt_get_user_ban_nocan_html(get_current_user_id(), __('暂时无法发表评论', 'aksrt'))
: '';
if ($ban_nocan !== '') {
echo $ban_nocan; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper escapes internally.
} else {
echo '<p class="comments-permission-denied muted-2-color">' . esc_html__('当前账号没有发表评论的权限。', 'aksrt') . '</p>';
}
}
return;
}
+7 -1
View File
@@ -191,9 +191,15 @@ if (!function_exists('aksrt_get_user_name')) {
: get_author_posts_url($user_id);
$display = $user->display_name;
$vip_html = '';
if (!empty($args['vip']) && function_exists('aksrt_render_vip_badge')) {
if (!empty($args['vip'])) {
// 禁封徽章占据 VIP 图标位(参考主题 user_name_badge 过滤器覆盖语义)。
$vip_html = function_exists('aksrt_get_user_ban_name_badge')
? aksrt_get_user_ban_name_badge($user_id)
: '';
if ($vip_html === '' && function_exists('aksrt_render_vip_badge')) {
$vip_html = (string) aksrt_render_vip_badge($user_id, 'sm');
}
}
$name_class = trim('display-name text-ellipsis ' . (string) $args['name_class']);
$name_link = '<a class="' . esc_attr($name_class) . '" href="' . esc_url($home) . '">' . esc_html($display) . '</a>';
+2 -1
View File
@@ -142,7 +142,8 @@ if (!function_exists('aksrt_get_single_meta_box')) :
$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 ($ban_item === '' && is_user_logged_in()) {
$post_author_id = (int) get_post_field('post_author', $post_id);
if ($ban_item === '' && is_user_logged_in() && !(function_exists('aksrt_is_user_ban_restricted') && $post_author_id > 0 && aksrt_is_user_ban_restricted($post_author_id))) {
$report_payload = wp_json_encode([
'target_type' => 'post',
'target_id' => (int) $post_id,
+1 -1
View File
@@ -26,7 +26,7 @@ defined('ABSPATH') || exit;
* @type array $profile_details Server-filtered fixed public profile fields.
* @type array $profile_signals Identity chips (verified, level, badges).
* @type array $verify_state Verification tri-state {state,label} for the details dialog.
* @type array $profile_notice Ban/restriction notice, if any.
* @type array $profile_notice Ban/restriction notice {state,label,text}, if any.
* @type string $background_url Cover image URL.
* @type float $credit_balance Credit balance (own profile only).
* @type string $currency_name Currency/credits display name.
+4
View File
@@ -162,6 +162,10 @@ if (!function_exists('aksrt_get_comment_report_html')) :
if ($author_id > 0 && $author_id === $reporter_id) {
return '';
}
// 已封禁/小黑屋用户不再显示举报入口(参考主题 zib_get_report_link 排除语义)。
if ($author_id > 0 && function_exists('aksrt_is_user_ban_restricted') && aksrt_is_user_ban_restricted($author_id)) {
return '';
}
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
if (empty($settings['report']['enabled'])) {
+156
View File
@@ -13,6 +13,162 @@
defined('ABSPATH') || exit;
/**
* 用户治理展示态(禁封 / 小黑屋 / 禁言),含请求级缓存。
*
* 单一数据源为 \AKSRT\User\Restriction::get_display_state() / state_labels();
* 本 helper 供用户中心页头、评论拒绝态、昵称徽章等位置复用,避免重复查库。
*
* 对齐参考主题:
* zib_user_is_ban → aksrt_get_user_ban_state(状态判定)
* zib_ban_author_header_identity_filter → 资料卡/用户中心身份行徽章
*
* @return array{state:string,label: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] = [];
}
$labels = class_exists('\AKSRT\User\Restriction') ? \AKSRT\User\Restriction::state_labels() : [];
$icons = [
'banned' => 'ban',
'restricted' => 'ban',
'muted' => 'comment-slash',
];
return $cache[$user_id] = [
'state' => $state,
'label' => (string) ($labels[$state] ?? ''),
'icon' => (string) ($icons[$state] ?? '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);
}
/**
* 禁封申诉页 URL(自定义页面模板 templates/pages/page-ban-appeal.php,
* 即参考主题 zib_get_ban_info_modal / zib_get_user_ban_info_link 的落地视图)。
* 站点未创建该页面时返回空串,调用方应降级为纯文本徽章。
*/
function aksrt_get_ban_appeal_page_url(): string
{
static $url = null;
if (null !== $url) {
return $url;
}
$pages = get_posts([
'post_type' => 'page',
'post_status' => 'publish',
'posts_per_page' => 1,
'fields' => 'ids',
'no_found_rows' => true,
'meta_query' => [
[
'key' => '_wp_page_template',
'value' => 'templates/pages/page-ban-appeal.php',
],
],
]);
return $url = !empty($pages) ? (string) get_permalink((int) $pages[0]) : '';
}
/**
* 治理状态徽章 +「查看详情」入口(用户中心页头,参考主题
* user_page_header_desc 钩子 / zib_get_user_ban_info_link 语义)。
*/
function aksrt_get_user_ban_notice_html(int $user_id): string
{
$state = aksrt_get_user_ban_state($user_id);
if (empty($state['state'])) {
return '';
}
$tone = 'banned' === $state['state'] ? 'c-red' : 'c-yellow';
$chip = '<span class="badg badg-sm ' . $tone . '" 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>';
$link = aksrt_get_ban_appeal_page_url();
if ($link === '') {
return $chip;
}
return '<a class="aksrt-user-ban-notice" href="' . esc_url($link) . '">' . $chip
. '<i class="fa fas fa-angle-right em09 ml6" aria-hidden="true"></i></a>';
}
/**
* 昵称旁禁封徽章(参考主题 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 '';
}
$tone = 'banned' === $state['state'] ? 'c-red' : 'c-yellow';
return '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . ' ' . $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>';
}
/**
* 受限位置治理提示(评论拒绝态,参考主题 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 : '');
$tone = 'banned' === $state['state'] ? 'c-red' : 'c-yellow';
$link = aksrt_get_ban_appeal_page_url();
$inner = '<i class="fa fas fa-' . esc_attr((string) $state['icon']) . ' mr6" aria-hidden="true"></i>'
. '<span class="' . $tone . '">' . esc_html($text) . '</span>';
if ($link !== '') {
$inner .= ' <a class="c-blue" href="' . esc_url($link) . '">' . 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>';
}
/**
* 禁封功能开关(后台“用户&互动 → 禁封/举报 → 禁封功能”,
* 对齐参考主题 _pz('user_ban_s', true):缺键默认开启)。
@@ -46,6 +46,13 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<?php endif; ?>
</h1>
<div class="uc-header__meta desc user-identity flex ac hh">
<?php
// 治理状态提示(参考主题 user_page_header_desc 钩子 / zib_get_user_ban_info_link:
// 徽章 +「查看禁封状态」入口,直达禁封申诉页)。
if (function_exists('aksrt_get_user_ban_notice_html')) {
echo aksrt_get_user_ban_notice_html((int) $user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- helper escapes internally.
}
?>
<?php if (function_exists('aksrt_render_user_medal_entry')) : ?>
<?php echo aksrt_render_user_medal_entry((int) $user_id, 'user-center', true); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php endif; ?>
@@ -173,6 +173,13 @@ trait UserControllerRestrictionOperations {
if ($time_day > 0) {
$notice .= ' ' . sprintf(__('期限:%1$d 天,%2$s 起自动解封。', 'aksrt'), $time_day, $expires);
}
// 申诉指引(参考主题「您可以登录后在用户中心发起申诉」同语义)。
if (!$no_appeal) {
$appeal_url = function_exists('aksrt_get_ban_appeal_page_url') ? aksrt_get_ban_appeal_page_url() : '';
if ($appeal_url !== '') {
$notice .= ' ' . sprintf(__('如有异议,可在禁封申诉页提交申诉:%s', 'aksrt'), $appeal_url);
}
}
$this->send_ban_notice($user_id, $notice, __('账号限制', 'aksrt'), $desc);
// 删除全部内容:全部公开内容与已审核评论移至回收站(参考主题同行为)。
+9 -3
View File
@@ -92,9 +92,15 @@ class BanAppeal {
wp_send_json(['error' => 1, 'msg' => __('申诉功能未开启', 'aksrt')]);
}
$banned = class_exists('\AKSRT\User\Restriction')
&& \AKSRT\User\Restriction::get_instance()->is_banned($user_id);
if (!$banned) {
// 禁封状态门禁:封号(ban)与小黑屋(suspend)均可申诉
// (对齐参考主题 zib_user_is_ban 的 1/2 双类型语义)。
$restricted = class_exists('\AKSRT\User\Restriction')
&& in_array(
\AKSRT\User\Restriction::get_instance()->get_display_state($user_id),
[\AKSRT\User\Restriction::STATE_BANNED, \AKSRT\User\Restriction::STATE_RESTRICTED],
true
);
if (!$restricted) {
wp_send_json(['error' => 1, 'msg' => __('当前账号未处于禁封状态', 'aksrt')]);
}
// 禁封时勾选“禁止用户申诉”的门禁(参考主题 no_appeal 同语义)。
+8 -9
View File
@@ -343,7 +343,12 @@ class Home {
/**
* 获取公开治理提示。
*
* @return array{state:string,text:string}|array{}
* 状态标识对齐参考主题公开主页禁封徽章(zib_ban_author_header_identity_filter):
* 封号(ban) → "违规已封号",小黑屋(suspend) → "小黑屋禁封中";
* 禁言(mute) 为 AKSRT 扩展状态 → "禁言中"。text 为后台可配置说明文案
* (未配置时回退默认文案,见 UserRuntime::get_user_identity_settings)。
*
* @return array{state:string,label:string,text:string}|array{}
*/
public function get_profile_notice(int $user_id): array {
if ($user_id <= 0 || !class_exists('\AKSRT\Settings\Settings')) {
@@ -357,14 +362,7 @@ class Home {
$state = '';
if (class_exists('\AKSRT\User\Restriction')) {
$svc = \AKSRT\User\Restriction::get_instance();
if ($svc->is_banned($user_id)) {
$state = 'banned';
} elseif ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_SUSPEND)) {
$state = 'restricted';
} elseif ($svc->is_restricted($user_id, \AKSRT\User\Restriction::TYPE_MUTE)) {
$state = 'muted';
}
$state = \AKSRT\User\Restriction::get_instance()->get_display_state($user_id);
}
if ($state === '') {
return [];
@@ -372,6 +370,7 @@ class Home {
return [
'state' => $state,
'label' => \AKSRT\User\Restriction::state_labels()[$state] ?? '',
'text' => (string) $settings['report']['ban_notice_text'],
];
}
+45
View File
@@ -22,6 +22,14 @@ class Restriction {
const TYPES = [self::TYPE_WARN, self::TYPE_MUTE, self::TYPE_SUSPEND, self::TYPE_BAN];
const SCOPES = [self::SCOPE_GLOBAL, self::SCOPE_COMMENT];
/**
* 治理展示态(与 user-home.css 的 --banned/--restricted/--muted 色调修饰符同名):
* 资料卡提示框、用户中心页头、评论拒绝态、昵称徽章共用同一组键。
*/
const STATE_BANNED = 'banned';
const STATE_RESTRICTED = 'restricted';
const STATE_MUTED = 'muted';
public static function get_instance(): self {
if (null === self::$instance) {
self::$instance = new self();
@@ -182,6 +190,43 @@ class Restriction {
return $this->is_restricted($user_id, self::TYPE_BAN, self::SCOPE_GLOBAL);
}
/**
* 治理展示态(空串 = 无治理状态;ban 优先于 suspend,对齐后台单一状态语义)。
*
* 供资料卡提示框(Home::get_profile_notice)、用户中心页头、评论拒绝态、
* 昵称徽章共用,文案见 state_labels()。
*/
public function get_display_state(int $user_id): string {
if ($user_id <= 0) {
return '';
}
if ($this->is_banned($user_id)) {
return self::STATE_BANNED;
}
if ($this->is_suspended($user_id)) {
return self::STATE_RESTRICTED;
}
if ($this->is_restricted($user_id, self::TYPE_MUTE)) {
return self::STATE_MUTED;
}
return '';
}
/**
* 治理展示态徽章文案 — 对齐参考主题公开主页禁封标识
* (zib_ban_author_header_identity_filter:封号 → 违规已封号,
* 小黑屋 → 小黑屋禁封中);禁言为 AKSRT 扩展状态。
*
* @return array<string,string> 展示态 => 文案
*/
public static function state_labels(): array {
return [
self::STATE_BANNED => __('违规已封号', 'aksrt'),
self::STATE_RESTRICTED => __('小黑屋禁封中', 'aksrt'),
self::STATE_MUTED => __('禁言中', 'aksrt'),
];
}
/**
* Check if a user is in 小黑屋 (active global suspend).
*
+57 -6
View File
@@ -22,8 +22,39 @@ if (!is_user_logged_in()) {
$user_id = get_current_user_id();
$settings = class_exists(BanAppeal::class) ? BanAppeal::settings() : ['enabled' => true, 'desc' => '', 'keys' => []];
$ban = class_exists(Restriction::class) ? Restriction::get_instance()->get_active_ban($user_id) : null;
if (!$ban && class_exists(Restriction::class)) {
$ban = Restriction::get_instance()->get_active_suspend($user_id);
}
$status = class_exists(BanAppeal::class) ? BanAppeal::get_status($user_id) : '';
$payload = class_exists(BanAppeal::class) ? BanAppeal::get_payload($user_id) : [];
// 禁封状态信息(对齐参考主题 zib_get_ban_info_modal:状态/起止时间/原因/说明/申诉权限)。
$ban_status_text = '';
$ban_reason = '';
$ban_desc = '';
$ban_end_text = '';
$no_appeal = get_user_meta($user_id, '_aksrt_no_appeal', true);
$no_appeal_desc = is_array($no_appeal) ? (string) ($no_appeal['desc'] ?? '') : '';
if (is_array($ban)) {
$ban_status_text = (string) ($ban['type'] ?? '') === Restriction::TYPE_BAN
? __('禁封中(禁止登录)', 'aksrt')
: __('小黑屋禁封中', 'aksrt');
$ban_end_text = !empty($ban['expires_at']) ? (string) $ban['expires_at'] : __('永久', 'aksrt');
$raw_reason = (string) ($ban['reason'] ?? '');
$ban_reason = $raw_reason;
$desc_marker = "\n" . __('禁封说明:', 'aksrt');
$marker_pos = strpos($raw_reason, $desc_marker);
if (false !== $marker_pos) {
$ban_reason = substr($raw_reason, 0, $marker_pos);
$ban_desc = substr($raw_reason, $marker_pos + strlen($desc_marker));
}
}
$ban_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>';
};
?>
<main class="container page-template-ban-appeal">
<div class="content-wrap">
@@ -32,6 +63,32 @@ $payload = class_exists(BanAppeal::class) ? BanAppeal::get_payload($user_id) : [
<h1 class="article-title"><?php echo esc_html(get_the_title() ?: __('禁封申诉', 'aksrt')); ?></h1>
</div>
<?php if (is_array($ban)) : ?>
<div class="theme-box radius8 main-bg main-shadow box-body aksrt-ban-status mb20">
<h2 class="mb15"><?php esc_html_e('禁封状态', 'aksrt'); ?></h2>
<div class="muted-box padding-10 em09">
<?php
echo $ban_set_row(__('状态', 'aksrt'), $ban_status_text); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped -- row builder escapes.
echo $ban_set_row(__('开始时间', 'aksrt'), (string) ($ban['created_at'] ?? ''));
echo $ban_set_row(__('结束时间', 'aksrt'), $ban_end_text);
echo $ban_set_row(__('原因', 'aksrt'), $ban_reason);
echo $ban_set_row(__('说明', 'aksrt'), $ban_desc);
?>
<div class="flex">
<div class="author-set-left" style="min-width: 80px;"><?php esc_html_e('申诉权限', 'aksrt'); ?></div>
<div class="author-set-right mt6">
<?php if ($no_appeal_desc !== '') : ?>
<span class="c-red"><?php esc_html_e('本次禁封您将不能发起申诉', 'aksrt'); ?></span>
<div class="mt6"><?php echo esc_html($no_appeal_desc); ?></div>
<?php else : ?>
<?php esc_html_e('可提交申诉', 'aksrt'); ?>
<?php endif; ?>
</div>
</div>
</div>
</div>
<?php endif; ?>
<div class="theme-box radius8 main-bg main-shadow box-body aksrt-ban-appeal">
<?php if (!$ban) : ?>
<div class="muted-2-color text-center" style="padding:40px 0;">
@@ -49,12 +106,6 @@ $payload = class_exists(BanAppeal::class) ? BanAppeal::get_payload($user_id) : [
<div class="muted-2-color mt6"><?php echo esc_html(sprintf(__('提交时间:%s', 'aksrt'), (string) $payload['time'])); ?></div>
<?php endif; ?>
<?php else : ?>
<?php if (!empty($ban['reason'])) : ?>
<div class="muted-box mb20">
<strong><?php esc_html_e('禁封原因', 'aksrt'); ?>:</strong>
<?php echo esc_html((string) $ban['reason']); ?>
</div>
<?php endif; ?>
<?php if (!empty($settings['desc'])) : ?>
<div class="muted-color mb20"><?php echo wp_kses_post(nl2br(esc_html((string) $settings['desc']))); ?></div>
<?php endif; ?>
+20 -3
View File
@@ -66,7 +66,9 @@ $unread_count = $is_own ? aksrt_get_user_unread_notification_count($user_id) : 0
$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;
// 已封禁/小黑屋用户不展示举报入口(参考主题 zib_get_report_link 排除语义)。
$target_ban_restricted = function_exists('aksrt_is_user_ban_restricted') && aksrt_is_user_ban_restricted($user_id);
$show_cover_report = $cover_ban_html === '' && $show_report && !$is_own && !$target_ban_restricted;
$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) : ?>
@@ -154,10 +156,25 @@ $show_cover_menu = $has_more_details || $show_cover_report || $cover_ban_html !=
<?php endif; ?>
<?php if (!empty($notice) && is_array($notice)) : ?>
<div class="aksrt-author-hero__notice aksrt-author-hero__notice--<?php echo esc_attr((string) ($notice['tone'] ?? 'muted')); ?>">
<?php echo esc_html((string) ($notice['message'] ?? '')); ?>
<?php
// 数据契约:\AKSRT\User\Home::get_profile_notice() 输出 state|label|text;
// 提示框色调修饰符与 state 同名(user-home.css:--banned/--restricted/--muted)。
$notice_state = (string) ($notice['state'] ?? '');
$notice_tone = in_array($notice_state, ['banned', 'restricted', 'muted'], true) ? $notice_state : 'muted';
$notice_label = trim((string) ($notice['label'] ?? ''));
$notice_text = trim((string) ($notice['text'] ?? ''));
$notice_message = $notice_label !== '' && $notice_text !== ''
? $notice_label . ',' . $notice_text
: ($notice_label !== '' ? $notice_label : $notice_text);
$notice_icon = 'muted' === $notice_state ? 'comment-slash' : 'ban';
?>
<?php if ($notice_message !== '') : ?>
<div class="aksrt-author-hero__notice aksrt-author-hero__notice--<?php echo esc_attr($notice_tone); ?>">
<i class="fa fas fa-<?php echo esc_attr($notice_icon); ?>" aria-hidden="true"></i>
<span><?php echo esc_html($notice_message); ?></span>
</div>
<?php endif; ?>
<?php endif; ?>
</div>
@@ -19,6 +19,16 @@ 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')
const authorHero = read('templates/parts/user/author-hero.php')
const ucHeader = read('inc/helpers/user-center-renderers/profile-header.php')
const commentsHelper = read('inc/helpers/aksrt-comments.php')
const appealPage = read('templates/pages/page-ban-appeal.php')
const identityHelper = read('inc/helpers/aksrt-identity.php')
const avatarHelper = read('inc/helpers/aksrt-avatar.php')
const singleComponents = read('inc/helpers/aksrt-single-components.php')
const banAppeal = read('src/User/BanAppeal.php')
const mainCss = read('assets/css/main.css')
const mainMinCss = read('assets/css/main.min.css')
test('report REST surface is canonical: submit + users/reports only', () => {
assert.match(userController, /'\/report\/submit'/)
@@ -130,9 +140,75 @@ test('ban appeal approval actually lifts restrictions', () => {
})
test('profile governance notice reads from the restriction table', () => {
assert.match(home, /->is_banned\(\$user_id\)/)
assert.match(home, /->get_display_state\(\$user_id\)/)
assert.match(home, /Restriction::state_labels\(\)\[\$state\]/)
assert.match(home, /\\AKSRT\\User\\Restriction::get_instance\(\)/)
assert.ok(!home.includes('aksrt_user_ban_state'))
// 提示框内容契约:state|label|text,label 对齐参考主题公开主页禁封徽章文案。
assert.match(home, /'state'\s*=> \$state/)
assert.match(home, /'label'\s*=>/)
assert.match(home, /'text'\s*=> \(string\) \$settings\['report'\]\['ban_notice_text'\]/)
// 模板必须消费真实契约键:tone/message 双键曾导致资料卡下方提示框渲染为空。
assert.match(authorHero, /\$notice\['state'\]/)
assert.match(authorHero, /\$notice\['text'\]/)
assert.match(authorHero, /\$notice\['label'\]/)
assert.ok(!authorHero.includes("$notice['message']"), 'template must not read the removed message key')
assert.ok(!authorHero.includes("$notice['tone']"), 'template must not read the removed tone key')
})
test('governance display state is a single source shared by every prompt surface', () => {
// 展示态与文案集中在 Restriction(资料卡/用户中心/评论拒绝态/昵称徽章共用)。
for (const state of ['STATE_BANNED', 'STATE_RESTRICTED', 'STATE_MUTED']) {
assert.match(restriction, new RegExp(`const ${state}\\s+=`), `missing display state: ${state}`)
}
assert.match(restriction, /public function get_display_state\(int \$user_id\): string/)
assert.match(restriction, /public static function state_labels\(\): array/)
for (const label of ["__('违规已封号', 'aksrt')", "__('小黑屋禁封中', 'aksrt')", "__('禁言中', 'aksrt')"]) {
assert.ok(restriction.includes(label), `missing state label: ${label}`)
}
assert.match(restriction, /is_banned\(\$user_id\)[\s\S]*?is_suspended\(\$user_id\)[\s\S]*?is_restricted\(\$user_id, self::TYPE_MUTE\)/)
// 请求级缓存:评论列表逐条渲染昵称徽章不得反复查库。
assert.match(userBanHelper, /static \$cache = \[\]/)
})
test('ban prompt surface mirrors the reference theme entry points', () => {
// 用户中心页头(参考主题 user_page_header_desc / zib_get_user_ban_info_link)。
assert.match(userBanHelper, /function aksrt_get_user_ban_notice_html/)
assert.match(userBanHelper, /function aksrt_get_ban_appeal_page_url/)
assert.match(ucHeader, /aksrt_get_user_ban_notice_html/)
// 评论拒绝态(参考主题 zib_get_user_ban_nocan_info)。
assert.match(userBanHelper, /function aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /aksrt_get_user_ban_nocan_html/)
assert.match(commentsHelper, /暂时无法发表评论/)
// 昵称徽章 + 头像角标(参考主题 user_name_badge / user_avatar_badge)。
assert.match(userBanHelper, /function aksrt_get_user_ban_name_badge/)
assert.match(userBanHelper, /function aksrt_get_user_ban_avatar_badge/)
assert.match(identityHelper, /aksrt_get_user_ban_name_badge\(\$user_id\)/)
assert.match(avatarHelper, /aksrt_get_user_ban_avatar_badge\(\$user_id\)/)
// 禁封申诉页完整状态信息(参考主题 zib_get_ban_info_modal)。
assert.match(appealPage, /get_active_suspend\(\$user_id\)/)
for (const row of ['状态', '开始时间', '结束时间', '原因', '说明']) {
assert.match(appealPage, new RegExp(`__\\('${row}', 'aksrt'\\)`), `appeal page missing status row: ${row}`)
}
assert.match(appealPage, /esc_html_e\('申诉权限', 'aksrt'\)/)
assert.match(appealPage, /禁封中\(禁止登录\)/)
assert.match(appealPage, /本次禁封您将不能发起申诉/)
// 禁封/小黑屋均可申诉(参考主题 zib_user_is_ban 双类型语义)。
assert.match(banAppeal, /STATE_BANNED, \\AKSRT\\User\\Restriction::STATE_RESTRICTED/)
// 禁封站内信附申诉入口(参考主题 zib_updata_user_ban_msg 申诉指引)。
assert.match(restrictionOps, /aksrt_get_ban_appeal_page_url/)
// 头像角标样式:源文件与入队的 min 产物同步。
for (const css of [mainCss, mainMinCss]) {
assert.ok(css.includes('aksrt-avatar-ban-badge'), 'ban avatar badge CSS must exist in both main.css and main.min.css')
}
})
test('report entry hides targets already banned or suspended', () => {
// 参考主题 zib_get_report_link:已封禁/小黑屋用户不展示举报入口(禁言不隐藏)。
assert.match(userBanHelper, /function aksrt_is_user_ban_restricted/)
assert.match(commentItem, /aksrt_is_user_ban_restricted\(\$author_id\)/)
assert.match(singleComponents, /aksrt_is_user_ban_restricted\(\$post_author_id\)/)
assert.match(authorHero, /aksrt_is_user_ban_restricted\(\$user_id\)/)
})
test('admin report notification links to the real page', () => {