improve RemoveBackground comparison view and split line styling

This commit is contained in:
jingrow 2025-11-19 06:59:55 +08:00
parent 6d861a2829
commit cac901e235

View File

@ -39,69 +39,67 @@
<div class="image-preview-container">
<div class="preview-header">
<h3>{{ t('Image Preview') }}</h3>
<button class="change-image-btn" @click="resetUpload">
<i class="fa fa-refresh"></i>
{{ t('Change Image') }}
</button>
<div class="header-actions">
<n-button
v-if="resultImage"
size="medium"
@click="handleDownload"
class="download-btn"
>
<template #icon>
<i class="fa fa-download"></i>
</template>
{{ t('Download') }}
</n-button>
<button class="change-image-btn" @click="resetUpload">
<i class="fa fa-refresh"></i>
{{ t('Change Image') }}
</button>
</div>
</div>
<div class="preview-grid">
<!-- 原图 -->
<div class="preview-card">
<div class="preview-label">{{ t('Original') }}</div>
<div class="image-wrapper">
<!-- 单窗口对比视图 -->
<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)` }"
>
<img :src="uploadedImageUrl" alt="Original" />
<div v-if="processing" class="processing-overlay">
<div class="spinner"></div>
<p>{{ t('Processing...') }}</p>
</div>
</div>
</div>
<!-- 去背景后的图片 -->
<div class="preview-card">
<div class="preview-label">{{ t('Background Removed') }}</div>
<div class="image-wrapper">
<div v-if="!resultImage && !processing" class="placeholder">
<i class="fa fa-image"></i>
<p>{{ t('Result will appear here') }}</p>
</div>
<img v-else-if="resultImage" :src="resultImageUrl" alt="Result" />
<div v-else class="processing-overlay">
<div class="spinner"></div>
<p>{{ t('Processing...') }}</p>
<!-- 去背景后的图片显示右侧部分 -->
<div
class="comparison-image result-image"
:style="{ clipPath: `inset(0 0 0 ${splitPosition}%)` }"
>
<img :src="resultImageUrl" alt="Result" />
</div>
<!-- 拖动竖线 -->
<div
class="split-line"
:class="{ 'dragging': isDraggingSplitLine }"
:style="{ left: `${splitPosition}%` }"
@mousedown.prevent.stop="handleSplitLineMouseDown"
>
<div class="split-line-handle">
<i class="fa fa-arrows-h"></i>
</div>
</div>
</div>
</div>
<!-- 操作按钮 -->
<div class="action-buttons">
<n-button
type="primary"
size="large"
:loading="processing"
:disabled="!uploadedImage || processing"
@click="handleRemoveBackground"
class="process-btn"
>
<template #icon>
<i class="fa fa-magic"></i>
</template>
{{ processing ? t('Processing...') : t('Remove Background') }}
</n-button>
<n-button
v-if="resultImage"
size="large"
@click="handleDownload"
class="download-btn"
>
<template #icon>
<i class="fa fa-download"></i>
</template>
{{ t('Download') }}
</n-button>
<!-- 处理中或未处理状态 -->
<div v-else class="single-image-view">
<div class="image-wrapper">
<img :src="uploadedImageUrl" alt="Original" />
<div v-if="processing" class="processing-overlay">
<div class="spinner"></div>
<p>{{ t('Processing...') }}</p>
</div>
</div>
</div>
</div>
</div>
@ -157,6 +155,9 @@ const resultImageUrl = computed(() => {
//
const isDragging = ref(false)
const processing = ref(false)
const splitPosition = ref(50) // 线
const comparisonContainerRef = ref<HTMLElement | null>(null)
const isDraggingSplitLine = ref(false)
//
const triggerFileInput = () => {
@ -209,11 +210,14 @@ const processFile = (file: File) => {
uploadedImage.value = file
resultImage.value = ''
splitPosition.value = 50 // 线
// URL
const reader = new FileReader()
reader.onload = (e) => {
uploadedImageUrl.value = e.target?.result as string
//
handleRemoveBackground()
}
reader.readAsDataURL(file)
}
@ -223,11 +227,36 @@ const resetUpload = () => {
uploadedImage.value = null
uploadedImageUrl.value = ''
resultImage.value = ''
splitPosition.value = 50
if (fileInputRef.value) {
fileInputRef.value.value = ''
}
}
// 线
const handleSplitLineMouseDown = (e: MouseEvent) => {
e.preventDefault()
isDraggingSplitLine.value = true
const handleMouseMove = (moveEvent: MouseEvent) => {
if (!comparisonContainerRef.value || !isDraggingSplitLine.value) return
const rect = comparisonContainerRef.value.getBoundingClientRect()
const x = moveEvent.clientX - rect.left
const percentage = Math.max(0, Math.min(100, (x / rect.width) * 100))
splitPosition.value = percentage
}
const handleMouseUp = () => {
isDraggingSplitLine.value = false
document.removeEventListener('mousemove', handleMouseMove)
document.removeEventListener('mouseup', handleMouseUp)
}
document.addEventListener('mousemove', handleMouseMove)
document.addEventListener('mouseup', handleMouseUp)
}
//
const handleRemoveBackground = async () => {
//
@ -378,6 +407,14 @@ const handleDownload = () => {
width: 100%;
padding: 16px;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
}
.remove-background-page > .page-content {
width: 100%;
max-width: 1000px;
}
.page-header {
@ -592,9 +629,114 @@ h1 {
letter-spacing: 0.5px;
}
/* 对比视图 */
.comparison-view {
width: 100%;
margin-bottom: 24px;
display: flex;
justify-content: center;
align-items: center;
}
.comparison-container {
position: relative;
width: 60%;
max-width: 600px;
aspect-ratio: 1;
border-radius: 12px;
overflow: hidden;
background:
linear-gradient(45deg, #f1f5f9 25%, transparent 25%),
linear-gradient(-45deg, #f1f5f9 25%, transparent 25%),
linear-gradient(45deg, transparent 75%, #f1f5f9 75%),
linear-gradient(-45deg, transparent 75%, #f1f5f9 75%);
background-size: 20px 20px;
background-position: 0 0, 0 10px, 10px -10px, -10px 0px;
}
.comparison-image {
position: absolute;
top: 0;
left: 0;
width: 100%;
height: 100%;
img {
width: 100%;
height: 100%;
object-fit: contain;
display: block;
}
}
.original-image {
z-index: 1;
/* clip-path 通过内联样式动态设置,用于裁剪显示区域 */
}
.result-image {
z-index: 2;
/* clip-path 通过内联样式动态设置,用于裁剪显示区域 */
}
/* 拖动竖线 */
.split-line {
position: absolute;
top: 0;
bottom: 0;
width: 2px;
background: transparent;
cursor: col-resize;
z-index: 10;
transform: translateX(-50%);
transition: left 0s;
user-select: none;
pointer-events: auto;
&:hover {
width: 2px;
}
&.dragging {
transition: none;
cursor: col-resize;
}
}
.split-line-handle {
position: absolute;
top: 50%;
left: 50%;
transform: translate(-50%, -50%);
width: 40px;
height: 40px;
background: rgba(255, 255, 255, 0.95);
border-radius: 50%;
display: flex;
align-items: center;
justify-content: center;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.15);
border: 2px solid #1fc76f;
i {
color: #1fc76f;
font-size: 16px;
}
}
/* 单图视图 */
.single-image-view {
width: 100%;
margin-bottom: 24px;
display: flex;
justify-content: center;
align-items: center;
}
.image-wrapper {
position: relative;
width: 100%;
width: 60%;
max-width: 600px;
aspect-ratio: 1;
border-radius: 8px;
overflow: hidden;