feat: 头像上传前端预压缩为 50x50 JPEG,降低上传延迟
This commit is contained in:
1 parent
c9635a99db
commit
3cdc02af0d
3 files changed
+67
-2
No files matched your search
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
Reference in new issue
Block a user