feat(badges): medal info remote modal + grant notification

Add zibll-like badge list/single AJAX modals, Message notify on grant,
author-home/user-center entry points, and front wear handler.
This commit is contained in:
2026-07-19 02:17:53 +08:00
parent 5fa902304b
commit 9f90d54821
5 changed files with 273 additions and 1 deletions
+32
View File
@@ -139,6 +139,38 @@
});
}
// 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);
} else {
@@ -65,6 +65,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
<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
$remote_badge = \AKSRT\User\Badges::get_info_link((int) $user_id, 'btn-ghost btn-sm', __('徽章弹层', 'aksrt'));
if ($remote_badge !== '') {
echo $remote_badge; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
?>
<?php endif; ?>
<a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm">
+22 -1
View File
@@ -968,7 +968,28 @@ trait ScriptsPart1b {
}
$this->enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $version);
// Badges UI: user center + public profile (zibll medal modal/list).
if (class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::enabled()) {
$is_badge_surface = $is_profile_page
|| is_author()
|| (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) {
$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(
'aksrt-user-badges',
$base_uri . '/assets/js/user-badges.js',
['aksrt-script', 'aksrt-core-remote-modal'],
(string) filemtime($badges_js_path_global),
true
);
}
}
}
$this->enqueue_public_fonts_js_and_data($base_uri, $core_dep, $deps, $is_fixed_homepage, $is_home_or_archive, $is_single_post, $is_single_resource, $version);
}
}
+203
View File
@@ -25,6 +25,209 @@ class Badges {
public function init(): void {
add_action('rest_api_init', [$this, 'register_routes']);
add_action('aksrt_badge_granted', [$this, 'on_badge_granted_notify'], 10, 3);
// HTML modal for remote-modal (zibll user_medal_info_modal).
add_action('wp_ajax_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']);
add_action('wp_ajax_nopriv_aksrt_user_badge_info_modal', [$this, 'ajax_info_modal']);
add_action('wp_ajax_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']);
add_action('wp_ajax_nopriv_aksrt_single_badge_info_modal', [$this, 'ajax_single_info_modal']);
}
/**
* Notify user when a badge is granted (zibll user_add_medal message).
*/
public function on_badge_granted_notify(int $user_id, string $slug, int $actor = 0): void {
if ($user_id <= 0 || $slug === '') {
return;
}
$map = self::get_dictionary_map();
if (!isset($map[$slug])) {
return;
}
$label = (string) ($map[$slug]['label'] ?? $slug);
$title = sprintf(
/* translators: %s badge name */
__('恭喜您!获得新的徽章[%s]', 'aksrt'),
$label
);
$content = sprintf(
/* translators: %s badge name */
__('恭喜您获得徽章「%s」', 'aksrt'),
$label
);
if (class_exists('\AKSRT\Messages\Message')) {
\AKSRT\Messages\Message::get_instance()->send($user_id, 'medal', $content, [
'title' => $title,
'target_url' => function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url()
: home_url('/'),
'slug' => $slug,
]);
}
}
/**
* AJAX: full badge list modal HTML.
*/
public function ajax_info_modal(): void {
$user_id = max(0, (int) ($_REQUEST['id'] ?? $_REQUEST['user_id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ($user_id <= 0) {
$user_id = get_current_user_id();
}
if ($user_id <= 0 || !get_userdata($user_id)) {
wp_send_json_error(['message' => __('用户不存在', 'aksrt')], 404);
}
$html = self::render_info_modal_html($user_id);
wp_send_json_success(['html' => $html]);
}
/**
* AJAX: single badge detail modal.
*/
public function ajax_single_info_modal(): void {
$slug = sanitize_key((string) ($_REQUEST['name'] ?? $_REQUEST['slug'] ?? '')); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
$user_id = max(0, (int) ($_REQUEST['user_id'] ?? $_REQUEST['id'] ?? 0)); // phpcs:ignore WordPress.Security.NonceVerification.Recommended
if ($slug === '') {
wp_send_json_error(['message' => __('徽章不存在', 'aksrt')], 404);
}
$html = self::render_single_modal_html($slug, $user_id);
wp_send_json_success(['html' => $html]);
}
/**
* zibll zib_get_user_medal_info_modal lite.
*/
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>';
}
$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]);
$data = self::list_for_user($user_id, $is_self || self::can_manually_set($current));
$items = is_array($data['items'] ?? null) ? $data['items'] : [];
$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>';
}
} else {
$html .= ' <span class="badg badg-sm c-red">' . esc_html__('暂无徽章', 'aksrt') . '</span>';
}
$html .= '</div>';
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button>';
$html .= '</div>';
$html .= '<div class="aksrt-badge-modal__list mini-scrollbar">';
if (!$items) {
$html .= '<div class="aksrt-badge-modal__empty">' . esc_html__('暂无徽章', 'aksrt') . '</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>';
}
}
$html .= '</div></div>';
return $html;
}
/**
* Single badge card 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>';
}
$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;
$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') . '">&times;</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>';
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>';
}
$html .= '</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>';
} 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>';
return $html;
}
/**
* Remote-modal trigger for a user's badge list (zibll zib_get_user_medal_info_link).
*/
public static function get_info_link(int $user_id, string $class = '', ?string $text = null): string {
if ($user_id <= 0 || !self::enabled()) {
return '';
}
if ($text === null) {
$text = __('徽章明细', 'aksrt');
}
if (!function_exists('aksrt_get_remote_modal_link')) {
return '';
}
$url = add_query_arg(
[
'action' => 'aksrt_user_badge_info_modal',
'id' => $user_id,
],
admin_url('admin-ajax.php')
);
return aksrt_get_remote_modal_link([
'tag' => 'a',
'class' => trim('user-medal-info ' . $class),
'text' => $text,
'url' => $url,
'data_class' => 'full-sm',
'height' => 240,
'mobile_bottom' => true,
]);
}
public function register_routes(): void {
+10
View File
@@ -102,6 +102,16 @@ $pm_url = ($pm_enabled && $messages_url !== '')
</h1>
<?php endif; ?>
<?php
// Badge detail remote-modal (zibll user medal info).
if (class_exists('\AKSRT\User\Badges') && \AKSRT\User\Badges::enabled() && \AKSRT\User\Badges::public_display()) {
$badge_link = \AKSRT\User\Badges::get_info_link($user_id, 'but but-sm jb-blue mt6', __('徽章明细', 'aksrt'));
if ($badge_link !== '') {
echo '<div class="aksrt-author-hero__badges">' . $badge_link . '</div>'; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped
}
}
?>
<?php if ($show_desc && $user->description) : ?>
<p class="aksrt-author-hero__desc"><?php echo esc_html($user->description); ?></p>
<?php endif; ?>