Files
jiang13-bbs/frontend/components/AvatarPicker.tsx
freefire 24c6dea38c feat: 完成头像上传、站点主题配置与UI优化
- 新增附件模型与上传服务,拆分头像上传/管理逻辑
- 重构用户资料接口,移除直接修改头像字段,改用专用上传接口
- 添加站点公开设置API与管理员外观配置页
- 全局替换hover背景为accent-soft,优化交互一致性
- 完善头像组件,支持图片加载失败回退与src更新重置
- 新增用户状态事件总线,解决同页头像更新不及时问题
- 调整依赖包顺序与CI依赖版本
- 重构板块列表接口,新增帖子计数
- 迁移账号设置到用户主页tab,合并重复代码
2026-09-15 01:06:12 +08:00

76 lines
2.8 KiB
TypeScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"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;
}
/**
* 可点击的头像(资料设置表单内的唯一修改入口)。
*
* 同步策略(三条链路缺一不可):
* 1. 弹窗内上传/选用/删除成功 → 本组件 state 立即更新(本地即时反馈)
* 2. emitUserUpdate 广播 → Header 等同页其他客户端组件立即更新
* 3. router.refresh() → 服务端渲染区域(Hero、帖子列表等)重新取数
* 另外监听广播,使页面上若存在多个 picker 实例也保持一致;
* 并随 src prop(SSR 校正)同步,避免 useState 初始值造成数据漂移。
*/
export default function AvatarPicker({ name, src, size = 56 }: AvatarPickerProps) {
const router = useRouter();
const [open, setOpen] = useState(false);
const [current, setCurrent] = useState(src);
// SSR props 校正(router.refresh 后服务端数据回流)
useEffect(() => setCurrent(src), [src]);
// 同页其他位置(或另一实例)改了头像 → 同步本实例
useEffect(() => onUserUpdate((patch) => {
if (typeof patch.avatar === "string") setCurrent(patch.avatar);
}), []);
const handleChanged = (avatar: string) => {
setCurrent(avatar);
setOpen(false);
emitUserUpdate({ avatar });
router.refresh();
};
return (
<>
<button
type="button"
onClick={() => setOpen(true)}
className="group relative inline-flex items-center justify-center shrink-0 p-0 rounded-full cursor-pointer outline-none focus-visible:ring-2 focus-visible:ring-[var(--accent)] focus-visible:ring-offset-2 focus-visible:ring-offset-[var(--panel)]"
style={{ width: size, height: size, lineHeight: 0 }}
aria-label="更换头像"
title="更换头像"
>
<Avatar name={name} src={current || undefined} size={size} />
{/* hover/focus 遮罩:与头像严格重合(按钮宽高固定 = size) */}
<span
className="absolute inset-0 m-0 rounded-full flex flex-col items-center justify-center gap-0.5 opacity-0 group-hover:opacity-100 transition-opacity pointer-events-none"
style={{ background: "color-mix(in srgb, var(--ink) 55%, transparent)", color: "#fff" }}
>
<Camera size={Math.round(size * 0.27)} />
<span className="text-[10px] font-bold leading-none">更换</span>
</span>
</button>
<AvatarCropModal
open={open}
onClose={() => setOpen(false)}
currentAvatar={current}
onChanged={handleChanged}
/>
</>
);
}