873 lines
26 KiB
PHP
873 lines
26 KiB
PHP
|
|
<!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>
|