feat: 前端压缩图片

master
LCJ-MinYa 7 months ago
parent 41aee4cd47
commit f5aaaabaf1

@ -0,0 +1,351 @@
<template>
<div class="min-h-screen bg-gray-50 p-6">
<div class="max-w-4xl mx-auto bg-white rounded-lg shadow-md p-6">
<h1 class="text-2xl font-bold text-gray-800 mb-6">图片压缩工具</h1>
<!-- 上传区域 -->
<el-upload
class="upload-area mb-6"
drag
action=""
:auto-upload="false"
:on-change="handleFileChange"
:show-file-list="false"
>
<el-icon class="el-icon--upload"><upload-filled /></el-icon>
<div class="el-upload__text">拖拽图片到此处或 <em>点击上传</em></div>
<template #tip>
<div class="el-upload__tip text-gray-500">支持 JPG/PNG/WEBP 格式大小不超过 10MB</div>
</template>
</el-upload>
<!-- 控制面板 -->
<div
v-if="originalImage"
class="control-panel mb-6 p-4 border rounded-lg"
>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 压缩模式选择 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">压缩模式</label>
<el-select
v-model="compressionMode"
class="w-full"
>
<el-option
v-for="mode in compressionModes"
:key="mode.value"
:label="mode.label"
:value="mode.value"
/>
</el-select>
</div>
<!-- 质量滑块 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2"> 质量: {{ quality }}% </label>
<el-slider
v-model="quality"
:min="10"
:max="100"
:step="5"
/>
</div>
<!-- 宽度设置 -->
<div v-if="compressionMode === 'resize'">
<label class="block text-sm font-medium text-gray-700 mb-2"> 宽度(px): {{ targetWidth }} </label>
<el-slider
v-model="targetWidth"
:min="100"
:max="maxWidth"
:step="50"
/>
</div>
<!-- 格式选择 -->
<div>
<label class="block text-sm font-medium text-gray-700 mb-2">输出格式</label>
<el-select
v-model="outputFormat"
class="w-full"
>
<el-option
label="原始格式"
value="original"
/>
<el-option
label="JPEG"
value="jpeg"
/>
<el-option
label="PNG"
value="png"
/>
<el-option
label="WEBP"
value="webp"
/>
</el-select>
</div>
</div>
<div class="mt-4 flex justify-end">
<el-button
type="primary"
@click="compressImage"
:loading="isCompressing"
>
开始压缩
</el-button>
</div>
</div>
<!-- 结果展示 -->
<div
v-if="compressedImage"
class="result-area"
>
<h2 class="text-xl font-semibold text-gray-800 mb-4">压缩结果</h2>
<div class="grid grid-cols-1 md:grid-cols-2 gap-6">
<!-- 原始图片 -->
<div class="image-card">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium">原始图片</h3>
<span class="text-sm text-gray-500">{{ formatFileSize(originalSize) }}</span>
</div>
<div class="image-container border rounded-md overflow-hidden">
<img
:src="originalImage"
alt="原始图片"
class="w-full h-auto"
/>
</div>
<div class="mt-2 text-sm text-gray-500">尺寸: {{ originalWidth }} × {{ originalHeight }} px</div>
</div>
<!-- 压缩后图片 -->
<div class="image-card">
<div class="flex justify-between items-center mb-2">
<h3 class="font-medium">压缩后图片</h3>
<span class="text-sm text-gray-500">{{ formatFileSize(compressedSize) }}</span>
</div>
<div class="image-container border rounded-md overflow-hidden">
<img
:src="compressedImage"
alt="压缩后图片"
class="w-full h-auto"
/>
</div>
<div class="mt-2 text-sm text-gray-500">尺寸: {{ compressedWidth }} × {{ compressedHeight }} px</div>
<div class="mt-2 text-sm">压缩率: {{ compressionRatio }}%</div>
</div>
</div>
<div class="mt-6 flex justify-end">
<el-button
type="success"
@click="downloadCompressedImage"
:icon="Download"
>
下载压缩图片
</el-button>
</div>
</div>
</div>
</div>
</template>
<script setup>
import { ref, computed } from 'vue';
import { UploadFilled, Download } from '@element-plus/icons-vue';
//
const originalImage = ref(null);
const originalSize = ref(0);
const originalWidth = ref(0);
const originalHeight = ref(0);
const compressedImage = ref(null);
const compressedSize = ref(0);
const compressedWidth = ref(0);
const compressedHeight = ref(0);
const isCompressing = ref(false);
const quality = ref(80);
const targetWidth = ref(800);
const maxWidth = ref(2000);
const compressionMode = ref('quality');
const outputFormat = ref('original');
//
const compressionModes = [
{ label: '质量优先 (调整质量参数)', value: 'quality' },
{ label: '尺寸优先 (调整图片宽度)', value: 'resize' },
{ label: '智能压缩 (平衡质量与尺寸)', value: 'smart' },
];
//
const compressionRatio = computed(() => {
if (!originalSize.value || !compressedSize.value) return 0;
return Math.round((1 - compressedSize.value / originalSize.value) * 100);
});
//
const handleFileChange = (file) => {
const reader = new FileReader();
reader.onload = (e) => {
const img = new Image();
img.onload = () => {
originalWidth.value = img.width;
originalHeight.value = img.height;
maxWidth.value = img.width;
targetWidth.value = Math.min(800, img.width);
};
img.src = e.target.result;
originalImage.value = e.target.result;
originalSize.value = file.size;
};
reader.readAsDataURL(file.raw);
//
compressedImage.value = null;
compressedSize.value = 0;
};
//
const compressImage = async () => {
if (!originalImage.value) return;
isCompressing.value = true;
try {
// Canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
const img = new Image();
//
await new Promise((resolve) => {
img.onload = resolve;
img.src = originalImage.value;
});
//
let width = img.width;
let height = img.height;
if (compressionMode.value === 'resize' || compressionMode.value === 'smart') {
const ratio = img.height / img.width;
width = compressionMode.value === 'smart' ? Math.min(targetWidth.value, img.width) : targetWidth.value;
height = width * ratio;
}
// Canvas
canvas.width = width;
canvas.height = height;
// Canvas
ctx.drawImage(img, 0, 0, width, height);
//
let format = outputFormat.value === 'original' ? getImageFormat(originalImage.value) : outputFormat.value;
//
let compressionQuality = quality.value / 100;
//
if (compressionMode.value === 'smart') {
const sizeRatio = (width * height) / (img.width * img.height);
compressionQuality = Math.min(0.9, Math.max(0.6, sizeRatio * 0.8));
}
// Blob
const blob = await new Promise((resolve) => {
canvas.toBlob(
(blob) => {
resolve(blob);
},
`image/${format}`,
compressionQuality
);
});
// DataURL
const reader = new FileReader();
reader.onload = (e) => {
compressedImage.value = e.target.result;
compressedSize.value = blob.size;
compressedWidth.value = width;
compressedHeight.value = height;
};
reader.readAsDataURL(blob);
} catch (error) {
console.error('压缩失败:', error);
ElMessage.error('图片压缩失败,请重试');
} finally {
isCompressing.value = false;
}
};
//
const getImageFormat = (dataUrl) => {
const match = dataUrl.match(/^data:image\/(\w+);/);
return match ? match[1].toLowerCase() : 'jpeg';
};
//
const downloadCompressedImage = () => {
if (!compressedImage.value) return;
const link = document.createElement('a');
const format = outputFormat.value === 'original' ? getImageFormat(originalImage.value) : outputFormat.value;
link.href = compressedImage.value;
link.download = `compressed-image.${format}`;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
};
//
const formatFileSize = (bytes) => {
if (bytes === 0) return '0 Bytes';
const k = 1024;
const sizes = ['Bytes', 'KB', 'MB', 'GB'];
const i = Math.floor(Math.log(bytes) / Math.log(k));
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
};
</script>
<style lang="scss" scoped>
.upload-area {
:deep(.el-upload-dragger) {
@apply p-8;
&:hover {
@apply border-blue-400;
}
}
}
.image-card {
@apply p-3 border rounded-lg bg-gray-50;
.image-container {
@apply flex items-center justify-center bg-gray-100;
min-height: 200px;
max-height: 400px;
overflow: hidden;
img {
object-fit: contain;
}
}
}
.control-panel {
@apply bg-gray-50 border-gray-200;
}
.result-area {
@apply mt-8 pt-6 border-t;
}
</style>
Loading…
Cancel
Save