<!DOCTYPE html>
<html lang="zh-CN">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0, maximum-scale=1.0, user-scalable=no">图片批量翻转工具 · 水平垂直</title>
<style>
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: system-ui, -apple-system, 'Segoe UI', Roboto, 'Helvetica Neue', sans-serif;
background: #f5f7fb;
min-height: 100vh;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
padding: 16px;
}
.card {
max-width: 820px;
width: 100%;
background: #ffffff;
border-radius: 32px;
box-shadow: 0 20px 40px rgba(0, 10, 30, 0.1);
padding: 28px 24px 32px;
transition: all 0.2s ease;
}
h1 {
font-size: 28px;
font-weight: 600;
letter-spacing: -0.3px;
color: #0b1a33;
display: flex;
align-items: center;
gap: 10px;
margin-bottom: 8px;
}
h1 small {
font-size: 18px;
font-weight: 400;
color: #64748b;
margin-left: 4px;
}
.subtitle {
color: #475569;
font-size: 16px;
border-left: 4px solid #3b82f6;
padding-left: 14px;
margin-bottom: 28px;
line-height: 1.4;
background: #f8fafc;
border-radius: 0 12px 12px 0;
}
.control-bar {
display: flex;
flex-wrap: wrap;
align-items: center;
justify-content: space-between;
gap: 16px 12px;
background: #f1f5f9;
padding: 16px 18px;
border-radius: 40px;
margin-bottom: 24px;
}
.flip-options {
display: flex;
flex-wrap: wrap;
gap: 10px 16px;
align-items: center;
}
.flip-options label {
display: flex;
align-items: center;
gap: 8px;
font-weight: 500;
color: #1e293b;
background: white;
padding: 6px 16px 6px 12px;
border-radius: 40px;
box-shadow: 0 2px 6px rgba(0,0,0,0.02);
cursor: pointer;
transition: 0.15s;
border: 1px solid #d1d9e6;
}
.flip-options label:hover {
border-color: #94a3b8;
background: #fcfdff;
}
.flip-options input[type="checkbox"] {
width: 18px;
height: 18px;
accent-color: #2563eb;
border-radius: 4px;
cursor: pointer;
}
.action-buttons {
display: flex;
flex-wrap: wrap;
gap: 10px;
align-items: center;
}
.btn {
background: white;
border: 1px solid #cdd9ed;
color: #1e293b;
padding: 8px 20px;
border-radius: 40px;
font-weight: 500;
font-size: 14px;
display: inline-flex;
align-items: center;
gap: 6px;
cursor: pointer;
transition: 0.15s;
box-shadow: 0 2px 4px rgba(0,0,0,0.02);
letter-spacing: 0.3px;
}
.btn-primary {
background: #1e3a8a;
border-color: #1e3a8a;
color: white;
box-shadow: 0 4px 8px rgba(30, 58, 138, 0.2);
}
.btn-primary:hover {
background: #172554;
border-color: #172554;
transform: scale(0.97);
}
.btn-outline {
background: transparent;
border: 1px solid #b9c7dd;
}
.btn-outline:hover {
background: #eef2f7;
border-color: #94a3b8;
}
.btn:active {
transform: scale(0.96);
}
.btn svg {
width: 18px;
height: 18px;
fill: none;
stroke: currentColor;
stroke-width: 2.2;
stroke-linecap: round;
stroke-linejoin: round;
}
.upload-area {
border: 2px dashed #cbd5e1;
border-radius: 28px;
padding: 32px 12px;
text-align: center;
background: #fafcfd;
transition: 0.2s;
cursor: pointer;
margin-bottom: 20px;
}
.upload-area:hover {
border-color: #3b82f6;
background: #f0f7ff;
}
.upload-icon {
font-size: 48px;
line-height: 1;
margin-bottom: 6px;
opacity: 0.7;
}
.upload-text {
font-weight: 500;
color: #1e293b;
}
.upload-hint {
font-size: 14px;
color: #64748b;
margin-top: 6px;
}
#fileInput {
display: none;
}
.preview-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(110px, 1fr));
gap: 16px;
margin-top: 24px;
margin-bottom: 8px;
}
.preview-item {
background: #f8fafc;
border-radius: 20px;
padding: 12px 8px 10px;
box-shadow: 0 4px 12px rgba(0,0,0,0.04);
display: flex;
flex-direction: column;
align-items: center;
transition: 0.1s;
border: 1px solid #e9edf2;
}
.preview-item canvas {
width: 100%;
aspect-ratio: 1 / 1;
object-fit: contain;
border-radius: 12px;
background: #ffffff;
display: block;
box-shadow: 0 2px 6px rgba(0,0,0,0.03);
}
.preview-name {
font-size: 13px;
font-weight: 500;
margin-top: 8px;
color: #1e293b;
text-overflow: ellipsis;
overflow: hidden;
white-space: nowrap;
max-width: 100%;
padding: 0 4px;
}
.preview-size {
font-size: 11px;
color: #64748b;
}
.empty-state {
color: #64748b;
text-align: center;
padding: 40px 10px;
font-size: 15px;
}
.footer-actions {
display: flex;
flex-wrap: wrap;
justify-content: flex-end;
gap: 12px;
margin-top: 28px;
border-top: 1px solid #e2e8f0;
padding-top: 22px;
}
.badge {
background: #eef2ff;
color: #1e3a8a;
padding: 2px 14px;
border-radius: 30px;
font-size: 14px;
font-weight: 500;
}
.btn-success {
background: #0f973d;
border-color: #0f973d;
color: white;
box-shadow: 0 4px 10px rgba(15, 151, 61, 0.25);
}
.btn-success:hover {
background: #0c7c32;
border-color: #0c7c32;
}
@media (max-width: 500px) {
.card { padding: 20px 14px; }
.control-bar { flex-direction: column; align-items: stretch; }
.flip-options { justify-content: center; }
.action-buttons { justify-content: center; }
h1 { font-size: 24px; }
}
</style>
</head>
<body>
<div class="card">
<h1>🔄 图片翻转 <small>批量 · 水平垂直</small></h1>
<div class="subtitle">同时翻转多张图片,支持水平(H)、垂直(V)或组合翻转,预览即时生成,一键批量下载。</div>
<!-- 控制栏 -->
<div class="control-bar">
<div class="flip-options">
<label>
<input type="checkbox" id="flipH" checked> 水平翻转
</label>
<label>
<input type="checkbox" id="flipV"> 垂直翻转
</label>
<span class="badge" id="modeLabel">水平</span>
</div>
<div class="action-buttons">
<button class="btn btn-outline" id="clearBtn" title="清空所有图片">
<svg viewBox="0 0 24 24"><path d="M3 6h18M8 6V4a2 2 0 012-2h4a2 2 0 012 2v2M19 6v14a2 2 0 01-2 2H7a2 2 0 01-2-2V6"/></svg>
清空
</button>
</div>
</div>
<!-- 上传区域 -->
<div class="upload-area" id="dropZone">
<div class="upload-icon">📸</div>
<div class="upload-text">点击选择图片 或 拖放图片到这里</div>
<div class="upload-hint">支持 JPG / PNG / WebP / BMP · 可多选</div>
</div>
<input type="file" id="fileInput" accept="image/*" multiple>
<!-- 预览网格 -->
<div id="previewContainer" class="preview-grid"></div>
<!-- 底部批量下载 -->
<div class="footer-actions" id="footerActions" style="display: none;">
<span id="fileCount" class="badge">0 张图片</span>
<button class="btn btn-success" id="downloadAllBtn">
⬇️ 批量下载翻转后图片
</button>
</div>
</div>
<script>
(function() {
// DOM 元素
const fileInput = document.getElementById('fileInput');
const dropZone = document.getElementById('dropZone');
const previewContainer = document.getElementById('previewContainer');
const clearBtn = document.getElementById('clearBtn');
const downloadAllBtn = document.getElementById('downloadAllBtn');
const footerActions = document.getElementById('footerActions');
const fileCount = document.getElementById('fileCount');
const flipHCheck = document.getElementById('flipH');
const flipVCheck = document.getElementById('flipV');
const modeLabel = document.getElementById('modeLabel');
// 状态存储:所有图片的原始数据 (File 对象 + 预览 URL)
let imageFiles = []; // 存储 File 对象
let imagePreviewURLs = []; // 存储 createObjectURL 生成的 url (用于预览)
let imageDataMapping = []; // 同时存储原始文件名和类型
// 更新模式标签
function updateModeLabel() {
const h = flipHCheck.checked;
const v = flipVCheck.checked;
if (h && v) modeLabel.textContent = '水平+垂直';
else if (h) modeLabel.textContent = '水平';
else if (v) modeLabel.textContent = '垂直';
else modeLabel.textContent = '无翻转 (原图)';
// 重新渲染所有预览 (翻转参数改变)
renderAllPreviews();
}
// 监听翻转复选框
flipHCheck.addEventListener('change', updateModeLabel);
flipVCheck.addEventListener('change', updateModeLabel);
// ----- 渲染预览 (根据当前翻转设置) -----
function renderAllPreviews() {
if (!previewContainer) return;
// 清空预览区
previewContainer.innerHTML = '';
if (imageFiles.length === 0) {
footerActions.style.display = 'none';
fileCount.textContent = '0 张图片';
// 显示空状态
const emptyDiv = document.createElement('div');
emptyDiv.className = 'empty-state';
emptyDiv.style.gridColumn = '1 / -1';
emptyDiv.textContent = '✨ 拖入或选择图片开始翻转';
previewContainer.appendChild(emptyDiv);
return;
}
// 显示底部操作
footerActions.style.display = 'flex';
fileCount.textContent = `${imageFiles.length} 张图片`;
const hFlip = flipHCheck.checked;
const vFlip = flipVCheck.checked;
// 遍历所有图片生成 canvas 预览
imageFiles.forEach((file, index) => {
const img = new Image();
const objectURL = imagePreviewURLs[index];
if (!objectURL) return;
img.onload = function() {
// 确保 index 依然有效 (如果图片被清空则忽略)
if (index >= imageFiles.length) return;
// 创建 canvas
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
// 应用翻转
ctx.save();
// 水平翻转: scale(-1, 1) 且 translate(-width, 0)
// 垂直翻转: scale(1, -1) 且 translate(0, -height)
let scaleX = 1, scaleY = 1;
let transX = 0, transY = 0;
if (hFlip) { scaleX = -1; transX = -canvas.width; }
if (vFlip) { scaleY = -1; transY = -canvas.height; }
ctx.translate(transX, transY);
ctx.scale(scaleX, scaleY);
ctx.drawImage(img, 0, 0);
ctx.restore();
// 创建预览容器
const itemDiv = document.createElement('div');
itemDiv.className = 'preview-item';
// 插入 canvas
const previewCanvas = canvas.cloneNode(true);
itemDiv.appendChild(previewCanvas);
// 文件名 + 尺寸
const nameSpan = document.createElement('div');
nameSpan.className = 'preview-name';
const baseName = file.name.length > 16 ? file.name.slice(0, 12) + '…' : file.name;
nameSpan.textContent = baseName;
itemDiv.appendChild(nameSpan);
const sizeSpan = document.createElement('div');
sizeSpan.className = 'preview-size';
sizeSpan.textContent = `${canvas.width} × ${canvas.height}`;
itemDiv.appendChild(sizeSpan);
// 如果预览项数量对得上,才追加 (防止异步乱序)
// 但多个图片加载完成顺序可能不一致,我们用 dataset 标记 index 来插入正确位置?
// 简便方式:全部追加到最后,但顺序可能乱,更优:根据 index 插入到对应位置
// 但我们保存所有item的顺序为files顺序,采用插入到指定位置方式
const existingItems = previewContainer.children;
let inserted = false;
for (let i = 0; i < existingItems.length; i++) {
const item = existingItems[i];
const idx = parseInt(item.dataset.index);
if (isNaN(idx) || idx > index) {
previewContainer.insertBefore(itemDiv, item);
inserted = true;
break;
}
}
if (!inserted) {
previewContainer.appendChild(itemDiv);
}
itemDiv.dataset.index = index;
};
img.onerror = function() {
// 图片加载失败占位
const itemDiv = document.createElement('div');
itemDiv.className = 'preview-item';
itemDiv.style.justifyContent = 'center';
itemDiv.style.padding = '20px 0';
itemDiv.textContent = '⚠️ 加载失败';
previewContainer.appendChild(itemDiv);
};
// 重新设置 src 触发加载 (使用缓存的 URL)
img.src = objectURL;
});
}
// ----- 添加文件 -----
function addFiles(files) {
if (!files || files.length === 0) return;
// 过滤仅图片
const validFiles = Array.from(files).filter(file => file.type.startsWith('image/'));
if (validFiles.length === 0) {
alert('请选择图片文件 (JPG, PNG, WebP, BMP 等)');
return;
}
// 存储文件以及生成预览URL
validFiles.forEach(file => {
imageFiles.push(file);
const url = URL.createObjectURL(file);
imagePreviewURLs.push(url);
imageDataMapping.push({ name: file.name, type: file.type });
});
// 重新渲染所有预览 (异步加载)
renderAllPreviews();
}
// ----- 清空所有图片 -----
function clearAll() {
// 释放所有 objectURL
imagePreviewURLs.forEach(url => {
if (url) URL.revokeObjectURL(url);
});
imageFiles = [];
imagePreviewURLs = [];
imageDataMapping = [];
previewContainer.innerHTML = '';
footerActions.style.display = 'none';
fileCount.textContent = '0 张图片';
// 显示空状态
const emptyDiv = document.createElement('div');
emptyDiv.className = 'empty-state';
emptyDiv.style.gridColumn = '1 / -1';
emptyDiv.textContent = '✨ 拖入或选择图片开始翻转';
previewContainer.appendChild(emptyDiv);
}
// ----- 批量下载 (每个文件单独下载) -----
function downloadAll() {
if (imageFiles.length === 0) {
alert('没有图片可下载');
return;
}
const hFlip = flipHCheck.checked;
const vFlip = flipVCheck.checked;
// 用 Promise 处理所有图片的异步导出
const downloadPromises = imageFiles.map((file, index) => {
return new Promise((resolve, reject) => {
const img = new Image();
const url = imagePreviewURLs[index];
if (!url) return reject('No URL');
img.onload = function() {
const canvas = document.createElement('canvas');
const ctx = canvas.getContext('2d');
canvas.width = img.naturalWidth || img.width;
canvas.height = img.naturalHeight || img.height;
ctx.save();
let scaleX = 1, scaleY = 1;
let transX = 0, transY = 0;
if (hFlip) { scaleX = -1; transX = -canvas.width; }
if (vFlip) { scaleY = -1; transY = -canvas.height; }
ctx.translate(transX, transY);
ctx.scale(scaleX, scaleY);
ctx.drawImage(img, 0, 0);
ctx.restore();
// 转为 Blob (尽量保持原格式)
const mimeType = file.type || 'image/png';
// 如果浏览器不支持原始格式,降级为 png
const outputType = (mimeType === 'image/webp' || mimeType === 'image/bmp' || mimeType === 'image/jpeg' || mimeType === 'image/png') ? mimeType : 'image/png';
canvas.toBlob((blob) => {
if (blob) {
// 构建新文件名: 原名称_翻转后缀
const dotIndex = file.name.lastIndexOf('.');
const baseName = dotIndex > 0 ? file.name.slice(0, dotIndex) : file.name;
const ext = dotIndex > 0 ? file.name.slice(dotIndex) : '.png';
const flipTag = [];
if (hFlip) flipTag.push('H');
if (vFlip) flipTag.push('V');
const suffix = flipTag.length ? '_' + flipTag.join('') : '_origin';
const newFileName = `${baseName}${suffix}${ext}`;
resolve({ blob, fileName: newFileName });
} else {
reject('Blob conversion failed');
}
}, outputType, 0.92);
};
img.onerror = () => reject('Image load error');
img.src = url;
});
});
// 逐个下载 (触发浏览器下载)
Promise.allSettled(downloadPromises).then(results => {
let successCount = 0;
results.forEach((result, idx) => {
if (result.status === 'fulfilled' && result.value) {
const { blob, fileName } = result.value;
const link = document.createElement('a');
link.href = URL.createObjectURL(blob);
link.download = fileName;
document.body.appendChild(link);
link.click();
document.body.removeChild(link);
// 释放临时 blob URL
setTimeout(() => URL.revokeObjectURL(link.href), 100);
successCount++;
} else {
console.warn(`下载失败: 索引${idx}`, result.reason);
}
});
if (successCount === 0) {
alert('下载失败,请重试');
} else {
// 可提示
}
});
}
// ----- 事件绑定 -----
// 点击上传区域触发 file input
dropZone.addEventListener('click', () => {
fileInput.click();
});
fileInput.addEventListener('change', (e) => {
addFiles(e.target.files);
fileInput.value = ''; // 允许重复选择相同文件
});
// 拖拽上传
dropZone.addEventListener('dragover', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#2563eb';
dropZone.style.background = '#eef2ff';
});
dropZone.addEventListener('dragleave', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#cbd5e1';
dropZone.style.background = '#fafcfd';
});
dropZone.addEventListener('drop', (e) => {
e.preventDefault();
dropZone.style.borderColor = '#cbd5e1';
dropZone.style.background = '#fafcfd';
if (e.dataTransfer.files.length > 0) {
addFiles(e.dataTransfer.files);
}
});
// 清空
clearBtn.addEventListener('click', clearAll);
// 批量下载
downloadAllBtn.addEventListener('click', downloadAll);
// 初始化空状态
clearAll();
// 额外:当窗口尺寸变化时,不额外操作,grid自动适应
// 但如果图片数量很多,渲染异步可能重复,但我们做了index保护
// 如果多次切换翻转,需要重新渲染;但 renderAllPreviews 会先清空再全部重建,可能会闪烁。
// 但为简单起见,直接清空重建。
// 优化:防止快速切换时多次重建,加防抖?但影响不大。
// 采用防抖简单处理
let debounceTimer;
function debounceRender() {
clearTimeout(debounceTimer);
debounceTimer = setTimeout(() => {
renderAllPreviews();
}, 80);
}
// 覆写 updateModeLabel 的渲染为防抖
const originalRender = updateModeLabel;
updateModeLabel = function() {
const h = flipHCheck.checked;
const v = flipVCheck.checked;
if (h && v) modeLabel.textContent = '水平+垂直';
else if (h) modeLabel.textContent = '水平';
else if (v) modeLabel.textContent = '垂直';
else modeLabel.textContent = '无翻转 (原图)';
debounceRender();
};
// 重新绑定事件(移除之前的监听,重新添加新函数)
flipHCheck.removeEventListener('change', originalRender);
flipVCheck.removeEventListener('change', originalRender);
flipHCheck.addEventListener('change', updateModeLabel);
flipVCheck.addEventListener('change', updateModeLabel);
// 首次调用更新
updateModeLabel();
})();
</script>
</body>
</html>
工具功能与操作流程
您可以通过简单的勾选和拖拽,快速完成图片的批量翻转与下载,其核心操作流程如下:

- 选择翻转模式:通过勾选“水平翻转”和/或“垂直翻转”复选框,您可以自由组合翻转效果,当前模式会实时显示在标签中。
- 导入图片:您可以通过点击上传区域或直接拖拽图片文件到该区域来导入图片,工具支持 JPG、PNG 等常见格式,并允许一次选择多张图片。
- 预览与下载:所有图片导入后,会立即根据当前翻转设置生成预览,确认效果后,点击底部的“批量下载翻转后图片”按钮,即可将每张图片按所选翻转模式处理并逐一保存到本地。
标签: 水平垂直翻转
版权声明:NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
NBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
CBA直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
足球直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播
英超直播