"use client"; import { useState, useEffect, useCallback } from "react"; import { useRouter } from "next/navigation"; import { motion, AnimatePresence } from "framer-motion"; import { ArrowLeft, Mail, Clock, Star, MapPin, Trash2, Loader2, ChevronDown, LogOut, Lock, Edit3, Check, X, Eye, EyeOff, Zap, ClipboardList, Heart, } from "lucide-react"; import EmptyState from "@/components/EmptyState"; import RestaurantImage from "@/components/RestaurantImage"; import { getUserId, getCachedProfile, setCachedProfile, setCachedPreferences, logout } from "@/lib/userId"; import { getAvatarBg, AVATARS } from "@/lib/avatars"; import type { UserProfile, UserPreferences, DecisionRecord, FavoriteRecord, Restaurant } from "@/types"; function firstImage(r: Restaurant): string { if (r.images?.length > 0) return r.images[0]; const legacy = (r as unknown as Record).image; return typeof legacy === "string" ? legacy : ""; } export default function ProfilePage() { const router = useRouter(); const [userId, setUserId] = useState(""); const [profile, setProfile] = useState<(UserProfile & { email?: string; preferences?: UserPreferences; decisionCount?: number }) | null>(null); const [loading, setLoading] = useState(true); const [history, setHistory] = useState([]); const [favorites, setFavorites] = useState([]); const [historyLoading, setHistoryLoading] = useState(false); const [favLoading, setFavLoading] = useState(false); const [editingUsername, setEditingUsername] = useState(false); const [newUsername, setNewUsername] = useState(""); const [usernameSaving, setUsernameSaving] = useState(false); const [usernameMsg, setUsernameMsg] = useState(""); const [editingPassword, setEditingPassword] = useState(false); const [currentPassword, setCurrentPassword] = useState(""); const [newPassword, setNewPassword] = useState(""); const [confirmPassword, setConfirmPassword] = useState(""); const [showPassword, setShowPassword] = useState(false); const [passwordSaving, setPasswordSaving] = useState(false); const [passwordMsg, setPasswordMsg] = useState(""); const [editingAvatar, setEditingAvatar] = useState(false); const [email, setEmail] = useState(""); const [emailSaving, setEmailSaving] = useState(false); const [emailMsg, setEmailMsg] = useState(""); const [showHistory, setShowHistory] = useState(true); const [showFavorites, setShowFavorites] = useState(true); const [toast, setToast] = useState(""); const showToast = useCallback((msg: string) => { setToast(msg); setTimeout(() => setToast(""), 2200); }, []); useEffect(() => { const cached = getCachedProfile(); if (!cached) { router.push("/"); return; } const id = getUserId(); setUserId(id); fetch(`/api/user?id=${id}`) .then((r) => r.json()) .then((data) => { if (data) { setProfile(data); setEmail(data.email ?? ""); setCachedProfile({ id: data.id, username: data.username, avatar: data.avatar }); if (data.preferences) setCachedPreferences(data.preferences); } else { router.push("/"); } }) .catch(() => { setProfile({ ...cached }); }) .finally(() => setLoading(false)); }, [router]); useEffect(() => { if (!userId) return; setHistoryLoading(true); fetch(`/api/user/history?userId=${userId}`) .then((r) => r.json()) .then(setHistory) .catch(() => {}) .finally(() => setHistoryLoading(false)); setFavLoading(true); fetch(`/api/user/favorite?userId=${userId}`) .then((r) => r.json()) .then(setFavorites) .catch(() => {}) .finally(() => setFavLoading(false)); }, [userId]); const handleSaveUsername = async () => { const trimmed = newUsername.trim(); if (trimmed.length < 2 || trimmed.length > 16) { setUsernameMsg("用户名需要 2-16 个字符"); return; } setUsernameSaving(true); setUsernameMsg(""); try { const res = await fetch("/api/user", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, username: trimmed }), }); const data = await res.json(); if (res.ok) { setProfile((prev) => prev ? { ...prev, username: trimmed } : prev); setCachedProfile({ id: userId, username: trimmed, avatar: profile!.avatar }); setEditingUsername(false); showToast("用户名已更新"); } else { setUsernameMsg(data.error ?? "更新失败"); } } catch { setUsernameMsg("网络错误"); } finally { setUsernameSaving(false); } }; const handleSavePassword = async () => { if (!currentPassword) { setPasswordMsg("请输入当前密码"); return; } if (newPassword.length < 6) { setPasswordMsg("新密码至少 6 个字符"); return; } if (newPassword !== confirmPassword) { setPasswordMsg("两次密码不一致"); return; } setPasswordSaving(true); setPasswordMsg(""); try { const res = await fetch("/api/user", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, currentPassword, newPassword }), }); const data = await res.json(); if (res.ok) { setEditingPassword(false); setCurrentPassword(""); setNewPassword(""); setConfirmPassword(""); showToast("密码已更新"); } else { setPasswordMsg(data.error ?? "更新失败"); } } catch { setPasswordMsg("网络错误"); } finally { setPasswordSaving(false); } }; const handleSaveAvatar = async (emoji: string) => { try { const res = await fetch("/api/user", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, avatar: emoji }), }); if (res.ok) { setProfile((prev) => prev ? { ...prev, avatar: emoji } : prev); setCachedProfile({ id: userId, username: profile!.username, avatar: emoji }); setEditingAvatar(false); showToast("头像已更新"); } } catch { showToast("更新失败"); } }; const handleSaveEmail = async () => { if (email && !/^[^\s@]+@[^\s@]+\.[^\s@]+$/.test(email)) { setEmailMsg("邮箱格式不正确"); return; } setEmailSaving(true); setEmailMsg(""); try { const res = await fetch("/api/user", { method: "PUT", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, email: email || null }), }); if (res.ok) { setEmailMsg(email ? "邮箱已绑定" : "邮箱已解绑"); } else { const data = await res.json().catch(() => ({})); setEmailMsg(data.error ?? "保存失败"); } } catch { setEmailMsg("网络错误"); } finally { setEmailSaving(false); } }; const handleRemoveFavorite = async (favId: string) => { try { await fetch("/api/user/favorite", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ userId, favoriteId: favId }), }); setFavorites((f) => f.filter((x) => x.id !== favId)); showToast("已取消收藏"); } catch { showToast("操作失败"); } }; const handleLogout = () => { logout(); router.push("/"); }; if (loading) { return (
); } if (!profile) return null; const amapNavUrl = (r: Restaurant) => r.location ? `https://uri.amap.com/marker?position=${r.location}&name=${encodeURIComponent(r.name)}&callnative=1` : `https://www.google.com/maps/search/?api=1&query=${encodeURIComponent(r.name)}`; return (
{/* Profile card */}
{editingUsername ? (
{ setNewUsername(e.target.value.slice(0, 16)); setUsernameMsg(""); }} maxLength={16} autoFocus className="h-8 flex-1 rounded-lg border-none bg-elevated px-2 text-sm text-white outline-none ring-1 ring-border focus:ring-2 focus:ring-accent/50" />
) : (

{profile.username}

)} {usernameMsg &&

{usernameMsg}

} {(profile.decisionCount ?? 0) > 0 && (

已拯救 {profile.decisionCount} 次选择困难症

)}
{/* Avatar picker */} {editingAvatar && (
{AVATARS.map((a) => ( ))}
)}
{/* Change password */} {editingPassword && (

当前密码

{ setCurrentPassword(e.target.value); setPasswordMsg(""); }} className="h-9 w-full rounded-lg border-none bg-elevated px-3 pr-9 text-sm text-heading outline-none ring-1 ring-border focus:ring-2 focus:ring-accent/50" />

新密码

{ setNewPassword(e.target.value); setPasswordMsg(""); }} placeholder="至少 6 个字符" className="mt-1 h-9 w-full rounded-lg border-none bg-elevated px-3 text-sm text-heading outline-none ring-1 ring-border placeholder:text-dim focus:ring-2 focus:ring-accent/50" />

