feat(checkin): add check-in functionality with status retrieval and user quota rewards
This commit is contained in:
@@ -0,0 +1,72 @@
|
|||||||
|
package controller
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"net/http"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/QuantumNous/new-api/logger"
|
||||||
|
"github.com/QuantumNous/new-api/model"
|
||||||
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
// GetCheckinStatus 获取用户签到状态和历史记录
|
||||||
|
func GetCheckinStatus(c *gin.Context) {
|
||||||
|
setting := operation_setting.GetCheckinSetting()
|
||||||
|
if !setting.Enabled {
|
||||||
|
common.ApiErrorMsg(c, "签到功能未启用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
// 获取月份参数,默认为当前月份
|
||||||
|
month := c.DefaultQuery("month", time.Now().Format("2006-01"))
|
||||||
|
|
||||||
|
stats, err := model.GetUserCheckinStats(userId, month)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"data": gin.H{
|
||||||
|
"enabled": setting.Enabled,
|
||||||
|
"min_quota": setting.MinQuota,
|
||||||
|
"max_quota": setting.MaxQuota,
|
||||||
|
"stats": stats,
|
||||||
|
},
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// DoCheckin 执行用户签到
|
||||||
|
func DoCheckin(c *gin.Context) {
|
||||||
|
setting := operation_setting.GetCheckinSetting()
|
||||||
|
if !setting.Enabled {
|
||||||
|
common.ApiErrorMsg(c, "签到功能未启用")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userId := c.GetInt("id")
|
||||||
|
|
||||||
|
checkin, err := model.UserCheckin(userId)
|
||||||
|
if err != nil {
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": false,
|
||||||
|
"message": err.Error(),
|
||||||
|
})
|
||||||
|
return
|
||||||
|
}
|
||||||
|
model.RecordLog(userId, model.LogTypeSystem, fmt.Sprintf("用户签到,获得额度 %s", logger.LogQuota(checkin.QuotaAwarded)))
|
||||||
|
c.JSON(http.StatusOK, gin.H{
|
||||||
|
"success": true,
|
||||||
|
"message": "签到成功",
|
||||||
|
"data": gin.H{
|
||||||
|
"quota_awarded": checkin.QuotaAwarded,
|
||||||
|
"checkin_date": checkin.CheckinDate},
|
||||||
|
})
|
||||||
|
}
|
||||||
@@ -114,6 +114,7 @@ func GetStatus(c *gin.Context) {
|
|||||||
"setup": constant.Setup,
|
"setup": constant.Setup,
|
||||||
"user_agreement_enabled": legalSetting.UserAgreement != "",
|
"user_agreement_enabled": legalSetting.UserAgreement != "",
|
||||||
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
|
"privacy_policy_enabled": legalSetting.PrivacyPolicy != "",
|
||||||
|
"checkin_enabled": operation_setting.GetCheckinSetting().Enabled,
|
||||||
}
|
}
|
||||||
|
|
||||||
// 根据启用状态注入可选内容
|
// 根据启用状态注入可选内容
|
||||||
|
|||||||
@@ -0,0 +1,179 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"math/rand"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/QuantumNous/new-api/common"
|
||||||
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Checkin 签到记录
|
||||||
|
type Checkin struct {
|
||||||
|
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
UserId int `json:"user_id" gorm:"not null;uniqueIndex:idx_user_checkin_date"`
|
||||||
|
CheckinDate string `json:"checkin_date" gorm:"type:varchar(10);not null;uniqueIndex:idx_user_checkin_date"` // 格式: YYYY-MM-DD
|
||||||
|
QuotaAwarded int `json:"quota_awarded" gorm:"not null"`
|
||||||
|
CreatedAt int64 `json:"created_at" gorm:"bigint"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// CheckinRecord 用于API返回的签到记录(不包含敏感字段)
|
||||||
|
type CheckinRecord struct {
|
||||||
|
CheckinDate string `json:"checkin_date"`
|
||||||
|
QuotaAwarded int `json:"quota_awarded"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (Checkin) TableName() string {
|
||||||
|
return "checkins"
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserCheckinRecords 获取用户在指定日期范围内的签到记录
|
||||||
|
func GetUserCheckinRecords(userId int, startDate, endDate string) ([]Checkin, error) {
|
||||||
|
var records []Checkin
|
||||||
|
err := DB.Where("user_id = ? AND checkin_date >= ? AND checkin_date <= ?",
|
||||||
|
userId, startDate, endDate).
|
||||||
|
Order("checkin_date DESC").
|
||||||
|
Find(&records).Error
|
||||||
|
return records, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// HasCheckedInToday 检查用户今天是否已签到
|
||||||
|
func HasCheckedInToday(userId int) (bool, error) {
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
var count int64
|
||||||
|
err := DB.Model(&Checkin{}).
|
||||||
|
Where("user_id = ? AND checkin_date = ?", userId, today).
|
||||||
|
Count(&count).Error
|
||||||
|
return count > 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// UserCheckin 执行用户签到
|
||||||
|
// MySQL 和 PostgreSQL 使用事务保证原子性
|
||||||
|
// SQLite 不支持嵌套事务,使用顺序操作 + 手动回滚
|
||||||
|
func UserCheckin(userId int) (*Checkin, error) {
|
||||||
|
setting := operation_setting.GetCheckinSetting()
|
||||||
|
if !setting.Enabled {
|
||||||
|
return nil, errors.New("签到功能未启用")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查今天是否已签到
|
||||||
|
hasChecked, err := HasCheckedInToday(userId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if hasChecked {
|
||||||
|
return nil, errors.New("今日已签到")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 计算随机额度奖励
|
||||||
|
quotaAwarded := setting.MinQuota
|
||||||
|
if setting.MaxQuota > setting.MinQuota {
|
||||||
|
quotaAwarded = setting.MinQuota + rand.Intn(setting.MaxQuota-setting.MinQuota+1)
|
||||||
|
}
|
||||||
|
|
||||||
|
today := time.Now().Format("2006-01-02")
|
||||||
|
checkin := &Checkin{
|
||||||
|
UserId: userId,
|
||||||
|
CheckinDate: today,
|
||||||
|
QuotaAwarded: quotaAwarded,
|
||||||
|
CreatedAt: time.Now().Unix(),
|
||||||
|
}
|
||||||
|
|
||||||
|
// 根据数据库类型选择不同的策略
|
||||||
|
if common.UsingSQLite {
|
||||||
|
// SQLite 不支持嵌套事务,使用顺序操作 + 手动回滚
|
||||||
|
return userCheckinWithoutTransaction(checkin, userId, quotaAwarded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// MySQL 和 PostgreSQL 支持事务,使用事务保证原子性
|
||||||
|
return userCheckinWithTransaction(checkin, userId, quotaAwarded)
|
||||||
|
}
|
||||||
|
|
||||||
|
// userCheckinWithTransaction 使用事务执行签到(适用于 MySQL 和 PostgreSQL)
|
||||||
|
func userCheckinWithTransaction(checkin *Checkin, userId int, quotaAwarded int) (*Checkin, error) {
|
||||||
|
err := DB.Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 步骤1: 创建签到记录
|
||||||
|
// 数据库有唯一约束 (user_id, checkin_date),可以防止并发重复签到
|
||||||
|
if err := tx.Create(checkin).Error; err != nil {
|
||||||
|
return errors.New("签到失败,请稍后重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤2: 在事务中增加用户额度
|
||||||
|
if err := tx.Model(&User{}).Where("id = ?", userId).
|
||||||
|
Update("quota", gorm.Expr("quota + ?", quotaAwarded)).Error; err != nil {
|
||||||
|
return errors.New("签到失败:更新额度出错")
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
})
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 事务成功后,异步更新缓存
|
||||||
|
go func() {
|
||||||
|
_ = cacheIncrUserQuota(userId, int64(quotaAwarded))
|
||||||
|
}()
|
||||||
|
|
||||||
|
return checkin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// userCheckinWithoutTransaction 不使用事务执行签到(适用于 SQLite)
|
||||||
|
func userCheckinWithoutTransaction(checkin *Checkin, userId int, quotaAwarded int) (*Checkin, error) {
|
||||||
|
// 步骤1: 创建签到记录
|
||||||
|
// 数据库有唯一约束 (user_id, checkin_date),可以防止并发重复签到
|
||||||
|
if err := DB.Create(checkin).Error; err != nil {
|
||||||
|
return nil, errors.New("签到失败,请稍后重试")
|
||||||
|
}
|
||||||
|
|
||||||
|
// 步骤2: 增加用户额度
|
||||||
|
// 使用 db=true 强制直接写入数据库,不使用批量更新
|
||||||
|
if err := IncreaseUserQuota(userId, quotaAwarded, true); err != nil {
|
||||||
|
// 如果增加额度失败,需要回滚签到记录
|
||||||
|
DB.Delete(checkin)
|
||||||
|
return nil, errors.New("签到失败:更新额度出错")
|
||||||
|
}
|
||||||
|
|
||||||
|
return checkin, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetUserCheckinStats 获取用户签到统计信息
|
||||||
|
func GetUserCheckinStats(userId int, month string) (map[string]interface{}, error) {
|
||||||
|
// 获取指定月份的所有签到记录
|
||||||
|
startDate := month + "-01"
|
||||||
|
endDate := month + "-31"
|
||||||
|
|
||||||
|
records, err := GetUserCheckinRecords(userId, startDate, endDate)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// 转换为不包含敏感字段的记录
|
||||||
|
checkinRecords := make([]CheckinRecord, len(records))
|
||||||
|
for i, r := range records {
|
||||||
|
checkinRecords[i] = CheckinRecord{
|
||||||
|
CheckinDate: r.CheckinDate,
|
||||||
|
QuotaAwarded: r.QuotaAwarded,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 检查今天是否已签到
|
||||||
|
hasCheckedToday, _ := HasCheckedInToday(userId)
|
||||||
|
|
||||||
|
// 获取用户所有时间的签到统计
|
||||||
|
var totalCheckins int64
|
||||||
|
var totalQuota int64
|
||||||
|
DB.Model(&Checkin{}).Where("user_id = ?", userId).Count(&totalCheckins)
|
||||||
|
DB.Model(&Checkin{}).Where("user_id = ?", userId).Select("COALESCE(SUM(quota_awarded), 0)").Scan(&totalQuota)
|
||||||
|
|
||||||
|
return map[string]interface{}{
|
||||||
|
"total_quota": totalQuota, // 所有时间累计获得的额度
|
||||||
|
"total_checkins": totalCheckins, // 所有时间累计签到次数
|
||||||
|
"checkin_count": len(records), // 本月签到次数
|
||||||
|
"checked_in_today": hasCheckedToday, // 今天是否已签到
|
||||||
|
"records": checkinRecords, // 本月签到记录详情(不含id和user_id)
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
@@ -267,6 +267,7 @@ func migrateDB() error {
|
|||||||
&Setup{},
|
&Setup{},
|
||||||
&TwoFA{},
|
&TwoFA{},
|
||||||
&TwoFABackupCode{},
|
&TwoFABackupCode{},
|
||||||
|
&Checkin{},
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -300,6 +301,7 @@ func migrateDBFast() error {
|
|||||||
{&Setup{}, "Setup"},
|
{&Setup{}, "Setup"},
|
||||||
{&TwoFA{}, "TwoFA"},
|
{&TwoFA{}, "TwoFA"},
|
||||||
{&TwoFABackupCode{}, "TwoFABackupCode"},
|
{&TwoFABackupCode{}, "TwoFABackupCode"},
|
||||||
|
{&Checkin{}, "Checkin"},
|
||||||
}
|
}
|
||||||
// 动态计算migration数量,确保errChan缓冲区足够大
|
// 动态计算migration数量,确保errChan缓冲区足够大
|
||||||
errChan := make(chan error, len(migrations))
|
errChan := make(chan error, len(migrations))
|
||||||
|
|||||||
@@ -93,6 +93,10 @@ func SetApiRouter(router *gin.Engine) {
|
|||||||
selfRoute.POST("/2fa/enable", controller.Enable2FA)
|
selfRoute.POST("/2fa/enable", controller.Enable2FA)
|
||||||
selfRoute.POST("/2fa/disable", controller.Disable2FA)
|
selfRoute.POST("/2fa/disable", controller.Disable2FA)
|
||||||
selfRoute.POST("/2fa/backup_codes", controller.RegenerateBackupCodes)
|
selfRoute.POST("/2fa/backup_codes", controller.RegenerateBackupCodes)
|
||||||
|
|
||||||
|
// Check-in routes
|
||||||
|
selfRoute.GET("/checkin", controller.GetCheckinStatus)
|
||||||
|
selfRoute.POST("/checkin", controller.DoCheckin)
|
||||||
}
|
}
|
||||||
|
|
||||||
adminRoute := userRoute.Group("/")
|
adminRoute := userRoute.Group("/")
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
package operation_setting
|
||||||
|
|
||||||
|
import "github.com/QuantumNous/new-api/setting/config"
|
||||||
|
|
||||||
|
// CheckinSetting 签到功能配置
|
||||||
|
type CheckinSetting struct {
|
||||||
|
Enabled bool `json:"enabled"` // 是否启用签到功能
|
||||||
|
MinQuota int `json:"min_quota"` // 签到最小额度奖励
|
||||||
|
MaxQuota int `json:"max_quota"` // 签到最大额度奖励
|
||||||
|
}
|
||||||
|
|
||||||
|
// 默认配置
|
||||||
|
var checkinSetting = CheckinSetting{
|
||||||
|
Enabled: false, // 默认关闭
|
||||||
|
MinQuota: 1000, // 默认最小额度 1000 (约 0.002 USD)
|
||||||
|
MaxQuota: 10000, // 默认最大额度 10000 (约 0.02 USD)
|
||||||
|
}
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
// 注册到全局配置管理器
|
||||||
|
config.GlobalConfig.Register("checkin_setting", &checkinSetting)
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCheckinSetting 获取签到配置
|
||||||
|
func GetCheckinSetting() *CheckinSetting {
|
||||||
|
return &checkinSetting
|
||||||
|
}
|
||||||
|
|
||||||
|
// IsCheckinEnabled 是否启用签到功能
|
||||||
|
func IsCheckinEnabled() bool {
|
||||||
|
return checkinSetting.Enabled
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCheckinQuotaRange 获取签到额度范围
|
||||||
|
func GetCheckinQuotaRange() (min, max int) {
|
||||||
|
return checkinSetting.MinQuota, checkinSetting.MaxQuota
|
||||||
|
}
|
||||||
@@ -26,6 +26,7 @@ import SettingsSensitiveWords from '../../pages/Setting/Operation/SettingsSensit
|
|||||||
import SettingsLog from '../../pages/Setting/Operation/SettingsLog';
|
import SettingsLog from '../../pages/Setting/Operation/SettingsLog';
|
||||||
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring';
|
import SettingsMonitoring from '../../pages/Setting/Operation/SettingsMonitoring';
|
||||||
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
|
import SettingsCreditLimit from '../../pages/Setting/Operation/SettingsCreditLimit';
|
||||||
|
import SettingsCheckin from '../../pages/Setting/Operation/SettingsCheckin';
|
||||||
import { API, showError, toBoolean } from '../../helpers';
|
import { API, showError, toBoolean } from '../../helpers';
|
||||||
|
|
||||||
const OperationSetting = () => {
|
const OperationSetting = () => {
|
||||||
@@ -70,7 +71,10 @@ const OperationSetting = () => {
|
|||||||
AutomaticEnableChannelEnabled: false,
|
AutomaticEnableChannelEnabled: false,
|
||||||
AutomaticDisableKeywords: '',
|
AutomaticDisableKeywords: '',
|
||||||
'monitor_setting.auto_test_channel_enabled': false,
|
'monitor_setting.auto_test_channel_enabled': false,
|
||||||
'monitor_setting.auto_test_channel_minutes': 10,
|
'monitor_setting.auto_test_channel_minutes': 10 /* 签到设置 */,
|
||||||
|
'checkin_setting.enabled': false,
|
||||||
|
'checkin_setting.min_quota': 1000,
|
||||||
|
'checkin_setting.max_quota': 10000,
|
||||||
});
|
});
|
||||||
|
|
||||||
let [loading, setLoading] = useState(false);
|
let [loading, setLoading] = useState(false);
|
||||||
@@ -140,6 +144,10 @@ const OperationSetting = () => {
|
|||||||
<Card style={{ marginTop: '10px' }}>
|
<Card style={{ marginTop: '10px' }}>
|
||||||
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
<SettingsCreditLimit options={inputs} refresh={onRefresh} />
|
||||||
</Card>
|
</Card>
|
||||||
|
{/* 签到设置 */}
|
||||||
|
<Card style={{ marginTop: '10px' }}>
|
||||||
|
<SettingsCheckin options={inputs} refresh={onRefresh} />
|
||||||
|
</Card>
|
||||||
</Spin>
|
</Spin>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -39,6 +39,7 @@ import { useTranslation } from 'react-i18next';
|
|||||||
import UserInfoHeader from './personal/components/UserInfoHeader';
|
import UserInfoHeader from './personal/components/UserInfoHeader';
|
||||||
import AccountManagement from './personal/cards/AccountManagement';
|
import AccountManagement from './personal/cards/AccountManagement';
|
||||||
import NotificationSettings from './personal/cards/NotificationSettings';
|
import NotificationSettings from './personal/cards/NotificationSettings';
|
||||||
|
import CheckinCalendar from './personal/cards/CheckinCalendar';
|
||||||
import EmailBindModal from './personal/modals/EmailBindModal';
|
import EmailBindModal from './personal/modals/EmailBindModal';
|
||||||
import WeChatBindModal from './personal/modals/WeChatBindModal';
|
import WeChatBindModal from './personal/modals/WeChatBindModal';
|
||||||
import AccountDeleteModal from './personal/modals/AccountDeleteModal';
|
import AccountDeleteModal from './personal/modals/AccountDeleteModal';
|
||||||
@@ -447,6 +448,13 @@ const PersonalSetting = () => {
|
|||||||
{/* 顶部用户信息区域 */}
|
{/* 顶部用户信息区域 */}
|
||||||
<UserInfoHeader t={t} userState={userState} />
|
<UserInfoHeader t={t} userState={userState} />
|
||||||
|
|
||||||
|
{/* 签到日历 - 仅在启用时显示 */}
|
||||||
|
{status?.checkin_enabled && (
|
||||||
|
<div className='mt-4 md:mt-6'>
|
||||||
|
<CheckinCalendar t={t} status={status} />
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{/* 账户管理和其他设置 */}
|
{/* 账户管理和其他设置 */}
|
||||||
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
|
<div className='grid grid-cols-1 xl:grid-cols-2 items-start gap-4 md:gap-6 mt-4 md:mt-6'>
|
||||||
{/* 左侧:账户管理设置 */}
|
{/* 左侧:账户管理设置 */}
|
||||||
|
|||||||
@@ -0,0 +1,321 @@
|
|||||||
|
/*
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useState, useEffect, useMemo } from 'react';
|
||||||
|
import {
|
||||||
|
Card,
|
||||||
|
Calendar,
|
||||||
|
Button,
|
||||||
|
Typography,
|
||||||
|
Avatar,
|
||||||
|
Spin,
|
||||||
|
Tooltip,
|
||||||
|
Collapsible,
|
||||||
|
} from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
CalendarCheck,
|
||||||
|
Gift,
|
||||||
|
Check,
|
||||||
|
ChevronDown,
|
||||||
|
ChevronUp,
|
||||||
|
} from 'lucide-react';
|
||||||
|
import { API, showError, showSuccess, renderQuota } from '../../../../helpers';
|
||||||
|
|
||||||
|
const CheckinCalendar = ({ t, status }) => {
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [checkinLoading, setCheckinLoading] = useState(false);
|
||||||
|
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 [isCollapsed, setIsCollapsed] = useState(true);
|
||||||
|
|
||||||
|
// 创建日期到额度的映射,方便快速查找
|
||||||
|
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) => {
|
||||||
|
setLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.get(`/api/user/checkin?month=${month}`);
|
||||||
|
const { success, data, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
setCheckinData(data);
|
||||||
|
} else {
|
||||||
|
showError(message || t('获取签到状态失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('获取签到状态失败'));
|
||||||
|
} finally {
|
||||||
|
setLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
// 执行签到
|
||||||
|
const doCheckin = async () => {
|
||||||
|
setCheckinLoading(true);
|
||||||
|
try {
|
||||||
|
const res = await API.post('/api/user/checkin');
|
||||||
|
const { success, data, message } = res.data;
|
||||||
|
if (success) {
|
||||||
|
showSuccess(
|
||||||
|
t('签到成功!获得') + ' ' + renderQuota(data.quota_awarded),
|
||||||
|
);
|
||||||
|
// 刷新签到状态
|
||||||
|
fetchCheckinStatus(currentMonth);
|
||||||
|
} else {
|
||||||
|
showError(message || t('签到失败'));
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
showError(t('签到失败'));
|
||||||
|
} finally {
|
||||||
|
setCheckinLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (status?.checkin_enabled) {
|
||||||
|
fetchCheckinStatus(currentMonth);
|
||||||
|
}
|
||||||
|
}, [status?.checkin_enabled, currentMonth]);
|
||||||
|
|
||||||
|
// 当签到状态加载完成后,根据是否已签到设置折叠状态
|
||||||
|
useEffect(() => {
|
||||||
|
if (checkinData.stats?.checked_in_today) {
|
||||||
|
setIsCollapsed(true);
|
||||||
|
} else {
|
||||||
|
setIsCollapsed(false);
|
||||||
|
}
|
||||||
|
}, [checkinData.stats?.checked_in_today]);
|
||||||
|
|
||||||
|
// 如果签到功能未启用,不显示组件
|
||||||
|
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 (
|
||||||
|
<Tooltip
|
||||||
|
content={`${t('获得')} ${renderQuota(quotaAwarded)}`}
|
||||||
|
position='top'
|
||||||
|
>
|
||||||
|
<div className='absolute inset-0 flex flex-col items-center justify-center cursor-pointer'>
|
||||||
|
<div className='w-6 h-6 rounded-full bg-green-500 flex items-center justify-center mb-0.5 shadow-sm'>
|
||||||
|
<Check size={14} className='text-white' strokeWidth={3} />
|
||||||
|
</div>
|
||||||
|
<div className='text-[10px] font-medium text-green-600 dark:text-green-400 leading-none'>
|
||||||
|
{renderQuota(quotaAwarded)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</Tooltip>
|
||||||
|
);
|
||||||
|
}
|
||||||
|
return null;
|
||||||
|
};
|
||||||
|
|
||||||
|
// 处理月份变化
|
||||||
|
const handleMonthChange = (date) => {
|
||||||
|
const month = date.toISOString().slice(0, 7);
|
||||||
|
setCurrentMonth(month);
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<Card className='!rounded-2xl'>
|
||||||
|
{/* 卡片头部 */}
|
||||||
|
<div className='flex items-center justify-between'>
|
||||||
|
<div
|
||||||
|
className='flex items-center flex-1 cursor-pointer'
|
||||||
|
onClick={() => setIsCollapsed(!isCollapsed)}
|
||||||
|
>
|
||||||
|
<Avatar size='small' color='green' className='mr-3 shadow-md'>
|
||||||
|
<CalendarCheck size={16} />
|
||||||
|
</Avatar>
|
||||||
|
<div className='flex-1'>
|
||||||
|
<div className='flex items-center gap-2'>
|
||||||
|
<Typography.Text className='text-lg font-medium'>
|
||||||
|
{t('每日签到')}
|
||||||
|
</Typography.Text>
|
||||||
|
{isCollapsed ? (
|
||||||
|
<ChevronDown size={16} className='text-gray-400' />
|
||||||
|
) : (
|
||||||
|
<ChevronUp size={16} className='text-gray-400' />
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500 dark:text-gray-400'>
|
||||||
|
{checkinData.stats?.checked_in_today
|
||||||
|
? t('今日已签到,累计签到') +
|
||||||
|
` ${checkinData.stats?.total_checkins || 0} ` +
|
||||||
|
t('天')
|
||||||
|
: t('每日签到可获得随机额度奖励')}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<Button
|
||||||
|
type='primary'
|
||||||
|
theme='solid'
|
||||||
|
icon={<Gift size={16} />}
|
||||||
|
onClick={doCheckin}
|
||||||
|
loading={checkinLoading}
|
||||||
|
disabled={checkinData.stats?.checked_in_today}
|
||||||
|
className='!bg-green-600 hover:!bg-green-700'
|
||||||
|
>
|
||||||
|
{checkinData.stats?.checked_in_today
|
||||||
|
? t('今日已签到')
|
||||||
|
: t('立即签到')}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 可折叠内容 */}
|
||||||
|
<Collapsible isOpen={!isCollapsed} keepDOM>
|
||||||
|
{/* 签到统计 */}
|
||||||
|
<div className='grid grid-cols-3 gap-3 mb-4 mt-4'>
|
||||||
|
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||||
|
<div className='text-xl font-bold text-green-600'>
|
||||||
|
{checkinData.stats?.total_checkins || 0}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500'>{t('累计签到')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||||
|
<div className='text-xl font-bold text-orange-600'>
|
||||||
|
{renderQuota(monthlyQuota, 6)}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500'>{t('本月获得')}</div>
|
||||||
|
</div>
|
||||||
|
<div className='text-center p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||||
|
<div className='text-xl font-bold text-blue-600'>
|
||||||
|
{renderQuota(checkinData.stats?.total_quota || 0, 6)}
|
||||||
|
</div>
|
||||||
|
<div className='text-xs text-gray-500'>{t('累计获得')}</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* 签到日历 - 使用更紧凑的样式 */}
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<div className='border rounded-lg overflow-hidden checkin-calendar'>
|
||||||
|
<style>{`
|
||||||
|
.checkin-calendar .semi-calendar {
|
||||||
|
font-size: 13px;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-header {
|
||||||
|
padding: 8px 12px;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-week-row {
|
||||||
|
height: 28px;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-week-row th {
|
||||||
|
font-size: 12px;
|
||||||
|
padding: 4px 0;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-grid-row {
|
||||||
|
height: auto;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-grid-row td {
|
||||||
|
height: 56px;
|
||||||
|
padding: 2px;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-grid-row-cell {
|
||||||
|
position: relative;
|
||||||
|
height: 100%;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-grid-row-cell-day {
|
||||||
|
position: absolute;
|
||||||
|
top: 4px;
|
||||||
|
left: 50%;
|
||||||
|
transform: translateX(-50%);
|
||||||
|
font-size: 12px;
|
||||||
|
z-index: 1;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-same {
|
||||||
|
background: transparent;
|
||||||
|
}
|
||||||
|
.checkin-calendar .semi-calendar-month-today .semi-calendar-month-grid-row-cell-day {
|
||||||
|
background: var(--semi-color-primary);
|
||||||
|
color: white;border-radius: 50%;
|
||||||
|
width: 20px;
|
||||||
|
height: 20px;
|
||||||
|
display: flex;
|
||||||
|
align-items: center;
|
||||||
|
justify-content: center;}
|
||||||
|
`}</style>
|
||||||
|
<Calendar
|
||||||
|
mode='month'
|
||||||
|
onChange={handleMonthChange}
|
||||||
|
dateGridRender={(dateString, date) => dateRender(dateString)}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</Spin>
|
||||||
|
|
||||||
|
{/* 签到说明 */}
|
||||||
|
<div className='mt-3 p-2.5 bg-slate-50 dark:bg-slate-800 rounded-lg'>
|
||||||
|
<Typography.Text type='tertiary' className='text-xs'>
|
||||||
|
<ul className='list-disc list-inside space-y-0.5'>
|
||||||
|
<li>{t('每日签到可获得随机额度奖励')}</li>
|
||||||
|
<li>{t('签到奖励将直接添加到您的账户余额')}</li>
|
||||||
|
<li>{t('每日仅可签到一次,请勿重复签到')}</li>
|
||||||
|
</ul>
|
||||||
|
</Typography.Text>
|
||||||
|
</div>
|
||||||
|
</Collapsible>
|
||||||
|
</Card>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default CheckinCalendar;
|
||||||
@@ -2185,6 +2185,29 @@
|
|||||||
"默认补全倍率": "Default completion ratio",
|
"默认补全倍率": "Default completion ratio",
|
||||||
"跨分组重试": "Cross-group retry",
|
"跨分组重试": "Cross-group retry",
|
||||||
"跨分组": "Cross-group",
|
"跨分组": "Cross-group",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "After enabling, when the current group channel fails, it will try the next group's channel in order"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "After enabling, when the current group channel fails, it will try the next group's channel in order",
|
||||||
|
"每日签到": "Daily Check-in",
|
||||||
|
"今日已签到,累计签到": "Checked in today, total check-ins",
|
||||||
|
"天": "days",
|
||||||
|
"每日签到可获得随机额度奖励": "Daily check-in rewards random quota",
|
||||||
|
"今日已签到": "Checked in today",
|
||||||
|
"立即签到": "Check in now",
|
||||||
|
"获取签到状态失败": "Failed to get check-in status",
|
||||||
|
"签到成功!获得": "Check-in successful! Received",
|
||||||
|
"签到失败": "Check-in failed",
|
||||||
|
"获得": "Received",
|
||||||
|
"累计签到": "Total check-ins",
|
||||||
|
"本月获得": "This month",
|
||||||
|
"累计获得": "Total received",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "Check-in rewards will be directly added to your account balance",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "Only one check-in per day, please do not check in repeatedly",
|
||||||
|
"签到设置": "Check-in Settings",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "Check-in feature allows users to check in daily to receive random quota rewards",
|
||||||
|
"启用签到功能": "Enable check-in feature",
|
||||||
|
"签到最小额度": "Minimum check-in quota",
|
||||||
|
"签到奖励的最小额度": "Minimum quota for check-in rewards",
|
||||||
|
"签到最大额度": "Maximum check-in quota",
|
||||||
|
"签到奖励的最大额度": "Maximum quota for check-in rewards",
|
||||||
|
"保存签到设置": "Save check-in settings"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2234,6 +2234,29 @@
|
|||||||
"默认补全倍率": "Taux de complétion par défaut",
|
"默认补全倍率": "Taux de complétion par défaut",
|
||||||
"跨分组重试": "Nouvelle tentative inter-groupes",
|
"跨分组重试": "Nouvelle tentative inter-groupes",
|
||||||
"跨分组": "Inter-groupes",
|
"跨分组": "Inter-groupes",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Après activation, lorsque le canal du groupe actuel échoue, il essaiera le canal du groupe suivant dans l'ordre"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Après activation, lorsque le canal du groupe actuel échoue, il essaiera le canal du groupe suivant dans l'ordre",
|
||||||
|
"每日签到": "Enregistrement quotidien",
|
||||||
|
"今日已签到,累计签到": "Enregistré aujourd'hui, total des enregistrements",
|
||||||
|
"天": "jours",
|
||||||
|
"每日签到可获得随机额度奖励": "L'enregistrement quotidien récompense un quota aléatoire",
|
||||||
|
"今日已签到": "Enregistré aujourd'hui",
|
||||||
|
"立即签到": "S'enregistrer maintenant",
|
||||||
|
"获取签到状态失败": "Échec de la récupération du statut d'enregistrement",
|
||||||
|
"签到成功!获得": "Enregistrement réussi ! Reçu",
|
||||||
|
"签到失败": "Échec de l'enregistrement",
|
||||||
|
"获得": "Reçu",
|
||||||
|
"累计签到": "Total des enregistrements",
|
||||||
|
"本月获得": "Ce mois-ci",
|
||||||
|
"累计获得": "Total reçu",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "Les récompenses d'enregistrement seront directement ajoutées à votre solde de compte",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "Un seul enregistrement par jour, veuillez ne pas vous enregistrer plusieurs fois",
|
||||||
|
"签到设置": "Paramètres d'enregistrement",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "La fonction d'enregistrement permet aux utilisateurs de s'enregistrer quotidiennement pour recevoir des récompenses de quota aléatoires",
|
||||||
|
"启用签到功能": "Activer la fonction d'enregistrement",
|
||||||
|
"签到最小额度": "Quota minimum d'enregistrement",
|
||||||
|
"签到奖励的最小额度": "Quota minimum pour les récompenses d'enregistrement",
|
||||||
|
"签到最大额度": "Quota maximum d'enregistrement",
|
||||||
|
"签到奖励的最大额度": "Quota maximum pour les récompenses d'enregistrement",
|
||||||
|
"保存签到设置": "Enregistrer les paramètres d'enregistrement"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2133,6 +2133,29 @@
|
|||||||
"随机种子 (留空为随机)": "ランダムシード(空欄でランダム)",
|
"随机种子 (留空为随机)": "ランダムシード(空欄でランダム)",
|
||||||
"跨分组重试": "グループ間リトライ",
|
"跨分组重试": "グループ間リトライ",
|
||||||
"跨分组": "グループ間",
|
"跨分组": "グループ間",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "有効にすると、現在のグループチャネルが失敗した場合、次のグループのチャネルを順番に試行します"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "有効にすると、現在のグループチャネルが失敗した場合、次のグループのチャネルを順番に試行します",
|
||||||
|
"每日签到": "毎日のチェックイン",
|
||||||
|
"今日已签到,累计签到": "本日チェックイン済み、累計チェックイン",
|
||||||
|
"天": "日",
|
||||||
|
"每日签到可获得随机额度奖励": "毎日のチェックインでランダムなクォータ報酬を獲得できます",
|
||||||
|
"今日已签到": "本日チェックイン済み",
|
||||||
|
"立即签到": "今すぐチェックイン",
|
||||||
|
"获取签到状态失败": "チェックイン状態の取得に失敗しました",
|
||||||
|
"签到成功!获得": "チェックイン成功!獲得",
|
||||||
|
"签到失败": "チェックインに失敗しました",
|
||||||
|
"获得": "獲得",
|
||||||
|
"累计签到": "累計チェックイン",
|
||||||
|
"本月获得": "今月の獲得",
|
||||||
|
"累计获得": "累計獲得",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "チェックイン報酬は直接アカウント残高に追加されます",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "1日1回のみチェックイン可能です。重複チェックインはしないでください",
|
||||||
|
"签到设置": "チェックイン設定",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "チェックイン機能により、ユーザーは毎日チェックインしてランダムなクォータ報酬を獲得できます",
|
||||||
|
"启用签到功能": "チェックイン機能を有効にする",
|
||||||
|
"签到最小额度": "チェックイン最小クォータ",
|
||||||
|
"签到奖励的最小额度": "チェックイン報酬の最小クォータ",
|
||||||
|
"签到最大额度": "チェックイン最大クォータ",
|
||||||
|
"签到奖励的最大额度": "チェックイン報酬の最大クォータ",
|
||||||
|
"保存签到设置": "チェックイン設定を保存"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2244,6 +2244,29 @@
|
|||||||
"随机种子 (留空为随机)": "Случайное зерно (оставьте пустым для случайного)",
|
"随机种子 (留空为随机)": "Случайное зерно (оставьте пустым для случайного)",
|
||||||
"跨分组重试": "Повторная попытка между группами",
|
"跨分组重试": "Повторная попытка между группами",
|
||||||
"跨分组": "Межгрупповой",
|
"跨分组": "Межгрупповой",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "После включения, когда канал текущей группы не работает, он будет пытаться использовать канал следующей группы по порядку"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "После включения, когда канал текущей группы не работает, он будет пытаться использовать канал следующей группы по порядку",
|
||||||
|
"每日签到": "Ежедневная регистрация",
|
||||||
|
"今日已签到,累计签到": "Зарегистрирован сегодня, всего регистраций",
|
||||||
|
"天": "дней",
|
||||||
|
"每日签到可获得随机额度奖励": "Ежедневная регистрация награждает случайной квотой",
|
||||||
|
"今日已签到": "Зарегистрирован сегодня",
|
||||||
|
"立即签到": "Зарегистрироваться сейчас",
|
||||||
|
"获取签到状态失败": "Не удалось получить статус регистрации",
|
||||||
|
"签到成功!获得": "Регистрация успешна! Получено",
|
||||||
|
"签到失败": "Регистрация не удалась",
|
||||||
|
"获得": "Получено",
|
||||||
|
"累计签到": "Всего регистраций",
|
||||||
|
"本月获得": "В этом месяце",
|
||||||
|
"累计获得": "Всего получено",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "Награды за регистрацию будут напрямую добавлены на баланс вашего счета",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "Только одна регистрация в день, пожалуйста, не регистрируйтесь повторно",
|
||||||
|
"签到设置": "Настройки регистрации",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "Функция регистрации позволяет пользователям регистрироваться ежедневно для получения случайных наград в виде квоты",
|
||||||
|
"启用签到功能": "Включить функцию регистрации",
|
||||||
|
"签到最小额度": "Минимальная квота регистрации",
|
||||||
|
"签到奖励的最小额度": "Минимальная квота для наград за регистрацию",
|
||||||
|
"签到最大额度": "Максимальная квота регистрации",
|
||||||
|
"签到奖励的最大额度": "Максимальная квота для наград за регистрацию",
|
||||||
|
"保存签到设置": "Сохранить настройки регистрации"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2744,6 +2744,29 @@
|
|||||||
"随机种子 (留空为随机)": "Hạt giống ngẫu nhiên (để trống cho ngẫu nhiên)",
|
"随机种子 (留空为随机)": "Hạt giống ngẫu nhiên (để trống cho ngẫu nhiên)",
|
||||||
"跨分组重试": "Thử lại giữa các nhóm",
|
"跨分组重试": "Thử lại giữa các nhóm",
|
||||||
"跨分组": "Giữa các nhóm",
|
"跨分组": "Giữa các nhóm",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Sau khi bật, khi kênh nhóm hiện tại thất bại, nó sẽ thử kênh của nhóm tiếp theo theo thứ tự"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "Sau khi bật, khi kênh nhóm hiện tại thất bại, nó sẽ thử kênh của nhóm tiếp theo theo thứ tự",
|
||||||
|
"每日签到": "Đăng nhập hàng ngày",
|
||||||
|
"今日已签到,累计签到": "Đã đăng nhập hôm nay, tổng số lần đăng nhập",
|
||||||
|
"天": "ngày",
|
||||||
|
"每日签到可获得随机额度奖励": "Đăng nhập hàng ngày để nhận phần thưởng hạn mức ngẫu nhiên",
|
||||||
|
"今日已签到": "Đã đăng nhập hôm nay",
|
||||||
|
"立即签到": "Đăng nhập ngay",
|
||||||
|
"获取签到状态失败": "Không thể lấy trạng thái đăng nhập",
|
||||||
|
"签到成功!获得": "Đăng nhập thành công! Đã nhận",
|
||||||
|
"签到失败": "Đăng nhập thất bại",
|
||||||
|
"获得": "Đã nhận",
|
||||||
|
"累计签到": "Tổng số lần đăng nhập",
|
||||||
|
"本月获得": "Tháng này",
|
||||||
|
"累计获得": "Tổng đã nhận",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "Phần thưởng đăng nhập sẽ được thêm trực tiếp vào số dư tài khoản của bạn",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "Chỉ có thể đăng nhập một lần mỗi ngày, vui lòng không đăng nhập lặp lại",
|
||||||
|
"签到设置": "Cài đặt đăng nhập",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "Tính năng đăng nhập cho phép người dùng đăng nhập hàng ngày để nhận phần thưởng hạn mức ngẫu nhiên",
|
||||||
|
"启用签到功能": "Bật tính năng đăng nhập",
|
||||||
|
"签到最小额度": "Hạn mức đăng nhập tối thiểu",
|
||||||
|
"签到奖励的最小额度": "Hạn mức tối thiểu cho phần thưởng đăng nhập",
|
||||||
|
"签到最大额度": "Hạn mức đăng nhập tối đa",
|
||||||
|
"签到奖励的最大额度": "Hạn mức tối đa cho phần thưởng đăng nhập",
|
||||||
|
"保存签到设置": "Lưu cài đặt đăng nhập"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2211,6 +2211,29 @@
|
|||||||
"随机种子 (留空为随机)": "随机种子 (留空为随机)",
|
"随机种子 (留空为随机)": "随机种子 (留空为随机)",
|
||||||
"跨分组重试": "跨分组重试",
|
"跨分组重试": "跨分组重试",
|
||||||
"跨分组": "跨分组",
|
"跨分组": "跨分组",
|
||||||
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道"
|
"开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道": "开启后,当前分组渠道失败时会按顺序尝试下一个分组的渠道",
|
||||||
|
"每日签到": "每日签到",
|
||||||
|
"今日已签到,累计签到": "今日已签到,累计签到",
|
||||||
|
"天": "天",
|
||||||
|
"每日签到可获得随机额度奖励": "每日签到可获得随机额度奖励",
|
||||||
|
"今日已签到": "今日已签到",
|
||||||
|
"立即签到": "立即签到",
|
||||||
|
"获取签到状态失败": "获取签到状态失败",
|
||||||
|
"签到成功!获得": "签到成功!获得",
|
||||||
|
"签到失败": "签到失败",
|
||||||
|
"获得": "获得",
|
||||||
|
"累计签到": "累计签到",
|
||||||
|
"本月获得": "本月获得",
|
||||||
|
"累计获得": "累计获得",
|
||||||
|
"签到奖励将直接添加到您的账户余额": "签到奖励将直接添加到您的账户余额",
|
||||||
|
"每日仅可签到一次,请勿重复签到": "每日仅可签到一次,请勿重复签到",
|
||||||
|
"签到设置": "签到设置",
|
||||||
|
"签到功能允许用户每日签到获取随机额度奖励": "签到功能允许用户每日签到获取随机额度奖励",
|
||||||
|
"启用签到功能": "启用签到功能",
|
||||||
|
"签到最小额度": "签到最小额度",
|
||||||
|
"签到奖励的最小额度": "签到奖励的最小额度",
|
||||||
|
"签到最大额度": "签到最大额度",
|
||||||
|
"签到奖励的最大额度": "签到奖励的最大额度",
|
||||||
|
"保存签到设置": "保存签到设置"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,152 @@
|
|||||||
|
/*
|
||||||
|
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 <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
For commercial licensing, please contact support@quantumnous.com
|
||||||
|
*/
|
||||||
|
|
||||||
|
import React, { useEffect, useState, useRef } from 'react';
|
||||||
|
import { Button, Col, Form, Row, Spin, Typography } from '@douyinfe/semi-ui';
|
||||||
|
import {
|
||||||
|
compareObjects,
|
||||||
|
API,
|
||||||
|
showError,
|
||||||
|
showSuccess,
|
||||||
|
showWarning,
|
||||||
|
} from '../../../helpers';
|
||||||
|
import { useTranslation } from 'react-i18next';
|
||||||
|
|
||||||
|
export default function SettingsCheckin(props) {
|
||||||
|
const { t } = useTranslation();
|
||||||
|
const [loading, setLoading] = useState(false);
|
||||||
|
const [inputs, setInputs] = useState({
|
||||||
|
'checkin_setting.enabled': false,
|
||||||
|
'checkin_setting.min_quota': 1000,
|
||||||
|
'checkin_setting.max_quota': 10000,
|
||||||
|
});
|
||||||
|
const refForm = useRef();
|
||||||
|
const [inputsRow, setInputsRow] = useState(inputs);
|
||||||
|
|
||||||
|
function handleFieldChange(fieldName) {
|
||||||
|
return (value) => {
|
||||||
|
setInputs((inputs) => ({ ...inputs, [fieldName]: value }));
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
function onSubmit() {
|
||||||
|
const updateArray = compareObjects(inputs, inputsRow);
|
||||||
|
if (!updateArray.length) return showWarning(t('你似乎并没有修改什么'));
|
||||||
|
const requestQueue = updateArray.map((item) => {
|
||||||
|
let value = '';
|
||||||
|
if (typeof inputs[item.key] === 'boolean') {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
} else {
|
||||||
|
value = String(inputs[item.key]);
|
||||||
|
}
|
||||||
|
return API.put('/api/option/', {
|
||||||
|
key: item.key,
|
||||||
|
value,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
setLoading(true);
|
||||||
|
Promise.all(requestQueue)
|
||||||
|
.then((res) => {
|
||||||
|
if (requestQueue.length === 1) {
|
||||||
|
if (res.includes(undefined)) return;
|
||||||
|
} else if (requestQueue.length > 1) {
|
||||||
|
if (res.includes(undefined))
|
||||||
|
return showError(t('部分保存失败,请重试'));
|
||||||
|
}
|
||||||
|
showSuccess(t('保存成功'));
|
||||||
|
props.refresh();
|
||||||
|
})
|
||||||
|
.catch(() => {
|
||||||
|
showError(t('保存失败,请重试'));
|
||||||
|
})
|
||||||
|
.finally(() => {
|
||||||
|
setLoading(false);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const currentInputs = {};
|
||||||
|
for (let key in props.options) {
|
||||||
|
if (Object.keys(inputs).includes(key)) {
|
||||||
|
currentInputs[key] = props.options[key];
|
||||||
|
}
|
||||||
|
}
|
||||||
|
setInputs(currentInputs);
|
||||||
|
setInputsRow(structuredClone(currentInputs));
|
||||||
|
refForm.current.setValues(currentInputs);
|
||||||
|
}, [props.options]);
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<Spin spinning={loading}>
|
||||||
|
<Form
|
||||||
|
values={inputs}
|
||||||
|
getFormApi={(formAPI) => (refForm.current = formAPI)}
|
||||||
|
style={{ marginBottom: 15 }}
|
||||||
|
>
|
||||||
|
<Form.Section text={t('签到设置')}>
|
||||||
|
<Typography.Text
|
||||||
|
type='tertiary'
|
||||||
|
style={{ marginBottom: 16, display: 'block' }}
|
||||||
|
>
|
||||||
|
{t('签到功能允许用户每日签到获取随机额度奖励')}
|
||||||
|
</Typography.Text>
|
||||||
|
<Row gutter={16}>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
|
<Form.Switch
|
||||||
|
field={'checkin_setting.enabled'}
|
||||||
|
label={t('启用签到功能')}
|
||||||
|
size='default'
|
||||||
|
checkedText='|'
|
||||||
|
uncheckedText='〇'
|
||||||
|
onChange={handleFieldChange('checkin_setting.enabled')}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
|
<Form.InputNumber
|
||||||
|
field={'checkin_setting.min_quota'}
|
||||||
|
label={t('签到最小额度')}
|
||||||
|
placeholder={t('签到奖励的最小额度')}
|
||||||
|
onChange={handleFieldChange('checkin_setting.min_quota')}
|
||||||
|
min={0}
|
||||||
|
disabled={!inputs['checkin_setting.enabled']}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
<Col xs={24} sm={12} md={8} lg={8} xl={8}>
|
||||||
|
<Form.InputNumber
|
||||||
|
field={'checkin_setting.max_quota'}
|
||||||
|
label={t('签到最大额度')}
|
||||||
|
placeholder={t('签到奖励的最大额度')}
|
||||||
|
onChange={handleFieldChange('checkin_setting.max_quota')}
|
||||||
|
min={0}
|
||||||
|
disabled={!inputs['checkin_setting.enabled']}
|
||||||
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
<Row>
|
||||||
|
<Button size='default' onClick={onSubmit}>
|
||||||
|
{t('保存签到设置')}
|
||||||
|
</Button>
|
||||||
|
</Row>
|
||||||
|
</Form.Section>
|
||||||
|
</Form>
|
||||||
|
</Spin>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user