diff --git a/assets/admin/src/schema/sections/users/badge.js b/assets/admin/src/schema/sections/users/badge.js index 9e12cdb1..914b2444 100644 --- a/assets/admin/src/schema/sections/users/badge.js +++ b/assets/admin/src/schema/sections/users/badge.js @@ -17,6 +17,14 @@ export const MEDAL_GET_TYPES = [ { label: '仅手动授予', value: 'manually_add' }, ] +// Resolve medal SVG asset path from theme assets when running in admin context. +const MEDAL_BASE = (() => { + const root = window.aksrtAdminData?.themeUri || '' + return root ? String(root).replace(/\/$/, '') + '/assets/img/medal' : '/assets/img/medal' +})() + +const medalIcon = (n) => `${MEDAL_BASE}/medal-${n}.svg` + export const defaultMedalArgs = () => [ { cat_name: '成长', @@ -25,7 +33,7 @@ export const defaultMedalArgs = () => [ slug: 'rookie', name: '初出茅庐', desc: '注册时间超过30天', - icon: '', + icon: medalIcon(1), get_type: 'registration_time', get_val: 30, }, @@ -33,10 +41,26 @@ export const defaultMedalArgs = () => [ slug: 'checkin-30', name: '签到达人', desc: '累计签到30天', - icon: '', + icon: medalIcon(2), get_type: 'checkin_all_day', get_val: 30, }, + { + slug: 'writer', + name: '创作先锋', + desc: '发布10篇文章', + icon: medalIcon(5), + get_type: 'new_post', + get_val: 10, + }, + { + slug: 'popular', + name: '人气之星', + desc: '累计获得100次点赞', + icon: medalIcon(9), + get_type: 'like', + get_val: 100, + }, ], }, { @@ -46,7 +70,15 @@ export const defaultMedalArgs = () => [ slug: 'annual-star', name: '年度人气达人', desc: '活动限定徽章', - icon: '', + icon: medalIcon(12), + get_type: 'manually_add', + get_val: 0, + }, + { + slug: 'vip-member', + name: '尊贵会员', + desc: '给予 VIP 用户的特殊徽章', + icon: medalIcon(15), get_type: 'manually_add', get_val: 0, }, diff --git a/assets/css/routes/user-center/shell.css b/assets/css/routes/user-center/shell.css index 580abfdf..27454ab8 100644 --- a/assets/css/routes/user-center/shell.css +++ b/assets/css/routes/user-center/shell.css @@ -25,6 +25,20 @@ padding: 0 0 40px; } +/* Medal icon alignment in UC header h1 name row (zibll parity). */ +.uc-header__name .medal-icon.img-icon, +.uc-header__name .aksrt-identity-medal, +h1.uc-header__name .medal-icon.img-icon, +h1.uc-header__name .aksrt-identity-medal { + width: auto; + height: 1.2em; + max-width: 6em; + object-fit: contain; + vertical-align: middle; + margin-left: 4px; + display: inline-block; +} + /* ==================== 页面自有双栏(对齐 zibll .user-center flex,不走全站 float 壳) ==================== */ /* DOM: sidebar first + content second(与 zibll user-center-sidebar / user-center-content 一致)。 * 选择器同时覆盖 body.aksrt-user-center 与 main.user-center / .aksrt-profile,避免 body class 漏打时失效。 */ diff --git a/inc/helpers/aksrt-identity.php b/inc/helpers/aksrt-identity.php index b2092e6a..782215f7 100644 --- a/inc/helpers/aksrt-identity.php +++ b/inc/helpers/aksrt-identity.php @@ -229,6 +229,92 @@ if (!function_exists('aksrt_get_user_name')) { } } +if (!function_exists('aksrt_get_worn_medal_icon')) { + /** + * Render the worn medal icon HTML for a user (standalone, zibll parity). + * + * Returns an tag when a badge is worn and has an icon_url in the + * dictionary. Handles the edge case where the worn slug lives in user meta + * but the badge class is unavailable (defensive: rebuilds from settings). + * + * @param int $user_id Target user (0 = current user). + * @param string $class Extra CSS classes ( space-separated ). + * @return string HTML safe for echo; '' when nothing to render. + */ + function aksrt_get_worn_medal_icon(int $user_id = 0, string $class = ''): string { + if ($user_id <= 0) { + $user_id = get_current_user_id(); + } + if ($user_id <= 0) { + return ''; + } + + $enabled = false; + $slug = ''; + $icon = ''; + $label = ''; + + if (class_exists('AKSRT\\User\\Badges') && is_callable(['AKSRT\\User\\Badges', 'enabled'])) { + if (\AKSRT\User\Badges::enabled()) { + $enabled = true; + } + } + + if (!$enabled && class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_identity_settings')) { + $ident = \AKSRT\Settings\Settings::get_user_identity_settings(); + if (!empty($ident['badge']['enabled'])) { + $enabled = true; + } + } + + if (!$enabled) { + return ''; + } + + // Class-backed path (canonical). + if (class_exists('AKSRT\\User\\Badges') + && is_callable(['AKSRT\\User\\Badges', 'get_wear']) + && is_callable(['AKSRT\\User\\Badges', 'get_dictionary_map'])) { + $slug = sanitize_key((string) \AKSRT\User\Badges::get_wear($user_id)); + $map = \AKSRT\User\Badges::get_dictionary_map(); + $entry = is_array($map[$slug] ?? null) ? $map[$slug] : []; + if (is_array($entry) && !empty($entry)) { + $icon = isset($entry['icon_url']) ? esc_url((string) $entry['icon_url']) : ''; + $label = isset($entry['label']) ? sanitize_text_field((string) $entry['label']) : ''; + } + } + + // Fallback when class is missing or returned nothing: rebuild from settings. + if ($slug === '' && $icon === '' && class_exists('\AKSRT\Settings\Settings') && method_exists('\AKSRT\Settings\Settings', 'get_user_identity_settings')) { + $ident = \AKSRT\Settings\Settings::get_user_identity_settings(); + $slug = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true)); + if ($slug !== '') { + $dictionary = is_array($ident['badge']['dictionary'] ?? null) ? $ident['badge']['dictionary'] : []; + foreach ((array) $dictionary as $item) { + if (is_array($item) && !empty($item['slug']) && (string) $item['slug'] === $slug) { + $icon = isset($item['icon_url']) ? esc_url((string) $item['icon_url']) : ''; + $label = isset($item['label']) ? sanitize_text_field((string) $item['label']) : ''; + break; + } + } + } + } + + if ($slug === '' || ($icon === '' && $label === '')) { + return ''; + } + + if ($icon !== '') { + $extra = $class !== '' ? ' ' . esc_attr($class) : ''; + return '' . esc_attr($label) . ''; + } + + // Text fallback (rare): small pill labelled with badge text. + $extra = $class !== '' ? ' ' . esc_attr($class) : ''; + return ' ' . esc_html($label) . ''; + } +} + if (!function_exists('aksrt_is_blog_comment_hot')) { /** * "神评" for ordinary post comments when likes cross threshold. diff --git a/inc/helpers/user-center-renderers/profile-header.php b/inc/helpers/user-center-renderers/profile-header.php index fcb06c58..8d999988 100644 --- a/inc/helpers/user-center-renderers/profile-header.php +++ b/inc/helpers/user-center-renderers/profile-header.php @@ -41,6 +41,10 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ if ($vip_badge) { echo $vip_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped } + // Worn medal icon inline to the right of the name (zibll parity). + if (function_exists('aksrt_get_worn_medal_icon')) { + echo aksrt_get_worn_medal_icon((int) $user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } // Level icon inline to the right of the name (zibll parity). if (class_exists('\AKSRT\User\Level') && method_exists('\AKSRT\User\Level', 'get_badge_html')) { echo \AKSRT\User\Level::get_badge_html((int) $user_id); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped diff --git a/src/Admin/Schema/SchemaUsers.php b/src/Admin/Schema/SchemaUsers.php index a1b1ddf4..ef3c298b 100644 --- a/src/Admin/Schema/SchemaUsers.php +++ b/src/Admin/Schema/SchemaUsers.php @@ -27,6 +27,81 @@ class SchemaUsers { ]; } + /** + * Default medal catalog with preset icons from assets/img/medal. + * + * @return array}> + */ + private static function default_medal_args(): array { + $medal = static function (int $n): string { + $path = sprintf('/assets/img/medal/medal-%d.svg', $n); + if (file_exists(get_theme_file_path($path))) { + return get_theme_file_uri($path); + } + return ''; + }; + return [ + [ + 'cat_name' => '成长', + 'items' => [ + [ + 'slug' => 'rookie', + 'name' => '初出茅庐', + 'desc' => '注册时间超过30天', + 'icon' => $medal(1), + 'get_type' => 'registration_time', + 'get_val' => 30, + ], + [ + 'slug' => 'checkin-30', + 'name' => '签到达人', + 'desc' => '累计签到30天', + 'icon' => $medal(2), + 'get_type' => 'checkin_all_day', + 'get_val' => 30, + ], + [ + 'slug' => 'writer', + 'name' => '创作先锋', + 'desc' => '发布10篇文章', + 'icon' => $medal(5), + 'get_type' => 'new_post', + 'get_val' => 10, + ], + [ + 'slug' => 'popular', + 'name' => '人气之星', + 'desc' => '累计获得100次点赞', + 'icon' => $medal(9), + 'get_type' => 'like', + 'get_val' => 100, + ], + ], + ], + [ + 'cat_name' => '限定', + 'items' => [ + [ + 'slug' => 'annual-star', + 'name' => '年度人气达人', + 'desc' => '活动限定徽章', + 'icon' => $medal(12), + 'get_type' => 'manually_add', + 'get_val' => 0, + ], + [ + 'slug' => 'vip-member', + 'name' => '尊贵会员', + 'desc' => '给予 VIP 用户的特殊徽章', + 'icon' => $medal(15), + 'get_type' => 'manually_add', + 'get_val' => 0, + ], + ], + ], + ]; + } + public static function build(): array { return [ 'domain' => 'users', @@ -681,42 +756,7 @@ class SchemaUsers { 'key' => 'user_medal_args', 'label' => '分类与条目', 'type' => 'json', - 'default' => [ - [ - 'cat_name' => '成长', - 'items' => [ - [ - 'slug' => 'rookie', - 'name' => '初出茅庐', - 'desc' => '注册时间超过30天', - 'icon' => '', - 'get_type' => 'registration_time', - 'get_val' => 30, - ], - [ - 'slug' => 'checkin-30', - 'name' => '签到达人', - 'desc' => '累计签到30天', - 'icon' => '', - 'get_type' => 'checkin_all_day', - 'get_val' => 30, - ], - ], - ], - [ - 'cat_name' => '限定', - 'items' => [ - [ - 'slug' => 'annual-star', - 'name' => '年度人气达人', - 'desc' => '活动限定徽章', - 'icon' => '', - 'get_type' => 'manually_add', - 'get_val' => 0, - ], - ], - ], - ], + 'default' => self::default_medal_args(), 'help' => '结构化目录:分类 → 条目(slug/name/desc/icon/get_type/get_val)。由 SPA UsersBadge 编辑。', ], ]],