|
|
|
@ -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>
|