拓十年匠心定制 · 商业建站与技术教学双线并行 咨询热线:400-886-1026 service@lmnt.cn
ARTICLE DETAIL

资讯详情

深耕网站建设与运营推广的一线实战洞察。

CodeCV 简历导出只出最后一页?一个油猴脚本搞定多页无水印 PDF(附完整源码)

CodeCV 简历导出只出最后一页?一个油猴脚本搞定多页无水印 PDF(附完整源码)

用 CodeCV 写完简历,兴冲冲点导出,结果两页只出来了第二页 —— 前面全白写。

这篇给出一个可直接用的油猴脚本,并说清原因、安装步骤和几个必须知道的坑。脚本基于原作者的 MIT 开源项目修改,已托管到 GitHub,点链接即可安装,文末附完整源码。


一、问题长什么样

CodeCV 导出 PDF 时,多页简历只会导出最后一页:

简历实际页数

导出结果

1 页

1 页,正常

2 页

1 页,内容是第 2 页

3 页

1 页,内容是第 3 页

单页永远正常,所以很多人以为是自己简历太长的问题 —— 其实是导出脚本的锅。


二、原因只有一行 CSS

CodeCV 的简历预览是三层绝对定位结构:

.re-render ← 最外层 └── .jufe-wrapper-page ← 页面容器 ├── .jufe-wrapper-page-item ← 第 1 页 ├── .jufe-wrapper-page-item ← 第 2 页 └── .jufe-wrapper-page-item ← 第

原脚本的打印样式里有这么一段:

