From 4f87a7d36b31ebe2bbdb51c3a185974b8edf507f Mon Sep 17 00:00:00 2001 From: iye <1713042409@qq.com> Date: Tue, 12 May 2026 09:45:47 +0800 Subject: [PATCH] feat(me): /me user center with quota card, sign-in calendar, stats and fan support --- src/app/me/page.tsx | 64 ++++++++++++++++++++++++ src/components/me/MyFanSupport.tsx | 63 +++++++++++++++++++++++ src/components/me/QuotaCard.tsx | 67 +++++++++++++++++++++++++ src/components/me/SignInCalendar.tsx | 74 ++++++++++++++++++++++++++++ src/components/me/StatsGrid.tsx | 52 +++++++++++++++++++ src/components/me/UserHeader.tsx | 45 +++++++++++++++++ src/lib/mock-user.ts | 59 ++++++++++++++++++++++ 7 files changed, 424 insertions(+) create mode 100644 src/app/me/page.tsx create mode 100644 src/components/me/MyFanSupport.tsx create mode 100644 src/components/me/QuotaCard.tsx create mode 100644 src/components/me/SignInCalendar.tsx create mode 100644 src/components/me/StatsGrid.tsx create mode 100644 src/components/me/UserHeader.tsx create mode 100644 src/lib/mock-user.ts diff --git a/src/app/me/page.tsx b/src/app/me/page.tsx new file mode 100644 index 0000000..2ff8d55 --- /dev/null +++ b/src/app/me/page.tsx @@ -0,0 +1,64 @@ +"use client"; + +import { Gift, Star } from "lucide-react"; +import UserHeader from "@/components/me/UserHeader"; +import QuotaCard from "@/components/me/QuotaCard"; +import StatsGrid from "@/components/me/StatsGrid"; +import SignInCalendar from "@/components/me/SignInCalendar"; +import MyFanSupport from "@/components/me/MyFanSupport"; +import { MOCK_USER, getFanSupports } from "@/lib/mock-user"; + +export default function MePage() { + const user = MOCK_USER; + const supports = getFanSupports(); + + const handleInvite = () => { + // TODO: 邀请好友逻辑(生成专属海报 / 复制链接) + console.log("Invite friends"); + }; + + const handleSignIn = () => { + // TODO: 调用签到 API + console.log("Sign in"); + }; + + return ( +
+ + + + + + + {/* 签到 */} +
+
+ +

+ 每日签到 +

+
+ +
+ + {/* 我的应援 */} +
+
+ +

+ 我的应援 +

+
+ +
+
+ ); +} diff --git a/src/components/me/MyFanSupport.tsx b/src/components/me/MyFanSupport.tsx new file mode 100644 index 0000000..dd88058 --- /dev/null +++ b/src/components/me/MyFanSupport.tsx @@ -0,0 +1,63 @@ +import Link from "next/link"; +import { AlertTriangle } from "lucide-react"; +import type { FanSupport } from "@/lib/mock-user"; +import ArtistPortrait from "@/components/cards/ArtistPortrait"; +import { cn } from "@/lib/cn"; + +export default function MyFanSupport({ supports }: { supports: FanSupport[] }) { + if (supports.length === 0) { + return ( +
+ 还没有应援的艺人 ·{" "} + + 去发现 → + +
+ ); + } + + return ( +
+ {supports.map(({ artist, votedCount }) => { + const inTop12 = artist.rank <= 12; + return ( + +
+ +
+
+
+ {artist.name} +
+
+ 已投 {votedCount} 票 +
+
+ 当前 #{artist.rank} + {!inTop12 && } +
+
+ + ); + })} +
+ ); +} diff --git a/src/components/me/QuotaCard.tsx b/src/components/me/QuotaCard.tsx new file mode 100644 index 0000000..f961c8b --- /dev/null +++ b/src/components/me/QuotaCard.tsx @@ -0,0 +1,67 @@ +"use client"; + +import { Gift, Users } from "lucide-react"; + +interface QuotaCardProps { + remaining: number; + daily: number; + onInvite?: () => void; +} + +export default function QuotaCard({ + remaining, + daily, + onInvite, +}: QuotaCardProps) { + return ( +
+ {/* 装饰星点 */} + + + ✧ + + + {/* 高光 */} +
+ +
+
+
+ + 今日剩余票数 +
+
+ {remaining}{" "} + +
+
+ 明日 00:00 自动重置为 {daily} 票 +
+
+ +
+ + 获取更多票数 + + +
+
+
+ ); +} diff --git a/src/components/me/SignInCalendar.tsx b/src/components/me/SignInCalendar.tsx new file mode 100644 index 0000000..e97a5c9 --- /dev/null +++ b/src/components/me/SignInCalendar.tsx @@ -0,0 +1,74 @@ +"use client"; + +import { Check, Gift } from "lucide-react"; +import { cn } from "@/lib/cn"; + +interface SignInCalendarProps { + weekly: boolean[]; + todaySigned: boolean; + /** 今天是周几(0 周日 ~ 6 周六),用 1~7 对应周一~周日 */ + todayIndex?: number; + onSignIn?: () => void; +} + +const WEEK_LABELS = ["周一", "周二", "周三", "周四", "周五", "周六", "周日"]; + +export default function SignInCalendar({ + weekly, + todaySigned, + todayIndex, + onSignIn, +}: SignInCalendarProps) { + // 默认今天 = 数组中第一个未签到(或最后一个 true 之后) + const computedToday = + todayIndex ?? + (weekly.indexOf(false) === -1 ? weekly.length - 1 : weekly.indexOf(false)); + + return ( +
+
+ {WEEK_LABELS.map((label, i) => { + const signed = weekly[i]; + const isToday = i === computedToday; + return ( + + ); + })} +
+ +

+ 连续签到 7 天可获得额外票数奖励 · 中断后从头计算 +

+
+ ); +} diff --git a/src/components/me/StatsGrid.tsx b/src/components/me/StatsGrid.tsx new file mode 100644 index 0000000..1eba8dd --- /dev/null +++ b/src/components/me/StatsGrid.tsx @@ -0,0 +1,52 @@ +import { Heart, Star, Calendar, UserPlus } from "lucide-react"; +import type { MockUser } from "@/lib/mock-user"; + +const ICON_MAP = { + votes: , + fan: , + signin: , + invite: , +}; + +export default function StatsGrid({ user }: { user: MockUser }) { + const stats = [ + { key: "votes", label: "累计投票", value: user.totalVotes, icon: ICON_MAP.votes }, + { + key: "fan", + label: "应援艺人", + value: user.supportingIds.length, + icon: ICON_MAP.fan, + }, + { + key: "signin", + label: "签到天数", + value: user.signInStreak, + icon: ICON_MAP.signin, + }, + { + key: "invite", + label: "邀请好友", + value: user.invitedCount, + icon: ICON_MAP.invite, + }, + ]; + + return ( +
+ {stats.map((s) => ( +
+
+ {s.value} +
+
+ {s.icon} + {s.label} +
+
+ ))} +
+ ); +} diff --git a/src/components/me/UserHeader.tsx b/src/components/me/UserHeader.tsx new file mode 100644 index 0000000..f1a9d5b --- /dev/null +++ b/src/components/me/UserHeader.tsx @@ -0,0 +1,45 @@ +import { Pencil } from "lucide-react"; +import type { MockUser } from "@/lib/mock-user"; + +export default function UserHeader({ user }: { user: MockUser }) { + const initial = user.nickname.charAt(0).toUpperCase(); + return ( +
+ {/* 头像 */} +
+
+ {initial} +
+
+ +
+
+ @{user.nickname} +
+
+ ID: {user.id} + | + 已连续签到{" "} + + {user.signInStreak} + {" "} + 天 +
+
+ + +
+ ); +} diff --git a/src/lib/mock-user.ts b/src/lib/mock-user.ts new file mode 100644 index 0000000..9bc8734 --- /dev/null +++ b/src/lib/mock-user.ts @@ -0,0 +1,59 @@ +import { ARTISTS } from "./mock-data"; +import type { Artist } from "@/types/artist"; + +export interface MockUser { + id: string; + nickname: string; + /** 头像 URL(无则使用首字母占位) */ + avatar: string; + signInStreak: number; + /** 今日签到状态:每天 true/false(按周一开始的一周 7 天) */ + weeklySignIn: boolean[]; + /** 今日是否已签到 */ + todaySignedIn: boolean; + /** 今日剩余票数 */ + remainingVotes: number; + /** 今日已用票数 */ + usedVotes: number; + /** 每日基础票数 */ + dailyQuota: number; + /** 累计投票数 */ + totalVotes: number; + /** 应援的艺人 ID 列表 */ + supportingIds: string[]; + /** 邀请好友数 */ + invitedCount: number; +} + +export interface FanSupport { + artist: Artist; + votedCount: number; +} + +export const MOCK_USER: MockUser = { + id: "12345678", + nickname: "粉丝昵称", + avatar: "", + signInStreak: 7, + weeklySignIn: [true, true, true, true, true, true, false], + todaySignedIn: false, + remainingVotes: 9, + usedVotes: 3, + dailyQuota: 12, + totalVotes: 87, + supportingIds: ["001", "005", "014"], + invitedCount: 2, +}; + +export function getFanSupports(): FanSupport[] { + return MOCK_USER.supportingIds + .map((id) => { + const artist = ARTISTS.find((a) => a.id === id); + if (!artist) return null; + return { + artist, + votedCount: Math.floor(Math.random() * 40) + 10, + }; + }) + .filter((x): x is FanSupport => x !== null); +}