feat(badges): zibll-style badge modal with category grid, detail view, and wear UX
This commit is contained in:
@@ -0,0 +1,352 @@
|
||||
/* ============================================================
|
||||
* 徽章弹窗(zibll medal 1:1 风格)
|
||||
* ------------------------------------------------------------
|
||||
* 共享于两个入口:
|
||||
* 1. 用户中心「我的徽章」弹窗(.uc-badges-modal,JS 渲染)
|
||||
* 2. 远程弹窗徽章明细/单卡(.medal-modal,PHP 渲染)
|
||||
* 加载条件与 user-badges.js 一致(badge surface 页面)。
|
||||
* ============================================================ */
|
||||
|
||||
/* ---------- 分类分组(zibll .medal-cat-box / .medal-cat-name) ---------- */
|
||||
.medal-cat-box {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 8px 8px 4px;
|
||||
margin-bottom: 16px;
|
||||
background: var(--muted-border-color, rgba(136, 136, 136, 0.06));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
}
|
||||
.medal-cat-box:last-child { margin-bottom: 0; }
|
||||
|
||||
.medal-cat-head { margin-bottom: 6px; }
|
||||
|
||||
.medal-cat-name {
|
||||
display: inline-block;
|
||||
margin-top: -8px;
|
||||
padding: 6px 22px;
|
||||
font-size: 13px;
|
||||
font-weight: 600;
|
||||
color: var(--focus-color);
|
||||
background: var(--focus-color-opacity1);
|
||||
border-radius: 0 0 1000px 1000px;
|
||||
}
|
||||
|
||||
/* ---------- 卡片网格(zibll .medal-list-box / .medal-list) ---------- */
|
||||
.medal-list-box {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(5, 1fr);
|
||||
gap: 8px 4px;
|
||||
}
|
||||
|
||||
.medal-list {
|
||||
display: block;
|
||||
min-width: 0;
|
||||
padding: 0;
|
||||
border: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
color: inherit;
|
||||
cursor: pointer;
|
||||
-webkit-appearance: none;
|
||||
appearance: none;
|
||||
}
|
||||
|
||||
.medal-card {
|
||||
position: relative;
|
||||
text-align: center;
|
||||
padding: 8px 4px 6px;
|
||||
border-radius: var(--mini-radius, 6px);
|
||||
transition: transform 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
.medal-list:hover .medal-card,
|
||||
.medal-list:focus-visible .medal-card {
|
||||
transform: translateY(-2px);
|
||||
background: var(--focus-color-opacity1);
|
||||
}
|
||||
.medal-list:focus-visible { outline: 2px solid var(--focus-color); outline-offset: 2px; border-radius: var(--mini-radius, 6px); }
|
||||
|
||||
.medal-card .medal-icon {
|
||||
width: auto;
|
||||
height: 70px;
|
||||
max-width: 100%;
|
||||
object-fit: contain;
|
||||
}
|
||||
.medal-card .medal-icon--fa {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 56px;
|
||||
height: 70px;
|
||||
font-size: 34px;
|
||||
color: var(--focus-color);
|
||||
}
|
||||
|
||||
/* 未获得:灰度 + 透明(zibll .no-has) */
|
||||
.medal-card.no-has {
|
||||
-webkit-filter: grayscale(100%);
|
||||
filter: grayscale(100%);
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.medal-card__name {
|
||||
margin-top: 6px;
|
||||
font-size: 12px;
|
||||
line-height: 1.4;
|
||||
color: var(--muted-color, var(--color-ink-3));
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
.medal-card.is-has .medal-card__name { color: var(--key-color, var(--color-ink)); }
|
||||
|
||||
.medal-card__wear {
|
||||
margin-top: 2px;
|
||||
font-size: 11px;
|
||||
line-height: 1.3;
|
||||
color: var(--focus-color);
|
||||
}
|
||||
|
||||
/* ---------- 单徽章详情卡(zibll .medal-single-card) ---------- */
|
||||
.medal-single-card {
|
||||
position: relative;
|
||||
overflow: hidden;
|
||||
text-align: center;
|
||||
padding: 22px 14px 18px;
|
||||
margin-bottom: 14px;
|
||||
background: var(--muted-border-color, rgba(136, 136, 136, 0.06));
|
||||
border-radius: var(--main-radius, 8px);
|
||||
}
|
||||
.medal-single-bg {
|
||||
position: absolute;
|
||||
top: 43px;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: 0.5;
|
||||
pointer-events: none;
|
||||
}
|
||||
.medal-single-bg img {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
object-fit: cover;
|
||||
}
|
||||
.medal-single-main { position: relative; }
|
||||
|
||||
.medal-single-card .medal-icon {
|
||||
width: auto;
|
||||
height: 120px;
|
||||
max-width: 200px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.medal-single-card .medal-icon--fa {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
height: 120px;
|
||||
font-size: 72px;
|
||||
color: var(--focus-color);
|
||||
}
|
||||
|
||||
.medal-single-name {
|
||||
margin-top: 12px;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.medal-single-desc {
|
||||
margin-top: 8px;
|
||||
font-size: 12px;
|
||||
line-height: 1.6;
|
||||
}
|
||||
.medal-single-state {
|
||||
margin-top: 10px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
/* ---------- 弹窗骨架(远程弹窗 .medal-modal) ---------- */
|
||||
.medal-modal { position: relative; padding: 6px 4px 12px; }
|
||||
.medal-modal__empty {
|
||||
padding: 24px 0;
|
||||
text-align: center;
|
||||
color: var(--muted-2-color, var(--color-ink-4));
|
||||
}
|
||||
|
||||
.medal-modal__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
padding-right: 28px;
|
||||
}
|
||||
.medal-modal__avatar {
|
||||
width: 42px;
|
||||
height: 42px;
|
||||
border-radius: 50%;
|
||||
object-fit: cover;
|
||||
box-shadow: 0 0 0 2px var(--focus-color-opacity1);
|
||||
}
|
||||
.medal-modal__title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
flex-wrap: wrap;
|
||||
min-width: 0;
|
||||
font-size: 15px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.medal-modal__title .medal-icon {
|
||||
width: auto;
|
||||
height: 1.6em;
|
||||
margin-left: 4px;
|
||||
object-fit: contain;
|
||||
}
|
||||
|
||||
.medal-modal__scroll {
|
||||
max-height: 50vh;
|
||||
max-height: 50dvh;
|
||||
overflow-y: auto;
|
||||
padding: 2px;
|
||||
}
|
||||
|
||||
/* 底部操作区(zibll .modal-buts.but-average 语义) */
|
||||
.medal-modal__buts {
|
||||
display: flex;
|
||||
margin: 12px -4px -12px;
|
||||
border-top: 1px solid var(--main-border-color, var(--color-line));
|
||||
}
|
||||
.medal-modal__buts > .but {
|
||||
flex: 1;
|
||||
padding: 0.7em;
|
||||
background: transparent;
|
||||
border-radius: 0;
|
||||
}
|
||||
.medal-modal__buts > .but + .but { border-left: 1px solid var(--main-border-color, var(--color-line)); }
|
||||
.medal-modal__buts > .but:hover { background: var(--focus-color-opacity05); opacity: 1; }
|
||||
|
||||
/* 单卡弹窗 */
|
||||
.medal-modal--single { padding: 10px 8px 14px; text-align: center; }
|
||||
.medal-modal__actions { margin-top: 4px; }
|
||||
.medal-modal__actions .but.btn-block,
|
||||
.medal-modal__actions .badg.btn-block { display: block; width: 100%; }
|
||||
.medal-modal__back {
|
||||
display: inline-block;
|
||||
margin-top: 12px;
|
||||
text-decoration: none;
|
||||
}
|
||||
.medal-modal__back:hover { color: var(--focus-color); }
|
||||
|
||||
/* ---------- 用户中心「我的徽章」弹窗 ---------- */
|
||||
.uc-badges-dialog {
|
||||
max-width: 560px;
|
||||
padding: 0;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
.uc-badges-head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 16px 46px 12px 18px;
|
||||
border-bottom: 1px solid var(--main-border-color, var(--color-line));
|
||||
}
|
||||
.uc-badges-head__icon {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 34px;
|
||||
height: 34px;
|
||||
flex: none;
|
||||
font-size: 16px;
|
||||
color: var(--focus-color);
|
||||
background: var(--focus-color-opacity1);
|
||||
border-radius: 10px;
|
||||
}
|
||||
.uc-badges-head__title {
|
||||
flex: 1;
|
||||
min-width: 0;
|
||||
font-size: 16px;
|
||||
font-weight: 700;
|
||||
}
|
||||
.uc-badges-head__count {
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 400;
|
||||
color: var(--muted-2-color, var(--color-ink-4));
|
||||
}
|
||||
.uc-badges-head__wear { flex: none; }
|
||||
.uc-badges-head__wear-tag {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
max-width: 140px;
|
||||
padding: 3px 10px;
|
||||
font-size: 12px;
|
||||
color: var(--focus-color);
|
||||
background: var(--focus-color-opacity1);
|
||||
border-radius: 100px;
|
||||
}
|
||||
.uc-badges-head__wear-tag .medal-icon {
|
||||
width: auto;
|
||||
height: 18px;
|
||||
object-fit: contain;
|
||||
}
|
||||
.uc-badges-head__wear-name {
|
||||
overflow: hidden;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
.uc-badges-body {
|
||||
max-height: 56vh;
|
||||
max-height: 56dvh;
|
||||
overflow-y: auto;
|
||||
padding: 16px 14px 18px;
|
||||
}
|
||||
.uc-badges-loading,
|
||||
.uc-badges-error {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
gap: 10px;
|
||||
padding: 32px 0;
|
||||
}
|
||||
|
||||
/* 详情视图 */
|
||||
.uc-badges-view--detail { padding: 8px 16px 18px; }
|
||||
.uc-badges-detail__head { margin-bottom: 6px; }
|
||||
.uc-badges-back {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
gap: 4px;
|
||||
padding: 4px 10px 4px 6px;
|
||||
border: 0;
|
||||
background: none;
|
||||
font: inherit;
|
||||
font-size: 13px;
|
||||
color: var(--muted-2-color, var(--color-ink-4));
|
||||
cursor: pointer;
|
||||
border-radius: var(--mini-radius, 6px);
|
||||
transition: color 0.15s ease, background-color 0.15s ease;
|
||||
}
|
||||
.uc-badges-back:hover {
|
||||
color: var(--focus-color);
|
||||
background: var(--focus-color-opacity1);
|
||||
}
|
||||
|
||||
/* ---------- 细滚动条(zibll .mini-scrollbar) ---------- */
|
||||
.mini-scrollbar { scrollbar-width: thin; scrollbar-color: var(--muted-border-color, rgba(136, 136, 136, 0.3)) transparent; }
|
||||
.mini-scrollbar::-webkit-scrollbar { width: 5px; height: 5px; }
|
||||
.mini-scrollbar::-webkit-scrollbar-thumb { background: var(--muted-border-color, rgba(136, 136, 136, 0.3)); border-radius: 10px; }
|
||||
.mini-scrollbar::-webkit-scrollbar-track { background: transparent; }
|
||||
|
||||
/* ---------- 响应式(zibll: 桌面 5 列 / 移动 4 列) ---------- */
|
||||
@media (max-width: 768px) {
|
||||
.medal-list-box { grid-template-columns: repeat(4, 1fr); }
|
||||
.medal-card .medal-icon { height: 55px; }
|
||||
.medal-card .medal-icon--fa { width: 44px; height: 55px; font-size: 26px; }
|
||||
.medal-single-card .medal-icon { height: 96px; }
|
||||
.uc-badges-dialog { max-width: 100%; }
|
||||
.uc-badges-body { max-height: 62vh; max-height: 62dvh; }
|
||||
}
|
||||
|
||||
@media (max-width: 400px) {
|
||||
.medal-list-box { grid-template-columns: repeat(3, 1fr); }
|
||||
}
|
||||
@@ -738,49 +738,7 @@
|
||||
}
|
||||
.aksrt-author-card__badge img { border-radius: 50%; }
|
||||
|
||||
/* Badge remote modal (batch6) */
|
||||
.aksrt-badge-modal { padding: 8px 4px 12px; }
|
||||
.aksrt-badge-modal__head {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin-bottom: 12px;
|
||||
position: relative;
|
||||
padding-right: 28px;
|
||||
}
|
||||
.aksrt-badge-modal__avatar {
|
||||
width: 40px; height: 40px; border-radius: 50%; object-fit: cover;
|
||||
}
|
||||
.aksrt-badge-modal__title { font-weight: 700; font-size: 15px; flex: 1; min-width: 0; }
|
||||
.aksrt-badge-modal__wear-tag {
|
||||
display: inline-block;
|
||||
margin-left: 6px;
|
||||
font-size: 12px;
|
||||
font-weight: 500;
|
||||
color: var(--color-primary, #3b82f6);
|
||||
}
|
||||
.aksrt-badge-modal__list { max-height: 50vh; overflow: auto; }
|
||||
.aksrt-badge-modal__item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
padding: 10px 4px;
|
||||
border-bottom: 1px solid var(--color-line, #eee);
|
||||
}
|
||||
.aksrt-badge-modal__item:last-child { border-bottom: 0; }
|
||||
.aksrt-badge-modal__item .medal-icon { width: 40px; height: 40px; object-fit: contain; }
|
||||
.aksrt-badge-modal__icon-fa {
|
||||
width: 40px; height: 40px; border-radius: 50%;
|
||||
display: inline-flex; align-items: center; justify-content: center;
|
||||
background: var(--surface-2, #f3f4f6); color: #f59e0b;
|
||||
}
|
||||
.aksrt-badge-modal__icon-fa.lg { width: 72px; height: 72px; font-size: 28px; }
|
||||
.aksrt-badge-modal__meta { flex: 1; min-width: 0; }
|
||||
.aksrt-badge-modal__name { font-weight: 600; font-size: 14px; }
|
||||
.aksrt-badge-modal__empty { text-align: center; color: var(--muted-2-color, #999); padding: 24px 0; }
|
||||
.aksrt-badge-modal--single { text-align: center; padding: 16px 8px 20px; }
|
||||
.aksrt-badge-modal__card { padding: 12px 8px 8px; }
|
||||
.aksrt-badge-modal__actions { margin-top: 12px; }
|
||||
/* 徽章远程弹窗样式已迁至 assets/css/frontend/medal.css(zibll medal 风格)。 */
|
||||
.aksrt-author-hero__badges { margin-top: 8px; }
|
||||
|
||||
/* Forum tab topics (batch6) */
|
||||
|
||||
@@ -1023,13 +1023,7 @@ body.uc-modal-open {
|
||||
}
|
||||
.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; }
|
||||
|
||||
/* 用户中心「我的徽章」弹窗样式已迁至 assets/css/frontend/medal.css(zibll medal 风格)。 */
|
||||
|
||||
.uc-points-tasks__row.is-done { opacity: .72; }
|
||||
.uc-points-tasks__progress-bar {
|
||||
|
||||
+273
-74
@@ -1,5 +1,9 @@
|
||||
/**
|
||||
* AKSRT user badges — wear modal.
|
||||
* AKSRT user badges — zibll medal UX parity.
|
||||
*
|
||||
* User-center modal: category-grouped medal card grid + in-modal detail
|
||||
* view (medal-background card, obtain time, wear action). Also provides the
|
||||
* global wear handler used inside remote (AJAX HTML) badge modals.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
@@ -45,40 +49,36 @@
|
||||
.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;
|
||||
/* ---------- shared partials ---------- */
|
||||
|
||||
function iconHtml(item, cls) {
|
||||
var icon = item && item.icon_url ? String(item.icon_url) : '';
|
||||
var label = item && item.label ? String(item.label) : '';
|
||||
if (icon) {
|
||||
return '<img class="medal-icon img-icon' + (cls ? ' ' + cls : '') + '" src="' + escapeHtml(icon) + '" alt="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '" loading="lazy" decoding="async">';
|
||||
}
|
||||
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('');
|
||||
return '<span class="medal-icon medal-icon--fa' + (cls ? ' ' + cls : '') + '"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
|
||||
function openModal(root) {
|
||||
root.hidden = false;
|
||||
document.body.classList.add('uc-modal-open');
|
||||
function emptyHtml(text, desc) {
|
||||
return '<div class="empty-state empty-state--sm">' +
|
||||
'<div class="empty-state__icon"><i class="fas fa-award" aria-hidden="true"></i></div>' +
|
||||
'<div class="empty-state__title">' + escapeHtml(text) + '</div>' +
|
||||
(desc ? '<div class="empty-state__desc">' + escapeHtml(desc) + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
function closeModal(root) {
|
||||
root.hidden = true;
|
||||
document.body.classList.remove('uc-modal-open');
|
||||
|
||||
function cardHtml(item) {
|
||||
var cls = 'medal-card' + (item.owned ? ' is-has' : ' no-has') + (item.wear ? ' is-wear' : '');
|
||||
return '<div class="' + cls + '">' +
|
||||
iconHtml(item) +
|
||||
'<div class="medal-card__name">' + escapeHtml(item.label) + '</div>' +
|
||||
(item.wear ? '<div class="medal-card__wear">' + escapeHtml('佩戴中') + '</div>' : '') +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
/* ---------- user-center modal ---------- */
|
||||
|
||||
function init() {
|
||||
var openers = document.querySelectorAll('[data-aksrt-badges-open]');
|
||||
var modal = document.querySelector('[data-aksrt-badges-modal]');
|
||||
@@ -86,24 +86,193 @@
|
||||
|
||||
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 medalBg = modal.getAttribute('data-medal-bg') || '';
|
||||
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>';
|
||||
var detailBox = modal.querySelector('[data-aksrt-badges-detail]');
|
||||
var countSlot = modal.querySelector('[data-aksrt-badges-count]');
|
||||
var wearSlot = modal.querySelector('[data-aksrt-badges-wear-slot]');
|
||||
|
||||
var state = { data: null, detailSlug: '' };
|
||||
|
||||
function findItem(slug) {
|
||||
var cats = (state.data && state.data.categories) || [];
|
||||
for (var i = 0; i < cats.length; i++) {
|
||||
var items = cats[i].items || [];
|
||||
for (var j = 0; j < items.length; j++) {
|
||||
if (items[j].slug === slug) return items[j];
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
function wornItem() {
|
||||
var wear = state.data && state.data.wear;
|
||||
return wear ? findItem(wear) : null;
|
||||
}
|
||||
|
||||
/* ----- view switching ----- */
|
||||
function showView(name) {
|
||||
modal.querySelectorAll('[data-aksrt-badges-view]').forEach(function (view) {
|
||||
view.hidden = view.getAttribute('data-aksrt-badges-view') !== name;
|
||||
});
|
||||
if (name === 'grid') state.detailSlug = '';
|
||||
}
|
||||
|
||||
function openModal() {
|
||||
modal.hidden = false;
|
||||
document.body.classList.add('uc-modal-open');
|
||||
}
|
||||
function closeModal() {
|
||||
modal.hidden = true;
|
||||
document.body.classList.remove('uc-modal-open');
|
||||
showView('grid');
|
||||
}
|
||||
|
||||
/* ----- render: head ----- */
|
||||
function renderHead() {
|
||||
var count = state.data && typeof state.data.count === 'number' ? state.data.count : 0;
|
||||
if (countSlot) {
|
||||
countSlot.textContent = count > 0 ? '共 ' + count + ' 枚' : '';
|
||||
}
|
||||
if (!wearSlot) return;
|
||||
var worn = wornItem();
|
||||
wearSlot.innerHTML = worn
|
||||
? '<span class="uc-badges-head__wear-tag" title="' + escapeHtml('当前佩戴:' + worn.label) + '">' + iconHtml(worn) + '<span class="uc-badges-head__wear-name">' + escapeHtml(worn.label) + '</span></span>'
|
||||
: '';
|
||||
}
|
||||
|
||||
/* ----- render: grid ----- */
|
||||
function renderGrid() {
|
||||
if (!body) return;
|
||||
var cats = (state.data && state.data.categories) || [];
|
||||
if (!cats.length) {
|
||||
body.innerHTML = emptyHtml('暂无徽章', '积极参与社区互动即可获得徽章');
|
||||
return;
|
||||
}
|
||||
renderList(modal, r.json.data, canWear);
|
||||
body.innerHTML = cats.map(function (cat) {
|
||||
var cards = (cat.items || []).map(function (item) {
|
||||
return '<button type="button" class="medal-list" data-aksrt-badge-detail="' + escapeHtml(item.slug) + '" aria-label="' + escapeHtml(item.label) + '">' +
|
||||
cardHtml(item) +
|
||||
'</button>';
|
||||
}).join('');
|
||||
return '<div class="medal-cat-box">' +
|
||||
'<div class="medal-cat-head"><b class="medal-cat-name">' + escapeHtml(cat.cat_name || '') + '</b></div>' +
|
||||
'<div class="medal-list-box">' + cards + '</div>' +
|
||||
'</div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
/* ----- render: detail ----- */
|
||||
function renderDetail(slug) {
|
||||
var item = findItem(slug);
|
||||
if (!item || !detailBox) return;
|
||||
state.detailSlug = slug;
|
||||
|
||||
var stateLine = '';
|
||||
if (item.owned && item.time) {
|
||||
stateLine = '<div class="medal-single-state c-blue">' + escapeHtml(item.time + ' 获得') + '</div>';
|
||||
} else if (!item.owned) {
|
||||
stateLine = '<div class="medal-single-state c-yellow">' + escapeHtml('暂未获得') + '</div>';
|
||||
}
|
||||
|
||||
var action = '';
|
||||
if (canWear && item.owned && !item.wear) {
|
||||
action = '<button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">' + escapeHtml('佩戴此徽章') + '</button>';
|
||||
} else if (item.wear) {
|
||||
action = '<span class="badg btn-block c-blue">' + escapeHtml('当前佩戴此徽章') + '</span>';
|
||||
}
|
||||
|
||||
detailBox.innerHTML =
|
||||
'<div class="uc-badges-detail__head">' +
|
||||
'<button type="button" class="uc-badges-back" data-aksrt-badges-back><i class="fas fa-angle-left" aria-hidden="true"></i> ' + escapeHtml('返回列表') + '</button>' +
|
||||
'</div>' +
|
||||
'<div class="medal-single-card">' +
|
||||
(medalBg ? '<div class="medal-single-bg"><img class="fit-cover" src="' + escapeHtml(medalBg) + '" alt=""></div>' : '') +
|
||||
'<div class="medal-single-main">' +
|
||||
iconHtml(item) +
|
||||
'<div class="medal-single-name">' + escapeHtml(item.label) + '</div>' +
|
||||
(item.desc ? '<div class="medal-single-desc muted-2-color">' + escapeHtml(item.desc) + '</div>' : '') +
|
||||
stateLine +
|
||||
'</div>' +
|
||||
'</div>' +
|
||||
(action ? '<div class="medal-modal__actions">' + action + '</div>' : '');
|
||||
|
||||
showView('detail');
|
||||
}
|
||||
|
||||
/* ----- header worn icon sync (profile header, next to the name) ----- */
|
||||
function syncHeaderIcon() {
|
||||
var worn = wornItem();
|
||||
if (!worn || !worn.icon_url) return;
|
||||
document.querySelectorAll('.aksrt-identity-medal.is-wear').forEach(function (node) {
|
||||
if (node.tagName === 'IMG') {
|
||||
node.src = worn.icon_url;
|
||||
node.alt = worn.label;
|
||||
node.title = worn.label;
|
||||
} else {
|
||||
var img = document.createElement('img');
|
||||
img.className = 'medal-icon img-icon ml3 aksrt-identity-medal is-wear';
|
||||
img.src = worn.icon_url;
|
||||
img.alt = worn.label;
|
||||
img.title = worn.label;
|
||||
img.setAttribute('loading', 'lazy');
|
||||
img.setAttribute('decoding', 'async');
|
||||
node.parentNode.replaceChild(img, node);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- data ----- */
|
||||
function load() {
|
||||
if (!userId) return Promise.resolve();
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="uc-badges-loading muted-2-color em09"><i class="fas fa-spinner fa-spin" aria-hidden="true"></i> ' + escapeHtml('加载中…') + '</div>';
|
||||
}
|
||||
return api('/users/' + userId + '/badges').then(function (r) {
|
||||
if (!r.ok || !r.json || !r.json.data) {
|
||||
if (body) {
|
||||
body.innerHTML = '<div class="uc-badges-error">' +
|
||||
'<span class="c-red em09">' + escapeHtml('加载失败') + '</span>' +
|
||||
'<button type="button" class="but but-sm" data-aksrt-badges-retry>' + escapeHtml('重试') + '</button>' +
|
||||
'</div>';
|
||||
}
|
||||
return;
|
||||
}
|
||||
state.data = r.json.data;
|
||||
renderHead();
|
||||
renderGrid();
|
||||
});
|
||||
}
|
||||
|
||||
function wear(slug, btn) {
|
||||
btn.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');
|
||||
btn.disabled = false;
|
||||
return;
|
||||
}
|
||||
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
|
||||
state.data = (r.json && r.json.data) || state.data;
|
||||
renderHead();
|
||||
renderGrid();
|
||||
renderDetail(slug);
|
||||
syncHeaderIcon();
|
||||
}).catch(function () {
|
||||
toast('网络错误', 'error');
|
||||
btn.disabled = false;
|
||||
});
|
||||
}
|
||||
|
||||
/* ----- events ----- */
|
||||
openers.forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
openModal(modal);
|
||||
openModal();
|
||||
showView('grid');
|
||||
load();
|
||||
});
|
||||
});
|
||||
@@ -111,15 +280,58 @@
|
||||
modal.querySelectorAll('[data-aksrt-badges-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
closeModal(modal);
|
||||
closeModal();
|
||||
});
|
||||
});
|
||||
|
||||
document.addEventListener('keydown', function (e) {
|
||||
if (e.key !== 'Escape' || modal.hidden) return;
|
||||
var detailView = modal.querySelector('[data-aksrt-badges-view="detail"]');
|
||||
// Detail view open: ESC steps back to the grid first.
|
||||
if (detailView && !detailView.hidden) {
|
||||
showView('grid');
|
||||
return;
|
||||
}
|
||||
closeModal();
|
||||
});
|
||||
|
||||
modal.addEventListener('click', function (e) {
|
||||
var back = e.target.closest('[data-aksrt-badges-back]');
|
||||
if (back) {
|
||||
e.preventDefault();
|
||||
showView('grid');
|
||||
return;
|
||||
}
|
||||
var retry = e.target.closest('[data-aksrt-badges-retry]');
|
||||
if (retry) {
|
||||
e.preventDefault();
|
||||
load();
|
||||
return;
|
||||
}
|
||||
var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
|
||||
if (!wearBtn || !canWear) return;
|
||||
if (wearBtn && canWear) {
|
||||
e.preventDefault();
|
||||
wear(wearBtn.getAttribute('data-aksrt-badge-wear') || '', wearBtn);
|
||||
return;
|
||||
}
|
||||
var card = e.target.closest('[data-aksrt-badge-detail]');
|
||||
if (card) {
|
||||
e.preventDefault();
|
||||
renderDetail(card.getAttribute('data-aksrt-badge-detail') || '');
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
/* ---------- remote modal: global wear handler ---------- */
|
||||
document.addEventListener('click', function (e) {
|
||||
var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
|
||||
if (!wearBtn) return;
|
||||
// UC modal has its own handler when canWear on modal root.
|
||||
var modalRoot = wearBtn.closest('[data-aksrt-badges-modal]');
|
||||
if (modalRoot) return;
|
||||
e.preventDefault();
|
||||
var slug = wearBtn.getAttribute('data-aksrt-badge-wear') || '';
|
||||
if (!slug) return;
|
||||
wearBtn.disabled = true;
|
||||
api('/users/me/badges/wear', {
|
||||
method: 'POST',
|
||||
@@ -131,45 +343,32 @@
|
||||
return;
|
||||
}
|
||||
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
|
||||
renderList(modal, (r.json && r.json.data) || {}, canWear);
|
||||
// Re-open the refreshed single-badge modal so state stays in sync.
|
||||
var refreshed = null;
|
||||
try {
|
||||
var uid = parseInt((r.json && r.json.data && r.json.data.user_id) || '0', 10);
|
||||
var data = r.json && r.json.data;
|
||||
var isSelf = data && data.is_self;
|
||||
if (isSelf && data.wear) {
|
||||
refreshed = (window.aksrtData && aksrtData.ajaxUrl)
|
||||
? aksrtData.ajaxUrl + '?action=aksrt_single_badge_info_modal&name=' + encodeURIComponent(data.wear) + '&user_id=' + (uid || 0)
|
||||
: null;
|
||||
}
|
||||
} catch (_) {}
|
||||
try {
|
||||
if (refreshed && window.AksrtRemoteModal && typeof window.AksrtRemoteModal.openRemote === 'function') {
|
||||
window.AksrtRemoteModal.openRemote({ url: refreshed, className: 'modal-mini', mobileBottom: true });
|
||||
} else {
|
||||
document.dispatchEvent(new CustomEvent('aksrt:remote-modal:close'));
|
||||
}
|
||||
} catch (_) {
|
||||
try { document.dispatchEvent(new CustomEvent('aksrt:remote-modal:close')); } catch (__) {}
|
||||
}
|
||||
}).catch(function () {
|
||||
toast('网络错误', 'error');
|
||||
wearBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
// Global wear handler (remote modal + UC modal).
|
||||
document.addEventListener("click", function (e) {
|
||||
var wearBtn = e.target.closest("[data-aksrt-badge-wear]");
|
||||
if (!wearBtn) return;
|
||||
// UC modal has its own handler when canWear on modal root.
|
||||
var modalRoot = wearBtn.closest("[data-aksrt-badges-modal]");
|
||||
if (modalRoot) return;
|
||||
e.preventDefault();
|
||||
var slug = wearBtn.getAttribute("data-aksrt-badge-wear") || "";
|
||||
if (!slug) return;
|
||||
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");
|
||||
// Soft refresh modal content if remote host present.
|
||||
try {
|
||||
document.dispatchEvent(new CustomEvent("aksrt:remote-modal:close"));
|
||||
} catch (_) {}
|
||||
}).catch(function () {
|
||||
toast("网络错误", "error");
|
||||
wearBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
|
||||
@@ -108,13 +108,25 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
||||
</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 uc-badges-modal" id="aksrt-badges-modal" hidden data-aksrt-badges-modal data-user-id="<?php echo (int) $user_id; ?>" data-can-wear="1" data-medal-bg="<?php echo esc_url(get_template_directory_uri() . '/assets/img/medal/medal-background.svg'); ?>">
|
||||
<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'); ?>">
|
||||
<div class="uc-modal__dialog uc-badges-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 class="uc-badges-view" data-aksrt-badges-view="grid">
|
||||
<div class="uc-badges-head">
|
||||
<span class="uc-badges-head__icon" aria-hidden="true"><i class="fas fa-award"></i></span>
|
||||
<div class="uc-badges-head__title">
|
||||
<?php esc_html_e('我的徽章', 'aksrt'); ?>
|
||||
<span class="uc-badges-head__count" data-aksrt-badges-count></span>
|
||||
</div>
|
||||
<div class="uc-badges-head__wear" data-aksrt-badges-wear-slot></div>
|
||||
</div>
|
||||
<div class="uc-badges-body mini-scrollbar" data-aksrt-badges-body>
|
||||
<div class="uc-badges-loading muted-2-color em09"><i class="fas fa-spinner fa-spin" aria-hidden="true"></i> <?php esc_html_e('加载中…', 'aksrt'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
<div class="uc-badges-view uc-badges-view--detail" data-aksrt-badges-view="detail" hidden>
|
||||
<div class="uc-badges-detail" data-aksrt-badges-detail></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -911,6 +911,7 @@ $hmb_js = get_template_directory() . '/assets/js/header-newadd.js';
|
||||
|| (function_exists('aksrt_is_user_home_page') && aksrt_is_user_home_page())
|
||||
|| (function_exists('aksrt_is_user_center_page') && aksrt_is_user_center_page());
|
||||
if ($is_badge_surface) {
|
||||
$enqueue_module('medal');
|
||||
$badges_js_path_global = get_template_directory() . '/assets/js/user-badges.js';
|
||||
if (file_exists($badges_js_path_global) && !wp_script_is('aksrt-user-badges', 'enqueued')) {
|
||||
wp_enqueue_script(
|
||||
|
||||
@@ -145,7 +145,7 @@ class UserRuntime {
|
||||
* verify:array{enabled:bool,public_display:bool,allow_apply:bool,disable_desc:string,desc:string,types:array<string,array{slug:string,label:string,badge_text:string,fee:float,free_applications:int,require_real_name:bool}>},
|
||||
* level:array{enabled:bool,public_display:bool,source:string,label_prefix:string,thresholds:array<int,int>},
|
||||
* medal:array{enabled:bool,public_display:bool,limit:int,categories:array<int,array{cat_name:string,items:array<int,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int}>}>,items_by_slug:array<string,array{slug:string,name:string,desc:string,icon:string,get_type:string,get_val:int,cat_name:string}>},
|
||||
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,icon_url?:string}>},
|
||||
* badge:array{enabled:bool,public_display:bool,limit:int,dictionary:array<int,array{slug:string,label:string,tone:string,desc:string,icon_url?:string}>},
|
||||
* report:array{enabled:bool,public_entry:bool,public_notice:bool,ban_notice_text:string,categories:array<int,array{slug:string,label:string}>}
|
||||
* }
|
||||
*/
|
||||
@@ -204,6 +204,7 @@ class UserRuntime {
|
||||
'slug' => $item['slug'],
|
||||
'label' => $item['name'],
|
||||
'tone' => 'info',
|
||||
'desc' => (string) ($item['desc'] ?? ''),
|
||||
'icon_url' => $item['icon'],
|
||||
];
|
||||
}, $medal['items_by_slug'])),
|
||||
|
||||
+226
-94
@@ -9,6 +9,10 @@ defined('ABSPATH') || exit;
|
||||
|
||||
class Badges {
|
||||
|
||||
/**
|
||||
* Owned badges storage (3.4.0-dev rewrite): user meta holds an
|
||||
* associative map `slug => grant time (Y-m-d H:i:s)`, newest first.
|
||||
*/
|
||||
public const META_OWNED = 'aksrt_user_badges';
|
||||
public const META_WEAR = 'aksrt_wear_badge';
|
||||
|
||||
@@ -95,106 +99,173 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* zibll zib_get_user_medal_info_modal lite.
|
||||
* Badge list modal HTML (zibll zib_get_user_medal_info_modal parity).
|
||||
*
|
||||
* Head (avatar + owner + worn medal) + category-grouped medal card grid.
|
||||
* Each card deep-links into the single badge modal via the remote-modal
|
||||
* contract (data-toggle="AksrtRemoteModal" + data-remote).
|
||||
*/
|
||||
public static function render_info_modal_html(int $user_id): string {
|
||||
$user = get_userdata($user_id);
|
||||
if (!$user) {
|
||||
return '<div class="aksrt-badge-modal"><p>' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
||||
return '<div class="medal-modal"><p class="medal-modal__empty">' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$current = get_current_user_id();
|
||||
$is_self = $current > 0 && $current === $user_id;
|
||||
$display = $is_self ? __('我', 'aksrt') : $user->display_name;
|
||||
$avatar = function_exists('aksrt_get_avatar_url')
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 64])
|
||||
: get_avatar_url($user_id, ['size' => 64]);
|
||||
? aksrt_get_avatar_url($user_id, ['size' => 80])
|
||||
: get_avatar_url($user_id, ['size' => 80]);
|
||||
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
|
||||
$items = is_array($data['items'] ?? null) ? $data['items'] : [];
|
||||
$categories = is_array($data['categories'] ?? null) ? $data['categories'] : [];
|
||||
$wear = (string) ($data['wear'] ?? '');
|
||||
|
||||
$html = '<div class="aksrt-badge-modal">';
|
||||
$html .= '<div class="aksrt-badge-modal__head">';
|
||||
$html .= '<img class="aksrt-badge-modal__avatar" src="' . esc_url($avatar) . '" alt="" width="40" height="40" />';
|
||||
$html .= '<div class="aksrt-badge-modal__title">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display));
|
||||
if ($wear !== '') {
|
||||
$map = self::get_dictionary_map();
|
||||
if (isset($map[$wear])) {
|
||||
$html .= ' <span class="aksrt-badge-modal__wear-tag">' . esc_html(sprintf(__('佩戴:%s', 'aksrt'), (string) $map[$wear]['label'])) . '</span>';
|
||||
|
||||
// Head slot: worn medal icon with tooltip, or an empty-state tag (zibll).
|
||||
$wear_slot = '';
|
||||
if ($wear !== '' && isset($map[$wear])) {
|
||||
$wear_label = (string) ($map[$wear]['label'] ?? $wear);
|
||||
$wear_icon = (string) ($map[$wear]['icon_url'] ?? '');
|
||||
if ($wear_icon !== '') {
|
||||
$wear_slot = '<img class="medal-icon img-icon ml3" src="' . esc_url($wear_icon) . '" title="' . esc_attr(sprintf(__('当前佩戴:%s', 'aksrt'), $wear_label)) . '" alt="' . esc_attr($wear_label) . '" />';
|
||||
}
|
||||
} else {
|
||||
$html .= ' <span class="badg badg-sm c-red">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
} elseif ((int) ($data['count'] ?? 0) === 0) {
|
||||
$wear_slot = '<span class="badg badg-sm c-red ml6">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html = '<div class="medal-modal">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-modal__head">';
|
||||
$html .= '<img class="medal-modal__avatar" src="' . esc_url((string) $avatar) . '" alt="" width="42" height="42" />';
|
||||
$html .= '<div class="medal-modal__title"><span class="em09">' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display)) . '</span>' . $wear_slot . '</div>';
|
||||
$html .= '</div>';
|
||||
|
||||
$html .= '<div class="aksrt-badge-modal__list mini-scrollbar">';
|
||||
if (!$items) {
|
||||
$html .= '<div class="aksrt-badge-modal__empty">' . esc_html__('暂无徽章', 'aksrt') . '</div>';
|
||||
$html .= '<div class="medal-modal__scroll mini-scrollbar">';
|
||||
if (!$categories) {
|
||||
$html .= '<div class="empty-state empty-state--sm">'
|
||||
. '<div class="empty-state__icon"><i class="fas fa-award" aria-hidden="true"></i></div>'
|
||||
. '<div class="empty-state__title">' . esc_html__('暂无徽章', 'aksrt') . '</div>'
|
||||
. '<div class="empty-state__desc">' . esc_html__('积极参与社区互动即可获得徽章', 'aksrt') . '</div>'
|
||||
. '</div>';
|
||||
} else {
|
||||
foreach ($items as $item) {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$is_wear = !empty($item['wear']);
|
||||
$html .= '<div class="aksrt-badge-modal__item' . ($is_wear ? ' is-wear' : '') . '" data-slug="' . esc_attr($slug) . '">';
|
||||
if ($icon !== '') {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="" width="40" height="40" loading="lazy" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__meta"><div class="aksrt-badge-modal__name">' . esc_html($label) . '</div>';
|
||||
if ($is_wear) {
|
||||
$html .= '<div class="em09 c-blue">' . esc_html__('当前佩戴', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
if ($is_self && !$is_wear) {
|
||||
$html .= '<button type="button" class="but jb-blue but-sm" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
}
|
||||
foreach ($categories as $cat) {
|
||||
$html .= '<div class="medal-cat-box">';
|
||||
$html .= '<div class="medal-cat-head"><b class="medal-cat-name">' . esc_html((string) ($cat['cat_name'] ?? '')) . '</b></div>';
|
||||
$html .= '<div class="medal-list-box">';
|
||||
foreach ((array) ($cat['items'] ?? []) as $item) {
|
||||
$html .= self::render_card_link($user_id, is_array($item) ? $item : []);
|
||||
}
|
||||
$html .= '</div></div>';
|
||||
}
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
// Footer: quick jump to my own badges while browsing someone else (zibll).
|
||||
if (!$is_self && $current > 0) {
|
||||
$my_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $current], admin_url('admin-ajax.php'));
|
||||
$html .= '<div class="medal-modal__buts">';
|
||||
$html .= '<a class="but c-blue" href="' . esc_url($my_url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($my_url) . '" data-class="full-sm" mobile-bottom="true"><i class="fas fa-medal" aria-hidden="true"></i> ' . esc_html__('查看我的徽章', 'aksrt') . '</a>';
|
||||
$html .= '</div>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge card modal.
|
||||
* Single medal card inside the list grid; deep-links to the detail modal.
|
||||
*
|
||||
* @param array{slug?:string,label?:string,icon_url?:string,owned?:bool,wear?:bool} $item
|
||||
*/
|
||||
protected static function render_card_link(int $user_id, array $item): string {
|
||||
$slug = (string) ($item['slug'] ?? '');
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$owned = !empty($item['owned']);
|
||||
$wear = !empty($item['wear']);
|
||||
if ($slug === '') {
|
||||
return '';
|
||||
}
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
$card = '<div class="medal-card' . ($owned ? ' is-has' : ' no-has') . ($wear ? ' is-wear' : '') . '">';
|
||||
$card .= $icon_html;
|
||||
$card .= '<div class="medal-card__name">' . esc_html($label) . '</div>';
|
||||
if ($wear) {
|
||||
$card .= '<div class="medal-card__wear">' . esc_html__('佩戴中', 'aksrt') . '</div>';
|
||||
}
|
||||
$card .= '</div>';
|
||||
|
||||
$url = add_query_arg(
|
||||
['action' => 'aksrt_single_badge_info_modal', 'name' => $slug, 'user_id' => $user_id],
|
||||
admin_url('admin-ajax.php')
|
||||
);
|
||||
|
||||
return '<a class="medal-list" href="' . esc_url($url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($url) . '" data-class="modal-mini" mobile-bottom="true">' . $card . '</a>';
|
||||
}
|
||||
|
||||
/**
|
||||
* Single badge detail modal (zibll zib_get_single_medal_info_modal parity).
|
||||
*
|
||||
* medal-background.svg backdrop + large icon + name + desc + obtain time,
|
||||
* with wear action for the owner and a back link into the list modal.
|
||||
*/
|
||||
public static function render_single_modal_html(string $slug, int $user_id = 0): string {
|
||||
$map = self::get_dictionary_map();
|
||||
$slug = sanitize_key($slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return '<div class="aksrt-badge-modal"><p>' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
return '<div class="medal-modal medal-modal--single"><p class="medal-modal__empty">' . esc_html__('徽章不存在', 'aksrt') . '</p></div>';
|
||||
}
|
||||
$item = $map[$slug];
|
||||
$current = get_current_user_id();
|
||||
$is_self = $user_id > 0 && $current === $user_id;
|
||||
$owned = $user_id > 0 ? self::get_owned($user_id) : [];
|
||||
$has = in_array($slug, $owned, true);
|
||||
$wear = $user_id > 0 ? self::get_wear($user_id) : '';
|
||||
$is_wear = $has && $wear === $slug;
|
||||
$owned_map = $user_id > 0 ? self::get_owned_map($user_id) : [];
|
||||
$has = isset($owned_map[$slug]);
|
||||
$is_wear = $has && self::get_wear($user_id) === $slug;
|
||||
|
||||
$html = '<div class="aksrt-badge-modal aksrt-badge-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="aksrt-badge-modal__card">';
|
||||
if (!empty($item['icon_url'])) {
|
||||
$html .= '<img class="medal-icon img-icon" src="' . esc_url((string) $item['icon_url']) . '" alt="" width="72" height="72" />';
|
||||
} else {
|
||||
$html .= '<span class="aksrt-badge-modal__icon-fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
}
|
||||
$html .= '<div class="aksrt-badge-modal__name">' . esc_html((string) $item['label']) . '</div>';
|
||||
$label = (string) ($item['label'] ?? $slug);
|
||||
$desc = (string) ($item['desc'] ?? '');
|
||||
$icon = (string) ($item['icon_url'] ?? '');
|
||||
$bg = get_template_directory_uri() . '/assets/img/medal/medal-background.svg';
|
||||
|
||||
$icon_html = $icon !== ''
|
||||
? '<img class="medal-icon img-icon" src="' . esc_url($icon) . '" alt="' . esc_attr($label) . '" loading="lazy" decoding="async" />'
|
||||
: '<span class="medal-icon medal-icon--fa lg"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
||||
|
||||
// Obtain state line (self only, zibll).
|
||||
$state = '';
|
||||
if ($is_self) {
|
||||
$html .= $has
|
||||
? '<div class="c-blue em09 mt6">' . esc_html__('已获得', 'aksrt') . '</div>'
|
||||
: '<div class="c-yellow em09 mt6">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
$state = $has
|
||||
? '<div class="medal-single-state c-blue">' . esc_html(sprintf(__('%s 获得', 'aksrt'), (string) $owned_map[$slug])) . '</div>'
|
||||
: '<div class="medal-single-state c-yellow">' . esc_html__('暂未获得', 'aksrt') . '</div>';
|
||||
}
|
||||
$html .= '</div>';
|
||||
|
||||
$html = '<div class="medal-modal medal-modal--single">';
|
||||
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">×</button>';
|
||||
$html .= '<div class="medal-single-card">';
|
||||
$html .= '<div class="medal-single-bg"><img class="fit-cover" src="' . esc_url($bg) . '" alt="" /></div>';
|
||||
$html .= '<div class="medal-single-main">' . $icon_html;
|
||||
$html .= '<div class="medal-single-name">' . esc_html($label) . '</div>';
|
||||
if ($desc !== '') {
|
||||
$html .= '<div class="medal-single-desc muted-2-color">' . esc_html($desc) . '</div>';
|
||||
}
|
||||
$html .= $state . '</div></div>';
|
||||
|
||||
if ($is_self && $has && !$is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><button type="button" class="but jb-blue btn-block" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
$html .= '<div class="medal-modal__actions"><button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' . esc_attr($slug) . '">' . esc_html__('佩戴此徽章', 'aksrt') . '</button></div>';
|
||||
} elseif ($is_self && $is_wear) {
|
||||
$html .= '<div class="aksrt-badge-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
$html .= '<div class="medal-modal__actions"><span class="badg btn-block c-blue">' . esc_html__('当前佩戴此徽章', 'aksrt') . '</span></div>';
|
||||
}
|
||||
|
||||
// Back to the full badge list modal.
|
||||
if ($user_id > 0) {
|
||||
$back_url = add_query_arg(['action' => 'aksrt_user_badge_info_modal', 'id' => $user_id], admin_url('admin-ajax.php'));
|
||||
$html .= '<a class="medal-modal__back muted-2-color em09" href="' . esc_url($back_url) . '" data-toggle="AksrtRemoteModal" data-remote="' . esc_url($back_url) . '" data-class="full-sm" mobile-bottom="true"><i class="fas fa-angle-left" aria-hidden="true"></i> ' . esc_html__('返回徽章列表', 'aksrt') . '</a>';
|
||||
}
|
||||
|
||||
$html .= '</div>';
|
||||
return $html;
|
||||
}
|
||||
@@ -300,7 +371,7 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
* @return list<array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary(): array {
|
||||
if (!class_exists('\AKSRT\Settings\Settings')) {
|
||||
@@ -318,6 +389,7 @@ class Badges {
|
||||
'slug' => $slug,
|
||||
'label' => (string) ($item['label'] ?? $slug),
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'desc' => (string) ($item['desc'] ?? ''),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
@@ -325,7 +397,7 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
* @return array<string,array{slug:string,label:string,tone:string,desc:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary_map(): array {
|
||||
$map = [];
|
||||
@@ -336,29 +408,43 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
* Owned badge map: slug => grant time (`Y-m-d H:i:s`), newest first.
|
||||
*
|
||||
* @return array<string,string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
public static function get_owned_map(int $user_id): array {
|
||||
if ($user_id <= 0) {
|
||||
return [];
|
||||
}
|
||||
$raw = get_user_meta($user_id, self::META_OWNED, true);
|
||||
if (!is_array($raw)) {
|
||||
$raw = preg_split('/[\s,]+/', (string) $raw, -1, PREG_SPLIT_NO_EMPTY);
|
||||
return [];
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ((array) $raw as $slug) {
|
||||
foreach ($raw as $slug => $time) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $slug;
|
||||
$out[$slug] = is_string($time) ? $time : '';
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* Owned badge slugs, newest first.
|
||||
*
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(int $user_id): array {
|
||||
return array_keys(self::get_owned_map($user_id));
|
||||
}
|
||||
|
||||
/**
|
||||
* Replace the owned set; existing slugs keep their grant time, new slugs
|
||||
* are stamped with the current time. Order follows the input list.
|
||||
*
|
||||
* @param list<string> $slugs
|
||||
*/
|
||||
public static function set_owned(int $user_id, array $slugs): void {
|
||||
@@ -366,18 +452,20 @@ class Badges {
|
||||
return;
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$existing = self::get_owned_map($user_id);
|
||||
$now = current_time('Y-m-d H:i:s');
|
||||
$clean = [];
|
||||
foreach ($slugs as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug !== '' && isset($map[$slug])) {
|
||||
$clean[] = $slug;
|
||||
if ($slug === '' || !isset($map[$slug]) || isset($clean[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$clean[$slug] = $existing[$slug] ?? $now;
|
||||
}
|
||||
$clean = array_values(array_unique($clean));
|
||||
update_user_meta($user_id, self::META_OWNED, $clean);
|
||||
|
||||
$wear = self::get_wear($user_id);
|
||||
if ($wear !== '' && !in_array($wear, $clean, true)) {
|
||||
if ($wear !== '' && !isset($clean[$wear])) {
|
||||
delete_user_meta($user_id, self::META_WEAR);
|
||||
}
|
||||
}
|
||||
@@ -441,8 +529,8 @@ class Badges {
|
||||
if (in_array($slug, $owned, true)) {
|
||||
return true;
|
||||
}
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
// Newest first (zibll medal ordering); grant time stamped by set_owned.
|
||||
self::set_owned($user_id, array_merge([$slug], $owned));
|
||||
/**
|
||||
* Fires after a badge is granted.
|
||||
*
|
||||
@@ -491,54 +579,98 @@ class Badges {
|
||||
}
|
||||
|
||||
/**
|
||||
* Public DTO for REST / modals.
|
||||
* Public DTO for REST / modals: catalog grouped by category with per-item
|
||||
* ownership state. Unearned badges are only exposed to the owner (or to
|
||||
* managers via $include_private), matching zibll visibility rules.
|
||||
*
|
||||
* @return array{enabled:bool,public_display:bool,wear:string,items:list<array>}
|
||||
* @return array{enabled:bool,public_display:bool,is_self:bool,can_wear:bool,user_id:int,wear:string,count:int,show_unearned:bool,categories:list<array{cat_name:string,items:list<array{slug:string,label:string,desc:string,icon_url:string,owned:bool,wear:bool,time:string}>}>}
|
||||
*/
|
||||
public static function list_for_user(int $user_id, bool $include_private = false): array {
|
||||
$enabled = self::enabled();
|
||||
$public = self::public_display();
|
||||
$viewer = get_current_user_id();
|
||||
$is_self = $viewer > 0 && $viewer === $user_id;
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| ($viewer > 0 && $viewer === $user_id)
|
||||
|| $is_self
|
||||
|| self::can_manually_set($viewer);
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
'can_wear' => $is_self,
|
||||
'user_id' => $user_id,
|
||||
'wear' => '',
|
||||
'items' => [],
|
||||
'count' => 0,
|
||||
'show_unearned' => false,
|
||||
'categories' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$map = self::get_dictionary_map();
|
||||
$owned = self::get_owned($user_id);
|
||||
$settings = \AKSRT\Settings\Settings::get_user_identity_settings();
|
||||
$medal = is_array($settings['medal'] ?? null) ? $settings['medal'] : [];
|
||||
$catalog = is_array($medal['categories'] ?? null) ? $medal['categories'] : [];
|
||||
|
||||
$owned_map = self::get_owned_map($user_id);
|
||||
$wear = self::get_wear($user_id);
|
||||
// Prefer worn first (Zibll).
|
||||
if ($wear !== '') {
|
||||
$owned = array_values(array_unique(array_merge([$wear], $owned)));
|
||||
}
|
||||
$items = [];
|
||||
foreach ($owned as $slug) {
|
||||
if (!isset($map[$slug])) {
|
||||
$show_unearned = $include_private || $is_self || self::can_manually_set($viewer);
|
||||
|
||||
$categories = [];
|
||||
foreach ($catalog as $cat) {
|
||||
$cat_name = (string) ($cat['cat_name'] ?? '');
|
||||
$rows = is_array($cat['items'] ?? null) ? $cat['items'] : [];
|
||||
$has_rows = [];
|
||||
$no_rows = [];
|
||||
foreach ($rows as $row) {
|
||||
if (!is_array($row)) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
$slug = sanitize_key((string) ($row['slug'] ?? ''));
|
||||
if ($slug === '') {
|
||||
continue;
|
||||
}
|
||||
$has = isset($owned_map[$slug]);
|
||||
if (!$has && !$show_unearned) {
|
||||
continue;
|
||||
}
|
||||
$item = [
|
||||
'slug' => $slug,
|
||||
'label' => $map[$slug]['label'],
|
||||
'tone' => $map[$slug]['tone'],
|
||||
'icon_url' => $map[$slug]['icon_url'],
|
||||
'wear' => ($slug === $wear),
|
||||
'label' => (string) ($row['name'] ?? $slug),
|
||||
'desc' => (string) ($row['desc'] ?? ''),
|
||||
'icon_url' => (string) ($row['icon'] ?? ''),
|
||||
'owned' => $has,
|
||||
'wear' => $has && $slug === $wear,
|
||||
'time' => $has ? (string) $owned_map[$slug] : '',
|
||||
];
|
||||
// Owned first within each category (zibll is-has / no-has order).
|
||||
if ($has) {
|
||||
$has_rows[] = $item;
|
||||
} else {
|
||||
$no_rows[] = $item;
|
||||
}
|
||||
}
|
||||
$items = array_merge($has_rows, $no_rows);
|
||||
if (!$items) {
|
||||
continue;
|
||||
}
|
||||
$categories[] = [
|
||||
'cat_name' => $cat_name,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'is_self' => $is_self,
|
||||
'can_wear' => $is_self,
|
||||
'user_id' => $user_id,
|
||||
'wear' => $wear,
|
||||
'items' => $items,
|
||||
'count' => count($owned_map),
|
||||
'show_unearned' => $show_unearned,
|
||||
'categories' => $categories,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
+14
-5
@@ -166,8 +166,14 @@ class Home {
|
||||
if (class_exists('AKSRT\\User\\Badges')) {
|
||||
$list = Badges::list_for_user($user_id, true);
|
||||
$badge_count = 0;
|
||||
foreach ((array) ($list['items'] ?? []) as $item) {
|
||||
$done = false;
|
||||
foreach ((array) ($list['categories'] ?? []) as $cat) {
|
||||
foreach ((array) ($cat['items'] ?? []) as $item) {
|
||||
if (empty($item['owned'])) {
|
||||
continue;
|
||||
}
|
||||
if ($badge_count >= $limit) {
|
||||
$done = true;
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
@@ -175,12 +181,16 @@ class Home {
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'tone' => 'info',
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
if ($done) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$badge_map = [];
|
||||
foreach ((array) ($settings['badge']['dictionary'] ?? []) as $item) {
|
||||
@@ -189,9 +199,8 @@ class Home {
|
||||
}
|
||||
}
|
||||
$assigned = get_user_meta($user_id, 'aksrt_user_badges', true);
|
||||
if (!is_array($assigned)) {
|
||||
$assigned = preg_split('/[\s,]+/', (string) $assigned, -1, PREG_SPLIT_NO_EMPTY);
|
||||
}
|
||||
// Storage shape: associative map slug => grant time.
|
||||
$assigned = is_array($assigned) ? array_keys($assigned) : [];
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
|
||||
$assigned = array_values(array_filter(array_map(static function ($slug) {
|
||||
return sanitize_key((string) $slug);
|
||||
|
||||
Reference in New Issue
Block a user