ui: 个人中心空状态插图优化,替换纯文字为图标动画 + CTA 按钮
提取可复用 EmptyState 组件,决策记录和收藏餐厅空状态 增加浮动图标、光晕动画和"去创建第一个房间"引导按钮
This commit is contained in:
@@ -20,7 +20,10 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
EyeOff,
|
EyeOff,
|
||||||
Zap,
|
Zap,
|
||||||
|
ClipboardList,
|
||||||
|
Heart,
|
||||||
} from "lucide-react";
|
} from "lucide-react";
|
||||||
|
import EmptyState from "@/components/EmptyState";
|
||||||
import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId";
|
import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId";
|
||||||
import { getAvatarBg, AVATARS } from "@/lib/avatars";
|
import { getAvatarBg, AVATARS } from "@/lib/avatars";
|
||||||
import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types";
|
import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types";
|
||||||
@@ -534,9 +537,14 @@ export default function ProfilePage() {
|
|||||||
<Loader2 size={18} className="animate-spin text-muted" />
|
<Loader2 size={18} className="animate-spin text-muted" />
|
||||||
</div>
|
</div>
|
||||||
) : history.length === 0 ? (
|
) : history.length === 0 ? (
|
||||||
<p className="py-6 text-center text-xs text-muted">
|
<EmptyState
|
||||||
还没有决策记录
|
icon={ClipboardList}
|
||||||
</p>
|
title="还没有决策记录"
|
||||||
|
subtitle="创建房间开始一起选餐厅"
|
||||||
|
ctaLabel="去创建第一个房间"
|
||||||
|
onCta={() => router.push("/blindbox")}
|
||||||
|
color="purple"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{history.map((d) => (
|
{history.map((d) => (
|
||||||
@@ -612,9 +620,14 @@ export default function ProfilePage() {
|
|||||||
<Loader2 size={18} className="animate-spin text-muted" />
|
<Loader2 size={18} className="animate-spin text-muted" />
|
||||||
</div>
|
</div>
|
||||||
) : favorites.length === 0 ? (
|
) : favorites.length === 0 ? (
|
||||||
<p className="py-6 text-center text-xs text-muted">
|
<EmptyState
|
||||||
还没有收藏的餐厅
|
icon={Heart}
|
||||||
</p>
|
title="还没有收藏的餐厅"
|
||||||
|
subtitle="在匹配结果中收藏喜欢的店"
|
||||||
|
ctaLabel="去创建第一个房间"
|
||||||
|
onCta={() => router.push("/blindbox")}
|
||||||
|
color="amber"
|
||||||
|
/>
|
||||||
) : (
|
) : (
|
||||||
<div className="mt-3 flex flex-col gap-2">
|
<div className="mt-3 flex flex-col gap-2">
|
||||||
{favorites.map((f) => {
|
{favorites.map((f) => {
|
||||||
|
|||||||
@@ -0,0 +1,58 @@
|
|||||||
|
"use client";
|
||||||
|
|
||||||
|
import { motion } from "framer-motion";
|
||||||
|
import type { LucideIcon } from "lucide-react";
|
||||||
|
|
||||||
|
interface EmptyStateProps {
|
||||||
|
icon: LucideIcon;
|
||||||
|
title: string;
|
||||||
|
subtitle?: string;
|
||||||
|
ctaLabel?: string;
|
||||||
|
onCta?: () => void;
|
||||||
|
color?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export default function EmptyState({
|
||||||
|
icon: Icon,
|
||||||
|
title,
|
||||||
|
subtitle,
|
||||||
|
ctaLabel,
|
||||||
|
onCta,
|
||||||
|
color = "purple",
|
||||||
|
}: EmptyStateProps) {
|
||||||
|
const colorMap: Record<string, { glow: string; icon: string; btn: string; btnHover: string }> = {
|
||||||
|
purple: { glow: "bg-purple-600/15", icon: "text-purple-400/60", btn: "bg-purple-600 hover:bg-purple-500", btnHover: "" },
|
||||||
|
amber: { glow: "bg-amber-500/15", icon: "text-amber-400/60", btn: "bg-amber-600 hover:bg-amber-500", btnHover: "" },
|
||||||
|
rose: { glow: "bg-rose-500/15", icon: "text-rose-400/60", btn: "bg-rose-600 hover:bg-rose-500", btnHover: "" },
|
||||||
|
sky: { glow: "bg-sky-500/15", icon: "text-sky-400/60", btn: "bg-sky-600 hover:bg-sky-500", btnHover: "" },
|
||||||
|
};
|
||||||
|
const c = colorMap[color] ?? colorMap.purple;
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="flex flex-col items-center py-6">
|
||||||
|
<motion.div
|
||||||
|
className="relative flex h-14 w-14 items-center justify-center"
|
||||||
|
animate={{ y: [0, -3, 0] }}
|
||||||
|
transition={{ duration: 2.5, repeat: Infinity, ease: "easeInOut" }}
|
||||||
|
>
|
||||||
|
<div className={`absolute inset-0 rounded-xl ${c.glow} blur-md`} />
|
||||||
|
<Icon size={24} className={`relative ${c.icon}`} strokeWidth={1.5} />
|
||||||
|
</motion.div>
|
||||||
|
|
||||||
|
<p className="mt-3 text-sm font-semibold text-secondary">{title}</p>
|
||||||
|
{subtitle && (
|
||||||
|
<p className="mt-1 text-xs text-muted">{subtitle}</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{ctaLabel && onCta && (
|
||||||
|
<motion.button
|
||||||
|
onClick={onCta}
|
||||||
|
className={`mt-4 rounded-lg px-4 py-1.5 text-xs font-semibold text-white transition-colors ${c.btn}`}
|
||||||
|
whileTap={{ scale: 0.95 }}
|
||||||
|
>
|
||||||
|
{ctaLabel}
|
||||||
|
</motion.button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user