- 新增附件模型与上传服务,拆分头像上传/管理逻辑 - 重构用户资料接口,移除直接修改头像字段,改用专用上传接口 - 添加站点公开设置API与管理员外观配置页 - 全局替换hover背景为accent-soft,优化交互一致性 - 完善头像组件,支持图片加载失败回退与src更新重置 - 新增用户状态事件总线,解决同页头像更新不及时问题 - 调整依赖包顺序与CI依赖版本 - 重构板块列表接口,新增帖子计数 - 迁移账号设置到用户主页tab,合并重复代码
60 lines
2.0 KiB
TypeScript
60 lines
2.0 KiB
TypeScript
import type { Area } from "react-easy-crop";
|
||
|
||
// 头像裁剪输出尺寸(正方形)。头像最大展示 88px,320px 足够覆盖高分屏
|
||
export const AVATAR_OUTPUT_SIZE = 320;
|
||
// 裁剪后 WebP 文件大小上限(与后端 service.AvatarMaxBytes 保持一致)
|
||
export const AVATAR_MAX_BYTES = 2 * 1024 * 1024;
|
||
|
||
function loadImage(src: string): Promise<HTMLImageElement> {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.addEventListener("load", () => resolve(img));
|
||
img.addEventListener("error", () => reject(new Error("图片加载失败,请换一张试试")));
|
||
img.src = src;
|
||
});
|
||
}
|
||
|
||
function canvasToWebp(canvas: HTMLCanvasElement, quality: number): Promise<Blob | null> {
|
||
return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality));
|
||
}
|
||
|
||
/**
|
||
* 按裁剪区域导出正方形 WebP。
|
||
* 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。
|
||
*/
|
||
export async function getCroppedAvatarWebp(
|
||
imageSrc: string,
|
||
pixelCrop: Area
|
||
): Promise<Blob> {
|
||
const image = await loadImage(imageSrc);
|
||
const canvas = document.createElement("canvas");
|
||
canvas.width = AVATAR_OUTPUT_SIZE;
|
||
canvas.height = AVATAR_OUTPUT_SIZE;
|
||
const ctx = canvas.getContext("2d");
|
||
if (!ctx) throw new Error("当前浏览器不支持图片裁剪");
|
||
|
||
ctx.imageSmoothingEnabled = true;
|
||
ctx.imageSmoothingQuality = "high";
|
||
ctx.drawImage(
|
||
image,
|
||
pixelCrop.x,
|
||
pixelCrop.y,
|
||
pixelCrop.width,
|
||
pixelCrop.height,
|
||
0,
|
||
0,
|
||
AVATAR_OUTPUT_SIZE,
|
||
AVATAR_OUTPUT_SIZE
|
||
);
|
||
|
||
let last: Blob | null = null;
|
||
for (const quality of [0.92, 0.85, 0.75, 0.65]) {
|
||
const blob = await canvasToWebp(canvas, quality);
|
||
if (!blob) throw new Error("当前浏览器不支持 WebP 导出,请使用最新版 Chrome/Edge/Safari");
|
||
last = blob;
|
||
if (blob.size <= AVATAR_MAX_BYTES) return blob;
|
||
}
|
||
if (last && last.size <= AVATAR_MAX_BYTES) return last;
|
||
throw new Error("裁剪后的图片仍超过 2MB,请缩小图片后再试");
|
||
}
|