推荐一个超好用的 PDF 合并工具:免费、本地、还能拖拽排序
平时工作学习,经常需要把好几个 PDF 合并成一个。比如把多份扫描件合成一份完整的文档,或者把课件章节合并起来方便打印。我以前试过不少在线工具,要么要收费,要么得注册会员,最担心的是文件上传到别人的服务器上,总觉得不太安全。
最近发现了一个纯网页版的小工具,完全解决了我的烦恼。它不用安装任何软件 ,打开就能用,而且所有文件都在你自己的电脑里处理,不会上传到任何网络 。更重要的是,它支持拖拽调整合并顺序 ,想先放哪份文件就拖到前面,特别直观。

这个工具有什么好处?
- 完全免费:没有任何隐藏收费,也没有文件数量限制。
- 本地处理,保护隐私:你的 PDF 文件不会被上传到任何服务器,所有操作都在浏览器内存中完成,适合处理合同、证件等敏感材料。
- 操作简单:点击上传或者直接把文件拖进框里就行,列表里每份文件都显示页数,一目了然。
- 自由调整顺序:用鼠标拖拽左侧的"⋮⋮"手柄,就能上下移动文件,合并后的 PDF 会按你排好的顺序排列。
- 随时删除或清空:如果加错了文件,点"删除"按钮单独移除,或者一键"清空列表"重新开始。
- 合并速度快:一般几十页的文件几秒钟就能完成,合并完直接下载到本地。
怎么使用?跟着下面三步走
第一步:添加 PDF 文件
打开工具页面,你会看到一个虚线框,写着"点击或者拖拽PDF文件到此处"。你可以:
- 点击这个区域,从电脑里选择多个 PDF 文件(按住 Ctrl 可以多选)。
- 或者直接把文件从文件夹拖到这个框里。
添加成功后,下面列表里就会出现你刚才添加的文件名和页数。

第二步:调整合并顺序(可选)
如果你希望最终的 PDF 按特定顺序排列,可以在列表中拖拽每个文件左侧的"⋮⋮"图标,上下移动,排成你想要的顺序。列表最上面的会出现在最终 PDF 的第一页,最下面的在最后。
第三步:合并并下载
排好顺序后,点击蓝色的"合并并下载PDF"按钮。工具会迅速处理,几秒钟后浏览器就会自动下载一个名为"合并输出.pdf"的文件。你打开它,就会发现所有文件已经按你的顺序合并好了。

