feat: 完成头像上传、站点主题配置与UI优化
- 新增附件模型与上传服务,拆分头像上传/管理逻辑 - 重构用户资料接口,移除直接修改头像字段,改用专用上传接口 - 添加站点公开设置API与管理员外观配置页 - 全局替换hover背景为accent-soft,优化交互一致性 - 完善头像组件,支持图片加载失败回退与src更新重置 - 新增用户状态事件总线,解决同页头像更新不及时问题 - 调整依赖包顺序与CI依赖版本 - 重构板块列表接口,新增帖子计数 - 迁移账号设置到用户主页tab,合并重复代码
This commit is contained in:
59
frontend/lib/cropImage.ts
Normal file
59
frontend/lib/cropImage.ts
Normal file
@@ -0,0 +1,59 @@
|
||||
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,请缩小图片后再试");
|
||||
}
|
||||
Reference in New Issue
Block a user