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 (
+
+ {/* 头像 */}
+
+
+
+
+ @{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);
+}