sync from local backup
This commit is contained in:
@@ -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;
|
||||
}
|
||||
}
|
||||
@@ -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;
|
||||
Reference in New Issue
Block a user