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;
}