diff --git a/frontend/src/components/AvatarCropDialog.tsx b/frontend/src/components/AvatarCropDialog.tsx
index dbdc862..78a7ea2 100644
--- a/frontend/src/components/AvatarCropDialog.tsx
+++ b/frontend/src/components/AvatarCropDialog.tsx
@@ -73,7 +73,7 @@ export default function AvatarCropDialog({
裁剪头像
- 拖动图片调整位置,滚轮或滑块缩放。GIF 裁剪后将变为静态 JPG。
+ 拖动图片调整位置,滚轮或滑块缩放。裁剪结果按原图格式保存(JPG→JPEG,PNG 保留透明);GIF 裁剪后变为静态 JPG。服务端会额外生成 WebP。
diff --git a/frontend/src/utils/avatarCrop.ts b/frontend/src/utils/avatarCrop.ts
index 02144a7..cb40049 100644
--- a/frontend/src/utils/avatarCrop.ts
+++ b/frontend/src/utils/avatarCrop.ts
@@ -40,11 +40,13 @@ export function validateAvatarOutput(file: File, maxMb: number): string | null {
return null;
}
-/** 将裁剪区域渲染为 WebP 文件(体积更小;不支持时回退 JPEG) */
+/** 将裁剪区域渲染为上传「原图」;服务端再衍生 WebP。
+ * 优先保留源格式:PNG 保透明;其余(含 JPG/GIF/WebP)导出为 JPEG。
+ */
export async function getCroppedAvatarFile(
imageSrc: string,
pixelCrop: Area,
- originalName = 'avatar.webp',
+ originalName = 'avatar.jpg',
): Promise {
const image = await loadImage(imageSrc);
const canvas = document.createElement('canvas');
@@ -67,13 +69,21 @@ export async function getCroppedAvatarFile(
size,
);
- const tryTypes: { mime: string; quality: number; ext: string }[] = [
- { mime: 'image/webp', quality: 0.86, ext: 'webp' },
- { mime: 'image/jpeg', quality: 0.92, ext: 'jpg' },
- ];
+ const srcExt = (originalName.match(/\.([^.]+)$/)?.[1] || '').toLowerCase();
+ const preferPng = srcExt === 'png';
+ // 勿优先 WebP:否则服务端只会存一份 WebP,丢失用户原图格式
+ const tryTypes: { mime: string; quality?: number; ext: string }[] = preferPng
+ ? [
+ { mime: 'image/png', ext: 'png' },
+ { mime: 'image/jpeg', quality: 0.92, ext: 'jpg' },
+ ]
+ : [
+ { mime: 'image/jpeg', quality: 0.92, ext: 'jpg' },
+ { mime: 'image/png', ext: 'png' },
+ ];
let blob: Blob | null = null;
- let picked = tryTypes[1];
+ let picked = tryTypes[0];
for (const t of tryTypes) {
blob = await new Promise(resolve => {
canvas.toBlob(b => resolve(b), t.mime, t.quality);