sync from local backup

This commit is contained in:
chunyu
2026-08-05 23:59:22 +08:00
commit a59f46b894
409 changed files with 166027 additions and 0 deletions
+765
View File
@@ -0,0 +1,765 @@
.task-center-page {
max-width: 960px;
margin: 0 auto;
padding: 24px 16px;
}
.task-center-header {
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
border-radius: 16px;
padding: 32px;
margin-bottom: 24px;
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.3);
}
.task-center-header-inner {
text-align: center;
}
.task-center-title-row {
display: flex;
align-items: center;
justify-content: center;
gap: 12px;
margin-bottom: 8px;
}
.task-center-main-icon {
font-size: 28px;
color: #fff;
}
.task-center-subtitle {
color: rgba(255, 255, 255, 0.85);
font-size: 14px;
display: block;
margin-bottom: 24px;
}
.task-center-stats {
display: flex;
align-items: center;
justify-content: center;
gap: 32px;
}
.task-stat-item {
text-align: center;
}
.task-stat-value {
font-size: 28px;
font-weight: 800;
color: #fff;
line-height: 1.2;
}
.task-stat-label {
font-size: 12px;
color: rgba(255, 255, 255, 0.75);
margin-top: 4px;
}
.task-stat-divider {
width: 1px;
height: 36px;
background: rgba(255, 255, 255, 0.3);
}
.task-tabs-card {
border-radius: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
background: var(--card-background);
border: 1px solid var(--border-color);
}
.task-list {
display: flex;
flex-direction: column;
gap: 12px;
}
.task-card {
display: flex;
align-items: center;
gap: 16px;
padding: 16px 20px;
background: var(--card-background);
border-radius: 10px;
border: 1px solid var(--border-color);
transition: all 0.2s ease;
}
.task-card:hover {
background: color-mix(in srgb, var(--card-background) 94%, var(--primary-color));
border-color: color-mix(in srgb, var(--border-color) 80%, var(--primary-color));
}
.task-card.task-completed {
background: rgba(82, 196, 26, 0.08);
border-color: rgba(82, 196, 26, 0.35);
}
.task-icon-wrapper {
width: 44px;
height: 44px;
border-radius: 10px;
display: flex;
align-items: center;
justify-content: center;
font-size: 20px;
flex-shrink: 0;
background: #fff7e6;
color: #faad14;
}
.task-icon-claimed {
background: #f0f0f0;
color: #bbb;
}
.task-info {
flex: 1;
min-width: 0;
}
.task-title-row {
display: flex;
align-items: center;
gap: 8px;
margin-bottom: 4px;
}
.task-title {
font-size: 14px;
color: var(--text-primary);
}
.task-reward-tag {
font-size: 12px;
line-height: 1;
}
.task-desc {
font-size: 12px;
color: var(--text-secondary);
display: block;
margin-bottom: 8px;
}
.task-progress-row {
display: flex;
align-items: center;
gap: 8px;
}
.task-progress-bar {
flex: 1;
}
.task-progress-text {
font-size: 12px;
color: var(--text-muted);
white-space: nowrap;
min-width: 36px;
text-align: right;
}
.task-action {
flex-shrink: 0;
}
.checkin-card-wrapper {
border-radius: 12px;
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.06);
background: var(--card-background);
border: 1px solid var(--border-color);
margin-bottom: 16px;
}
.checkin-card {
padding: 4px 0;
}
.checkin-top {
display: flex;
align-items: center;
gap: 20px;
margin-bottom: 24px;
}
.checkin-left {
display: flex;
align-items: center;
gap: 14px;
flex: 1;
}
.checkin-icon-bg {
width: 52px;
height: 52px;
border-radius: 14px;
background: linear-gradient(135deg, #ffd700 0%, #ffed4a 100%);
display: flex;
align-items: center;
justify-content: center;
flex-shrink: 0;
}
.checkin-main-icon {
font-size: 24px;
color: #7a5a00;
}
.checkin-info {
display: flex;
flex-direction: column;
gap: 2px;
}
.checkin-sub {
font-size: 13px;
color: var(--text-secondary);
}
.checkin-streak {
color: #faad14;
font-weight: 600;
}
.checkin-reward {
display: flex;
align-items: baseline;
gap: 4px;
flex-shrink: 0;
}
.checkin-reward-value {
font-size: 24px;
font-weight: 800;
color: #faad14;
}
.checkin-reward-label {
font-size: 13px;
color: var(--text-secondary);
}
.checkin-btn {
min-width: 96px;
height: 40px;
border-radius: 20px;
font-size: 15px;
font-weight: 600;
flex-shrink: 0;
}
.checkin-btn-done {
background: #f0f0f0 !important;
border-color: #f0f0f0 !important;
color: #8c8c8c !important;
}
.checkin-week {
background: rgba(129, 140, 248, 0.05);
border-radius: 12px;
padding: 16px 20px;
border: 1px solid var(--border-color);
}
.checkin-week-title {
font-size: 13px;
color: var(--text-secondary);
font-weight: 600;
}
.checkin-week-header {
display: flex;
align-items: center;
justify-content: space-between;
margin-bottom: 16px;
}
.checkin-week-row {
display: flex;
justify-content: space-between;
}
.checkin-day {
display: flex;
flex-direction: column;
align-items: center;
gap: 8px;
flex: 1;
}
.checkin-day-label {
font-size: 12px;
color: var(--text-secondary);
font-weight: 600;
}
.checkin-day-circle {
width: 40px;
height: 40px;
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
background: rgba(148, 163, 184, 0.15);
border: 2px solid var(--border-color);
transition: all 0.2s ease;
}
.checkin-day-checked .checkin-day-circle {
background: linear-gradient(135deg, #52c41a 0%, #73d13d 100%);
border-color: #52c41a;
}
.checkin-day-today .checkin-day-circle {
border-color: #faad14;
box-shadow: 0 0 0 3px rgba(250, 173, 20, 0.2);
}
.checkin-day-future .checkin-day-circle {
background: rgba(51, 65, 85, 0.15);
border-color: rgba(51, 65, 85, 0.3);
}
.checkin-day-check {
font-size: 18px;
color: #fff;
}
.checkin-day-clock {
font-size: 16px;
color: var(--text-muted);
}
.checkin-day-empty {
width: 10px;
height: 10px;
border-radius: 50%;
background: var(--text-muted);
}
.checkin-day-today .checkin-day-empty {
background: #faad14;
}
.checkin-day-date {
font-size: 11px;
color: var(--text-muted);
}
.checkin-day-today .checkin-day-date {
color: #faad14;
font-weight: 600;
}
.checkin-day-checked .checkin-day-date {
color: #52c41a;
}
.checkin-week-bonus {
display: flex;
align-items: center;
justify-content: space-between;
margin-top: 16px;
padding: 12px 16px;
background: #fffbe6;
border-radius: 10px;
border: 1px dashed #ffe58f;
}
.checkin-week-bonus-done {
background: #f6ffed;
border-color: #b7eb8f;
}
.checkin-week-bonus-info {
display: flex;
align-items: center;
gap: 8px;
}
.checkin-week-bonus-icon {
font-size: 18px;
}
.checkin-week-bonus-reward {
font-size: 16px;
font-weight: 700;
}
.task-center-level-bar {
margin-top: 16px;
padding: 12px 16px;
background: rgba(255, 255, 255, 0.12);
border-radius: 10px;
backdrop-filter: blur(6px);
display: flex;
flex-direction: column;
gap: 6px;
}
.level-badge {
display: inline-block;
padding: 2px 10px;
background: linear-gradient(135deg, #ffd700, #ff8c00);
color: #fff;
font-size: 13px;
font-weight: 700;
border-radius: 12px;
align-self: flex-start;
text-shadow: 0 1px 2px rgba(0, 0, 0, 0.2);
}
.level-info-row {
display: flex;
justify-content: space-between;
align-items: center;
}
.task-progress-row {
display: flex;
align-items: center;
gap: 8px;
margin-top: 6px;
}
.task-progress-row .ant-progress {
flex: 1;
}
.task-progress-text {
font-size: 11px;
color: var(--text-muted);
white-space: nowrap;
}
.task-rewards {
display: flex;
gap: 4px;
flex-wrap: wrap;
}
.task-claimed {
opacity: 0.55;
}
.task-claim-btn {
background: linear-gradient(135deg, #52c41a, #73d13d);
border: none;
}
/* ============ 暗色模式覆盖 ============ */
[data-theme="dark"] {
.task-center-header {
box-shadow: 0 4px 20px rgba(102, 126, 234, 0.2);
}
.task-tabs-card {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
.task-card:hover {
background: color-mix(in srgb, var(--card-background) 90%, var(--primary-color));
}
.task-icon-wrapper {
background: rgba(250, 173, 20, 0.15);
color: #ffc53d;
}
.task-icon-claimed {
background: rgba(148, 163, 184, 0.15);
color: var(--text-muted);
}
.checkin-card-wrapper {
box-shadow: 0 1px 4px rgba(0, 0, 0, 0.3);
}
.checkin-icon-bg {
box-shadow: 0 4px 14px rgba(255, 215, 0, 0.18);
}
.checkin-main-icon {
color: #fbbf24;
}
.checkin-reward-value {
color: #fbbf24;
}
.checkin-btn-done {
background: rgba(148, 163, 184, 0.18) !important;
border-color: transparent !important;
color: var(--text-secondary) !important;
}
.checkin-week {
background: rgba(129, 140, 248, 0.08);
}
.checkin-day-circle {
background: rgba(148, 163, 184, 0.12);
border-color: var(--border-color);
}
.checkin-day-future .checkin-day-circle {
background: rgba(51, 65, 85, 0.4);
border-color: rgba(51, 65, 85, 0.6);
}
.checkin-day-empty {
background: #475569;
}
.checkin-week-bonus {
background: rgba(250, 173, 20, 0.1);
border-color: rgba(250, 173, 20, 0.35);
}
.checkin-week-bonus-done {
background: rgba(82, 196, 26, 0.12) !important;
border-color: rgba(82, 196, 26, 0.35) !important;
}
.checkin-week-bonus-done .checkin-week-bonus-reward {
color: #4ade80 !important;
}
}
/* ============ 移动端响应式适配 ============ */
@media (max-width: 768px) {
.task-center-page {
padding: 12px 8px;
}
.task-center-header {
padding: 20px 16px;
border-radius: 12px;
margin-bottom: 16px;
}
.task-center-title-row {
gap: 8px;
}
.task-center-main-icon {
font-size: 22px;
}
.task-center-subtitle {
font-size: 12px;
margin-bottom: 16px;
}
.task-center-level-bar {
padding: 10px 12px;
}
.level-badge {
font-size: 12px;
padding: 2px 8px;
}
.level-info-row {
flex-wrap: wrap;
gap: 4px;
}
.checkin-top {
flex-wrap: wrap;
gap: 12px;
}
.checkin-left {
flex: 1 1 auto;
}
.checkin-icon-bg {
width: 44px;
height: 44px;
border-radius: 12px;
}
.checkin-main-icon {
font-size: 20px;
}
.checkin-info {
gap: 2px;
}
.checkin-reward {
position: absolute;
top: 16px;
right: 16px;
}
.checkin-reward-value {
font-size: 20px;
}
.checkin-btn {
min-width: 80px;
height: 36px;
font-size: 14px;
border-radius: 18px;
}
.checkin-card-wrapper {
position: relative;
padding-top: 60px;
}
.checkin-week {
padding: 12px;
}
.checkin-week-row {
gap: 0;
}
.checkin-day {
flex: 1;
gap: 4px;
}
.checkin-day-circle {
width: 32px;
height: 32px;
}
.checkin-day-label {
font-size: 10px;
}
.checkin-day-date {
font-size: 9px;
}
.checkin-week-bonus {
padding: 10px 12px;
flex-wrap: wrap;
gap: 8px;
}
.checkin-week-bonus-info {
flex: 1;
}
.checkin-week-bonus-reward {
font-size: 14px;
}
.task-tabs-card {
margin-top: 12px;
}
.task-list {
gap: 8px;
}
.task-card {
padding: 12px;
gap: 12px;
}
.task-icon-wrapper {
width: 36px;
height: 36px;
font-size: 16px;
border-radius: 8px;
}
.task-info {
flex: 1;
min-width: 0;
}
.task-title-row {
flex-wrap: wrap;
gap: 4px;
}
.task-title {
font-size: 13px;
}
.task-desc {
font-size: 11px;
margin-bottom: 6px;
}
.task-rewards {
gap: 2px;
}
.task-action {
min-width: auto;
}
.task-claim-btn {
font-size: 12px;
padding: 0 12px;
height: 28px;
}
}
@media (max-width: 480px) {
.task-center-header {
padding: 16px 12px;
}
.task-center-level-bar {
padding: 8px 10px;
}
.checkin-top {
flex-direction: column;
align-items: flex-start;
}
.checkin-reward {
position: static;
top: auto;
right: auto;
}
.checkin-btn {
width: 100%;
min-width: unset;
}
.checkin-week-row {
justify-content: space-between;
}
.checkin-day {
min-width: 0;
}
.checkin-day-circle {
width: 28px;
height: 28px;
}
.task-card {
flex-wrap: wrap;
}
.task-info {
width: calc(100% - 48px);
}
.task-action {
width: 100%;
display: flex;
justify-content: flex-end;
margin-top: 8px;
}
}
+428
View File
@@ -0,0 +1,428 @@
import React, { useState, useEffect, useCallback } from "react";
import { Card, Button, Typography, Tag, Tabs, Badge, Empty, Spin, Progress } from "antd";
import { message } from "@/utils/message";
import {
TrophyOutlined,
CheckCircleOutlined,
FireOutlined,
CalendarOutlined,
ClockCircleOutlined,
ReloadOutlined,
LoginOutlined,
GiftOutlined,
StarOutlined,
UserAddOutlined,
EditOutlined,
MobileOutlined,
ShareAltOutlined,
EyeOutlined,
ReadOutlined,
CommentOutlined,
} from "@ant-design/icons";
import { useNavigate } from "react-router-dom";
import { useSelector } from "react-redux";
import { selectIsLogin, updateUser, updateLevelInfo, selectLevelInfo } from "@/features/login";
import { useAppDispatch } from "@/app/store";
import { useLoginModal } from "@/contexts/LoginModalContext";
import { useTranslation } from "@/contexts/LanguageContext";
import { api_request } from "@/utils/request";
import "./TaskCenter.css";
const { Title, Text } = Typography;
const WEEK_DAYS = ["一", "二", "三", "四", "五", "六", "日"];
interface WeekDay {
date: string;
checked: boolean;
is_today: boolean;
is_future: boolean;
}
interface CheckinStatus {
signed_today: boolean;
week_days: WeekDay[];
week_signed_count: number;
full_week: boolean;
checkin_points: number;
full_week_bonus: number;
points: number;
}
interface ApiTask {
id: number;
name: string;
task_type: string;
action_type: string;
description: string;
target_count: number;
reward_points: number;
reward_coins: number;
reward_xp: number;
icon: string;
redirect_url: string;
current_count: number;
is_completed: boolean;
is_claimed: boolean;
}
const ACTION_ICONS: Record<string, React.ReactNode> = {
browse: <EyeOutlined />,
learn: <ReadOutlined />,
post: <CommentOutlined />,
share: <ShareAltOutlined />,
checkin: <CalendarOutlined />,
profile: <EditOutlined />,
bind: <MobileOutlined />,
invite: <UserAddOutlined />,
custom: <StarOutlined />,
};
const getDefaultWeekDays = (): WeekDay[] => {
const now = new Date();
const dayOfWeek = now.getDay();
const monday = new Date(now);
monday.setDate(now.getDate() - ((dayOfWeek + 6) % 7));
return Array.from({ length: 7 }, (_, i) => {
const d = new Date(monday);
d.setDate(monday.getDate() + i);
return {
date: d.toISOString().split("T")[0],
checked: false,
is_today: d.toDateString() === now.toDateString(),
is_future: d > now && d.toDateString() !== now.toDateString(),
};
});
};
const TaskCenter: React.FC = () => {
const navigate = useNavigate();
const isLogin = useSelector(selectIsLogin);
const levelInfo = useSelector(selectLevelInfo);
const dispatch = useAppDispatch();
const { openLoginModal } = useLoginModal();
const { t } = useTranslation();
const [loading, setLoading] = useState(false);
const [tasksLoading, setTasksLoading] = useState(false);
const [signing, setSigning] = useState(false);
const [claiming, setClaiming] = useState<number | null>(null);
const [checkinStatus, setCheckinStatus] = useState<CheckinStatus | null>(null);
const [tasks, setTasks] = useState<ApiTask[]>([]);
const fetchCheckinStatus = useCallback(async () => {
setLoading(true);
try {
const res: any = await api_request.login.wallet.get_checkin_status();
if (res?.code === 10000 && res?.data) {
setCheckinStatus(res.data);
}
} catch (error: any) {
if (error?.response?.status !== 401) {
message.error(t('taskCenter.fetchStatusFailed'));
}
} finally {
setLoading(false);
}
}, []);
const fetchTasks = useCallback(async () => {
if (!isLogin) return;
setTasksLoading(true);
try {
const res: any = await api_request.login.tasks.get_list();
if (res?.code === 10000 && res?.data) {
setTasks(res.data.tasks || []);
if (res.data.level) {
dispatch(updateLevelInfo(res.data.level));
}
}
} catch (error: any) {
if (error?.response?.status !== 401) {
message.error(t('taskCenter.fetchTasksFailed'));
}
} finally {
setTasksLoading(false);
}
}, [isLogin, dispatch]);
const fetchLevel = useCallback(async () => {
if (!isLogin) return;
try {
const res: any = await api_request.login.level.get();
if (res?.code === 10000 && res?.data) {
dispatch(updateLevelInfo(res.data));
}
} catch {}
}, [isLogin, dispatch]);
useEffect(() => {
if (isLogin) {
fetchCheckinStatus();
fetchTasks();
fetchLevel();
} else {
setLoading(false);
setCheckinStatus(null);
setTasks([]);
}
}, [isLogin, fetchCheckinStatus, fetchTasks, fetchLevel]);
const handleSign = async () => {
if (!isLogin) { openLoginModal(); return; }
if (!checkinStatus || checkinStatus.signed_today || signing) return;
setSigning(true);
try {
const res: any = await api_request.login.wallet.checkin();
if (res?.code === 10000 && res?.data) {
const data = res.data;
const bonusText = data.bonus > 0 ? t('taskCenter.includeBonus', { bonus: data.bonus }) : "";
message.success(t('taskCenter.signSuccess', { points: data.earned }) + bonusText);
setCheckinStatus((prev) =>
prev ? { ...prev, signed_today: true, week_signed_count: data.week_signed_count, full_week: data.full_week, points: data.points, week_days: prev.week_days.map((d) => d.is_today ? { ...d, checked: true } : d) } : prev
);
dispatch(updateUser({ points: data.points }));
fetchTasks();
} else {
message.error(res?.message || t('taskCenter.signFailed'));
}
} catch (error: any) {
if (error?.response?.status !== 401) { message.error(t('taskCenter.signFailed')); }
} finally {
setSigning(false);
}
};
const handleClaim = async (taskId: number) => {
if (!isLogin) { openLoginModal(); return; }
setClaiming(taskId);
try {
const res: any = await api_request.login.tasks.claim(taskId);
if (res?.code === 10000 && res?.data) {
const d = res.data;
const rewards: string[] = [];
if (d.reward_points > 0) rewards.push(`${d.reward_points} ${t('taskCenter.points')}`);
if (d.reward_coins > 0) rewards.push(`${d.reward_coins} ${t('taskCenter.yCoins')}`);
if (d.reward_xp > 0) rewards.push(`${d.reward_xp} ${t('taskCenter.exp')}`);
message.success(t('taskCenter.claimSuccess', { rewards: rewards.join(", ") }));
dispatch(updateUser({ points: d.points, coins: d.coins }));
dispatch(updateLevelInfo({ ...levelInfo, xp: d.xp, level: d.level, next_level_xp: d.next_level_xp }));
setTasks((prev) => prev.map((t) => t.id === taskId ? { ...t, is_claimed: true } : t));
} else {
message.error(res?.message || t('taskCenter.claimFailed'));
}
} catch {
message.error(t('taskCenter.claimFailed'));
} finally {
setClaiming(null);
}
};
const handleGoTask = (task: ApiTask) => {
if (!isLogin) { openLoginModal(); return; }
if (task.redirect_url) {
navigate(task.redirect_url);
}
};
const renderRewardTags = (task: ApiTask) => {
const tags: React.ReactNode[] = [];
if (task.reward_points > 0) tags.push(<Tag key="p" color="gold">+{task.reward_points} {t('taskCenter.points')}</Tag>);
if (task.reward_coins > 0) tags.push(<Tag key="c" color="purple">+{task.reward_coins} {t('taskCenter.yCoins')}</Tag>);
if (task.reward_xp > 0) tags.push(<Tag key="x" color="blue">+{task.reward_xp} {t('taskCenter.exp')}</Tag>);
return tags;
};
const renderTaskCard = (task: ApiTask) => {
const progressPct = task.target_count > 0 ? Math.min(Math.round((task.current_count / task.target_count) * 100), 100) : 0;
const icon = ACTION_ICONS[task.action_type] || <StarOutlined />;
return (
<div className={`task-card ${task.is_completed ? "task-completed" : ""} ${task.is_claimed ? "task-claimed" : ""}`} key={task.id}>
<div className={`task-icon-wrapper ${task.is_claimed ? "task-icon-claimed" : ""}`}>
{icon}
</div>
<div className="task-info">
<div className="task-title-row">
<Text strong className="task-title" delete={task.is_claimed}>{task.name}</Text>
<div className="task-rewards">{renderRewardTags(task)}</div>
</div>
<Text className="task-desc">{task.description}</Text>
{task.target_count > 1 && (
<div className="task-progress-row">
<Progress percent={progressPct} size="small" strokeColor={task.is_completed ? "#52c41a" : "#7c3aed"} showInfo={false} />
<Text className="task-progress-text">{task.current_count}/{task.target_count}</Text>
</div>
)}
</div>
<div className="task-action">
{task.is_claimed ? (
<Text type="secondary" style={{ fontSize: 12 }}>{t('taskCenter.claimed')}</Text>
) : task.is_completed ? (
<Button type="primary" size="small" className="task-claim-btn" onClick={() => handleClaim(task.id)} loading={claiming === task.id}>
{t('taskCenter.claim')}
</Button>
) : task.redirect_url ? (
<Button size="small" onClick={() => handleGoTask(task)}>{t('taskCenter.goComplete')}</Button>
) : null}
</div>
</div>
);
};
const weekDays = checkinStatus?.week_days || getDefaultWeekDays();
const signedToday = checkinStatus?.signed_today ?? false;
const weekSignedCount = checkinStatus?.week_signed_count ?? 0;
const fullWeek = checkinStatus?.full_week ?? false;
const checkinPoints = checkinStatus?.checkin_points;
const fullWeekBonus = checkinStatus?.full_week_bonus;
const dailyTasks = tasks.filter((t) => t.task_type === "daily");
const weeklyTasks = tasks.filter((t) => t.task_type === "weekly");
const specialTasks = tasks.filter((t) => t.task_type === "special");
const renderCheckinCard = () => {
if (isLogin && !checkinStatus) {
return (
<Card className="checkin-card-wrapper">
<div style={{ textAlign: "center", padding: 40 }}>
{loading ? <Spin tip={t('taskCenter.loading')} /> : (
<div style={{ color: "#7c3aed", cursor: "pointer", fontSize: 15, display: "inline-flex", alignItems: "center", gap: 8 }} onClick={() => fetchCheckinStatus()}>
<ReloadOutlined style={{ fontSize: 18 }} /> {t('taskCenter.loadFailed')}
</div>
)}
</div>
</Card>
);
}
return (
<Card className="checkin-card-wrapper">
<div className="checkin-card">
<div className="checkin-top">
<div className="checkin-left">
<Badge dot={isLogin && !signedToday} offset={[-2, 2]}>
<div className="checkin-icon-bg"><CalendarOutlined className="checkin-main-icon" /></div>
</Badge>
<div className="checkin-info">
<Text strong style={{ fontSize: 16 }}>{t('taskCenter.checkin')}</Text>
<Text className="checkin-sub">{!isLogin ? t('taskCenter.loginToCheckin') : signedToday ? t('taskCenter.signed') : t('taskCenter.notSignedToday')}</Text>
</div>
</div>
{checkinPoints != null && (
<div className="checkin-reward">
<Text className="checkin-reward-value">+{checkinPoints}</Text>
<Text className="checkin-reward-label">{t('taskCenter.points')}</Text>
</div>
)}
{!isLogin ? (
<Button type="primary" size="large" className="checkin-btn" onClick={openLoginModal} icon={<LoginOutlined />}>{t('taskCenter.signIn')}</Button>
) : (
<Button type="primary" size="large" className={`checkin-btn ${signedToday ? "checkin-btn-done" : ""}`} onClick={handleSign} disabled={signedToday} loading={signing} icon={signedToday ? <CheckCircleOutlined /> : undefined}>
{signedToday ? t('taskCenter.signed') : t('taskCenter.sign')}
</Button>
)}
</div>
<div className="checkin-week">
<div className="checkin-week-header">
<Text className="checkin-week-title">{t('taskCenter.weekCheckin')}</Text>
<Tag color={fullWeek ? "green" : "gold"}>{fullWeek ? t('taskCenter.fullySigned') : t('taskCenter.signedDays', { count: weekSignedCount })}</Tag>
</div>
<div className="checkin-week-row">
{weekDays.map((day: WeekDay, idx: number) => {
const date = new Date(day.date);
return (
<div key={idx} className={`checkin-day ${day.is_today ? "checkin-day-today" : ""} ${day.checked ? "checkin-day-checked" : ""} ${day.is_future ? "checkin-day-future" : ""}`}>
<Text className="checkin-day-label">{WEEK_DAYS[idx]}</Text>
<div className="checkin-day-circle">
{day.checked ? <CheckCircleOutlined className="checkin-day-check" /> : day.is_future ? <ClockCircleOutlined className="checkin-day-clock" /> : <span className="checkin-day-empty" />}
</div>
<Text className="checkin-day-date">{date.getMonth() + 1}/{date.getDate()}</Text>
</div>
);
})}
</div>
{fullWeekBonus != null && (
<div className={`checkin-week-bonus ${fullWeek ? "checkin-week-bonus-done" : ""}`}>
<div className="checkin-week-bonus-info">
<span className="checkin-week-bonus-icon">🎁</span>
<Text strong style={{ fontSize: 13 }}>{fullWeek ? t('taskCenter.weekBonusClaimed') : t('taskCenter.weekBonus')}</Text>
</div>
<Text className="checkin-week-bonus-reward" style={{ color: fullWeek ? "#52c41a" : "#faad14" }}>+{fullWeekBonus} {t('taskCenter.points')}</Text>
</div>
)}
</div>
</div>
</Card>
);
};
const renderTaskList = (taskList: ApiTask[], emptyText: string) => {
if (tasksLoading) return <div style={{ textAlign: "center", padding: 24 }}><Spin /></div>;
if (taskList.length === 0) return <Empty description={t('taskCenter.noTasks')} />;
return <div className="task-list">{taskList.map(renderTaskCard)}</div>;
};
return (
<div className="task-center-page">
<div className="task-center-header">
<div className="task-center-header-inner">
<div className="task-center-title-row">
<TrophyOutlined className="task-center-main-icon" />
<Title level={3} style={{ margin: 0, color: "#fff" }}>{t('taskCenter.title')}</Title>
</div>
<Text className="task-center-subtitle">{t('taskCenter.subtitle')}</Text>
{levelInfo && (
<div className="task-center-level-bar">
<div className="level-badge">{t('taskCenter.levelTitle', { level: levelInfo.level })}</div>
<div className="level-info-row">
<Text style={{ color: "rgba(255,255,255,0.85)", fontSize: 12 }}>{levelInfo.title}</Text>
{levelInfo.next_level_title && (
<Text style={{ color: "rgba(255,255,255,0.6)", fontSize: 11 }}>→ {levelInfo.next_level_title}</Text>
)}
</div>
<Progress percent={levelInfo.progress_pct} showInfo={false} strokeColor={{ from: "#ffd700", to: "#ff6b6b" }} trailColor="rgba(255,255,255,0.2)" size="small" />
<Text style={{ color: "rgba(255,255,255,0.7)", fontSize: 11 }}>
{levelInfo.xp} XP{levelInfo.next_level_xp != null ? ` / ${levelInfo.next_level_xp} XP` : ""}
</Text>
</div>
)}
</div>
</div>
{renderCheckinCard()}
<Card className="task-tabs-card" style={{ marginTop: 16 }}>
<Tabs
defaultActiveKey="daily"
items={[
{
key: "daily",
label: <span><FireOutlined /> {t('taskCenter.dailyTasks')}</span>,
children: renderTaskList(dailyTasks, t('taskCenter.noTasks')),
},
{
key: "weekly",
label: <span><CalendarOutlined /> {t('taskCenter.weeklyTasks')}</span>,
children: renderTaskList(weeklyTasks, t('taskCenter.noTasks')),
},
]}
/>
</Card>
{specialTasks.length > 0 && (
<Card className="task-tabs-card" style={{ marginTop: 16 }}>
<div className="task-list">
<Text strong style={{ fontSize: 15, marginBottom: 8, display: "block" }}>
<GiftOutlined style={{ marginRight: 6, color: "#eb2f96" }} />
{t('taskCenter.specialTasks')}
</Text>
{specialTasks.map(renderTaskCard)}
</div>
</Card>
)}
</div>
);
};
export default TaskCenter;