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 { 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 { return new Promise((resolve) => canvas.toBlob(resolve, "image/webp", quality)); } /** * 按裁剪区域导出正方形 WebP。 * 自动按 0.92 → 0.65 降质量,直到满足大小上限;仍超限则抛出错误。 */ export async function getCroppedAvatarWebp( imageSrc: string, pixelCrop: Area ): Promise { 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,请缩小图片后再试"); }