包含 webapp(Next.js 用户端)、webapp-back(Go 后端)、 antdesign(管理后台)、landingpage(营销落地页)、 数据库 SQL 和配置文件。
195 lines
9.7 KiB
TypeScript
195 lines
9.7 KiB
TypeScript
"use client";
|
|
|
|
import { useState, useEffect } from "react";
|
|
import Image from "next/image";
|
|
import VipCard from "./VipCard";
|
|
import { useApp } from "@/contexts/AppContext";
|
|
import { fetchDashboard, type DashboardData } from "@/lib/api/points";
|
|
|
|
export default function PointsDashboard() {
|
|
const { t } = useApp();
|
|
const [dashboard, setDashboard] = useState<DashboardData | null>(null);
|
|
const [loading, setLoading] = useState(true);
|
|
|
|
useEffect(() => {
|
|
async function loadDashboard() {
|
|
setLoading(true);
|
|
const data = await fetchDashboard();
|
|
setDashboard(data);
|
|
setLoading(false);
|
|
}
|
|
loadDashboard();
|
|
}, []);
|
|
|
|
if (loading) {
|
|
return (
|
|
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-8 animate-pulse">
|
|
<div className="h-64 bg-gray-200 dark:bg-gray-700 rounded-xl" />
|
|
</div>
|
|
);
|
|
}
|
|
|
|
if (!dashboard) {
|
|
return (
|
|
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-8 animate-fade-in">
|
|
<p className="text-text-tertiary dark:text-gray-400">{t("points.failedToLoadDashboard")}</p>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
return (
|
|
<div className="bg-bg-surface dark:bg-gray-800 rounded-3xl border border-border-gray dark:border-gray-700 p-4 md:p-8 flex flex-col gap-6 animate-fade-in">
|
|
{/* Header Row */}
|
|
<div className="flex items-center justify-between">
|
|
<div className="flex items-center gap-3">
|
|
<div className="bg-[#f9fafb] dark:bg-gray-700 border border-border-normal dark:border-gray-600 rounded-full px-3 py-1 flex items-center justify-center">
|
|
<span className="text-caption-tiny font-bold text-text-primary dark:text-white leading-[150%] tracking-[0.01em]">
|
|
{dashboard.season.seasonName}
|
|
</span>
|
|
</div>
|
|
{dashboard.season.isLive && (
|
|
<div className="bg-[#e1f8ec] dark:bg-green-900/30 border border-[#b8ecd2] dark:border-green-700 rounded-full px-3 py-1 flex items-center gap-2">
|
|
<div className="w-1.5 h-1.5 rounded-full bg-[#10b981] opacity-55" />
|
|
<span className="text-caption-tiny font-bold text-[#10b981] dark:text-green-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.live")}
|
|
</span>
|
|
</div>
|
|
)}
|
|
</div>
|
|
<div className="rounded-[14px] px-4 py-2 flex items-center gap-2 h-[34.78px] shadow-lg bg-gradient-to-r from-[#fee685] to-[#fdc700] rotate-1">
|
|
<Image src="/components/points/icon-star.svg" alt="" width={17} height={17} />
|
|
<span className="text-caption-tiny font-bold text-text-primary leading-[150%] tracking-[0.01em]">
|
|
{t("points.xPoints")}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* ── 移动端布局 ── */}
|
|
<div className="md:hidden flex flex-col gap-4">
|
|
{/* Title */}
|
|
<div className="flex flex-col gap-1">
|
|
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
|
|
{t("points.pointsDashboard")}
|
|
</h2>
|
|
<p className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
|
<span>{t("points.unlockUpTo")} </span>
|
|
<span className="font-bold text-[#fdc700]">{t("points.xPoints")}</span>
|
|
<span> {t("points.withEcosystemMultipliers")}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats Row - 3 items in one line */}
|
|
<div className="bg-bg-subtle dark:bg-gray-700 rounded-2xl border border-white/5 dark:border-gray-600 px-4 py-3 flex items-center justify-between gap-2">
|
|
<div className="flex flex-col gap-0.5 min-w-0">
|
|
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.yourTotalPoints")}
|
|
</span>
|
|
<span className="text-[22px] font-extrabold leading-[110%] tracking-[-0.01em] bg-gradient-to-b from-[#111827] to-[#4b5563] bg-clip-text text-transparent dark:from-white dark:to-gray-400">
|
|
{dashboard.totalPoints.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div className="w-px h-8 bg-border-normal dark:bg-gray-600 flex-shrink-0" />
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.globalRank")}
|
|
</span>
|
|
<span className="text-body-large font-bold text-text-primary dark:text-white leading-[130%]">
|
|
#{dashboard.globalRank.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
|
|
Top {dashboard.topPercentage}
|
|
</span>
|
|
</div>
|
|
<div className="w-px h-8 bg-border-normal dark:bg-gray-600 flex-shrink-0" />
|
|
<div className="flex flex-col gap-0.5">
|
|
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.endsIn")}
|
|
</span>
|
|
<span className="text-body-large font-bold text-text-primary dark:text-white leading-[130%]">
|
|
{dashboard.season.daysRemaining}d {dashboard.season.hoursRemaining}h
|
|
</span>
|
|
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
|
|
{new Date(dashboard.season.endTime).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
|
|
{/* VipCard - full width, own row */}
|
|
<VipCard
|
|
memberTier={dashboard.memberTier}
|
|
level={dashboard.vipLevel}
|
|
currentPoints={dashboard.totalPoints}
|
|
totalPoints={dashboard.totalPoints + dashboard.pointsToNextTier}
|
|
nextTier={dashboard.nextTier}
|
|
pointsToNextTier={dashboard.pointsToNextTier}
|
|
/>
|
|
</div>
|
|
|
|
{/* ── 桌面端布局 ── */}
|
|
<div className="hidden md:flex gap-6">
|
|
{/* Left Section - Points Info */}
|
|
<div className="flex-1 flex flex-col gap-6">
|
|
{/* Title */}
|
|
<div className="flex flex-col gap-1">
|
|
<h2 className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
|
|
{t("points.pointsDashboard")}
|
|
</h2>
|
|
<p className="text-body-small font-regular text-text-tertiary dark:text-gray-400 leading-[150%]">
|
|
<span>{t("points.unlockUpTo")} </span>
|
|
<span className="font-bold text-[#fdc700]">{t("points.xPoints")}</span>
|
|
<span> {t("points.withEcosystemMultipliers")}</span>
|
|
</p>
|
|
</div>
|
|
|
|
{/* Stats Grid */}
|
|
<div className="flex items-center justify-between gap-6">
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-body-small font-bold text-text-secondary dark:text-gray-300 leading-[150%]">
|
|
{t("points.yourTotalPoints")}
|
|
</span>
|
|
<span className="text-[48px] font-extrabold leading-[110%] tracking-[-0.01em] font-inter bg-gradient-to-b from-[#111827] to-[#4b5563] bg-clip-text text-transparent dark:from-white dark:to-gray-400">
|
|
{dashboard.totalPoints.toLocaleString()}
|
|
</span>
|
|
</div>
|
|
<div className="bg-bg-subtle dark:bg-gray-700 rounded-2xl border border-white/5 dark:border-gray-600 px-6 h-28 flex items-center gap-12">
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.globalRank")}
|
|
</span>
|
|
<span className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
|
|
#{dashboard.globalRank.toLocaleString()}
|
|
</span>
|
|
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
|
|
Top {dashboard.topPercentage}
|
|
</span>
|
|
</div>
|
|
<div className="w-px h-8 bg-border-normal dark:bg-gray-600" />
|
|
<div className="flex flex-col gap-1">
|
|
<span className="text-[10px] font-bold text-text-tertiary dark:text-gray-400 leading-[150%] tracking-[0.01em]">
|
|
{t("points.endsIn")}
|
|
</span>
|
|
<span className="text-heading-h3 font-bold text-text-primary dark:text-white leading-[130%] tracking-[-0.005em]">
|
|
{dashboard.season.daysRemaining}d {dashboard.season.hoursRemaining}h
|
|
</span>
|
|
<span className="text-[10px] font-regular text-text-secondary dark:text-gray-300 leading-[150%] tracking-[0.01em]">
|
|
{new Date(dashboard.season.endTime).toLocaleDateString('en-US', { month: 'short', day: 'numeric' })}
|
|
</span>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{/* Right Section - VIP Card */}
|
|
<VipCard
|
|
memberTier={dashboard.memberTier}
|
|
level={dashboard.vipLevel}
|
|
currentPoints={dashboard.totalPoints}
|
|
totalPoints={dashboard.totalPoints + dashboard.pointsToNextTier}
|
|
nextTier={dashboard.nextTier}
|
|
pointsToNextTier={dashboard.pointsToNextTier}
|
|
/>
|
|
</div>
|
|
</div>
|
|
);
|
|
}
|