343 lines
16 KiB
HTML
343 lines
16 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>PNG ↔ JPG 图片转换器</title>
|
||
<script src="https://cdn.tailwindcss.com"></script>
|
||
<style>
|
||
@import url('https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@400;500;700&display=swap');
|
||
body { font-family: 'Noto Sans SC
|
||
|
||
|
||
', system-ui, sans-serif; }
|
||
.drop-zone {
|
||
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
|
||
}
|
||
.drop-zone.dragover {
|
||
border-color: #3b82f6;
|
||
background-color: #eff6ff;
|
||
transform: scale(1.02);
|
||
}
|
||
canvas { display: none; }
|
||
</style>
|
||
</head>
|
||
<body class="bg-gradient-to-br from-blue-50 to-indigo-50 min-h-screen py-12">
|
||
<div class="max-w-6xl mx-auto px-6">
|
||
<!-- 头部 -->
|
||
<div class="text-center mb-10">
|
||
<div class="inline-flex items-center gap-3 bg-white shadow-md rounded-3xl px-8 py-4">
|
||
<div class="w-12 h-12 bg-gradient-to-br from-blue-500 to-purple-600 rounded-2xl flex items-center justify-center text-white text-3xl">🖼️</div>
|
||
<h1 class="text-4xl font-bold text-gray-900 tracking-tight">PNG ↔ JPG 转换器</h1>
|
||
</div>
|
||
<p class="mt-4 text-gray-600 max-w-md mx-auto">纯浏览器本地转换 • 零上传 • 安全隐私 • 秒级处理</p>
|
||
</div>
|
||
|
||
<div class="bg-white rounded-3xl shadow-2xl overflow-hidden">
|
||
<div class="p-8">
|
||
<!-- 上传区域 -->
|
||
<div id="drop-zone"
|
||
class="drop-zone border-4 border-dashed border-gray-300 rounded-3xl p-16 text-center cursor-pointer hover:border-blue-500">
|
||
<input type="file" id="file-input" accept="image/png,image/jpeg" class="hidden">
|
||
<div class="mx-auto w-20 h-20 bg-blue-100 rounded-full flex items-center justify-center mb-6">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="w-10 h-10 text-blue-600" fill="none" viewBox="0 0 24 24" stroke="currentColor" stroke-width="2">
|
||
<path stroke-linecap="round" stroke-linejoin="round" d="M4 16v-4m0 0l4 4m-4-4l4-4m12 4v4m0 0l-4-4m4 4l-4 4" />
|
||
</svg>
|
||
</div>
|
||
<p class="text-2xl font-semibold text-gray-700">点击或拖拽图片到此区域</p>
|
||
<p class="text-sm text-gray-500 mt-3">支持 PNG / JPG 格式,最大 50MB</p>
|
||
</div>
|
||
|
||
<!-- 主内容区 -->
|
||
<div id="main-content" class="hidden mt-12 grid grid-cols-1 lg:grid-cols-12 gap-8">
|
||
<!-- 原图 -->
|
||
<div class="lg:col-span-5 bg-gray-50 rounded-3xl p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="font-semibold text-lg text-gray-800 flex items-center gap-2">
|
||
<span class="w-3 h-3 bg-green-500 rounded-full"></span>
|
||
原图
|
||
</h3>
|
||
<span id="original-info" class="text-xs text-gray-500 font-mono"></span>
|
||
</div>
|
||
<div class="aspect-video bg-white rounded-2xl overflow-hidden border border-gray-200 flex items-center justify-center">
|
||
<img id="original-img" class="max-h-full max-w-full object-contain" alt="原图预览">
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 控制面板 -->
|
||
<div class="lg:col-span-2 flex flex-col justify-center gap-6">
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-2">转换目标格式</label>
|
||
<div class="flex gap-2">
|
||
<button id="btn-jpg"
|
||
class="flex-1 py-3 px-6 rounded-2xl font-medium transition-all active:scale-95 border-2 border-transparent bg-white shadow-sm hover:shadow-md data-[active=true]:border-blue-600 data-[active=true]:bg-blue-50">
|
||
转为 JPG
|
||
</button>
|
||
<button id="btn-png"
|
||
class="flex-1 py-3 px-6 rounded-2xl font-medium transition-all active:scale-95 border-2 border-transparent bg-white shadow-sm hover:shadow-md data-[active=true]:border-blue-600 data-[active=true]:bg-blue-50">
|
||
转为 PNG
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- JPG 选项 -->
|
||
<div id="jpg-options" class="hidden space-y-5">
|
||
<div>
|
||
<div class="flex justify-between text-sm mb-1.5">
|
||
<span class="font-medium text-gray-700">JPG 质量</span>
|
||
<span id="quality-value" class="font-mono text-blue-600">92%</span>
|
||
</div>
|
||
<input type="range" id="quality-slider" min="10" max="100" value="92"
|
||
class="w-full accent-blue-600">
|
||
</div>
|
||
|
||
<div>
|
||
<label class="block text-sm font-medium text-gray-700 mb-1.5">透明背景填充颜色(PNG→JPG)</label>
|
||
<div class="flex gap-3 items-center">
|
||
<input type="color" id="bg-color" value="#ffffff"
|
||
class="w-12 h-10 rounded-xl border border-gray-300 cursor-pointer">
|
||
<div id="bg-color-hex" class="font-mono text-sm text-gray-500">#ffffff</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<button onclick="convertImage()"
|
||
class="mt-4 w-full bg-gradient-to-r from-blue-600 to-indigo-600 hover:from-blue-700 hover:to-indigo-700 text-white font-semibold py-4 rounded-3xl shadow-lg transition-all active:scale-95 flex items-center justify-center gap-2">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M14 5l7 7-7 7" />
|
||
</svg>
|
||
开始转换
|
||
</button>
|
||
</div>
|
||
|
||
<!-- 转换后 -->
|
||
<div class="lg:col-span-5 bg-gray-50 rounded-3xl p-6">
|
||
<div class="flex items-center justify-between mb-4">
|
||
<h3 class="font-semibold text-lg text-gray-800 flex items-center gap-2">
|
||
<span class="w-3 h-3 bg-purple-500 rounded-full"></span>
|
||
转换结果
|
||
</h3>
|
||
<span id="converted-info" class="text-xs text-gray-500 font-mono"></span>
|
||
</div>
|
||
<div class="aspect-video bg-white rounded-2xl overflow-hidden border border-gray-200 flex items-center justify-center relative">
|
||
<img id="converted-img" class="max-h-full max-w-full object-contain" alt="转换后预览">
|
||
<div id="converted-placeholder"
|
||
class="absolute inset-0 flex flex-col items-center justify-center text-gray-400">
|
||
<div class="w-16 h-16 border-4 border-gray-200 border-t-blue-500 rounded-full animate-spin mb-4"></div>
|
||
<p class="text-sm">转换后图片将显示在这里</p>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 下载栏 -->
|
||
<div id="download-bar" class="hidden border-t bg-gray-50 px-8 py-5 flex items-center justify-between">
|
||
<div class="text-sm text-gray-600">
|
||
文件已转换完成 • <span id="file-size" class="font-mono"></span>
|
||
</div>
|
||
<button onclick="downloadImage()"
|
||
class="bg-emerald-600 hover:bg-emerald-700 text-white px-8 py-3 rounded-2xl font-semibold flex items-center gap-3 transition-all active:scale-95">
|
||
<svg xmlns="http://www.w3.org/2000/svg" class="w-5 h-5" fill="none" viewBox="0 0 24 24" stroke="currentColor">
|
||
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M4 16v-4m0 0l4 4m-4-4l4-4m12 4v4m0 0l-4-4m4 4l-4 4" />
|
||
</svg>
|
||
下载转换后的图片
|
||
</button>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="text-center mt-8 text-xs text-gray-400">
|
||
完全本地运行 • 无需联网 • 图片不会离开您的浏览器
|
||
</div>
|
||
</div>
|
||
|
||
<canvas id="canvas" class="hidden"></canvas>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let originalFile = null;
|
||
let originalDataURL = null;
|
||
let convertedDataURL = null;
|
||
let outputFormat = 'image/jpeg';
|
||
let currentQuality = 0.92;
|
||
let canvas = document.getElementById('canvas');
|
||
let ctx = canvas.getContext('2d', { willReadFrequently: true });
|
||
|
||
// Tailwind 脚本已加载,无需额外初始化
|
||
|
||
// 拖拽与点击上传
|
||
const dropZone = document.getElementById('drop-zone');
|
||
const fileInput = document.getElementById('file-input');
|
||
|
||
dropZone.addEventListener('click', () => fileInput.click());
|
||
|
||
fileInput.addEventListener('change', (e) => {
|
||
if (e.target.files.length > 0) handleFile(e.target.files[0]);
|
||
});
|
||
|
||
// 拖拽事件
|
||
['dragover', 'dragenter'].forEach(evt => {
|
||
dropZone.addEventListener(evt, (e) => {
|
||
e.preventDefault();
|
||
dropZone.classList.add('dragover');
|
||
});
|
||
});
|
||
|
||
['dragleave', 'dragend'].forEach(evt => {
|
||
dropZone.addEventListener(evt, () => dropZone.classList.remove('dragover'));
|
||
});
|
||
|
||
dropZone.addEventListener('drop', (e) => {
|
||
e.preventDefault();
|
||
dropZone.classList.remove('dragover');
|
||
if (e.dataTransfer.files.length > 0) {
|
||
handleFile(e.dataTransfer.files[0]);
|
||
}
|
||
});
|
||
|
||
function handleFile(file) {
|
||
if (!file.type.startsWith('image/') || !['image/png', 'image/jpeg'].includes(file.type)) {
|
||
alert('❌ 请上传 PNG 或 JPG 格式的图片!');
|
||
return;
|
||
}
|
||
if (file.size > 50 * 1024 * 1024) {
|
||
alert('❌ 文件过大!请上传小于 50MB 的图片。');
|
||
return;
|
||
}
|
||
|
||
originalFile = file;
|
||
const reader = new FileReader();
|
||
reader.onload = function(e) {
|
||
originalDataURL = e.target.result;
|
||
document.getElementById('original-img').src = originalDataURL;
|
||
|
||
// 显示信息
|
||
const img = new Image();
|
||
img.onload = () => {
|
||
const sizeKB = (file.size / 1024).toFixed(1);
|
||
document.getElementById('original-info').textContent =
|
||
`${img.width}×${img.height} • ${sizeKB} KB`;
|
||
};
|
||
img.src = originalDataURL;
|
||
|
||
// 显示主界面
|
||
document.getElementById('main-content').classList.remove('hidden');
|
||
document.getElementById('download-bar').classList.add('hidden');
|
||
document.getElementById('converted-img').src = '';
|
||
document.getElementById('converted-placeholder').style.display = 'flex';
|
||
};
|
||
reader.readAsDataURL(file);
|
||
}
|
||
|
||
// 格式切换
|
||
const btnJPG = document.getElementById('btn-jpg');
|
||
const btnPNG = document.getElementById('btn-png');
|
||
const jpgOptions = document.getElementById('jpg-options');
|
||
|
||
btnJPG.addEventListener('click', () => {
|
||
outputFormat = 'image/jpeg';
|
||
btnJPG.dataset.active = 'true';
|
||
btnPNG.dataset.active = 'false';
|
||
jpgOptions.classList.remove('hidden');
|
||
});
|
||
|
||
btnPNG.addEventListener('click', () => {
|
||
outputFormat = 'image/png';
|
||
btnPNG.dataset.active = 'true';
|
||
btnJPG.dataset.active = 'false';
|
||
jpgOptions.classList.add('hidden');
|
||
});
|
||
|
||
// 默认选择 JPG
|
||
btnJPG.click();
|
||
|
||
// 质量滑块
|
||
const qualitySlider = document.getElementById('quality-slider');
|
||
const qualityValue = document.getElementById('quality-value');
|
||
qualitySlider.addEventListener('input', () => {
|
||
currentQuality = qualitySlider.value / 100;
|
||
qualityValue.textContent = qualitySlider.value + '%';
|
||
});
|
||
|
||
// 背景颜色
|
||
const bgColorInput = document.getElementById('bg-color');
|
||
const bgColorHex = document.getElementById('bg-color-hex');
|
||
bgColorInput.addEventListener('input', () => {
|
||
bgColorHex.textContent = bgColorInput.value.toUpperCase();
|
||
});
|
||
|
||
// 转换函数
|
||
window.convertImage = function() {
|
||
if (!originalDataURL) {
|
||
alert('请先上传图片!');
|
||
return;
|
||
}
|
||
|
||
const loadingPlaceholder = document.getElementById('converted-placeholder');
|
||
loadingPlaceholder.style.display = 'none';
|
||
|
||
const img = new Image();
|
||
img.onload = function() {
|
||
canvas.width = img.width;
|
||
canvas.height = img.height;
|
||
|
||
// 清空画布
|
||
ctx.clearRect(0, 0, canvas.width, canvas.height);
|
||
|
||
if (outputFormat === 'image/jpeg') {
|
||
// PNG→JPG 时填充背景色
|
||
ctx.fillStyle = bgColorInput.value;
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
}
|
||
|
||
// 绘制图片
|
||
ctx.drawImage(img, 0, 0);
|
||
|
||
// 生成新 DataURL
|
||
convertedDataURL = canvas.toDataURL(outputFormat, currentQuality);
|
||
|
||
// 显示转换结果
|
||
document.getElementById('converted-img').src = convertedDataURL;
|
||
|
||
// 计算新大小
|
||
const newSize = Math.round((convertedDataURL.length * 3 / 4) / 1024);
|
||
document.getElementById('converted-info').textContent =
|
||
`${img.width}×${img.height} • ${newSize} KB`;
|
||
|
||
// 显示下载栏
|
||
document.getElementById('download-bar').classList.remove('hidden');
|
||
document.getElementById('file-size').textContent = `${newSize} KB`;
|
||
};
|
||
|
||
img.src = originalDataURL;
|
||
};
|
||
|
||
// 下载函数
|
||
window.downloadImage = function() {
|
||
if (!convertedDataURL) return;
|
||
|
||
const link = document.createElement('a');
|
||
link.href = convertedDataURL;
|
||
|
||
// 生成新文件名
|
||
let originalName = originalFile.name;
|
||
const ext = outputFormat === 'image/jpeg' ? '.jpg' : '.png';
|
||
const baseName = originalName.substring(0, originalName.lastIndexOf('.')) || originalName;
|
||
link.download = baseName + ext;
|
||
|
||
document.body.appendChild(link);
|
||
link.click();
|
||
document.body.removeChild(link);
|
||
};
|
||
|
||
// 键盘快捷键
|
||
document.addEventListener('keydown', (e) => {
|
||
if (e.key === '/' && document.getElementById('main-content').classList.contains('hidden')) {
|
||
fileInput.click();
|
||
}
|
||
});
|
||
</script>
|
||
</body>
|
||
</html>
|