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 '';
+ }
+
+ // Text fallback (rare): small pill labelled with badge text.
+ $extra = $class !== '' ? ' ' . esc_attr($class) : '';
+ return '';
+ }
+}
+
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 编辑。',
],
]],