From 1d75170f8e924c424d752344a12bafbcad263f1d Mon Sep 17 00:00:00 2001 From: unknown Date: Thu, 16 Jul 2026 22:17:21 +0800 Subject: [PATCH] feat(user): badge icon urls and wear medal chrome Extend badge dictionary with optional icon_url, prefer aksrt_wear_badge when assigned, and render medal images in identity chips. --- .../admin/src/schema/sections/users/badge.js | 9 ++++- assets/css/frontend/comments.css | 17 +++++++++ assets/css/frontend/single-post/cover.css | 8 +++++ .../schema/class-aksrt-admin-schema-users.php | 8 ++++- inc/classes/class-aksrt-user-home.php | 31 ++++++++++++---- .../class-aksrt-settings-user-runtime.php | 12 ++++--- inc/helpers/aksrt-identity.php | 35 +++++++++++++++---- 7 files changed, 99 insertions(+), 21 deletions(-) diff --git a/assets/admin/src/schema/sections/users/badge.js b/assets/admin/src/schema/sections/users/badge.js index 48fc7c91..768e55f6 100644 --- a/assets/admin/src/schema/sections/users/badge.js +++ b/assets/admin/src/schema/sections/users/badge.js @@ -25,7 +25,14 @@ export const badgeSchema = { title: '徽章字典', icon: 'fas fa-bookmark', fields: [ - { key: 'user_badge_dictionary', label: '徽章列表', type: 'textarea', default: 'founder|创始成员|warning\nstaff|站务团队|info', help: '每行一个徽章:slug|显示文案|色调(neutral/info/success/warning/brand)', dependsOn: { key: 'user_badge_enabled', value: true } }, + { + key: 'user_badge_dictionary', + label: '徽章列表', + type: 'textarea', + default: 'founder|创始成员|warning\nstaff|站务团队|info', + help: '每行一个徽章:slug|显示文案|色调(neutral/info/success/warning/brand)|图标URL(可选)。有 icon_url 时前台显示勋章图;用户 meta aksrt_wear_badge 指定当前佩戴一枚。', + dependsOn: { key: 'user_badge_enabled', value: true }, + }, ], }, ], diff --git a/assets/css/frontend/comments.css b/assets/css/frontend/comments.css index 783dfbd6..72806fc6 100644 --- a/assets/css/frontend/comments.css +++ b/assets/css/frontend/comments.css @@ -688,6 +688,23 @@ body.dark-theme .comment-login-prompt__inner { font-size: 11px; padding: 0 6px; } +.medal-icon.img-icon, +.aksrt-identity-medal { + display: inline-block; + width: 1.35em; + height: 1.35em; + object-fit: contain; + vertical-align: middle; + margin-left: 4px; +} +.aksrt-identity-medal--comment { + width: 1.15em; + height: 1.15em; +} +.aksrt-identity-medal.is-wear { + outline: 1px solid rgba(245, 158, 11, 0.45); + border-radius: 3px; +} .aksrt-comment__badge--hot { color: #ea580c; background: rgba(234, 88, 12, 0.1); diff --git a/assets/css/frontend/single-post/cover.css b/assets/css/frontend/single-post/cover.css index eb863168..3af39fea 100644 --- a/assets/css/frontend/single-post/cover.css +++ b/assets/css/frontend/single-post/cover.css @@ -285,6 +285,14 @@ line-height: 1.2; vertical-align: middle; } +.article-avatar .medal-icon.img-icon, +.article-avatar .aksrt-identity-medal { + width: 1.4em; + height: 1.4em; + object-fit: contain; + vertical-align: middle; + margin-left: 4px; +} .article-avatar .display-name { margin-right: 2px; } diff --git a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php index 6edd2ed8..f06e99a6 100644 --- a/inc/classes/admin/schema/class-aksrt-admin-schema-users.php +++ b/inc/classes/admin/schema/class-aksrt-admin-schema-users.php @@ -648,7 +648,13 @@ class AKSRT_Admin_Schema_Users { ['key' => 'user_badge_limit', 'label' => '最多显示数量', 'type' => 'number', 'default' => 3, 'min' => 1, 'max' => 8], ]], ['key' => 'dictionary', 'title' => '徽章字典', 'fields' => [ - ['key' => 'user_badge_dictionary', 'label' => '徽章列表', 'type' => 'textarea', 'default' => "founder|创始成员|warning\nstaff|站务团队|info"], + [ + 'key' => 'user_badge_dictionary', + 'label' => '徽章列表', + 'type' => 'textarea', + 'default' => "founder|创始成员|warning\nstaff|站务团队|info", + 'help' => '每行:slug|显示文案|色调(neutral/info/success/warning/brand)|图标URL(可选)。配置 icon_url 后前台显示勋章图;用户 meta aksrt_wear_badge 指定当前佩戴。', + ], ]], ], ], diff --git a/inc/classes/class-aksrt-user-home.php b/inc/classes/class-aksrt-user-home.php index e70c5438..8b5d1c76 100644 --- a/inc/classes/class-aksrt-user-home.php +++ b/inc/classes/class-aksrt-user-home.php @@ -130,7 +130,7 @@ class AKSRT_User_Home { /** * 获取公开展示的身份信号(认证 / 等级 / 徽章)。 * - * @return array + * @return array */ public function get_profile_signals(int $user_id): array { if ($user_id <= 0 || !class_exists('AKSRT_Settings')) { @@ -142,6 +142,7 @@ class AKSRT_User_Home { if (!empty($settings['verify']['enabled']) && !empty($settings['verify']['public_display']) && $this->is_user_verified($user_id, $settings['verify'])) { $signals[] = [ + 'type' => 'verify', 'label' => (string) $settings['verify']['badge_text'], 'icon' => 'check-circle', 'tone' => 'success', @@ -152,6 +153,7 @@ class AKSRT_User_Home { $level = $this->get_user_level($user_id, $settings['level']); if ($level > 0) { $signals[] = [ + 'type' => 'level', 'label' => (string) $settings['level']['label_prefix'] . $level, 'icon' => 'layer-group', 'tone' => 'info', @@ -171,21 +173,36 @@ class AKSRT_User_Home { if (!is_array($assigned)) { $assigned = preg_split('/[\s,]+/', (string) $assigned, -1, PREG_SPLIT_NO_EMPTY); } + $assigned = array_values(array_filter(array_map(static function ($slug) { + return sanitize_key((string) $slug); + }, (array) $assigned))); + + // Prefer worn badge first (Zibll wear-medal). + $wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true)); + if ($wear !== '' && in_array($wear, $assigned, true) && isset($badge_map[$wear])) { + $assigned = array_values(array_unique(array_merge([$wear], $assigned))); + } $limit = max(1, (int) ($settings['badge']['limit'] ?? 3)); - foreach ((array) $assigned as $slug) { - $slug = sanitize_key((string) $slug); + $badge_count = 0; + foreach ($assigned as $slug) { if ($slug === '' || !isset($badge_map[$slug])) { continue; } + $icon_url = (string) ($badge_map[$slug]['icon_url'] ?? ''); $signals[] = [ - 'label' => (string) $badge_map[$slug]['label'], - 'icon' => 'award', - 'tone' => (string) ($badge_map[$slug]['tone'] ?? 'neutral'), + 'type' => 'badge', + 'slug' => $slug, + 'label' => (string) $badge_map[$slug]['label'], + 'icon' => 'award', + 'tone' => (string) ($badge_map[$slug]['tone'] ?? 'neutral'), + 'icon_url' => $icon_url, + 'wear' => ($wear !== '' && $slug === $wear), ]; + $badge_count++; - if (count($signals) >= $limit + 2) { + if ($badge_count >= $limit) { break; } } diff --git a/inc/classes/settings/class-aksrt-settings-user-runtime.php b/inc/classes/settings/class-aksrt-settings-user-runtime.php index bb92f9a8..78bd1a27 100644 --- a/inc/classes/settings/class-aksrt-settings-user-runtime.php +++ b/inc/classes/settings/class-aksrt-settings-user-runtime.php @@ -120,7 +120,7 @@ class AKSRT_Settings_User_Runtime { * @return array{ * verify:array{enabled:bool,public_display:bool,badge_text:string,accept_email_verified:bool,require_manual_review:bool}, * level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array}, - * badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array}, + * badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array}, * report:array{enabled:bool,public_entry:bool,public_notice:bool,ban_notice_text:string,categories:array} * } */ @@ -238,7 +238,7 @@ class AKSRT_Settings_User_Runtime { } /** - * @return array + * @return array */ public static function parse_badge_dictionary(string $raw): array { $lines = preg_split('/[\r\n]+/', $raw, -1, PREG_SPLIT_NO_EMPTY); @@ -254,6 +254,7 @@ class AKSRT_Settings_User_Runtime { $slug = sanitize_key((string) $parts[0]); $label = sanitize_text_field((string) $parts[1]); $tone = isset($parts[2]) ? sanitize_key((string) $parts[2]) : 'neutral'; + $icon_url = isset($parts[3]) ? esc_url_raw((string) $parts[3]) : ''; if ($slug === '' || $label === '') { continue; @@ -264,9 +265,10 @@ class AKSRT_Settings_User_Runtime { } $items[] = [ - 'slug' => $slug, - 'label' => $label, - 'tone' => $tone, + 'slug' => $slug, + 'label' => $label, + 'tone' => $tone, + 'icon_url' => $icon_url, ]; } diff --git a/inc/helpers/aksrt-identity.php b/inc/helpers/aksrt-identity.php index 3c9ac91c..8ff34f17 100644 --- a/inc/helpers/aksrt-identity.php +++ b/inc/helpers/aksrt-identity.php @@ -9,7 +9,7 @@ defined('ABSPATH') || exit; if (!function_exists('aksrt_get_user_identity_signals')) { /** - * @return list + * @return list */ function aksrt_get_user_identity_signals(int $user_id): array { if ($user_id <= 0) { @@ -27,6 +27,8 @@ if (!function_exists('aksrt_render_user_identity_chips')) { /** * Render compact identity chips (verify / level / badges). * + * Badge signals with icon_url render as Zibll-like medal images. + * * @param int $user_id * @param string $context author|comment|card */ @@ -42,8 +44,9 @@ if (!function_exists('aksrt_render_user_identity_chips')) { } } $signals = aksrt_get_user_identity_signals($user_id); - $limit = $context === 'comment' ? 3 : 6; - $n = 0; + // Comments stay compact; author/card can show more. + $limit = $context === 'comment' ? 3 : 6; + $n = 0; foreach ($signals as $signal) { if ($n >= $limit) { break; @@ -52,14 +55,32 @@ if (!function_exists('aksrt_render_user_identity_chips')) { if ($label === '') { continue; } - $tone = sanitize_html_class((string) ($signal['tone'] ?? 'info')); - $icon = sanitize_html_class((string) ($signal['icon'] ?? 'check')); + $tone = sanitize_html_class((string) ($signal['tone'] ?? 'info')); + $icon = sanitize_html_class((string) ($signal['icon'] ?? 'check')); + $icon_url = esc_url((string) ($signal['icon_url'] ?? '')); + $title = (string) ($signal['title'] ?? $label); + $is_wear = !empty($signal['wear']); + $type = sanitize_html_class((string) ($signal['type'] ?? '')); + + // Image medal when dictionary provides icon_url. + if ($icon_url !== '') { + $classes = 'medal-icon img-icon ml3 aksrt-identity-medal aksrt-identity-medal--' . $context; + if ($is_wear) { + $classes .= ' is-wear'; + } + if ($type !== '') { + $classes .= ' aksrt-identity-medal--' . $type; + } + $html .= '' . esc_attr($label) . ''; + $n++; + continue; + } + // Font Awesome: map short names to fa-* if needed. if ($icon !== '' && !str_contains($icon, 'fa-')) { $icon = 'fa-' . $icon; } - $title = (string) ($signal['title'] ?? $label); - $html .= ''; + $html .= ''; if ($icon !== '') { $html .= ' '; }