2025-04-12 19:01:23 +08:00
|
|
|
<template>
|
2025-04-12 19:23:19 +08:00
|
|
|
<div class="container mx-auto px-4 py-8">
|
|
|
|
<div class="space-y-8">
|
|
|
|
<!-- 上传区域 -->
|
2025-04-12 19:01:23 +08:00
|
|
|
<div
|
2025-04-12 19:23:19 +08:00
|
|
|
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
|
|
|
|
}"
|
2025-04-12 19:01:23 +08:00
|
|
|
@drop.prevent="handleDrop"
|
2025-04-12 19:23:19 +08:00
|
|
|
@dragover.prevent="isDragging = true"
|
|
|
|
@dragleave.prevent="isDragging = false"
|
2025-04-12 19:01:23 +08:00
|
|
|
>
|
|
|
|
<input
|
|
|
|
ref="fileInput"
|
2025-04-12 19:23:19 +08:00
|
|
|
type="file"
|
2025-04-12 19:01:23 +08:00
|
|
|
accept="image/*"
|
|
|
|
multiple
|
2025-04-12 19:23:19 +08:00
|
|
|
class="hidden"
|
2025-04-12 19:01:23 +08:00
|
|
|
@change="handleFileSelect"
|
|
|
|
/>
|
2025-04-12 19:23:19 +08:00
|
|
|
|
2025-04-12 19:01:23 +08:00
|
|
|
<div class="space-y-4">
|
2025-04-12 19:23:19 +08:00
|
|
|
<div class="w-16 h-16 mx-auto bg-primary-100 dark:bg-primary-500/10 rounded-lg flex items-center justify-center">
|
2025-04-12 19:01:23 +08:00
|
|
|
<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>
|
2025-04-12 19:23:19 +08:00
|
|
|
<div>
|
2025-04-12 19:01:23 +08:00
|
|
|
<button
|
2025-04-12 19:23:19 +08:00
|
|
|
class="text-primary-600 dark:text-primary-400 hover:text-primary-700 dark:hover:text-primary-300 font-medium"
|
|
|
|
@click="(fileInput as HTMLInputElement).click()"
|
2025-04-12 19:01:23 +08:00
|
|
|
>
|
2025-04-12 19:23:19 +08:00
|
|
|
选择图片
|
2025-04-12 19:01:23 +08:00
|
|
|
</button>
|
2025-04-12 19:23:19 +08:00
|
|
|
<span class="text-dark-600 dark:text-dark-300">或将图片拖放到此处</span>
|
2025-04-12 19:01:23 +08:00
|
|
|
</div>
|
2025-04-12 19:23:19 +08:00
|
|
|
<p class="text-sm text-dark-500 dark:text-dark-400">
|
|
|
|
支持 JPG、PNG、WebP 等格式
|
|
|
|
</p>
|
2025-04-12 19:01:23 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
<!-- 文件列表 -->
|
|
|
|
<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>
|
2025-04-12 19:01:23 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
2025-04-12 19:23:19 +08:00
|
|
|
</div>
|
2025-04-12 19:01:23 +08:00
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
<!-- 控制面板 -->
|
|
|
|
<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">
|
2025-04-12 19:01:23 +08:00
|
|
|
<div>
|
2025-04-12 19:23:19 +08:00
|
|
|
<label class="block text-sm font-medium text-dark-900 dark:text-dark-50 mb-2">
|
2025-04-12 19:01:23 +08:00
|
|
|
输出格式
|
|
|
|
</label>
|
2025-04-12 19:23:19 +08:00
|
|
|
<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"
|
2025-04-12 19:01:23 +08:00
|
|
|
>
|
|
|
|
<option value="jpeg">JPEG</option>
|
|
|
|
<option value="png">PNG</option>
|
|
|
|
<option value="webp">WebP</option>
|
|
|
|
</select>
|
|
|
|
</div>
|
|
|
|
<div>
|
2025-04-12 19:23:19 +08:00
|
|
|
<label class="block text-sm font-medium text-dark-900 dark:text-dark-50 mb-2">
|
|
|
|
压缩质量
|
2025-04-12 19:01:23 +08:00
|
|
|
</label>
|
2025-04-12 19:23:19 +08:00
|
|
|
<div class="flex items-center space-x-4">
|
2025-04-12 19:01:23 +08:00
|
|
|
<input
|
2025-04-12 19:23:19 +08:00
|
|
|
v-model="quality"
|
2025-04-12 19:01:23 +08:00
|
|
|
type="range"
|
|
|
|
min="0"
|
2025-04-12 19:23:19 +08:00
|
|
|
max="1"
|
|
|
|
step="0.1"
|
|
|
|
class="flex-1"
|
2025-04-12 19:01:23 +08:00
|
|
|
/>
|
2025-04-12 19:23:19 +08:00
|
|
|
<span class="text-dark-900 dark:text-dark-50 w-12 text-center">
|
|
|
|
{{ Math.round(quality * 100) }}%
|
|
|
|
</span>
|
2025-04-12 19:01:23 +08:00
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
<div class="flex justify-between items-center">
|
2025-04-12 19:01:23 +08:00
|
|
|
<button
|
2025-04-12 19:23:19 +08:00
|
|
|
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"
|
2025-04-12 19:01:23 +08:00
|
|
|
>
|
2025-04-12 19:23:19 +08:00
|
|
|
开始处理
|
2025-04-12 19:01:23 +08:00
|
|
|
</button>
|
|
|
|
<button
|
2025-04-12 19:23:19 +08:00
|
|
|
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"
|
2025-04-12 19:01:23 +08:00
|
|
|
>
|
2025-04-12 19:23:19 +08:00
|
|
|
下载全部
|
2025-04-12 19:01:23 +08:00
|
|
|
</button>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</div>
|
|
|
|
</template>
|
|
|
|
|
|
|
|
<script setup lang="ts">
|
2025-04-12 19:23:19 +08:00
|
|
|
import { ref, computed } from 'vue'
|
2025-04-12 19:01:23 +08:00
|
|
|
import imageCompression from 'browser-image-compression'
|
|
|
|
import JSZip from 'jszip'
|
|
|
|
|
|
|
|
interface ProcessedFile {
|
2025-04-12 19:23:19 +08:00
|
|
|
originalFile: File
|
2025-04-12 19:01:23 +08:00
|
|
|
processedBlob?: Blob
|
2025-04-12 19:23:19 +08:00
|
|
|
processedSize?: number
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
|
|
|
|
const fileInput = ref<HTMLInputElement | null>(null)
|
|
|
|
const isDragging = ref(false)
|
|
|
|
const files = ref<ProcessedFile[]>([])
|
2025-04-12 19:23:19 +08:00
|
|
|
const outputFormat = ref<'jpeg' | 'png' | 'webp'>('jpeg')
|
|
|
|
const quality = ref(0.8)
|
2025-04-12 19:01:23 +08:00
|
|
|
|
|
|
|
const hasProcessedFiles = computed(() => {
|
2025-04-12 19:23:19 +08:00
|
|
|
return files.value.some(file => file.processedSize !== undefined)
|
2025-04-12 19:01:23 +08:00
|
|
|
})
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
function handleDrop(e: DragEvent) {
|
2025-04-12 19:01:23 +08:00
|
|
|
isDragging.value = false
|
2025-04-12 19:23:19 +08:00
|
|
|
if (!e.dataTransfer) return
|
|
|
|
|
|
|
|
const droppedFiles = Array.from(e.dataTransfer.files).filter(file => file.type.startsWith('image/'))
|
|
|
|
addFiles(droppedFiles)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
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)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
function addFiles(newFiles: File[]) {
|
|
|
|
const processedFiles: ProcessedFile[] = newFiles.map(file => ({
|
|
|
|
originalFile: file
|
|
|
|
}))
|
|
|
|
files.value.push(...processedFiles)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
function removeFile(index: number) {
|
2025-04-12 19:01:23 +08:00
|
|
|
files.value.splice(index, 1)
|
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
function formatFileSize(bytes: number): string {
|
|
|
|
if (bytes === 0) return '0 B'
|
2025-04-12 19:01:23 +08:00
|
|
|
const k = 1024
|
2025-04-12 19:23:19 +08:00
|
|
|
const sizes = ['B', 'KB', 'MB', 'GB']
|
2025-04-12 19:01:23 +08:00
|
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k))
|
2025-04-12 19:23:19 +08:00
|
|
|
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(2))} ${sizes[i]}`
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
async function processFile(file: ProcessedFile) {
|
2025-04-12 19:01:23 +08:00
|
|
|
try {
|
2025-04-12 19:23:19 +08:00
|
|
|
const options = {
|
|
|
|
maxSizeMB: 1,
|
|
|
|
maxWidthOrHeight: 1920,
|
|
|
|
useWebWorker: true,
|
|
|
|
fileType: `image/${outputFormat.value}`,
|
|
|
|
initialQuality: quality.value
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
2025-04-12 19:23:19 +08:00
|
|
|
|
|
|
|
const compressedBlob = await imageCompression(file.originalFile, options)
|
|
|
|
file.processedBlob = compressedBlob
|
|
|
|
file.processedSize = compressedBlob.size
|
|
|
|
} catch (error) {
|
|
|
|
console.error('Error processing file:', error)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2025-04-12 19:23:19 +08:00
|
|
|
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() {
|
2025-04-12 19:01:23 +08:00
|
|
|
const zip = new JSZip()
|
|
|
|
|
|
|
|
for (const file of files.value) {
|
2025-04-12 19:23:19 +08:00
|
|
|
if (file.processedBlob) {
|
|
|
|
const fileName = `optimized_${file.originalFile.name.replace(/\.[^/.]+$/, '')}.${outputFormat.value}`
|
|
|
|
zip.file(fileName, file.processedBlob)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
}
|
2025-04-12 19:23:19 +08:00
|
|
|
|
2025-04-12 19:01:23 +08:00
|
|
|
const content = await zip.generateAsync({ type: 'blob' })
|
|
|
|
const link = document.createElement('a')
|
2025-04-12 19:23:19 +08:00
|
|
|
link.href = URL.createObjectURL(content)
|
2025-04-12 19:01:23 +08:00
|
|
|
link.download = 'optimized_images.zip'
|
|
|
|
link.click()
|
2025-04-12 19:23:19 +08:00
|
|
|
URL.revokeObjectURL(link.href)
|
2025-04-12 19:01:23 +08:00
|
|
|
}
|
|
|
|
</script>
|