image-optimizer/src/views/OptimizeView.vue

243 lines
8.8 KiB
Vue

<template>
<div class="container mx-auto px-4 py-8">
<div class="space-y-8">
<!-- 上传区域 -->
<div
class="border-2 border-dashed border-primary-300 dark:border-primary-700 rounded-xl p-8 text-center"
:class="{
'bg-primary-50 dark:bg-primary-900/20': isDragging,
'bg-white dark:bg-dark-800': !isDragging
}"
@drop.prevent="handleDrop"
@dragover.prevent="isDragging = true"
@dragleave.prevent="isDragging = false"
>
<input
ref="fileInput"
type="file"
accept="image/*"
multiple
class="hidden"
@change="handleFileSelect"
/>
<div class="space-y-4">
<div class="w-16 h-16 mx-auto bg-primary-100 dark:bg-primary-500/10 rounded-lg flex items-center justify-center">
<svg class="w-8 h-8 text-primary-500 dark:text-primary-400" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16l4.586-4.586a2 2 0 012.828 0L16 16m-2-2l1.586-1.586a2 2 0 012.828 0L20 14m-6-6h.01M6 20h12a2 2 0 002-2V6a2 2 0 00-2-2H6a2 2 0 00-2 2v12a2 2 0 002 2z" />
</svg>
</div>
<div>
<button
class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium"
@click="(fileInput as HTMLInputElement).click()"
>
选择图片
</button>
<span class="text-dark-600 dark:text-dark-300">或将图片拖放到此处</span>
</div>
<p class="text-sm text-dark-500 dark:text-dark-400">
支持 JPGPNGWebP 等格式
</p>
</div>
</div>
<!-- 文件列表 -->
<div v-if="files.length > 0" class="space-y-4">
<div v-for="(file, index) in files" :key="index" class="bg-white dark:bg-dark-800 rounded-lg p-4 shadow-sm">
<div class="flex items-center justify-between">
<div class="flex-1 min-w-0 pr-4">
<p class="font-medium text-dark-900 dark:text-dark-50 truncate">{{ file.originalFile.name }}</p>
<p class="text-sm text-dark-500 dark:text-dark-400">{{ formatFileSize(file.originalFile.size) }}</p>
<div v-if="file.processedSize !== undefined" class="mt-1 text-sm">
<span class="text-primary-600 dark:text-primary-400">
处理完成
- 压缩率 {{ Math.round((1 - file.processedSize / file.originalFile.size) * 100) }}%
</span>
</div>
</div>
<div class="flex items-center space-x-2">
<button
v-if="file.processedSize !== undefined"
class="px-3 py-1 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 text-white rounded"
@click="downloadFile(file)"
>
下载
</button>
<button
class="p-1 text-dark-500 hover:text-dark-700 dark:text-dark-400 dark:hover:text-dark-200"
@click="removeFile(index)"
>
<svg class="w-5 h-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M6 18L18 6M6 6l12 12" />
</svg>
</button>
</div>
</div>
</div>
</div>
<!-- 控制面板 -->
<div v-if="files.length > 0" class="bg-white dark:bg-dark-800 rounded-lg p-6 shadow-sm space-y-6">
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<div>
<label class="block text-sm font-medium text-dark-900 dark:text-dark-50 mb-2">
输出格式
</label>
<select
v-model="outputFormat"
class="w-full bg-light-50 dark:bg-dark-700 border border-dark-200 dark:border-dark-600 rounded-lg px-3 py-2 focus:outline-none focus:ring-2 focus:ring-primary-500 dark:focus:ring-primary-400"
>
<option value="jpeg">JPEG</option>
<option value="png">PNG</option>
<option value="webp">WebP</option>
</select>
</div>
<div>
<label class="block text-sm font-medium text-dark-900 dark:text-dark-50 mb-2">
压缩质量
</label>
<div class="flex items-center space-x-4">
<input
v-model="quality"
type="range"
min="0"
max="1"
step="0.1"
class="flex-1"
/>
<span class="text-dark-900 dark:text-dark-50 w-12 text-center">
{{ Math.round(quality * 100) }}%
</span>
</div>
</div>
</div>
<div class="flex justify-between items-center">
<button
class="px-6 py-2 bg-primary-500 hover:bg-primary-600 dark:bg-primary-600 dark:hover:bg-primary-700 text-white rounded-lg shadow-sm hover:shadow transition-all duration-200"
@click="processAllFiles"
>
开始处理
</button>
<button
v-if="hasProcessedFiles"
class="px-6 py-2 border border-primary-500 dark:border-primary-400 text-primary-600 dark:text-primary-400 hover:bg-primary-50 dark:hover:bg-primary-900/20 rounded-lg transition-colors duration-200"
@click="downloadAllFiles"
>
下载全部
</button>
</div>
</div>
</div>
</div>
</template>
<script setup lang="ts">
import { ref, computed } from 'vue'
import imageCompression from 'browser-image-compression'
import JSZip from 'jszip'
interface ProcessedFile {
originalFile: File
processedBlob?: Blob
processedSize?: number
}
const fileInput = ref<HTMLInputElement | null>(null)
const isDragging = ref(false)
const files = ref<ProcessedFile[]>([])
const outputFormat = ref<'jpeg' | 'png' | 'webp'>('jpeg')
const quality = ref(0.8)
const hasProcessedFiles = computed(() => {
return files.value.some(file => file.processedSize !== undefined)
})
function handleDrop(e: DragEvent) {
isDragging.value = false
if (!e.dataTransfer) return
const droppedFiles = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'))
addFiles(droppedFiles)
}
function handleFileSelect(e: Event) {
const target = e.target as HTMLInputElement
if (!target.files) return
const selectedFiles = Array.from(target.files).filter(file => file.type.startsWith('image/'))
addFiles(selectedFiles)
}
function addFiles(newFiles: File[]) {
const processedFiles: ProcessedFile[] = newFiles.map(file => ({
originalFile: file
}))
files.value.push(...processedFiles)
}
function removeFile(index: number) {
files.value.splice(index, 1)
}
function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
}
async function processFile(file: ProcessedFile) {
try {
const options = {
maxSizeMB: 1,
maxWidthOrHeight: 1920,
useWebWorker: true,
fileType: `image/${outputFormat.value}`,
initialQuality: quality.value
}
const compressedBlob = await imageCompression(file.originalFile, options)
file.processedBlob = compressedBlob
file.processedSize = compressedBlob.size
} catch (error) {
console.error('Error processing file:', error)
}
}
async function processAllFiles() {
for (const file of files.value) {
await processFile(file)
}
}
function downloadFile(file: ProcessedFile) {
if (!file.processedBlob) return
const link = document.createElement('a')
link.href = URL.createObjectURL(file.processedBlob)
link.download = `optimized_${file.originalFile.name.replace(/\.[^/.]+$/, '')}.${outputFormat.value}`
link.click()
URL.revokeObjectURL(link.href)
}
async function downloadAllFiles() {
const zip = new JSZip()
for (const file of files.value) {
if (file.processedBlob) {
const fileName = `optimized_${file.originalFile.name.replace(/\.[^/.]+$/, '')}.${outputFormat.value}`
zip.file(fileName, file.processedBlob)
}
}
const content = await zip.generateAsync({ type: 'blob' })
const link = document.createElement('a')
link.href = URL.createObjectURL(content)
link.download = 'optimized_images.zip'
link.click()
URL.revokeObjectURL(link.href)
}
</script>