/* Copyright (C) 2025 QuantumNous This program is free software: you can redistribute it and/or modify it under the terms of the GNU Affero General Public License as published by the Free Software Foundation, either version 3 of the License, or (at your option) any later version. This program is distributed in the hope that it will be useful, but WITHOUT ANY WARRANTY; without even the implied warranty of MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU Affero General Public License for more details. You should have received a copy of the GNU Affero General Public License along with this program. If not, see . For commercial licensing, please contact support@quantumnous.com */ import React, { useState, useEffect, useMemo } from 'react'; import { Card, Calendar, Button, Typography, Avatar, Spin, Tooltip, Collapsible, Modal, } from '@douyinfe/semi-ui'; import { CalendarCheck, Gift, Check, ChevronDown, ChevronUp, } from 'lucide-react'; import Turnstile from 'react-turnstile'; import { API, showError, showSuccess, renderQuota } from '../../../../helpers'; const CheckinCalendar = ({ t, status, turnstileEnabled, turnstileSiteKey }) => { const [loading, setLoading] = useState(false); const [checkinLoading, setCheckinLoading] = useState(false); const [turnstileModalVisible, setTurnstileModalVisible] = useState(false); const [turnstileWidgetKey, setTurnstileWidgetKey] = useState(0); const [checkinData, setCheckinData] = useState({ enabled: false, stats: { checked_in_today: false, total_checkins: 0, total_quota: 0, checkin_count: 0, records: [], }, }); const [currentMonth, setCurrentMonth] = useState( new Date().toISOString().slice(0, 7), ); // 初始加载状态,用于避免折叠状态闪烁 const [initialLoaded, setInitialLoaded] = useState(false); // 折叠状态:null 表示未确定(等待首次加载) const [isCollapsed, setIsCollapsed] = useState(null); // 创建日期到额度的映射,方便快速查找 const checkinRecordsMap = useMemo(() => { const map = {}; const records = checkinData.stats?.records || []; records.forEach((record) => { map[record.checkin_date] = record.quota_awarded; }); return map; }, [checkinData.stats?.records]); // 计算本月获得的额度 const monthlyQuota = useMemo(() => { const records = checkinData.stats?.records || []; return records.reduce( (sum, record) => sum + (record.quota_awarded || 0), 0, ); }, [checkinData.stats?.records]); // 获取签到状态 const fetchCheckinStatus = async (month) => { const isFirstLoad = !initialLoaded; setLoading(true); try { const res = await API.get(`/api/user/checkin?month=${month}`); const { success, data, message } = res.data; if (success) { setCheckinData(data); // 首次加载时,根据签到状态设置折叠状态 if (isFirstLoad) { setIsCollapsed(data.stats?.checked_in_today ?? false); setInitialLoaded(true); } } else { showError(message || t('获取签到状态失败')); if (isFirstLoad) { setIsCollapsed(false); setInitialLoaded(true); } } } catch (error) { showError(t('获取签到状态失败')); if (isFirstLoad) { setIsCollapsed(false); setInitialLoaded(true); } } finally { setLoading(false); } }; const postCheckin = async (token) => { const url = token ? `/api/user/checkin?turnstile=${encodeURIComponent(token)}` : '/api/user/checkin'; return API.post(url); }; const shouldTriggerTurnstile = (message) => { if (!turnstileEnabled) return false; if (typeof message !== 'string') return true; return message.includes('Turnstile'); }; const doCheckin = async (token) => { setCheckinLoading(true); try { const res = await postCheckin(token); const { success, data, message } = res.data; if (success) { showSuccess( t('签到成功!获得') + ' ' + renderQuota(data.quota_awarded), ); // 刷新签到状态 fetchCheckinStatus(currentMonth); setTurnstileModalVisible(false); } else { if (!token && shouldTriggerTurnstile(message)) { if (!turnstileSiteKey) { showError('Turnstile is enabled but site key is empty.'); return; } setTurnstileModalVisible(true); return; } if (token && shouldTriggerTurnstile(message)) { setTurnstileWidgetKey((v) => v + 1); } showError(message || t('签到失败')); } } catch (error) { showError(t('签到失败')); } finally { setCheckinLoading(false); } }; useEffect(() => { if (status?.checkin_enabled) { fetchCheckinStatus(currentMonth); } }, [status?.checkin_enabled, currentMonth]); // 如果签到功能未启用,不显示组件 if (!status?.checkin_enabled) { return null; } // 日期渲染函数 - 显示签到状态和获得的额度 const dateRender = (dateString) => { // Semi Calendar 传入的 dateString 是 Date.toString() 格式 // 需要转换为 YYYY-MM-DD 格式来匹配后端数据 const date = new Date(dateString); if (isNaN(date.getTime())) { return null; } // 使用本地时间格式化,避免时区问题 const year = date.getFullYear(); const month = String(date.getMonth() + 1).padStart(2, '0'); const day = String(date.getDate()).padStart(2, '0'); const formattedDate = `${year}-${month}-${day}`; // YYYY-MM-DD const quotaAwarded = checkinRecordsMap[formattedDate]; const isCheckedIn = quotaAwarded !== undefined; if (isCheckedIn) { return (
{renderQuota(quotaAwarded)}
); } return null; }; // 处理月份变化 const handleMonthChange = (date) => { const month = date.toISOString().slice(0, 7); setCurrentMonth(month); }; return ( { setTurnstileModalVisible(false); setTurnstileWidgetKey((v) => v + 1); }} >
{ doCheckin(token); }} onExpire={() => { setTurnstileWidgetKey((v) => v + 1); }} />
{/* 卡片头部 */}
setIsCollapsed(!isCollapsed)} >
{t('每日签到')} {isCollapsed ? ( ) : ( )}
{!initialLoaded ? t('正在加载签到状态...') : checkinData.stats?.checked_in_today ? t('今日已签到,累计签到') + ` ${checkinData.stats?.total_checkins || 0} ` + t('天') : t('每日签到可获得随机额度奖励')}
{/* 可折叠内容 */} {/* 签到统计 */}
{checkinData.stats?.total_checkins || 0}
{t('累计签到')}
{renderQuota(monthlyQuota, 6)}
{t('本月获得')}
{renderQuota(checkinData.stats?.total_quota || 0, 6)}
{t('累计获得')}
{/* 签到日历 - 使用更紧凑的样式 */}
dateRender(dateString)} />
{/* 签到说明 */}
  • {t('每日签到可获得随机额度奖励')}
  • {t('签到奖励将直接添加到您的账户余额')}
  • {t('每日仅可签到一次,请勿重复签到')}
); }; export default CheckinCalendar;