diff --git a/assets/css/frontend/widget-batch3f.css b/assets/css/frontend/widget-batch3f.css
index 5ad9a74d..9f1b0d1d 100644
--- a/assets/css/frontend/widget-batch3f.css
+++ b/assets/css/frontend/widget-batch3f.css
@@ -155,3 +155,124 @@
}
.aksrt-tilt__img img { width: 100%; height: 100%; object-fit: cover; display: block; }
.aksrt-tilt__img.is-empty { background: linear-gradient(135deg, #dbeafe, #ede9fe); }
+
+
+/* Check-in details modal */
+.aksrt-checkin-modal { padding: 4px 2px 8px; }
+.aksrt-checkin-modal__head {
+ display: flex;
+ align-items: center;
+ gap: 8px;
+ font-size: 16px;
+ margin-bottom: 14px;
+}
+.aksrt-checkin-modal__head .aksrt-remote-modal__close {
+ margin-left: auto;
+ border: 0;
+ background: transparent;
+ font-size: 22px;
+ line-height: 1;
+ cursor: pointer;
+ opacity: .55;
+}
+.aksrt-checkin-modal__user {
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 12px;
+}
+.aksrt-checkin-modal__avatar {
+ width: 40px;
+ height: 40px;
+ border-radius: 50%;
+ object-fit: cover;
+}
+.aksrt-checkin-modal__name { font-weight: 700; }
+.aksrt-checkin-modal__sub { font-size: 12px; color: var(--muted-2-color, #888); }
+.aksrt-checkin-modal__wallet { margin-bottom: 12px; }
+.aksrt-checkin-modal__wallet-item {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ padding: 10px 12px;
+ border-radius: 10px;
+ background: var(--surface-2, #f3f4f6);
+ text-decoration: none;
+ color: inherit;
+}
+.aksrt-checkin-modal__card {
+ display: flex;
+ align-items: center;
+ justify-content: space-between;
+ gap: 12px;
+ padding: 12px;
+ border-radius: 12px;
+ border: 1px solid var(--color-line, #e5e7eb);
+ margin-bottom: 14px;
+}
+.aksrt-checkin-modal__card-title { font-size: 13px; margin-bottom: 6px; }
+.aksrt-checkin-modal__badge {
+ display: inline-block;
+ font-size: 12px;
+ padding: 2px 8px;
+ border-radius: 999px;
+ background: #ecfdf5;
+ color: #059669;
+}
+.aksrt-checkin-modal__done { text-align: right; color: #2563eb; font-size: 13px; }
+.aksrt-checkin-modal__btn { white-space: nowrap; }
+.aksrt-checkin-modal__streak-head {
+ display: flex;
+ justify-content: space-between;
+ align-items: center;
+ font-size: 13px;
+ margin-bottom: 8px;
+}
+.aksrt-checkin-modal__ladder {
+ display: grid;
+ grid-template-columns: repeat(7, minmax(0, 1fr));
+ gap: 6px;
+ margin-bottom: 14px;
+}
+.aksrt-checkin-modal__day {
+ text-align: center;
+ border-radius: 8px;
+ padding: 8px 2px;
+ background: var(--surface-2, #f8fafc);
+ border: 1px solid transparent;
+ font-size: 11px;
+}
+.aksrt-checkin-modal__day.is-done {
+ background: #eff6ff;
+ border-color: #bfdbfe;
+ color: #1d4ed8;
+}
+.aksrt-checkin-modal__day-n { font-weight: 700; margin-bottom: 2px; }
+.aksrt-checkin-modal__cal-title {
+ font-size: 13px;
+ margin-bottom: 8px;
+}
+.aksrt-signin-calendar__grid {
+ display: grid;
+ grid-template-columns: repeat(10, minmax(0, 1fr));
+ gap: 4px;
+}
+.aksrt-signin-calendar__day {
+ aspect-ratio: 1;
+ border-radius: 6px;
+ display: flex;
+ align-items: center;
+ justify-content: center;
+ font-size: 11px;
+ background: var(--surface-2, #f3f4f6);
+ color: #999;
+}
+.aksrt-signin-calendar__day.is-signed {
+ background: #dbeafe;
+ color: #1d4ed8;
+ font-weight: 700;
+}
+.aksrt-signin-calendar__day.is-today {
+ outline: 1px solid var(--color-primary, #3b82f6);
+}
+.aksrt-header-checkin-details { text-decoration: none; }
diff --git a/assets/js/enhancements2.js b/assets/js/enhancements2.js
index 30a136a4..110ef7c3 100644
--- a/assets/js/enhancements2.js
+++ b/assets/js/enhancements2.js
@@ -302,6 +302,10 @@
initParallax();
initFloatLabels();
initSigninCalendar();
+ document.addEventListener('aksrt:remote-modal:open', function () {
+ // Remote modal may inject calendar markup after open.
+ window.setTimeout(initSigninCalendar, 30);
+ });
});
// 暴露到全局供动态内容使用
diff --git a/inc/helpers/aksrt-header-userbox.php b/inc/helpers/aksrt-header-userbox.php
index 442ec454..0f293fa9 100644
--- a/inc/helpers/aksrt-header-userbox.php
+++ b/inc/helpers/aksrt-header-userbox.php
@@ -39,17 +39,32 @@ if (!function_exists('aksrt_header_user_checkin_btn')) :
$signed = false;
}
+ $details = '';
+ if (function_exists('aksrt_get_remote_modal_link')) {
+ $details = aksrt_get_remote_modal_link([
+ 'tag' => 'a',
+ 'class' => 'aksrt-header-checkin-details muted-2-color em09 mt6 block text-center',
+ 'text' => __('签到详情', 'aksrt'),
+ 'query_arg' => ['action' => 'aksrt_checkin_details_modal'],
+ 'data_class' => 'modal-mini full-sm',
+ 'height' => 420,
+ 'mobile_bottom' => true,
+ ]);
+ }
+
if ($signed) {
return '';
+ . ''
+ . $details;
}
return '';
+ . ''
+ . $details;
}
endif;
diff --git a/inc/helpers/user-center-renderers/credits-signin.php b/inc/helpers/user-center-renderers/credits-signin.php
index 5a9973de..335bfc2c 100644
--- a/inc/helpers/user-center-renderers/credits-signin.php
+++ b/inc/helpers/user-center-renderers/credits-signin.php
@@ -30,9 +30,6 @@ if (class_exists('\AKSRT\Credits\Signin')) {
}, $signin_log));
}
-if (empty($signin_dates)) {
- return;
-}
?>
diff --git a/src/AKSRT/Credits/Signin.php b/src/AKSRT/Credits/Signin.php
index ccb28e45..fca51f9c 100644
--- a/src/AKSRT/Credits/Signin.php
+++ b/src/AKSRT/Credits/Signin.php
@@ -20,6 +20,7 @@ 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 {
@@ -37,6 +38,20 @@ class Signin {
return is_user_logged_in();
},
]);
+ register_rest_route('aksrt/v1', '/signin/history', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_history'],
+ 'permission_callback' => function () {
+ return is_user_logged_in();
+ },
+ ]);
+ register_rest_route('aksrt/v1', '/signin/details', [
+ 'methods' => 'GET',
+ 'callback' => [$this, 'rest_details_html'],
+ 'permission_callback' => function () {
+ return is_user_logged_in();
+ },
+ ]);
}
/** GET /signin/today */
@@ -141,14 +156,187 @@ class Signin {
}
}
+ // Keep lightweight meta for header/badge consumers (zibll checkin_all_day).
+ update_user_meta($user_id, 'aksrt_signin_' . $today_str, 1);
+ $all_day = (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true);
+ update_user_meta($user_id, 'aksrt_checkin_all_day', $all_day + 1);
+ update_user_meta($user_id, 'aksrt_checkin_continuous_day', $new_streak);
+
return new \WP_REST_Response([
'success' => true,
+ 'message' => sprintf(
+ /* translators: 1: reward points 2: streak days */
+ __('签到成功,积分 +%1$d(连续 %2$d 天)', 'aksrt'),
+ (int) $reward,
+ (int) $new_streak
+ ),
'streak' => $new_streak,
'reward' => $reward,
- 'balance' => class_exists('AKSRT\\Credits\\Credits') ? Credits::balance($user_id) : 0,
+ 'balance' => class_exists('AKSRT\Credits\Credits') ? Credits::balance($user_id) : 0,
], 200);
}
+ /** 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) {
+ return false;
+ }
+ $today = $this->today_date();
+ if ((bool) get_user_meta($user_id, 'aksrt_signin_' . $today, true)) {
+ return true;
+ }
+ return (bool) $this->get_record($user_id, $today);
+ }
+
+ public function get_total_days(int $user_id): int {
+ global $wpdb;
+ $table = \AKSRT\Core\DBMigrator::table('signin_log');
+ if (!$table) {
+ return (int) get_user_meta($user_id, 'aksrt_checkin_all_day', true);
+ }
+ $n = (int) $wpdb->get_var($wpdb->prepare(
+ "SELECT COUNT(*) FROM {$table} WHERE user_id = %d",
+ $user_id
+ ));
+ return $n;
+ }
+
+ /**
+ * zibll checkin_details_modal lite HTML.
+ */
+ public function render_details_modal_html(int $user_id): string {
+ $user = get_userdata($user_id);
+ if (!$user) {
+ return '' . esc_html__('用户不存在', 'aksrt') . '
';
+ }
+ $cfg = class_exists('\AKSRT\Settings\Settings')
+ ? \AKSRT\Settings\Settings::get_signin_reward_settings()
+ : ['enabled' => false, 'base' => 0, 'streak' => []];
+ $today = $this->today_date();
+ $rec = $this->get_record($user_id, $today);
+ $streak = $this->compute_current_streak($user_id, $today);
+ $total = $this->get_total_days($user_id);
+ $balance = class_exists('\AKSRT\Credits\Credits') ? (int) Credits::balance($user_id) : 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('/');
+
+ $html = '';
+ $html .= '
';
+ $html .= '';
+ $html .= '' . esc_html__('签到领奖励', 'aksrt') . '';
+ $html .= '';
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
 . ')
';
+ $html .= '
';
+
+ $html .= '
';
+
+ $html .= '
';
+ $html .= '
';
+ $html .= '
' . esc_html__('每日签到奖励', 'aksrt') . '
';
+ $html .= '
' . esc_html($currency) . ' +' . esc_html((string) $base) . '
';
+ $html .= '
';
+ if ($rec) {
+ $html .= '
' . esc_html__('今日已签到', 'aksrt');
+ $html .= '
+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '
';
+ } else {
+ $html .= '
';
+ }
+ $html .= '
';
+
+ $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 .= '
';
+ $html .= '
' . esc_html__('连续签到奖励', 'aksrt') . '';
+ if ($streak > 1) {
+ $html .= '' . esc_html(sprintf(__('已连续 %d 天', 'aksrt'), $streak)) . '';
+ }
+ $html .= '
';
+ for ($d = 1; $d <= $max; $d++) {
+ $extra = 0;
+ foreach ($streak_table as $k => $v) {
+ if ((int) $k === $d) {
+ $extra = (int) $v;
+ }
+ }
+ $pts = $base + $extra;
+ $is_done = $streak >= $d;
+ $html .= '
';
+ $html .= '
D' . (int) $d . '
';
+ $html .= '
+' . esc_html((string) $pts) . '
';
+ $html .= '
';
+ }
+ $html .= '
';
+
+ $dates = [];
+ foreach ($this->get_recent_signins($user_id, 30) as $row) {
+ if (!empty($row['signin_date'])) {
+ $dates[] = (string) $row['signin_date'];
+ }
+ }
+ $html .= '
' . esc_html__('最近 30 天', 'aksrt') . '
';
+ $html .= '
';
+ $html .= '
';
+ return $html;
+ }
+
// ---------- 工具方法 ----------
/** 站点时区下的今日日期 'Y-m-d' */