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 .= '
' . esc_html($user->display_name) . '
'; + $html .= '
' . esc_html(sprintf(__('累计签到 %d 天', 'aksrt'), $total)) . '
'; + $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' */