如果中间想重新开始,点击"清空列表"就能清掉所有文件。
一些小提示
- 这个工具目前只支持 PDF 格式,其他格式(如 Word、图片)需要先转成 PDF 再上传。
- 如果文件很大(比如几百页的大文档),合并可能会慢一点,请耐心等待,不要重复点击按钮。
- 合并过程中出现任何错误,页面会有提示,一般重新添加文件就能解决。
最后说几句
我一直喜欢这种"小而美"的工具,不花哨,但解决问题很实在。如果你经常需要合并 PDF,不妨把这个网页收藏起来,用的时候打开就行,省去了安装各种软件的麻烦。而且因为全程本地处理,用起来特别安心。
有需要的朋友可以试试,也欢迎转给身边需要的同事或同学。要是遇到问题,或者有什么改进建议,也可以留言告诉我。
代码
如下:
bash
<!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://cdn.jsdelivr.net/npm/pdf-lib@1.17.1/dist/pdf-lib.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.min.js"></script>
<script src="https://cdn.jsdelivr.net/npm/sortablejs@1.15.0/Sortable.min.js"></script>
<style>
* {box-sizing: border-box;margin:0;padding:0;font-family:system-ui, sans-serif;}
body{background:#f5f7fa;padding:24px;max-width:900px;margin:0 auto;color:#222;}
h1{font-size:24px;margin-bottom:20px;text-align:center;}
.upload-area{border:2px dashed #4080ff;border-radius:12px;padding:40px;text-align:center;background:#f0f6ff;margin-bottom:20px;cursor:pointer;transition:0.2s;}
.upload-area:hover{background:#e4edff;}
.upload-area.drag-over{background:#d4e2ff;border-color:#2563eb;}
#fileInput{display:none;}
.pdf-list{background:#fff;border-radius:12px;padding:16px;min-height:120px;margin-bottom:20px;box-shadow:0 1px 4px #00000015;}
.pdf-item{display:flex;align-items:center;padding:12px;border:1px solid #e5e7eb;border-radius:8px;margin-bottom:8px;gap:12px;background:#fafafa;}
.drag-handle{cursor:grab;color:#888;font-size:20px;user-select:none;}
.pdf-item-name{flex:1;overflow:hidden;text-overflow:ellipsis;white-space:nowrap;}
.pdf-item-page{color:#666;font-size:13px;}
.pdf-item-del{border:none;background:#ef4444;color:#fff;padding:4px 10px;border-radius:6px;cursor:pointer;}
.btn-wrap{display:flex;gap:12px;justify-content:center;flex-wrap:wrap;}
button{padding:12px 24px;border:none;border-radius:8px;font-size:15px;cursor:pointer;}
#mergeBtn{background:#2563eb;color:#fff;}
#clearBtn{background:#e5e7eb;color:#333;}
.tip{margin-top:16px;text-align:center;color:#666;font-size:13px;}
.loading{text-align:center;padding:20px;color:#2563eb;display:none;}
</style>
</head>
<body>
<h1>PDF合并工具(拖拽调整顺序)</h1>
<div class="upload-area" id="uploadArea">
<p>点击或者拖拽PDF文件到此处</p>
<p style="font-size:13px;color:#666;margin-top:8px;">本地处理,文件不会上传网络</p>
<input type="file" id="fileInput" accept=".pdf" multiple>
</div>
<div class="pdf-list" id="pdfList"></div>
<div class="loading" id="loading">处理中,请稍候...</div>
<div class="btn-wrap">
<button id="mergeBtn">合并并下载PDF</button>
<button id="clearBtn">清空列表</button>
</div>
<p class="tip">拖拽条目调整合并顺序,顺序从上到下为最终PDF页码顺序</p>
<script>
const { PDFDocument } = PDFLib;
const uploadArea = document.getElementById('uploadArea');
const fileInput = document.getElementById('fileInput');
const pdfListEl = document.getElementById('pdfList');
const mergeBtn = document.getElementById('mergeBtn');
const clearBtn = document.getElementById('clearBtn');
const loadingEl = document.getElementById('loading');
let pdfFiles = []; // {name, bytes:Uint8Array, pageCount}
// 拖拽列表初始化
const sortable = new Sortable(pdfListEl, {
animation:150,
handle:'.drag-handle',
onEnd(evt){
const item = pdfFiles.splice(evt.oldIndex,1)[0];
pdfFiles.splice(evt.newIndex,0,item);
}
});
uploadArea.addEventListener('click',()=>fileInput.click());
uploadArea.addEventListener('dragover',e=>{e.preventDefault();uploadArea.classList.add('drag-over');});
uploadArea.addEventListener('dragleave',()=>uploadArea.classList.remove('drag-over'));
uploadArea.addEventListener('drop',async e=>{
e.preventDefault();
uploadArea.classList.remove('drag-over');
await handleFiles(e.dataTransfer.files);
});
fileInput.addEventListener('change',async ()=>{
await handleFiles(fileInput.files);
fileInput.value='';
});
// ========== 🔧 关键修复:使用 slice(0) 复制 ArrayBuffer ==========
async function handleFiles(fileList){
loadingEl.style.display='block';
for(let file of fileList){
if(file.type!=='application/pdf') continue;
const arrayBuf = await file.arrayBuffer();
// 复制两份完全独立的 ArrayBuffer
const readBuffer = arrayBuf.slice(0);
const storageBuffer = arrayBuf.slice(0);
const readCopy = new Uint8Array(readBuffer);
const storageCopy = new Uint8Array(storageBuffer);
const pdfDoc = await pdfjsLib.getDocument(readCopy).promise;
pdfFiles.push({
name: file.name,
bytes: storageCopy, // 独立副本,永远不会被 detach
pageCount: pdfDoc.numPages
});
}
renderList();
loadingEl.style.display='none';
}
function renderList(){
pdfListEl.innerHTML='';
pdfFiles.forEach((item,idx)=>{
const div = document.createElement('div');
div.className='pdf-item';
div.innerHTML=`
<span class="drag-handle">⋮⋮</span>
<div class="pdf-item-name">${item.name}</div>
<span class="pdf-item-page">${item.pageCount}页</span>
<button class="pdf-item-del" data-idx="${idx}">删除</button>
`;
pdfListEl.appendChild(div);
})
document.querySelectorAll('.pdf-item-del').forEach(btn=>{
btn.onclick=e=>{
const i = Number(e.target.dataset.idx);
pdfFiles.splice(i,1);
renderList();
}
})
}
mergeBtn.onclick = async ()=>{
if(pdfFiles.length===0){alert('请先添加PDF文件');return;}
loadingEl.style.display='block';
try{
const outPdf = await PDFDocument.create();
for(const f of pdfFiles){
// 从存储的副本再复制一份用于加载,确保万无一失
const copyBuf = new Uint8Array(f.bytes);
const srcPdf = await PDFDocument.load(copyBuf);
const pages = await outPdf.copyPages(srcPdf, srcPdf.getPageIndices());
pages.forEach(p=>outPdf.addPage(p));
}
const pdfBytes = await outPdf.save();
downloadFile(pdfBytes,'合并输出.pdf');
}catch(err){
alert('合并失败:'+err.message);
console.error(err);
}finally{
loadingEl.style.display='none';
}
}
function downloadFile(uint8,filename){
const blob = new Blob([uint8],{type:'application/pdf'});
const a = document.createElement('a');
a.href = URL.createObjectURL(blob);
a.download = filename;
a.click();
URL.revokeObjectURL(a.href);
}
clearBtn.onclick = ()=>{
pdfFiles=[];
renderList();
}
pdfjsLib.GlobalWorkerOptions.workerSrc = 'https://cdn.jsdelivr.net/npm/pdfjs-dist@3.4.120/build/pdf.worker.min.js';
</script>
</body>
</html>