新增: - 书库导入导出(library_import/library_export)及测试 - 图片变体生成(image_variants)与响应式图片(responsiveImage) - 书籍搜索(bookSearch)+ BookSearch/LibrarySearchGrid 组件 - 小组件运行时(widgetRuntime)与静态检查(widgetLint) - 上传缓存中间件(upload_cache)与字体 CSS 提取脚本 其它: - 后端 handlers/services 全量调整 - 前端页面、组件、库函数与配置更新
63 lines
2.0 KiB
TypeScript
63 lines
2.0 KiB
TypeScript
"use client";
|
||
|
||
import { useEffect, useState } from "react";
|
||
import { densitySrcSet, pickVariantWidth, variantURL } from "@/lib/responsiveImage";
|
||
|
||
// 柔和多色头像:按用户名确定性取色,圆形软底,浅/暗主题在 globals.css 各自配色
|
||
function hash(s: string) {
|
||
let h = 0;
|
||
for (let i = 0; i < s.length; i++) h = (h * 31 + s.charCodeAt(i)) >>> 0;
|
||
return h;
|
||
}
|
||
|
||
export default function Avatar({
|
||
name,
|
||
size = 36,
|
||
src,
|
||
className = "",
|
||
square = false,
|
||
}: {
|
||
name: string;
|
||
size?: number;
|
||
src?: string | null;
|
||
className?: string;
|
||
/** 方形(带小圆角)轮廓,用于帖子列表行;默认圆形 */
|
||
square?: boolean;
|
||
}) {
|
||
const idx = hash(name || "?") % 8;
|
||
const initial = (name || "?").trim().charAt(0).toUpperCase();
|
||
// 图片 404(如附件已被彻底删除)时回退到字母头像,不显示裂图
|
||
const [imgFailed, setImgFailed] = useState(false);
|
||
// 切换到新的图片地址时重置失败状态
|
||
useEffect(() => setImgFailed(false), [src]);
|
||
const showImg = !!src && !imgFailed;
|
||
// showImg 为真时 src 必为字符串;统一收敛类型供变体 helper 使用
|
||
const imgSrc = src ?? "";
|
||
|
||
return (
|
||
<span
|
||
className={`inline-flex items-center justify-center shrink-0 select-none font-bold overflow-hidden av-${idx} ${
|
||
square ? "rounded-[9px]" : "rounded-full"
|
||
} ${className}`}
|
||
style={{ width: size, height: size, fontSize: size * 0.42 }}
|
||
aria-hidden="true"
|
||
>
|
||
{showImg ? (
|
||
// 源图 ≤512px,按显示尺寸取 1x/2x 变体(PageSpeed:38px 位不再下 512 原图)
|
||
// eslint-disable-next-line @next/next/no-img-element
|
||
<img
|
||
src={variantURL(imgSrc, pickVariantWidth(size))}
|
||
srcSet={densitySrcSet(imgSrc, size)}
|
||
alt={name.trim() || "用户头像"}
|
||
className="w-full h-full object-cover"
|
||
draggable={false}
|
||
onContextMenu={(e) => e.preventDefault()}
|
||
onError={() => setImgFailed(true)}
|
||
/>
|
||
) : (
|
||
initial
|
||
)}
|
||
</span>
|
||
);
|
||
}
|