Merge pull request 'Worktree zibll align p1p2' (#180) from worktree-zibll-align-p1p2 into main
Reviewed-on: CYL/wordpress-theme-aksrt#180
This commit is contained in:
@@ -2,7 +2,7 @@
|
||||
<div class="aksrt-page-container">
|
||||
<PageHeader
|
||||
title="用户徽章"
|
||||
desc="维护公开徽章字典、展示数量和前台展示位。具体授予逻辑可以后续再接业务,但字段和展示契约已经固定。"
|
||||
desc="维护徽章字典(含可选 icon_url)、展示数量。授予/收回在 WP 用户资料页;用户前台可佩戴徽章。"
|
||||
>
|
||||
<template #actions>
|
||||
<n-space>
|
||||
@@ -65,11 +65,11 @@
|
||||
v-model:value="form.user_badge_dictionary"
|
||||
type="textarea"
|
||||
:autosize="{ minRows: 5, maxRows: 10 }"
|
||||
placeholder="founder|创始成员|warning staff|站务团队|info"
|
||||
placeholder="founder|创始成员|warning staff|站务团队|info|https://example.com/medal.png"
|
||||
:disabled="!form.user_badge_enabled"
|
||||
/>
|
||||
<template #feedback>
|
||||
<span class="aksrt-hint-text">每行一个徽章,格式 `slug|显示文案|色调`,色调支持 `neutral / info / success / warning / brand`。</span>
|
||||
<span class="aksrt-hint-text">每行:`slug|显示文案|色调|图标URL(可选)`。色调:neutral/info/success/warning/brand。授予在用户资料页;前台「我的徽章」可佩戴。</span>
|
||||
</template>
|
||||
</n-form-item>
|
||||
</n-form>
|
||||
@@ -77,7 +77,7 @@
|
||||
|
||||
<n-card class="aksrt-section-card">
|
||||
<n-alert type="info" :show-icon="false">
|
||||
前台从 `aksrt_user_badges` user meta 读取已授予徽章列表,并在公开主页和本人中心头部展示。这里只负责定义合法徽章字典和展示上限。
|
||||
字典定义合法徽章;`aksrt_user_badges` 存已授予 slug;`aksrt_wear_badge` 为当前佩戴。公开页/评论通过 identity chips 展示(有 icon_url 时为勋章图)。
|
||||
</n-alert>
|
||||
</n-card>
|
||||
|
||||
|
||||
@@ -986,3 +986,10 @@ body.uc-modal-open {
|
||||
color: var(--focus-color, #f04494);
|
||||
}
|
||||
.focus-color { color: var(--focus-color, #f59e0b); }
|
||||
|
||||
/* User badges wear modal */
|
||||
.uc-badges-list { max-height: 60vh; overflow: auto; }
|
||||
.uc-badges-item { gap: 12px; }
|
||||
.uc-badges-item .medal-icon { width: 28px; height: 28px; object-fit: contain; }
|
||||
.uc-badges-wear-tag { margin-left: 6px; }
|
||||
.uc-badges-empty { padding: 16px 0; text-align: center; }
|
||||
|
||||
@@ -0,0 +1,147 @@
|
||||
/**
|
||||
* AKSRT user badges — wear modal.
|
||||
*/
|
||||
(function () {
|
||||
'use strict';
|
||||
|
||||
var aksrt = window.aksrtData || {};
|
||||
var siteUrl = aksrt.siteUrl || '';
|
||||
if (window.location.protocol === 'https:' && siteUrl.substring(0, 5) === 'http:') {
|
||||
siteUrl = 'https:' + siteUrl.substring(5);
|
||||
}
|
||||
var REST = (siteUrl || '').replace(/\/$/, '') + '/wp-json/aksrt/v1';
|
||||
var NONCE = aksrt.restNonce || aksrt.nonce || '';
|
||||
|
||||
function api(path, options) {
|
||||
var opts = Object.assign({
|
||||
credentials: 'same-origin',
|
||||
headers: Object.assign({ 'Content-Type': 'application/json', 'X-WP-Nonce': NONCE }, (options && options.headers) || {}),
|
||||
}, options || {});
|
||||
return fetch(REST + path, opts).then(function (r) {
|
||||
return r.json().catch(function () { return {}; }).then(function (j) {
|
||||
return { ok: r.ok, status: r.status, json: j };
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
function toast(msg, type) {
|
||||
if (window.AksrtToast && typeof window.AksrtToast.show === 'function') {
|
||||
window.AksrtToast.show(msg, type || 'info');
|
||||
return;
|
||||
}
|
||||
if (window.aksrtNotify) {
|
||||
window.aksrtNotify(msg, type || 'info');
|
||||
return;
|
||||
}
|
||||
// fallback
|
||||
try { console.log('[AKSRT badges]', msg); } catch (_) {}
|
||||
}
|
||||
|
||||
function escapeHtml(s) {
|
||||
return String(s == null ? '' : s)
|
||||
.replace(/&/g, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
}
|
||||
|
||||
function renderList(root, data, canWear) {
|
||||
var body = root.querySelector('[data-aksrt-badges-body]');
|
||||
if (!body) return;
|
||||
var items = (data && data.items) || [];
|
||||
if (!items.length) {
|
||||
body.innerHTML = '<div class="uc-badges-empty muted-2-color em09">' + escapeHtml('暂无徽章') + '</div>';
|
||||
return;
|
||||
}
|
||||
body.innerHTML = items.map(function (item) {
|
||||
var icon = item.icon_url
|
||||
? '<img class="medal-icon img-icon" src="' + escapeHtml(item.icon_url) + '" alt="">'
|
||||
: '<span class="badg badg-sm aksrt-identity-chip aksrt-identity-chip--' + escapeHtml(item.tone || 'neutral') + '"><i class="fa fa-award"></i> ' + escapeHtml(item.label) + '</span>';
|
||||
var wearBadge = item.wear ? '<span class="uc-badges-wear-tag c-yellow em09">佩戴中</span>' : '';
|
||||
var action = '';
|
||||
if (canWear && !item.wear) {
|
||||
action = '<button type="button" class="but jb-blue but-sm" data-aksrt-badge-wear="' + escapeHtml(item.slug) + '">佩戴此徽章</button>';
|
||||
} else if (item.wear) {
|
||||
action = '<span class="muted-2-color em09">当前佩戴</span>';
|
||||
}
|
||||
return '<div class="uc-badges-item flex jsb ac border-bottom padding-h10" data-slug="' + escapeHtml(item.slug) + '">' +
|
||||
'<div class="flex ac gap8">' + icon + '<div><div class="font-bold">' + escapeHtml(item.label) + '</div>' + wearBadge + '</div></div>' +
|
||||
'<div class="shrink0">' + action + '</div></div>';
|
||||
}).join('');
|
||||
}
|
||||
|
||||
function openModal(root) {
|
||||
root.hidden = false;
|
||||
document.body.classList.add('uc-modal-open');
|
||||
}
|
||||
function closeModal(root) {
|
||||
root.hidden = true;
|
||||
document.body.classList.remove('uc-modal-open');
|
||||
}
|
||||
|
||||
function init() {
|
||||
var openers = document.querySelectorAll('[data-aksrt-badges-open]');
|
||||
var modal = document.querySelector('[data-aksrt-badges-modal]');
|
||||
if (!modal || !openers.length) return;
|
||||
|
||||
var userId = parseInt(modal.getAttribute('data-user-id') || '0', 10);
|
||||
var canWear = modal.getAttribute('data-can-wear') === '1';
|
||||
|
||||
function load() {
|
||||
if (!userId) return Promise.resolve();
|
||||
var body = modal.querySelector('[data-aksrt-badges-body]');
|
||||
if (body) body.innerHTML = '<div class="muted-2-color em09">加载中…</div>';
|
||||
return api('/users/' + userId + '/badges').then(function (r) {
|
||||
if (!r.ok || !r.json || !r.json.data) {
|
||||
if (body) body.innerHTML = '<div class="c-red em09">加载失败</div>';
|
||||
return;
|
||||
}
|
||||
renderList(modal, r.json.data, canWear);
|
||||
});
|
||||
}
|
||||
|
||||
openers.forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
openModal(modal);
|
||||
load();
|
||||
});
|
||||
});
|
||||
|
||||
modal.querySelectorAll('[data-aksrt-badges-close]').forEach(function (btn) {
|
||||
btn.addEventListener('click', function (e) {
|
||||
e.preventDefault();
|
||||
closeModal(modal);
|
||||
});
|
||||
});
|
||||
|
||||
modal.addEventListener('click', function (e) {
|
||||
var wearBtn = e.target.closest('[data-aksrt-badge-wear]');
|
||||
if (!wearBtn || !canWear) return;
|
||||
e.preventDefault();
|
||||
var slug = wearBtn.getAttribute('data-aksrt-badge-wear') || '';
|
||||
wearBtn.disabled = true;
|
||||
api('/users/me/badges/wear', {
|
||||
method: 'POST',
|
||||
body: JSON.stringify({ slug: slug }),
|
||||
}).then(function (r) {
|
||||
if (!r.ok) {
|
||||
toast((r.json && (r.json.message || (r.json.data && r.json.data.message))) || '佩戴失败', 'error');
|
||||
wearBtn.disabled = false;
|
||||
return;
|
||||
}
|
||||
toast((r.json && r.json.message) || '已佩戴徽章', 'success');
|
||||
renderList(modal, (r.json && r.json.data) || {}, canWear);
|
||||
}).catch(function () {
|
||||
toast('网络错误', 'error');
|
||||
wearBtn.disabled = false;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (document.readyState === 'loading') {
|
||||
document.addEventListener('DOMContentLoaded', init);
|
||||
} else {
|
||||
init();
|
||||
}
|
||||
})();
|
||||
@@ -364,6 +364,13 @@ if (class_exists('AKSRT_User_Balance_Admin')) {
|
||||
AKSRT_User_Balance_Admin::get_instance()->init();
|
||||
}
|
||||
|
||||
if (class_exists('AKSRT_User_Badges')) {
|
||||
AKSRT_User_Badges::get_instance()->init();
|
||||
}
|
||||
if (class_exists('AKSRT_User_Badges_Admin')) {
|
||||
AKSRT_User_Badges_Admin::get_instance()->init();
|
||||
}
|
||||
|
||||
// WordPress 原生侧栏:支付/订单运营中心(zibll zibpay 对齐)
|
||||
if (is_admin() && class_exists('AKSRT_Admin_Pay_Menus')) {
|
||||
AKSRT_Admin_Pay_Menus::get_instance()->init();
|
||||
|
||||
@@ -219,6 +219,8 @@ require_once AKSRT_DIR . '/inc/classes/class-aksrt-email-rollback.php';
|
||||
// 原生用户菜单:积分管理
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-credits-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-balance-admin.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges.php';
|
||||
require_once AKSRT_DIR . '/inc/classes/class-aksrt-user-badges-admin.php';
|
||||
|
||||
// 加载数据表迁移器(CMS / 积分 / 下载 token)
|
||||
require_once AKSRT_DIR . '/inc/classes/db-traits/trait-aksrt-db-migrator-part-2.php';
|
||||
|
||||
@@ -0,0 +1,123 @@
|
||||
<?php
|
||||
/**
|
||||
* Admin grant/revoke badges on WP user profile screens.
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Badges_Admin {
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
if (!is_admin()) {
|
||||
return;
|
||||
}
|
||||
add_action('show_user_profile', [$this, 'render_section']);
|
||||
add_action('edit_user_profile', [$this, 'render_section']);
|
||||
add_action('personal_options_update', [$this, 'handle_update']);
|
||||
add_action('edit_user_profile_update', [$this, 'handle_update']);
|
||||
}
|
||||
|
||||
private function can_manage(): bool {
|
||||
return class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public function render_section(WP_User $user): void {
|
||||
if (!class_exists('AKSRT_User_Badges') || !AKSRT_User_Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$dict = AKSRT_User_Badges::get_dictionary();
|
||||
if (!$dict) {
|
||||
return;
|
||||
}
|
||||
$owned = AKSRT_User_Badges::get_owned((int) $user->ID);
|
||||
$wear = AKSRT_User_Badges::get_wear((int) $user->ID);
|
||||
$can = $this->can_manage();
|
||||
?>
|
||||
<a id="aksrt-user-badges"></a>
|
||||
<h2><?php esc_html_e('AKSRT 徽章', 'aksrt'); ?></h2>
|
||||
<table class="form-table" role="presentation">
|
||||
<tr>
|
||||
<th><label><?php esc_html_e('已授予徽章', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<?php if (!$can) : ?>
|
||||
<p class="description"><?php esc_html_e('当前账号无权授予/收回徽章。', 'aksrt'); ?></p>
|
||||
<?php endif; ?>
|
||||
<fieldset <?php disabled(!$can); ?>>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php
|
||||
$slug = $item['slug'];
|
||||
$checked = in_array($slug, $owned, true);
|
||||
?>
|
||||
<label style="display:inline-flex;align-items:center;gap:6px;margin:0 16px 10px 0;">
|
||||
<input type="checkbox" name="aksrt_user_badges[]" value="<?php echo esc_attr($slug); ?>" <?php checked($checked); ?> <?php disabled(!$can); ?>>
|
||||
<?php if (!empty($item['icon_url'])) : ?>
|
||||
<img src="<?php echo esc_url($item['icon_url']); ?>" alt="" width="18" height="18" style="object-fit:contain;vertical-align:middle;">
|
||||
<?php endif; ?>
|
||||
<span><?php echo esc_html($item['label']); ?> <code><?php echo esc_html($slug); ?></code></span>
|
||||
</label>
|
||||
<?php endforeach; ?>
|
||||
</fieldset>
|
||||
<p class="description"><?php esc_html_e('勾选即授予;取消勾选并保存即收回。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<th><label for="aksrt_wear_badge"><?php esc_html_e('当前佩戴', 'aksrt'); ?></label></th>
|
||||
<td>
|
||||
<select name="aksrt_wear_badge" id="aksrt_wear_badge" <?php disabled(!$can); ?>>
|
||||
<option value=""><?php esc_html_e('(未佩戴)', 'aksrt'); ?></option>
|
||||
<?php foreach ($dict as $item) : ?>
|
||||
<?php if (!in_array($item['slug'], $owned, true) && $item['slug'] !== $wear) : ?>
|
||||
<?php continue; ?>
|
||||
<?php endif; ?>
|
||||
<option value="<?php echo esc_attr($item['slug']); ?>" <?php selected($wear, $item['slug']); ?>>
|
||||
<?php echo esc_html($item['label']); ?>
|
||||
</option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
<p class="description"><?php esc_html_e('仅可佩戴已授予徽章;用户也可在前台自行佩戴。', 'aksrt'); ?></p>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<?php
|
||||
wp_nonce_field('aksrt_user_badges_admin', 'aksrt_user_badges_nonce');
|
||||
}
|
||||
|
||||
public function handle_update(int $user_id): void {
|
||||
if (!$this->can_manage()) {
|
||||
return;
|
||||
}
|
||||
if (!isset($_POST['aksrt_user_badges_nonce']) || !wp_verify_nonce(sanitize_text_field(wp_unslash((string) $_POST['aksrt_user_badges_nonce'])), 'aksrt_user_badges_admin')) {
|
||||
return;
|
||||
}
|
||||
if (!class_exists('AKSRT_User_Badges') || !AKSRT_User_Badges::enabled()) {
|
||||
return;
|
||||
}
|
||||
$raw = isset($_POST['aksrt_user_badges']) ? (array) wp_unslash($_POST['aksrt_user_badges']) : [];
|
||||
$slugs = array_map('sanitize_key', $raw);
|
||||
AKSRT_User_Badges::set_owned($user_id, $slugs);
|
||||
|
||||
$wear = isset($_POST['aksrt_wear_badge']) ? sanitize_key((string) wp_unslash($_POST['aksrt_wear_badge'])) : '';
|
||||
if ($wear === '') {
|
||||
delete_user_meta($user_id, AKSRT_User_Badges::META_WEAR);
|
||||
} else {
|
||||
$res = AKSRT_User_Badges::wear($user_id, $wear);
|
||||
if (is_wp_error($res)) {
|
||||
// leave owned as-is; wear ignored if invalid
|
||||
delete_user_meta($user_id, AKSRT_User_Badges::META_WEAR);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,407 @@
|
||||
<?php
|
||||
/**
|
||||
* User badges — own / wear / grant (Zibll medal stack parity).
|
||||
*
|
||||
* Meta:
|
||||
* aksrt_user_badges string[]|CSV owned slug list
|
||||
* aksrt_wear_badge string currently worn slug
|
||||
*
|
||||
* @package AKSRT
|
||||
*/
|
||||
|
||||
defined('ABSPATH') || exit;
|
||||
|
||||
class AKSRT_User_Badges {
|
||||
|
||||
public const META_OWNED = 'aksrt_user_badges';
|
||||
public const META_WEAR = 'aksrt_wear_badge';
|
||||
|
||||
private static $instance = null;
|
||||
|
||||
private function __construct() {}
|
||||
|
||||
public static function get_instance(): self {
|
||||
if (null === self::$instance) {
|
||||
self::$instance = new self();
|
||||
}
|
||||
return self::$instance;
|
||||
}
|
||||
|
||||
public function init(): void {
|
||||
add_action('rest_api_init', [$this, 'register_routes']);
|
||||
}
|
||||
|
||||
public function register_routes(): void {
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges', [
|
||||
'methods' => 'GET',
|
||||
'callback' => [$this, 'rest_list'],
|
||||
'permission_callback' => '__return_true',
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/me/badges/wear', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_wear'],
|
||||
'permission_callback' => static function () {
|
||||
return is_user_logged_in();
|
||||
},
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/grant', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_grant'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
register_rest_route('aksrt/v1', '/users/(?P<id>\d+)/badges/revoke', [
|
||||
'methods' => 'POST',
|
||||
'callback' => [$this, 'rest_revoke'],
|
||||
'permission_callback' => [$this, 'rest_can_manually_set'],
|
||||
'args' => [
|
||||
'id' => [
|
||||
'type' => 'integer',
|
||||
'required' => true,
|
||||
'sanitize_callback' => 'absint',
|
||||
],
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_can_manually_set(): bool {
|
||||
return self::can_manually_set(get_current_user_id());
|
||||
}
|
||||
|
||||
public static function enabled(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
return !empty($settings['badge']['enabled']);
|
||||
}
|
||||
|
||||
public static function public_display(): bool {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return false;
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
return !empty($settings['badge']['enabled']) && !empty($settings['badge']['public_display']);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary(): array {
|
||||
if (!class_exists('AKSRT_Settings')) {
|
||||
return [];
|
||||
}
|
||||
$settings = AKSRT_Settings::get_user_identity_settings();
|
||||
$dict = is_array($settings['badge']['dictionary'] ?? null) ? $settings['badge']['dictionary'] : [];
|
||||
$out = [];
|
||||
foreach ($dict as $item) {
|
||||
$slug = sanitize_key((string) ($item['slug'] ?? ''));
|
||||
if ($slug === '') {
|
||||
continue;
|
||||
}
|
||||
$out[] = [
|
||||
'slug' => $slug,
|
||||
'label' => (string) ($item['label'] ?? $slug),
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
];
|
||||
}
|
||||
return $out;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return array<string,array{slug:string,label:string,tone:string,icon_url:string}>
|
||||
*/
|
||||
public static function get_dictionary_map(): array {
|
||||
$map = [];
|
||||
foreach (self::get_dictionary() as $item) {
|
||||
$map[$item['slug']] = $item;
|
||||
}
|
||||
return $map;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public static function get_owned(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);
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$out = [];
|
||||
foreach ((array) $raw as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
$out[] = $slug;
|
||||
}
|
||||
return array_values(array_unique($out));
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $slugs
|
||||
*/
|
||||
public static function set_owned(int $user_id, array $slugs): void {
|
||||
if ($user_id <= 0) {
|
||||
return;
|
||||
}
|
||||
$map = self::get_dictionary_map();
|
||||
$clean = [];
|
||||
foreach ($slugs as $slug) {
|
||||
$slug = sanitize_key((string) $slug);
|
||||
if ($slug !== '' && isset($map[$slug])) {
|
||||
$clean[] = $slug;
|
||||
}
|
||||
}
|
||||
$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)) {
|
||||
delete_user_meta($user_id, self::META_WEAR);
|
||||
}
|
||||
}
|
||||
|
||||
public static function get_wear(int $user_id): string {
|
||||
if ($user_id <= 0) {
|
||||
return '';
|
||||
}
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, self::META_WEAR, true));
|
||||
if ($wear === '') {
|
||||
return '';
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
return in_array($wear, $owned, true) ? $wear : '';
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function wear(int $user_id, string $slug) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!self::enabled()) {
|
||||
return new WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$map = self::get_dictionary_map();
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return new WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if (!in_array($slug, $owned, true)) {
|
||||
return new WP_Error('aksrt_badge_not_owned', __('尚未获得该徽章', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
update_user_meta($user_id, self::META_WEAR, $slug);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function grant(int $user_id, string $slug, int $actor = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
if (!self::enabled()) {
|
||||
return new WP_Error('aksrt_badge_disabled', __('徽章系统未启用', 'aksrt'), ['status' => 403]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$map = self::get_dictionary_map();
|
||||
if ($slug === '' || !isset($map[$slug])) {
|
||||
return new WP_Error('aksrt_badge_unknown', __('徽章不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$owned = self::get_owned($user_id);
|
||||
if (!in_array($slug, $owned, true)) {
|
||||
$owned[] = $slug;
|
||||
self::set_owned($user_id, $owned);
|
||||
}
|
||||
/**
|
||||
* Fires after a badge is granted.
|
||||
*
|
||||
* @param int $user_id
|
||||
* @param string $slug
|
||||
* @param int $actor
|
||||
*/
|
||||
do_action('aksrt_badge_granted', $user_id, $slug, $actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return true|WP_Error
|
||||
*/
|
||||
public static function revoke(int $user_id, string $slug, int $actor = 0) {
|
||||
if ($user_id <= 0) {
|
||||
return new WP_Error('aksrt_badge_user', __('无效用户', 'aksrt'), ['status' => 400]);
|
||||
}
|
||||
$slug = sanitize_key($slug);
|
||||
$owned = self::get_owned($user_id);
|
||||
$owned = array_values(array_filter($owned, static function ($s) use ($slug) {
|
||||
return $s !== $slug;
|
||||
}));
|
||||
self::set_owned($user_id, $owned);
|
||||
/**
|
||||
* @param int $user_id
|
||||
* @param string $slug
|
||||
* @param int $actor
|
||||
*/
|
||||
do_action('aksrt_badge_revoked', $user_id, $slug, $actor);
|
||||
return true;
|
||||
}
|
||||
|
||||
public static function can_manually_set(int $actor = 0): bool {
|
||||
$actor = $actor > 0 ? $actor : get_current_user_id();
|
||||
if ($actor <= 0) {
|
||||
return false;
|
||||
}
|
||||
if (user_can($actor, 'manage_options')) {
|
||||
return true;
|
||||
}
|
||||
if (class_exists('AKSRT_Settings_Capabilities')) {
|
||||
return AKSRT_Settings_Capabilities::user_can('medal_manually_set', $actor);
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Public DTO for REST / modals.
|
||||
*
|
||||
* @return array{enabled:bool,public_display:bool,wear:string,items:list<array>}
|
||||
*/
|
||||
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();
|
||||
$can_see = $include_private
|
||||
|| $public
|
||||
|| ($viewer > 0 && $viewer === $user_id)
|
||||
|| self::can_manually_set($viewer);
|
||||
|
||||
if (!$enabled || !$can_see || $user_id <= 0) {
|
||||
return [
|
||||
'enabled' => $enabled,
|
||||
'public_display' => $public,
|
||||
'wear' => '',
|
||||
'items' => [],
|
||||
];
|
||||
}
|
||||
|
||||
$map = self::get_dictionary_map();
|
||||
$owned = self::get_owned($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])) {
|
||||
continue;
|
||||
}
|
||||
$items[] = [
|
||||
'slug' => $slug,
|
||||
'label' => $map[$slug]['label'],
|
||||
'tone' => $map[$slug]['tone'],
|
||||
'icon_url' => $map[$slug]['icon_url'],
|
||||
'wear' => ($slug === $wear),
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'enabled' => true,
|
||||
'public_display' => $public,
|
||||
'wear' => $wear,
|
||||
'items' => $items,
|
||||
];
|
||||
}
|
||||
|
||||
public function rest_list(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$data = self::list_for_user($user_id);
|
||||
return rest_ensure_response(['success' => true, 'data' => $data]);
|
||||
}
|
||||
|
||||
public function rest_wear(WP_REST_Request $request) {
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::wear(get_current_user_id(), $slug);
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user(get_current_user_id(), true),
|
||||
'message' => __('已佩戴徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_grant(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::grant($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已授予徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
|
||||
public function rest_revoke(WP_REST_Request $request) {
|
||||
$user_id = (int) $request['id'];
|
||||
if ($user_id <= 0 || !get_userdata($user_id)) {
|
||||
return new WP_Error('aksrt_badge_user', __('用户不存在', 'aksrt'), ['status' => 404]);
|
||||
}
|
||||
$params = $request->get_json_params();
|
||||
if (!is_array($params)) {
|
||||
$params = $request->get_params();
|
||||
}
|
||||
$slug = sanitize_key((string) ($params['slug'] ?? $params['name'] ?? ''));
|
||||
$result = self::revoke($user_id, $slug, get_current_user_id());
|
||||
if (is_wp_error($result)) {
|
||||
return $result;
|
||||
}
|
||||
return rest_ensure_response([
|
||||
'success' => true,
|
||||
'data' => self::list_for_user($user_id, true),
|
||||
'message' => __('已收回徽章', 'aksrt'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
@@ -162,51 +162,64 @@ class AKSRT_User_Home {
|
||||
}
|
||||
|
||||
if (!empty($settings['badge']['enabled']) && !empty($settings['badge']['public_display'])) {
|
||||
$limit = max(1, (int) ($settings['badge']['limit'] ?? 3));
|
||||
if (class_exists('AKSRT_User_Badges')) {
|
||||
$list = AKSRT_User_Badges::list_for_user($user_id, true);
|
||||
$badge_count = 0;
|
||||
foreach ((array) ($list['items'] ?? []) as $item) {
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => (string) ($item['slug'] ?? ''),
|
||||
'label' => (string) ($item['label'] ?? ''),
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($item['tone'] ?? 'neutral'),
|
||||
'icon_url' => (string) ($item['icon_url'] ?? ''),
|
||||
'wear' => !empty($item['wear']),
|
||||
];
|
||||
$badge_count++;
|
||||
}
|
||||
} else {
|
||||
$badge_map = [];
|
||||
foreach ((array) ($settings['badge']['dictionary'] ?? []) as $item) {
|
||||
if (!empty($item['slug'])) {
|
||||
$badge_map[(string) $item['slug']] = $item;
|
||||
}
|
||||
}
|
||||
|
||||
$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);
|
||||
}
|
||||
$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);
|
||||
}, (array) $assigned)));
|
||||
|
||||
// Prefer worn badge first (Zibll wear-medal).
|
||||
$wear = sanitize_key((string) get_user_meta($user_id, 'aksrt_wear_badge', true));
|
||||
if ($wear !== '' && in_array($wear, $assigned, true) && isset($badge_map[$wear])) {
|
||||
$assigned = array_values(array_unique(array_merge([$wear], $assigned)));
|
||||
}
|
||||
|
||||
$limit = max(1, (int) ($settings['badge']['limit'] ?? 3));
|
||||
$badge_count = 0;
|
||||
foreach ($assigned as $slug) {
|
||||
if ($slug === '' || !isset($badge_map[$slug])) {
|
||||
continue;
|
||||
}
|
||||
|
||||
$icon_url = (string) ($badge_map[$slug]['icon_url'] ?? '');
|
||||
$signals[] = [
|
||||
'type' => 'badge',
|
||||
'slug' => $slug,
|
||||
'label' => (string) $badge_map[$slug]['label'],
|
||||
'icon' => 'award',
|
||||
'tone' => (string) ($badge_map[$slug]['tone'] ?? 'neutral'),
|
||||
'icon_url' => $icon_url,
|
||||
'icon_url' => (string) ($badge_map[$slug]['icon_url'] ?? ''),
|
||||
'wear' => ($wear !== '' && $slug === $wear),
|
||||
];
|
||||
$badge_count++;
|
||||
|
||||
if ($badge_count >= $limit) {
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $signals;
|
||||
}
|
||||
|
||||
@@ -761,6 +761,17 @@ trait AKSRT_Scripts_Part_1b {
|
||||
file_exists($credits_js_path) ? (string) filemtime($credits_js_path) : $version,
|
||||
true
|
||||
);
|
||||
|
||||
$badges_js_path = get_template_directory() . '/assets/js/user-badges.js';
|
||||
if (file_exists($badges_js_path)) {
|
||||
wp_enqueue_script(
|
||||
'aksrt-user-badges',
|
||||
$base_uri . '/assets/js/user-badges.js',
|
||||
['aksrt-core'],
|
||||
(string) filemtime($badges_js_path),
|
||||
true
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -182,6 +182,40 @@ if (!function_exists('aksrt_single_render_box_header')) :
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_render_share_dropdown')) :
|
||||
/**
|
||||
* Shared #share-dropdown markup (actions row + tabbar fallback).
|
||||
*
|
||||
* @param array<string,mixed> $attrs Optional data-share-* overrides.
|
||||
*/
|
||||
function aksrt_render_share_dropdown(array $attrs = []): void {
|
||||
static $printed = false;
|
||||
if ($printed) {
|
||||
return;
|
||||
}
|
||||
$printed = true;
|
||||
$title = (string) ($attrs['title'] ?? get_the_title());
|
||||
$url = (string) ($attrs['url'] ?? (get_permalink() ?: ''));
|
||||
$image = (string) ($attrs['image'] ?? (get_the_post_thumbnail_url(null, 'large') ?: ''));
|
||||
?>
|
||||
<div class="share-dropdown" id="share-dropdown" aria-hidden="true"
|
||||
data-share-title="<?php echo esc_attr($title); ?>"
|
||||
data-share-url="<?php echo esc_url($url); ?>"
|
||||
<?php if ($image !== '') : ?>data-share-image="<?php echo esc_url($image); ?>"<?php endif; ?>>
|
||||
<div class="share-dropdown__grid">
|
||||
<button type="button" class="share-item" data-platform="qq"><i class="fab fa-qq"></i><span>QQ</span></button>
|
||||
<button type="button" class="share-item" data-platform="weibo"><i class="fab fa-weibo"></i><span>微博</span></button>
|
||||
<button type="button" class="share-item" data-platform="twitter"><i class="fab fa-x-twitter"></i><span>X</span></button>
|
||||
<button type="button" class="share-item" data-platform="wechat"><i class="fab fa-weixin"></i><span>微信</span></button>
|
||||
<button type="button" class="share-item" data-platform="copy"><i class="fas fa-link"></i><span><?php esc_html_e('复制链接', 'aksrt'); ?></span></button>
|
||||
<button type="button" class="share-item" data-platform="poster"><i class="fas fa-image"></i><span><?php esc_html_e('海报分享', 'aksrt'); ?></span></button>
|
||||
</div>
|
||||
<div id="share-qrcode-box" aria-hidden="true"><img id="share-qrcode-image" src="" alt="QR Code"></div>
|
||||
</div>
|
||||
<?php
|
||||
}
|
||||
endif;
|
||||
|
||||
if (!function_exists('aksrt_single_render_actions')) :
|
||||
/**
|
||||
* Like / reward / share / favorite row.
|
||||
@@ -228,18 +262,9 @@ if (!function_exists('aksrt_single_render_actions')) :
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
<?php if (!empty($settings['share_enabled'])) : ?>
|
||||
<div class="share-dropdown" id="share-dropdown" aria-hidden="true">
|
||||
<div class="share-dropdown__grid">
|
||||
<button type="button" class="share-item" data-platform="qq"><i class="fab fa-qq"></i><span>QQ</span></button>
|
||||
<button type="button" class="share-item" data-platform="weibo"><i class="fab fa-weibo"></i><span>微博</span></button>
|
||||
<button type="button" class="share-item" data-platform="twitter"><i class="fab fa-x-twitter"></i><span>X</span></button>
|
||||
<button type="button" class="share-item" data-platform="wechat"><i class="fab fa-weixin"></i><span>微信</span></button>
|
||||
<button type="button" class="share-item" data-platform="copy"><i class="fas fa-link"></i><span><?php esc_html_e('复制链接', 'aksrt'); ?></span></button>
|
||||
<button type="button" class="share-item" data-platform="poster"><i class="fas fa-image"></i><span><?php esc_html_e('海报分享', 'aksrt'); ?></span></button>
|
||||
</div>
|
||||
<div id="share-qrcode-box" aria-hidden="true"><img id="share-qrcode-image" src="" alt="QR Code"></div>
|
||||
</div>
|
||||
<?php if (!empty($settings['share_enabled']) && function_exists('aksrt_render_share_dropdown')) : ?>
|
||||
<?php aksrt_render_share_dropdown();
|
||||
?>
|
||||
<?php endif; ?>
|
||||
</div>
|
||||
|
||||
@@ -323,6 +348,14 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
]);
|
||||
$is_self = is_user_logged_in() && get_current_user_id() === $author_id;
|
||||
$login_url = function_exists('aksrt_get_login_url') ? aksrt_get_login_url() : wp_login_url(get_permalink());
|
||||
$aksrt_pm_on = class_exists('AKSRT_Settings') && !empty(AKSRT_Settings::private_message_settings()['enabled']);
|
||||
$aksrt_msg_url = function_exists('aksrt_get_messages_page_url') ? aksrt_get_messages_page_url() : '';
|
||||
$aksrt_post_settings = class_exists('AKSRT_Settings') && method_exists('AKSRT_Settings', 'get_post_page_settings')
|
||||
? AKSRT_Settings::get_post_page_settings()
|
||||
: [];
|
||||
$rewards_on = !empty($aksrt_post_settings['rewards_enabled'])
|
||||
|| !empty($aksrt_post_settings['post_rewards_enabled'])
|
||||
|| !empty($aksrt_post_settings['post_rewards_s']);
|
||||
?>
|
||||
<div class="user-card author zib-widget">
|
||||
<div class="card-content mt10 relative">
|
||||
@@ -350,6 +383,15 @@ if (!function_exists('aksrt_single_render_author_card')) :
|
||||
</a>
|
||||
<?php endif; ?>
|
||||
<?php endif; ?>
|
||||
<?php if (!$is_self && is_user_logged_in() && $aksrt_pm_on && $aksrt_msg_url !== '') : ?>
|
||||
<a class="but pw-1em mr6 c-blue" href="<?php echo esc_url(add_query_arg('msg_type', 'conversation', $aksrt_msg_url)); ?>">
|
||||
<i class="fa fa-comment em09" aria-hidden="true"></i> <?php esc_html_e('私信', 'aksrt'); ?>
|
||||
</a>
|
||||
<?php elseif (!$is_self && $rewards_on) : ?>
|
||||
<button type="button" class="but pw-1em mr6 c-yellow" data-aksrt-reward-open>
|
||||
<i class="fa fa-gift em09" aria-hidden="true"></i> <?php esc_html_e('赞赏', 'aksrt'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
<a class="but pw-1em mr6 jb-blue" href="<?php echo esc_url($author_url); ?>"><?php esc_html_e('个人主页', 'aksrt'); ?></a>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1387,6 +1387,14 @@ if (!function_exists('aksrt_render_footer_tabbar_single_extend')) :
|
||||
<?php
|
||||
endif;
|
||||
|
||||
if ($share_on && function_exists('aksrt_render_share_dropdown')) {
|
||||
aksrt_render_share_dropdown([
|
||||
'title' => get_the_title($post_id),
|
||||
'url' => get_permalink($post_id) ?: '',
|
||||
'image' => get_the_post_thumbnail_url($post_id, 'large') ?: '',
|
||||
]);
|
||||
}
|
||||
|
||||
if ($comments_open) :
|
||||
?>
|
||||
<a href="#comments"
|
||||
|
||||
@@ -61,6 +61,12 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
||||
</div>
|
||||
</div>
|
||||
<div class="uc-header__actions">
|
||||
<?php if (class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::enabled()) : ?>
|
||||
<button type="button" class="btn-ghost btn-sm" data-aksrt-badges-open>
|
||||
<i class="fas fa-award" aria-hidden="true"></i> <?php esc_html_e('我的徽章', 'aksrt'); ?>
|
||||
</button>
|
||||
<?php endif; ?>
|
||||
|
||||
<a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm">
|
||||
<i class="fas fa-bell" aria-hidden="true"></i> <?php esc_html_e('消息通知', 'aksrt'); ?>
|
||||
<?php if ($notification_unread_count > 0) : ?>
|
||||
@@ -78,3 +84,16 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<?php if (class_exists('AKSRT_User_Badges') && AKSRT_User_Badges::enabled()) : ?>
|
||||
<div class="uc-modal" id="aksrt-badges-modal" hidden data-aksrt-badges-modal data-user-id="<?php echo (int) $user_id; ?>" data-can-wear="1">
|
||||
<div class="uc-modal__overlay" data-aksrt-badges-close></div>
|
||||
<div class="uc-modal__dialog" role="dialog" aria-modal="true" aria-label="<?php esc_attr_e('我的徽章', 'aksrt'); ?>">
|
||||
<button type="button" class="uc-modal__close" data-aksrt-badges-close aria-label="<?php esc_attr_e('关闭', 'aksrt'); ?>"><i class="fas fa-times" aria-hidden="true"></i></button>
|
||||
<h3 class="uc-modal__title"><i class="fas fa-award mr6" aria-hidden="true"></i><?php esc_html_e('我的徽章', 'aksrt'); ?></h3>
|
||||
<div class="uc-badges-list" data-aksrt-badges-body>
|
||||
<div class="muted-2-color em09"><?php esc_html_e('加载中…', 'aksrt'); ?></div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<?php endif; ?>
|
||||
|
||||
Reference in New Issue
Block a user