|
|
@{ |
|
|
ViewData["Title"] = "图片压缩工具"; |
|
|
Layout = "_Layout"; |
|
|
} |
|
|
|
|
|
<div class="container" style="padding-top: 6rem; padding-bottom: 4rem;"> |
|
|
|
|
|
<div class="text-center mb-8"> |
|
|
<div class="brand-icon" style="width: 4rem; height: 4rem; margin: 0 auto 1.5rem; font-size: 1.8rem;"> |
|
|
<i class="fas fa-compress-alt"></i> |
|
|
</div> |
|
|
<h1 class="hero-title">图片压缩工具</h1> |
|
|
<p class="hero-description">快速压缩图片文件,支持JPG、PNG、WebP等格式<br> |
|
|
保持高质量的同时大幅减小文件体积,提升网站加载速度</p> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card" style="max-width: 800px; margin: 0 auto;"> |
|
|
<div class="card-body" style="padding: 2rem;"> |
|
|
|
|
|
<div id="uploadArea" class="upload-area"> |
|
|
<div class="upload-content"> |
|
|
<i class="fas fa-cloud-upload-alt" style="font-size: 3rem; color: var(--primary); margin-bottom: 1rem;"></i> |
|
|
<h3>拖拽图片到此处或点击选择</h3> |
|
|
<p style="color: var(--dark-2); margin-bottom: 1.5rem;">支持 JPG、PNG、WebP、GIF 格式,单文件最大 10MB</p> |
|
|
<button type="button" class="btn btn-primary" onclick="selectFiles()"> |
|
|
<i class="fas fa-upload"></i> |
|
|
选择图片 |
|
|
</button> |
|
|
</div> |
|
|
<input type="file" id="fileInput" multiple accept="image/*" style="display: none;" onchange="handleFiles(this.files)"> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="settingsPanel" class="settings-panel" style="display: none;"> |
|
|
<h4 style="margin-bottom: 1rem;">压缩设置</h4> |
|
|
<div style="display: grid; grid-template-columns: 1fr 1fr; gap: 1.5rem; margin-bottom: 1.5rem;"> |
|
|
<div> |
|
|
<label>压缩质量</label> |
|
|
<div style="display: flex; align-items: center; gap: 1rem;"> |
|
|
<input type="range" id="qualitySlider" min="0.1" max="1" step="0.1" value="0.8" |
|
|
style="flex: 1;" oninput="updateQualityDisplay(this.value)"> |
|
|
<span id="qualityDisplay" style="font-weight: 600; min-width: 50px;">80%</span> |
|
|
</div> |
|
|
</div> |
|
|
<div> |
|
|
<label>输出格式</label> |
|
|
<select id="formatSelect" style="width: 100%; padding: 0.75rem; border: 1px solid var(--light-2); border-radius: var(--border-radius);"> |
|
|
<option value="original">保持原格式</option> |
|
|
<option value="jpeg">JPEG</option> |
|
|
<option value="png">PNG</option> |
|
|
<option value="webp">WebP</option> |
|
|
</select> |
|
|
</div> |
|
|
</div> |
|
|
<button type="button" class="btn btn-primary" onclick="compressAllImages()" id="compressBtn"> |
|
|
<i class="fas fa-compress"></i> |
|
|
开始压缩 |
|
|
</button> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="fileList" class="file-list" style="display: none;"> |
|
|
<h4 style="margin-bottom: 1rem;">图片列表</h4> |
|
|
<div id="imageItems"></div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div id="resultsPanel" class="results-panel" style="display: none;"> |
|
|
<h4 style="margin-bottom: 1rem;">压缩完成</h4> |
|
|
<div id="compressResults"></div> |
|
|
<div style="margin-top: 1.5rem; text-align: center;"> |
|
|
<button type="button" class="btn btn-primary" onclick="downloadAll()"> |
|
|
<i class="fas fa-download"></i> |
|
|
下载全部 |
|
|
</button> |
|
|
<button type="button" class="btn btn-outline" onclick="resetTool()" style="margin-left: 1rem;"> |
|
|
<i class="fas fa-redo"></i> |
|
|
重新开始 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
|
|
|
<div class="card mt-8"> |
|
|
<div class="card-body"> |
|
|
<h3 style="margin-bottom: 1.5rem; text-align: center;">使用说明</h3> |
|
|
<div style="display: grid; grid-template-columns: repeat(auto-fit, minmax(250px, 1fr)); gap: 2rem;"> |
|
|
<div style="text-align: center;"> |
|
|
<div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">1</div> |
|
|
<h4>选择图片</h4> |
|
|
<p style="color: var(--dark-2); font-size: 0.875rem;">支持拖拽上传或点击选择,可同时处理多张图片</p> |
|
|
</div> |
|
|
<div style="text-align: center;"> |
|
|
<div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">2</div> |
|
|
<h4>调整设置</h4> |
|
|
<p style="color: var(--dark-2); font-size: 0.875rem;">设置压缩质量和输出格式,质量越低文件越小</p> |
|
|
</div> |
|
|
<div style="text-align: center;"> |
|
|
<div style="width: 3rem; height: 3rem; background: linear-gradient(135deg, var(--primary), var(--secondary)); border-radius: 50%; display: flex; align-items: center; justify-content: center; margin: 0 auto 1rem; color: white; font-size: 1.2rem;">3</div> |
|
|
<h4>下载结果</h4> |
|
|
<p style="color: var(--dark-2); font-size: 0.875rem;">压缩完成后可单独下载或批量下载所有图片</p> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
</div> |
|
|
|
|
|
<style> |
|
|
.upload-area { |
|
|
border: 2px dashed var(--light-2); |
|
|
border-radius: var(--border-radius-lg); |
|
|
padding: 3rem 2rem; |
|
|
text-align: center; |
|
|
background: var(--light-1); |
|
|
transition: var(--transition); |
|
|
cursor: pointer; |
|
|
} |
|
|
|
|
|
.upload-area:hover { |
|
|
border-color: var(--primary); |
|
|
background: rgba(22, 93, 255, 0.05); |
|
|
} |
|
|
|
|
|
.upload-area.dragover { |
|
|
border-color: var(--primary); |
|
|
background: rgba(22, 93, 255, 0.1); |
|
|
} |
|
|
|
|
|
.settings-panel, .file-list, .results-panel { |
|
|
margin-top: 2rem; |
|
|
padding-top: 2rem; |
|
|
border-top: 1px solid var(--light-2); |
|
|
} |
|
|
|
|
|
.image-item { |
|
|
display: flex; |
|
|
align-items: center; |
|
|
padding: 1rem; |
|
|
border: 1px solid var(--light-2); |
|
|
border-radius: var(--border-radius); |
|
|
margin-bottom: 1rem; |
|
|
background: white; |
|
|
} |
|
|
|
|
|
.image-preview { |
|
|
width: 60px; |
|
|
height: 60px; |
|
|
object-fit: cover; |
|
|
border-radius: var(--border-radius); |
|
|
margin-right: 1rem; |
|
|
} |
|
|
|
|
|
.image-info { |
|
|
flex: 1; |
|
|
} |
|
|
|
|
|
.image-actions { |
|
|
display: flex; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
|
|
|
.progress-bar { |
|
|
width: 100%; |
|
|
height: 6px; |
|
|
background: var(--light-2); |
|
|
border-radius: 3px; |
|
|
overflow: hidden; |
|
|
margin-top: 0.5rem; |
|
|
} |
|
|
|
|
|
.progress-fill { |
|
|
height: 100%; |
|
|
background: linear-gradient(135deg, var(--primary), var(--secondary)); |
|
|
transition: width 0.3s ease; |
|
|
width: 0%; |
|
|
} |
|
|
|
|
|
.compression-stats { |
|
|
display: grid; |
|
|
grid-template-columns: 1fr 1fr 1fr; |
|
|
gap: 1rem; |
|
|
text-align: center; |
|
|
padding: 1rem; |
|
|
background: var(--light-1); |
|
|
border-radius: var(--border-radius); |
|
|
margin-bottom: 1rem; |
|
|
} |
|
|
|
|
|
.stat-item h5 { |
|
|
margin: 0 0 0.25rem; |
|
|
font-size: 1.2rem; |
|
|
font-weight: 700; |
|
|
color: var(--primary); |
|
|
} |
|
|
|
|
|
.stat-item p { |
|
|
margin: 0; |
|
|
font-size: 0.875rem; |
|
|
color: var(--dark-2); |
|
|
} |
|
|
|
|
|
|
|
|
@@media (max-width: 768px) { |
|
|
.upload-area { |
|
|
padding: 2rem 1rem; |
|
|
} |
|
|
|
|
|
.compression-stats { |
|
|
grid-template-columns: 1fr; |
|
|
gap: 0.5rem; |
|
|
} |
|
|
} |
|
|
</style> |
|
|
|
|
|
<script> |
|
|
let selectedFiles = []; |
|
|
let compressedFiles = []; |
|
|
|
|
|
|
|
|
function selectFiles() { |
|
|
document.getElementById('fileInput').click(); |
|
|
} |
|
|
|
|
|
|
|
|
function handleFiles(files) { |
|
|
if (files.length === 0) return; |
|
|
|
|
|
selectedFiles = Array.from(files).filter(file => { |
|
|
return file.type.startsWith('image/') && file.size <= 10 * 1024 * 1024; // 10MB限制 |
|
|
}); |
|
|
|
|
|
if (selectedFiles.length === 0) { |
|
|
toolHub.showToast('请选择有效的图片文件(小于10MB)', 'error'); |
|
|
return; |
|
|
} |
|
|
|
|
|
displaySelectedFiles(); |
|
|
document.getElementById('settingsPanel').style.display = 'block'; |
|
|
document.getElementById('fileList').style.display = 'block'; |
|
|
} |
|
|
|
|
|
|
|
|
function displaySelectedFiles() { |
|
|
const container = document.getElementById('imageItems'); |
|
|
container.innerHTML = ''; |
|
|
|
|
|
selectedFiles.forEach((file, index) => { |
|
|
const item = document.createElement('div'); |
|
|
item.className = 'image-item'; |
|
|
item.innerHTML = ` |
|
|
<img class="image-preview" src="${URL.createObjectURL(file)}" alt="${file.name}"> |
|
|
<div class="image-info"> |
|
|
<div style="font-weight: 600; margin-bottom: 0.25rem;">${file.name}</div> |
|
|
<div style="font-size: 0.875rem; color: var(--dark-2);"> |
|
|
${formatFileSize(file.size)} • ${file.type} |
|
|
</div> |
|
|
<div class="progress-bar" id="progress-${index}" style="display: none;"> |
|
|
<div class="progress-fill" id="progress-fill-${index}"></div> |
|
|
</div> |
|
|
</div> |
|
|
<div class="image-actions"> |
|
|
<button class="btn btn-outline btn-sm" onclick="removeFile(${index})"> |
|
|
<i class="fas fa-times"></i> |
|
|
</button> |
|
|
</div> |
|
|
`; |
|
|
container.appendChild(item); |
|
|
}); |
|
|
} |
|
|
|
|
|
// 移除文件 |
|
|
function removeFile(index) { |
|
|
selectedFiles.splice(index, 1); |
|
|
if (selectedFiles.length === 0) { |
|
|
document.getElementById('settingsPanel').style.display = 'none'; |
|
|
document.getElementById('fileList').style.display = 'none'; |
|
|
} else { |
|
|
displaySelectedFiles(); |
|
|
} |
|
|
} |
|
|
|
|
|
|
|
|
function updateQualityDisplay(value) { |
|
|
document.getElementById('qualityDisplay').textContent = Math.round(value * 100) + '%'; |
|
|
} |
|
|
|
|
|
|
|
|
async function compressAllImages() { |
|
|
const quality = parseFloat(document.getElementById('qualitySlider').value); |
|
|
const format = document.getElementById('formatSelect').value; |
|
|
|
|
|
compressedFiles = []; |
|
|
document.getElementById('compressBtn').disabled = true; |
|
|
document.getElementById('compressBtn').innerHTML = '<i class="fas fa-spinner fa-spin"></i> 压缩中...'; |
|
|
|
|
|
for (let i = 0; i < selectedFiles.length; i++) { |
|
|
const file = selectedFiles[i]; |
|
|
document.getElementById(`progress-${i}`).style.display = 'block'; |
|
|
|
|
|
try { |
|
|
const compressedFile = await compressImage(file, quality, format, (progress) => { |
|
|
document.getElementById(`progress-fill-${i}`).style.width = progress + '%'; |
|
|
}); |
|
|
compressedFiles.push(compressedFile); |
|
|
} catch (error) { |
|
|
console.error('压缩失败:', error); |
|
|
toolHub.showToast(`压缩 ${file.name} 失败`, 'error'); |
|
|
} |
|
|
} |
|
|
|
|
|
showResults(); |
|
|
document.getElementById('compressBtn').disabled = false; |
|
|
document.getElementById('compressBtn').innerHTML = '<i class="fas fa-compress"></i> 开始压缩'; |
|
|
} |
|
|
|
|
|
// 压缩单个图片 |
|
|
function compressImage(file, quality, format, onProgress) { |
|
|
return new Promise((resolve, reject) => { |
|
|
const canvas = document.createElement('canvas'); |
|
|
const ctx = canvas.getContext('2d'); |
|
|
const img = new Image(); |
|
|
|
|
|
img.onload = function() { |
|
|
canvas.width = img.width; |
|
|
canvas.height = img.height; |
|
|
|
|
|
ctx.drawImage(img, 0, 0); |
|
|
|
|
|
const outputFormat = format === 'original' ? file.type : `image/${format}`; |
|
|
|
|
|
canvas.toBlob((blob) => { |
|
|
if (blob) { |
|
|
const compressedFile = new File([blob], |
|
|
getCompressedFileName(file.name, format), |
|
|
{ type: outputFormat } |
|
|
); |
|
|
onProgress(100); |
|
|
resolve({ |
|
|
original: file, |
|
|
compressed: compressedFile, |
|
|
compressionRatio: ((file.size - blob.size) / file.size * 100).toFixed(1) |
|
|
}); |
|
|
} else { |
|
|
reject(new Error('压缩失败')); |
|
|
} |
|
|
}, outputFormat, quality); |
|
|
}; |
|
|
|
|
|
img.onerror = () => reject(new Error('图片加载失败')); |
|
|
img.src = URL.createObjectURL(file); |
|
|
|
|
|
|
|
|
let progress = 0; |
|
|
const progressInterval = setInterval(() => { |
|
|
progress += 10; |
|
|
if (progress >= 90) { |
|
|
clearInterval(progressInterval); |
|
|
} else { |
|
|
onProgress(progress); |
|
|
} |
|
|
}, 50); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function getCompressedFileName(originalName, format) { |
|
|
if (format === 'original') return originalName; |
|
|
|
|
|
const nameWithoutExt = originalName.substring(0, originalName.lastIndexOf('.')); |
|
|
return `${nameWithoutExt}_compressed.${format === 'jpeg' ? 'jpg' : format}`; |
|
|
} |
|
|
|
|
|
// 显示压缩结果 |
|
|
function showResults() { |
|
|
const totalOriginalSize = compressedFiles.reduce((sum, item) => sum + item.original.size, 0); |
|
|
const totalCompressedSize = compressedFiles.reduce((sum, item) => sum + item.compressed.size, 0); |
|
|
const totalSavings = ((totalOriginalSize - totalCompressedSize) / totalOriginalSize * 100).toFixed(1); |
|
|
|
|
|
document.getElementById('compressResults').innerHTML = ` |
|
|
<div class="compression-stats"> |
|
|
<div class="stat-item"> |
|
|
<h5>${formatFileSize(totalOriginalSize)}</h5> |
|
|
<p>原始大小</p> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<h5>${formatFileSize(totalCompressedSize)}</h5> |
|
|
<p>压缩后大小</p> |
|
|
</div> |
|
|
<div class="stat-item"> |
|
|
<h5>${totalSavings}%</h5> |
|
|
<p>压缩率</p> |
|
|
</div> |
|
|
</div> |
|
|
${compressedFiles.map((item, index) => ` |
|
|
<div class="image-item"> |
|
|
<img class="image-preview" src="${URL.createObjectURL(item.compressed)}" alt="${item.compressed.name}"> |
|
|
<div class="image-info"> |
|
|
<div style="font-weight: 600; margin-bottom: 0.25rem;">${item.compressed.name}</div> |
|
|
<div style="font-size: 0.875rem; color: var(--dark-2);"> |
|
|
${formatFileSize(item.original.size)} → ${formatFileSize(item.compressed.size)} (节省 ${item.compressionRatio}%) |
|
|
</div> |
|
|
</div> |
|
|
<div class="image-actions"> |
|
|
<button class="btn btn-primary btn-sm" onclick="downloadFile(${index})"> |
|
|
<i class="fas fa-download"></i> 下载 |
|
|
</button> |
|
|
</div> |
|
|
</div> |
|
|
`).join('')} |
|
|
`; |
|
|
|
|
|
document.getElementById('resultsPanel').style.display = 'block'; |
|
|
} |
|
|
|
|
|
|
|
|
function downloadFile(index) { |
|
|
const item = compressedFiles[index]; |
|
|
const url = URL.createObjectURL(item.compressed); |
|
|
const a = document.createElement('a'); |
|
|
a.href = url; |
|
|
a.download = item.compressed.name; |
|
|
a.click(); |
|
|
URL.revokeObjectURL(url); |
|
|
} |
|
|
|
|
|
|
|
|
function downloadAll() { |
|
|
compressedFiles.forEach((item, index) => { |
|
|
setTimeout(() => downloadFile(index), index * 200); |
|
|
}); |
|
|
} |
|
|
|
|
|
|
|
|
function resetTool() { |
|
|
selectedFiles = []; |
|
|
compressedFiles = []; |
|
|
document.getElementById('fileInput').value = ''; |
|
|
document.getElementById('settingsPanel').style.display = 'none'; |
|
|
document.getElementById('fileList').style.display = 'none'; |
|
|
document.getElementById('resultsPanel').style.display = 'none'; |
|
|
document.getElementById('imageItems').innerHTML = ''; |
|
|
document.getElementById('compressResults').innerHTML = ''; |
|
|
} |
|
|
|
|
|
|
|
|
function formatFileSize(bytes) { |
|
|
if (bytes === 0) return '0 B'; |
|
|
const k = 1024; |
|
|
const sizes = ['B', 'KB', 'MB', 'GB']; |
|
|
const i = Math.floor(Math.log(bytes) / Math.log(k)); |
|
|
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i]; |
|
|
} |
|
|
|
|
|
|
|
|
document.addEventListener('DOMContentLoaded', function() { |
|
|
const uploadArea = document.getElementById('uploadArea'); |
|
|
|
|
|
uploadArea.addEventListener('dragover', function(e) { |
|
|
e.preventDefault(); |
|
|
uploadArea.classList.add('dragover'); |
|
|
}); |
|
|
|
|
|
uploadArea.addEventListener('dragleave', function(e) { |
|
|
e.preventDefault(); |
|
|
uploadArea.classList.remove('dragover'); |
|
|
}); |
|
|
|
|
|
uploadArea.addEventListener('drop', function(e) { |
|
|
e.preventDefault(); |
|
|
uploadArea.classList.remove('dragover'); |
|
|
handleFiles(e.dataTransfer.files); |
|
|
}); |
|
|
|
|
|
uploadArea.addEventListener('click', selectFiles); |
|
|
}); |
|
|
</script> |
|
|
|