feat(user): wear badge modal and user-center entry
Expose 我的徽章 modal on profile header with REST wear actions and update SPA badge help for icon_url + grant/wear flows.
This commit is contained in:
@@ -2,7 +2,7 @@
|
|||||||
<div class="aksrt-page-container">
|
<div class="aksrt-page-container">
|
||||||
<PageHeader
|
<PageHeader
|
||||||
title="用户徽章"
|
title="用户徽章"
|
||||||
desc="维护公开徽章字典、展示数量和前台展示位。具体授予逻辑可以后续再接业务,但字段和展示契约已经固定。"
|
desc="维护徽章字典(含可选 icon_url)、展示数量。授予/收回在 WP 用户资料页;用户前台可佩戴徽章。"
|
||||||
>
|
>
|
||||||
<template #actions>
|
<template #actions>
|
||||||
<n-space>
|
<n-space>
|
||||||
@@ -65,11 +65,11 @@
|
|||||||
v-model:value="form.user_badge_dictionary"
|
v-model:value="form.user_badge_dictionary"
|
||||||
type="textarea"
|
type="textarea"
|
||||||
:autosize="{ minRows: 5, maxRows: 10 }"
|
: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"
|
:disabled="!form.user_badge_enabled"
|
||||||
/>
|
/>
|
||||||
<template #feedback>
|
<template #feedback>
|
||||||
<span class="aksrt-hint-text">每行一个徽章,格式 `slug|显示文案|色调`,色调支持 `neutral / info / success / warning / brand`。</span>
|
<span class="aksrt-hint-text">每行:`slug|显示文案|色调|图标URL(可选)`。色调:neutral/info/success/warning/brand。授予在用户资料页;前台「我的徽章」可佩戴。</span>
|
||||||
</template>
|
</template>
|
||||||
</n-form-item>
|
</n-form-item>
|
||||||
</n-form>
|
</n-form>
|
||||||
@@ -77,7 +77,7 @@
|
|||||||
|
|
||||||
<n-card class="aksrt-section-card">
|
<n-card class="aksrt-section-card">
|
||||||
<n-alert type="info" :show-icon="false">
|
<n-alert type="info" :show-icon="false">
|
||||||
前台从 `aksrt_user_badges` user meta 读取已授予徽章列表,并在公开主页和本人中心头部展示。这里只负责定义合法徽章字典和展示上限。
|
字典定义合法徽章;`aksrt_user_badges` 存已授予 slug;`aksrt_wear_badge` 为当前佩戴。公开页/评论通过 identity chips 展示(有 icon_url 时为勋章图)。
|
||||||
</n-alert>
|
</n-alert>
|
||||||
</n-card>
|
</n-card>
|
||||||
|
|
||||||
|
|||||||
@@ -986,3 +986,10 @@ body.uc-modal-open {
|
|||||||
color: var(--focus-color, #f04494);
|
color: var(--focus-color, #f04494);
|
||||||
}
|
}
|
||||||
.focus-color { color: var(--focus-color, #f59e0b); }
|
.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; }
|
||||||
|
|||||||
@@ -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, '>')
|
||||||
|
.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 = '<div class="uc-badges-empty muted-2-color em09">' + escapeHtml('暂无徽章') + '</div>';
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
body.innerHTML = items.map(function (item) {
|
||||||
|
var icon = item.icon_url
|
||||||
|
? '<img class="medal-icon img-icon" src="' + escapeHtml(item.icon_url) + '" alt="">'
|
||||||
|
: '<span class="badg badg-sm aksrt-identity-chip aksrt-identity-chip--' + escapeHtml(item.tone || 'neutral') + '"><i class="fa fa-award"></i> ' + escapeHtml(item.label) + '</span>';
|
||||||
|
var wearBadge = item.wear ? '<span class="uc-badges-wear-tag c-yellow em09">佩戴中</span>' : '';
|
||||||
|
var action = '';
|
||||||
|
if (canWear && !item.wear) {
|
||||||
|
action = '<button type="button" class="but jb-blue but-sm" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">佩戴此徽章</button>';
|
||||||
|
} else if (item.wear) {
|
||||||
|
action = '<span class="muted-2-color em09">当前佩戴</span>';
|
||||||
|
}
|
||||||
|
return '<div class="uc-badges-item flex jsb ac border-bottom padding-h10" data-slug="' + escapeHtml(item.slug) + '">' +
|
||||||
|
'<div class="flex ac gap8">' + icon + '<div><div class="font-bold">' + escapeHtml(item.label) + '</div>' + wearBadge + '</div></div>' +
|
||||||
|
'<div class="shrink0">' + action + '</div></div>';
|
||||||
|
}).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 = '<div class="muted-2-color em09">加载中…</div>';
|
||||||
|
return api('/users/' + userId + '/badges').then(function (r) {
|
||||||
|
if (!r.ok || !r.json || !r.json.data) {
|
||||||
|
if (body) body.innerHTML = '<div class="c-red em09">加载失败</div>';
|
||||||
|
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();
|
||||||
|
}
|
||||||
|
})();
|
||||||
@@ -761,6 +761,17 @@ trait AKSRT_Scripts_Part_1b {
|
|||||||
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
||||||
true
|
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
|
||||||
|
);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -61,6 +61,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div class="uc-header__actions">
|
<div class="uc-header__actions">
|
||||||
|
<?php if (class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::enabled()) : ?>
|
||||||
|
<button type="button" class="btn-ghost btn-sm" data-aksrt-badges-open>
|
||||||
|
<i class="fas fa-award" aria-hidden="true"></i> <?php esc_html_e('我的徽章', 'aksrt'); ?>
|
||||||
|
</button>
|
||||||
|
<?php endif; ?>
|
||||||
|
|
||||||
<a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm">
|
<a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm">
|
||||||
<i class="fas fa-bell" aria-hidden="true"></i> <?php esc_html_e('消息通知', 'aksrt'); ?>
|
<i class="fas fa-bell" aria-hidden="true"></i> <?php esc_html_e('消息通知', 'aksrt'); ?>
|
||||||
<?php if ($notification_unread_count > 0) : ?>
|
<?php if ($notification_unread_count > 0) : ?>
|
||||||
@@ -78,3 +84,16 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
|
<?php if (class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::enabled()) : ?>
|
||||||
|
<div class="uc-modal" id="aksrt-badges-modal" hidden data-aksrt-badges-modal data-user-id="<?php echo (int) $user_id; ?>" data-can-wear="1">
|
||||||
|
<div class="uc-modal__overlay" data-aksrt-badges-close></div>
|
||||||
|
<div class="uc-modal__dialog" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e('我的徽章', 'aksrt'); ?>">
|
||||||
|
<button type="button" class="uc-modal__close" data-aksrt-badges-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fas fa-times" aria-hidden="true"></i></button>
|
||||||
|
<h3 class="uc-modal__title"><i class="fas fa-award mr6" aria-hidden="true"></i><?php esc_html_e('我的徽章', 'aksrt'); ?></h3>
|
||||||
|
<div class="uc-badges-list" data-aksrt-badges-body>
|
||||||
|
<div class="muted-2-color em09"><?php esc_html_e('加载中…', 'aksrt'); ?></div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<?php endif; ?>
|
||||||
|
|||||||
Reference in New Issue
Block a user