1349 lines
38 KiB
HTML
1349 lines
38 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>图片转PDF</title>
|
||
<script src="https://cdnjs.cloudflare.com/ajax/libs/jspdf/2.5.1/jspdf.umd.min.js"></script>
|
||
<style>
|
||
body {
|
||
font-family:
|
||
-apple-system, BlinkMacSystemFont, "Segoe UI", Roboto,
|
||
"Helvetica Neue", Arial, sans-serif;
|
||
margin: 20px;
|
||
line-height: 1.6;
|
||
}
|
||
|
||
.container {
|
||
max-width: 1400px;
|
||
margin: 0 auto;
|
||
padding: 20px;
|
||
background: white;
|
||
border-radius: 12px;
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.main-content {
|
||
display: flex;
|
||
gap: 20px;
|
||
align-items: flex-start;
|
||
}
|
||
|
||
.left-panel {
|
||
flex: 1;
|
||
min-width: 0;
|
||
max-width: 600px;
|
||
}
|
||
|
||
.right-panel {
|
||
flex: 1;
|
||
min-width: 0;
|
||
max-width: 600px;
|
||
position: sticky;
|
||
top: 20px;
|
||
}
|
||
|
||
h1 {
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
font-size: 2rem;
|
||
}
|
||
|
||
.upload-area {
|
||
border: 2px dashed #3498db;
|
||
border-radius: 8px;
|
||
padding: 40px 20px;
|
||
text-align: center;
|
||
margin-bottom: 30px;
|
||
background: #f8f9fa;
|
||
transition: all 0.3s ease;
|
||
cursor: pointer;
|
||
}
|
||
|
||
.upload-area:hover {
|
||
border-color: #2980b9;
|
||
background: #e3f2fd;
|
||
}
|
||
|
||
.upload-area.drag-over {
|
||
border-color: #27ae60;
|
||
background: #d4edda;
|
||
}
|
||
|
||
.upload-icon {
|
||
font-size: 48px;
|
||
color: #3498db;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.upload-text {
|
||
font-size: 16px;
|
||
color: #555;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.upload-hint {
|
||
font-size: 14px;
|
||
color: #888;
|
||
}
|
||
|
||
input[type="file"] {
|
||
display: none;
|
||
}
|
||
|
||
.file-select-btn {
|
||
background-color: #3498db;
|
||
color: white;
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
transition: background-color 0.3s ease;
|
||
margin-top: 15px;
|
||
}
|
||
|
||
.file-select-btn:hover {
|
||
background-color: #2980b9;
|
||
}
|
||
|
||
.image-preview-container {
|
||
margin-top: 30px;
|
||
}
|
||
|
||
.section-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
margin-bottom: 20px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
}
|
||
|
||
.image-count {
|
||
font-size: 14px;
|
||
color: #666;
|
||
font-weight: normal;
|
||
}
|
||
|
||
.image-grid {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fill, minmax(150px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.image-item {
|
||
position: relative;
|
||
border: 1px solid #ddd;
|
||
border-radius: 8px;
|
||
overflow: hidden;
|
||
background: #f8f9fa;
|
||
cursor: move;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.image-item:hover {
|
||
transform: translateY(-2px);
|
||
box-shadow: 0 4px 12px rgba(0, 0, 0, 0.15);
|
||
}
|
||
|
||
.image-item.dragging {
|
||
opacity: 0.5;
|
||
transform: rotate(2deg);
|
||
}
|
||
|
||
.image-preview {
|
||
width: 100%;
|
||
height: 120px;
|
||
object-fit: cover;
|
||
border-bottom: 1px solid #ddd;
|
||
}
|
||
|
||
.image-info {
|
||
padding: 8px;
|
||
font-size: 12px;
|
||
color: #666;
|
||
background: white;
|
||
}
|
||
|
||
.image-name {
|
||
white-space: nowrap;
|
||
overflow: hidden;
|
||
text-overflow: ellipsis;
|
||
margin-bottom: 4px;
|
||
}
|
||
|
||
.image-size {
|
||
color: #888;
|
||
}
|
||
|
||
.image-actions {
|
||
position: absolute;
|
||
top: 5px;
|
||
right: 5px;
|
||
display: flex;
|
||
gap: 5px;
|
||
}
|
||
|
||
.image-action-btn {
|
||
width: 24px;
|
||
height: 24px;
|
||
border: none;
|
||
border-radius: 50%;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
font-size: 12px;
|
||
transition: all 0.3s ease;
|
||
}
|
||
|
||
.delete-btn {
|
||
background: rgba(231, 76, 60, 0.9);
|
||
color: white;
|
||
}
|
||
|
||
.delete-btn:hover {
|
||
background: #e74c3c;
|
||
}
|
||
|
||
.move-btn {
|
||
background: rgba(52, 152, 219, 0.9);
|
||
color: white;
|
||
}
|
||
|
||
.move-btn:hover {
|
||
background: #3498db;
|
||
}
|
||
|
||
.action-buttons {
|
||
display: flex;
|
||
gap: 15px;
|
||
justify-content: center;
|
||
margin-top: 30px;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.btn {
|
||
padding: 12px 24px;
|
||
border: none;
|
||
border-radius: 8px;
|
||
cursor: pointer;
|
||
font-size: 16px;
|
||
font-weight: 500;
|
||
transition: all 0.3s ease;
|
||
min-width: 120px;
|
||
}
|
||
|
||
.btn-primary {
|
||
background-color: #3498db;
|
||
color: white;
|
||
}
|
||
|
||
.btn-primary:hover {
|
||
background-color: #2980b9;
|
||
}
|
||
|
||
.btn-secondary {
|
||
background-color: #95a5a6;
|
||
color: white;
|
||
}
|
||
|
||
.btn-secondary:hover {
|
||
background-color: #7f8c8d;
|
||
}
|
||
|
||
.btn-danger {
|
||
background-color: #e74c3c;
|
||
color: white;
|
||
}
|
||
|
||
.btn-danger:hover {
|
||
background-color: #c0392b;
|
||
}
|
||
|
||
.btn:disabled {
|
||
opacity: 0.6;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.pdf-options {
|
||
background: #f8f9fa;
|
||
padding: 20px;
|
||
border-radius: 8px;
|
||
margin-bottom: 20px;
|
||
}
|
||
|
||
.option-group {
|
||
display: grid;
|
||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||
gap: 15px;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.form-group {
|
||
display: flex;
|
||
flex-direction: column;
|
||
}
|
||
|
||
.form-group label {
|
||
margin-bottom: 5px;
|
||
font-weight: 500;
|
||
color: #333;
|
||
}
|
||
|
||
.form-group input,
|
||
.form-group select {
|
||
padding: 8px 12px;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
font-size: 14px;
|
||
}
|
||
|
||
.form-group input:focus,
|
||
.form-group select:focus {
|
||
outline: none;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.progress-container {
|
||
display: none;
|
||
margin-top: 20px;
|
||
}
|
||
|
||
.progress-bar {
|
||
width: 100%;
|
||
height: 8px;
|
||
background: #ecf0f1;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
}
|
||
|
||
.progress-fill {
|
||
height: 100%;
|
||
background: linear-gradient(90deg, #3498db, #2980b9);
|
||
width: 0%;
|
||
transition: width 0.3s ease;
|
||
}
|
||
|
||
.progress-text {
|
||
text-align: center;
|
||
margin-top: 10px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.empty-state {
|
||
text-align: center;
|
||
padding: 60px 20px;
|
||
color: #888;
|
||
}
|
||
|
||
.empty-state-icon {
|
||
font-size: 64px;
|
||
margin-bottom: 20px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.empty-state-text {
|
||
font-size: 16px;
|
||
margin-bottom: 10px;
|
||
}
|
||
|
||
.empty-state-hint {
|
||
font-size: 14px;
|
||
color: #aaa;
|
||
}
|
||
|
||
/* PDF预览区域样式 */
|
||
.preview-container {
|
||
background: #f8f9fa;
|
||
border-radius: 8px;
|
||
padding: 20px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
.preview-header {
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: space-between;
|
||
margin-bottom: 15px;
|
||
}
|
||
|
||
.preview-title {
|
||
font-size: 18px;
|
||
font-weight: 600;
|
||
color: #333;
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
}
|
||
|
||
.preview-status {
|
||
font-size: 14px;
|
||
color: #666;
|
||
padding: 4px 8px;
|
||
background: #e9ecef;
|
||
border-radius: 4px;
|
||
}
|
||
|
||
.preview-status.ready {
|
||
background: #d4edda;
|
||
color: #155724;
|
||
}
|
||
|
||
.preview-status.generating {
|
||
background: #fff3cd;
|
||
color: #856404;
|
||
}
|
||
|
||
.preview-canvas-container {
|
||
background: white;
|
||
border: 1px solid #ddd;
|
||
border-radius: 4px;
|
||
overflow: hidden;
|
||
position: relative;
|
||
margin-bottom: 15px;
|
||
min-height: 400px;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
}
|
||
|
||
.preview-canvas {
|
||
max-width: 100%;
|
||
max-height: 500px;
|
||
display: block;
|
||
margin: 0 auto;
|
||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
|
||
}
|
||
|
||
.preview-placeholder {
|
||
text-align: center;
|
||
color: #888;
|
||
padding: 40px 20px;
|
||
}
|
||
|
||
.preview-placeholder-icon {
|
||
font-size: 48px;
|
||
margin-bottom: 10px;
|
||
opacity: 0.5;
|
||
}
|
||
|
||
.preview-placeholder-text {
|
||
font-size: 14px;
|
||
line-height: 1.4;
|
||
}
|
||
|
||
.preview-controls {
|
||
display: flex;
|
||
gap: 10px;
|
||
justify-content: center;
|
||
align-items: center;
|
||
flex-wrap: wrap;
|
||
}
|
||
|
||
.preview-page-nav {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.preview-page-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 1px solid #ddd;
|
||
background: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.preview-page-btn:hover:not(:disabled) {
|
||
background: #f8f9fa;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.preview-page-btn:disabled {
|
||
opacity: 0.5;
|
||
cursor: not-allowed;
|
||
}
|
||
|
||
.preview-zoom {
|
||
display: flex;
|
||
align-items: center;
|
||
gap: 8px;
|
||
font-size: 14px;
|
||
color: #666;
|
||
}
|
||
|
||
.preview-zoom-btn {
|
||
width: 28px;
|
||
height: 28px;
|
||
border: 1px solid #ddd;
|
||
background: white;
|
||
border-radius: 4px;
|
||
cursor: pointer;
|
||
display: flex;
|
||
align-items: center;
|
||
justify-content: center;
|
||
transition: all 0.2s ease;
|
||
}
|
||
|
||
.preview-zoom-btn:hover {
|
||
background: #f8f9fa;
|
||
border-color: #3498db;
|
||
}
|
||
|
||
.preview-zoom-slider {
|
||
width: 100px;
|
||
}
|
||
|
||
.preview-info {
|
||
display: flex;
|
||
justify-content: space-between;
|
||
align-items: center;
|
||
font-size: 12px;
|
||
color: #666;
|
||
margin-top: 10px;
|
||
padding: 8px;
|
||
background: white;
|
||
border-radius: 4px;
|
||
border: 1px solid #e9ecef;
|
||
}
|
||
|
||
@media (max-width: 1200px) {
|
||
.main-content {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.left-panel,
|
||
.right-panel {
|
||
max-width: none;
|
||
position: static;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 768px) {
|
||
.container {
|
||
margin: 10px;
|
||
padding: 15px;
|
||
}
|
||
|
||
.image-grid {
|
||
grid-template-columns: repeat(auto-fill, minmax(120px, 1fr));
|
||
gap: 10px;
|
||
}
|
||
|
||
.option-group {
|
||
grid-template-columns: 1fr;
|
||
}
|
||
|
||
.action-buttons {
|
||
flex-direction: column;
|
||
}
|
||
|
||
.btn {
|
||
width: 100%;
|
||
}
|
||
|
||
.preview-canvas-container {
|
||
min-height: 300px;
|
||
}
|
||
|
||
.preview-controls {
|
||
flex-direction: column;
|
||
gap: 15px;
|
||
}
|
||
|
||
.preview-info {
|
||
flex-direction: column;
|
||
gap: 5px;
|
||
text-align: center;
|
||
}
|
||
}
|
||
|
||
@media (max-width: 480px) {
|
||
.image-grid {
|
||
grid-template-columns: repeat(2, 1fr);
|
||
}
|
||
}
|
||
</style>
|
||
</head>
|
||
<body>
|
||
<div class="container">
|
||
<h1>图片转PDF(纯前端,后台不保存数据)</h1>
|
||
|
||
<div class="main-content">
|
||
<!-- 左侧面板 -->
|
||
<div class="left-panel">
|
||
<!-- 上传区域 -->
|
||
<div class="upload-area" id="uploadArea">
|
||
<div class="upload-icon">📷</div>
|
||
<div class="upload-text">点击或拖拽图片到这里</div>
|
||
<div class="upload-hint">支持 JPG、PNG、GIF、BMP、WEBP 格式</div>
|
||
<button class="file-select-btn">选择图片</button>
|
||
<input type="file" id="fileInput" multiple accept="image/*" />
|
||
</div>
|
||
|
||
<!-- PDF选项 -->
|
||
<div class="pdf-options">
|
||
<div class="option-group">
|
||
<div class="form-group">
|
||
<label for="pageSize">页面尺寸:</label>
|
||
<select id="pageSize">
|
||
<option value="a4" selected>A4 (210×297mm)</option>
|
||
<option value="a3">A3 (297×420mm)</option>
|
||
<option value="letter">Letter (216×279mm)</option>
|
||
<option value="legal">Legal (216×356mm)</option>
|
||
<option value="auto">自动适应图片</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="orientation">页面方向:</label>
|
||
<select id="orientation">
|
||
<option value="auto" selected>自动适应</option>
|
||
<option value="portrait">纵向</option>
|
||
<option value="landscape">横向</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="imageQuality">图片质量:</label>
|
||
<select id="imageQuality">
|
||
<option value="high">高质量</option>
|
||
<option value="medium" selected>中等质量</option>
|
||
<option value="low">低质量</option>
|
||
</select>
|
||
</div>
|
||
<div class="form-group">
|
||
<label for="margin">边距 (mm):</label>
|
||
<input type="number" id="margin" value="0" min="0" max="50" />
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 图片预览容器 -->
|
||
<div
|
||
class="image-preview-container"
|
||
id="imagePreviewContainer"
|
||
style="display: none"
|
||
>
|
||
<div class="section-title">
|
||
<span>图片列表</span>
|
||
<span class="image-count" id="imageCount">0 张图片</span>
|
||
</div>
|
||
<div class="image-grid" id="imageGrid"></div>
|
||
</div>
|
||
|
||
<!-- 空状态 -->
|
||
<div class="empty-state" id="emptyState">
|
||
<div class="empty-state-icon">🖼️</div>
|
||
<div class="empty-state-text">还没有选择图片</div>
|
||
<div class="empty-state-hint">点击上方区域选择要转换的图片</div>
|
||
</div>
|
||
|
||
<!-- 操作按钮 -->
|
||
<div class="action-buttons" id="actionButtons" style="display: none">
|
||
<button class="btn btn-secondary" id="clearAllBtn">清空图片</button>
|
||
<button class="btn btn-primary" id="generatePdfBtn">生成PDF</button>
|
||
</div>
|
||
|
||
<!-- 进度条 -->
|
||
<div class="progress-container" id="progressContainer">
|
||
<div class="progress-bar">
|
||
<div class="progress-fill" id="progressFill"></div>
|
||
</div>
|
||
<div class="progress-text" id="progressText">处理中...</div>
|
||
</div>
|
||
</div>
|
||
|
||
<!-- 右侧面板 - PDF预览 -->
|
||
<div class="right-panel">
|
||
<div class="preview-container">
|
||
<div class="preview-header">
|
||
<div class="preview-title">📄 PDF预览</div>
|
||
<div class="preview-status" id="previewStatus">等待图片</div>
|
||
</div>
|
||
|
||
<div class="preview-canvas-container" id="previewCanvasContainer">
|
||
<div class="preview-placeholder" id="previewPlaceholder">
|
||
<div class="preview-placeholder-icon">📄</div>
|
||
<div class="preview-placeholder-text">
|
||
添加图片后将显示PDF预览<br />
|
||
可以实时查看生成效果
|
||
</div>
|
||
</div>
|
||
<canvas
|
||
class="preview-canvas"
|
||
id="previewCanvas"
|
||
style="display: none"
|
||
></canvas>
|
||
</div>
|
||
|
||
<div
|
||
class="preview-controls"
|
||
id="previewControls"
|
||
style="display: none"
|
||
>
|
||
<div class="preview-page-nav">
|
||
<button
|
||
class="preview-page-btn"
|
||
id="prevPageBtn"
|
||
title="上一页"
|
||
>
|
||
‹
|
||
</button>
|
||
<span id="pageInfo">1 / 1</span>
|
||
<button
|
||
class="preview-page-btn"
|
||
id="nextPageBtn"
|
||
title="下一页"
|
||
>
|
||
›
|
||
</button>
|
||
</div>
|
||
|
||
<div class="preview-zoom">
|
||
<button class="preview-zoom-btn" id="zoomOutBtn" title="缩小">
|
||
-
|
||
</button>
|
||
<input
|
||
type="range"
|
||
class="preview-zoom-slider"
|
||
id="zoomSlider"
|
||
min="50"
|
||
max="200"
|
||
value="100"
|
||
step="10"
|
||
/>
|
||
<button class="preview-zoom-btn" id="zoomInBtn" title="放大">
|
||
+
|
||
</button>
|
||
<span id="zoomLevel">100%</span>
|
||
</div>
|
||
</div>
|
||
|
||
<div class="preview-info" id="previewInfo" style="display: none">
|
||
<span id="pageSizeInfo">页面: A4</span>
|
||
<span id="imageCountInfo">0 张图片</span>
|
||
<span id="fileSizeInfo">约 0 KB</span>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
</div>
|
||
|
||
<script>
|
||
// 全局变量
|
||
let uploadedImages = [];
|
||
let isDragging = false;
|
||
let currentPage = 0;
|
||
let currentZoom = 1;
|
||
let previewPages = [];
|
||
let previewUpdateTimeout = null;
|
||
|
||
// DOM元素
|
||
const uploadArea = document.getElementById("uploadArea");
|
||
const fileInput = document.getElementById("fileInput");
|
||
const imagePreviewContainer = document.getElementById(
|
||
"imagePreviewContainer",
|
||
);
|
||
const imageGrid = document.getElementById("imageGrid");
|
||
const imageCount = document.getElementById("imageCount");
|
||
const emptyState = document.getElementById("emptyState");
|
||
const actionButtons = document.getElementById("actionButtons");
|
||
const generatePdfBtn = document.getElementById("generatePdfBtn");
|
||
const clearAllBtn = document.getElementById("clearAllBtn");
|
||
const progressContainer = document.getElementById("progressContainer");
|
||
const progressFill = document.getElementById("progressFill");
|
||
const progressText = document.getElementById("progressText");
|
||
|
||
// 预览相关DOM元素
|
||
const previewStatus = document.getElementById("previewStatus");
|
||
const previewPlaceholder = document.getElementById("previewPlaceholder");
|
||
const previewCanvas = document.getElementById("previewCanvas");
|
||
const previewControls = document.getElementById("previewControls");
|
||
const previewInfo = document.getElementById("previewInfo");
|
||
const prevPageBtn = document.getElementById("prevPageBtn");
|
||
const nextPageBtn = document.getElementById("nextPageBtn");
|
||
const pageInfo = document.getElementById("pageInfo");
|
||
const zoomOutBtn = document.getElementById("zoomOutBtn");
|
||
const zoomInBtn = document.getElementById("zoomInBtn");
|
||
const zoomSlider = document.getElementById("zoomSlider");
|
||
const zoomLevel = document.getElementById("zoomLevel");
|
||
const pageSizeInfo = document.getElementById("pageSizeInfo");
|
||
const imageCountInfo = document.getElementById("imageCountInfo");
|
||
const fileSizeInfo = document.getElementById("fileSizeInfo");
|
||
|
||
// 事件监听器
|
||
uploadArea.addEventListener("click", () => fileInput.click());
|
||
fileInput.addEventListener("change", handleFileSelect);
|
||
generatePdfBtn.addEventListener("click", generatePdf);
|
||
clearAllBtn.addEventListener("click", clearAllImages);
|
||
|
||
// 预览控制事件监听器
|
||
prevPageBtn.addEventListener("click", () => navigatePage(-1));
|
||
nextPageBtn.addEventListener("click", () => navigatePage(1));
|
||
zoomOutBtn.addEventListener("click", () => changeZoom(-0.1));
|
||
zoomInBtn.addEventListener("click", () => changeZoom(0.1));
|
||
zoomSlider.addEventListener("input", (e) => {
|
||
currentZoom = e.target.value / 100;
|
||
zoomLevel.textContent = e.target.value + "%";
|
||
renderPreview();
|
||
});
|
||
|
||
// 监听PDF选项变化
|
||
document
|
||
.getElementById("pageSize")
|
||
.addEventListener("change", schedulePreviewUpdate);
|
||
document
|
||
.getElementById("orientation")
|
||
.addEventListener("change", schedulePreviewUpdate);
|
||
document
|
||
.getElementById("margin")
|
||
.addEventListener("input", schedulePreviewUpdate);
|
||
|
||
// 拖拽功能
|
||
uploadArea.addEventListener("dragover", handleDragOver);
|
||
uploadArea.addEventListener("dragleave", handleDragLeave);
|
||
uploadArea.addEventListener("drop", handleDrop);
|
||
|
||
function handleDragOver(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.add("drag-over");
|
||
}
|
||
|
||
function handleDragLeave(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove("drag-over");
|
||
}
|
||
|
||
function handleDrop(e) {
|
||
e.preventDefault();
|
||
uploadArea.classList.remove("drag-over");
|
||
|
||
const files = Array.from(e.dataTransfer.files).filter((file) =>
|
||
file.type.startsWith("image/"),
|
||
);
|
||
|
||
if (files.length > 0) {
|
||
processFiles(files);
|
||
}
|
||
}
|
||
|
||
function handleFileSelect(e) {
|
||
const files = Array.from(e.target.files);
|
||
processFiles(files);
|
||
// 清空input,允许重复选择同一文件
|
||
e.target.value = "";
|
||
}
|
||
|
||
function processFiles(files) {
|
||
files.forEach((file) => {
|
||
if (!file.type.startsWith("image/")) {
|
||
alert(`文件 ${file.name} 不是有效的图片格式`);
|
||
return;
|
||
}
|
||
|
||
const reader = new FileReader();
|
||
reader.onload = (e) => {
|
||
const imageObj = {
|
||
id: Date.now() + Math.random(),
|
||
name: file.name,
|
||
size: formatFileSize(file.size),
|
||
dataUrl: e.target.result,
|
||
file: file,
|
||
};
|
||
uploadedImages.push(imageObj);
|
||
updateUI();
|
||
};
|
||
reader.readAsDataURL(file);
|
||
});
|
||
}
|
||
|
||
function formatFileSize(bytes) {
|
||
if (bytes === 0) return "0 Bytes";
|
||
const k = 1024;
|
||
const sizes = ["Bytes", "KB", "MB", "GB"];
|
||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + " " + sizes[i];
|
||
}
|
||
|
||
function updateUI() {
|
||
const hasImages = uploadedImages.length > 0;
|
||
|
||
// 显示/隐藏相关元素
|
||
emptyState.style.display = hasImages ? "none" : "block";
|
||
imagePreviewContainer.style.display = hasImages ? "block" : "none";
|
||
actionButtons.style.display = hasImages ? "flex" : "none";
|
||
|
||
// 更新图片计数
|
||
imageCount.textContent = `${uploadedImages.length} 张图片`;
|
||
|
||
// 渲染图片网格
|
||
renderImageGrid();
|
||
|
||
// 更新预览
|
||
if (hasImages) {
|
||
schedulePreviewUpdate();
|
||
} else {
|
||
resetPreview();
|
||
}
|
||
}
|
||
|
||
function renderImageGrid() {
|
||
imageGrid.innerHTML = "";
|
||
|
||
uploadedImages.forEach((image, index) => {
|
||
const imageItem = document.createElement("div");
|
||
imageItem.className = "image-item";
|
||
imageItem.draggable = true;
|
||
imageItem.dataset.index = index;
|
||
|
||
imageItem.innerHTML = `
|
||
<img src="${image.dataUrl}" alt="${image.name}" class="image-preview">
|
||
<div class="image-info">
|
||
<div class="image-name">${image.name}</div>
|
||
<div class="image-size">${image.size}</div>
|
||
</div>
|
||
<div class="image-actions">
|
||
<button class="image-action-btn move-btn" onclick="moveImage(${index}, -1)" title="上移">↑</button>
|
||
<button class="image-action-btn move-btn" onclick="moveImage(${index}, 1)" title="下移">↓</button>
|
||
<button class="image-action-btn delete-btn" onclick="deleteImage(${index})" title="删除">×</button>
|
||
</div>
|
||
`;
|
||
|
||
// 拖拽排序功能
|
||
imageItem.addEventListener("dragstart", handleDragStart);
|
||
imageItem.addEventListener("dragend", handleDragEnd);
|
||
imageItem.addEventListener("dragover", handleItemDragOver);
|
||
imageItem.addEventListener("drop", handleItemDrop);
|
||
|
||
imageGrid.appendChild(imageItem);
|
||
});
|
||
}
|
||
|
||
let draggedElement = null;
|
||
let draggedIndex = null;
|
||
|
||
function handleDragStart(e) {
|
||
draggedElement = this;
|
||
draggedIndex = parseInt(this.dataset.index);
|
||
this.classList.add("dragging");
|
||
e.dataTransfer.effectAllowed = "move";
|
||
}
|
||
|
||
function handleDragEnd(e) {
|
||
this.classList.remove("dragging");
|
||
draggedElement = null;
|
||
draggedIndex = null;
|
||
}
|
||
|
||
function handleItemDragOver(e) {
|
||
if (e.preventDefault) {
|
||
e.preventDefault();
|
||
}
|
||
e.dataTransfer.dropEffect = "move";
|
||
return false;
|
||
}
|
||
|
||
function handleItemDrop(e) {
|
||
if (e.stopPropagation) {
|
||
e.stopPropagation();
|
||
}
|
||
|
||
if (draggedElement !== this) {
|
||
const dropIndex = parseInt(this.dataset.index);
|
||
const draggedImage = uploadedImages[draggedIndex];
|
||
|
||
// 重新排序数组
|
||
uploadedImages.splice(draggedIndex, 1);
|
||
uploadedImages.splice(dropIndex, 0, draggedImage);
|
||
|
||
updateUI();
|
||
}
|
||
return false;
|
||
}
|
||
|
||
function moveImage(index, direction) {
|
||
const newIndex = index + direction;
|
||
if (newIndex < 0 || newIndex >= uploadedImages.length) {
|
||
return;
|
||
}
|
||
// 交换位置
|
||
[uploadedImages[index], uploadedImages[newIndex]] = [
|
||
uploadedImages[newIndex],
|
||
uploadedImages[index],
|
||
];
|
||
updateUI();
|
||
}
|
||
|
||
function deleteImage(index) {
|
||
uploadedImages.splice(index, 1);
|
||
updateUI();
|
||
}
|
||
|
||
function clearAllImages() {
|
||
if (confirm("确定要清空所有图片吗?")) {
|
||
uploadedImages = [];
|
||
fileInput.value = "";
|
||
updateUI();
|
||
}
|
||
}
|
||
|
||
// --- PDF预览功能 ---
|
||
|
||
function schedulePreviewUpdate() {
|
||
if (previewUpdateTimeout) {
|
||
clearTimeout(previewUpdateTimeout);
|
||
}
|
||
previewUpdateTimeout = setTimeout(() => {
|
||
updatePreview();
|
||
}, 500);
|
||
}
|
||
|
||
function resetPreview() {
|
||
currentPage = 0;
|
||
previewPages = [];
|
||
previewStatus.textContent = "等待图片";
|
||
previewStatus.className = "preview-status";
|
||
previewPlaceholder.style.display = "block";
|
||
previewCanvas.style.display = "none";
|
||
previewControls.style.display = "none";
|
||
previewInfo.style.display = "none";
|
||
}
|
||
|
||
async function updatePreview() {
|
||
if (uploadedImages.length === 0) {
|
||
resetPreview();
|
||
return;
|
||
}
|
||
|
||
try {
|
||
previewStatus.textContent = "生成预览中...";
|
||
previewStatus.className = "preview-status generating";
|
||
|
||
previewPages = [];
|
||
|
||
// 获取用户设置
|
||
const pageSizeSelect = document.getElementById("pageSize").value;
|
||
const orientationSelect =
|
||
document.getElementById("orientation").value;
|
||
const margin = parseInt(document.getElementById("margin").value) || 0;
|
||
|
||
// 生成每页预览
|
||
for (let i = 0; i < uploadedImages.length; i++) {
|
||
const imageObj = uploadedImages[i];
|
||
const img = await loadImagePromise(imageObj.dataUrl);
|
||
|
||
// 确定页面方向
|
||
let pageOrientation = img.width > img.height ? "l" : "p";
|
||
if (orientationSelect !== "auto") {
|
||
pageOrientation = orientationSelect === "landscape" ? "l" : "p";
|
||
}
|
||
|
||
// 确定页面尺寸
|
||
let pageWidth, pageHeight;
|
||
if (pageSizeSelect === "auto") {
|
||
pageWidth = 210;
|
||
pageHeight = 297;
|
||
// 如果是横向,交换宽高
|
||
if (pageOrientation === "l") {
|
||
[pageWidth, pageHeight] = [pageHeight, pageWidth];
|
||
}
|
||
} else {
|
||
const pageSizes = {
|
||
a4: { width: 210, height: 297 },
|
||
a3: { width: 297, height: 420 },
|
||
letter: { width: 216, height: 279 },
|
||
legal: { width: 216, height: 356 },
|
||
};
|
||
const size = pageSizes[pageSizeSelect] || pageSizes["a4"];
|
||
pageWidth = pageOrientation === "l" ? size.height : size.width;
|
||
pageHeight = pageOrientation === "l" ? size.width : size.height;
|
||
}
|
||
|
||
previewPages.push({
|
||
image: img,
|
||
pageWidth: pageWidth,
|
||
pageHeight: pageHeight,
|
||
margin: margin,
|
||
});
|
||
}
|
||
|
||
// 显示预览控件
|
||
previewPlaceholder.style.display = "none";
|
||
previewCanvas.style.display = "block";
|
||
previewControls.style.display = "flex";
|
||
previewInfo.style.display = "flex";
|
||
|
||
// 更新状态
|
||
previewStatus.textContent = "预览就绪";
|
||
previewStatus.className = "preview-status ready";
|
||
|
||
// 更新信息
|
||
updatePreviewInfo();
|
||
|
||
// 确保当前页在有效范围内
|
||
if (currentPage >= previewPages.length) {
|
||
currentPage = previewPages.length - 1;
|
||
}
|
||
|
||
// 渲染当前页
|
||
renderPreview();
|
||
} catch (error) {
|
||
console.error("预览生成失败:", error);
|
||
previewStatus.textContent = "预览失败";
|
||
previewStatus.className = "preview-status";
|
||
}
|
||
}
|
||
|
||
function updatePreviewInfo() {
|
||
const pageSize = document.getElementById("pageSize").value;
|
||
const pageSizeText =
|
||
pageSize === "auto" ? "自动" : pageSize.toUpperCase();
|
||
|
||
pageSizeInfo.textContent = `页面: ${pageSizeText}`;
|
||
imageCountInfo.textContent = `${previewPages.length} 张图片`;
|
||
|
||
// 估算文件大小
|
||
const estimatedSize = previewPages.length * 500; // 每页约500KB
|
||
if (estimatedSize < 1024) {
|
||
fileSizeInfo.textContent = `约 ${estimatedSize} KB`;
|
||
} else {
|
||
fileSizeInfo.textContent = `约 ${(estimatedSize / 1024).toFixed(1)} MB`;
|
||
}
|
||
|
||
// 更新页面导航
|
||
pageInfo.textContent = `${currentPage + 1} / ${previewPages.length}`;
|
||
prevPageBtn.disabled = currentPage === 0;
|
||
nextPageBtn.disabled = currentPage === previewPages.length - 1;
|
||
}
|
||
|
||
function renderPreview() {
|
||
if (previewPages.length === 0) return;
|
||
|
||
const pageData = previewPages[currentPage];
|
||
const canvas = previewCanvas;
|
||
const ctx = canvas.getContext("2d");
|
||
|
||
// 设置canvas尺寸(考虑缩放)
|
||
const scale = Math.min(2, Math.max(0.5, currentZoom));
|
||
canvas.width = pageData.pageWidth * scale * 3.78; // mm to pixels at 96dpi
|
||
canvas.height = pageData.pageHeight * scale * 3.78;
|
||
|
||
// 清空画布
|
||
ctx.fillStyle = "#ffffff";
|
||
ctx.fillRect(0, 0, canvas.width, canvas.height);
|
||
|
||
// 计算内容区域
|
||
const contentWidth =
|
||
(pageData.pageWidth - pageData.margin * 2) * scale * 3.78;
|
||
const contentHeight =
|
||
(pageData.pageHeight - pageData.margin * 2) * scale * 3.78;
|
||
const offsetX = pageData.margin * scale * 3.78;
|
||
const offsetY = pageData.margin * scale * 3.78;
|
||
|
||
// 计算图片缩放和位置
|
||
const imgRatio = pageData.image.width / pageData.image.height;
|
||
const contentRatio = contentWidth / contentHeight;
|
||
|
||
let drawWidth, drawHeight;
|
||
if (imgRatio > contentRatio) {
|
||
drawWidth = contentWidth;
|
||
drawHeight = contentWidth / imgRatio;
|
||
} else {
|
||
drawHeight = contentHeight;
|
||
drawWidth = contentHeight * imgRatio;
|
||
}
|
||
|
||
const drawX = offsetX + (contentWidth - drawWidth) / 2;
|
||
const drawY = offsetY + (contentHeight - drawHeight) / 2;
|
||
|
||
// 绘制图片
|
||
ctx.drawImage(pageData.image, drawX, drawY, drawWidth, drawHeight);
|
||
|
||
// 绘制边框(可选)
|
||
ctx.strokeStyle = "#e0e0e0";
|
||
ctx.lineWidth = 1;
|
||
ctx.strokeRect(0, 0, canvas.width, canvas.height);
|
||
}
|
||
|
||
function navigatePage(direction) {
|
||
const newPage = currentPage + direction;
|
||
if (newPage >= 0 && newPage < previewPages.length) {
|
||
currentPage = newPage;
|
||
renderPreview();
|
||
updatePreviewInfo();
|
||
}
|
||
}
|
||
|
||
function changeZoom(delta) {
|
||
const newZoom = Math.max(0.5, Math.min(2, currentZoom + delta));
|
||
currentZoom = newZoom;
|
||
const percentage = Math.round(newZoom * 100);
|
||
zoomLevel.textContent = percentage + "%";
|
||
zoomSlider.value = percentage;
|
||
renderPreview();
|
||
}
|
||
|
||
// --- 核心逻辑修改部分 ---
|
||
|
||
// 辅助函数:将图片加载封装为 Promise,以便提前获取尺寸
|
||
function loadImagePromise(dataUrl) {
|
||
return new Promise((resolve, reject) => {
|
||
const img = new Image();
|
||
img.onload = () => resolve(img);
|
||
img.onerror = reject;
|
||
img.src = dataUrl;
|
||
});
|
||
}
|
||
|
||
async function generatePdf() {
|
||
if (uploadedImages.length === 0) {
|
||
alert("请先选择图片");
|
||
return;
|
||
}
|
||
|
||
try {
|
||
showProgress(true);
|
||
updateProgress(0, "准备生成PDF...");
|
||
|
||
// 获取用户设置
|
||
const pageSizeSelect = document.getElementById("pageSize").value;
|
||
const orientationSelect =
|
||
document.getElementById("orientation").value;
|
||
const imageQuality = document.getElementById("imageQuality").value;
|
||
const margin = parseInt(document.getElementById("margin").value) || 0;
|
||
|
||
const { jsPDF } = window.jspdf;
|
||
let pdf = null;
|
||
|
||
// 遍历处理每一张图片
|
||
for (let i = 0; i < uploadedImages.length; i++) {
|
||
updateProgress(
|
||
(i / uploadedImages.length) * 90,
|
||
`正在处理第 ${i + 1}/${uploadedImages.length} 张图片...`,
|
||
);
|
||
|
||
const imageObj = uploadedImages[i];
|
||
const img = await loadImagePromise(imageObj.dataUrl);
|
||
|
||
// 1. 确定当前页面的方向 (Portrait 'p' 或 Landscape 'l')
|
||
let pageOrientation = "p"; // 默认纵向
|
||
|
||
if (orientationSelect === "auto") {
|
||
// 自适应:如果图片宽 > 高,则使用横向
|
||
pageOrientation = img.width > img.height ? "l" : "p";
|
||
} else {
|
||
// 用户强制指定
|
||
pageOrientation = orientationSelect === "landscape" ? "l" : "p";
|
||
}
|
||
|
||
// 2. 确定页面尺寸格式 (如 'a4', 'a3' 或自定义尺寸)
|
||
let format = pageSizeSelect;
|
||
if (pageSizeSelect === "auto") {
|
||
// 如果页面完全适应图片,这里我们给一个动态的尺寸
|
||
// 为了方便打印,通常我们还是会映射到标准纸张,或者直接用图片像素转mm
|
||
// 这里实现:将图片大小按比例映射到A4的宽度或高度基准,或者直接自定义大小
|
||
// 简化逻辑:如果是 auto,我们用 A4 作为基准,但是完全贴合比例
|
||
format = "a4";
|
||
}
|
||
|
||
// 3. 添加页面或初始化 PDF
|
||
if (i === 0) {
|
||
// 第一张图:初始化 PDF 实例
|
||
// 注意:jsPDF 初始化时就需要指定方向,这样第一页才正确
|
||
pdf = new jsPDF({
|
||
orientation: pageOrientation,
|
||
unit: "mm",
|
||
format: format,
|
||
});
|
||
} else {
|
||
// 后续图片:添加新页面,指定该页面的方向
|
||
pdf.addPage(format, pageOrientation);
|
||
}
|
||
|
||
// 4. 计算图片在页面中的绘制尺寸和位置
|
||
// 获取当前页面的物理宽高 (jsPDF 会根据 orientation 自动调整 internal.pageSize 的宽高)
|
||
const pageWidth = pdf.internal.pageSize.getWidth();
|
||
const pageHeight = pdf.internal.pageSize.getHeight();
|
||
|
||
// 可用绘图区域 (减去边距)
|
||
const contentWidth = pageWidth - margin * 2;
|
||
const contentHeight = pageHeight - margin * 2;
|
||
|
||
// 计算缩放比例:保持长宽比,填满可用区域
|
||
// Math.min 确保图片完全包含在页面内 (contain)
|
||
const widthRatio = contentWidth / img.width;
|
||
const heightRatio = contentHeight / img.height;
|
||
const scaleFactor = Math.min(widthRatio, heightRatio);
|
||
|
||
const finalWidth = img.width * scaleFactor;
|
||
const finalHeight = img.height * scaleFactor;
|
||
|
||
// 居中计算
|
||
const x = (pageWidth - finalWidth) / 2;
|
||
const y = (pageHeight - finalHeight) / 2;
|
||
|
||
// 5. 绘制图片
|
||
let jpegQuality = 0.8;
|
||
if (imageQuality === "high") jpegQuality = 0.95;
|
||
if (imageQuality === "low") jpegQuality = 0.6;
|
||
|
||
pdf.addImage(
|
||
imageObj.dataUrl,
|
||
"JPEG",
|
||
x,
|
||
y,
|
||
finalWidth,
|
||
finalHeight,
|
||
undefined,
|
||
"FAST", // 压缩速度
|
||
0, // 旋转角度
|
||
);
|
||
}
|
||
|
||
updateProgress(95, "正在保存PDF...");
|
||
|
||
const fileName = `images_${new Date().getTime()}.pdf`;
|
||
pdf.save(fileName);
|
||
|
||
updateProgress(100, "PDF生成完成!");
|
||
|
||
setTimeout(() => {
|
||
showProgress(false);
|
||
}, 2000);
|
||
} catch (error) {
|
||
console.error("生成PDF时出错:", error);
|
||
alert("生成PDF时出错: " + error.message);
|
||
showProgress(false);
|
||
}
|
||
}
|
||
|
||
function showProgress(show) {
|
||
progressContainer.style.display = show ? "block" : "none";
|
||
generatePdfBtn.disabled = show;
|
||
clearAllBtn.disabled = show;
|
||
}
|
||
|
||
function updateProgress(percent, text) {
|
||
progressFill.style.width = percent + "%";
|
||
progressText.textContent = text;
|
||
}
|
||
|
||
// 初始化
|
||
updateUI();
|
||
</script>
|
||
</body>
|
||
</html> |