332 lines
15 KiB
HTML
332 lines
15 KiB
HTML
<!DOCTYPE html>
|
||
<html lang="zh-CN">
|
||
<head>
|
||
<meta charset="UTF-8">
|
||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||
<title>图片转 WebP</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Inter:wght@400;500;600&display=swap');
|
||
body { font-family: 'Inter', system-ui, sans-serif; }
|
||
.drop-zone {
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
.drop-zone.dragover {
|
||
background-color: rgb(249 250 251);
|
||
border-color: rgb(59 130 246);
|
||
}
|
||
.preview-img {
|
||
max-height: 320px;
|
||
object-fit: contain;
|
||
box-shadow: 0 10px 15px -3px rgb(0 0 0 / 0.1);
|
||
}
|
||
</style>
|
||
</head>
|
||
<body class="bg-zinc-950 text-zinc-100 min-h-screen">
|
||
<div class="max-w-5xl mx-auto px-4 py-8">
|
||
<!-- 头部 -->
|
||
<div class="flex flex-col items-center mb-10">
|
||
<div class="flex items-center gap-3 mb-2">
|
||
<div class="w-9 h-9 bg-blue-600 rounded-xl flex items-center justify-center text-white font-bold text-2xl">W</div>
|
||
<h1 class="text-4xl font-semibold tracking-tight">图片转 WebP</h1>
|
||
</div>
|
||
<p class="text-zinc-400 text-center max-w-md">
|
||
本地转换 · 零上传 · 隐私安全<br>
|
||
<span class="text-xs">支持 JPEG / PNG / GIF / BMP / AVIF 等任意格式</span>
|
||
</p>
|
||
</div>
|
||
|
||
<div id="main" class="grid grid-cols-1 lg:grid-cols-2 gap-8">
|
||
|
||
<!-- 左侧:上传 & 原图 -->
|
||
<div class="space-y-6">
|
||
<!-- 上传区 -->
|
||
<div id="upload-zone"
|
||
class="drop-zone border-2 border-dashed border-zinc-700 hover:border-blue-500 rounded-3xl h-80 flex flex-col items-center justify-center cursor-pointer bg-zinc-900/50">
|
||
<input type="file" id="file-input" accept="image/*" class="hidden">
|
||
<div class="text-center">
|
||
<div class="w-16 h-16 mx-auto mb-4 bg-zinc-800 rounded-2xl flex items-center justify-center">
|
||
📸
|
||
</div>
|
||
<p class="text-lg font-medium mb-1">拖拽图片到这里</p>
|
||
<p class="text-sm text-zinc-400">或点击选择文件</p>
|
||
<p class="text-[10px] text-zinc-500 mt-6">支持所有常见图片格式</p>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 原图预览 -->
|
||
<div id="original-preview" class="hidden bg-zinc-900 rounded-3xl overflow-hidden border border-zinc-800">
|
||
<div class="bg-zinc-950 px-5 py-3 flex items-center justify-between border-b border-zinc-800">
|
||
<span class="font-medium text-sm">原图</span>
|
||
<span id="original-info" class="text-xs text-zinc-400"></span>
|
||
</div>
|
||
<div class="p-5">
|
||
<img id="original-img" class="preview-img w-full mx-auto rounded-2xl" alt="原图">
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧:设置 & 转换结果 -->
|
||
<div class="space-y-6">
|
||
<!-- 参数设置 -->
|
||
<div id="controls" class="hidden bg-zinc-900 rounded-3xl p-6 border border-zinc-800">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<span class="font-medium text-sm">转换设置</span>
|
||
</div>
|
||
|
||
<div class="space-y-6">
|
||
<!-- 质量滑块 -->
|
||
<div>
|
||
<div class="flex justify-between text-sm mb-2">
|
||
<span class="text-zinc-400">压缩质量</span>
|
||
<span id="quality-value" class="font-mono text-blue-400">82</span>
|
||
</div>
|
||
<input type="range" id="quality"
|
||
min="1" max="100" value="82"
|
||
class="w-full accent-blue-500">
|
||
<div class="flex justify-between text-[10px] text-zinc-500 mt-1">
|
||
<span>节省空间</span>
|
||
<span>最高画质</span>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 转换按钮 -->
|
||
<button id="convert-btn"
|
||
class="w-full bg-blue-600 hover:bg-blue-500 transition-colors text-white font-medium py-4 rounded-2xl text-lg shadow-lg shadow-blue-500/30 flex items-center justify-center gap-2">
|
||
<span>🚀</span>
|
||
<span>开始转换为 WebP</span>
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 转换结果 -->
|
||
<div id="result" class="hidden bg-zinc-900 rounded-3xl overflow-hidden border border-zinc-800">
|
||
<div class="bg-zinc-950 px-5 py-3 flex items-center justify-between border-b border-zinc-800">
|
||
<span class="font-medium text-sm">转换结果</span>
|
||
<button id="convert-again"
|
||
class="text-xs text-blue-400 hover:text-blue-300 flex items-center gap-1">
|
||
<span>↻</span> 转换另一张
|
||
</button>
|
||
</div>
|
||
|
||
<div class="p-5">
|
||
<img id="webp-img" class="preview-img w-full mx-auto rounded-2xl" alt="WebP">
|
||
</div>
|
||
|
||
<!-- 数据对比 -->
|
||
<div class="grid grid-cols-2 gap-px bg-zinc-800 mx-5 mb-5 rounded-2xl overflow-hidden">
|
||
<div class="bg-zinc-900 p-4">
|
||
<div class="text-xs text-zinc-400">原图大小</div>
|
||
<div id="orig-size" class="font-mono text-lg font-medium text-white"></div>
|
||
<div id="orig-dim" class="text-xs text-zinc-500 mt-1"></div>
|
||
</div>
|
||
<div class="bg-zinc-900 p-4">
|
||
<div class="text-xs text-emerald-400">WebP 大小</div>
|
||
<div id="webp-size" class="font-mono text-lg font-medium text-emerald-400"></div>
|
||
<div id="webp-dim" class="text-xs text-zinc-500 mt-1"></div>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="mx-5 mb-6 bg-emerald-950 border border-emerald-900 rounded-2xl px-4 py-3 flex items-center justify-between">
|
||
<span class="text-emerald-400 text-sm">压缩率</span>
|
||
<span id="compress-rate" class="font-mono text-2xl font-semibold text-emerald-400"></span>
|
||
</div>
|
||
|
||
<!-- 下载按钮 -->
|
||
<div class="px-5 pb-5">
|
||
<a id="download-link"
|
||
class="block text-center bg-white text-zinc-950 font-medium py-4 rounded-2xl text-lg hover:bg-zinc-100 transition-colors">
|
||
⬇️ 下载 WebP 文件
|
||
</a>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 提示 -->
|
||
<div class="mt-12 text-center text-xs text-zinc-500">
|
||
全部在浏览器本地完成 · 无需联网 · 图片永不上传
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// Tailwind 脚本已通过 CDN 加载,这里无需额外 script
|
||
function initTailwind() {
|
||
// 已在 head 加载
|
||
}
|
||
|
||
let originalFile = null;
|
||
let webpBlob = null;
|
||
let originalUrl = null;
|
||
let webpUrl = null;
|
||
|
||
const uploadZone = document.getElementById('upload-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
const originalPreview = document.getElementById('original-preview');
|
||
const originalImg = document.getElementById('original-img');
|
||
const originalInfo = document.getElementById('original-info');
|
||
|
||
const controls = document.getElementById('controls');
|
||
const qualitySlider = document.getElementById('quality');
|
||
const qualityValue = document.getElementById('quality-value');
|
||
const convertBtn = document.getElementById('convert-btn');
|
||
|
||
const resultPanel = document.getElementById('result');
|
||
const webpImg = document.getElementById('webp-img');
|
||
const origSizeEl = document.getElementById('orig-size');
|
||
const origDimEl = document.getElementById('orig-dim');
|
||
const webpSizeEl = document.getElementById('webp-size');
|
||
const webpDimEl = document.getElementById('webp-dim');
|
||
const compressRateEl = document.getElementById('compress-rate');
|
||
const downloadLink = document.getElementById('download-link');
|
||
const convertAgain = document.getElementById('convert-again');
|
||
|
||
// 拖拽事件
|
||
function setupDragDrop() {
|
||
uploadZone.addEventListener('dragover', e => {
|
||
e.preventDefault();
|
||
uploadZone.classList.add('dragover');
|
||
});
|
||
uploadZone.addEventListener('dragleave', () => {
|
||
uploadZone.classList.remove('dragover');
|
||
});
|
||
uploadZone.addEventListener('drop', e => {
|
||
e.preventDefault();
|
||
uploadZone.classList.remove('dragover');
|
||
handleFiles(e.dataTransfer.files);
|
||
});
|
||
uploadZone.addEventListener('click', () => fileInput.click());
|
||
}
|
||
|
||
// 文件选择
|
||
fileInput.addEventListener('change', e => {
|
||
if (e.target.files.length > 0) handleFiles(e.target.files);
|
||
});
|
||
|
||
function handleFiles(files) {
|
||
const file = files[0];
|
||
if (!file || !file.type.startsWith('image/')) {
|
||
alert('请选择图片文件!');
|
||
return;
|
||
}
|
||
originalFile = file;
|
||
|
||
// 释放之前的 URL
|
||
if (originalUrl) URL.revokeObjectURL(originalUrl);
|
||
|
||
originalUrl = URL.createObjectURL(file);
|
||
originalImg.src = originalUrl;
|
||
|
||
// 显示原图信息
|
||
const sizeMB = (file.size / 1024 / 1024).toFixed(2);
|
||
originalInfo.textContent = `${file.name} · ${(file.size / 1024).toFixed(1)} KB`;
|
||
|
||
originalPreview.classList.remove('hidden');
|
||
controls.classList.remove('hidden');
|
||
resultPanel.classList.add('hidden');
|
||
|
||
// 重置
|
||
webpBlob = null;
|
||
}
|
||
|
||
// 质量滑块实时显示
|
||
qualitySlider.addEventListener('input', () => {
|
||
qualityValue.textContent = qualitySlider.value;
|
||
});
|
||
|
||
// 转换核心函数
|
||
async function convertToWebP() {
|
||
if (!originalFile) return;
|
||
|
||
convertBtn.innerHTML = `
|
||
<svg class="animate-spin h-5 w-5 text-white" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4z"></path>
|
||
</svg>
|
||
`;
|
||
convertBtn.disabled = true;
|
||
|
||
const img = new Image();
|
||
img.onload = async () => {
|
||
const canvas = document.createElement('canvas');
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
const ctx = canvas.getContext('2d');
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
const quality = parseFloat(qualitySlider.value) / 100;
|
||
|
||
// 使用 toBlob 获得更精确的 Blob
|
||
canvas.toBlob(async (blob) => {
|
||
webpBlob = blob;
|
||
|
||
if (webpUrl) URL.revokeObjectURL(webpUrl);
|
||
webpUrl = URL.createObjectURL(webpBlob);
|
||
|
||
webpImg.src = webpUrl;
|
||
|
||
// 计算数据
|
||
const origSize = originalFile.size;
|
||
const webpSize = webpBlob.size;
|
||
const rate = ((origSize - webpSize) / origSize * 100).toFixed(1);
|
||
|
||
// 填充界面
|
||
origSizeEl.textContent = formatSize(origSize);
|
||
origDimEl.textContent = `${img.width}×${img.height}`;
|
||
|
||
webpSizeEl.textContent = formatSize(webpSize);
|
||
webpDimEl.textContent = `${img.width}×${img.height}`;
|
||
|
||
compressRateEl.textContent = rate + '%';
|
||
|
||
// 下载链接
|
||
const newName = originalFile.name.replace(/\.[^/.]+$/, "") + ".webp";
|
||
downloadLink.download = newName;
|
||
downloadLink.href = webpUrl;
|
||
|
||
// 显示结果
|
||
resultPanel.classList.remove('hidden');
|
||
convertBtn.innerHTML = `✅ 已完成`;
|
||
setTimeout(() => {
|
||
convertBtn.innerHTML = `<span>🚀</span><span>重新转换</span>`;
|
||
convertBtn.disabled = false;
|
||
}, 800);
|
||
|
||
}, 'image/webp', quality);
|
||
};
|
||
|
||
img.src = originalUrl;
|
||
}
|
||
|
||
function formatSize(bytes) {
|
||
if (bytes < 1024) return bytes + ' B';
|
||
else if (bytes < 1024 * 1024) return (bytes / 1024).toFixed(1) + ' KB';
|
||
else return (bytes / 1024 / 1024).toFixed(2) + ' MB';
|
||
}
|
||
|
||
// 绑定按钮
|
||
convertBtn.addEventListener('click', convertToWebP);
|
||
|
||
convertAgain.addEventListener('click', () => {
|
||
resultPanel.classList.add('hidden');
|
||
// 清空结果但保留原图
|
||
});
|
||
|
||
// 初始化
|
||
function init() {
|
||
setupDragDrop();
|
||
// 初始质量显示
|
||
qualityValue.textContent = qualitySlider.value;
|
||
|
||
// 键盘快捷键:回车转换
|
||
document.addEventListener('keydown', e => {
|
||
if (e.key === 'Enter' && !resultPanel.classList.contains('hidden')) {
|
||
convertToWebP();
|
||
}
|
||
});
|
||
}
|
||
|
||
window.onload = init;
|
||
</script>
|
||
</body>
|
||
</html> |