update: 2026-03-28 20:59

This commit is contained in:
2026-03-28 20:59:52 +08:00
parent e21d58e603
commit 1c81d4e6ea
611 changed files with 27847 additions and 65061 deletions

View File

@@ -0,0 +1,332 @@
<!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>