add paste image and URL input support
This commit is contained in:
parent
f702b8596a
commit
870091a835
@ -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;
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user