fix(web): align avatar file extension with output type (#41662)

This commit is contained in:
yyh 2026-09-02 10:22:59 +00:00 committed by GitHub
parent b1eafb0d82
commit 7664ad7149
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
4 changed files with 40 additions and 6 deletions

View File

@ -16,7 +16,7 @@ import Divider from '@/app/components/base/divider'
import { useLocalFileUploader } from '@/app/components/base/image-uploader/hooks'
import { DISABLE_UPLOAD_IMAGE_AS_ICON } from '@/config'
import { updateUserProfile } from '@/service/common'
import { createCroppedAvatarImage } from './avatar-image'
import { createAvatarImageFile, createCroppedAvatarImage } from './avatar-image'
type InputImageInfo =
| { file: File }
@ -111,7 +111,7 @@ const AvatarWithEdit = ({ onSave, ...props }: AvatarWithEditProps) => {
inputImageInfo.croppedAreaPixels,
inputImageInfo.fileName,
)
const file = new File([blob], inputImageInfo.fileName, { type: blob.type })
const file = createAvatarImageFile(blob, inputImageInfo.fileName)
handleLocalFileUpload(file)
}, [handleLocalFileUpload, inputImageInfo])

View File

@ -45,9 +45,13 @@ vi.mock('@/app/components/base/image-uploader/hooks', () => ({
}),
}))
vi.mock('../avatar-image', () => ({
createCroppedAvatarImage: vi.fn(),
}))
vi.mock('../avatar-image', async (importOriginal) => {
const actual = await importOriginal<typeof import('../avatar-image')>()
return {
...actual,
createCroppedAvatarImage: vi.fn(),
}
})
const mockedCreateCroppedAvatarImage = vi.mocked(createCroppedAvatarImage)

View File

@ -1,6 +1,10 @@
import type { Area } from 'react-easy-crop'
import { createImage } from '@/app/components/base/app-icon-picker/utils'
import { createCroppedAvatarImage, getBoundedAvatarImageSize } from '../avatar-image'
import {
createAvatarImageFile,
createCroppedAvatarImage,
getBoundedAvatarImageSize,
} from '../avatar-image'
vi.mock('@/app/components/base/app-icon-picker/utils', async (importOriginal) => {
const actual =
@ -58,4 +62,13 @@ describe('avatar image', () => {
expect(context.drawImage).toHaveBeenCalledWith(image, 40, 20, 1000, 1000, 0, 0, 256, 256)
expect(canvas.toBlob).toHaveBeenCalledWith(expect.any(Function), 'image/png', 0.85)
})
it('matches the file extension to the browser output type', () => {
const blob = new Blob(['avatar'], { type: 'image/png' })
const file = createAvatarImageFile(blob, 'avatar.webp')
expect(file.name).toBe('avatar.png')
expect(file.type).toBe('image/png')
})
})

View File

@ -3,6 +3,11 @@ import { createImage, getMimeType } from '@/app/components/base/app-icon-picker/
const AVATAR_IMAGE_MAX_SIZE = 256
const AVATAR_IMAGE_QUALITY = 0.85
const AVATAR_IMAGE_EXTENSION_BY_MIME_TYPE: Record<string, string> = {
'image/jpeg': 'jpg',
'image/png': 'png',
'image/webp': 'webp',
}
export const getBoundedAvatarImageSize = (
crop: Pick<Area, 'width' | 'height'>,
@ -55,3 +60,15 @@ export const createCroppedAvatarImage = async (
)
})
}
export const createAvatarImageFile = (blob: Blob, fileName: string) => {
const extension = AVATAR_IMAGE_EXTENSION_BY_MIME_TYPE[blob.type]
if (!extension || getMimeType(fileName) === blob.type)
return new File([blob], fileName, { type: blob.type })
const extensionStart = fileName.lastIndexOf('.')
const baseName = extensionStart > 0 ? fileName.slice(0, extensionStart) : fileName
return new File([blob], `${baseName}.${extension}`, { type: blob.type })
}