.re-render, .jufe-wrapper-page, .jufe-wrapper-page-item { position: absolute !important; top: 0 !important; left: 0 !important; /* ← 元凶 */

.jufe-wrapper-page-item就是每一页。它们本该依次往下排,被强制top: 0之后全部叠在同一个坐标上—— 后一页盖住前一页,所以只剩最后一页。

这里还有第二个坑:同一段里的height: 297mm !important会把超出一页的内容裁掉。这个后面第六节单独讲。


三、修复思路:只改一个属性

分页思路本身没问题,问题在于不该动站点赖以定位的容器层。

正确做法是:保留absolute,只把每页的top从「都是 0」改成「第 n 页 = (n-1) × 297mm」:

第 1 页 top: 0mm 第 2 页 top: 297mm 第 3 页 top: 594

一页一张纸,内容不重叠,站点的定位体系一根毫毛都不动。

为什么不直接用更标准的break-after: page?我试过。CodeCV 的预览依赖三层absolute共用同一个坐标系,一旦把容器改成static走文档流,整块布局就散了 —— 内容重复、顺序错乱、页与页之间还露出深色底。在第三方站点上做适配,优先「最少改动」,而不是「最标准写法」。这是踩了三次坑换来的结论。


四、安装(点一下就行)

第 1 步浏览器先装Tampermonkey(Chrome / Edge 应用商店搜一下,装一次就够)。

第 2 步点下面的链接:

👉 一键安装脚本

Tampermonkey 会自动弹出安装页面,点「安装」即可。装好之后脚本跟随仓库自动更新。

装过旧版本?先在 Tampermonkey 仪表盘里删掉旧版,再点上面的链接,否则新旧两版会同时运行、页面上出现两个按钮。

第 3 步打开 CodeCV 简历页 →按 F5 刷新。

刷新后,页面左侧会出现两个按钮:

按钮

作用

🖨️ 导出完美简历

主按钮,一键导出

诊断

导出有问题时点它,会把页面结构信息自动复制到剪贴板

按钮只在页面加载时挂载,所以装完必须刷新一次CodeCV 页面。

备选安装方式

方式

地址

jsDelivr(国内推荐)

https://cdn.jsdelivr.net/gh/XXWen0201/codecv-export@main/codecv-export.user.js

GitHub 仓库

XXWen0201/codecv-export

GitHub Raw

https://raw.githubusercontent.com/XXWen0201/codecv-export/main/codecv-export.user.js

手动粘贴

复制文末完整源码→ Tampermonkey 新建脚本 → 全选删除模板 → 粘贴 →Ctrl + S


五、怎么用

点「导出完美简历」,在弹出的打印对话框里按下表设置:

选项

值

目标 / 打印机

另存为 PDF

纸张

A4

边距

无

背景图形

勾选(不勾的话色块、分隔线会丢)

直接按Ctrl + P也照样生效,不必先点按钮。

如果导出结果不对,打开F12控制台还有几个排错用的命令:

命令

作用

__codecvExport.dump()

打印并复制完整结构诊断

__codecvExport.detectPages()

看看脚本把哪些元素识别成了「页」

__codecvExport.checkOverflow(pages)

返回内容超出一页 A4 的页码

__codecvExport.restore()

手动还原打印排版


六、必须知道的三个坑

1. 某一页内容超过一页 A4,超出的部分会静默丢失

打印样式给每页写死了height: 297mm,超出部分会被裁掉。页数还是对的,肉眼很难发现。

我做过压力测试:一页里灌入 46 段文字,A4 内只装得下约 29 段,第 30~46 段从生成的 PDF 里彻底消失(不是画面上裁掉,是根本没输出)。

这个修不掉 —— 把高度放开或改成overflow: visible,只会让内容盖到下一页上,更糟。

所以脚本内置了告警:导出时若检测到某页超长,会在页面下方弹红条写明具体页码,控制台也会console.warn提示。

看到红条,就去 CodeCV 里把那一页精简到一页之内。

2. 只在浏览器渲染层生效

脚本处理的是你本机已经渲染出来的页面内容:不构造请求、不绕过付费墙、不批量抓取。

如果站点是把水印烧进服务端生成的文件里,脚本无能为力 —— 它只能在浏览器渲染层隐藏页面上已有的元素。

3. 请只用于导出你自己的简历

脚本去水印靠的是隐藏页面上已有的水印元素,不是破解。请勿用于他人简历或绕过付费。


七、常见问题

Q:安装链接点不开 / 打开是一堆代码?

说明 Tampermonkey 没装或没启用。先确认地址栏右侧有 Tampermonkey 图标,再重新点链接。若网络访问不到 jsDelivr,改用文末的完整源码手动安装。

Q:导出后是空白纸张?

先按 F5 刷新页面。脚本只改内联样式和类名,刷新即可恢复。若刷新后仍空白,点「诊断」按钮把输出贴出来。

Q:页数对了,但内容重复 / 错位?

大概率是 CodeCV 改版了。点「诊断」按钮 —— 它会把每页的类名、position/top/height、祖先链等自动复制到剪贴板,改CONFIG.pageSelector即可适配,也欢迎到 Issues 反馈。

Q:页面左侧没有按钮?

① 确认油猴脚本是启用状态;② 确认已经按 F5 刷新过;③ 确认地址是www.codecvcv.com/cv/...(脚本按这个路径匹配)。

Q:水印还在?

脚本按class/id里含watermark的元素来隐藏水印。如果 CodeCV 换了命名方式,把「诊断」结果发出来即可。

Q:不想装 Tampermonkey?

可以改造成 Chrome 扩展(Manifest V3),代价是多一步"加载已解压的扩展程序"。有需要再补。


八、我怎么确认它真的修好了

不是"看着像好了",是用本机 Chrome 无头模式真实打印成 PDF,再用 pypdf 逐页数页数、提取文本比对。

把脚本原文注入模拟页面(而不是手抄一份 CSS 进去),由脚本自己去注入样式、自己跑布局逻辑,再打印:

模拟简历

导出页数

内容完整

误报

1 页

1 页

是

无

2 页

2 页

是

无

3 页

3 页

是

无

5 页

5 页

是

无

超长内容

2 页

正确检出超长并向用户告警

—

同时还验证了「导出后是否干净还原」:内联样式清空、标记类摘除、水印元素数量不变(证明是隐藏而不是删除)。

一个容易忽略的细节:页数对,不等于脚本生效 —— 不注入打印样式时,页数也可能碰巧对上。所以模拟页里埋了三个「诱饵」元素(干扰层、校验徽标、水印),只有脚本真的执行了它们才会从 PDF 里消失。校验脚本就在仓库的test/verify_e2e.py,可以自己跑。


九、更新日志

版本

改动

v4.1.0

水印从"直接删 DOM"改为可恢复的隐藏(避免误删内容);还原top时补回!important优先级;新增「内容超出一页 A4」告警

v4.0.0

核心修复:每页top逐页错开

原版

Campus 的单页导出脚本

早期两版方案(改成文档流 +break-after: page、按position批量隐藏元素)会破坏站点布局或导致内容消失,已废弃,不再提供。


十、完整源码

如果安装链接用不了,复制下面全部内容,粘贴进 Tampermonkey 新建脚本里,Ctrl + S保存即可。

// ==UserScript== // @name CodeCV 简历一键无水印导出(多页修复版) // @name:en CodeCV Resume One-Click PDF Export (multi-page fix) // @namespace https://github.com/XXWen0201/codecv-export // @version 4.1.0 // @description 一键把 CodeCV 简历导出成 A4 高清 PDF,修复原版「多页简历只导出最后一页」的问题;打印后自动还原页面,水印只隐藏不删除。 // @author Campus(原版)· 多页修复 by XXWen0201 // @match *://www.codecvcv.com/cv/* // @match *://codecvcv.com/cv/* // @grant none // @license MIT // @run-at document-idle // @homepageURL https://github.com/XXWen0201/codecv-export // @supportURL https://github.com/XXWen0201/codecv-export/issues // @icon https://cdn.jsdelivr.net/gh/XXWen0201/codecv-export@main/icons/icon128.png // @downloadURL https://cdn.jsdelivr.net/gh/XXWen0201/codecv-export@main/codecv-export.user.js // @updateURL https://cdn.jsdelivr.net/gh/XXWen0201/codecv-export@main/codecv-export.user.js // ==/UserScript== /* ============================================================================ * 【v4 设计原则:能不动就不动】 * ---------------------------------------------------------------------------- * 前两版翻车原因(都是"改动超出必要范围"): * v2 把 CodeCV 依赖的 absolute 定位容器改成 static → 内容重复、错位、露黑底。 * v3 加了「隐藏所有 position:fixed/sticky 元素」的规则去清浮层, * 结果把简历正文容器一起隐藏了 → 纸张还在、内容全空。 * * v4 只做一件事:把每一页的 top 从「全部 0」改成「第 n 页 = (n-1) × 297mm」。 * 其余所有规则**逐字沿用原脚本**,一个字都不多加。 * 浮层改成按文字特征精准匹配(且限定在小尺寸徽标上),绝不再批量隐藏元素。 * * 【根因】原脚本的这一段把每一页都压在同一个点 (0,0) 上, * 后一页盖住前一页,所以两页简历只剩最后一页: * .re-render, .jufe-wrapper-page, .jufe-wrapper-page-item { * position: absolute !important; * top: 0 !important; left: 0 !important; * } * 只需让每页的 top 依次为 0 / 297mm / 594mm……即恢复成一页一张纸。 * * 已用无头 Chrome 实测:原版 CSS 输出 1 页;本方案输出页数与实际一致。 * * v4.1 只修「潜伏的雷」,不动任何已跑通的逻辑: * a) 水印原来用 el.remove() 直接删 DOM —— 不可逆。一旦误命中包裹简历的容器, * 内容会被永久删掉(和 v3 空白 bug 同源)。改为「打印时隐藏 + 打印后恢复」, * 并且明确跳过「包裹着页元素的祖先」,宁可漏隐藏也不动简历。 * b) 还原 top 时丢了 !important 优先级 —— 改为连优先级一起存、一起还原。 * c) 新增「某页内容超出一页 A4」告警 —— 这是原脚本就有的静默丢失路径: * 打印样式给每页写死 height:297mm,超出部分会从 PDF 里彻底消失且毫无提示。 * 修不掉(放开 height 只会盖到下一页),所以至少明确报警。 * 另:__codecvExport 上多挂了 checkOverflow / layout / restore,方便手动排错。 * * 【合规边界】 * 只处理你自己本机已渲染出来的简历内容。不构造请求、不绕过付费墙、不批量抓取。 * 站点若在服务端往导出文件里烧水印,本脚本无能为力。 * ========================================================================== */ (function () { 'use strict'; /* ======================================================================== * CONFIG —— 唯一适配层:站点改版只改这里 * ====================================================================== */ const CONFIG = { pageSelector: '.jufe-wrapper-page-item', pageFallbacks: [ '.jufe-wrapper-page > *', '[class*="page-item"]', '.re-render > *', '[class*="wrapper-page"]' ], singlePageSelector: '.markdown-transform-html', pageHeightMM: 297, // 一页的高度(A4) hideWatermark: true, // 打印时隐藏类名含 watermark 的元素(原脚本是直接删 DOM,这里改为可恢复的隐藏) hideBadges: true, // 打印前隐藏「正在校验」这类小徽标(精准匹配,不批量隐藏) debug: false }; /* ====================================================================== */ const BTN_ID = 'codecv-export-btn'; const DIAG_ID = 'codecv-diag-btn'; const TOAST_ID = 'codecv-toast'; const PAGE_MARK = 'codecv-print-page'; const BADGE_CLASS = 'codecv-hide-badge'; const TOP_FLAG = 'data-codecv-top'; const TOP_PRIO_FLAG = 'data-codecv-top-prio'; // 「校验 / 防伪」类小徽标的文字特征 const BADGE_RE = /正在校验|校验中|校验第|防伪|仅供预览|禁止转载|下载后|请勿外传/; /* --------------------------------------------------------------------- * 1. 样式 * —— 打印部分严格沿用原脚本,只在标注「★」的两处做了必要调整 * ------------------------------------------------------------------- */ const CSS = ` /* ===== 屏幕态:按钮 ===== */ #${BTN_ID} { position: fixed; top: 50%; left: 0; transform: translateY(-50%); z-index: 2147483647; width: 35px; padding: 15px 5px; background: #333; color: #fff; border: none; outline: none; border-radius: 0 5px 5px 0; cursor: pointer; writing-mode: vertical-lr; font-size: 12px; letter-spacing: 1px; font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif; opacity: .5; transition: .3s; } #${BTN_ID}:hover { opacity: 1; background: #ff7449; } #${DIAG_ID} { position: fixed; top: calc(50% + 80px); left: 0; z-index: 2147483647; width: 35px; padding: 8px 5px; background: #777; color: #fff; border: none; outline: none; border-radius: 0 5px 5px 0; cursor: pointer; writing-mode: vertical-lr; font-size: 11px; letter-spacing: 1px; font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif; opacity: .35; transition: .3s; } #${DIAG_ID}:hover { opacity: 1; background: #ff7449; } #${TOAST_ID} { position: fixed; left: 50%; bottom: 40px; transform: translateX(-50%); z-index: 2147483647; max-width: 78vw; padding: 10px 18px; background: rgba(30,30,30,.92); color: #fff; border-radius: 8px; font-size: 13px; line-height: 1.6; font-family: system-ui, -apple-system, "Microsoft YaHei", sans-serif; opacity: 0; pointer-events: none; transition: opacity .25s; white-space: pre-wrap; text-align: center; } #${TOAST_ID}.show { opacity: 1; } #${TOAST_ID}.warn { background: rgba(200,60,40,.94); } /* ===== 打印态 ===== */ @media print { /* --- 原脚本的隐藏列表,仅追加我们自己的三个元素 --- */ #${BTN_ID}, #${DIAG_ID}, #${TOAST_ID}, .tamper-print-btn, .operator, #header-nav, .markdown-edit, .shadow, .history, .crisp-client, #ark-float-btn, .el-scrollbar__bar, .toggle-edit-mode, .preview-cv::before, .preview-cv::after, .${BADGE_CLASS} { display: none !important; } /* --- 原脚本的容器重置,逐字不动 --- */ html, body, #__nuxt, #main, #editor, .flex, .preview-cv, .el-scrollbar__wrap, .el-scrollbar__view { margin: 0 !important; padding: 0 !important; height: auto !important; width: 100% !important; display: block !important; position: static !important; overflow: visible !important; background: white !important; } /* --- 两层容器:定位方式原样保留(absolute + top:0)--- ★调整:放开 height 与 overflow,否则第 2 页之后会被裁掉; 背景置透明,避免页与页之间露出深色底。 --- */ .re-render, .jufe-wrapper-page { position: absolute !important; top: 0 !important; left: 0 !important; width: 100% !important; height: auto !important; max-height: none !important; overflow: visible !important; margin: 0 !important; padding: 0 !important; border: none !important; box-shadow: none !important; transform: none !important; background: transparent !important; } /* --- ★核心修复★ 每一页仍然 absolute(与站点一致), 只是 top 不再写死 0 —— 由 JS 写成 0 / 297mm / 594mm … --- */ .jufe-wrapper-page-item, .${PAGE_MARK} { position: absolute !important; left: 0 !important; width: 100% !important; height: 297mm !important; margin: 0 !important; padding: 0 !important; border: none !important; box-shadow: none !important; transform: none !important; background: #fff !important; } /* JS 万一没跑到时的兜底:按序号排开(最多 10 页) */ .jufe-wrapper-page-item:nth-of-type(1), .${PAGE_MARK}:nth-of-type(1) { top: 0mm !important; } .jufe-wrapper-page-item:nth-of-type(2), .${PAGE_MARK}:nth-of-type(2) { top: 297mm !important; } .jufe-wrapper-page-item:nth-of-type(3), .${PAGE_MARK}:nth-of-type(3) { top: 594mm !important; } .jufe-wrapper-page-item:nth-of-type(4), .${PAGE_MARK}:nth-of-type(4) { top: 891mm !important; } .jufe-wrapper-page-item:nth-of-type(5), .${PAGE_MARK}:nth-of-type(5) { top: 1188mm !important; } .jufe-wrapper-page-item:nth-of-type(6), .${PAGE_MARK}:nth-of-type(6) { top: 1485mm !important; } .jufe-wrapper-page-item:nth-of-type(7), .${PAGE_MARK}:nth-of-type(7) { top: 1782mm !important; } .jufe-wrapper-page-item:nth-of-type(8), .${PAGE_MARK}:nth-of-type(8) { top: 2079mm !important; } .jufe-wrapper-page-item:nth-of-type(9), .${PAGE_MARK}:nth-of-type(9) { top: 2376mm !important; } .jufe-wrapper-page-item:nth-of-type(10), .${PAGE_MARK}:nth-of-type(10) { top: 2673mm !important; } /* --- 原脚本的正文内边距 --- */ .markdown-transform-html { padding: 8mm 12mm !important; } /* --- 原脚本的页面设置 --- */ @page { size: A4 portrait; margin: 0; } * { -webkit-print-color-adjust: exact !important; print-color-adjust: exact !important; } } `; const styleEl = document.createElement('style'); styleEl.textContent = CSS; (document.head || document.documentElement).appendChild(styleEl); /* --------------------------------------------------------------------- * 2. 页元素探测 * ------------------------------------------------------------------- */ function isPageLike(el) { return el && el.nodeType === 1 && el.getBoundingClientRect().height > 400; } function detectPages() { let pages = Array.from(document.querySelectorAll(CONFIG.pageSelector)); if (pages.length) return pages; for (const sel of CONFIG.pageFallbacks) { pages = Array.from(document.querySelectorAll(sel)).filter(isPageLike); if (pages.length) return pages; } const md = document.querySelector(CONFIG.singlePageSelector); if (md) return [md]; return []; } /* --------------------------------------------------------------------- * 3. 打印布局:只做「逐页错开 top」这一件事 * ------------------------------------------------------------------- */ let lastOverflow = []; // 上一次检查中「内容超出一页」的页码 /** * 检查有没有哪一页内容超过了一页 A4。 * * 背景:打印样式给每页写死 `height: 297mm`(沿用原脚本),而站点在页元素上 * 有 `overflow: hidden` —— 一旦某页内容超过 297mm,**超出部分会被静默裁掉, * 页数还是对的,肉眼很难发现**。这是原脚本就存在的设计,改不掉(改成 * overflow: visible 会让它盖到下一页上,更糟),所以至少给个明确告警。 * * 必须在「套用打印样式之前」量,此时还是屏幕态:clientHeight 是一页高度, * scrollHeight 是内容真实高度。clientHeight 太小(元素被隐藏)时跳过,避免误报。 */ function checkOverflow(pages) { const out = []; pages.forEach((el, i) => { if (el.clientHeight > 100 && el.scrollHeight > el.clientHeight + 2) { out.push(i + 1); } }); return out; } function applyPrintLayout() { const pages = detectPages(); if (!pages.length) return null; // 先量,再改样式 —— 顺序反了就量不准(那时打印样式已生效) lastOverflow = checkOverflow(pages); pages.forEach((el, i) => { if (!el.hasAttribute(TOP_FLAG)) { // 连同「优先级」一起存:站点若把 top 写成 !important,还原时不能丢 el.setAttribute(TOP_FLAG, el.style.getPropertyValue('top') || ''); el.setAttribute(TOP_PRIO_FLAG, el.style.getPropertyPriority('top') || ''); } el.classList.add(PAGE_MARK); el.style.setProperty('top', (i * CONFIG.pageHeightMM) + 'mm', 'important'); }); if (CONFIG.hideBadges) hideBadges(pages); hideWatermarks(pages); return pages; } function restorePrintLayout() { document.querySelectorAll('[' + TOP_FLAG + ']').forEach(el => { const original = el.getAttribute(TOP_FLAG); const prio = el.getAttribute(TOP_PRIO_FLAG) || ''; el.style.removeProperty('top'); if (original) el.style.setProperty('top', original, prio); el.removeAttribute(TOP_FLAG); el.removeAttribute(TOP_PRIO_FLAG); el.classList.remove(PAGE_MARK); }); document.querySelectorAll('.' + BADGE_CLASS).forEach(el => { el.classList.remove(BADGE_CLASS); }); } /** * 隐藏「正在校验第 N 页」这类小徽标。 * 严格限定:文字匹配 + 尺寸很小 + 不在简历页的祖先链上。 * 绝不像 v3 那样按 position 批量隐藏元素(那会连正文容器一起干掉)。 */ function hideBadges(pages) { if (!document.body) return; const protect = new Set(); pages.forEach(p => { let e = p; while (e) { protect.add(e); e = e.parentElement; } }); const candidates = document.body.querySelectorAll('div,span,p,section,aside,button,label'); for (const el of candidates) { if (protect.has(el) || el.id === BTN_ID || el.id === DIAG_ID || el.id === TOAST_ID) continue; const t = (el.textContent || '').trim(); if (!t || t.length > 60 || !BADGE_RE.test(t)) continue; const r = el.getBoundingClientRect(); if (r.width > 0 && r.width <= 340 && r.height <= 140) { el.classList.add(BADGE_CLASS); } } } /** * 打印时隐藏水印。 * * 注意两点(都是踩过的雷): * 1. 绝不用 el.remove()。那是不可逆的 —— 一旦某天误命中「包裹着简历的容器」, * 内容会被永久删掉,刷新都救不回来(v3 的空白 bug 就是这么来的)。 * 这里只挂一个「仅打印态生效」的隐藏类,打印结束由 restorePrintLayout() 摘掉。 * 2. 明确跳过「祖先」:若某元素内部**包含**页元素,说明它是简历的外壳,一律不碰。 * 宁可漏隐藏一个水印,也不能动简历本身。 */ function hideWatermarks(pages) { if (!CONFIG.hideWatermark) return 0; let n = 0; document.querySelectorAll('[class*="watermark"], [id*="watermark"]').forEach(el => { if (pages.some(p => el !== p && el.contains(p))) return; // 是简历祖先,跳过 el.classList.add(BADGE_CLASS); n++; }); return n; } /* --------------------------------------------------------------------- * 4. 打印主流程 * ------------------------------------------------------------------- */ let printing = false; function runPrint() { const pages = applyPrintLayout(); if (!pages) { toast('没找到简历内容。\n请先进入简历预览页(能看到白色纸张)再点导出。', true); return; } let msg = `检测到 ${pages.length} 页\n正在唤起打印…`; if (lastOverflow.length) { msg += `\n⚠️ 第 ${lastOverflow.join(' / ')} 页内容超出一页 A4,` + `超出部分会被裁掉,请先在 CodeCV 里精简该页`; console.warn('[CodeCV] 内容超出一页 A4 的页码:', lastOverflow); } toast(msg, lastOverflow.length > 0); requestAnimationFrame(() => { setTimeout(() => { if (printing) return; printing = true; window.print(); setTimeout(() => { printing = false; restorePrintLayout(); }, 1500); }, 60); }); } /* --------------------------------------------------------------------- * 5. UI * ------------------------------------------------------------------- */ function toast(msg, warn) { let el = document.getElementById(TOAST_ID); if (!el) { el = document.createElement('div'); el.id = TOAST_ID; document.body.appendChild(el); } el.textContent = msg; el.className = 'show' + (warn ? ' warn' : ''); clearTimeout(el._t); el._t = setTimeout(() => { el.className = warn ? 'warn' : ''; }, 3200); } function mountButtons() { if (document.body && !document.getElementById(BTN_ID)) { const btn = document.createElement('button'); btn.id = BTN_ID; btn.type = 'button'; btn.textContent = '🖨️ 导出完美简历'; btn.title = '一键导出 A4 无水印 PDF(支持多页)'; btn.addEventListener('click', runPrint); document.body.appendChild(btn); } if (document.body && !document.getElementById(DIAG_ID)) { const d = document.createElement('button'); d.id = DIAG_ID; d.type = 'button'; d.textContent = '诊断'; d.title = '点一下:把页面结构信息复制到剪贴板(导出有问题时用)'; d.addEventListener('click', dump); document.body.appendChild(d); } } /* --------------------------------------------------------------------- * 6. 钩子:直接按 Ctrl+P 也生效 * ------------------------------------------------------------------- */ window.addEventListener('beforeprint', () => { applyPrintLayout(); }); window.addEventListener('afterprint', () => { printing = false; restorePrintLayout(); }); /* --------------------------------------------------------------------- * 7. 诊断 dump —— 点「诊断」按钮即可,会把结构复制到剪贴板 * ------------------------------------------------------------------- */ function describe(el) { if (!el) return 'null'; let cls = ''; try { cls = (typeof el.className === 'string' ? el.className : '').trim().replace(/\s+/g, '.'); } catch (e) { cls = ''; } return el.tagName.toLowerCase() + (cls ? '.' + cls.slice(0, 90) : ''); } function dump() { const pages = detectPages(); const L = []; const push = s => L.push(s); push('===== CodeCV 导出脚本 结构诊断 (v4.1) ====='); push('URL: ' + location.href); push('窗口: ' + window.innerWidth + 'x' + window.innerHeight + ' dpr=' + window.devicePixelRatio); push('探测到页数: ' + pages.length); if (pages.length) { const of = checkOverflow(pages); push('内容超出一页 A4 的页码: ' + (of.length ? of.join(', ') + ' ← 这些页会被裁掉' : '无')); } push(''); if (!pages.length) push('!! 没探测到页元素,请检查 CONFIG.pageSelector'); pages.forEach((el, i) => { const cs = getComputedStyle(el); const r = el.getBoundingClientRect(); push(`--- 第 ${i + 1} 页 ---`); push(' 选择器: ' + describe(el)); push(` rect: top=${r.top.toFixed(0)} left=${r.left.toFixed(0)} ` + `w=${r.width.toFixed(0)} h=${r.height.toFixed(0)}`); push(` offsetTop=${el.offsetTop} offsetLeft=${el.offsetLeft}`); push(` position=${cs.position} top=${cs.top} left=${cs.left} ` + `height=${cs.height} overflow=${cs.overflow}`); push(` transform=${cs.transform}`); push(` margin=${cs.margin}`); push(` padding=${cs.padding}`); push(` offsetParent: ${describe(el.offsetParent)}`); push(` 父节点: ${describe(el.parentElement)}`); push(` 文本开头: ${(el.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 45)}`); }); if (pages.length) { push(''); push('--- 第 1 页的祖先链(往上到 body)---'); let el = pages[0].parentElement; let d = 0; while (el && el !== document.body && d < 12) { const cs = getComputedStyle(el); push(` ${'.'.repeat(d)}${describe(el)} position=${cs.position} ` + `height=${cs.height} overflow=${cs.overflow} ` + `transform=${cs.transform} bg=${cs.backgroundColor} margin=${cs.margin}`); el = el.parentElement; d++; } const bcs = getComputedStyle(document.body); push(` body margin=${bcs.margin} bg=${bcs.backgroundColor} ` + `scrollHeight=${document.body.scrollHeight}`); } push(''); push('--- 深色大块背景(排查页间黑条)---'); let dark = 0; if (document.body) { for (const el of document.body.querySelectorAll('*')) { const cs = getComputedStyle(el); const m = cs.backgroundColor.match(/rgba?\((\d+),\s*(\d+),\s*(\d+)/); if (!m) continue; const lum = 0.299 * +m[1] + 0.587 * +m[2] + 0.114 * +m[3]; const r = el.getBoundingClientRect(); if (lum < 130 && r.width > 300 && r.height > 20) { push(` ${describe(el)} bg=${cs.backgroundColor} ` + `size=${r.width.toFixed(0)}x${r.height.toFixed(0)}`); if (++dark >= 12) break; } } } if (!dark) push(' (无)'); push(''); push('--- position:fixed / sticky 元素(仅列出,供排查浮层)---'); let fx = 0; if (document.body) { for (const el of document.body.querySelectorAll('*')) { const cs = getComputedStyle(el); if (cs.position === 'fixed' || cs.position === 'sticky') { const t = (el.innerText || '').replace(/\s+/g, ' ').trim().slice(0, 28); const r = el.getBoundingClientRect(); push(` ${describe(el)} ${cs.position} ` + `${r.width.toFixed(0)}x${r.height.toFixed(0)} 文本=${t}`); if (++fx >= 20) break; } } } if (!fx) push(' (无)'); push(''); push('===== 诊断结束 ====='); const text = L.join('\n'); console.log(text); try { navigator.clipboard.writeText(text).then( () => toast('诊断信息已复制到剪贴板,直接粘贴发我即可'), () => toast('已输出到控制台,请手动复制(F12 → Console)', true) ); } catch (e) { toast('已输出到控制台,请手动复制(F12 → Console)', true); } return text; } window.__codecvExport = { print: runPrint, dump: dump, detectPages: detectPages, checkOverflow: checkOverflow, // 返回「内容超出一页 A4」的页码数组 layout: applyPrintLayout, // 手动套用打印布局(不弹打印框

十一、协议与致谢

  • 原脚本:CodeCV 简历一键无水印导出byCampus(MIT License) 发布地址:https://greasyfork.org/scripts/575852
  • 本版本基于原脚本修改,同样以MIT License发布,并保留原作者版权声明
  • 修复版仓库:XXWen0201/codecv-export,问题反馈走 Issues
  • 本文为原创内容;代码部分为原项目的衍生作品,依据 MIT 协议修改与再分发

MIT 协议允许修改和再发布,但要求保留原作者的版权声明与许可证。如果你是二次修改后再发布,请同样保留这一段。

返回列表