292 lines
12 KiB
JavaScript
292 lines
12 KiB
JavaScript
/**
|
|
* AKSRT user badges — canonical JSON/runtime path.
|
|
*
|
|
* User-center and public-author triggers share one privacy-aware REST DTO,
|
|
* short user-scoped runtime cache, and one in-modal list/detail renderer.
|
|
*
|
|
* @package AKSRT
|
|
*/
|
|
(function (window, document) {
|
|
'use strict';
|
|
|
|
var activeState = null;
|
|
|
|
function escapeHtml(value) {
|
|
return String(value == null ? '' : value)
|
|
.replace(/&/g, '&')
|
|
.replace(/</g, '<')
|
|
.replace(/>/g, '>')
|
|
.replace(/"/g, '"')
|
|
.replace(/'/g, ''');
|
|
}
|
|
|
|
function api(path, options) {
|
|
return window.AksrtRequest.request(path, options || {}).then(function (response) {
|
|
return {
|
|
ok: response.ok,
|
|
status: response.status,
|
|
json: response.json || {},
|
|
};
|
|
}).catch(function (error) {
|
|
var response = error && error.response ? error.response : {};
|
|
return {
|
|
ok: false,
|
|
status: error && error.status ? error.status : (response.status || 0),
|
|
json: error && error.json ? error.json : (response.json || { message: error && error.message }),
|
|
};
|
|
});
|
|
}
|
|
|
|
function iconHtml(item, className) {
|
|
var icon = item && item.icon_url ? String(item.icon_url) : '';
|
|
var label = item && item.label ? String(item.label) : '';
|
|
if (icon) {
|
|
return '<img' + window.aksrtGetLazyImageAttr('lazy_other', icon, 'medal-icon img-icon' + (className ? ' ' + className : '')) + ' alt="' + escapeHtml(label) + '" title="' + escapeHtml(label) + '" decoding="async">';
|
|
}
|
|
return '<span class="medal-icon medal-icon--fa' + (className ? ' ' + className : '') + '"><i class="fas fa-award" aria-hidden="true"></i></span>';
|
|
}
|
|
|
|
function emptyHtml() {
|
|
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">暂无徽章</div>' +
|
|
'<div class="empty-state__desc">积极参与社区互动即可获得徽章</div>' +
|
|
'</div>';
|
|
}
|
|
|
|
function findItem(state, slug) {
|
|
var categories = state.data && Array.isArray(state.data.categories) ? state.data.categories : [];
|
|
for (var i = 0; i < categories.length; i += 1) {
|
|
var items = Array.isArray(categories[i].items) ? categories[i].items : [];
|
|
for (var j = 0; j < items.length; j += 1) {
|
|
if (String(items[j].slug || '') === String(slug || '')) return items[j];
|
|
}
|
|
}
|
|
return null;
|
|
}
|
|
|
|
function wornItem(state) {
|
|
var wear = state.data && state.data.wear ? state.data.wear : '';
|
|
return wear ? findItem(state, wear) : null;
|
|
}
|
|
|
|
function cardHtml(item) {
|
|
var classes = 'medal-card' + (item.owned ? ' is-has' : ' no-has') + (item.wear ? ' is-wear' : '');
|
|
return '<div class="' + classes + '">' +
|
|
iconHtml(item) +
|
|
'<div class="medal-card__name">' + escapeHtml(item.label) + '</div>' +
|
|
(item.wear ? '<div class="medal-card__wear">佩戴中</div>' : '') +
|
|
'</div>';
|
|
}
|
|
|
|
function listHtml(state) {
|
|
var data = state.data || {};
|
|
var categories = Array.isArray(data.categories) ? data.categories : [];
|
|
var worn = wornItem(state);
|
|
var title = data.is_self ? '我的徽章' : '徽章明细';
|
|
var wearHtml = 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>'
|
|
: '';
|
|
var html = '<div class="medal-modal">' +
|
|
'<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">' + escapeHtml(title) + '<span class="uc-badges-head__count">' + (Number(data.count || 0) > 0 ? '共 ' + Number(data.count) + ' 枚' : '') + '</span></div>' +
|
|
'<div class="uc-badges-head__wear">' + wearHtml + '</div>' +
|
|
'</div>';
|
|
|
|
if (!categories.length) return html + '<div class="medal-modal__scroll mini-scrollbar">' + emptyHtml() + '</div></div>';
|
|
|
|
html += '<div class="medal-modal__scroll mini-scrollbar">';
|
|
categories.forEach(function (category) {
|
|
var items = Array.isArray(category.items) ? category.items : [];
|
|
var cards = 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('');
|
|
html += '<div class="medal-cat-box"><div class="medal-cat-head"><b class="medal-cat-name">' + escapeHtml(category.cat_name || '') + '</b></div><div class="medal-list-box">' + cards + '</div></div>';
|
|
});
|
|
return html + '</div></div>';
|
|
}
|
|
|
|
function detailHtml(state) {
|
|
var item = findItem(state, state.detailSlug);
|
|
if (!item) return listHtml(state);
|
|
|
|
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">暂未获得</div>';
|
|
}
|
|
|
|
var action = '';
|
|
if (state.canWear && item.owned && !item.wear) {
|
|
action = '<button type="button" class="but jb-blue btn-block radius" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">佩戴此徽章</button>';
|
|
} else if (item.wear) {
|
|
action = '<span class="badg btn-block c-blue">当前佩戴此徽章</span>';
|
|
}
|
|
|
|
return '<div class="medal-modal medal-modal--single">' +
|
|
'<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> 返回列表</button></div>' +
|
|
'<div class="medal-single-card"><div class="medal-single-bg"><img' + window.aksrtGetLazyImageAttr('lazy_other', (window.aksrtData && window.aksrtData.themeUri ? window.aksrtData.themeUri : '') + '/assets/img/medal/medal-background.svg', 'fit-cover') + ' 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>' : '') +
|
|
'</div>';
|
|
}
|
|
|
|
function renderState(state) {
|
|
return state.detailSlug ? detailHtml(state) : listHtml(state);
|
|
}
|
|
|
|
function rehydrate(content) {
|
|
window.AksrtFrontend.rehydrate(content, 'modal');
|
|
}
|
|
|
|
function renderInto(state) {
|
|
if (!state.session || state.session.closed || !state.session.content) return;
|
|
state.session.content.innerHTML = renderState(state);
|
|
rehydrate(state.session.content);
|
|
bindContent(state);
|
|
}
|
|
|
|
function syncHeaderIcon(state) {
|
|
var worn = wornItem(state);
|
|
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;
|
|
}
|
|
});
|
|
}
|
|
|
|
function wear(state, slug, button) {
|
|
if (!state.canWear || state.wearing || !slug) return;
|
|
state.wearing = true;
|
|
state.wearController = window.AbortController ? new window.AbortController() : null;
|
|
if (button) button.disabled = true;
|
|
api('/users/me/badges/wear', {
|
|
method: 'POST',
|
|
body: JSON.stringify({ slug: slug }),
|
|
signal: state.wearController ? state.wearController.signal : undefined,
|
|
}).then(function (result) {
|
|
if (state.session && state.session.closed) return;
|
|
if (!result.ok) {
|
|
notyf((result.json && (result.json.message || (result.json.data && result.json.data.message))) || '佩戴失败', 'danger');
|
|
if (button) button.disabled = false;
|
|
return;
|
|
}
|
|
if (result.json && result.json.data) state.data = result.json.data;
|
|
state.detailSlug = slug;
|
|
window.AksrtModalRuntime.invalidate(['badges', 'identity', 'badges:user:' + state.userId]);
|
|
notyf((result.json && result.json.message) || '已佩戴徽章', 'success');
|
|
renderInto(state);
|
|
syncHeaderIcon(state);
|
|
}).catch(function (error) {
|
|
if (error && error.name === 'AbortError') return;
|
|
notyf('网络错误', 'danger');
|
|
if (button) button.disabled = false;
|
|
}).finally(function () {
|
|
state.wearing = false;
|
|
state.wearController = null;
|
|
});
|
|
}
|
|
|
|
function bindContent(state) {
|
|
if (!state.session || !state.session.content || state.session.content.dataset.aksrtBadgesBound === '1') return;
|
|
state.session.content.dataset.aksrtBadgesBound = '1';
|
|
state.session.content.addEventListener('click', function (event) {
|
|
var back = event.target.closest('[data-aksrt-badges-back]');
|
|
if (back) {
|
|
event.preventDefault();
|
|
state.detailSlug = '';
|
|
renderInto(state);
|
|
return;
|
|
}
|
|
var detail = event.target.closest('[data-aksrt-badge-detail]');
|
|
if (detail) {
|
|
event.preventDefault();
|
|
state.detailSlug = detail.getAttribute('data-aksrt-badge-detail') || '';
|
|
renderInto(state);
|
|
return;
|
|
}
|
|
var wearButton = event.target.closest('[data-aksrt-badge-wear]');
|
|
if (wearButton) {
|
|
event.preventDefault();
|
|
wear(state, wearButton.getAttribute('data-aksrt-badge-wear') || '', wearButton);
|
|
}
|
|
});
|
|
}
|
|
|
|
function openBadges(trigger) {
|
|
var runtime = window.AksrtModalRuntime;
|
|
if (!runtime.isRolloutEnabled("badge")) return;
|
|
var userId = parseInt(trigger.getAttribute('data-aksrt-badges-user-id') || '0', 10);
|
|
if (!userId) return;
|
|
if (activeState && activeState.wearController) {
|
|
try { activeState.wearController.abort(); } catch (_) {}
|
|
}
|
|
|
|
var state = {
|
|
userId: userId,
|
|
canWear: trigger.getAttribute('data-aksrt-badges-can-wear') === '1',
|
|
data: null,
|
|
detailSlug: '',
|
|
session: null,
|
|
wearing: false,
|
|
wearController: null,
|
|
};
|
|
activeState = state;
|
|
runtime.openRemote({
|
|
url: '/users/' + userId + '/badges',
|
|
kind: 'badges',
|
|
title: '徽章',
|
|
className: 'modal-mini full-sm aksrt-badges-modal-shell',
|
|
mobileBottom: true,
|
|
skeleton: '<div class="medal-modal"><div class="uc-badges-head"><span class="aksrt-modal-runtime__skeleton-line is-short"></span><span class="aksrt-modal-runtime__skeleton-line is-wide"></span></div><div class="medal-list-box aksrt-modal-runtime__skeleton"><span class="aksrt-modal-runtime__skeleton-block"></span><span class="aksrt-modal-runtime__skeleton-block"></span><span class="aksrt-modal-runtime__skeleton-block"></span></div></div>',
|
|
cacheTtl: 12000,
|
|
cacheTags: ['badges', 'identity', 'badges:user:' + userId],
|
|
responseType: 'json',
|
|
renderData: function (data, session) {
|
|
state.session = session;
|
|
state.data = data || {};
|
|
state.canWear = state.canWear && state.data.can_wear === true;
|
|
session.content.innerHTML = renderState(state);
|
|
},
|
|
onData: function (data, session) {
|
|
state.session = session;
|
|
state.data = data || state.data;
|
|
state.canWear = state.canWear && state.data.can_wear === true;
|
|
bindContent(state);
|
|
syncHeaderIcon(state);
|
|
},
|
|
});
|
|
}
|
|
|
|
document.addEventListener('aksrt:modal:close', function (event) {
|
|
if (!activeState || !event.detail || event.detail.session !== activeState.session) return;
|
|
if (activeState.wearController) {
|
|
try { activeState.wearController.abort(); } catch (_) {}
|
|
}
|
|
activeState = null;
|
|
});
|
|
|
|
function bindOnce() {
|
|
if (window.__aksrtBadgesBound) return;
|
|
window.__aksrtBadgesBound = true;
|
|
document.addEventListener('click', function (event) {
|
|
var trigger = event.target.closest('[data-aksrt-badges-open]');
|
|
if (!trigger) return;
|
|
event.preventDefault();
|
|
openBadges(trigger);
|
|
});
|
|
}
|
|
|
|
if (document.readyState === 'loading') {
|
|
document.addEventListener('DOMContentLoaded', bindOnce, { once: true });
|
|
} else {
|
|
bindOnce();
|
|
}
|
|
})(window, document);
|