Files
i-love/resources/views/compress.blade.php
2025-06-17 10:19:06 +08:00

873 lines
26 KiB
PHP
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!DOCTYPE html>
<html lang="zh">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>图片压缩工具</title>
<link rel="stylesheet" href="https://cdnjs.cloudflare.com/ajax/libs/font-awesome/6.4.0/css/all.min.css">
<style>
:root {
--primary-color: #3498db;
--secondary-color: #2ecc71;
--warning-color: #e74c3c;
--dark-color: #2c3e50;
--light-color: #ecf0f1;
--gray-color: #bdc3c7;
--accent-color: #9b59b6;
--transition: all 0.3s ease;
--shadow: 0 4px 6px rgba(0, 0, 0, 0.1);
--card-radius: 10px;
}
* {
box-sizing: border-box;
margin: 0;
padding: 0;
}
body {
font-family: 'Segoe UI', Tahoma, Geneva, Verdana, sans-serif;
background: linear-gradient(135deg, #1a2980, #26d0ce);
color: #333;
min-height: 100vh;
padding: 20px;
line-height: 1.6;
}
.container {
max-width: 1200px;
margin: 0 auto;
background: rgba(255, 255, 255, 0.95);
border-radius: 20px;
box-shadow: 0 15px 35px rgba(0, 0, 0, 0.2);
overflow: hidden;
}
header {
background: var(--dark-color);
color: white;
padding: 25px 30px;
display: flex;
align-items: center;
justify-content: space-between;
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
}
.logo {
display: flex;
align-items: center;
gap: 15px;
}
.logo i {
font-size: 2.2rem;
color: var(--secondary-color);
}
.logo h1 {
font-weight: 600;
font-size: 1.8rem;
}
.logo span {
color: var(--secondary-color);
}
main {
display: grid;
grid-template-columns: 300px 1fr;
min-height: 80vh;
}
.sidebar {
background: var(--dark-color);
color: var(--light-color);
padding: 25px;
overflow-y: auto;
}
.file-browser {
background: rgba(255, 255, 255, 0.1);
border-radius: var(--card-radius);
padding: 20px;
margin-bottom: 20px;
}
.directory-path {
font-size: 0.9rem;
color: var(--gray-color);
margin-bottom: 10px;
padding: 8px;
background: rgba(0, 0, 0, 0.2);
border-radius: 6px;
word-break: break-all;
}
.file-list {
max-height: 300px;
overflow-y: auto;
border: 1px solid rgba(255, 255, 255, 0.1);
border-radius: var(--card-radius);
margin-bottom: 15px;
}
.file-item {
padding: 10px 15px;
cursor: pointer;
display: flex;
align-items: center;
gap: 10px;
border-bottom: 1px solid rgba(255, 255, 255, 0.05);
transition: var(--transition);
}
.file-item:hover {
background: rgba(44, 62, 80, 0.8);
}
.file-item.selected {
background: rgba(52, 152, 219, 0.7);
}
.file-item i {
color: var(--secondary-color);
}
.file-item span {
flex: 1;
font-size: 0.9rem;
}
.file-size {
color: var(--gray-color);
font-size: 0.8rem;
}
.controls {
margin-top: 25px;
}
.control-group {
margin-bottom: 25px;
}
.control-group h3 {
margin-bottom: 12px;
color: var(--secondary-color);
font-size: 1.1rem;
}
.quality-slider {
width: 100%;
height: 10px;
-webkit-appearance: none;
background: rgba(255, 255, 255, 0.1);
border-radius: 10px;
outline: none;
}
.quality-slider::-webkit-slider-thumb {
-webkit-appearance: none;
width: 22px;
height: 22px;
border-radius: 50%;
background: var(--primary-color);
cursor: pointer;
border: 2px solid white;
box-shadow: 0 2px 5px rgba(0, 0, 0, 0.2);
}
.quality-value {
text-align: center;
font-size: 1.1rem;
font-weight: bold;
margin-top: 5px;
color: white;
}
.btn {
display: inline-flex;
align-items: center;
justify-content: center;
gap: 8px;
padding: 12px 25px;
background: var(--primary-color);
color: white;
border: none;
border-radius: 50px;
cursor: pointer;
font-size: 1rem;
font-weight: 600;
transition: var(--transition);
width: 100%;
margin-top: 10px;
box-shadow: var(--shadow);
}
.btn:hover {
transform: translateY(-2px);
box-shadow: 0 5px 15px rgba(0, 0, 0, 0.2);
}
.btn i {
font-size: 1rem;
}
.btn-compress {
background: var(--secondary-color);
}
.btn-compress-all {
background: var(--accent-color);
}
.content {
padding: 30px;
display: grid;
grid-template-columns: repeat(auto-fit, minmax(450px, 1fr));
gap: 30px;
}
.preview-card {
background: white;
border-radius: var(--card-radius);
box-shadow: var(--shadow);
overflow: hidden;
display: flex;
flex-direction: column;
}
.card-header {
background: var(--dark-color);
color: white;
padding: 15px 20px;
display: flex;
justify-content: space-between;
}
.card-header h2 {
font-size: 1.2rem;
font-weight: 500;
}
.card-body {
padding: 20px;
display: flex;
flex-direction: column;
align-items: center;
justify-content: center;
flex: 1;
min-height: 300px;
}
.preview-container {
width: 100%;
height: 250px;
display: flex;
align-items: center;
justify-content: center;
border: 1px solid var(--gray-color);
border-radius: 8px;
overflow: hidden;
margin-bottom: 20px;
position: relative;
}
.preview-image {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
.file-info {
display: flex;
width: 100%;
justify-content: space-between;
background: rgba(189, 195, 199, 0.2);
padding: 15px;
border-radius: 8px;
font-size: 0.9rem;
}
.info-item {
display: flex;
flex-direction: column;
align-items: center;
}
.info-label {
color: #7f8c8d;
font-size: 0.85rem;
margin-bottom: 5px;
}
.info-value {
font-weight: 600;
font-size: 1.1rem;
}
.before .info-value {
color: var(--dark-color);
}
.after .info-value {
color: var(--secondary-color);
}
.comparison {
display: flex;
width: 100%;
gap: 10px;
}
.savings {
font-weight: bold;
color: var(--secondary-color);
text-align: center;
padding: 15px;
font-size: 1.1rem;
background: rgba(46, 204, 113, 0.1);
border-radius: var(--card-radius);
width: 100%;
}
.no-image {
color: #7f8c8d;
font-size: 1.1rem;
text-align: center;
}
.no-image i {
font-size: 3rem;
margin-bottom: 15px;
display: block;
color: var(--gray-color);
}
.progress-container {
margin-top: 20px;
width: 100%;
}
.progress-header {
display: flex;
justify-content: space-between;
margin-bottom: 10px;
}
.progress-bar {
height: 12px;
background: #e0e0e0;
border-radius: 10px;
overflow: hidden;
}
.progress-fill {
height: 100%;
background: var(--secondary-color);
width: 0%;
transition: width 0.5s ease;
}
.spinner {
width: 50px;
height: 50px;
border: 5px solid rgba(52, 152, 219, 0.2);
border-top-color: var(--primary-color);
border-radius: 50%;
animation: spin 1s linear infinite;
}
@keyframes spin {
to {
transform: rotate(360deg);
}
}
.completed {
display: none;
color: var(--secondary-color);
font-size: 1.3rem;
text-align: center;
}
.completed i {
font-size: 2rem;
margin-bottom: 15px;
}
.overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.7);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
opacity: 0;
visibility: hidden;
transition: var(--transition);
}
.overlay.active {
opacity: 1;
visibility: visible;
}
.overlay-content {
background: white;
border-radius: 15px;
width: 90%;
max-width: 600px;
padding: 30px;
text-align: center;
transform: translateY(-30px);
transition: all 0.5s cubic-bezier(0.175, 0.885, 0.32, 1.275);
}
.overlay.active .overlay-content {
transform: translateY(0);
}
.overlay-title {
font-size: 1.8rem;
margin-bottom: 20px;
color: var(--dark-color);
}
.overlay-text {
font-size: 1.1rem;
margin-bottom: 30px;
line-height: 1.7;
}
.confirm-buttons {
display: flex;
gap: 15px;
justify-content: center;
}
.overlay-btn {
min-width: 120px;
padding: 12px 20px;
border-radius: 50px;
font-size: 1rem;
font-weight: 600;
cursor: pointer;
border: 2px solid transparent;
transition: var(--transition);
}
.btn-confirm {
background: var(--secondary-color);
color: white;
}
.btn-confirm:hover {
background: #27ae60;
}
.btn-cancel {
background: transparent;
color: var(--dark-color);
border-color: var(--gray-color);
}
.btn-cancel:hover {
background: #f5f5f5;
}
@media (max-width: 900px) {
main {
grid-template-columns: 1fr;
}
.sidebar {
padding: 20px 15px;
}
}
@media (max-width: 600px) {
.content {
grid-template-columns: 1fr;
}
header {
flex-direction: column;
text-align: center;
gap: 15px;
}
.logo {
justify-content: center;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<div class="logo">
<i class="fas fa-compress-alt"></i>
<h1>图片<span>压缩</span>工具</h1>
</div>
<div>
<p>批量压缩并替换图片文件</p>
</div>
</header>
<main>
<aside class="sidebar">
<div class="file-browser">
<h3>选择目录</h3>
<div class="directory-path">
storage/app/public/images/compressed
</div>
<div class="file-list">
<!-- File items will be added dynamically -->
</div>
<button class="btn" id="refreshBtn">
<i class="fas fa-sync-alt"></i> 刷新文件列表
</button>
</div>
<div class="controls">
<div class="control-group">
<h3>压缩质量设置</h3>
<input type="range" min="40" max="95" value="75" class="quality-slider" id="qualitySlider">
<div class="quality-value" id="qualityValue">75%</div>
</div>
<div class="control-group">
<h3>操作</h3>
<button class="btn btn-compress" id="compressBtn">
<i class="fas fa-file-image"></i> 压缩当前图片
</button>
<button class="btn btn-compress-all" id="compressAllBtn">
<i class="fas fa-layer-group"></i> 批量压缩目录
</button>
</div>
</div>
</aside>
<div class="content">
<div class="preview-card">
<div class="card-header">
<h2>原始图片</h2>
</div>
<div class="card-body">
<div class="preview-container" id="originalPreview">
<div class="no-image">
<i class="fas fa-image"></i>
<p>请从左侧选择一张图片</p>
</div>
</div>
<div class="file-info before">
<div class="info-item">
<div class="info-label">文件大小</div>
<div class="info-value" id="originalSize">0 KB</div>
</div>
<div class="info-item">
<div class="info-label">分辨率</div>
<div class="info-value" id="originalDims">0×0</div>
</div>
</div>
</div>
</div>
<div class="preview-card">
<div class="card-header">
<h2>压缩后图片</h2>
</div>
<div class="card-body">
<div class="preview-container" id="compressedPreview">
<div class="no-image">
<i class="fas fa-compress-arrows-alt"></i>
<p>压缩后图片将显示在这里</p>
</div>
</div>
<div class="file-info after">
<div class="info-item">
<div class="info-label">文件大小</div>
<div class="info-value" id="compressedSize">0 KB</div>
</div>
<div class="info-item">
<div class="info-label">分辨率</div>
<div class="info-value" id="compressedDims">0×0</div>
</div>
</div>
<div class="savings" id="savingsText">
尺寸减少0%
</div>
</div>
</div>
<!-- Progress card for batch processing -->
<div class="preview-card">
<div class="card-header">
<h2>批量压缩进度</h2>
</div>
<div class="card-body" style="align-items: center;">
<div class="no-image" id="batchPlaceholder">
<i class="fas fa-layer-group"></i>
<p>批量压缩状态将显示在这里</p>
</div>
<div class="progress-container" id="progressContainer" style="display: none;">
<div class="progress-header">
<span>处理中...</span>
<span id="progressText">0/0</span>
</div>
<div class="progress-bar">
<div class="progress-fill" id="progressFill"></div>
</div>
</div>
<div class="completed" id="completedMsg">
<i class="fas fa-check-circle"></i>
<p>压缩完成!所有文件已更新</p>
</div>
</div>
</div>
</div>
</main>
</div>
<!-- Confirmation overlay -->
<div class="overlay" id="confirmOverlay">
<div class="overlay-content">
<h2 class="overlay-title">确认批量操作</h2>
<p class="overlay-text">
您将压缩 <strong id="fileCount">0</strong> 个文件,覆盖其原始文件。<br>
此操作不可逆,请确保您已备份重要文件。
</p>
<div class="confirm-buttons">
<button class="overlay-btn btn-confirm" id="confirmBatch">开始压缩</button>
<button class="overlay-btn btn-cancel" id="cancelBatch">取消</button>
</div>
</div>
</div>
<script>
// Mock data for demonstration (in a real app this would come from server)
const directoryData = [
{ name: 'engagement1.jpg', size: '2.8MB', path: 'engagement1.jpg', type: 'image/jpeg' },
{ name: 'wedding_prep.png', size: '4.2MB', path: 'wedding_prep.png', type: 'image/png' },
{ name: 'ceremony_moment.jpg', size: '3.1MB', path: 'ceremony_moment.jpg', type: 'image/jpeg' },
{ name: 'family_group.jpg', size: '5.4MB', path: 'family_group.jpg', type: 'image/jpeg' },
{ name: 'reception_dance.jpg', size: '3.7MB', path: 'reception_dance.jpg', type: 'image/jpeg' },
{ name: 'bridal_portrait.jpg', size: '4.8MB', path: 'bridal_portrait.jpg', type: 'image/jpeg' },
{ name: 'groom_portrait.png', size: '3.9MB', path: 'groom_portrait.png', type: 'image/png' },
{ name: 'venue_overview.jpg', size: '6.2MB', path: 'venue_overview.jpg', type: 'image/jpeg' },
];
// DOM Elements
const fileList = document.querySelector('.file-list');
const qualitySlider = document.getElementById('qualitySlider');
const qualityValue = document.getElementById('qualityValue');
const refreshBtn = document.getElementById('refreshBtn');
const compressBtn = document.getElementById('compressBtn');
const compressAllBtn = document.getElementById('compressAllBtn');
const originalPreview = document.getElementById('originalPreview');
const compressedPreview = document.getElementById('compressedPreview');
const originalSize = document.getElementById('originalSize');
const compressedSize = document.getElementById('compressedSize');
const originalDims = document.getElementById('originalDims');
const compressedDims = document.getElementById('compressedDims');
const savingsText = document.getElementById('savingsText');
const batchPlaceholder = document.getElementById('batchPlaceholder');
const progressContainer = document.getElementById('progressContainer');
const progressFill = document.getElementById('progressFill');
const progressText = document.getElementById('progressText');
const completedMsg = document.getElementById('completedMsg');
const confirmOverlay = document.getElementById('confirmOverlay');
const fileCount = document.getElementById('fileCount');
const confirmBatch = document.getElementById('confirmBatch');
const cancelBatch = document.getElementById('cancelBatch');
// Current state
let currentFile = null;
let compressedImage = null;
let currentQuality = 75;
// Initialize file browser
function initFileBrowser() {
fileList.innerHTML = '';
directoryData.forEach(file => {
const fileItem = document.createElement('div');
fileItem.className = 'file-item';
fileItem.dataset.path = file.path;
fileItem.dataset.size = file.size;
fileItem.dataset.type = file.type;
fileItem.innerHTML = `
<i class="fas fa-file-image"></i>
<span>${file.name}</span>
<span class="file-size">${file.size}</span>
`;
fileItem.addEventListener('click', () => selectFile(fileItem, file));
fileList.appendChild(fileItem);
});
}
// Select file handler
function selectFile(item, file) {
// Clear previous selection
document.querySelectorAll('.file-item').forEach(el => {
el.classList.remove('selected');
});
// Select current
item.classList.add('selected');
currentFile = file;
// Preview the image
previewImage(file);
}
// Preview image function
function previewImage(file) {
// For demonstration, we'll use placeholder images
const placeholderImage = 'https://picsum.photos/600/400';
// Clear previous previews
originalPreview.innerHTML = '';
compressedPreview.innerHTML = '<div class="no-image"><i class="fas fa-compress-arrows-alt"></i><p>压缩后图片将显示在这里</p></div>';
// Create image element
const img = document.createElement('img');
img.className = 'preview-image';
img.src = placeholderImage;
originalPreview.appendChild(img);
// Simulate file info
const sizeParts = file.size.match(/(\d+\.?\d*)(\w+)/);
if (sizeParts) {
const sizeNum = parseFloat(sizeParts[1]);
originalSize.textContent = file.size;
originalDims.textContent = '1920×1080'; // Fixed for demo
// Simulate compression for demo
setTimeout(() => {
simulateCompression(sizeNum, sizeParts[2]);
}, 1000);
}
}
// Simulate compression (in a real app, real compression would happen)
function simulateCompression(size, unit) {
// Calculate compression for demo
const compressionRatio = 1 - (currentQuality / 100);
const compressedSize = (size * compressionRatio * 0.9).toFixed(1);
const savings = Math.round((1 - (compressedSize / size)) * 100);
// Update compressed panel
compressedSize.textContent = compressedSize + unit;
compressedDims.textContent = '1920×1080';
savingsText.innerHTML = `尺寸减少:<strong>${savings}%</strong>`;
// Show compressed image (same for demo)
compressedPreview.innerHTML = '';
const img = document.createElement('img');
img.className = 'preview-image';
img.src = 'https://picsum.photos/600/400';
compressedPreview.appendChild(img);
}
// Compress current image
function compressImage() {
if (!currentFile) {
alert('请先选择一张图片');
return;
}
// In a real implementation, this would send the image to the server for processing
// Here we just simulate the compression
const sizeParts = currentFile.size.match(/(\d+\.?\d*)(\w+)/);
if (sizeParts) {
const sizeNum = parseFloat(sizeParts[1]);
simulateCompression(sizeNum, sizeParts[2]);
// Show success message
savingsText.innerHTML = `压缩成功!保存为原始文件`;
savingsText.style.backgroundColor = 'rgba(46, 204, 113, 0.15)';
setTimeout(() => {
savingsText.style.backgroundColor = 'rgba(46, 204, 113, 0.1)';
}, 3000);
}
}
// Show batch compression confirmation
function showBatchConfirm() {
fileCount.textContent = directoryData.length;
confirmOverlay.classList.add('active');
}
// Execute batch compression
function executeBatchCompression() {
confirmOverlay.classList.remove('active');
// Hide placeholder and show progress
batchPlaceholder.style.display = 'none';
progressContainer.style.display = 'block';
completedMsg.style.display = 'none';
const totalFiles = directoryData.length;
let processed = 0;
const interval = setInterval(() => {
processed++;
const progress = (processed / totalFiles) * 100;
progressFill.style.width = `${progress}%`;
progressText.textContent = `${processed}/${totalFiles}`;
if (processed >= totalFiles) {
clearInterval(interval);
// Show completion message
setTimeout(() => {
progressContainer.style.display = 'none';
completedMsg.style.display = 'block';
}, 1000);
}
}, 800);
}
// Event listeners
qualitySlider.addEventListener('input', () => {
currentQuality = parseInt(qualitySlider.value);
qualityValue.textContent = currentQuality + '%';
if (currentFile) {
const sizeParts = currentFile.size.match(/(\d+\.?\d*)(\w+)/);
if (sizeParts) {
const sizeNum = parseFloat(sizeParts[1]);
simulateCompression(sizeNum, sizeParts[2]);
}
}
});
refreshBtn.addEventListener('click', initFileBrowser);
compressBtn.addEventListener('click', compressImage);
compressAllBtn.addEventListener('click', showBatchConfirm);
confirmBatch.addEventListener('click', executeBatchCompression);
cancelBatch.addEventListener('click', () => {
confirmOverlay.classList.remove('active');
});
// Initialize the app
initFileBrowser();
</script>
</body>
</html>