[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/pdf.js/3.11.174/pdf.min.js"></script>
<link href="https://fonts.googleapis.com/css2?family=Noto+Sans+SC:wght@300;400;500;700&family=Playfair+Display:wght@600&display=swap" rel="stylesheet">
<style>
:root {
--primary: #1a365d;
--primary-light: #2c5282;
--accent: #ed8936;
--accent-light: #f6ad55;
--bg: #f7fafc;
--card-bg: #ffffff;
--text: #2d3748;
--text-light: #718096;
--border: #e2e8f0;
--success: #38a169;
--danger: #e53e3e;
--shadow: 0 10px 40px rgba(26, 54, 93, 0.1);
--shadow-hover: 0 20px 60px rgba(26, 54, 93, 0.15);
}
* {
margin: 0;
padding: 0;
box-sizing: border-box;
}
body {
font-family: 'Noto Sans SC', sans-serif;
background: var(--bg);
color: var(--text);
min-height: 100vh;
background-image:
radial-gradient(circle at 20% 50%, rgba(237, 137, 54, 0.05) 0%, transparent 50%),
radial-gradient(circle at 80% 20%, rgba(26, 54, 93, 0.05) 0%, transparent 50%),
radial-gradient(circle at 40% 80%, rgba(44, 82, 130, 0.03) 0%, transparent 40%);
}
.container {
max-width: 1400px;
margin: 0 auto;
padding: 40px 20px;
}
header {
text-align: center;
margin-bottom: 50px;
animation: fadeInDown 0.8s ease-out;
}
@keyframes fadeInDown {
from {
opacity: 0;
transform: translateY(-30px);
}
to {
opacity: 1;
transform: translateY(0);
}
}
h1 {
font-family: 'Playfair Display', serif;
font-size: 3rem;
font-weight: 600;
color: var(--primary);
margin-bottom: 12px;
letter-spacing: -0.02em;
}
.subtitle {
color: var(--text-light);
font-size: 1.1rem;
font-weight: 300;
}
.toolbar {
display: flex;
gap: 16px;
justify-content: center;
flex-wrap: wrap;
margin-bottom: 40px;
animation: fadeIn 0.8s ease-out 0.2s both;
}
@keyframes fadeIn {
from { opacity: 0; }
to { opacity: 1; }
}
.btn {
padding: 14px 32px;
border: none;
border-radius: 12px;
font-size: 1rem;
font-weight: 500;
cursor: pointer;
transition: all 0.3s cubic-bezier(0.4, 0, 0.2, 1);
display: inline-flex;
align-items: center;
gap: 10px;
font-family: inherit;
}
.btn-primary {
background: linear-gradient(135deg, var(--primary) 0%, var(--primary-light) 100%);
color: white;
box-shadow: 0 4px 15px rgba(26, 54, 93, 0.3);
}
.btn-primary:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(26, 54, 93, 0.4);
}
.btn-accent {
background: linear-gradient(135deg, var(--accent) 0%, var(--accent-light) 100%);
color: white;
box-shadow: 0 4px 15px rgba(237, 137, 54, 0.3);
}
.btn-accent:hover {
transform: translateY(-2px);
box-shadow: 0 8px 25px rgba(237, 137, 54, 0.4);
}
.btn-secondary {
background: var(--card-bg);
color: var(--text);
border: 2px solid var(--border);
}
.btn-secondary:hover {
border-color: var(--primary);
color: var(--primary);
}
.btn:disabled {
opacity: 0.5;
cursor: not-allowed;
transform: none !important;
}
.btn svg {
width: 20px;
height: 20px;
}
.stats {
display: flex;
justify-content: center;
gap: 40px;
margin-bottom: 40px;
animation: fadeIn 0.8s ease-out 0.3s both;
}
.stat-item {
text-align: center;
}
.stat-value {
font-size: 2.5rem;
font-weight: 700;
color: var(--primary);
line-height: 1;
}
.stat-label {
color: var(--text-light);
font-size: 0.9rem;
margin-top: 8px;
}
/* 上传区域 */
.upload-zone {
background: var(--card-bg);
border: 3px dashed var(--border);
border-radius: 20px;
padding: 60px 40px;
text-align: center;
margin-bottom: 40px;
transition: all 0.3s ease;
cursor: pointer;
animation: fadeIn 0.8s ease-out 0.4s both;
}
.upload-zone:hover,
.upload-zone.dragover {
border-color: var(--accent);
background: rgba(237, 137, 54, 0.05);
}
.upload-zone.dragover {
transform: scale(1.02);
}
.upload-icon {
width: 80px;
height: 80px;
margin: 0 auto 20px;
color: var(--text-light);
opacity: 0.6;
}
.upload-title {
font-size: 1.4rem;
font-weight: 600;
color: var(--text);
margin-bottom: 10px;
}
.upload-hint {
color: var(--text-light);
font-size: 0.95rem;
}
.upload-hint span {
color: var(--accent);
font-weight: 500;
}
#fileInput {
display: none;
}
/* PDF列表 */
.pdf-grid {
display: grid;
grid-template-columns: repeat(auto-fill, minmax(280px, 1fr));
gap: 24px;
animation: fadeIn 0.8s ease-out 0.5s both;
}
.pdf-card {
background: var(--card-bg);
border-radius: 16px;
overflow: hidden;
box-shadow: var(--shadow);
transition: all 0.3s ease;
position: relative;
}
.pdf-card:hover {
transform: translateY(-4px);
box-shadow: var(--shadow-hover);
}
.pdf-preview {
width: 100%;
background: #f0f0f0;
display: flex;
align-items: center;
justify-content: center;
position: relative;
padding: 12px;
min-height: 220px;
}
.pdf-preview img {
width: 100%;
height: auto;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.pdf-preview canvas {
width: 100%;
height: auto;
display: block;
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.1);
}
.pdf-preview .loading {
color: var(--text-light);
font-size: 0.9rem;
}
.pdf-badge {
position: absolute;
top: 10px;
right: 10px;
background: var(--danger);
color: white;
padding: 4px 10px;
border-radius: 20px;
font-size: 0.75rem;
font-weight: 600;
}
.pdf-info {
padding: 16px;
}
.pdf-name {
font-weight: 600;
color: var(--text);
margin-bottom: 8px;
white-space: nowrap;
overflow: hidden;
text-overflow: ellipsis;
font-size: 0.95rem;
}
.pdf-meta {
display: flex;
justify-content: space-between;
color: var(--text-light);
font-size: 0.85rem;
}
.pdf-actions {
display: flex;
gap: 8px;
padding: 12px 16px;
border-top: 1px solid var(--border);
}
.btn-small {
padding: 8px 14px;
font-size: 0.85rem;
border-radius: 8px;
flex: 1;
}
.btn-delete {
background: transparent;
color: var(--danger);
border: 1px solid #fed7d7;
}
.btn-delete:hover {
background: #fff5f5;
border-color: #fc8181;
}
.empty-state {
text-align: center;
padding: 80px 20px;
color: var(--text-light);
grid-column: 1 / -1;
}
.empty-icon {
font-size: 4rem;
margin-bottom: 20px;
opacity: 0.5;
}
/* 打印样式 */
[url=home.php?mod=space&uid=945662]@media[/url] print {
body {
background: white;
}
.container,
.toolbar,
.stats,
header,
.upload-zone,
.pdf-grid,
.no-print {
display: none !important;
}
.print-container {
display: block !important;
}
.print-page {
page-break-after: always;
page-break-inside: avoid;
width: 297mm;
height: 210mm;
display: flex;
align-items: center;
justify-content: center;
padding: 0;
margin: 0;
box-sizing: border-box;
background: white;
}
.print-page:last-child {
page-break-after: auto;
}
.print-invoice {
width: 75%;
height: 75%;
display: flex;
align-items: center;
justify-content: center;
}
.print-invoice img,
.print-invoice canvas {
max-width: 100%;
max-height: 100%;
object-fit: contain;
}
[url=home.php?mod=space&uid=1953840]@page[/url] {
size: A4 landscape;
margin: 0;
}
}
.print-container {
display: none;
}
/* 进度条 */
.progress-bar {
width: 100%;
height: 4px;
background: var(--border);
border-radius: 2px;
overflow: hidden;
margin-top: 10px;
}
.progress-fill {
height: 100%;
background: linear-gradient(90deg, var(--primary), var(--accent));
transition: width 0.3s ease;
}
/* 响应式 */
@media (max-width: 768px) {
h1 {
font-size: 2rem;
}
.stats {
gap: 20px;
}
.stat-value {
font-size: 2rem;
}
.pdf-grid {
grid-template-columns: 1fr;
}
.upload-zone {
padding: 40px 20px;
}
}
</style>
</head>
<body>
<div class="container">
<header>
<h1>发票批量打印</h1>
<p class="subtitle">上传PDF发票原件,一键批量打印</p>
</header>
<div class="toolbar">
<button class="btn btn-primary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<path d="M21 15v4a2 2 0 0 1-2 2H5a2 2 0 0 1-2-2v-4"></path>
<polyline points="17 8 12 3 7 8"></polyline>
<line x1="12" y1="3" x2="12" y2="15"></line>
</svg>
上传PDF发票
</button>
<button class="btn btn-accent" id="printAllBtn" disabled>
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="6 9 6 2 18 2 18 9"></polyline>
<path d="M6 18H4a2 2 0 0 1-2-2v-5a2 2 0 0 1 2-2h16a2 2 0 0 1 2 2v5a2 2 0 0 1-2 2h-2"></path>
<rect x="6" y="14" width="12" height="8"></rect>
</svg>
批量打印
</button>
<button class="btn btn-secondary">
<svg viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="2">
<polyline points="3 6 5 6 21 6"></polyline>
<path d="M19 6v14a2 2 0 0 1-2 2H7a2 2 0 0 1-2-2V6m3 0V4a2 2 0 0 1 2-2h4a2 2 0 0 1 2 2v2"></path>
</svg>
清空全部
</button>
</div>
<div class="stats">
<div class="stat-item">
<div class="stat-value" id="pdfCount">0</div>
<div class="stat-label">发票数量</div>
</div>
<div class="stat-item">
<div class="stat-value" id="totalPages">0</div>
<div class="stat-label">总页数</div>
</div>
</div>
<div class="upload-zone" id="uploadZone">
<svg class="upload-icon" viewBox="0 0 24 24" fill="none" stroke="currentColor" stroke-width="1.5">
<path d="M14 2H6a2 2 0 0 0-2 2v16a2 2 0 0 0 2 2h12a2 2 0 0 0 2-2V8z"></path>
<polyline points="14 2 14 8 20 8"></polyline>
<line x1="12" y1="18" x2="12" y2="12"></line>
<line x1="9" y1="15" x2="15" y2="15"></line>
</svg>
<div class="upload-title">点击或拖拽上传PDF发票</div>
<div class="upload-hint">支持 <span>批量上传</span> 多个PDF文件</div>
</div>
<input type="file" id="fileInput" accept=".pdf" multiple>
<div class="pdf-grid" id="pdfGrid">
<div class="empty-state" id="emptyState">
<div class="empty-icon">📄</div>
<p>暂无发票,请上传PDF文件</p>
</div>
</div>
</div>
<!-- 打印区域 -->
<div class="print-container" id="printContainer"></div>
<script>
// 设置PDF.js worker
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/3.11.174/pdf.worker.min.js';
// PDF文件存储
let pdfFiles = [];
// 初始化
document.addEventListener('DOMContentLoaded', function () {
setupDragDrop();
});
// 设置拖拽上传
function setupDragDrop() {
const uploadZone = document.getElementById('uploadZone');
['dragenter', 'dragover', 'dragleave', 'drop'].forEach(eventName => {
uploadZone.addEventListener(eventName, preventDefaults, false);
});
function preventDefaults(e) {
e.preventDefault();
e.stopPropagation();
}
['dragenter', 'dragover'].forEach(eventName => {
uploadZone.addEventListener(eventName, () => {
uploadZone.classList.add('dragover');
}, false);
});
['dragleave', 'drop'].forEach(eventName => {
uploadZone.addEventListener(eventName, () => {
uploadZone.classList.remove('dragover');
}, false);
});
uploadZone.addEventListener('drop', handleDrop, false);
}
// 处理拖拽文件
function handleDrop(e) {
const dt = e.dataTransfer;
const files = dt.files;
handleFiles(files);
}
// 文件选择处理
document.getElementById('fileInput').addEventListener('change', function (e) {
handleFiles(e.target.files);
e.target.value = ''; // 清空以允许重复选择相同文件
});
// 处理文件
async function handleFiles(files) {
const pdfFilesArray = Array.from(files).filter(file => file.type === 'application/pdf');
if (pdfFilesArray.length === 0) {
alert('请选择PDF文件');
return;
}
for (const file of pdfFilesArray) {
await addPDFFile(file);
}
}
// 添加PDF文件
async function addPDFFile(file) {
const id = Date.now() + Math.random();
const pdfData = {
id: id,
name: file.name,
size: file.size,
file: file,
pageCount: 0,
preview: null,
pdfDoc: null
};
pdfFiles.push(pdfData);
renderPDFList();
// 读取PDF
try {
const arrayBuffer = await file.arrayBuffer();
const pdf = await pdfjsLib.getDocument({ data: arrayBuffer }).promise;
pdfData.pageCount = pdf.numPages;
pdfData.pdfDoc = pdf;
// 生成预览 - 使用更高清晰度,完整显示发票
const page = await pdf.getPage(1);
const scale = 2;
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
pdfData.preview = canvas.toDataURL('image/jpeg', 0.9);
renderPDFList();
updateStats();
} catch (error) {
console.error('PDF读取失败:', error);
alert(`文件 "${file.name}" 读取失败,请确保是有效的PDF文件`);
pdfFiles = pdfFiles.filter(p => p.id !== id);
renderPDFList();
}
}
// 渲染PDF列表
function renderPDFList() {
const grid = document.getElementById('pdfGrid');
if (pdfFiles.length === 0) {
grid.innerHTML = `
<div class="empty-state" id="emptyState">
<div class="empty-icon">📄</div>
<p>暂无发票,请上传PDF文件</p>
</div>
`;
document.getElementById('printAllBtn').disabled = true;
} else {
document.getElementById('printAllBtn').disabled = false;
grid.innerHTML = pdfFiles.map(pdf => `
<div class="pdf-card" data-id="${pdf.id}">
<div class="pdf-preview">
${pdf.preview
? `<img src="${pdf.preview}" alt="${pdf.name}">`
: '<div class="loading">加载中...</div>'
}
<span class="pdf-badge">PDF</span>
</div>
<div class="pdf-info">
<div class="pdf-name" title="${pdf.name}">${pdf.name}</div>
<div class="pdf-meta">
<span>${formatFileSize(pdf.size)}</span>
<span>${pdf.pageCount > 0 ? pdf.pageCount + '页' : '读取中...'}</span>
</div>
</div>
<div class="pdf-actions">
<button class="btn btn-small btn-secondary">打印预览</button>
<button class="btn btn-small btn-delete">删除</button>
</div>
</div>
`).join('');
}
updateStats();
}
// 更新统计
function updateStats() {
document.getElementById('pdfCount').textContent = pdfFiles.length;
const totalPages = pdfFiles.reduce((sum, pdf) => sum + pdf.pageCount, 0);
document.getElementById('totalPages').textContent = totalPages;
}
// 格式化文件大小
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];
}
// 删除PDF
function removePDF(id) {
pdfFiles = pdfFiles.filter(pdf => pdf.id !== id);
renderPDFList();
}
// 清空所有PDF
function clearAllPDFs() {
if (pdfFiles.length === 0) return;
if (confirm('确定要清空所有PDF文件吗?')) {
pdfFiles = [];
renderPDFList();
}
}
// 打印预览PDF
async function previewPDF(id) {
const pdfData = pdfFiles.find(p => p.id === id);
if (!pdfData || !pdfData.pdfDoc) return;
const printContainer = document.getElementById('printContainer');
printContainer.innerHTML = '';
// 渲染PDF所有页面到打印容器,横向A4 75%显示
for (let i = 1; i <= pdfData.pageCount; i++) {
const page = await pdfData.pdfDoc.getPage(i);
// A4横向: 297mm x 210mm, 75%大小
const a4Width = 297 * 0.75;
const a4Height = 210 * 0.75;
// 获取PDF页面尺寸(以点为单位,1点 = 1/72英寸)
const pdfWidth = page.view[2];
const pdfHeight = page.view[3];
// 计算缩放比例以适应75%的A4横向尺寸
const targetWidthPt = a4Width * 2.83;
const targetHeightPt = a4Height * 2.83;
const scaleX = targetWidthPt / pdfWidth;
const scaleY = targetHeightPt / pdfHeight;
const scale = Math.min(scaleX, scaleY) * 1.5;
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
const pageDiv = document.createElement('div');
pageDiv.className = 'print-page';
const invoiceDiv = document.createElement('div');
invoiceDiv.className = 'print-invoice';
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
invoiceDiv.appendChild(img);
pageDiv.appendChild(invoiceDiv);
printContainer.appendChild(pageDiv);
}
// 触发打印
setTimeout(() => {
window.print();
}, 500);
}
// 批量打印所有PDF
async function printAllPDFs() {
if (pdfFiles.length === 0) {
alert('没有可打印的PDF文件');
return;
}
// 检查是否所有PDF都已加载完成
const unloadedPDFs = pdfFiles.filter(pdf => !pdf.pdfDoc);
if (unloadedPDFs.length > 0) {
alert('部分PDF文件仍在加载中,请稍候...');
return;
}
const printContainer = document.getElementById('printContainer');
printContainer.innerHTML = '';
// 渲染所有PDF的所有页面
for (const pdfData of pdfFiles) {
for (let i = 1; i <= pdfData.pageCount; i++) {
const page = await pdfData.pdfDoc.getPage(i);
// A4横向: 297mm x 210mm, 75%大小
const a4Width = 297 * 0.75;
const a4Height = 210 * 0.75;
// 获取PDF页面尺寸(以点为单位,1点 = 1/72英寸)
const pdfWidth = page.view[2];
const pdfHeight = page.view[3];
// 计算缩放比例以适应75%的A4横向尺寸
// 1mm ≈ 2.83点
const targetWidthPt = a4Width * 2.83;
const targetHeightPt = a4Height * 2.83;
const scaleX = targetWidthPt / pdfWidth;
const scaleY = targetHeightPt / pdfHeight;
const scale = Math.min(scaleX, scaleY) * 1.5;
const viewport = page.getViewport({ scale: scale });
const canvas = document.createElement('canvas');
const context = canvas.getContext('2d');
canvas.width = viewport.width;
canvas.height = viewport.height;
await page.render({
canvasContext: context,
viewport: viewport
}).promise;
const pageDiv = document.createElement('div');
pageDiv.className = 'print-page';
const invoiceDiv = document.createElement('div');
invoiceDiv.className = 'print-invoice';
const img = document.createElement('img');
img.src = canvas.toDataURL('image/png');
invoiceDiv.appendChild(img);
pageDiv.appendChild(invoiceDiv);
printContainer.appendChild(pageDiv);
}
}
// 触发打印
setTimeout(() => {
window.print();
}, 500);
}
</script>
</body>
</html>