feat(lovart-home): add hover avatar dropdown menu
Some checks failed
Lint / build (20) (push) Has been cancelled
Playwright Tests / test (push) Has been cancelled

This commit is contained in:
Your Name 2026-02-14 01:35:54 +08:00
parent 310dd115b2
commit b30ebc58b9

View File

@ -2,7 +2,7 @@
import { Fragment, useEffect, useMemo, useRef, useState } from "react";
import { useRouter } from "next/navigation";
import { signIn } from "next-auth/react";
import { signIn, signOut } from "next-auth/react";
import useSWR from "swr";
import useSWRInfinite from "swr/infinite";
import {
@ -12,11 +12,12 @@ import {
ChevronDown,
Clapperboard,
Folder,
Heart,
Home,
ImageIcon,
Info,
Loader2,
LogOut,
Mail,
Monitor,
Palette,
Plus,
@ -26,6 +27,7 @@ import {
Star,
Upload,
User,
Users,
X,
Zap,
} from "lucide-react";
@ -161,6 +163,12 @@ const PROMPT_TAGS = [
const MASONRY_HEIGHTS = [420, 290, 340, 470, 300, 360, 430, 320];
const FEATURED_PAGE_SIZE = 24;
function getUserInitials(user: HomeUser | null) {
const source = (user?.name || user?.email || "mu").trim();
if (!source) return "mu";
return source.slice(0, 2).toLowerCase();
}
function formatRelativeTime(value?: string) {
if (!value) return "更新于今天";
const date = new Date(value);
@ -179,6 +187,29 @@ function formatPrice(currency: string, value: number) {
return `${currency}${value}`;
}
function AccountMenuItem({
icon: Icon,
label,
onClick,
}: {
icon: typeof User;
label: string;
onClick: () => void;
}) {
return (
<button
type="button"
onClick={onClick}
className="flex w-full items-center gap-3 rounded-2xl px-3 py-2 text-left text-[15px] font-medium text-black/80 hover:bg-black/5"
>
<span className="flex h-9 w-9 items-center justify-center rounded-xl bg-black/5 text-black/70">
<Icon className="h-[18px] w-[18px]" />
</span>
<span className="flex-1">{label}</span>
</button>
);
}
export function LovartHome({
user,
isAuthenticated,
@ -669,6 +700,12 @@ export function LovartHome({
previewCursorVisible ? " |" : ""
}`;
const userInitials = useMemo(() => getUserInitials(user), [user]);
const userDisplayName = useMemo(() => {
if (!user) return "";
return (user.name || user.email || "用户").toString();
}, [user]);
return (
<div className="min-h-svh bg-[#f4f4f4] text-[#171717]">
<header className="fixed left-0 top-0 z-40 h-[68px] w-full bg-[#f4f4f4]/90 backdrop-blur">
@ -754,9 +791,94 @@ export function LovartHome({
</HoverCard>
{isAuthenticated ? (
<button className="flex h-8 w-8 items-center justify-center rounded-full bg-[#d4551f] text-[14px] font-semibold text-white">
{(user?.name || user?.email || "mu").slice(0, 2).toLowerCase()}
</button>
<HoverCard openDelay={80} closeDelay={120}>
<HoverCardTrigger asChild>
<button
type="button"
className="flex h-8 w-8 items-center justify-center rounded-full bg-[#d4551f] text-[14px] font-semibold text-white"
aria-label="Account menu"
>
{userInitials}
</button>
</HoverCardTrigger>
<HoverCardContent
align="end"
sideOffset={12}
className="w-[360px] rounded-3xl border border-black/10 bg-white p-0 shadow-xl"
>
<div className="p-4">
<div className="rounded-3xl border border-black/10 bg-white p-4">
<div className="flex justify-center">
<div className="flex h-14 w-14 items-center justify-center rounded-full bg-[#d4551f] text-[18px] font-semibold text-white">
{userInitials}
</div>
</div>
<div className="mt-3 flex items-center justify-between gap-3">
<div className="min-w-0">
<div className="truncate text-[18px] font-semibold text-black">
{userDisplayName}
</div>
<div className="truncate text-[14px] text-black/45">
{user?.email || ""}
</div>
</div>
<Button
size="sm"
className="h-9 rounded-2xl bg-[#2f3542] px-6 text-[14px] text-white hover:bg-[#242a36]"
onClick={() => setUpgradeDialogOpen(true)}
>
升级
</Button>
</div>
<div className="mt-4 border-t border-black/10 pt-3">
<button
type="button"
className="flex w-full items-center justify-between rounded-2xl px-1 py-1 text-[15px] font-semibold text-black/80 hover:bg-black/5"
onClick={() => setUpgradeDialogOpen(true)}
>
<span>积分</span>
<span className="inline-flex items-center gap-2 text-[15px] font-semibold text-black/60">
{membershipData?.summary?.currentCredits ?? 0}
<ArrowRight className="h-4 w-4" />
</span>
</button>
</div>
</div>
<div className="mt-3 space-y-1">
<AccountMenuItem
icon={Users}
label="创建团队"
onClick={() => toast.info("团队功能后续开放")}
/>
<AccountMenuItem
icon={User}
label="账户管理"
onClick={() => toast.info("账户管理后续开放")}
/>
<AccountMenuItem
icon={Info}
label="使用指南"
onClick={() => toast.info("使用指南后续开放")}
/>
<AccountMenuItem
icon={Mail}
label="联系我们"
onClick={() => toast.info("联系我们后续开放")}
/>
<AccountMenuItem
icon={LogOut}
label="退出登录"
onClick={() => {
void signOut({ redirect: true, callbackUrl: "/" });
}}
/>
</div>
</div>
</HoverCardContent>
</HoverCard>
) : (
<button
type="button"