add paste image and URL input support

This commit is contained in:
jingrow 2025-11-21 23:54:43 +08:00
parent f702b8596a
commit 870091a835

View File

@ -56,6 +56,27 @@
<h3>{{ t('Upload Image') }}</h3>
<p>{{ t('Drag and drop your image here, or click to browse') }}</p>
<p class="upload-hint">{{ t('Supports JPG, PNG, WebP formats') }}</p>
<div class="url-input-wrapper" @click.stop>
<div class="url-input-container">
<input
ref="urlInputRef"
v-model="imageUrl"
type="text"
class="url-input"
:placeholder="t('Or paste image URL here')"
@keyup.enter="handleUrlSubmit"
@paste="handleUrlPaste"
/>
<button
type="button"
class="url-submit-btn"
@click="handleUrlSubmit"
:disabled="!imageUrl.trim() || processing"
>
<i class="fa fa-arrow-right"></i>
</button>
</div>
</div>
</div>
</div>
@ -176,9 +197,11 @@ interface HistoryItem {
}
const fileInputRef = ref<HTMLInputElement | null>(null)
const urlInputRef = ref<HTMLInputElement | null>(null)
const uploadedImage = ref<File | null>(null)
const uploadedImageUrl = ref<string>('')
const resultImage = ref<string>('')
const imageUrl = ref<string>('')
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;
}