吾爱破解 - 52pojie.cn

 找回密码
 注册[Register]

QQ登录

只需一步,快速开始

查看: 1640|回复: 13
收起左侧

[其他转载] 利用Deepseek+qoder实现一个html解决简单标签设计和PDF图片合并打印

[复制链接]
kissfox 发表于 2026-4-17 14:34
公司销售部及采购部一直在使用网上下载的PDF合并工具来实现发票打印,可由于公司对网上下载的工具安全性考虑,特想着结合deepseek+qoder做一个html网页来实现普通标签设计和PDF合并、图片合并打印,具体功能如下图
微信图片_2026-04-17_143324_026.png 微信图片_2026-04-17_143328_472.png 微信图片_2026-04-17_143331_449.png
[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>简单标签设计+PDF图片合并打印</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,[email]wght@14..32[/email],300;14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); font-family: 'Inter', sans-serif; padding: 24px; min-height: 100vh; }
        .app-wrapper { max-width: 1600px; margin: 0 auto; }
        .main-nav { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 60px; padding: 8px 16px; margin-bottom: 24px; display: flex; gap: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); }
        .nav-tab { padding: 12px 32px; border-radius: 48px; background: transparent; border: none; font-weight: 600; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; color: #4a5568; font-family: 'Inter', sans-serif; }
        .nav-tab:hover { background: rgba(102, 126, 234, 0.1); color: #667eea; }
        .nav-tab.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); }
        .page { display: none; background: white; border-radius: 32px; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); animation: fadeInUp 0.4s ease; }
        @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
        .page.active-page { display: block; }
        .page-header { padding: 20px 28px; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; }
        .page-header h2 { font-size: 1.5rem; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
        .toolbar { background: #fefefe; padding: 20px 28px; display: flex; flex-wrap: wrap; gap: 16px; border-bottom: 1px solid #eef2f6; }
        .tool-group { background: #ffffff; border-radius: 24px; padding: 12px 18px; border: 1px solid #eef2f8; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); transition: all 0.2s ease; min-width: 150px; }
        .tool-group:hover { border-color: #cbd5e1; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); }
        .tool-group label { font-size: 0.7rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; display: block; }
        .tool-group select, .tool-group input, .tool-group button { border-radius: 40px; border: 1px solid #e2e8f0; padding: 8px 16px; font-size: 0.85rem; background: white; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif; }
        .tool-group select:hover, .tool-group input:hover { border-color: #cbd5e1; }
        .tool-group button.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; font-weight: 500; }
        .tool-group button.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
        .inline-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .canvas-area { background: linear-gradient(145deg, #e2e8f0 0%, #cbd5e6 100%); padding: 28px; display: flex; justify-content: center; position: relative; }
        .canvas-wrapper { background: #ffffff; border-radius: 20px; box-shadow: 0 20px 35px -10px rgba(0, 0, 0, 0.2); display: inline-block; overflow: auto; max-width: 100%; }
        canvas { display: block; margin: 0 auto; border-radius: 12px; cursor: crosshair; }
        .info-panel { background: #f8fafc; padding: 16px 28px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; border-top: 1px solid #eef2f8; font-size: 0.85rem; color: #475569; }
        .btn-export { background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; color: white; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .btn-print { background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); border: none; color: white; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .btn-export:hover, .btn-print:hover, .nav-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); }
        .nav-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .file-list { max-height: 180px; overflow-y: auto; background: #f1f5f9; border-radius: 16px; padding: 8px; margin-top: 8px; }
        .file-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: white; margin: 6px 0; border-radius: 40px; border: 1px solid #e2e8f0; font-size: 0.8rem; }
        .remove-file-btn { background: #ef4444; color: white; border: none; border-radius: 40px; padding: 4px 16px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
        .remove-file-btn:hover { background: #dc2626; transform: scale(1.02); }
        .page-controls { display: flex; gap: 12px; align-items: center; background: white; padding: 6px 20px; border-radius: 60px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
        .page-btn { background: #e2e8f0; color: #1e293b; border: none; padding: 6px 16px; border-radius: 40px; cursor: pointer; transition: all 0.2s; font-weight: 500; }
        .page-btn:hover:not(:disabled) { background: #cbd5e1; }
        .page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
        .size-input { width: 80px; text-align: center; font-weight: 500; }
        .badge { display: inline-block; background: #e2e8f0; border-radius: 20px; padding: 2px 8px; font-size: 10px; margin-left: 8px; }
        ::-webkit-scrollbar { width: 8px; height: 8px; }
        ::-webkit-scrollbar-track { background: #f1f5f9; border-radius: 10px; }
        ::-webkit-scrollbar-thumb { background: #cbd5e1; border-radius: 10px; }
        ::-webkit-scrollbar-thumb:hover { background: #94a3b8; }
        [url=home.php?mod=space&uid=945662]@media[/url] (max-width: 780px) { body { padding: 12px; } .toolbar { flex-direction: column; } .tool-group { width: 100%; } .page-header { flex-direction: column; gap: 12px; } }
    </style>
</head>
<body>
<div class="app-wrapper">
    <div class="main-nav">
        <button class="nav-tab active" data-page="single">&#127912; 单页设计窗口</button>
        <button class="nav-tab" data-page="layout">&#128450;&#65039; 整版预览打印</button>
        <button class="nav-tab" data-page="pdf">&#128209; PDF/图片合并打印</button>
    </div>

    <!-- 单页设计 -->
    <div id="singlePagePanel" class="page active-page">
        <div class="page-header">
            <h2>&#127912; 单页设计 · 自由创作</h2>
            <button id="saveAndGoToLayoutBtn" class="nav-btn">&#128190; 保存并进入整版预览 →</button>
        </div>
        <div class="toolbar">
            <div class="tool-group"><label>&#128208; 纸张预设</label><select id="paperPreset"><option value="custom">自定义尺寸</option><option value="A5">A5 (148×210mm)</option></select><button id="applyPresetBtn" class="primary">应用</button><div class="inline-row" style="margin-top: 8px;"><input type="number" id="customWidthMM" value="100" step="1" class="size-input" placeholder="宽"> mm<span>×</span><input type="number" id="customHeightMM" value="150" step="1" class="size-input" placeholder="高"> mm<button id="applySizeBtn" class="primary">应用</button></div></div>
            <div class="tool-group"><label>&#128444;&#65039; 插入图片</label><input type="file" id="singleImageUpload" accept="image/jpeg,image/png,image/webp"></div>
            <div class="tool-group"><label>&#128209; 插入PDF</label><input type="file" id="singlePdfUpload" accept="application/pdf"></div>
            <div class="tool-group"><label>&#9997;&#65039; 添加文字</label><div class="inline-row"><input type="text" id="singleTextInput" placeholder="文字内容" value="设计稿"><button id="singleAddTextBtn" class="primary">添加</button></div></div>
            <div class="tool-group"><label>&#128207; 绘制线条</label><div class="inline-row"><button id="singleHorLineBtn" style="background:#8b5cf6; color:white; border:none;">━ 横线</button><button id="singleVerLineBtn" style="background:#8b5cf6; color:white; border:none;">┃ 竖线</button></div><div class="inline-row"><input type="color" id="singleLineColor" value="#e11d48"><input type="number" id="singleLineWidth" value="2" step="0.5" style="width:65px;"></div></div>
            <div class="tool-group"><label>&#9881;&#65039; 辅助功能</label><label style="display: flex; align-items: center; gap: 8px;"><input type="checkbox" id="singleShowGrid" checked> 显示参考网格</label><button id="singleResetBtn" style="background:#f97316; color:white; border:none;">&#128465;&#65039; 清空画板</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="singleCanvas"></canvas></div></div>
        <div class="info-panel"><div>&#128161; 右键点击元素可删除 | 支持自定义尺寸和A5纸张</div><div style="display: flex; gap: 12px;"><button id="singlePrintBtn" class="btn-print">&#128424;&#65039; 直接打印</button><button id="singleExportBtn" class="btn-export">&#128248; 导出PNG</button></div></div>
    </div>

    <!-- PDF/图片合并(支持图片和PDF混合上传) -->
    <div id="pdfMergePanel" class="page">
        <div class="page-header"><h2>&#128209; PDF/图片合并 · 多页拼版打印</h2></div>
        <div class="toolbar">
            <div class="tool-group"><label>&#128206; 上传文件 (PDF/图片)</label><input type="file" id="fileUploadInput" accept="image/jpeg,image/png,image/webp,application/pdf" multiple><div style="font-size: 11px; color: #64748b; margin-top: 4px;">支持 JPG、PNG、PDF,混合上传自动拼版</div></div>
            <div class="tool-group"><label>&#129513; 每页拼版数量</label><div class="inline-row"><select id="pdfLayoutRows"><option value="1">1行</option><option value="2">2行</option><option value="3" selected>3行</option><option value="4">4行</option><option value="5">5行</option><option value="6">6行</option></select><span>×</span><select id="pdfLayoutCols"><option value="1">1列</option><option value="2">2列</option><option value="3" selected>3列</option><option value="4">4列</option><option value="5">5列</option><option value="6">6列</option></select><button id="applyPdfLayoutBtn" class="primary">应用</button></div></div>
            <div class="tool-group"><label>&#128207; 页边距 (mm)</label><input type="range" id="pdfMarginSlider" min="0" max="18" step="0.5" value="5"><span id="pdfMarginValue">5 mm</span></div>
            <div class="tool-group"><button id="clearFileListBtn" style="background:#ef4444; color:white; border:none;">清空所有文件</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="pdfMergeCanvas" width="1240" height="1754"></canvas></div></div>
        <div class="info-panel">
            <div class="inline-row"><span>&#128196; 已上传: <strong id="fileCountDisplay" style="color:#667eea;">0</strong> 个文件 | 总页数: <strong id="totalPagesDisplay" style="color:#667eea;">0</strong> 页A4</span><div class="page-controls"><button id="prevPageBtn" class="page-btn">&#9664; 上一页</button><span id="currentPageSpan" style="font-weight: 500;">第 1 / 1 页</span><button id="nextPageBtn" class="page-btn">下一页 &#9654;</button></div></div>
            <div id="fileList" class="file-list"></div>
            <div style="display: flex; gap: 12px;"><button id="pdfPrintAllBtn" class="btn-print">&#128424;&#65039; 直接打印全部</button><button id="pdfExportAllBtn" class="btn-export">&#128248; 导出全部PNG</button></div>
        </div>
    </div>

    <!-- 整版预览 -->
    <div id="layoutPanel" class="page">
        <div class="page-header"><h2>&#128450;&#65039; 整版预览 · A4拼版输出</h2><button id="backToDesignBtn" class="nav-btn" style="background:#64748b;">← 返回单页设计</button></div>
        <div class="toolbar">
            <div class="tool-group"><label>&#129513; 拼版数量</label><div class="inline-row"><select id="layoutRows"><option>1</option><option>2</option><option selected>3</option><option>4</option><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select><span>×</span><select id="layoutCols"><option>1</option><option>2</option><option selected>3</option><option>4</option><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select><button id="applyLayoutBtn" class="primary">生成拼版</button></div><div style="font-size: 12px; margin-top: 6px;">共 <strong id="totalCellsCount" style="color:#667eea;">9</strong> 个页面</div></div>
            <div class="tool-group"><label>&#128207; 页边距 (mm)</label><input type="range" id="layoutMarginSlider" min="0" max="18" step="0.5" value="5"><span id="layoutMarginValue">5 mm</span><label style="display: flex; align-items: center; gap: 8px; margin-top: 6px;"><input type="checkbox" id="layoutShowCrop" checked> &#9986;&#65039; 显示裁剪线</label></div>
            <div class="tool-group"><button id="resetLayoutBtn" style="background:#ef4444; color:white; border:none;">重置拼版内容</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="layoutCanvas" width="1240" height="1754"></canvas></div></div>
        <div class="info-panel"><div>&#10024; 基于单页设计内容,自动缩放并完美拼版到A4纸</div><div style="display: flex; gap: 12px;"><button id="layoutPrintBtn" class="btn-print">&#128424;&#65039; 直接打印</button><button id="layoutExportBtn" class="btn-export">&#128248; 导出PNG</button></div></div>
    </div>
</div>

<script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
    const A4_W_PX = 1240, A4_H_PX = 1754;
    const MM_TO_PX = 150 / 25.4;
    
    let singleCanvas = null;
    let singleWidthMM = 100, singleHeightMM = 150, singleWidthPX = 0, singleHeightPX = 0;
    let singleLineColor = "#e11d48", singleLineWidth = 2, singleShowGridFlag = true;
    let savedSingleObjectsJSON = [], savedSingleSizeMM = { w: 100, h: 150 };
    
    let layoutCanvas = null;
    let currentRows = 3, currentCols = 3, currentMarginMM = 5, showCropLines = true, layoutUserObjects = [];
    
    // 合并页面变量 - 支持图片和PDF
    let mergeCanvas = null;
    let uploadedFiles = [];  // 存储 { name, type, data, width, height }
    let mergeLayoutRows = 3, mergeLayoutCols = 3, mergeMarginMM = 5;
    let mergePagesData = [];
    let currentPageIndex = 0;
    let totalMergePages = 0;
    
    // 打印函数
    function directPrint(canvasInstance, widthMM, heightMM, title) {
        if (!canvasInstance) return;
        const offscreenCanvas = document.createElement('canvas');
        offscreenCanvas.width = A4_W_PX;
        offscreenCanvas.height = A4_H_PX;
        const ctx = offscreenCanvas.getContext('2d');
        const originalCanvas = canvasInstance.getElement();
        ctx.drawImage(originalCanvas, 0, 0, A4_W_PX, A4_H_PX);
        const imgDataURL = offscreenCanvas.toDataURL('image/png');
        
        const printWindow = window.open('', '_blank', 'width=800,height=600,toolbar=yes,menubar=yes,scrollbars=yes');
        if (!printWindow) { alert("请允许弹出窗口以进行打印"); return; }
        
        printWindow.document.write(`<!DOCTYPE html><html><head><title>${title}</title><style>*{margin:0;padding:0;} @page{size:A4;margin:0;} body{display:flex;justify-content:center;align-items:center;min-height:100vh;background:white;} img{width:${widthMM}mm;height:${heightMM}mm;}</style></head><body><img src="${imgDataURL}"><script>window.onload=function(){window.print();window.onafterprint=function(){window.close();};};<\/script></body></html>`);
        printWindow.document.close();
    }
    
    function directPrintMultiPage(pageDataURLs, title) {
        if (!pageDataURLs || pageDataURLs.length === 0) { alert("没有可打印的内容"); return; }
        const printWindow = window.open('', '_blank', 'width=800,height=600,toolbar=yes,menubar=yes,scrollbars=yes');
        if (!printWindow) { alert("请允许弹出窗口以进行打印"); return; }
        let html = `<!DOCTYPE html><html><head><title>${title}</title><style>*{margin:0;padding:0;} @page{size:A4;margin:0;} .page{page-break-after:always;display:flex;justify-content:center;align-items:center;min-height:297mm;background:white;} img{width:210mm;height:297mm;display:block;}</style></head><body>`;
        for (let i = 0; i < pageDataURLs.length; i++) { html += `<div class="page"><img src="${pageDataURLs[i]}" /></div>`; }
        html += `<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();};};<\/script></body></html>`;
        printWindow.document.write(html);
        printWindow.document.close();
    }
    
    // 单页设计模块
    function initSingleCanvas() {
        singleWidthPX = Math.round(100 * MM_TO_PX); singleHeightPX = Math.round(150 * MM_TO_PX);
        singleCanvas = new fabric.Canvas('singleCanvas', { width: singleWidthPX, height: singleHeightPX, selection: true });
        drawSingleBackground(); setupContextMenu(singleCanvas); addTextToSingle("自定义尺寸\n设计内容"); saveSingleDesign();
    }
    function setupContextMenu(canvas) {
        canvas.on('mouse:down', e => { if(e.button===2 && e.target && !e.target.data?.isBackground) { canvas.remove(e.target); canvas.renderAll(); e.e.preventDefault(); } });
        canvas.wrapperEl.addEventListener('contextmenu', e => e.preventDefault());
    }
    function drawSingleBackground() {
        if(!singleCanvas) return;
        singleCanvas.getObjects().filter(o=>o.data?.isBackground).forEach(o=>singleCanvas.remove(o));
        singleCanvas.add(new fabric.Rect({ left:0,top:0,width:singleCanvas.width,height:singleCanvas.height,fill:'#fff',selectable:false,evented:false,data:{isBackground:true}}));
        if(singleShowGridFlag) {
            for(let x=0;x<=singleCanvas.width;x+=40) singleCanvas.add(new fabric.Line([x,0,x,singleCanvas.height],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false,data:{isBackground:true}}));
            for(let y=0;y<=singleCanvas.height;y+=40) singleCanvas.add(new fabric.Line([0,y,singleCanvas.width,y],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false,data:{isBackground:true}}));
        }
        singleCanvas.renderAll();
    }
    function updateSingleCanvasSize() {
        singleWidthPX = Math.round(singleWidthMM * MM_TO_PX); singleHeightPX = Math.round(singleHeightMM * MM_TO_PX);
        const objs = singleCanvas.getObjects().filter(o=>!o.data?.isBackground);
        const oldCx = singleCanvas.width/2, oldCy = singleCanvas.height/2;
        singleCanvas.setWidth(singleWidthPX); singleCanvas.setHeight(singleHeightPX); singleCanvas.calcOffset();
        const newCx = singleWidthPX/2, newCy = singleHeightPX/2;
        objs.forEach(o=>{ o.set({left:o.left+(newCx-oldCx),top:o.top+(newCy-oldCy)}); o.setCoords(); });
        drawSingleBackground();
        objs.forEach(o=>singleCanvas.add(o));
        singleCanvas.renderAll();
    }
    function addTextToSingle(t) { if(!t.trim()) t="文字"; let o=new fabric.IText(t,{fontSize:Math.min(48,singleCanvas.height*0.08),fill:'#1f2937',backgroundColor:'#fef9c3',padding:8,editable:true,left:singleCanvas.width/2-60,top:singleCanvas.height/2-20}); singleCanvas.add(o); singleCanvas.setActiveObject(o); singleCanvas.renderAll(); }
    function addImageToSingle(f){let r=new FileReader(); r.onload=e=>{fabric.Image.fromURL(e.target.result,img=>{let s=Math.min(singleCanvas.width*0.6/img.width,singleCanvas.height*0.6/img.height,1); img.scale(s); img.set({left:singleCanvas.width/2-(img.width*s)/2,top:singleCanvas.height/2-(img.height*s)/2}); singleCanvas.add(img); singleCanvas.renderAll();})}; r.readAsDataURL(f);}
    async function addPdfToSingle(f){let r=new FileReader(); r.onload=async e=>{let arr=new Uint8Array(e.target.result); try{let pdf=await pdfjsLib.getDocument({data:arr}).promise; let page=await pdf.getPage(1); let vp=page.getViewport({scale:1.5}); let c=document.createElement('canvas'); c.width=vp.width; c.height=vp.height; await page.render({canvasContext:c.getContext('2d'),viewport:vp}).promise; fabric.Image.fromURL(c.toDataURL(),img=>{let s=Math.min(singleCanvas.width*0.7/img.width,singleCanvas.height*0.7/img.height,1); img.scale(s); img.set({left:singleCanvas.width/2-(img.width*s)/2,top:singleCanvas.height/2-(img.height*s)/2}); singleCanvas.add(img); singleCanvas.renderAll();})}catch(e){alert("PDF失败")}}; r.readAsArrayBuffer(f);}
    function createLineOnSingle(h){let l=Math.min(singleCanvas.width,singleCanvas.height)*0.3; let line=h?new fabric.Line([singleCanvas.width/2-l/2,singleCanvas.height/2,singleCanvas.width/2+l/2,singleCanvas.height/2],{stroke:singleLineColor,strokeWidth:singleLineWidth}):new fabric.Line([singleCanvas.width/2,singleCanvas.height/2-l/2,singleCanvas.width/2,singleCanvas.height/2+l/2],{stroke:singleLineColor,strokeWidth:singleLineWidth}); singleCanvas.add(line); singleCanvas.renderAll();}
    function clearSingleCanvas(){let bg=singleCanvas.getObjects().filter(o=>o.data?.isBackground); singleCanvas.clear(); bg.forEach(b=>singleCanvas.add(b)); singleCanvas.renderAll();}
    function saveSingleDesign(){savedSingleObjectsJSON=singleCanvas.getObjects().filter(o=>!o.data?.isBackground).map(o=>o.toJSON()); savedSingleSizeMM={w:singleWidthMM,h:singleHeightMM}; return savedSingleObjectsJSON.length>0;}
    
    // 整版拼版
    function getLayoutParams(w,h,m,r,c){let aw=w-m*2, ah=h-m*2; return {cellW:aw/c,cellH:ah/r,startX:m,startY:m};}
    function getCellCenter(r,c,m,rows,cols,w,h){let {cellW,cellH,startX,startY}=getLayoutParams(w,h,m,rows,cols); return {x:startX+c*cellW+cellW/2,y:startY+r*cellH+cellH/2};}
    function drawLayoutBackground(){if(!layoutCanvas)return; layoutCanvas.getObjects().filter(o=>o.data?.isBackground).forEach(o=>layoutCanvas.remove(o)); layoutCanvas.add(new fabric.Rect({left:0,top:0,width:A4_W_PX,height:A4_H_PX,fill:'#fff',selectable:false,evented:false,data:{isBackground:true}})); let m=currentMarginMM*MM_TO_PX; let {cellW,cellH,startX,startY}=getLayoutParams(A4_W_PX,A4_H_PX,m,currentRows,currentCols); for(let c=0;c<=currentCols;c++){let x=startX+c*cellW; if(x>=m&&x<=A4_W_PX-m) layoutCanvas.add(new fabric.Line([x,startY,x,startY+currentRows*cellH],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false,data:{isBackground:true}}));} for(let r=0;r<=currentRows;r++){let y=startY+r*cellH; if(y>=m&&y<=startY+currentRows*cellH) layoutCanvas.add(new fabric.Line([startX,y,startX+currentCols*cellW,y],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false,data:{isBackground:true}}));} if(showCropLines){let crop="#d97706",len=12; [[m,m],[A4_W_PX-m,m],[m,A4_H_PX-m],[A4_W_PX-m,A4_H_PX-m]].forEach(([x,y],i)=>{layoutCanvas.add(new fabric.Line([x,y,x+len,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}})); layoutCanvas.add(new fabric.Line([x,y,x,y+len],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}})); if(i===1||i===3){layoutCanvas.add(new fabric.Line([x-len,y,x,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}}));} if(i===2||i===3){layoutCanvas.add(new fabric.Line([x,y-len,x,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}}));} });} layoutCanvas.renderAll();}
    async function generateImposition(){if(!layoutCanvas)return; if(!savedSingleObjectsJSON.length){alert("请先设计单页内容并保存");return;} layoutUserObjects.forEach(o=>layoutCanvas.remove(o)); layoutUserObjects=[]; let m=currentMarginMM*MM_TO_PX; let total=currentRows*currentCols; let sw_mm=savedSingleSizeMM.w, sh_mm=savedSingleSizeMM.h; let tw=(A4_W_PX-m*2)/currentCols, th=(A4_H_PX-m*2)/currentRows; let scale=Math.min(tw/(sw_mm*MM_TO_PX), th/(sh_mm*MM_TO_PX)); drawLayoutBackground(); for(let i=0;i<total;i++){let r=Math.floor(i/currentCols),c=i%currentCols; let center=getCellCenter(r,c,m,currentRows,currentCols,A4_W_PX,A4_H_PX); for(let json of savedSingleObjectsJSON){let cloned=await new Promise(res=>fabric.util.enlivenObjects([json],objs=>res(objs[0]))); if(cloned){let srcCx=(sw_mm*MM_TO_PX)/2, srcCy=(sh_mm*MM_TO_PX)/2; let dx=cloned.left-srcCx, dy=cloned.top-srcCy; cloned.scaleX=(cloned.scaleX||1)*scale; cloned.scaleY=(cloned.scaleY||1)*scale; cloned.set({left:center.x+dx*scale,top:center.y+dy*scale}); cloned.setCoords(); layoutCanvas.add(cloned); layoutUserObjects.push(cloned);}}} layoutCanvas.renderAll(); document.getElementById('totalCellsCount').innerText=total;}
    
    // ========== 合并模块:支持图片和PDF混合上传 ==========
    async function addFileToMerge(file) {
        const fileType = file.type;
        const fileName = file.name;
        
        if (fileType.startsWith('image/')) {
            // 处理图片
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.onload = () => {
                    uploadedFiles.push({ name: fileName, type: 'image', data: e.target.result, width: img.width, height: img.height });
                    updateFileList();
                    rebuildAllMergePages();
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        } else if (fileType === 'application/pdf') {
            // 处理PDF
            const reader = new FileReader();
            reader.onload = async (e) => {
                const typedarray = new Uint8Array(e.target.result);
                try {
                    const pdf = await pdfjsLib.getDocument({ data: typedarray }).promise;
                    const page = await pdf.getPage(1);
                    const viewport = page.getViewport({ scale: 1.5 });
                    const canvasTemp = document.createElement('canvas');
                    const context = canvasTemp.getContext('2d');
                    canvasTemp.width = viewport.width;
                    canvasTemp.height = viewport.height;
                    await page.render({ canvasContext: context, viewport: viewport }).promise;
                    const imgData = canvasTemp.toDataURL();
                    uploadedFiles.push({ name: fileName, type: 'pdf', data: imgData, width: viewport.width, height: viewport.height });
                    updateFileList();
                    await rebuildAllMergePages();
                } catch(err) { alert(`PDF解析失败: ${fileName}`); }
            };
            reader.readAsArrayBuffer(file);
        } else {
            alert(`不支持的文件类型: ${fileName}`);
        }
    }
    
    async function rebuildAllMergePages() {
        if (!mergeCanvas) return;
        const perPage = mergeLayoutRows * mergeLayoutCols;
        totalMergePages = Math.ceil(uploadedFiles.length / perPage);
        if (totalMergePages === 0) totalMergePages = 1;
        document.getElementById('totalPagesDisplay').innerText = totalMergePages;
        mergePagesData = [];
        const marginPx = mergeMarginMM * MM_TO_PX;
        
        for (let pageIdx = 0; pageIdx < totalMergePages; pageIdx++) {
            const startIdx = pageIdx * perPage;
            const endIdx = Math.min(startIdx + perPage, uploadedFiles.length);
            const tempCanvas = new fabric.Canvas(null, { width: A4_W_PX, height: A4_H_PX });
            tempCanvas.add(new fabric.Rect({ left:0, top:0, width:A4_W_PX, height:A4_H_PX, fill:'#ffffff', selectable:false, evented:false }));
            const { cellW, cellH, startX, startY } = getLayoutParams(A4_W_PX, A4_H_PX, marginPx, mergeLayoutRows, mergeLayoutCols);
            for(let c=0; c<=mergeLayoutCols; c++) { let x=startX+c*cellW; if(x>=marginPx&&x<=A4_W_PX-marginPx) tempCanvas.add(new fabric.Line([x,startY,x,startY+mergeLayoutRows*cellH],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false})); }
            for(let r=0; r<=mergeLayoutRows; r++) { let y=startY+r*cellH; if(y>=startY&&y<=startY+mergeLayoutRows*cellH) tempCanvas.add(new fabric.Line([startX,y,startX+mergeLayoutCols*cellW,y],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false})); }
            
            for (let i = startIdx; i < endIdx; i++) {
                const localIdx = i - startIdx;
                const row = Math.floor(localIdx / mergeLayoutCols);
                const col = localIdx % mergeLayoutCols;
                const center = getCellCenter(row, col, marginPx, mergeLayoutRows, mergeLayoutCols, A4_W_PX, A4_H_PX);
                const fileData = uploadedFiles[i].data;
                const img = await new Promise(resolve => fabric.Image.fromURL(fileData, resolve));
                if (img) {
                    const targetW = (A4_W_PX - marginPx * 2) / mergeLayoutCols;
                    const targetH = (A4_H_PX - marginPx * 2) / mergeLayoutRows;
                    const scale = Math.min(targetW / img.width, targetH / img.height);
                    img.scale(scale);
                    img.set({ left: center.x - (img.width * scale)/2, top: center.y - (img.height * scale)/2 });
                    tempCanvas.add(img);
                }
            }
            tempCanvas.renderAll();
            mergePagesData.push(tempCanvas.toDataURL('png'));
            tempCanvas.dispose();
        }
        currentPageIndex = 0;
        await displayMergeCurrentPage();
        updateMergePageControls();
    }
    
    async function displayMergeCurrentPage() {
        if (!mergeCanvas || mergePagesData.length === 0) { mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o)); mergeCanvas.renderAll(); return; }
        const imgData = mergePagesData[currentPageIndex];
        mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o));
        mergeCanvas.add(new fabric.Rect({ left:0, top:0, width:A4_W_PX, height:A4_H_PX, fill:'#ffffff', selectable:false, evented:false, data:{isBackground:true} }));
        const img = await new Promise(resolve => fabric.Image.fromURL(imgData, resolve));
        if (img) { img.set({ left: 0, top: 0, width: A4_W_PX, height: A4_H_PX, selectable: false, evented: false }); mergeCanvas.add(img); }
        mergeCanvas.renderAll();
    }
    
    function updateMergePageControls() {
        document.getElementById('currentPageSpan').innerHTML = `第 ${currentPageIndex+1} / ${totalMergePages} 页`;
        document.getElementById('prevPageBtn').disabled = (currentPageIndex === 0);
        document.getElementById('nextPageBtn').disabled = (currentPageIndex >= totalMergePages - 1);
    }
    function prevMergePage() { if (currentPageIndex > 0) { currentPageIndex--; displayMergeCurrentPage(); updateMergePageControls(); } }
    function nextMergePage() { if (currentPageIndex < totalMergePages - 1) { currentPageIndex++; displayMergeCurrentPage(); updateMergePageControls(); } }
    
    function updateFileList() {
        const container = document.getElementById('fileList');
        const countSpan = document.getElementById('fileCountDisplay');
        countSpan.innerText = uploadedFiles.length;
        container.innerHTML = uploadedFiles.map((file, idx) => `<div class="file-item"><span>${file.type === 'image' ? '&#128444;&#65039;' : '&#128209;'} ${file.name.substring(0, 35)}<span class="badge">${file.type === 'image' ? '图片' : 'PDF'}</span></span><button class="remove-file-btn" data-idx="${idx}">删除</button></div>`).join('');
        document.querySelectorAll('.remove-file-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const idx = parseInt(btn.dataset.idx);
                uploadedFiles.splice(idx, 1);
                updateFileList();
                rebuildAllMergePages();
            });
        });
    }
    
    function drawMergeBackground() { if(!mergeCanvas) return; mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o)); mergeCanvas.add(new fabric.Rect({left:0,top:0,width:A4_W_PX,height:A4_H_PX,fill:'#fff',selectable:false,evented:false})); mergeCanvas.renderAll(); }
    
    function switchPage(pageId) {
        document.getElementById('singlePagePanel').classList.remove('active-page');
        document.getElementById('pdfMergePanel').classList.remove('active-page');
        document.getElementById('layoutPanel').classList.remove('active-page');
        if (pageId === 'single') document.getElementById('singlePagePanel').classList.add('active-page');
        else if (pageId === 'pdf') { document.getElementById('pdfMergePanel').classList.add('active-page'); if(mergeCanvas && mergePagesData.length===0 && uploadedFiles.length===0) drawMergeBackground(); }
        else if (pageId === 'layout') { document.getElementById('layoutPanel').classList.add('active-page'); generateImposition(); }
        document.querySelectorAll('.nav-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.page === pageId); });
    }
    
    function bindEvents() {
        document.querySelectorAll('.nav-tab').forEach(btn => btn.addEventListener('click', () => switchPage(btn.dataset.page)));
        document.getElementById('applyPresetBtn').onclick = () => { if(document.getElementById('paperPreset').value==='A5'){singleWidthMM=148;singleHeightMM=210;updateSingleCanvasSize();saveSingleDesign();alert("已切换A5");} };
        document.getElementById('applySizeBtn').onclick = () => { let w=parseFloat(document.getElementById('customWidthMM').value),h=parseFloat(document.getElementById('customHeightMM').value); if(w>0&&h>0){singleWidthMM=w;singleHeightMM=h;updateSingleCanvasSize();saveSingleDesign();alert(`尺寸改为${w}×${h}mm`);} };
        document.getElementById('singleAddTextBtn').onclick = () => addTextToSingle(document.getElementById('singleTextInput').value||"文字");
        document.getElementById('singleImageUpload').onchange = e => { if(e.target.files[0]) addImageToSingle(e.target.files[0]); e.target.value=''; };
        document.getElementById('singlePdfUpload').onchange = e => { if(e.target.files[0]) addPdfToSingle(e.target.files[0]); e.target.value=''; };
        document.getElementById('singleHorLineBtn').onclick = () => { singleLineColor=document.getElementById('singleLineColor').value; singleLineWidth=parseFloat(document.getElementById('singleLineWidth').value); createLineOnSingle(true); };
        document.getElementById('singleVerLineBtn').onclick = () => { singleLineColor=document.getElementById('singleLineColor').value; singleLineWidth=parseFloat(document.getElementById('singleLineWidth').value); createLineOnSingle(false); };
        document.getElementById('singleResetBtn').onclick = () => clearSingleCanvas();
        document.getElementById('singleExportBtn').onclick = () => { let a=document.createElement('a'); a.download='single_design.png'; a.href=singleCanvas.toDataURL('png'); a.click(); };
        document.getElementById('singlePrintBtn').onclick = () => directPrint(singleCanvas, singleWidthMM, singleHeightMM, "单页设计");
        document.getElementById('singleShowGrid').onchange = e => { singleShowGridFlag=e.target.checked; drawSingleBackground(); singleCanvas.getObjects().filter(o=>!o.data?.isBackground).forEach(o=>singleCanvas.add(o)); singleCanvas.renderAll(); };
        document.getElementById('saveAndGoToLayoutBtn').onclick = async () => { if(saveSingleDesign()){ alert("已保存"); switchPage('layout'); await generateImposition(); } else alert("请先添加内容"); };
        document.getElementById('backToDesignBtn').onclick = () => switchPage('single');
        document.getElementById('layoutMarginSlider').oninput = async e => { currentMarginMM=parseFloat(e.target.value); document.getElementById('layoutMarginValue').innerText=currentMarginMM+" mm"; await generateImposition(); };
        document.getElementById('layoutShowCrop').onchange = async e => { showCropLines=e.target.checked; await generateImposition(); };
        document.getElementById('applyLayoutBtn').onclick = async () => { currentRows=parseInt(document.getElementById('layoutRows').value); currentCols=parseInt(document.getElementById('layoutCols').value); await generateImposition(); };
        document.getElementById('resetLayoutBtn').onclick = () => { layoutUserObjects.forEach(o=>layoutCanvas.remove(o)); layoutUserObjects=[]; drawLayoutBackground(); layoutCanvas.renderAll(); };
        document.getElementById('layoutExportBtn').onclick = () => { let a=document.createElement('a'); a.download='full_imposition.png'; a.href=layoutCanvas.toDataURL('png'); a.click(); };
        document.getElementById('layoutPrintBtn').onclick = () => directPrint(layoutCanvas, 210, 297, "A4拼版");
        document.getElementById('layoutRows').addEventListener('change', async () => { currentRows=parseInt(document.getElementById('layoutRows').value); document.getElementById('totalCellsCount').innerText=currentRows*currentCols; await generateImposition(); });
        document.getElementById('layoutCols').addEventListener('change', async () => { currentCols=parseInt(document.getElementById('layoutCols').value); document.getElementById('totalCellsCount').innerText=currentRows*currentCols; await generateImposition(); });
        
        // 合并页面事件(支持图片和PDF)
        document.getElementById('fileUploadInput').onchange = e => { Array.from(e.target.files).forEach(f => addFileToMerge(f)); e.target.value=''; };
        document.getElementById('clearFileListBtn').onclick = () => { uploadedFiles = []; updateFileList(); rebuildAllMergePages(); };
        document.getElementById('applyPdfLayoutBtn').onclick = async () => { mergeLayoutRows = parseInt(document.getElementById('pdfLayoutRows').value); mergeLayoutCols = parseInt(document.getElementById('pdfLayoutCols').value); await rebuildAllMergePages(); };
        document.getElementById('pdfMarginSlider').oninput = async e => { mergeMarginMM = parseFloat(e.target.value); document.getElementById('pdfMarginValue').innerText = mergeMarginMM+" mm"; await rebuildAllMergePages(); };
        document.getElementById('pdfPrintAllBtn').onclick = () => { if(mergePagesData.length) directPrintMultiPage(mergePagesData, "PDF/图片合并"); else alert("没有可打印的内容"); };
        document.getElementById('pdfExportAllBtn').onclick = async () => { if(mergePagesData.length===0){alert("无内容");return;} for(let i=0;i<mergePagesData.length;i++){let a=document.createElement('a'); a.download=`merge_page_${i+1}.png`; a.href=mergePagesData[i]; a.click(); await new Promise(r=>setTimeout(r,200));} alert(`已导出${mergePagesData.length}张`); };
        document.getElementById('prevPageBtn').onclick = () => prevMergePage();
        document.getElementById('nextPageBtn').onclick = () => nextMergePage();
    }
    
    function init() {
        initSingleCanvas();
        layoutCanvas = new fabric.Canvas('layoutCanvas', { width: A4_W_PX, height: A4_H_PX, selection: true });
        drawLayoutBackground();
        mergeCanvas = new fabric.Canvas('pdfMergeCanvas', { width: A4_W_PX, height: A4_H_PX, selection: true });
        drawMergeBackground();
        bindEvents();
        setTimeout(() => saveSingleDesign(), 500);
    }
    init();
</script>
</body>
</html>

免费评分

参与人数 3吾爱币 +7 热心值 +2 收起 理由
苏紫方璇 + 5 + 1 欢迎分析讨论交流,吾爱破解论坛有你更精彩!
laozhang4201 + 1 + 1 热心回复!
wuloveyou + 1 我很赞同!

查看全部评分

发帖前要善用论坛搜索功能,那里可能会有你要找的答案或者已经有人发布过相同内容了,请勿重复发帖。

 楼主| kissfox 发表于 2026-4-17 15:49
应要求修改PDF合并后增加边框

[HTML] 纯文本查看 复制代码
<!DOCTYPE html>
<html lang="zh-CN">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0, user-scalable=no">
    <title>印前工坊 - 专业拼版系统</title>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/fabric.js/5.3.0/fabric.min.js"></script>
    <script src="https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.min.js"></script>
    <link href="https://fonts.googleapis.com/css2?family=Inter:opsz,[email]wght@14..32[/email],300;14..32,400;14..32,500;14..32,600;14..32,700&display=swap" rel="stylesheet">
    <style>
        * { margin: 0; padding: 0; box-sizing: border-box; }
        body { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); font-family: 'Inter', sans-serif; padding: 24px; min-height: 100vh; }
        .app-wrapper { max-width: 1600px; margin: 0 auto; }
        .main-nav { background: rgba(255, 255, 255, 0.95); backdrop-filter: blur(10px); border-radius: 60px; padding: 8px 16px; margin-bottom: 24px; display: flex; gap: 12px; box-shadow: 0 8px 32px rgba(0, 0, 0, 0.1); border: 1px solid rgba(255, 255, 255, 0.3); }
        .nav-tab { padding: 12px 32px; border-radius: 48px; background: transparent; border: none; font-weight: 600; font-size: 1rem; cursor: pointer; transition: all 0.3s ease; color: #4a5568; font-family: 'Inter', sans-serif; }
        .nav-tab:hover { background: rgba(102, 126, 234, 0.1); color: #667eea; }
        .nav-tab.active { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; box-shadow: 0 4px 15px rgba(102, 126, 234, 0.4); }
        .page { display: none; background: white; border-radius: 32px; overflow: hidden; box-shadow: 0 25px 50px -12px rgba(0, 0, 0, 0.25); animation: fadeInUp 0.4s ease; }
        @keyframes fadeInUp { from { opacity: 0; transform: translateY(20px); } to { opacity: 1; transform: translateY(0); } }
        .page.active-page { display: block; }
        .page-header { padding: 20px 28px; background: linear-gradient(135deg, #f8fafc 0%, #f1f5f9 100%); border-bottom: 1px solid #e2e8f0; display: flex; justify-content: space-between; align-items: center; }
        .page-header h2 { font-size: 1.5rem; font-weight: 600; background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); -webkit-background-clip: text; -webkit-text-fill-color: transparent; background-clip: text; }
        .toolbar { background: #fefefe; padding: 20px 28px; display: flex; flex-wrap: wrap; gap: 16px; border-bottom: 1px solid #eef2f6; }
        .tool-group { background: #ffffff; border-radius: 24px; padding: 12px 18px; border: 1px solid #eef2f8; box-shadow: 0 2px 8px rgba(0, 0, 0, 0.02); transition: all 0.2s ease; min-width: 150px; }
        .tool-group:hover { border-color: #cbd5e1; box-shadow: 0 4px 12px rgba(0, 0, 0, 0.05); }
        .tool-group label { font-size: 0.7rem; font-weight: 700; color: #64748b; text-transform: uppercase; letter-spacing: 0.5px; margin-bottom: 8px; display: block; }
        .tool-group select, .tool-group input, .tool-group button { border-radius: 40px; border: 1px solid #e2e8f0; padding: 8px 16px; font-size: 0.85rem; background: white; cursor: pointer; transition: all 0.2s; font-family: 'Inter', sans-serif; }
        .tool-group select:hover, .tool-group input:hover { border-color: #cbd5e1; }
        .tool-group button.primary { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); border: none; color: white; font-weight: 500; }
        .tool-group button.primary:hover { transform: translateY(-1px); box-shadow: 0 4px 12px rgba(102, 126, 234, 0.4); }
        .inline-row { display: flex; gap: 8px; align-items: center; flex-wrap: wrap; }
        .canvas-area { background: linear-gradient(145deg, #e2e8f0 0%, #cbd5e6 100%); padding: 28px; display: flex; justify-content: center; position: relative; }
        .canvas-wrapper { background: #ffffff; border-radius: 20px; box-shadow: 0 20px 35px -10px rgba(0, 0, 0, 0.2); display: inline-block; overflow: auto; max-width: 100%; }
        canvas { display: block; margin: 0 auto; border-radius: 12px; cursor: crosshair; }
        .info-panel { background: #f8fafc; padding: 16px 28px; display: flex; justify-content: space-between; align-items: center; flex-wrap: wrap; gap: 16px; border-top: 1px solid #eef2f8; font-size: 0.85rem; color: #475569; }
        .btn-export { background: linear-gradient(135deg, #10b981 0%, #059669 100%); border: none; color: white; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .btn-print { background: linear-gradient(135deg, #8b5cf6 0%, #6d28d9 100%); border: none; color: white; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .btn-export:hover, .btn-print:hover, .nav-btn:hover { transform: translateY(-2px); box-shadow: 0 6px 20px rgba(0, 0, 0, 0.15); }
        .nav-btn { background: linear-gradient(135deg, #667eea 0%, #764ba2 100%); color: white; border: none; padding: 10px 24px; border-radius: 40px; font-weight: 600; cursor: pointer; transition: all 0.2s; }
        .file-list { max-height: 180px; overflow-y: auto; background: #f1f5f9; border-radius: 16px; padding: 8px; margin-top: 8px; }
        .file-item { display: flex; justify-content: space-between; align-items: center; padding: 8px 16px; background: white; margin: 6px 0; border-radius: 40px; border: 1px solid #e2e8f0; font-size: 0.8rem; }
        .remove-file-btn { background: #ef4444; color: white; border: none; border-radius: 40px; padding: 4px 16px; cursor: pointer; font-size: 12px; transition: all 0.2s; }
        .remove-file-btn:hover { background: #dc2626; transform: scale(1.02); }
        .page-controls { display: flex; gap: 12px; align-items: center; background: white; padding: 6px 20px; border-radius: 60px; box-shadow: 0 1px 3px rgba(0, 0, 0, 0.05); }
        .page-btn { background: #e2e8f0; color: #1e293b; border: none; padding: 6px 16px; border-radius: 40px; cursor: pointer; transition: all 0.2s; font-weight: 500; }
        .page-btn:hover:not(:disabled) { background: #cbd5e1; }
        .page-btn:disabled { opacity: 0.4; cursor: not-allowed; }
        .size-input { width: 80px; text-align: center; font-weight: 500; }
        .badge { display: inline-block; background: #e2e8f0; border-radius: 20px; padding: 2px 8px; font-size: 10px; margin-left: 8px; }
        .border-color-input { width: 50px; height: 32px; padding: 2px; border-radius: 8px; }
        @media (max-width: 780px) { body { padding: 12px; } .toolbar { flex-direction: column; } .tool-group { width: 100%; } .page-header { flex-direction: column; gap: 12px; } }
    </style>
</head>
<body>
<div class="app-wrapper">
    <div class="main-nav">
        <button class="nav-tab active" data-page="single">&#127912; 单页设计</button>
        <button class="nav-tab" data-page="pdf">&#128209; PDF/图片合并</button>
        <button class="nav-tab" data-page="layout">&#128450;&#65039; 整版预览</button>
    </div>

    <!-- 单页设计 -->
    <div id="singlePagePanel" class="page active-page">
        <div class="page-header">
            <h2>&#127912; 单页设计 · 自由创作</h2>
            <button id="saveAndGoToLayoutBtn" class="nav-btn">&#128190; 保存并进入整版预览 →</button>
        </div>
        <div class="toolbar">
            <div class="tool-group"><label>&#128208; 纸张预设</label><select id="paperPreset"><option value="custom">自定义尺寸</option><option value="A5">A5 (148×210mm)</option></select><button id="applyPresetBtn" class="primary">应用</button><div class="inline-row" style="margin-top: 8px;"><input type="number" id="customWidthMM" value="100" step="1" class="size-input" placeholder="宽"> mm<span>×</span><input type="number" id="customHeightMM" value="150" step="1" class="size-input" placeholder="高"> mm<button id="applySizeBtn" class="primary">应用</button></div></div>
            <div class="tool-group"><label>&#128444;&#65039; 插入图片</label><input type="file" id="singleImageUpload" accept="image/jpeg,image/png,image/webp"></div>
            <div class="tool-group"><label>&#128209; 插入PDF</label><input type="file" id="singlePdfUpload" accept="application/pdf"></div>
            <div class="tool-group"><label>&#9997;&#65039; 添加文字</label><div class="inline-row"><input type="text" id="singleTextInput" placeholder="文字内容" value="设计稿"><button id="singleAddTextBtn" class="primary">添加</button></div></div>
            <div class="tool-group"><label>&#128207; 绘制线条</label><div class="inline-row"><button id="singleHorLineBtn" style="background:#8b5cf6; color:white; border:none;">━ 横线</button><button id="singleVerLineBtn" style="background:#8b5cf6; color:white; border:none;">┃ 竖线</button></div><div class="inline-row"><input type="color" id="singleLineColor" value="#e11d48"><input type="number" id="singleLineWidth" value="2" step="0.5" style="width:65px;"></div></div>
            <div class="tool-group"><label>&#9881;&#65039; 辅助功能</label><label style="display: flex; align-items: center; gap: 8px;"><input type="checkbox" id="singleShowGrid" checked> 显示参考网格</label><button id="singleResetBtn" style="background:#f97316; color:white; border:none;">&#128465;&#65039; 清空画板</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="singleCanvas"></canvas></div></div>
        <div class="info-panel"><div>&#128161; 右键点击元素可删除 | 支持自定义尺寸和A5纸张</div><div style="display: flex; gap: 12px;"><button id="singlePrintBtn" class="btn-print">&#128424;&#65039; 直接打印</button><button id="singleExportBtn" class="btn-export">&#128248; 导出PNG</button></div></div>
    </div>

    <!-- PDF/图片合并(支持图片和PDF混合上传,带边框选项) -->
    <div id="pdfMergePanel" class="page">
        <div class="page-header"><h2>&#128209; PDF/图片合并 · 多页拼版打印(带边框)</h2></div>
        <div class="toolbar">
            <div class="tool-group"><label>&#128206; 上传文件 (PDF/图片)</label><input type="file" id="fileUploadInput" accept="image/jpeg,image/png,image/webp,application/pdf" multiple><div style="font-size: 11px; color: #64748b; margin-top: 4px;">支持 JPG、PNG、PDF,混合上传自动拼版</div></div>
            <div class="tool-group"><label>&#129513; 每页拼版数量</label><div class="inline-row"><select id="pdfLayoutRows"><option value="1">1行</option><option value="2">2行</option><option value="3" selected>3行</option><option value="4">4行</option><option value="5">5行</option><option value="6">6行</option></select><span>×</span><select id="pdfLayoutCols"><option value="1">1列</option><option value="2">2列</option><option value="3" selected>3列</option><option value="4">4列</option><option value="5">5列</option><option value="6">6列</option></select><button id="applyPdfLayoutBtn" class="primary">应用</button></div></div>
            <div class="tool-group"><label>&#128207; 页边距 (mm)</label><input type="range" id="pdfMarginSlider" min="0" max="18" step="0.5" value="5"><span id="pdfMarginValue">5 mm</span></div>
            <div class="tool-group"><label>&#128444;&#65039; 单元格边框</label><div class="inline-row"><input type="color" id="cellBorderColor" value="#cbd5e1"><input type="number" id="cellBorderWidth" value="1.5" min="0" max="5" step="0.5" style="width:60px;"> px</div><label style="display: flex; align-items: center; gap: 8px; margin-top: 6px;"><input type="checkbox" id="showCellBorder" checked> 显示边框</label></div>
            <div class="tool-group"><button id="clearFileListBtn" style="background:#ef4444; color:white; border:none;">清空所有文件</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="pdfMergeCanvas" width="1240" height="1754"></canvas></div></div>
        <div class="info-panel">
            <div class="inline-row"><span>&#128196; 已上传: <strong id="fileCountDisplay" style="color:#667eea;">0</strong> 个文件 | 总页数: <strong id="totalPagesDisplay" style="color:#667eea;">0</strong> 页A4</span><div class="page-controls"><button id="prevPageBtn" class="page-btn">&#9664; 上一页</button><span id="currentPageSpan" style="font-weight: 500;">第 1 / 1 页</span><button id="nextPageBtn" class="page-btn">下一页 &#9654;</button></div></div>
            <div id="fileList" class="file-list"></div>
            <div style="display: flex; gap: 12px;"><button id="pdfPrintAllBtn" class="btn-print">&#128424;&#65039; 直接打印全部</button><button id="pdfExportAllBtn" class="btn-export">&#128248; 导出全部PNG</button></div>
        </div>
    </div>

    <!-- 整版预览 -->
    <div id="layoutPanel" class="page">
        <div class="page-header"><h2>&#128450;&#65039; 整版预览 · A4拼版输出</h2><button id="backToDesignBtn" class="nav-btn" style="background:#64748b;">← 返回单页设计</button></div>
        <div class="toolbar">
            <div class="tool-group"><label>&#129513; 拼版数量</label><div class="inline-row"><select id="layoutRows"><option>1</option><option>2</option><option selected>3</option><option>4</option><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select><span>×</span><select id="layoutCols"><option>1</option><option>2</option><option selected>3</option><option>4</option><option>5</option><option>6</option><option>7</option><option>8</option><option>9</option><option>10</option></select><button id="applyLayoutBtn" class="primary">生成拼版</button></div><div style="font-size: 12px; margin-top: 6px;">共 <strong id="totalCellsCount" style="color:#667eea;">9</strong> 个页面</div></div>
            <div class="tool-group"><label>&#128207; 页边距 (mm)</label><input type="range" id="layoutMarginSlider" min="0" max="18" step="0.5" value="5"><span id="layoutMarginValue">5 mm</span><label style="display: flex; align-items: center; gap: 8px; margin-top: 6px;"><input type="checkbox" id="layoutShowCrop" checked> &#9986;&#65039; 显示裁剪线</label></div>
            <div class="tool-group"><button id="resetLayoutBtn" style="background:#ef4444; color:white; border:none;">重置拼版内容</button></div>
        </div>
        <div class="canvas-area"><div class="canvas-wrapper"><canvas id="layoutCanvas" width="1240" height="1754"></canvas></div></div>
        <div class="info-panel"><div>&#10024; 基于单页设计内容,自动缩放并完美拼版到A4纸</div><div style="display: flex; gap: 12px;"><button id="layoutPrintBtn" class="btn-print">&#128424;&#65039; 直接打印</button><button id="layoutExportBtn" class="btn-export">&#128248; 导出PNG</button></div></div>
    </div>
</div>

<script>
    pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdnjs.cloudflare.com/ajax/libs/pdf.js/2.16.105/pdf.worker.min.js';
    const A4_W_PX = 1240, A4_H_PX = 1754;
    const MM_TO_PX = 150 / 25.4;
    
    let singleCanvas = null;
    let singleWidthMM = 100, singleHeightMM = 150, singleWidthPX = 0, singleHeightPX = 0;
    let singleLineColor = "#e11d48", singleLineWidth = 2, singleShowGridFlag = true;
    let savedSingleObjectsJSON = [], savedSingleSizeMM = { w: 100, h: 150 };
    
    let layoutCanvas = null;
    let currentRows = 3, currentCols = 3, currentMarginMM = 5, showCropLines = true, layoutUserObjects = [];
    
    // 合并页面变量
    let mergeCanvas = null;
    let uploadedFiles = [];
    let mergeLayoutRows = 3, mergeLayoutCols = 3, mergeMarginMM = 5;
    let mergePagesData = [];
    let currentPageIndex = 0;
    let totalMergePages = 0;
    // 边框设置
    let showCellBorder = true;
    let cellBorderColor = "#cbd5e1";
    let cellBorderWidth = 1.5;
    
    // 打印函数
    function directPrint(canvasInstance, widthMM, heightMM, title) {
        if (!canvasInstance) return;
        const offscreenCanvas = document.createElement('canvas');
        offscreenCanvas.width = A4_W_PX;
        offscreenCanvas.height = A4_H_PX;
        const ctx = offscreenCanvas.getContext('2d');
        const originalCanvas = canvasInstance.getElement();
        ctx.drawImage(originalCanvas, 0, 0, A4_W_PX, A4_H_PX);
        const imgDataURL = offscreenCanvas.toDataURL('image/png');
        
        const printWindow = window.open('', '_blank', 'width=800,height=600,toolbar=yes,menubar=yes,scrollbars=yes');
        if (!printWindow) { alert("请允许弹出窗口以进行打印"); return; }
        
        printWindow.document.write(`<!DOCTYPE html><html><head><title>${title}</title><style>*{margin:0;padding:0;} @page{size:A4;margin:0;} body{display:flex;justify-content:center;align-items:center;min-height:100vh;background:white;} img{width:${widthMM}mm;height:${heightMM}mm;}</style></head><body><img src="${imgDataURL}"><script>window.onload=function(){window.print();window.onafterprint=function(){window.close();};};<\/script></body></html>`);
        printWindow.document.close();
    }
    
    function directPrintMultiPage(pageDataURLs, title) {
        if (!pageDataURLs || pageDataURLs.length === 0) { alert("没有可打印的内容"); return; }
        const printWindow = window.open('', '_blank', 'width=800,height=600,toolbar=yes,menubar=yes,scrollbars=yes');
        if (!printWindow) { alert("请允许弹出窗口以进行打印"); return; }
        let html = `<!DOCTYPE html><html><head><title>${title}</title><style>*{margin:0;padding:0;} @page{size:A4;margin:0;} .page{page-break-after:always;display:flex;justify-content:center;align-items:center;min-height:297mm;background:white;} img{width:210mm;height:297mm;display:block;}</style></head><body>`;
        for (let i = 0; i < pageDataURLs.length; i++) { html += `<div class="page"><img src="${pageDataURLs[i]}" /></div>`; }
        html += `<script>window.onload=function(){window.print();window.onafterprint=function(){window.close();};};<\/script></body></html>`;
        printWindow.document.write(html);
        printWindow.document.close();
    }
    
    // 单页设计模块
    function initSingleCanvas() {
        singleWidthPX = Math.round(100 * MM_TO_PX); singleHeightPX = Math.round(150 * MM_TO_PX);
        singleCanvas = new fabric.Canvas('singleCanvas', { width: singleWidthPX, height: singleHeightPX, selection: true });
        drawSingleBackground(); setupContextMenu(singleCanvas); addTextToSingle("自定义尺寸\n设计内容"); saveSingleDesign();
    }
    function setupContextMenu(canvas) {
        canvas.on('mouse:down', e => { if(e.button===2 && e.target && !e.target.data?.isBackground) { canvas.remove(e.target); canvas.renderAll(); e.e.preventDefault(); } });
        canvas.wrapperEl.addEventListener('contextmenu', e => e.preventDefault());
    }
    function drawSingleBackground() {
        if(!singleCanvas) return;
        singleCanvas.getObjects().filter(o=>o.data?.isBackground).forEach(o=>singleCanvas.remove(o));
        singleCanvas.add(new fabric.Rect({ left:0,top:0,width:singleCanvas.width,height:singleCanvas.height,fill:'#fff',selectable:false,evented:false,data:{isBackground:true}}));
        if(singleShowGridFlag) {
            for(let x=0;x<=singleCanvas.width;x+=40) singleCanvas.add(new fabric.Line([x,0,x,singleCanvas.height],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false,data:{isBackground:true}}));
            for(let y=0;y<=singleCanvas.height;y+=40) singleCanvas.add(new fabric.Line([0,y,singleCanvas.width,y],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false,data:{isBackground:true}}));
        }
        singleCanvas.renderAll();
    }
    function updateSingleCanvasSize() {
        singleWidthPX = Math.round(singleWidthMM * MM_TO_PX); singleHeightPX = Math.round(singleHeightMM * MM_TO_PX);
        const objs = singleCanvas.getObjects().filter(o=>!o.data?.isBackground);
        const oldCx = singleCanvas.width/2, oldCy = singleCanvas.height/2;
        singleCanvas.setWidth(singleWidthPX); singleCanvas.setHeight(singleHeightPX); singleCanvas.calcOffset();
        const newCx = singleWidthPX/2, newCy = singleHeightPX/2;
        objs.forEach(o=>{ o.set({left:o.left+(newCx-oldCx),top:o.top+(newCy-oldCy)}); o.setCoords(); });
        drawSingleBackground();
        objs.forEach(o=>singleCanvas.add(o));
        singleCanvas.renderAll();
    }
    function addTextToSingle(t) { if(!t.trim()) t="文字"; let o=new fabric.IText(t,{fontSize:Math.min(48,singleCanvas.height*0.08),fill:'#1f2937',backgroundColor:'#fef9c3',padding:8,editable:true,left:singleCanvas.width/2-60,top:singleCanvas.height/2-20}); singleCanvas.add(o); singleCanvas.setActiveObject(o); singleCanvas.renderAll(); }
    function addImageToSingle(f){let r=new FileReader(); r.onload=e=>{fabric.Image.fromURL(e.target.result,img=>{let s=Math.min(singleCanvas.width*0.6/img.width,singleCanvas.height*0.6/img.height,1); img.scale(s); img.set({left:singleCanvas.width/2-(img.width*s)/2,top:singleCanvas.height/2-(img.height*s)/2}); singleCanvas.add(img); singleCanvas.renderAll();})}; r.readAsDataURL(f);}
    async function addPdfToSingle(f){let r=new FileReader(); r.onload=async e=>{let arr=new Uint8Array(e.target.result); try{let pdf=await pdfjsLib.getDocument({data:arr}).promise; let page=await pdf.getPage(1); let vp=page.getViewport({scale:1.5}); let c=document.createElement('canvas'); c.width=vp.width; c.height=vp.height; await page.render({canvasContext:c.getContext('2d'),viewport:vp}).promise; fabric.Image.fromURL(c.toDataURL(),img=>{let s=Math.min(singleCanvas.width*0.7/img.width,singleCanvas.height*0.7/img.height,1); img.scale(s); img.set({left:singleCanvas.width/2-(img.width*s)/2,top:singleCanvas.height/2-(img.height*s)/2}); singleCanvas.add(img); singleCanvas.renderAll();})}catch(e){alert("PDF失败")}}; r.readAsArrayBuffer(f);}
    function createLineOnSingle(h){let l=Math.min(singleCanvas.width,singleCanvas.height)*0.3; let line=h?new fabric.Line([singleCanvas.width/2-l/2,singleCanvas.height/2,singleCanvas.width/2+l/2,singleCanvas.height/2],{stroke:singleLineColor,strokeWidth:singleLineWidth}):new fabric.Line([singleCanvas.width/2,singleCanvas.height/2-l/2,singleCanvas.width/2,singleCanvas.height/2+l/2],{stroke:singleLineColor,strokeWidth:singleLineWidth}); singleCanvas.add(line); singleCanvas.renderAll();}
    function clearSingleCanvas(){let bg=singleCanvas.getObjects().filter(o=>o.data?.isBackground); singleCanvas.clear(); bg.forEach(b=>singleCanvas.add(b)); singleCanvas.renderAll();}
    function saveSingleDesign(){savedSingleObjectsJSON=singleCanvas.getObjects().filter(o=>!o.data?.isBackground).map(o=>o.toJSON()); savedSingleSizeMM={w:singleWidthMM,h:singleHeightMM}; return savedSingleObjectsJSON.length>0;}
    
    // 整版拼版
    function getLayoutParams(w,h,m,r,c){let aw=w-m*2, ah=h-m*2; return {cellW:aw/c,cellH:ah/r,startX:m,startY:m};}
    function getCellCenter(r,c,m,rows,cols,w,h){let {cellW,cellH,startX,startY}=getLayoutParams(w,h,m,rows,cols); return {x:startX+c*cellW+cellW/2,y:startY+r*cellH+cellH/2};}
    function drawLayoutBackground(){if(!layoutCanvas)return; layoutCanvas.getObjects().filter(o=>o.data?.isBackground).forEach(o=>layoutCanvas.remove(o)); layoutCanvas.add(new fabric.Rect({left:0,top:0,width:A4_W_PX,height:A4_H_PX,fill:'#fff',selectable:false,evented:false,data:{isBackground:true}})); let m=currentMarginMM*MM_TO_PX; let {cellW,cellH,startX,startY}=getLayoutParams(A4_W_PX,A4_H_PX,m,currentRows,currentCols); for(let c=0;c<=currentCols;c++){let x=startX+c*cellW; if(x>=m&&x<=A4_W_PX-m) layoutCanvas.add(new fabric.Line([x,startY,x,startY+currentRows*cellH],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false,data:{isBackground:true}}));} for(let r=0;r<=currentRows;r++){let y=startY+r*cellH; if(y>=m&&y<=startY+currentRows*cellH) layoutCanvas.add(new fabric.Line([startX,y,startX+currentCols*cellW,y],{stroke:'#cbd5e6',strokeWidth:1,selectable:false,evented:false,data:{isBackground:true}}));} if(showCropLines){let crop="#d97706",len=12; [[m,m],[A4_W_PX-m,m],[m,A4_H_PX-m],[A4_W_PX-m,A4_H_PX-m]].forEach(([x,y],i)=>{layoutCanvas.add(new fabric.Line([x,y,x+len,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}})); layoutCanvas.add(new fabric.Line([x,y,x,y+len],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}})); if(i===1||i===3){layoutCanvas.add(new fabric.Line([x-len,y,x,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}}));} if(i===2||i===3){layoutCanvas.add(new fabric.Line([x,y-len,x,y],{stroke:crop,strokeWidth:1.5,selectable:false,evented:false,data:{isBackground:true}}));} });} layoutCanvas.renderAll();}
    async function generateImposition(){if(!layoutCanvas)return; if(!savedSingleObjectsJSON.length){alert("请先设计单页内容并保存");return;} layoutUserObjects.forEach(o=>layoutCanvas.remove(o)); layoutUserObjects=[]; let m=currentMarginMM*MM_TO_PX; let total=currentRows*currentCols; let sw_mm=savedSingleSizeMM.w, sh_mm=savedSingleSizeMM.h; let tw=(A4_W_PX-m*2)/currentCols, th=(A4_H_PX-m*2)/currentRows; let scale=Math.min(tw/(sw_mm*MM_TO_PX), th/(sh_mm*MM_TO_PX)); drawLayoutBackground(); for(let i=0;i<total;i++){let r=Math.floor(i/currentCols),c=i%currentCols; let center=getCellCenter(r,c,m,currentRows,currentCols,A4_W_PX,A4_H_PX); for(let json of savedSingleObjectsJSON){let cloned=await new Promise(res=>fabric.util.enlivenObjects([json],objs=>res(objs[0]))); if(cloned){let srcCx=(sw_mm*MM_TO_PX)/2, srcCy=(sh_mm*MM_TO_PX)/2; let dx=cloned.left-srcCx, dy=cloned.top-srcCy; cloned.scaleX=(cloned.scaleX||1)*scale; cloned.scaleY=(cloned.scaleY||1)*scale; cloned.set({left:center.x+dx*scale,top:center.y+dy*scale}); cloned.setCoords(); layoutCanvas.add(cloned); layoutUserObjects.push(cloned);}}} layoutCanvas.renderAll(); document.getElementById('totalCellsCount').innerText=total;}
    
    // ========== 合并模块:支持图片和PDF混合上传,带边框 ==========
    async function addFileToMerge(file) {
        const fileType = file.type;
        const fileName = file.name;
        
        if (fileType.startsWith('image/')) {
            const reader = new FileReader();
            reader.onload = (e) => {
                const img = new Image();
                img.onload = () => {
                    uploadedFiles.push({ name: fileName, type: 'image', data: e.target.result, width: img.width, height: img.height });
                    updateFileList();
                    rebuildAllMergePages();
                };
                img.src = e.target.result;
            };
            reader.readAsDataURL(file);
        } else if (fileType === 'application/pdf') {
            const reader = new FileReader();
            reader.onload = async (e) => {
                const typedarray = new Uint8Array(e.target.result);
                try {
                    const pdf = await pdfjsLib.getDocument({ data: typedarray }).promise;
                    const page = await pdf.getPage(1);
                    const viewport = page.getViewport({ scale: 1.5 });
                    const canvasTemp = document.createElement('canvas');
                    const context = canvasTemp.getContext('2d');
                    canvasTemp.width = viewport.width;
                    canvasTemp.height = viewport.height;
                    await page.render({ canvasContext: context, viewport: viewport }).promise;
                    const imgData = canvasTemp.toDataURL();
                    uploadedFiles.push({ name: fileName, type: 'pdf', data: imgData, width: viewport.width, height: viewport.height });
                    updateFileList();
                    await rebuildAllMergePages();
                } catch(err) { alert(`PDF解析失败: ${fileName}`); }
            };
            reader.readAsArrayBuffer(file);
        } else {
            alert(`不支持的文件类型: ${fileName}`);
        }
    }
    
    async function rebuildAllMergePages() {
        if (!mergeCanvas) return;
        const perPage = mergeLayoutRows * mergeLayoutCols;
        totalMergePages = Math.ceil(uploadedFiles.length / perPage);
        if (totalMergePages === 0) totalMergePages = 1;
        document.getElementById('totalPagesDisplay').innerText = totalMergePages;
        mergePagesData = [];
        const marginPx = mergeMarginMM * MM_TO_PX;
        
        for (let pageIdx = 0; pageIdx < totalMergePages; pageIdx++) {
            const startIdx = pageIdx * perPage;
            const endIdx = Math.min(startIdx + perPage, uploadedFiles.length);
            const tempCanvas = new fabric.Canvas(null, { width: A4_W_PX, height: A4_H_PX });
            tempCanvas.add(new fabric.Rect({ left:0, top:0, width:A4_W_PX, height:A4_H_PX, fill:'#ffffff', selectable:false, evented:false }));
            
            const { cellW, cellH, startX, startY } = getLayoutParams(A4_W_PX, A4_H_PX, marginPx, mergeLayoutRows, mergeLayoutCols);
            
            // 绘制网格线(辅助线)
            for(let c=0; c<=mergeLayoutCols; c++) { let x=startX+c*cellW; if(x>=marginPx&&x<=A4_W_PX-marginPx) tempCanvas.add(new fabric.Line([x,startY,x,startY+mergeLayoutRows*cellH],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false})); }
            for(let r=0; r<=mergeLayoutRows; r++) { let y=startY+r*cellH; if(y>=startY&&y<=startY+mergeLayoutRows*cellH) tempCanvas.add(new fabric.Line([startX,y,startX+mergeLayoutCols*cellW,y],{stroke:'#e2e8f0',strokeWidth:0.5,selectable:false,evented:false})); }
            
            // 绘制单元格边框(如果启用)
            if (showCellBorder && cellBorderWidth > 0) {
                for (let r = 0; r < mergeLayoutRows; r++) {
                    for (let c = 0; c < mergeLayoutCols; c++) {
                        const rectLeft = startX + c * cellW;
                        const rectTop = startY + r * cellH;
                        const rect = new fabric.Rect({
                            left: rectLeft,
                            top: rectTop,
                            width: cellW,
                            height: cellH,
                            fill: 'transparent',
                            stroke: cellBorderColor,
                            strokeWidth: cellBorderWidth,
                            selectable: false,
                            evented: false
                        });
                        tempCanvas.add(rect);
                    }
                }
            }
            
            // 添加文件内容
            for (let i = startIdx; i < endIdx; i++) {
                const localIdx = i - startIdx;
                const row = Math.floor(localIdx / mergeLayoutCols);
                const col = localIdx % mergeLayoutCols;
                const center = getCellCenter(row, col, marginPx, mergeLayoutRows, mergeLayoutCols, A4_W_PX, A4_H_PX);
                const fileData = uploadedFiles[i].data;
                const img = await new Promise(resolve => fabric.Image.fromURL(fileData, resolve));
                if (img) {
                    const targetW = (A4_W_PX - marginPx * 2) / mergeLayoutCols;
                    const targetH = (A4_H_PX - marginPx * 2) / mergeLayoutRows;
                    const scale = Math.min(targetW / img.width, targetH / img.height);
                    img.scale(scale);
                    img.set({ left: center.x - (img.width * scale)/2, top: center.y - (img.height * scale)/2 });
                    tempCanvas.add(img);
                }
            }
            tempCanvas.renderAll();
            mergePagesData.push(tempCanvas.toDataURL('png'));
            tempCanvas.dispose();
        }
        currentPageIndex = 0;
        await displayMergeCurrentPage();
        updateMergePageControls();
    }
    
    async function displayMergeCurrentPage() {
        if (!mergeCanvas || mergePagesData.length === 0) { mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o)); mergeCanvas.renderAll(); return; }
        const imgData = mergePagesData[currentPageIndex];
        mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o));
        mergeCanvas.add(new fabric.Rect({ left:0, top:0, width:A4_W_PX, height:A4_H_PX, fill:'#ffffff', selectable:false, evented:false, data:{isBackground:true} }));
        const img = await new Promise(resolve => fabric.Image.fromURL(imgData, resolve));
        if (img) { img.set({ left: 0, top: 0, width: A4_W_PX, height: A4_H_PX, selectable: false, evented: false }); mergeCanvas.add(img); }
        mergeCanvas.renderAll();
    }
    
    function updateMergePageControls() {
        document.getElementById('currentPageSpan').innerHTML = `第 ${currentPageIndex+1} / ${totalMergePages} 页`;
        document.getElementById('prevPageBtn').disabled = (currentPageIndex === 0);
        document.getElementById('nextPageBtn').disabled = (currentPageIndex >= totalMergePages - 1);
    }
    function prevMergePage() { if (currentPageIndex > 0) { currentPageIndex--; displayMergeCurrentPage(); updateMergePageControls(); } }
    function nextMergePage() { if (currentPageIndex < totalMergePages - 1) { currentPageIndex++; displayMergeCurrentPage(); updateMergePageControls(); } }
    
    function updateFileList() {
        const container = document.getElementById('fileList');
        const countSpan = document.getElementById('fileCountDisplay');
        countSpan.innerText = uploadedFiles.length;
        container.innerHTML = uploadedFiles.map((file, idx) => `<div class="file-item"><span>${file.type === 'image' ? '&#128444;&#65039;' : '&#128209;'} ${file.name.substring(0, 35)}<span class="badge">${file.type === 'image' ? '图片' : 'PDF'}</span></span><button class="remove-file-btn" data-idx="${idx}">删除</button></div>`).join('');
        document.querySelectorAll('.remove-file-btn').forEach(btn => {
            btn.addEventListener('click', (e) => {
                const idx = parseInt(btn.dataset.idx);
                uploadedFiles.splice(idx, 1);
                updateFileList();
                rebuildAllMergePages();
            });
        });
    }
    
    function drawMergeBackground() { if(!mergeCanvas) return; mergeCanvas.getObjects().forEach(o=>mergeCanvas.remove(o)); mergeCanvas.add(new fabric.Rect({left:0,top:0,width:A4_W_PX,height:A4_H_PX,fill:'#fff',selectable:false,evented:false})); mergeCanvas.renderAll(); }
    
    function switchPage(pageId) {
        document.getElementById('singlePagePanel').classList.remove('active-page');
        document.getElementById('pdfMergePanel').classList.remove('active-page');
        document.getElementById('layoutPanel').classList.remove('active-page');
        if (pageId === 'single') document.getElementById('singlePagePanel').classList.add('active-page');
        else if (pageId === 'pdf') { document.getElementById('pdfMergePanel').classList.add('active-page'); if(mergeCanvas && mergePagesData.length===0 && uploadedFiles.length===0) drawMergeBackground(); }
        else if (pageId === 'layout') { document.getElementById('layoutPanel').classList.add('active-page'); generateImposition(); }
        document.querySelectorAll('.nav-tab').forEach(tab => { tab.classList.toggle('active', tab.dataset.page === pageId); });
    }
    
    function bindEvents() {
        document.querySelectorAll('.nav-tab').forEach(btn => btn.addEventListener('click', () => switchPage(btn.dataset.page)));
        document.getElementById('applyPresetBtn').onclick = () => { if(document.getElementById('paperPreset').value==='A5'){singleWidthMM=148;singleHeightMM=210;updateSingleCanvasSize();saveSingleDesign();alert("已切换A5");} };
        document.getElementById('applySizeBtn').onclick = () => { let w=parseFloat(document.getElementById('customWidthMM').value),h=parseFloat(document.getElementById('customHeightMM').value); if(w>0&&h>0){singleWidthMM=w;singleHeightMM=h;updateSingleCanvasSize();saveSingleDesign();alert(`尺寸改为${w}×${h}mm`);} };
        document.getElementById('singleAddTextBtn').onclick = () => addTextToSingle(document.getElementById('singleTextInput').value||"文字");
        document.getElementById('singleImageUpload').onchange = e => { if(e.target.files[0]) addImageToSingle(e.target.files[0]); e.target.value=''; };
        document.getElementById('singlePdfUpload').onchange = e => { if(e.target.files[0]) addPdfToSingle(e.target.files[0]); e.target.value=''; };
        document.getElementById('singleHorLineBtn').onclick = () => { singleLineColor=document.getElementById('singleLineColor').value; singleLineWidth=parseFloat(document.getElementById('singleLineWidth').value); createLineOnSingle(true); };
        document.getElementById('singleVerLineBtn').onclick = () => { singleLineColor=document.getElementById('singleLineColor').value; singleLineWidth=parseFloat(document.getElementById('singleLineWidth').value); createLineOnSingle(false); };
        document.getElementById('singleResetBtn').onclick = () => clearSingleCanvas();
        document.getElementById('singleExportBtn').onclick = () => { let a=document.createElement('a'); a.download='single_design.png'; a.href=singleCanvas.toDataURL('png'); a.click(); };
        document.getElementById('singlePrintBtn').onclick = () => directPrint(singleCanvas, singleWidthMM, singleHeightMM, "单页设计");
        document.getElementById('singleShowGrid').onchange = e => { singleShowGridFlag=e.target.checked; drawSingleBackground(); singleCanvas.getObjects().filter(o=>!o.data?.isBackground).forEach(o=>singleCanvas.add(o)); singleCanvas.renderAll(); };
        document.getElementById('saveAndGoToLayoutBtn').onclick = async () => { if(saveSingleDesign()){ alert("已保存"); switchPage('layout'); await generateImposition(); } else alert("请先添加内容"); };
        document.getElementById('backToDesignBtn').onclick = () => switchPage('single');
        document.getElementById('layoutMarginSlider').oninput = async e => { currentMarginMM=parseFloat(e.target.value); document.getElementById('layoutMarginValue').innerText=currentMarginMM+" mm"; await generateImposition(); };
        document.getElementById('layoutShowCrop').onchange = async e => { showCropLines=e.target.checked; await generateImposition(); };
        document.getElementById('applyLayoutBtn').onclick = async () => { currentRows=parseInt(document.getElementById('layoutRows').value); currentCols=parseInt(document.getElementById('layoutCols').value); await generateImposition(); };
        document.getElementById('resetLayoutBtn').onclick = () => { layoutUserObjects.forEach(o=>layoutCanvas.remove(o)); layoutUserObjects=[]; drawLayoutBackground(); layoutCanvas.renderAll(); };
        document.getElementById('layoutExportBtn').onclick = () => { let a=document.createElement('a'); a.download='full_imposition.png'; a.href=layoutCanvas.toDataURL('png'); a.click(); };
        document.getElementById('layoutPrintBtn').onclick = () => directPrint(layoutCanvas, 210, 297, "A4拼版");
        document.getElementById('layoutRows').addEventListener('change', async () => { currentRows=parseInt(document.getElementById('layoutRows').value); document.getElementById('totalCellsCount').innerText=currentRows*currentCols; await generateImposition(); });
        document.getElementById('layoutCols').addEventListener('change', async () => { currentCols=parseInt(document.getElementById('layoutCols').value); document.getElementById('totalCellsCount').innerText=currentRows*currentCols; await generateImposition(); });
        
        // 合并页面事件
        document.getElementById('fileUploadInput').onchange = e => { Array.from(e.target.files).forEach(f => addFileToMerge(f)); e.target.value=''; };
        document.getElementById('clearFileListBtn').onclick = () => { uploadedFiles = []; updateFileList(); rebuildAllMergePages(); };
        document.getElementById('applyPdfLayoutBtn').onclick = async () => { mergeLayoutRows = parseInt(document.getElementById('pdfLayoutRows').value); mergeLayoutCols = parseInt(document.getElementById('pdfLayoutCols').value); await rebuildAllMergePages(); };
        document.getElementById('pdfMarginSlider').oninput = async e => { mergeMarginMM = parseFloat(e.target.value); document.getElementById('pdfMarginValue').innerText = mergeMarginMM+" mm"; await rebuildAllMergePages(); };
        document.getElementById('pdfPrintAllBtn').onclick = () => { if(mergePagesData.length) directPrintMultiPage(mergePagesData, "PDF/图片合并"); else alert("没有可打印的内容"); };
        document.getElementById('pdfExportAllBtn').onclick = async () => { if(mergePagesData.length===0){alert("无内容");return;} for(let i=0;i<mergePagesData.length;i++){let a=document.createElement('a'); a.download=`merge_page_${i+1}.png`; a.href=mergePagesData[i]; a.click(); await new Promise(r=>setTimeout(r,200));} alert(`已导出${mergePagesData.length}张`); };
        document.getElementById('prevPageBtn').onclick = () => prevMergePage();
        document.getElementById('nextPageBtn').onclick = () => nextMergePage();
        
        // 边框设置事件
        document.getElementById('showCellBorder').onchange = async (e) => { showCellBorder = e.target.checked; await rebuildAllMergePages(); };
        document.getElementById('cellBorderColor').onchange = async (e) => { cellBorderColor = e.target.value; await rebuildAllMergePages(); };
        document.getElementById('cellBorderWidth').oninput = async (e) => { cellBorderWidth = parseFloat(e.target.value); await rebuildAllMergePages(); };
    }
    
    function init() {
        initSingleCanvas();
        layoutCanvas = new fabric.Canvas('layoutCanvas', { width: A4_W_PX, height: A4_H_PX, selection: true });
        drawLayoutBackground();
        mergeCanvas = new fabric.Canvas('pdfMergeCanvas', { width: A4_W_PX, height: A4_H_PX, selection: true });
        drawMergeBackground();
        bindEvents();
        setTimeout(() => saveSingleDesign(), 500);
    }
    init();
</script>
</body>
</html>
 楼主| kissfox 发表于 2026-4-17 15:52
px307 发表于 2026-4-17 15:48
大佬这方面怎么学

其实只要你有自己的想法,再加上自己有html基础,再结合deepseek或qoder就可以实现了,重点就是html基础
hezhiyong 发表于 2026-4-17 14:46
Deepsee功能确实很强大!结合qoder节省了好多时间和精力!
winwoo 发表于 2026-4-17 14:48
希望 楼主有时间进行改良,   PDF/图片合井打印   边上 方框
ScantyH 发表于 2026-4-17 15:25
学习学习,感谢楼主
 楼主| kissfox 发表于 2026-4-17 15:41
winwoo 发表于 2026-4-17 14:48
希望 楼主有时间进行改良,   PDF/图片合井打印   边上 方框

是需要在边上加入边框吗
px307 发表于 2026-4-17 15:48
大佬这方面怎么学
px307 发表于 2026-4-17 15:57
kissfox 发表于 2026-4-17 15:52
其实只要你有自己的想法,再加上自己有html基础,再结合deepseek或qoder就可以实现了,重点就是html基础

我还以为是那种cli 的形式
 楼主| kissfox 发表于 2026-4-17 16:00
px307 发表于 2026-4-17 15:57
我还以为是那种cli 的形式

你没看代码和标题?
您需要登录后才可以回帖 登录 | 注册[Register]

本版积分规则

返回列表

RSS订阅|小黑屋|处罚记录|联系我们|吾爱破解 - 52pojie.cn ( 京ICP备16042023号 | 京公网安备 11010502030087号 )

GMT+8, 2026-7-14 22:52

Powered by Discuz!

Copyright © 2001-2020, Tencent Cloud.

快速回复 返回顶部 返回列表