feat: improve image browsing experience
This commit is contained in:
@@ -31,6 +31,8 @@ export interface UploadResult {
|
||||
}
|
||||
|
||||
let nextId = 0
|
||||
const THUMBNAIL_MAX_EDGE = 640
|
||||
const THUMBNAIL_QUALITY = 0.72
|
||||
|
||||
function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
return new Promise((resolve, reject) => {
|
||||
@@ -41,6 +43,84 @@ function readFileAsArrayBuffer(file: File): Promise<ArrayBuffer> {
|
||||
})
|
||||
}
|
||||
|
||||
function loadImageElement(file: File): Promise<HTMLImageElement> {
|
||||
return new Promise((resolve, reject) => {
|
||||
const objectUrl = URL.createObjectURL(file)
|
||||
const image = new Image()
|
||||
|
||||
image.onload = () => {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
resolve(image)
|
||||
}
|
||||
|
||||
image.onerror = () => {
|
||||
URL.revokeObjectURL(objectUrl)
|
||||
reject(new Error('图片读取失败'))
|
||||
}
|
||||
|
||||
image.src = objectUrl
|
||||
})
|
||||
}
|
||||
|
||||
function getResizedDimensions(width: number, height: number, maxEdge: number) {
|
||||
const largestEdge = Math.max(width, height)
|
||||
if (largestEdge <= maxEdge) {
|
||||
return { width, height }
|
||||
}
|
||||
|
||||
const scale = maxEdge / largestEdge
|
||||
return {
|
||||
width: Math.round(width * scale),
|
||||
height: Math.round(height * scale),
|
||||
}
|
||||
}
|
||||
|
||||
function fileBaseName(name: string) {
|
||||
const idx = name.lastIndexOf('.')
|
||||
return idx === -1 ? name : name.slice(0, idx)
|
||||
}
|
||||
|
||||
async function renderJpegFile(
|
||||
image: HTMLImageElement,
|
||||
sourceFile: File,
|
||||
maxEdge: number,
|
||||
quality: number,
|
||||
suffix = '',
|
||||
) {
|
||||
const { width, height } = getResizedDimensions(image.naturalWidth || image.width, image.naturalHeight || image.height, maxEdge)
|
||||
const canvas = document.createElement('canvas')
|
||||
canvas.width = width
|
||||
canvas.height = height
|
||||
|
||||
const context = canvas.getContext('2d')
|
||||
if (!context) {
|
||||
throw new Error('图片压缩失败')
|
||||
}
|
||||
|
||||
context.drawImage(image, 0, 0, width, height)
|
||||
|
||||
const blob = await new Promise<Blob | null>(resolve => {
|
||||
canvas.toBlob(resolve, 'image/jpeg', quality)
|
||||
})
|
||||
|
||||
if (!blob) {
|
||||
throw new Error('图片压缩失败')
|
||||
}
|
||||
|
||||
return new File([blob], `${fileBaseName(sourceFile.name)}${suffix}.jpg`, {
|
||||
type: 'image/jpeg',
|
||||
lastModified: Date.now(),
|
||||
})
|
||||
}
|
||||
|
||||
async function createUploadAssets(file: File) {
|
||||
const image = await loadImageElement(file)
|
||||
|
||||
return {
|
||||
thumbnailFile: await renderJpegFile(image, file, THUMBNAIL_MAX_EDGE, THUMBNAIL_QUALITY, '-thumb'),
|
||||
}
|
||||
}
|
||||
|
||||
function extractExifDate(buffer: ArrayBuffer): string | null {
|
||||
const view = new DataView(buffer)
|
||||
if (view.getUint16(0, false) !== 0xffd8) return null
|
||||
@@ -253,18 +333,32 @@ export function useUpload() {
|
||||
|
||||
overallProgress.value = Math.round(i / items.value.length * 100)
|
||||
|
||||
const ext = item.file.name.split('.').pop() || 'jpg'
|
||||
const basePath = `${userId}/${Date.now()}-${Math.random().toString(36).slice(2, 8)}`
|
||||
const ext = item.file.name.split('.').pop() || 'jpg'
|
||||
const imagePath = `${basePath}.${ext}`
|
||||
const thumbnailPath = `${basePath}-thumb.jpg`
|
||||
const { thumbnailFile } = await createUploadAssets(item.file)
|
||||
|
||||
const { error: imgError } = await supabase.storage
|
||||
.from('clouds')
|
||||
.upload(imagePath, item.file, { upsert: false })
|
||||
.upload(imagePath, item.file, {
|
||||
upsert: false,
|
||||
contentType: item.file.type,
|
||||
})
|
||||
if (imgError) throw imgError
|
||||
|
||||
const { error: thumbError } = await supabase.storage
|
||||
.from('clouds')
|
||||
.upload(thumbnailPath, thumbnailFile, {
|
||||
upsert: false,
|
||||
contentType: thumbnailFile.type,
|
||||
})
|
||||
if (thumbError) throw thumbError
|
||||
|
||||
overallProgress.value = Math.round((i + 0.5) / items.value.length * 100)
|
||||
|
||||
const { data: { publicUrl: imageUrl } } = supabase.storage.from('clouds').getPublicUrl(imagePath)
|
||||
const { data: { publicUrl: thumbnailUrl } } = supabase.storage.from('clouds').getPublicUrl(thumbnailPath)
|
||||
|
||||
const latitude = item.latitude ? blurCoordinate(item.latitude) : null
|
||||
const longitude = item.longitude ? blurCoordinate(item.longitude) : null
|
||||
@@ -276,7 +370,7 @@ export function useUpload() {
|
||||
cloud_type_id: item.cloudCategoryId === 'other' ? null : item.cloudCategoryId,
|
||||
custom_cloud_type: item.cloudCategoryId === 'other' ? (item.customCloudType.trim() || null) : null,
|
||||
image_url: imageUrl,
|
||||
thumbnail_url: null,
|
||||
thumbnail_url: thumbnailUrl,
|
||||
latitude,
|
||||
longitude,
|
||||
location_name: item.locationName || null,
|
||||
|
||||
Reference in New Issue
Block a user