feat(credits): check-in details modal + history; oneline/tilt widgets

Add zibll-like check-in details remote modal (REST/AJAX), history
endpoint, header details link, and keep oneline posts + tilt showcase
widgets from this batch.
This commit is contained in:
2026-07-19 00:48:46 +08:00
parent 3efcc440c4
commit be4f23fd6f
5 changed files with 331 additions and 6 deletions
+121
View File
@@ -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; }
+4
View File
@@ -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);
});
});
// 暴露到全局供动态内容使用
+17 -2
View File
@@ -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 '<button type="button" class="but block c-yellow aksrt-header-checkin is-done" disabled>'
. '<i class="fa fa-calendar-check-o" aria-hidden="true"></i> '
. esc_html__('今日已签到', 'aksrt')
. '</button>';
. '</button>'
. $details;
}
return '<button type="button" class="but block c-yellow aksrt-header-checkin" data-aksrt-checkin="1">'
. '<i class="fa fa-calendar-check-o" aria-hidden="true"></i> '
. esc_html__('签到领取今日奖励', 'aksrt')
. '</button>';
. '</button>'
. $details;
}
endif;
@@ -30,9 +30,6 @@ if (class_exists('\AKSRT\Credits\Signin')) {
}, $signin_log));
}
if (empty($signin_dates)) {
return;
}
?>
<div class="uc-signin-calendar" style="margin-bottom:16px;">
<h4 class="uc-signin-calendar__title">
+189 -1
View File
@@ -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 '<div class="aksrt-checkin-modal"><p>' . esc_html__('用户不存在', 'aksrt') . '</p></div>';
}
$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 = '<div class="aksrt-checkin-modal">';
$html .= '<div class="aksrt-checkin-modal__head">';
$html .= '<i class="fa fa-calendar-check-o" aria-hidden="true"></i>';
$html .= '<strong>' . esc_html__('签到领奖励', 'aksrt') . '</strong>';
$html .= '<button type="button" class="aksrt-remote-modal__close" data-aksrt-remote-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></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>';
$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="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 fa-calendar-check-o" aria-hidden="true"></i> ' . esc_html__('今日已签到', 'aksrt');
$html .= '<div class="em09">+' . esc_html((string) (int) ($rec['reward'] ?? 0)) . '</div></div>';
} else {
$html .= '<button type="button" class="but c-blue aksrt-checkin-modal__btn" data-aksrt-checkin="1">' . esc_html__('立即签到', 'aksrt') . '</button>';
}
$html .= '</div>';
$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++) {
$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>';
$dates = [];
foreach ($this->get_recent_signins($user_id, 30) as $row) {
if (!empty($row['signin_date'])) {
$dates[] = (string) $row['signin_date'];
}
}
$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>';
$html .= '</div>';
return $html;
}
// ---------- 工具方法 ----------
/** 站点时区下的今日日期 'Y-m-d' */