diff --git a/assets/admin/src/pages/users/UsersBadge.vue b/assets/admin/src/pages/users/UsersBadge.vue
index 2b2363bb..3dda7781 100644
--- a/assets/admin/src/pages/users/UsersBadge.vue
+++ b/assets/admin/src/pages/users/UsersBadge.vue
@@ -2,7 +2,7 @@
@@ -65,11 +65,11 @@
v-model:value="form.user_badge_dictionary"
type="textarea"
:autosize="{ minRows: 5, maxRows: 10 }"
- placeholder="founder|创始成员|warning
staff|站务团队|info"
+ placeholder="founder|创始成员|warning
staff|站务团队|info|https://example.com/medal.png"
:disabled="!form.user_badge_enabled"
/>
- 每行一个徽章,格式 `slug|显示文案|色调`,色调支持 `neutral / info / success / warning / brand`。
+ 每行:`slug|显示文案|色调|图标URL(可选)`。色调:neutral/info/success/warning/brand。授予在用户资料页;前台「我的徽章」可佩戴。
@@ -77,7 +77,7 @@
- 前台从 `aksrt_user_badges` user meta 读取已授予徽章列表,并在公开主页和本人中心头部展示。这里只负责定义合法徽章字典和展示上限。
+ 字典定义合法徽章;`aksrt_user_badges` 存已授予 slug;`aksrt_wear_badge` 为当前佩戴。公开页/评论通过 identity chips 展示(有 icon_url 时为勋章图)。
diff --git a/assets/css/routes/user-center/credits.css b/assets/css/routes/user-center/credits.css
index 216a39ce..94a19592 100644
--- a/assets/css/routes/user-center/credits.css
+++ b/assets/css/routes/user-center/credits.css
@@ -986,3 +986,10 @@ body.uc-modal-open {
color: var(--focus-color, #f04494);
}
.focus-color { color: var(--focus-color, #f59e0b); }
+
+/* User badges wear modal */
+.uc-badges-list { max-height: 60vh; overflow: auto; }
+.uc-badges-item { gap: 12px; }
+.uc-badges-item .medal-icon { width: 28px; height: 28px; object-fit: contain; }
+.uc-badges-wear-tag { margin-left: 6px; }
+.uc-badges-empty { padding: 16px 0; text-align: center; }
diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js
new file mode 100644
index 00000000..421c003d
--- /dev/null
+++ b/assets/js/user-badges.js
@@ -0,0 +1,147 @@
+/**
+ * AKSRT user badges — wear modal.
+ */
+(function () {
+ 'use strict';
+
+ var aksrt = window.aksrtData || {};
+ var siteUrl = aksrt.siteUrl || '';
+ if (window.location.protocol === 'https:' && siteUrl.substring(0, 5) === 'http:') {
+ siteUrl = 'https:' + siteUrl.substring(5);
+ }
+ var REST = (siteUrl || '').replace(/\/$/, '') + '/wp-json/aksrt/v1';
+ var NONCE = aksrt.restNonce || aksrt.nonce || '';
+
+ function api(path, options) {
+ var opts = Object.assign({
+ credentials: 'same-origin',
+ headers: Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE }, (options && options.headers) || {}),
+ }, options || {});
+ return fetch(REST + path, opts).then(function (r) {
+ return r.json().catch(function () { return {}; }).then(function (j) {
+ return { ok: r.ok, status: r.status, json: j };
+ });
+ });
+ }
+
+ function toast(msg, type) {
+ if (window.AksrtToast && typeof window.AksrtToast.show === 'function') {
+ window.AksrtToast.show(msg, type || 'info');
+ return;
+ }
+ if (window.aksrtNotify) {
+ window.aksrtNotify(msg, type || 'info');
+ return;
+ }
+ // fallback
+ try { console.log('[AKSRT badges]', msg); } catch (_) {}
+ }
+
+ function escapeHtml(s) {
+ return String(s == null ? '' : s)
+ .replace(/&/g, '&')
+ .replace(//g, '>')
+ .replace(/"/g, '"');
+ }
+
+ function renderList(root, data, canWear) {
+ var body = root.querySelector('[data-aksrt-badges-body]');
+ if (!body) return;
+ var items = (data && data.items) || [];
+ if (!items.length) {
+ body.innerHTML = '' + escapeHtml('暂无徽章') + '
';
+ return;
+ }
+ body.innerHTML = items.map(function (item) {
+ var icon = item.icon_url
+ ? '
'
+ : ' ' + escapeHtml(item.label) + '';
+ var wearBadge = item.wear ? '佩戴中' : '';
+ var action = '';
+ if (canWear && !item.wear) {
+ action = '';
+ } else if (item.wear) {
+ action = '当前佩戴';
+ }
+ return '' +
+ '
' + icon + '
' + escapeHtml(item.label) + '
' + wearBadge + '
' +
+ '
' + action + '
';
+ }).join('');
+ }
+
+ function openModal(root) {
+ root.hidden = false;
+ document.body.classList.add('uc-modal-open');
+ }
+ function closeModal(root) {
+ root.hidden = true;
+ document.body.classList.remove('uc-modal-open');
+ }
+
+ function init() {
+ var openers = document.querySelectorAll('[data-aksrt-badges-open]');
+ var modal = document.querySelector('[data-aksrt-badges-modal]');
+ if (!modal || !openers.length) return;
+
+ var userId = parseInt(modal.getAttribute('data-user-id') || '0', 10);
+ var canWear = modal.getAttribute('data-can-wear') === '1';
+
+ function load() {
+ if (!userId) return Promise.resolve();
+ var body = modal.querySelector('[data-aksrt-badges-body]');
+ if (body) body.innerHTML = '加载中…
';
+ return api('/users/' + userId + '/badges').then(function (r) {
+ if (!r.ok || !r.json || !r.json.data) {
+ if (body) body.innerHTML = '加载失败
';
+ return;
+ }
+ renderList(modal, r.json.data, canWear);
+ });
+ }
+
+ openers.forEach(function (btn) {
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ openModal(modal);
+ load();
+ });
+ });
+
+ modal.querySelectorAll('[data-aksrt-badges-close]').forEach(function (btn) {
+ btn.addEventListener('click', function (e) {
+ e.preventDefault();
+ closeModal(modal);
+ });
+ });
+
+ modal.addEventListener('click', function (e) {
+ var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
+ if (!wearBtn || !canWear) return;
+ e.preventDefault();
+ var slug = wearBtn.getAttribute('data-aksrt-badge-wear') || '';
+ wearBtn.disabled = true;
+ api('/users/me/badges/wear', {
+ method: 'POST',
+ body: JSON.stringify({ slug: slug }),
+ }).then(function (r) {
+ if (!r.ok) {
+ toast((r.json && (r.json.message || (r.json.data && r.json.data.message))) || '佩戴失败', 'error');
+ wearBtn.disabled = false;
+ return;
+ }
+ toast((r.json && r.json.message) || '已佩戴徽章', 'success');
+ renderList(modal, (r.json && r.json.data) || {}, canWear);
+ }).catch(function () {
+ toast('网络错误', 'error');
+ wearBtn.disabled = false;
+ });
+ });
+ }
+
+ if (document.readyState === 'loading') {
+ document.addEventListener('DOMContentLoaded', init);
+ } else {
+ init();
+ }
+})();
diff --git a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php
index 21f9c63b..80e17d1a 100644
--- a/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php
+++ b/inc/classes/scripts-traits/trait-aksrt-scripts-part-1b.php
@@ -761,6 +761,17 @@ trait AKSRT_Scripts_Part_1b {
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
true
);
+
+ $badges_js_path = get_template_directory() . '/assets/js/user-badges.js';
+ if (file_exists($badges_js_path)) {
+ wp_enqueue_script(
+ 'aksrt-user-badges',
+ $base_uri . '/assets/js/user-badges.js',
+ ['aksrt-core'],
+ (string) filemtime($badges_js_path),
+ true
+ );
+ }
}
}
diff --git a/inc/helpers/user-center-renderers/profile-header.php b/inc/helpers/user-center-renderers/profile-header.php
index 38d6b5be..000f6cdf 100644
--- a/inc/helpers/user-center-renderers/profile-header.php
+++ b/inc/helpers/user-center-renderers/profile-header.php
@@ -61,6 +61,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
+
+
+
+