add image compression and client-side download cache
This commit is contained in:
parent
ea6d1b218c
commit
ad43d0f8bb
@ -211,6 +211,7 @@ const urlInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
const uploadedImage = ref<File | null>(null)
|
const uploadedImage = ref<File | null>(null)
|
||||||
const uploadedImageUrl = ref<string>('')
|
const uploadedImageUrl = ref<string>('')
|
||||||
const resultImage = ref<string>('')
|
const resultImage = ref<string>('')
|
||||||
|
const resultImageBlobUrl = ref<string>('') // 缓存的 blob URL,用于下载
|
||||||
const imageUrl = ref<string>('')
|
const imageUrl = ref<string>('')
|
||||||
const resultImageUrl = computed(() => {
|
const resultImageUrl = computed(() => {
|
||||||
if (!resultImage.value) return ''
|
if (!resultImage.value) return ''
|
||||||
@ -459,6 +460,9 @@ const handleRemoveBackground = async () => {
|
|||||||
if (firstResult.success && firstResult.image_url) {
|
if (firstResult.success && firstResult.image_url) {
|
||||||
resultImage.value = firstResult.image_url
|
resultImage.value = firstResult.image_url
|
||||||
|
|
||||||
|
// 缓存图片 blob URL 用于下载
|
||||||
|
await cacheResultImage(firstResult.image_url)
|
||||||
|
|
||||||
// 添加到历史记录
|
// 添加到历史记录
|
||||||
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
|
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
|
||||||
const historyItem: HistoryItem = {
|
const historyItem: HistoryItem = {
|
||||||
@ -495,6 +499,30 @@ const handleRemoveBackground = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存结果图片为 blob URL,用于下载
|
||||||
|
*/
|
||||||
|
const cacheResultImage = async (imageUrl: string) => {
|
||||||
|
try {
|
||||||
|
// 清理旧的 blob URL
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
resultImageBlobUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片并转换为 blob URL
|
||||||
|
const response = await fetch(imageUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch image: ${response.status}`)
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
resultImageBlobUrl.value = URL.createObjectURL(blob)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('缓存图片失败:', error)
|
||||||
|
// 缓存失败不影响显示,只是下载时需要重新获取
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
if (!resultImage.value) return
|
if (!resultImage.value) return
|
||||||
|
|
||||||
@ -506,15 +534,27 @@ const handleDownload = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
|
// 优先使用缓存的 blob URL
|
||||||
|
let blobUrl = resultImageBlobUrl.value
|
||||||
|
|
||||||
|
// 如果没有缓存,则获取并缓存
|
||||||
|
if (!blobUrl) {
|
||||||
const response = await fetch(resultImage.value)
|
const response = await fetch(resultImage.value)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch image: ${response.status}`)
|
||||||
|
}
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
blobUrl = URL.createObjectURL(blob)
|
||||||
|
resultImageBlobUrl.value = blobUrl
|
||||||
|
}
|
||||||
|
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
link.href = blobUrl
|
link.href = blobUrl
|
||||||
link.download = `removed-background-${Date.now()}.png`
|
link.download = `removed-background-${Date.now()}.png`
|
||||||
link.click()
|
link.click()
|
||||||
URL.revokeObjectURL(blobUrl)
|
// 不立即清理 blob URL,保留缓存供后续下载使用
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
|
console.error('下载失败:', error)
|
||||||
message.error('下载失败')
|
message.error('下载失败')
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@ -523,6 +563,11 @@ const resetUpload = () => {
|
|||||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||||
}
|
}
|
||||||
|
// 清理结果图片的 blob URL 缓存
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
resultImageBlobUrl.value = ''
|
||||||
|
}
|
||||||
uploadedImage.value = null
|
uploadedImage.value = null
|
||||||
uploadedImageUrl.value = ''
|
uploadedImageUrl.value = ''
|
||||||
resultImage.value = ''
|
resultImage.value = ''
|
||||||
@ -771,7 +816,7 @@ const handleUrlSubmit = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectHistoryItem = (index: number) => {
|
const selectHistoryItem = async (index: number) => {
|
||||||
if (index < 0 || index >= historyList.value.length) return
|
if (index < 0 || index >= historyList.value.length) return
|
||||||
|
|
||||||
const item = historyList.value[index]
|
const item = historyList.value[index]
|
||||||
@ -780,6 +825,11 @@ const selectHistoryItem = (index: number) => {
|
|||||||
resultImage.value = item.resultImage
|
resultImage.value = item.resultImage
|
||||||
splitPosition.value = 0
|
splitPosition.value = 0
|
||||||
uploadedImage.value = item.originalImageFile
|
uploadedImage.value = item.originalImageFile
|
||||||
|
|
||||||
|
// 缓存历史记录的结果图片
|
||||||
|
if (item.resultImage) {
|
||||||
|
await cacheResultImage(item.resultImage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHistoryThumbnailUrl = (item: HistoryItem): string => {
|
const getHistoryThumbnailUrl = (item: HistoryItem): string => {
|
||||||
@ -836,6 +886,10 @@ onUnmounted(() => {
|
|||||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||||
}
|
}
|
||||||
|
// 清理结果图片的 blob URL 缓存
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
|
|||||||
@ -191,6 +191,7 @@ import { t } from '@/shared/i18n'
|
|||||||
import { get_session_api_headers } from '@/shared/api/auth'
|
import { get_session_api_headers } from '@/shared/api/auth'
|
||||||
import { useAuthStore } from '@/shared/stores/auth'
|
import { useAuthStore } from '@/shared/stores/auth'
|
||||||
import { useSEO } from '@/shared/composables/useSEO'
|
import { useSEO } from '@/shared/composables/useSEO'
|
||||||
|
import { compressImageFile } from '@/shared/utils/imageResize'
|
||||||
|
|
||||||
const message = useMessage()
|
const message = useMessage()
|
||||||
const authStore = useAuthStore()
|
const authStore = useAuthStore()
|
||||||
@ -217,6 +218,7 @@ const urlInputRef = ref<HTMLInputElement | null>(null)
|
|||||||
const uploadedImage = ref<File | null>(null)
|
const uploadedImage = ref<File | null>(null)
|
||||||
const uploadedImageUrl = ref<string>('')
|
const uploadedImageUrl = ref<string>('')
|
||||||
const resultImage = ref<string>('')
|
const resultImage = ref<string>('')
|
||||||
|
const resultImageBlobUrl = ref<string>('') // 缓存的 blob URL,用于下载
|
||||||
const imageUrl = ref<string>('')
|
const imageUrl = ref<string>('')
|
||||||
const resultImageUrl = computed(() => {
|
const resultImageUrl = computed(() => {
|
||||||
if (!resultImage.value) return ''
|
if (!resultImage.value) return ''
|
||||||
@ -367,14 +369,27 @@ const handleUrlSubmit = async () => {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// 转换为File对象
|
// 转换为File对象
|
||||||
const file = new File([blob], 'image-from-url', { type: blob.type })
|
const originalFile = new File([blob], 'image-from-url', { type: blob.type })
|
||||||
uploadedImage.value = file
|
|
||||||
|
|
||||||
// 创建预览URL
|
// 压缩图片到 1024x1024
|
||||||
uploadedImageUrl.value = URL.createObjectURL(blob)
|
try {
|
||||||
|
const compressedFile = await compressImageFile(originalFile, {
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
quality: 0.92,
|
||||||
|
mode: 'contain'
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadedImage.value = compressedFile
|
||||||
|
uploadedImageUrl.value = URL.createObjectURL(compressedFile)
|
||||||
|
|
||||||
// 开始处理
|
// 开始处理
|
||||||
await handleRemoveBackground()
|
await handleRemoveBackground()
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片压缩失败:', error)
|
||||||
|
message.error('图片处理失败,请重试')
|
||||||
|
processing.value = false
|
||||||
|
}
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('加载图片URL失败:', error)
|
console.error('加载图片URL失败:', error)
|
||||||
let errorMessage = t('Failed to load image from URL')
|
let errorMessage = t('Failed to load image from URL')
|
||||||
@ -402,6 +417,10 @@ onUnmounted(() => {
|
|||||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||||
}
|
}
|
||||||
|
// 清理结果图片的 blob URL 缓存
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
|
|
||||||
const triggerFileInput = () => {
|
const triggerFileInput = () => {
|
||||||
@ -458,7 +477,7 @@ const handleDrop = (event: DragEvent) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const processFile = (file: File) => {
|
const processFile = async (file: File) => {
|
||||||
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
|
||||||
if (!validTypes.includes(file.type)) {
|
if (!validTypes.includes(file.type)) {
|
||||||
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
|
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
|
||||||
@ -471,7 +490,16 @@ const processFile = (file: File) => {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
uploadedImage.value = file
|
// 压缩图片到 1024x1024
|
||||||
|
try {
|
||||||
|
const compressedFile = await compressImageFile(file, {
|
||||||
|
maxWidth: 1024,
|
||||||
|
maxHeight: 1024,
|
||||||
|
quality: 0.92,
|
||||||
|
mode: 'contain'
|
||||||
|
})
|
||||||
|
|
||||||
|
uploadedImage.value = compressedFile
|
||||||
resultImage.value = ''
|
resultImage.value = ''
|
||||||
splitPosition.value = 0
|
splitPosition.value = 0
|
||||||
currentHistoryIndex.value = -1
|
currentHistoryIndex.value = -1
|
||||||
@ -481,7 +509,11 @@ const processFile = (file: File) => {
|
|||||||
uploadedImageUrl.value = e.target?.result as string
|
uploadedImageUrl.value = e.target?.result as string
|
||||||
handleRemoveBackground()
|
handleRemoveBackground()
|
||||||
}
|
}
|
||||||
reader.readAsDataURL(file)
|
reader.readAsDataURL(compressedFile)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('图片压缩失败:', error)
|
||||||
|
message.error('图片处理失败,请重试')
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const resetUpload = () => {
|
const resetUpload = () => {
|
||||||
@ -489,6 +521,11 @@ const resetUpload = () => {
|
|||||||
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) {
|
||||||
URL.revokeObjectURL(uploadedImageUrl.value)
|
URL.revokeObjectURL(uploadedImageUrl.value)
|
||||||
}
|
}
|
||||||
|
// 清理结果图片的 blob URL 缓存
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
resultImageBlobUrl.value = ''
|
||||||
|
}
|
||||||
uploadedImage.value = null
|
uploadedImage.value = null
|
||||||
uploadedImageUrl.value = ''
|
uploadedImageUrl.value = ''
|
||||||
resultImage.value = ''
|
resultImage.value = ''
|
||||||
@ -504,7 +541,7 @@ const resetUpload = () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const selectHistoryItem = (index: number) => {
|
const selectHistoryItem = async (index: number) => {
|
||||||
if (index < 0 || index >= historyList.value.length) return
|
if (index < 0 || index >= historyList.value.length) return
|
||||||
|
|
||||||
const item = historyList.value[index]
|
const item = historyList.value[index]
|
||||||
@ -513,6 +550,11 @@ const selectHistoryItem = (index: number) => {
|
|||||||
resultImage.value = item.resultImage
|
resultImage.value = item.resultImage
|
||||||
splitPosition.value = 0
|
splitPosition.value = 0
|
||||||
uploadedImage.value = item.originalImageFile
|
uploadedImage.value = item.originalImageFile
|
||||||
|
|
||||||
|
// 缓存历史记录的结果图片
|
||||||
|
if (item.resultImage) {
|
||||||
|
await cacheResultImage(item.resultImage)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const getHistoryThumbnailUrl = (item: HistoryItem): string => {
|
const getHistoryThumbnailUrl = (item: HistoryItem): string => {
|
||||||
@ -581,6 +623,9 @@ const handleRemoveBackground = async () => {
|
|||||||
if (firstResult.success && firstResult.image_url) {
|
if (firstResult.success && firstResult.image_url) {
|
||||||
resultImage.value = firstResult.image_url
|
resultImage.value = firstResult.image_url
|
||||||
|
|
||||||
|
// 缓存图片 blob URL 用于下载
|
||||||
|
await cacheResultImage(firstResult.image_url)
|
||||||
|
|
||||||
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
|
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
|
||||||
const historyItem: HistoryItem = {
|
const historyItem: HistoryItem = {
|
||||||
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
|
||||||
@ -633,18 +678,47 @@ const handleRemoveBackground = async () => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 缓存结果图片为 blob URL,用于下载
|
||||||
|
*/
|
||||||
|
const cacheResultImage = async (imageUrl: string) => {
|
||||||
|
try {
|
||||||
|
// 清理旧的 blob URL
|
||||||
|
if (resultImageBlobUrl.value) {
|
||||||
|
URL.revokeObjectURL(resultImageBlobUrl.value)
|
||||||
|
resultImageBlobUrl.value = ''
|
||||||
|
}
|
||||||
|
|
||||||
|
// 获取图片并转换为 blob URL
|
||||||
|
const response = await fetch(imageUrl)
|
||||||
|
if (!response.ok) {
|
||||||
|
throw new Error(`Failed to fetch image: ${response.status}`)
|
||||||
|
}
|
||||||
|
const blob = await response.blob()
|
||||||
|
resultImageBlobUrl.value = URL.createObjectURL(blob)
|
||||||
|
} catch (error) {
|
||||||
|
console.error('缓存图片失败:', error)
|
||||||
|
// 缓存失败不影响显示,只是下载时需要重新获取
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
const handleDownload = async () => {
|
const handleDownload = async () => {
|
||||||
if (!resultImage.value) return
|
if (!resultImage.value) return
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// 获取图片数据并转换为 Blob,确保跨域图片也能下载
|
// 优先使用缓存的 blob URL
|
||||||
|
let blobUrl = resultImageBlobUrl.value
|
||||||
|
|
||||||
|
// 如果没有缓存,则获取并缓存
|
||||||
|
if (!blobUrl) {
|
||||||
const response = await fetch(resultImage.value)
|
const response = await fetch(resultImage.value)
|
||||||
if (!response.ok) {
|
if (!response.ok) {
|
||||||
throw new Error(`HTTP error! status: ${response.status}`)
|
throw new Error(`HTTP error! status: ${response.status}`)
|
||||||
}
|
}
|
||||||
|
|
||||||
const blob = await response.blob()
|
const blob = await response.blob()
|
||||||
const blobUrl = URL.createObjectURL(blob)
|
blobUrl = URL.createObjectURL(blob)
|
||||||
|
resultImageBlobUrl.value = blobUrl
|
||||||
|
}
|
||||||
|
|
||||||
// 创建下载链接
|
// 创建下载链接
|
||||||
const link = document.createElement('a')
|
const link = document.createElement('a')
|
||||||
@ -655,10 +729,9 @@ const handleDownload = async () => {
|
|||||||
document.body.appendChild(link)
|
document.body.appendChild(link)
|
||||||
link.click()
|
link.click()
|
||||||
|
|
||||||
// 清理:移除 DOM 元素并释放 blob URL
|
// 清理:移除 DOM 元素,但不释放 blob URL(保留缓存供后续下载使用)
|
||||||
requestAnimationFrame(() => {
|
requestAnimationFrame(() => {
|
||||||
document.body.removeChild(link)
|
document.body.removeChild(link)
|
||||||
URL.revokeObjectURL(blobUrl)
|
|
||||||
})
|
})
|
||||||
} catch (error: any) {
|
} catch (error: any) {
|
||||||
console.error('下载图片失败:', error)
|
console.error('下载图片失败:', error)
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user