"use client"; import { useEffect, useState } from "react"; import { useRouter } from "next/navigation"; import { Camera } from "lucide-react"; import Avatar from "@/components/Avatar"; import AvatarCropModal from "@/components/AvatarCropModal"; import { emitUserUpdate, onUserUpdate } from "@/lib/userEvents"; interface AvatarPickerProps { name: string; src: string; size?: number; /** 外部拖入的文件:非空时打开裁剪弹窗并进入裁剪 */ incomingFile?: File | null; onIncomingConsumed?: () => void; } /** * 可点击的头像(资料设置表单内的唯一修改入口)。 * * 同步策略(三条链路缺一不可): * 1. 弹窗内上传/选用/删除成功 → 本组件 state 立即更新(本地即时反馈) * 2. emitUserUpdate 广播 → Header 等同页其他客户端组件立即更新 * 3. router.refresh() → 服务端渲染区域(Hero、帖子列表等)重新取数 * 另外监听广播,使页面上若存在多个 picker 实例也保持一致; * 并随 src prop(SSR 校正)同步,避免 useState 初始值造成数据漂移。 */ export default function AvatarPicker({ name, src, size = 56, incomingFile, onIncomingConsumed, }: AvatarPickerProps) { const router = useRouter(); const [open, setOpen] = useState(false); const [current, setCurrent] = useState(src); const [seedFile, setSeedFile] = useState(null); // SSR props 校正(router.refresh 后服务端数据回流) useEffect(() => setCurrent(src), [src]); // 同页其他位置(或另一实例)改了头像 → 同步本实例 useEffect(() => onUserUpdate((patch) => { if (typeof patch.avatar === "string") setCurrent(patch.avatar); }), []); // 外层拖入:打开弹窗并交给裁剪流程 useEffect(() => { if (!incomingFile) return; setSeedFile(incomingFile); setOpen(true); }, [incomingFile]); const handleChanged = (avatar: string) => { setCurrent(avatar); setOpen(false); setSeedFile(null); emitUserUpdate({ avatar }); router.refresh(); }; const handleSeedConsumed = () => { setSeedFile(null); onIncomingConsumed?.(); }; return ( <> { setOpen(false); setSeedFile(null); }} currentAvatar={current} onChanged={handleChanged} seedFile={seedFile} onSeedConsumed={handleSeedConsumed} /> ); }