确认新密码

{ setConfirmPassword(e.target.value); setPasswordMsg(""); }} placeholder="再次输入新密码" className="mt-1 h-9 w-full rounded-lg border-none bg-elevated px-3 text-sm text-heading outline-none ring-1 ring-border placeholder:text-dim focus:ring-2 focus:ring-accent/50" />
{passwordMsg && (

{passwordMsg}

)}
)}
{/* Email binding */}

绑定邮箱

(可选)
{ setEmail(e.target.value); setEmailMsg(""); }} className="h-9 flex-1 rounded-lg border-none bg-elevated px-3 text-sm text-heading outline-none ring-1 ring-border placeholder:text-dim focus:ring-2 focus:ring-accent/50" />
{emailMsg && (

{emailMsg}

)}
{/* Decision History */} {showHistory && ( {historyLoading ? (
) : history.length === 0 ? ( router.push("/blindbox")} color="purple" /> ) : ( )}
)}
{/* Favorites */} {showFavorites && ( {favLoading ? (
) : favorites.length === 0 ? ( router.push("/blindbox")} color="amber" /> ) : (
{favorites.map((f) => { const r = f.restaurantData; return (
{firstImage(r) && ( )}

{r.name}

{r.rating} {r.price} {r.distance && ( {r.distance} )}
); })}
)}
)}
{/* Logout */}
{toast && ( {toast} )}
); }