clean up RemoveBackground component and optimize code

This commit is contained in:
jingrow 2025-11-19 22:18:13 +08:00
parent 2a47e6a705
commit 6475c947c0

View File

@ -11,7 +11,6 @@
<p>{{ t('Drop image anywhere to remove background') }}</p>
</div>
</div>
<!-- 文件输入框始终存在 -->
<input
ref="fileInputRef"
type="file"
@ -22,7 +21,6 @@
<div class="page-header">
<h2>{{ t('Remove Background') }}</h2>
<!-- 工具栏图标 -->
<div v-if="uploadedImage" class="toolbar-actions">
<button
v-if="resultImage"
@ -44,7 +42,6 @@
<div class="page-content">
<div class="tool-container">
<!-- 上传区域 -->
<div class="upload-section">
<div
v-if="!uploadedImage"
@ -62,12 +59,9 @@
</div>
</div>
<!-- 图片预览和处理区域 -->
<div v-else class="preview-section">
<!-- 单窗口对比视图 -->
<div class="comparison-view" v-if="resultImage">
<div class="comparison-container" ref="comparisonContainerRef">
<!-- 原图显示左侧部分 -->
<div
class="comparison-image original-image"
:style="{ clipPath: `inset(0 ${100 - splitPosition}% 0 0)` }"
@ -80,7 +74,6 @@
/>
</div>
<!-- 去背景后的图片显示右侧部分 -->
<div
class="comparison-image result-image"
:style="{ clipPath: `inset(0 0 0 ${splitPosition}%)` }"
@ -93,7 +86,6 @@
/>
</div>
<!-- 拖动竖线 -->
<div
class="split-line"
:class="{ 'dragging': isDraggingSplitLine }"
@ -107,7 +99,6 @@
</div>
</div>
<!-- 处理中或未处理状态 -->
<div v-else class="single-image-view">
<div class="image-wrapper">
<img :src="uploadedImageUrl" alt="Original" />
@ -120,10 +111,8 @@
</div>
</div>
<!-- 底部历史记录栏 -->
<div v-if="historyList.length > 0 || uploadedImage" class="history-bar">
<div class="history-scroll-container">
<!-- 添加新图片按钮 -->
<button
type="button"
class="history-item add-button"
@ -133,7 +122,6 @@
<i class="fa fa-plus"></i>
</button>
<!-- 历史记录缩略图列表 -->
<div
v-for="(item, index) in historyList"
:key="item.id"
@ -153,7 +141,6 @@
</div>
</div>
<!-- 当前正在处理的图片如果还没有添加到历史记录 -->
<div
v-if="uploadedImage && resultImage && currentHistoryIndex === -1"
class="history-item active"
@ -180,45 +167,37 @@ import { useAuthStore } from '@/shared/stores/auth'
const message = useMessage()
const authStore = useAuthStore()
//
interface HistoryItem {
id: string
originalImageUrl: string
originalImageFile: File | null
resultImage: string // Base64
resultImage: string
timestamp: number
}
//
const fileInputRef = ref<HTMLInputElement | null>(null)
const uploadedImage = ref<File | null>(null)
const uploadedImageUrl = ref<string>('')
const resultImage = ref<string>('') // Base64
const resultImage = ref<string>('')
const resultImageUrl = computed(() => {
if (!resultImage.value) return ''
// data URL
if (resultImage.value.startsWith('data:')) {
return resultImage.value
}
// data URL
return `data:image/png;base64,${resultImage.value}`
})
//
const historyList = ref<HistoryItem[]>([])
const currentHistoryIndex = ref<number>(-1) // -1 >=0
//
const currentHistoryIndex = ref<number>(-1)
const isDragging = ref(false)
const dragCounter = ref(0)
const processing = ref(false)
const splitPosition = ref(0) // 线0
const splitPosition = ref(0)
const comparisonContainerRef = ref<HTMLElement | null>(null)
const originalImageRef = ref<HTMLImageElement | null>(null)
const resultImageRef = ref<HTMLImageElement | null>(null)
const isDraggingSplitLine = ref(false)
//
const adjustContainerSize = () => {
const container = comparisonContainerRef.value
if (!container) return
@ -226,19 +205,16 @@ const adjustContainerSize = () => {
const img = originalImageRef.value || resultImageRef.value
if (!img) return
// preview-section
const previewSection = container.closest('.preview-section') as HTMLElement
if (!previewSection) return
const previewRect = previewSection.getBoundingClientRect()
// padding (12px * 2 = 24px)
const padding = 24
const maxAvailableWidth = Math.max(0, previewRect.width - padding)
const maxAvailableHeight = Math.max(0, previewRect.height - padding)
if (maxAvailableWidth <= 0 || maxAvailableHeight <= 0) return
//
if (!img.complete) {
img.addEventListener('load', adjustContainerSize, { once: true })
return
@ -247,24 +223,18 @@ const adjustContainerSize = () => {
const { naturalWidth, naturalHeight } = img
if (naturalWidth === 0 || naturalHeight === 0) return
//
const scale = Math.min(
maxAvailableWidth / naturalWidth,
maxAvailableHeight / naturalHeight,
1 //
1
)
//
container.style.width = `${naturalWidth * scale}px`
container.style.height = `${naturalHeight * scale}px`
}
//
watch([uploadedImageUrl, resultImageUrl], () => {
adjustContainerSize()
}, { immediate: true })
watch([uploadedImageUrl, resultImageUrl], adjustContainerSize, { immediate: true })
//
const handleResize = () => {
adjustContainerSize()
}
@ -277,16 +247,13 @@ onUnmounted(() => {
window.removeEventListener('resize', handleResize)
})
//
const triggerFileInput = () => {
if (fileInputRef.value) {
// change
fileInputRef.value.value = ''
fileInputRef.value.click()
}
}
//
const handleFileSelect = (event: Event) => {
const target = event.target as HTMLInputElement
const file = target.files?.[0]
@ -295,14 +262,12 @@ const handleFileSelect = (event: Event) => {
}
}
//
const hasFiles = (event: DragEvent) => {
const types = event.dataTransfer?.types
if (!types) return false
return Array.from(types).includes('Files')
}
//
const handleDragEnter = (event: DragEvent) => {
if (!hasFiles(event)) return
dragCounter.value += 1
@ -336,16 +301,13 @@ const handleDrop = (event: DragEvent) => {
}
}
//
const processFile = (file: File) => {
//
const validTypes = ['image/jpeg', 'image/jpg', 'image/png', 'image/webp']
if (!validTypes.includes(file.type)) {
message.warning(t('Unsupported image format. Please use JPG, PNG, or WebP'))
return
}
// 10MB
const maxSize = 10 * 1024 * 1024
if (file.size > maxSize) {
message.warning(t('Image size exceeds 10MB limit'))
@ -354,20 +316,17 @@ const processFile = (file: File) => {
uploadedImage.value = file
resultImage.value = ''
splitPosition.value = 0 // 线
currentHistoryIndex.value = -1 //
splitPosition.value = 0
currentHistoryIndex.value = -1
// URL
const reader = new FileReader()
reader.onload = (e) => {
uploadedImageUrl.value = e.target?.result as string
//
handleRemoveBackground()
}
reader.readAsDataURL(file)
}
//
const resetUpload = () => {
uploadedImage.value = null
uploadedImageUrl.value = ''
@ -377,43 +336,33 @@ const resetUpload = () => {
if (fileInputRef.value) {
fileInputRef.value.value = ''
}
//
if (comparisonContainerRef.value) {
comparisonContainerRef.value.style.width = ''
comparisonContainerRef.value.style.height = ''
}
}
//
const selectHistoryItem = (index: number) => {
if (index < 0 || index >= historyList.value.length) return
const item = historyList.value[index]
currentHistoryIndex.value = index
//
uploadedImageUrl.value = item.originalImageUrl
resultImage.value = item.resultImage
splitPosition.value = 0
//
uploadedImage.value = item.originalImageFile
}
// URL
const getHistoryThumbnailUrl = (item: HistoryItem): string => {
//
if (item.resultImage) {
if (item.resultImage.startsWith('data:')) {
return item.resultImage
}
return `data:image/png;base64,${item.resultImage}`
}
//
return item.originalImageUrl
}
// 线
const handleSplitLineMouseDown = (e: MouseEvent) => {
e.preventDefault()
isDraggingSplitLine.value = true
@ -437,17 +386,12 @@ const handleSplitLineMouseDown = (e: MouseEvent) => {
document.addEventListener('mouseup', handleMouseUp)
}
//
const handleRemoveBackground = async () => {
//
if (!authStore.isLoggedIn) {
message.error(t('Please login first to use this feature'))
return
}
// cookie API
// whitelist cookie 401
if (!uploadedImage.value) {
message.warning(t('Please upload an image first'))
return
@ -457,7 +401,6 @@ const handleRemoveBackground = async () => {
resultImage.value = ''
try {
// base64
const reader = new FileReader()
const base64Promise = new Promise<string>((resolve, reject) => {
reader.onload = (e) => {
@ -469,7 +412,6 @@ const handleRemoveBackground = async () => {
reader.readAsDataURL(uploadedImage.value)
const base64Data = await base64Promise
// API使 withCredentials session cookie
const response = await axios.post(
'/jingrow.tools.tools.remove_background',
{
@ -477,25 +419,20 @@ const handleRemoveBackground = async () => {
},
{
headers: get_session_api_headers(),
withCredentials: true, // session cookie
timeout: 180000 // 3
withCredentials: true,
timeout: 180000
}
)
// whitelist : {success: true, data: function_result}
// function_result : {success: true/false, data: [...], ...}
if (response.data?.success && response.data?.data) {
const result = response.data.data
// success
if (result.success) {
// data
if (result.data && Array.isArray(result.data) && result.data.length > 0) {
const firstResult = result.data[0]
if (firstResult.success && firstResult.image_content) {
resultImage.value = firstResult.image_content
//
if (currentHistoryIndex.value === -1 && uploadedImage.value && uploadedImageUrl.value) {
const historyItem: HistoryItem = {
id: `history-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`,
@ -507,7 +444,6 @@ const handleRemoveBackground = async () => {
historyList.value.unshift(historyItem)
currentHistoryIndex.value = 0
} else if (currentHistoryIndex.value >= 0) {
//
historyList.value[currentHistoryIndex.value].resultImage = firstResult.image_content
}
@ -519,18 +455,15 @@ const handleRemoveBackground = async () => {
message.error(t('No image data returned'))
}
} else {
//
message.error(result.error || t('Failed to remove background'))
}
} else {
// whitelist
message.error(response.data?.error || response.data?.message || t('Failed to remove background'))
}
} catch (error: any) {
let errorMessage = t('Failed to remove background')
if (error.response) {
//
if (error.response.data) {
errorMessage = error.response.data.error ||
error.response.data.detail ||
@ -543,10 +476,8 @@ const handleRemoveBackground = async () => {
errorMessage = t('Authentication failed. Please login again.')
}
} else if (error.request) {
//
errorMessage = t('Network error. Please check your connection.')
} else {
//
errorMessage = error.message || errorMessage
}
@ -556,17 +487,14 @@ const handleRemoveBackground = async () => {
}
}
//
const handleDownload = () => {
if (!resultImage.value) return
try {
// base64
const base64Data = resultImage.value.includes(',')
? resultImage.value.split(',')[1]
: resultImage.value
// base64
const byteCharacters = atob(base64Data)
const byteNumbers = new Array(byteCharacters.length)
for (let i = 0; i < byteCharacters.length; i++) {
@ -574,10 +502,6 @@ const handleDownload = () => {
}
const byteArray = new Uint8Array(byteNumbers)
const blob = new Blob([byteArray], { type: 'image/png' })
// 使 blob URL
// HTTP
// HTTPS
const url = URL.createObjectURL(blob)
const link = document.createElement('a')
link.href = url
@ -586,7 +510,6 @@ const handleDownload = () => {
document.body.appendChild(link)
link.click()
//
setTimeout(() => {
document.body.removeChild(link)
URL.revokeObjectURL(url)
@ -622,13 +545,12 @@ const removeHistoryItem = (index: number) => {
<style scoped lang="scss">
.remove-background-page {
position: relative;
width: calc(100% + 40px); /* 抵消父容器左右 padding */
height: calc(100vh - 64px - 40px); /* 100vh - header高度(64px) - content-wrapper上下padding(40px) */
margin: -20px; /* 抵消父容器 content-wrapper 的 padding */
width: calc(100% + 40px);
height: calc(100vh - 64px - 40px);
margin: -20px;
display: flex;
flex-direction: column;
overflow: hidden;
background: #f8fafc;
}
.global-drag-overlay {
@ -742,9 +664,7 @@ const removeHistoryItem = (index: number) => {
justify-content: space-between;
align-items: center;
width: 100%;
padding: 12px 16px;
border-bottom: 1px solid #e5e7eb;
background: white;
padding: 12px 36px;
}
.page-content {
@ -766,7 +686,6 @@ const removeHistoryItem = (index: number) => {
margin: 0;
}
/* 工具栏图标 */
.toolbar-actions {
display: flex;
gap: 8px;
@ -801,55 +720,6 @@ const removeHistoryItem = (index: number) => {
}
}
.header-content {
display: flex;
align-items: center;
justify-content: space-between;
}
.header-left {
display: flex;
align-items: center;
gap: 16px;
}
.page-icon {
width: 56px;
height: 56px;
border-radius: 16px;
background: linear-gradient(135deg, #1fc76f 0%, #16a085 100%);
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 4px 12px rgba(31, 199, 111, 0.25);
i {
font-size: 28px;
color: white;
}
}
h1 {
font-size: 28px;
font-weight: 700;
color: #0f172a;
margin: 0 0 4px 0;
}
.page-description {
font-size: 14px;
color: #64748b;
margin: 0;
}
.page-content {
flex: 1;
display: flex;
flex-direction: column;
overflow: hidden;
min-height: 0;
}
.tool-container {
flex: 1;
display: flex;
@ -858,7 +728,6 @@ h1 {
min-height: 0;
}
/* 上传区域 */
.upload-section {
flex: 1;
display: flex;
@ -872,7 +741,6 @@ h1 {
border: 1px solid #e5e7eb;
}
/* 当显示图片时,移除 padding 和边框 */
.upload-section:has(.preview-section) {
padding: 0;
border-radius: 0;
@ -951,7 +819,6 @@ h1 {
margin-top: 4px;
}
/* 预览区域 */
.preview-section {
flex: 1;
width: 100%;
@ -961,10 +828,9 @@ h1 {
justify-content: center;
overflow: hidden;
min-height: 0;
padding: 12px; /* 用于计算可用空间 */
padding: 12px;
}
.preview-grid {
display: grid;
grid-template-columns: 1fr 1fr;
@ -992,7 +858,6 @@ h1 {
letter-spacing: 0.5px;
}
/* 对比视图 */
.comparison-view {
width: 100%;
height: 100%;
@ -1005,8 +870,6 @@ h1 {
.comparison-container {
position: relative;
display: inline-block;
/* 容器大小由 JavaScript 动态设置,匹配图片实际显示尺寸 */
/* 同时设置 max-width 和 max-height 作为安全限制,确保不超过父元素 */
max-width: 100%;
max-height: 100%;
overflow: hidden;
@ -1021,7 +884,6 @@ h1 {
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
/* 所有对比图片容器的公共样式 */
.comparison-image {
position: absolute;
top: 0;
@ -1044,15 +906,12 @@ h1 {
.original-image {
z-index: 1;
/* clip-path 通过内联样式动态设置,用于裁剪显示区域 */
}
.result-image {
z-index: 2;
/* clip-path 通过内联样式动态设置,用于裁剪显示区域 */
}
/* 拖动竖线 */
.split-line {
position: absolute;
top: 0;
@ -1097,7 +956,6 @@ h1 {
}
}
/* 单图视图 */
.single-image-view {
width: 100%;
height: 100%;
@ -1134,24 +992,6 @@ h1 {
}
}
.placeholder {
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
gap: 12px;
color: #cbd5e1;
i {
font-size: 48px;
}
p {
font-size: 14px;
margin: 0;
}
}
.processing-overlay {
position: absolute;
top: 0;
@ -1188,13 +1028,11 @@ h1 {
}
}
/* 底部历史记录栏 */
.history-bar {
flex-shrink: 0;
width: 100%;
padding: 12px 0;
background: transparent;
border-top: 1px solid #e5e7eb;
display: flex;
justify-content: center;
}
@ -1355,8 +1193,6 @@ h1 {
}
}
/* 响应式设计 */
@media (max-width: 768px) {
.page-header {
padding: 10px 12px;