From 9f90d54821296d22d98b0e0cd67f7e0772098878 Mon Sep 17 00:00:00 2001 From: unknown Date: Sun, 19 Jul 2026 02:17:53 +0800 Subject: [PATCH] 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. --- assets/js/user-badges.js | 32 +++ .../user-center-renderers/profile-header.php | 6 + src/AKSRT/Core/ScriptsPart1b.php | 23 +- src/AKSRT/User/Badges.php | 203 ++++++++++++++++++ templates/parts/user/author-hero.php | 10 + 5 files changed, 273 insertions(+), 1 deletion(-) diff --git a/assets/js/user-badges.js b/assets/js/user-badges.js index 421c003d..c508668d 100644 --- a/assets/js/user-badges.js +++ b/assets/js/user-badges.js @@ -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 { diff --git a/inc/helpers/user-center-renderers/profile-header.php b/inc/helpers/user-center-renderers/profile-header.php index 24ad9086..43e1a6dc 100644 --- a/inc/helpers/user-center-renderers/profile-header.php +++ b/inc/helpers/user-center-renderers/profile-header.php @@ -65,6 +65,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_ + diff --git a/src/AKSRT/Core/ScriptsPart1b.php b/src/AKSRT/Core/ScriptsPart1b.php index a326408e..8deaf0cf 100644 --- a/src/AKSRT/Core/ScriptsPart1b.php +++ b/src/AKSRT/Core/ScriptsPart1b.php @@ -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); } } diff --git a/src/AKSRT/User/Badges.php b/src/AKSRT/User/Badges.php index bee2531c..b172641f 100644 --- a/src/AKSRT/User/Badges.php +++ b/src/AKSRT/User/Badges.php @@ -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 '

' . esc_html__('用户不存在', 'aksrt') . '

'; + } + $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 = '
'; + $html .= '
'; + $html .= ''; + $html .= '
' . esc_html(sprintf(__('%s的徽章', 'aksrt'), $display)); + if ($wear !== '') { + $map = self::get_dictionary_map(); + if (isset($map[$wear])) { + $html .= ' ' . esc_html(sprintf(__('佩戴:%s', 'aksrt'), (string) $map[$wear]['label'])) . ''; + } + } else { + $html .= ' ' . esc_html__('暂无徽章', 'aksrt') . ''; + } + $html .= '
'; + $html .= ''; + $html .= '
'; + + $html .= '
'; + if (!$items) { + $html .= '
' . esc_html__('暂无徽章', 'aksrt') . '
'; + } 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 .= '
'; + if ($icon !== '') { + $html .= ''; + } else { + $html .= ''; + } + $html .= '
' . esc_html($label) . '
'; + if ($is_wear) { + $html .= '
' . esc_html__('当前佩戴', 'aksrt') . '
'; + } + $html .= '
'; + if ($is_self && !$is_wear) { + $html .= ''; + } + $html .= '
'; + } + } + $html .= '
'; + 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 '

' . esc_html__('徽章不存在', 'aksrt') . '

'; + } + $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 = '
'; + $html .= ''; + $html .= '
'; + if (!empty($item['icon_url'])) { + $html .= ''; + } else { + $html .= ''; + } + $html .= '
' . esc_html((string) $item['label']) . '
'; + if ($is_self) { + $html .= $has + ? '
' . esc_html__('已获得', 'aksrt') . '
' + : '
' . esc_html__('暂未获得', 'aksrt') . '
'; + } + $html .= '
'; + if ($is_self && $has && !$is_wear) { + $html .= '
'; + } elseif ($is_self && $is_wear) { + $html .= '
' . esc_html__('当前佩戴此徽章', 'aksrt') . '
'; + } + $html .= '
'; + 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 { diff --git a/templates/parts/user/author-hero.php b/templates/parts/user/author-hero.php index 02dee3cf..62248511 100644 --- a/templates/parts/user/author-hero.php +++ b/templates/parts/user/author-hero.php @@ -102,6 +102,16 @@ $pm_url = ($pm_enabled && $messages_url !== '') + ' . $badge_link . ''; // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped + } + } + ?> + description) : ?>

description); ?>