From 870091a83540ffc63b61a63fb22541fd08ed5125 Mon Sep 17 00:00:00 2001 From: jingrow Date: Fri, 21 Nov 2025 23:54:43 +0800 Subject: [PATCH] add paste image and URL input support --- .../remove_background/remove_background.vue | 236 ++++++++++++++++++ 1 file changed, 236 insertions(+) diff --git a/apps/jingrow/frontend/src/views/tools/remove_background/remove_background.vue b/apps/jingrow/frontend/src/views/tools/remove_background/remove_background.vue index 892d44c..fce3848 100644 --- a/apps/jingrow/frontend/src/views/tools/remove_background/remove_background.vue +++ b/apps/jingrow/frontend/src/views/tools/remove_background/remove_background.vue @@ -56,6 +56,27 @@

{{ t('Upload Image') }}

{{ t('Drag and drop your image here, or click to browse') }}

{{ t('Supports JPG, PNG, WebP formats') }}

+
+
+ + +
+
@@ -176,9 +197,11 @@ interface HistoryItem { } const fileInputRef = ref(null) +const urlInputRef = ref(null) const uploadedImage = ref(null) const uploadedImageUrl = ref('') const resultImage = ref('') +const imageUrl = ref('') const resultImageUrl = computed(() => { if (!resultImage.value) return '' // 直接返回图片URL @@ -237,12 +260,132 @@ const handleResize = () => { adjustContainerSize() } +const handlePaste = async (event: ClipboardEvent) => { + const items = event.clipboardData?.items + if (!items) return + + // 检查是否有图片数据 + for (let i = 0; i < items.length; i++) { + const item = items[i] + if (item.type.indexOf('image') !== -1) { + event.preventDefault() + const file = item.getAsFile() + if (file) { + processFile(file) + } + return + } + } + + // 检查是否有文本URL + const text = event.clipboardData?.getData('text') + if (text && isValidImageUrl(text)) { + event.preventDefault() + imageUrl.value = text.trim() + await handleUrlSubmit() + } +} + +const isValidImageUrl = (url: string): boolean => { + if (!url || typeof url !== 'string') return false + try { + const urlObj = new URL(url) + const pathname = urlObj.pathname.toLowerCase() + const imageExtensions = ['.jpg', '.jpeg', '.png', '.webp', '.gif', '.bmp'] + return imageExtensions.some(ext => pathname.endsWith(ext)) || + urlObj.pathname.match(/\.(jpg|jpeg|png|webp|gif|bmp)(\?|$)/i) !== null + } catch { + return false + } +} + +const handleUrlPaste = async (event: ClipboardEvent) => { + const text = event.clipboardData?.getData('text') + if (text && isValidImageUrl(text)) { + event.preventDefault() + imageUrl.value = text.trim() + await handleUrlSubmit() + } +} + +const handleUrlSubmit = async () => { + const url = imageUrl.value.trim() + if (!url) { + message.warning(t('Please enter an image URL')) + return + } + + if (!isValidImageUrl(url)) { + message.warning(t('Please enter a valid image URL')) + return + } + + processing.value = true + resultImage.value = '' + splitPosition.value = 0 + currentHistoryIndex.value = -1 + + try { + // 使用代理或直接加载图片 + const response = await fetch(url, { mode: 'cors' }) + if (!response.ok) { + throw new Error(`Failed to load image: ${response.status}`) + } + + const blob = await response.blob() + + // 验证文件类型 + const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp'] + if (!validTypes.includes(blob.type)) { + message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP')) + processing.value = false + return + } + + // 验证文件大小 + const maxSize = 10 * 1024 * 1024 + if (blob.size > maxSize) { + message.warning(t('Image size exceeds 10MB limit')) + processing.value = false + return + } + + // 转换为File对象 + const file = new File([blob], 'image-from-url', { type: blob.type }) + uploadedImage.value = file + + // 创建预览URL + uploadedImageUrl.value = URL.createObjectURL(blob) + + // 开始处理 + await handleRemoveBackground() + } catch (error: any) { + console.error('加载图片URL失败:', error) + let errorMessage = t('Failed to load image from URL') + + if (error.message?.includes('CORS')) { + errorMessage = t('CORS error. The image server does not allow cross-origin access.') + } else if (error.message?.includes('Failed to load')) { + errorMessage = t('Failed to load image. Please check the URL and try again.') + } + + message.error(errorMessage) + processing.value = false + } +} + onMounted(() => { window.addEventListener('resize', handleResize) + window.addEventListener('paste', handlePaste) }) onUnmounted(() => { window.removeEventListener('resize', handleResize) + window.removeEventListener('paste', handlePaste) + // 清理对象URL + if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { + URL.revokeObjectURL(uploadedImageUrl.value) + } }) const triggerFileInput = () => { @@ -326,9 +469,14 @@ const processFile = (file: File) => { } const resetUpload = () => { + // 清理对象URL + if (uploadedImageUrl.value && uploadedImageUrl.value.startsWith('blob:')) { + URL.revokeObjectURL(uploadedImageUrl.value) + } uploadedImage.value = null uploadedImageUrl.value = '' resultImage.value = '' + imageUrl.value = '' splitPosition.value = 0 currentHistoryIndex.value = -1 if (fileInputRef.value) { @@ -812,6 +960,75 @@ const removeHistoryItem = (index: number) => { margin-top: 4px; } +.url-input-wrapper { + width: 100%; + max-width: 500px; + margin-top: 20px; +} + +.url-input-container { + display: flex; + gap: 8px; + align-items: center; + background: white; + border: 1px solid #e5e7eb; + border-radius: 8px; + padding: 4px; + transition: all 0.2s ease; + + &:focus-within { + border-color: #1fc76f; + box-shadow: 0 0 0 3px rgba(31, 199, 111, 0.1); + } +} + +.url-input { + flex: 1; + border: none; + outline: none; + padding: 8px 12px; + font-size: 13px; + color: #1f2937; + background: transparent; + + &::placeholder { + color: #94a3b8; + } +} + +.url-submit-btn { + width: 32px; + height: 32px; + border: none; + border-radius: 6px; + background: #1fc76f; + color: white; + cursor: pointer; + display: flex; + align-items: center; + justify-content: center; + transition: all 0.2s ease; + flex-shrink: 0; + + i { + font-size: 12px; + } + + &:hover:not(:disabled) { + background: #16a085; + transform: scale(1.05); + } + + &:active:not(:disabled) { + transform: scale(0.95); + } + + &:disabled { + opacity: 0.5; + cursor: not-allowed; + } +} + .preview-section { flex: 1; width: 100%; @@ -1224,6 +1441,25 @@ const removeHistoryItem = (index: number) => { font-size: 12px; } + .url-input-wrapper { + max-width: 100%; + margin-top: 16px; + } + + .url-input { + font-size: 12px; + padding: 6px 10px; + } + + .url-submit-btn { + width: 28px; + height: 28px; + + i { + font-size: 11px; + } + } + .preview-section { padding: 8px; }