feat(checkin): open reference-parity details modal from signed buttons

- Rebuild checkin details modal HTML 1:1 with reference theme:
  colorful header c-blue, user card (points/level), daily reward
  card and 7-day continuous mini cards
- Signed entry buttons (header dropdown, user-card badge, user
  center) become clickable checkin-details-link modal triggers
  instead of disabled buttons
- Swap data-signin-btn to details link in place after successful
  signin (reference .initiate-checkin replacement behavior)
- Add aksrt-checkin-details-modal runtime shell (modal-mini 400px)
- Remove dead code: legacy lite modal, admin-ajax details handler,
  /signin/history route, signin calendar; rebuild public bundles
- Update contract tests to the new checkin modal contract
This commit is contained in:
2026-09-19 14:49:58 +08:00
parent c289016324
commit 4653128a18
15 changed files with 211 additions and 266 deletions
+19 -39
View File
@@ -3046,10 +3046,8 @@ main.container,
} }
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮), /* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
* 仅禁用指针交互;此前的灰底灰字覆盖是与参考主题的偏差。 */ * 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
.aksrt-header-checkin.is-done,
.aksrt-header-checkin:disabled, .aksrt-header-checkin:disabled,
.aksrt-header-checkin.is-done:hover,
.aksrt-header-checkin:disabled:hover { .aksrt-header-checkin:disabled:hover {
cursor: default; cursor: default;
opacity: .85; opacity: .85;
@@ -6269,42 +6267,7 @@ body.aksrt-body-scroll .aksrt-show-on-scroll {
} }
/* ============================================================ /* ============================================================
* 18. 签到日历可视化 * 18. VIP 等级标识
* ============================================================ */
.aksrt-signin-calendar__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.aksrt-signin-calendar__day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--surface-2, #F4F5F7);
font-size: 12px;
color: var(--color-ink-3);
transition: all 0.2s;
}
.aksrt-signin-calendar__day.is-signed {
background: var(--color-success, #10B981);
color: #fff;
}
.aksrt-signin-calendar__day.is-today {
border: 2px solid var(--color-primary, var(--color-primary));
}
.aksrt-signin-calendar__day.is-signed.is-today {
background: var(--color-primary, var(--color-primary));
border-color: var(--color-primary, var(--color-primary));
}
/* ============================================================
* 19. VIP 等级标识
* ============================================================ */ * ============================================================ */
.aksrt-vip-badge { .aksrt-vip-badge {
display: inline-flex; display: inline-flex;
@@ -7180,6 +7143,23 @@ body.aksrt-modal-runtime-open {
color: var(--color-on-primary, #fff); color: var(--color-on-primary, #fff);
} }
/* ---- 签到详情弹窗(参考主题 refresh_modal modal-mini 同构壳) ----
内容为服务端 HTML(modal-colorful-header + modal-body),与参考主题
.modal-dialog.modal-mini 几何 1:1:宽 400px、圆角 var(--main-radius)。 */
.aksrt-modal-runtime.aksrt-checkin-details-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__content {
padding: 0;
}
.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* ---- ajax list ---- */ /* ---- ajax list ---- */
.ajaxpager.is-ajax-loading { .ajaxpager.is-ajax-loading {
position: relative; position: relative;
+1 -1
View File
File diff suppressed because one or more lines are too long
@@ -356,42 +356,7 @@
} }
/* ============================================================ /* ============================================================
* 18. 签到日历可视化 * 18. VIP 等级标识
* ============================================================ */
.aksrt-signin-calendar__grid {
display: grid;
grid-template-columns: repeat(7, 1fr);
gap: 4px;
}
.aksrt-signin-calendar__day {
aspect-ratio: 1;
display: flex;
align-items: center;
justify-content: center;
border-radius: 6px;
background: var(--surface-2, #F4F5F7);
font-size: 12px;
color: var(--color-ink-3);
transition: all 0.2s;
}
.aksrt-signin-calendar__day.is-signed {
background: var(--color-success, #10B981);
color: #fff;
}
.aksrt-signin-calendar__day.is-today {
border: 2px solid var(--color-primary, var(--color-primary));
}
.aksrt-signin-calendar__day.is-signed.is-today {
background: var(--color-primary, var(--color-primary));
border-color: var(--color-primary, var(--color-primary));
}
/* ============================================================
* 19. VIP 等级标识
* ============================================================ */ * ============================================================ */
.aksrt-vip-badge { .aksrt-vip-badge {
display: inline-flex; display: inline-flex;
+1 -3
View File
@@ -327,10 +327,8 @@
} }
/* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮), /* 已签到态对齐参考主题:保留原配色(参考主题签到后仍为同一彩色按钮),
* 仅禁用指针交互;此前的灰底灰字覆盖是与参考主题的偏差。 */ * 且按钮可点击(打开签到详情弹窗);仅请求进行中的禁用态降不透明度。 */
.aksrt-header-checkin.is-done,
.aksrt-header-checkin:disabled, .aksrt-header-checkin:disabled,
.aksrt-header-checkin.is-done:hover,
.aksrt-header-checkin:disabled:hover { .aksrt-header-checkin:disabled:hover {
cursor: default; cursor: default;
opacity: .85; opacity: .85;
+17
View File
@@ -223,6 +223,23 @@ body.aksrt-modal-runtime-open {
color: var(--color-on-primary, #fff); color: var(--color-on-primary, #fff);
} }
/* ---- 签到详情弹窗(参考主题 refresh_modal modal-mini 同构壳) ----
内容为服务端 HTML(modal-colorful-header + modal-body),与参考主题
.modal-dialog.modal-mini 几何 1:1:宽 400px、圆角 var(--main-radius)。 */
.aksrt-modal-runtime.aksrt-checkin-details-modal:not(.aksrt-modal-runtime--bottom) .aksrt-modal-runtime__dialog {
width: min(400px, calc(100vw - 32px));
border-radius: var(--main-radius, 8px);
background: var(--main-bg-color, #fff);
}
.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__content {
padding: 0;
}
.aksrt-modal-runtime.aksrt-checkin-details-modal .aksrt-modal-runtime__skeleton {
padding: 24px 18px;
}
/* ---- ajax list ---- */ /* ---- ajax list ---- */
.ajaxpager.is-ajax-loading { .ajaxpager.is-ajax-loading {
position: relative; position: relative;
-36
View File
@@ -241,41 +241,6 @@
}); });
} }
/* ============================================================
* 8. 签到日历可视化
* ============================================================ */
function initSigninCalendar() {
var calendar = document.querySelector('[data-aksrt-signin-calendar]');
if (!calendar) return;
var data = [];
try {
data = JSON.parse(calendar.dataset.aksrtSigninCalendar || '[]');
} catch (e) {}
// 生成最近 30 天的格子
var today = new Date();
var html = '<div class="aksrt-signin-calendar__grid">';
for (var i = 29; i >= 0; i--) {
var d = new Date(today);
d.setDate(d.getDate() - i);
var dateStr = d.toISOString().split('T')[0];
var isSigned = data.indexOf(dateStr) !== -1;
var isToday = i === 0;
html += '<div class="aksrt-signin-calendar__day' +
(isSigned ? ' is-signed' : '') +
(isToday ? ' is-today' : '') +
'" title="' + dateStr + (isSigned ? ' ✓' : '') + '">' +
'<span>' + d.getDate() + '</span>' +
'</div>';
}
html += '</div>';
calendar.innerHTML = html;
}
/* ============================================================ /* ============================================================
* 初始化入口 — 先执行一次,随后监听 auto_fun 重扫(各 init 幂等) * 初始化入口 — 先执行一次,随后监听 auto_fun 重扫(各 init 幂等)
* ============================================================ */ * ============================================================ */
@@ -286,7 +251,6 @@
initSticky(); initSticky();
initParallax(); initParallax();
initFloatLabels(); initFloatLabels();
initSigninCalendar();
} }
if (document.readyState === 'loading') { if (document.readyState === 'loading') {
+1 -1
View File
File diff suppressed because one or more lines are too long
+22
View File
@@ -72,6 +72,26 @@
}); });
} }
// 签到成功后,入口按钮就地替换为「已签到」详情弹窗触发链接
// (参考主题:.initiate-checkin → details_link 的替换行为)。
function swapButtonsToDetailsLink() {
var url = window.aksrtData && window.aksrtData.signinDetailsUrl;
if (!url) return;
currentButtons().forEach(function (button) {
var link = document.createElement('a');
link.href = 'javascript:;';
link.className = (button.className || '') + ' checkin-details-link';
link.setAttribute('data-aksrt-modal', 'remote');
link.setAttribute('data-aksrt-modal-url', url);
link.setAttribute('data-aksrt-modal-class', 'aksrt-checkin-details-modal modal-mini');
link.setAttribute('data-aksrt-modal-height', '240');
link.setAttribute('data-aksrt-modal-mobile-bottom', 'true');
link.setAttribute('data-aksrt-modal-cache', 'false');
link.innerHTML = button.innerHTML;
button.replaceWith(link);
});
}
function refresh() { function refresh() {
api('/signin/today').then(function (result) { api('/signin/today').then(function (result) {
if (!result.ok) { if (!result.ok) {
@@ -90,6 +110,7 @@
if (data.signed) { if (data.signed) {
setStatusHtml('已签到 <strong>' + data.streak + '</strong> 天 +' + data.reward_today); setStatusHtml('已签到 <strong>' + data.streak + '</strong> 天 +' + data.reward_today);
setButtonState('done'); setButtonState('done');
swapButtonsToDetailsLink();
return; return;
} }
setStatusHtml('连续 <strong>' + data.streak + '</strong> 天,+' + data.reward_if_signin); setStatusHtml('连续 <strong>' + data.streak + '</strong> 天,+' + data.reward_if_signin);
@@ -120,6 +141,7 @@
element.title = element.textContent; element.title = element.textContent;
}); });
setButtonState('done'); setButtonState('done');
swapButtonsToDetailsLink();
refresh(); refresh();
}).finally(function () { }).finally(function () {
if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('signin', captchaMount()); if (window.AKSRTCaptcha) window.AKSRTCaptcha.reset('signin', captchaMount());
+41 -9
View File
@@ -7,6 +7,29 @@
defined('ABSPATH') || exit; defined('ABSPATH') || exit;
if (!function_exists('aksrt_get_user_checkin_details_link')) :
/**
* 已签到详情弹窗触发链接(参考主题 zib_get_user_checkin_details_link 1:1):
* 打开刷新式 mini 弹窗(高 240,移动端底部弹出,每次都重新拉取)。
*
* @param string $class 沿用入口按钮的类。
* @param string $text 链接内文(含图标)。
*/
function aksrt_get_user_checkin_details_link(string $class = '', string $text = ''): string {
$class = trim($class . ' checkin-details-link');
return '<a class="' . esc_attr($class) . '" href="javascript:;"'
. ' data-aksrt-modal="remote"'
. ' data-aksrt-modal-url="' . esc_url(rest_url('aksrt/v1/signin/details')) . '"'
. ' data-aksrt-modal-class="aksrt-checkin-details-modal modal-mini"'
. ' data-aksrt-modal-height="240"'
. ' data-aksrt-modal-mobile-bottom="true"'
. ' data-aksrt-modal-cache="false">'
. $text
. '</a>';
}
endif;
if (!function_exists('aksrt_header_user_checkin_btn')) : if (!function_exists('aksrt_header_user_checkin_btn')) :
/** /**
* Check-in CTA for header dropdown and compact card placements. * Check-in CTA for header dropdown and compact card placements.
@@ -31,27 +54,36 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
$signed = \AKSRT\Credits\Signin::get_instance()->is_signed_today($user_id); $signed = \AKSRT\Credits\Signin::get_instance()->is_signed_today($user_id);
// 已签到:与参考主题一致,入口按钮变为签到详情弹窗触发链接(可点击)。
if ($signed) {
$icon = '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>';
if ($class !== null) {
// 用户卡片签到书签:保留书签类与 is-done 视觉,仅将行为切换为打开详情弹窗。
return aksrt_get_user_checkin_details_link(
$class . ' is-done',
$icon . '<span>' . esc_html__('已签到', 'aksrt') . '</span>'
);
}
return aksrt_get_user_checkin_details_link(
'but block c-yellow aksrt-header-checkin is-done',
'<i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt'))
);
}
$classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : '')); $classes = $class ?? ('but block c-yellow aksrt-header-checkin' . ($compact ? ' aksrt-header-checkin--compact' : ''));
// Card badge variant (explicit $class, e.g. the user-card 签到 badge): // Card badge variant (explicit $class, e.g. the user-card 签到 badge):
// reference parity renders an <a> so the badge inherits the card font // reference parity renders an <a> so the badge inherits the card font
// instead of the UA button stylesheet (font/UI 1:1 with the reference). // instead of the UA button stylesheet (font/UI 1:1 with the reference).
if ($class !== null) { if ($class !== null) {
return '<a class="' . esc_attr($classes . ($signed ? ' is-done' : '')) . '" href="javascript:;" data-signin-btn' return '<a class="' . esc_attr($classes) . '" href="javascript:;" data-signin-btn'
. ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"' . ' data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '"'
. ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">' . ' data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
. '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>' . '<i class="fa fas fa-calendar-check ml3 mr6" aria-hidden="true"></i>'
. '<span data-signin-label>' . esc_html($signed ? __('已签到', 'aksrt') : __('签到', 'aksrt')) . '</span>' . '<span data-signin-label>' . esc_html__('签到', 'aksrt') . '</span>'
. '</a>'; . '</a>';
} }
if ($signed) {
return '<button type="button" class="' . esc_attr($classes . ' is-done') . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '" disabled>'
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
. '<span data-signin-label>' . esc_html($compact ? __('已签到', 'aksrt') : __('今日已签到', 'aksrt')) . '</span>'
. '</button>';
}
return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">' return '<button type="button" class="' . esc_attr($classes) . '" data-signin-btn data-signin-idle-label="' . esc_attr__('签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '">'
. '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>' . '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'
. '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>' . '<span data-signin-label>' . esc_html($compact ? __('签到', 'aksrt') : __('签到领取今日奖励', 'aksrt')) . '</span>'
@@ -32,9 +32,13 @@ $currency = (string) ($currency_name ?? __('积分', 'aksrt'));
</div> </div>
<?php if (!empty($signin_enabled)) : ?> <?php if (!empty($signin_enabled)) : ?>
<span class="uc-asset-card__head-action muted-color px12" data-aksrt-signin> <span class="uc-asset-card__head-action muted-color px12" data-aksrt-signin>
<?php if (!empty($signin_signed_today)) : ?>
<?php echo aksrt_get_user_checkin_details_link('uc-asset-card__signin muted-color px12', '<span>' . esc_html__('已签到', 'aksrt') . '</span><i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i>'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
<?php else : ?>
<button type="button" class="uc-asset-card__signin muted-color px12" data-signin-btn data-signin-idle-label="<?php esc_attr_e('签到领积分', 'aksrt'); ?>" data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>" disabled> <button type="button" class="uc-asset-card__signin muted-color px12" data-signin-btn data-signin-idle-label="<?php esc_attr_e('签到领积分', 'aksrt'); ?>" data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>" disabled>
<span data-signin-label><?php esc_html_e('签到领积分', 'aksrt'); ?></span><i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i> <span data-signin-label><?php esc_html_e('签到领积分', 'aksrt'); ?></span><i class="fa fas fa-angle-right em12 ml3" aria-hidden="true"></i>
</button> </button>
<?php endif; ?>
<span class="uc-asset-card__signin-status" data-signin-status></span> <span class="uc-asset-card__signin-status" data-signin-status></span>
</span> </span>
<?php endif; ?> <?php endif; ?>
@@ -69,21 +69,24 @@ extract($context, EXTR_SKIP); // phpcs:ignore WordPress.PHP.DontExtract.extract_
</div> </div>
</div> </div>
<div class="uc-header__actions"> <div class="uc-header__actions">
<?php if (!empty($signin_enabled)) : ?> <?php if (!empty($signin_enabled)) : ?>
<span data-aksrt-signin> <span data-aksrt-signin>
<button <?php if (!empty($signin_signed_today)) : ?>
type="button" <?php echo aksrt_get_user_checkin_details_link('btn btn-sm uc-header__signin-btn is-done', '<i class="fa fas fa-calendar-check" aria-hidden="true"></i><span>' . esc_html__('已签到', 'aksrt') . '</span>'); // phpcs:ignore WordPress.Security.EscapeOutput.OutputNotEscaped ?>
class="btn btn-sm uc-header__signin-btn<?php echo !empty($signin_signed_today) ? ' is-done' : ''; ?>" <?php else : ?>
data-signin-btn <button
data-signin-idle-label="<?php esc_attr_e('签到', 'aksrt'); ?>" type="button"
data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>" class="btn btn-sm uc-header__signin-btn"
<?php disabled(!empty($signin_signed_today)); ?> data-signin-btn
> data-signin-idle-label="<?php esc_attr_e('签到', 'aksrt'); ?>"
<i class="fa fas fa-calendar-check" aria-hidden="true"></i> data-signin-done-label="<?php esc_attr_e('已签到', 'aksrt'); ?>"
<span data-signin-label><?php echo !empty($signin_signed_today) ? esc_html__('已签到', 'aksrt') : esc_html__('签到', 'aksrt'); ?></span> >
</button> <i class="fa fas fa-calendar-check" aria-hidden="true"></i>
</span> <span data-signin-label><?php esc_html_e('签到', 'aksrt'); ?></span>
</button>
<?php endif; ?> <?php endif; ?>
</span>
<?php endif; ?>
<?php if (!empty($messages_url)) : ?> <?php if (!empty($messages_url)) : ?>
<a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm btn-icon uc-header__message-btn" aria-label="<?php esc_attr_e('消息通知', 'aksrt'); ?>" title="<?php esc_attr_e('消息通知', 'aksrt'); ?>"> <a href="<?php echo esc_url($messages_url); ?>" class="btn-ghost btn-sm btn-icon uc-header__message-btn" aria-label="<?php esc_attr_e('消息通知', 'aksrt'); ?>" title="<?php esc_attr_e('消息通知', 'aksrt'); ?>">
<i class="fa fas fa-bell" aria-hidden="true"></i> <i class="fa fas fa-bell" aria-hidden="true"></i>
+2
View File
@@ -461,6 +461,8 @@ trait ScriptsPart1c {
'nonce' => wp_create_nonce('aksrt_public_nonce'), 'nonce' => wp_create_nonce('aksrt_public_nonce'),
'restNonce' => wp_create_nonce('wp_rest'), 'restNonce' => wp_create_nonce('wp_rest'),
'captchaSignin' => function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('signin'), 'captchaSignin' => function_exists('aksrt_is_captcha_enabled_for') && aksrt_is_captcha_enabled_for('signin'),
// 已签到详情弹窗(签到成功后就地替换入口按钮用)。
'signinDetailsUrl' => esc_url_raw(rest_url('aksrt/v1/signin/details')),
'visibilityNonce' => wp_create_nonce('aksrt_visibility'), 'visibilityNonce' => wp_create_nonce('aksrt_visibility'),
'siteUrl' => home_url('/'), 'siteUrl' => home_url('/'),
'themeUrl' => AKSRT_URI, 'themeUrl' => AKSRT_URI,
+78 -123
View File
@@ -20,7 +20,6 @@ class Signin {
public function init(): void { public function init(): void {
add_action('rest_api_init', [$this, 'register_routes']); add_action('rest_api_init', [$this, 'register_routes']);
add_action('wp_ajax_aksrt_checkin_details_modal', [$this, 'ajax_details_modal']);
} }
public function register_routes(): void { public function register_routes(): void {
@@ -34,11 +33,6 @@ class Signin {
'callback' => [$this, 'rest_signin'], 'callback' => [$this, 'rest_signin'],
'permission_callback' => [$this, 'check_rest_permission'], 'permission_callback' => [$this, 'check_rest_permission'],
]); ]);
register_rest_route('aksrt/v1', '/signin/history', [
'methods' => 'GET',
'callback' => [$this, 'rest_history'],
'permission_callback' => [$this, 'check_rest_permission'],
]);
register_rest_route('aksrt/v1', '/signin/details', [ register_rest_route('aksrt/v1', '/signin/details', [
'methods' => 'GET', 'methods' => 'GET',
'callback' => [$this, 'rest_details_html'], 'callback' => [$this, 'rest_details_html'],
@@ -237,42 +231,12 @@ class Signin {
do_action('aksrt_checkin_done', $user_id); do_action('aksrt_checkin_done', $user_id);
} }
/** GET /signin/history — last N days for calendar/modals. */
public function rest_history(\WP_REST_Request $request) {
$user_id = get_current_user_id();
$days = max(7, min(90, (int) ($request->get_param('days') ?: 30)));
$rows = $this->get_recent_signins($user_id, $days);
$dates = [];
foreach ($rows as $row) {
if (!empty($row['signin_date'])) {
$dates[] = (string) $row['signin_date'];
}
}
$today = $this->today_date();
return new \WP_REST_Response([
'dates' => $dates,
'streak' => $this->compute_current_streak($user_id, $today),
'total' => $this->get_total_days($user_id),
'signed' => (bool) $this->get_record($user_id, $today),
'all_day' => (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true) ?: $this->get_total_days($user_id),
], 200);
}
/** GET /signin/details — HTML fragment for remote modal. */ /** GET /signin/details — HTML fragment for remote modal. */
public function rest_details_html(\WP_REST_Request $request) { public function rest_details_html(\WP_REST_Request $request) {
$html = $this->render_details_modal_html(get_current_user_id()); $html = $this->render_details_modal_html(get_current_user_id());
return new \WP_REST_Response(['success' => true, 'html' => $html], 200); return new \WP_REST_Response(['success' => true, 'html' => $html], 200);
} }
/** admin-ajax HTML (remote modal fetch often hits admin-ajax). */
public function ajax_details_modal(): void {
if (!is_user_logged_in()) {
wp_send_json_error(['message' => __('请先登录', 'aksrt')], 401);
}
$html = $this->render_details_modal_html(get_current_user_id());
wp_send_json_success(['html' => $html]);
}
public function is_signed_today(int $user_id = 0): bool { public function is_signed_today(int $user_id = 0): bool {
$user_id = $user_id > 0 ? $user_id : get_current_user_id(); $user_id = $user_id > 0 ? $user_id : get_current_user_id();
if ($user_id <= 0) { if ($user_id <= 0) {
@@ -299,12 +263,13 @@ class Signin {
} }
/** /**
* legacy checkin_details_modal lite HTML. * 签到详情弹窗 HTML(参考主题 zib_get_user_checkin_details_modal 1:1):
* 炫彩头部 + 用户卡片(我的积分/我的等级)+ 每日签到奖励卡 + 连续签到奖励 7 天迷你卡。
*/ */
public function render_details_modal_html(int $user_id): string { public function render_details_modal_html(int $user_id): string {
$user = get_userdata($user_id); $user = get_userdata($user_id);
if (!$user) { if (!$user) {
return '<div class="aksrt-checkin-modal"><p>' . esc_html__('用户不存在', 'aksrt') . '</p></div>'; return '<div class="modal-body"><p class="muted-color padding-10 text-center">' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
} }
$cfg = class_exists('\AKSRT\Settings\Settings') $cfg = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_signin_reward_settings() ? \AKSRT\Settings\Settings::get_signin_reward_settings()
@@ -313,87 +278,100 @@ class Signin {
$rec = $this->get_record($user_id, $today); $rec = $this->get_record($user_id, $today);
$streak = $this->compute_current_streak($user_id, $today); $streak = $this->compute_current_streak($user_id, $today);
$total = $this->get_total_days($user_id); $total = $this->get_total_days($user_id);
$balance = \AKSRT\Ledger\LedgerService::get_instance()->balance($user_id, \AKSRT\Ledger\AssetType::POINTS); $base = (int) ($cfg['base'] ?? 0);
$currency = class_exists('\AKSRT\Settings\Settings') $currency = class_exists('\AKSRT\Settings\Settings')
? \AKSRT\Settings\Settings::get_currency_name() ? \AKSRT\Settings\Settings::get_currency_name()
: __('积分', 'aksrt'); : __('积分', 'aksrt');
$base = (int) ($cfg['base'] ?? 0); $points_svg = static fn(string $class = 'icon'): string =>
$avatar = function_exists('aksrt_get_avatar_url') '<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-points"></use></svg>';
? aksrt_get_avatar_url($user_id, ['size' => 64]) $gift_svg = static fn(string $class = 'icon'): string =>
: get_avatar_url($user_id, ['size' => 64]); '<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-gift-color"></use></svg>';
$center = function_exists('aksrt_get_user_center_url') $signed_today = (bool) $rec;
? aksrt_get_user_center_url('assets')
: home_url('/');
$html = '<div class="aksrt-checkin-modal">'; // 炫彩头部
$html .= '<div class="aksrt-checkin-modal__head">'; $html = '<div class="modal-body">';
$html .= '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>'; $html .= '<div class="modal-colorful-header colorful-bg c-blue">';
$html .= '<strong>' . esc_html__('签到领奖励', 'aksrt') . '</strong>'; $html .= '<button class="close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '"><svg class="ic-close" aria-hidden="true"><use xlink:href="#icon-close"></use></svg></button>';
$html .= '<button type="button" class="aksrt-modal-runtime__close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button>'; $html .= '<div class="colorful-make"></div>';
$html .= '</div>'; $html .= '<div class="text-center">';
$html .= '<div class="em2x"><i class="fa fas fa-calendar-check" aria-hidden="true"></i></div>';
$html .= '<div class="aksrt-checkin-modal__user">'; $html .= '<div class="mt10 em12 padding-w10">' . esc_html__('签到领奖励', 'aksrt') . '</div>';
$html .= '<img class="aksrt-checkin-modal__avatar" src="' . esc_url($avatar) . '" alt="" width="40" height="40" />';
$html .= '<div class="aksrt-checkin-modal__user-meta">';
$html .= '<div class="aksrt-checkin-modal__name">' . esc_html($user->display_name) . '</div>';
$html .= '<div class="aksrt-checkin-modal__sub">' . esc_html(sprintf(__('累计签到 %d 天', 'aksrt'), $total)) . '</div>';
$html .= '</div></div>'; $html .= '</div></div>';
$html .= '<div class="aksrt-checkin-modal__wallet">'; // 用户卡片(头像/昵称 + 我的积分 + 我的等级)
$html .= '<a class="aksrt-checkin-modal__wallet-item" href="' . esc_url($center) . '">'; $points_svg_mini = $points_svg('px12 mr6 muted-2-color');
$html .= '<span class="muted">' . esc_html($currency) . '</span>'; $user_card = '<a rel="nofollow" class="block flex1 muted-box padding-10" href="' . esc_url(aksrt_get_user_center_url('credits')) . '">
$html .= '<b>' . esc_html(number_format_i18n($balance)) . '</b>'; <div class="muted-2-color em09 mb6">' . esc_html__('我的积分', 'aksrt') . '</div>
$html .= '</a></div>'; <div class="em12">' . $points_svg_mini . esc_html(number_format_i18n(aksrt_user_points($user_id))) . '<i class="muted-2-color px12 fa fas fa-angle-right ml6" aria-hidden="true"></i></div>
</a>';
$html .= '<div class="aksrt-checkin-modal__card">'; $level_s = class_exists('\AKSRT\User\Level')
$html .= '<div class="aksrt-checkin-modal__card-main">'; && \AKSRT\User\Level::settings()['enabled']
$html .= '<div class="aksrt-checkin-modal__card-title"><i class="fa fas fa-gift" aria-hidden="true"></i> ' . esc_html__('每日签到奖励', 'aksrt') . '</div>'; && \AKSRT\User\Level::settings()['public_display'];
$html .= '<div class="aksrt-checkin-modal__badge">' . esc_html($currency) . ' +' . esc_html((string) $base) . '</div>'; if ($level_s) {
$html .= '</div>'; $level_badge = \AKSRT\User\Level::get_badge_html($user_id, 'em12 mr10');
if ($rec) { $user_card .= '<a rel="nofollow" class="block flex1 muted-box padding-10 ml6" href="' . esc_url(aksrt_get_user_center_url('level')) . '">
$html .= '<div class="aksrt-checkin-modal__done"><i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html__('今日已签到', 'aksrt'); <div class="muted-2-color em09 mb6 flex jsb ac"><div>' . esc_html__('我的等级', 'aksrt') . '</div>' . $level_badge . '</div>
$html .= '<div class="em09">+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '</div></div>'; <div class="em12">' . esc_html(number_format_i18n(\AKSRT\User\Level::get_integral($user_id))) . '<span class="px12 ml6 muted-2-color">' . esc_html__('经验值', 'aksrt') . '<i class="fa fas fa-angle-right ml6" aria-hidden="true"></i></span></div>
</a>';
}
$checkin_all_day = $total ? '<div class="mb6 em09">' . sprintf(__('累计签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $total) . '</badge>') . '</div>' : '';
$avatar_img = aksrt_get_avatar_box($user_id, 'avatar-img avatar-mini mr6', false, true);
$html .= '<div class="mb20"><div class="flex ac jsb"><div class="mb10">' . $avatar_img . esc_html($user->display_name) . '</div>' . $checkin_all_day . '</div><div class="flex ab jsb">' . $user_card . '</div></div>';
// 每日签到奖励卡片
$reward_badge = $base ? '<badge class=" p2-10 mr6 c-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $base)) . '">' . $points_svg('icon') . '+' . esc_html((string) $base) . '</badge>' : '';
if ($signed_today) {
$checkin_btn = '<div class="em09 c-blue">
<div class="mb10"><i class="fa fas fa-calendar-check mr6" aria-hidden="true"></i>' . esc_html__('今日已签到', 'aksrt') . '</div>
<div class="px12">' . sprintf(__('积分', 'aksrt') . '+%d', (int) ($rec['reward'] ?? 0)) . '</div>
</div>';
} else { } else {
$html .= '<button type="button" class="but c-blue aksrt-checkin-modal__btn" data-signin-btn data-signin-idle-label="' . esc_attr__('立即签到', 'aksrt') . '" data-signin-done-label="' . esc_attr__('已签到', 'aksrt') . '"><span data-signin-label>' . esc_html__('立即签到', 'aksrt') . '</span></button>'; $checkin_btn = '<button type="button" class="but c-blue ml10 p2-10 radius" data-signin-btn'
. ' data-signin-idle-label="' . esc_attr__('立即签到', 'aksrt') . '"'
. ' data-signin-done-label="' . esc_attr__('今日已签到', 'aksrt') . '">'
. '<span data-signin-label>' . esc_html__('立即签到', 'aksrt') . '</span></button>';
} }
$html .= '</div>';
$card_1 = '<div class="zib-widget flex ac jsb">
<div class="mr10">
<div class="em09 mb10">' . $gift_svg('mr6 em12') . esc_html__('每日签到奖励', 'aksrt') . '</div>
<div class="flex ac jsb">' . $reward_badge . '</div>
</div>
' . $checkin_btn . '
</div>';
// 连续签到奖励卡片(参考主题固定 7 天)
$streak_table = is_array($cfg['streak'] ?? null) ? $cfg['streak'] : []; $streak_table = is_array($cfg['streak'] ?? null) ? $cfg['streak'] : [];
$max = 7; $reward_continuous_day_html = $streak > 1 ? '<div class="mb6 em09">' . sprintf(__('已连续签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $streak) . '</badge>') . '</div>' : '';
foreach (array_keys($streak_table) as $k) { $continuous_lists = '';
$max = max($max, (int) $k); $max_day = 7;
} for ($d = 1; $d <= $max_day; $d++) {
$max = min(14, max(2, $max));
$html .= '<div class="aksrt-checkin-modal__streak">';
$html .= '<div class="aksrt-checkin-modal__streak-head"><span>' . esc_html__('连续签到奖励', 'aksrt') . '</span>';
if ($streak > 1) {
$html .= '<badge class="c-blue">' . esc_html(sprintf(__('已连续 %d 天', 'aksrt'), $streak)) . '</badge>';
}
$html .= '</div><div class="aksrt-checkin-modal__ladder">';
for ($d = 1; $d <= $max; $d++) {
$extra = 0; $extra = 0;
foreach ($streak_table as $k => $v) { foreach ($streak_table as $k => $v) {
if ((int) $k === $d) { if ((int) $k === $d) {
$extra = (int) $v; $extra = (int) $v;
} }
} }
$pts = $base + $extra; $_points = $base + $extra;
$is_done = $streak >= $d; $is_ed = $signed_today && $streak >= $d;
$html .= '<div class="aksrt-checkin-modal__day' . ($is_done ? ' is-done' : '') . '">'; $_mini_card = $_points ? '<div class="badg badg-sm ' . ($is_ed ? 'b' : 'c') . '-green" data-toggle="tooltip" title="' . esc_attr(sprintf(__('积分奖励%s', 'aksrt'), $_points)) . '">' . $points_svg('icon') . '<br/>+' . esc_html((string) $_points) . '</div>' : '';
$html .= '<div class="aksrt-checkin-modal__day-n">D' . (int) $d . '</div>';
$html .= '<div class="aksrt-checkin-modal__day-p">+' . esc_html((string) $pts) . '</div>';
$html .= '</div>';
}
$html .= '</div></div>';
$dates = []; $continuous_lists .= '<div class="flex1 text-center" style="flex: 1;">
foreach ($this->get_recent_signins($user_id, 30) as $row) { <div class="mb6 checkin-mini-box">' . $_mini_card . '</div>
if (!empty($row['signin_date'])) { <div class="muted-2-color smail' . ($is_ed ? ' c-blue' : '') . '">' . ($is_ed ? '<i class="fa fas fa-check-circle" aria-hidden="true"></i>' : '') . sprintf(__('第%d天', 'aksrt'), $d) . '</div>
$dates[] = (string) $row['signin_date']; </div>';
}
} }
$html .= '<div class="aksrt-checkin-modal__cal-title">' . esc_html__('最近 30 天', 'aksrt') . '</div>';
$html .= '<div data-aksrt-signin-calendar=\'' . esc_attr(wp_json_encode(array_values($dates))) . '\'></div>'; $card_2 = '<div class="mt10">
<div class="flex ac jsb"><div class="em09 mb10">' . $gift_svg('mr6 em12') . esc_html__('连续签到奖励', 'aksrt') . '</div>' . $reward_continuous_day_html . '</div>
<div class="flex ac jsb">' . $continuous_lists . '</div>
</div>';
$html .= $card_1;
$html .= $card_2;
$html .= '</div>'; $html .= '</div>';
return $html; return $html;
} }
@@ -418,29 +396,6 @@ class Signin {
return $row ?: null; return $row ?: null;
} }
/**
* 获取最近 N 天的签到记录
*
* @param int $user_id
* @param int $days
* @return array
*/
public function get_recent_signins(int $user_id, int $days = 30): array {
global $wpdb;
$table = \AKSRT\Core\DBMigrator::table('signin_log');
if (!$table) return [];
$from_date = (new \DateTimeImmutable("-{$days} days", wp_timezone()))->format('Y-m-d');
$rows = $wpdb->get_results($wpdb->prepare(
"SELECT signin_date FROM {$table} WHERE user_id = %d AND signin_date >= %s ORDER BY signin_date ASC",
$user_id,
$from_date
), ARRAY_A);
return $rows ?: [];
}
/** /**
* 算"今天之前"的连续签到天数。 * 算"今天之前"的连续签到天数。
*/ */
@@ -460,11 +460,14 @@ test('profile routes load versioned leaf styles and keep dialogs hidden initiall
assert.match(userCenterCreditsCss, /#panel-credits\.dashboard-panel\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/); assert.match(userCenterCreditsCss, /#panel-credits\.dashboard-panel\s*\{[^}]*background:\s*transparent;[^}]*box-shadow:\s*none;/);
assert.match(userCenterCreditsCss, /\.uc-asset-tabs\s*\{[^}]*background:\s*var\(--surface-1,\s*#fff\);[^}]*border:[^;}]+;[^}]*box-shadow:[^;}]+;/); assert.match(userCenterCreditsCss, /\.uc-asset-tabs\s*\{[^}]*background:\s*var\(--surface-1,\s*#fff\);[^}]*border:[^;}]+;[^}]*box-shadow:[^;}]+;/);
assert.doesNotMatch(profileCredits, /credits-signin\.php|data-aksrt-signin-calendar/); assert.doesNotMatch(profileCredits, /credits-signin\.php|data-aksrt-signin-calendar/);
// 签到日历只存在于签到弹窗(src/Credits/Signin.php + enhancements2.js), // 签到详情弹窗为参考主题 1:1 结构(src/Credits/Signin.php),日历已随
// 用户中心里的孤儿渲染器已整包移除。 // 旧版 lite 弹窗一并移除;用户中心里的孤儿渲染器也已整包移除。
assert.equal(fs.existsSync(path.join(root, 'inc/helpers/user-center-renderers/credits-signin.php')), false); assert.equal(fs.existsSync(path.join(root, 'inc/helpers/user-center-renderers/credits-signin.php')), false);
assert.doesNotMatch(userCenterCreditsCss + read('assets/css/frontend/enhancements/components/custom-amount.css'), /uc-signin-calendar/); assert.doesNotMatch(userCenterCreditsCss + read('assets/css/frontend/enhancements/components/custom-amount.css'), /uc-signin-calendar/);
assert.match(read('src/Credits/Signin.php'), /data-aksrt-signin-calendar/); const signinModalHtml = read('src/Credits/Signin.php');
assert.match(signinModalHtml, /modal-colorful-header colorful-bg c-blue/);
assert.match(signinModalHtml, /checkin-mini-box/);
assert.doesNotMatch(signinModalHtml, /data-aksrt-signin-calendar/);
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden/); assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden/);
assert.doesNotMatch(profileOrders, /\$_GET|add_query_arg|name="order_(?:status|type|number|page)"/); assert.doesNotMatch(profileOrders, /\$_GET|add_query_arg|name="order_(?:status|type|number|page)"/);
assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden[\s\S]*?aksrt-orders-page__status-filters[\s\S]*?aksrt-orders-page__type-filters/); assert.match(profileOrders, /data-aksrt-orders-filter-panel hidden[\s\S]*?aksrt-orders-page__status-filters[\s\S]*?aksrt-orders-page__type-filters/);
+1 -1
View File
@@ -253,7 +253,7 @@ test('balance center actions use direct fixed-id notyf flows', () => {
assert.match(headerUserbox, /data-signin-btn[\s\S]*?data-signin-label/); assert.match(headerUserbox, /data-signin-btn[\s\S]*?data-signin-label/);
assert.match(headerUserbox, /add_action\('wp_footer', 'aksrt_render_signin_captcha'\)/); assert.match(headerUserbox, /add_action\('wp_footer', 'aksrt_render_signin_captcha'\)/);
assert.match(headerUserbox, /data-signin-captcha[\s\S]*?aksrt_render_captcha\('signin'\)/); assert.match(headerUserbox, /data-signin-captcha[\s\S]*?aksrt_render_captcha\('signin'\)/);
assert.match(signinService, /aksrt-checkin-modal__btn[^>]+data-signin-btn/); assert.match(signinService, /but c-blue ml10 p2-10 radius" data-signin-btn/);
assert.doesNotMatch(headerUserbox + signinService, /data-aksrt-checkin/); assert.doesNotMatch(headerUserbox + signinService, /data-aksrt-checkin/);
assert.match(context, /'signin_enabled'\s*=>\s*\$signin_enabled/); assert.match(context, /'signin_enabled'\s*=>\s*\$signin_enabled/);
assert.match(context, /'signin_signed_today'\s*=>\s*\$signin_signed_today/); assert.match(context, /'signin_signed_today'\s*=>\s*\$signin_signed_today/);