refactor: 提取 UserAvatar、Input、Card 三个可复用 UI 组件
- UserAvatar: 统一头像渲染(5 种尺寸),新增 resolveAvatar 工具函数,替换 SwipeDeck 和 RoomManageModal 中的重复逻辑 - Input: 统一表单输入样式(4 种尺寸 + default/purple 变体),替换 AuthModal、ProfilePage、BlindboxPage 共 12 处 - Card: 统一卡片容器样式 + 可选淡入动画和延迟,替换 ProfilePage 中 7 处重复的 motion.div
This commit is contained in:
@@ -15,6 +15,7 @@ import {
|
||||
import { getCachedProfile, isRegistered } from "@/lib/userId";
|
||||
import AuthModal from "@/components/AuthModal";
|
||||
import Button from "@/components/Button";
|
||||
import Input from "@/components/Input";
|
||||
import { BlindboxListSkeleton } from "@/components/Skeleton";
|
||||
import type { UserProfile } from "@/types";
|
||||
|
||||
@@ -244,7 +245,7 @@ export default function BlindboxLobbyPage() {
|
||||
{/* Inline create form */}
|
||||
<div className="mt-7 w-full max-w-xs">
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="我们的周末"
|
||||
value={createName}
|
||||
@@ -254,7 +255,9 @@ export default function BlindboxLobbyPage() {
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }}
|
||||
maxLength={30}
|
||||
className="h-11 flex-1 rounded-xl border-none bg-surface px-4 text-sm text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
|
||||
size="xl"
|
||||
variant="purple"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
@@ -275,7 +278,7 @@ export default function BlindboxLobbyPage() {
|
||||
</div>
|
||||
|
||||
<div className="mt-3 flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="6 位房间号"
|
||||
value={joinCode}
|
||||
@@ -285,7 +288,9 @@ export default function BlindboxLobbyPage() {
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }}
|
||||
maxLength={6}
|
||||
className="h-11 flex-1 rounded-xl border-none bg-surface px-4 text-center font-mono text-sm tracking-[0.15em] text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
|
||||
size="xl"
|
||||
variant="purple"
|
||||
className="flex-1 text-center font-mono tracking-[0.15em]"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleJoin}
|
||||
@@ -321,7 +326,7 @@ export default function BlindboxLobbyPage() {
|
||||
>
|
||||
{/* Create row */}
|
||||
<div className="flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="新房间名称"
|
||||
value={createName}
|
||||
@@ -331,7 +336,9 @@ export default function BlindboxLobbyPage() {
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleCreate(); }}
|
||||
maxLength={30}
|
||||
className="h-10 flex-1 rounded-xl border-none bg-surface px-3 text-sm text-foreground outline-none ring-1 ring-border transition-all placeholder:text-dim focus:ring-2 focus:ring-purple-600"
|
||||
size="lg"
|
||||
variant="purple"
|
||||
className="flex-1"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleCreate}
|
||||
@@ -345,7 +352,7 @@ export default function BlindboxLobbyPage() {
|
||||
|
||||
{/* Join row */}
|
||||
<div className="mt-2 flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
placeholder="输入 6 位房间号加入"
|
||||
value={joinCode}
|
||||
@@ -355,7 +362,9 @@ export default function BlindboxLobbyPage() {
|
||||
}}
|
||||
onKeyDown={(e) => { if (e.key === "Enter") handleJoin(); }}
|
||||
maxLength={6}
|
||||
className="h-10 flex-1 rounded-xl border-none bg-surface px-3 text-center font-mono text-sm tracking-[0.15em] text-foreground outline-none ring-1 ring-border transition-all placeholder:font-sans placeholder:tracking-normal placeholder:text-dim focus:ring-2 focus:ring-purple-600"
|
||||
size="lg"
|
||||
variant="purple"
|
||||
className="flex-1 text-center font-mono tracking-[0.15em] placeholder:font-sans placeholder:tracking-normal"
|
||||
/>
|
||||
<Button
|
||||
onClick={handleJoin}
|
||||
|
||||
+27
-48
@@ -24,6 +24,8 @@ import {
|
||||
Heart,
|
||||
} from "lucide-react";
|
||||
import EmptyState from "@/components/EmptyState";
|
||||
import Card from "@/components/Card";
|
||||
import Input from "@/components/Input";
|
||||
import { useToast } from "@/hooks/useToast";
|
||||
import RestaurantImage from "@/components/RestaurantImage";
|
||||
import { ProfileCardSkeleton, RecordItemSkeleton } from "@/components/Skeleton";
|
||||
@@ -261,18 +263,18 @@ export default function ProfilePage() {
|
||||
</nav>
|
||||
<div className="mx-auto max-w-sm px-5">
|
||||
<ProfileCardSkeleton />
|
||||
<div className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border">
|
||||
<Card className="mt-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<RecordItemSkeleton />
|
||||
<RecordItemSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
<div className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border">
|
||||
</Card>
|
||||
<Card className="mt-4">
|
||||
<div className="flex flex-col gap-2">
|
||||
<RecordItemSkeleton />
|
||||
<RecordItemSkeleton />
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -295,11 +297,7 @@ export default function ProfilePage() {
|
||||
|
||||
<div className="mx-auto max-w-sm px-5">
|
||||
{/* Profile card */}
|
||||
<motion.div
|
||||
className="rounded-2xl bg-surface p-4 ring-1 ring-border"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
>
|
||||
<Card animated>
|
||||
<div className="flex items-center gap-4">
|
||||
<button
|
||||
onClick={() => setEditingAvatar(!editingAvatar)}
|
||||
@@ -313,7 +311,7 @@ export default function ProfilePage() {
|
||||
<div className="flex-1">
|
||||
{editingUsername ? (
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="text"
|
||||
value={newUsername}
|
||||
onChange={(e) => {
|
||||
@@ -322,7 +320,8 @@ export default function ProfilePage() {
|
||||
}}
|
||||
maxLength={16}
|
||||
autoFocus
|
||||
className="h-8 flex-1 rounded-lg border-none bg-elevated px-2 text-sm text-foreground outline-none ring-1 ring-border focus:ring-2 focus:ring-accent/50"
|
||||
size="sm"
|
||||
className="flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveUsername}
|
||||
@@ -387,15 +386,10 @@ export default function ProfilePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Card>
|
||||
|
||||
{/* Change password */}
|
||||
<motion.div
|
||||
className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.05 }}
|
||||
>
|
||||
<Card animated className="mt-4" delay={0.05}>
|
||||
<button
|
||||
onClick={() => { setEditingPassword(!editingPassword); setPasswordMsg(""); }}
|
||||
className="flex w-full items-center gap-2"
|
||||
@@ -417,11 +411,11 @@ export default function ProfilePage() {
|
||||
<div>
|
||||
<p className="text-xs text-muted">当前密码</p>
|
||||
<div className="relative mt-1">
|
||||
<input
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={currentPassword}
|
||||
onChange={(e) => { 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"
|
||||
className="pr-9"
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
@@ -434,22 +428,22 @@ export default function ProfilePage() {
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted">新密码</p>
|
||||
<input
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={newPassword}
|
||||
onChange={(e) => { 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"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
<div>
|
||||
<p className="text-xs text-muted">确认新密码</p>
|
||||
<input
|
||||
<Input
|
||||
type={showPassword ? "text" : "password"}
|
||||
value={confirmPassword}
|
||||
onChange={(e) => { 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"
|
||||
className="mt-1"
|
||||
/>
|
||||
</div>
|
||||
|
||||
@@ -470,22 +464,17 @@ export default function ProfilePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Card>
|
||||
|
||||
{/* Email binding */}
|
||||
<motion.div
|
||||
className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.1 }}
|
||||
>
|
||||
<Card animated className="mt-4" delay={0.1}>
|
||||
<div className="flex items-center gap-2">
|
||||
<Mail size={15} className="text-muted" />
|
||||
<h3 className="text-sm font-semibold text-secondary">绑定邮箱</h3>
|
||||
<span className="text-[10px] text-dim">(可选)</span>
|
||||
</div>
|
||||
<div className="mt-3 flex gap-2">
|
||||
<input
|
||||
<Input
|
||||
type="email"
|
||||
placeholder="your@email.com"
|
||||
value={email}
|
||||
@@ -493,7 +482,7 @@ export default function ProfilePage() {
|
||||
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"
|
||||
className="flex-1"
|
||||
/>
|
||||
<button
|
||||
onClick={handleSaveEmail}
|
||||
@@ -508,15 +497,10 @@ export default function ProfilePage() {
|
||||
{emailMsg}
|
||||
</p>
|
||||
)}
|
||||
</motion.div>
|
||||
</Card>
|
||||
|
||||
{/* Decision History */}
|
||||
<motion.div
|
||||
className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.15 }}
|
||||
>
|
||||
<Card animated className="mt-4" delay={0.15}>
|
||||
<button
|
||||
onClick={() => setShowHistory((v) => !v)}
|
||||
className="flex w-full items-center justify-between"
|
||||
@@ -591,15 +575,10 @@ export default function ProfilePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Card>
|
||||
|
||||
{/* Favorites */}
|
||||
<motion.div
|
||||
className="mt-4 rounded-2xl bg-surface p-4 ring-1 ring-border"
|
||||
initial={{ y: 10, opacity: 0 }}
|
||||
animate={{ y: 0, opacity: 1 }}
|
||||
transition={{ delay: 0.2 }}
|
||||
>
|
||||
<Card animated className="mt-4" delay={0.2}>
|
||||
<button
|
||||
onClick={() => setShowFavorites((v) => !v)}
|
||||
className="flex w-full items-center justify-between"
|
||||
@@ -688,7 +667,7 @@ export default function ProfilePage() {
|
||||
</motion.div>
|
||||
)}
|
||||
</AnimatePresence>
|
||||
</motion.div>
|
||||
</Card>
|
||||
|
||||
{/* Logout */}
|
||||
<motion.div
|
||||
|
||||
Reference in New Issue
Block a user