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:
kate522 committed 2026-09-19 14:49:58 +08:00
1 parent c289016324
commit 4653128a18
15 files changed
+211 -266

No files matched your search

+2
View File
@@ -461,6 +461,8 @@ trait ScriptsPart1c {
'nonce' => wp_create_nonce('aksrt_public_nonce'),
'restNonce' => wp_create_nonce('wp_rest'),
'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'),
'siteUrl' => home_url('/'),
'themeUrl' => AKSRT_URI,
+78 -123
View File
@@ -20,7 +20,6 @@ class Signin {
public function init(): void {
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 {
@@ -34,11 +33,6 @@ class Signin {
'callback' => [$this, 'rest_signin'],
'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', [
'methods' => 'GET',
'callback' => [$this, 'rest_details_html'],
@@ -237,42 +231,12 @@ class Signin {
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. */
public function rest_details_html(\WP_REST_Request $request) {
$html = $this->render_details_modal_html(get_current_user_id());
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 {
$user_id = $user_id > 0 ? $user_id : get_current_user_id();
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 {
$user = get_userdata($user_id);
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')
? \AKSRT\Settings\Settings::get_signin_reward_settings()
@@ -313,87 +278,100 @@ class Signin {
$rec = $this->get_record($user_id, $today);
$streak = $this->compute_current_streak($user_id, $today);
$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')
? \AKSRT\Settings\Settings::get_currency_name()
: __('积分', 'aksrt');
$base = (int) ($cfg['base'] ?? 0);
$avatar = function_exists('aksrt_get_avatar_url')
? aksrt_get_avatar_url($user_id, ['size' => 64])
: get_avatar_url($user_id, ['size' => 64]);
$center = function_exists('aksrt_get_user_center_url')
? aksrt_get_user_center_url('assets')
: home_url('/');
$points_svg = static fn(string $class = 'icon'): string =>
'<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-points"></use></svg>';
$gift_svg = static fn(string $class = 'icon'): string =>
'<svg class="' . esc_attr($class) . '" aria-hidden="true"><use xlink:href="#icon-gift-color"></use></svg>';
$signed_today = (bool) $rec;
$html = '<div class="aksrt-checkin-modal">';
$html .= '<div class="aksrt-checkin-modal__head">';
$html .= '<i class="fa fas fa-calendar-check" aria-hidden="true"></i>';
$html .= '<strong>' . esc_html__('签到领奖励', 'aksrt') . '</strong>';
$html .= '<button type="button" class="aksrt-modal-runtime__close" data-aksrt-modal-close aria-label="' . esc_attr__('关闭', 'aksrt') . '">&times;</button>';
$html .= '</div>';
$html .= '<div class="aksrt-checkin-modal__user">';
$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 class="modal-body">';
$html .= '<div class="modal-colorful-header colorful-bg c-blue">';
$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 .= '<div class="colorful-make"></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="mt10 em12 padding-w10">' . esc_html__('签到领奖励', 'aksrt') . '</div>';
$html .= '</div></div>';
$html .= '<div class="aksrt-checkin-modal__wallet">';
$html .= '<a class="aksrt-checkin-modal__wallet-item" href="' . esc_url($center) . '">';
$html .= '<span class="muted">' . esc_html($currency) . '</span>';
$html .= '<b>' . esc_html(number_format_i18n($balance)) . '</b>';
$html .= '</a></div>';
// 用户卡片(头像/昵称 + 我的积分 + 我的等级)
$points_svg_mini = $points_svg('px12 mr6 muted-2-color');
$user_card = '<a rel="nofollow" class="block flex1 muted-box padding-10" href="' . esc_url(aksrt_get_user_center_url('credits')) . '">
<div class="muted-2-color em09 mb6">' . esc_html__('我的积分', 'aksrt') . '</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">';
$html .= '<div class="aksrt-checkin-modal__card-main">';
$html .= '<div class="aksrt-checkin-modal__card-title"><i class="fa fas fa-gift" aria-hidden="true"></i> ' . esc_html__('每日签到奖励', 'aksrt') . '</div>';
$html .= '<div class="aksrt-checkin-modal__badge">' . esc_html($currency) . ' +' . esc_html((string) $base) . '</div>';
$html .= '</div>';
if ($rec) {
$html .= '<div class="aksrt-checkin-modal__done"><i class="fa fas fa-calendar-check" aria-hidden="true"></i> ' . esc_html__('今日已签到', 'aksrt');
$html .= '<div class="em09">+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '</div></div>';
$level_s = class_exists('\AKSRT\User\Level')
&& \AKSRT\User\Level::settings()['enabled']
&& \AKSRT\User\Level::settings()['public_display'];
if ($level_s) {
$level_badge = \AKSRT\User\Level::get_badge_html($user_id, 'em12 mr10');
$user_card .= '<a rel="nofollow" class="block flex1 muted-box padding-10 ml6" href="' . esc_url(aksrt_get_user_center_url('level')) . '">
<div class="muted-2-color em09 mb6 flex jsb ac"><div>' . esc_html__('我的等级', 'aksrt') . '</div>' . $level_badge . '</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 {
$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'] : [];
$max = 7;
foreach (array_keys($streak_table) as $k) {
$max = max($max, (int) $k);
}
$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++) {
$reward_continuous_day_html = $streak > 1 ? '<div class="mb6 em09">' . sprintf(__('已连续签到%s天', 'aksrt'), '<badge class="c-blue">' . esc_html((string) $streak) . '</badge>') . '</div>' : '';
$continuous_lists = '';
$max_day = 7;
for ($d = 1; $d <= $max_day; $d++) {
$extra = 0;
foreach ($streak_table as $k => $v) {
if ((int) $k === $d) {
$extra = (int) $v;
}
}
$pts = $base + $extra;
$is_done = $streak >= $d;
$html .= '<div class="aksrt-checkin-modal__day' . ($is_done ? ' is-done' : '') . '">';
$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>';
$_points = $base + $extra;
$is_ed = $signed_today && $streak >= $d;
$_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>' : '';
$dates = [];
foreach ($this->get_recent_signins($user_id, 30) as $row) {
if (!empty($row['signin_date'])) {
$dates[] = (string) $row['signin_date'];
}
$continuous_lists .= '<div class="flex1 text-center" style="flex: 1;">
<div class="mb6 checkin-mini-box">' . $_mini_card . '</div>
<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>
</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>';
return $html;
}
@@ -418,29 +396,6 @@ class Signin {
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 ?: [];
}
/**
* 算"今天之前"的连续签到天数。
*/