feat: 头像上传前端预压缩为 50x50 JPEG,降低上传延迟

This commit is contained in:
TermiNexus committed 2026-10-13 18:52:47 +08:00
1 parent c9635a99db
commit 3cdc02af0d
3 files changed
+67 -2

No files matched your search

+61
View File
@@ -0,0 +1,61 @@
/**
* 图片压缩工具 —— 头像上传前的前端预压缩,降低网络延迟
*
* 头像展示尺寸很小(会话列表 38px、成员/消息 30px、编辑预览 96px),
* 上传前压成 AVATAR_UPLOAD_SIZE 见方的 JPEG,可显著减少请求体体积。
*/
/** 头像上传目标边长(正方形,px) */
export const AVATAR_UPLOAD_SIZE = 50
/** 载入图片为可绘制源(优先 ImageBitmap,回退 HTMLImageElement) */
async function LoadImageSource(file: File): Promise<ImageBitmap | HTMLImageElement> {
if (typeof createImageBitmap === 'function') {
return await createImageBitmap(file)
}
const url = URL.createObjectURL(file)
try {
return await new Promise<HTMLImageElement>((resolve, reject) => {
const el = new Image()
el.onload = () => resolve(el)
el.onerror = () => reject(new Error('图片解码失败'))
el.src = url
})
} finally {
URL.revokeObjectURL(url)
}
}
/**
* 将图片居中裁剪为正方形并缩放为 Size×Size 的 JPEG File。
*
* - 透明区域以白色填充(JPEG 不支持透明,避免出现黑底)
* - 非图片、canvas 不可用或解码失败时原样返回,保证上传不中断
*/
export async function compressImageSquare(file: File, Size = AVATAR_UPLOAD_SIZE): Promise<File> {
if (!file.type.startsWith('image/')) return file
try {
const source = await LoadImageSource(file)
const canvas = document.createElement('canvas')
canvas.width = Size
canvas.height = Size
const ctx = canvas.getContext('2d')
if (!ctx) return file
// 居中裁剪正方形后缩放
const side = Math.min(source.width, source.height)
const sx = (source.width - side) / 2
const sy = (source.height - side) / 2
ctx.fillStyle = '#ffffff'
ctx.fillRect(0, 0, Size, Size)
ctx.imageSmoothingEnabled = true
ctx.imageSmoothingQuality = 'high'
ctx.drawImage(source, sx, sy, side, side, 0, 0, Size, Size)
if (typeof ImageBitmap !== 'undefined' && source instanceof ImageBitmap) source.close()
const blob = await new Promise<Blob | null>((resolve) => canvas.toBlob(resolve, 'image/jpeg', 0.9))
if (!blob) return file
const base = file.name.replace(/\.[^.]+$/, '')
return new File([blob], `${base}.jpg`, { type: 'image/jpeg', lastModified: Date.now() })
} catch {
return file
}
}
+3 -1
View File
@@ -16,6 +16,7 @@ import { computed, ref } from 'vue'
import { ElMessage } from 'element-plus'
import { useUserStore } from '@/stores/user'
import { getAvatarUrl, uploadAvatar, modifyPersonalConfig, modifyPassword } from '@/api/auth'
import { compressImageSquare } from '@/utils/imageCompress'
const userStore = useUserStore()
@@ -40,7 +41,8 @@ async function handleAvatarChange(e: Event): Promise<void> {
if (avatarUploading.value) return
avatarUploading.value = true
try {
await uploadAvatar(file)
// 前端预压缩为 50×50 JPEG,降低上传体积与延迟(后端仍会再规范化)
await uploadAvatar(await compressImageSquare(file))
avatarVersion.value++
ElMessage.success('头像修改成功')
} catch {
+3 -1
View File
@@ -50,6 +50,7 @@ import {
subscribeTalkEvent,
emitTalkEvent,
} from '@/utils/talkSocket'
import { compressImageSquare } from '@/utils/imageCompress'
import GroupCreateDialog from './GroupCreateDialog.vue'
import TalkAvatar from './TalkAvatar.vue'
@@ -765,7 +766,8 @@ async function OnAvatarPicked(event: Event): Promise<void> {
}
AvatarUploading.value = true
try {
const result = await uploadTalkAttachment(file)
// 前端预压缩为 50×50 JPEG 后上传,降低体积与延迟
const result = await uploadTalkAttachment(await compressImageSquare(file))
AvatarSHA1.value = result.SHA1
AvatarPreview.value = buildTalkAssetUrl(result.SHA1)
} catch {