为 Resume-Matcher 添加全新简历模板:从组件到 PDF 渲染的完整实战指南
【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher
本文围绕 Resume-Matcher 前端的简历模板系统,完整讲解如何从零新增一套简历排版模板:从创建
resume-{name}.tsx组件、理解TemplateProps数据契约与必备 CSS 类,到注册进模板选择器、生成缩略图,并最终打通 Playwright 驱动的 PDF 渲染链路。读完你将掌握一套可复现、可验证的模板接入流程,并理解模板设置(TemplateSettings)如何在实时预览与打印 PDF 之间共享同一套 CSS 变量体系。
模板系统全景:先理解你要接入的骨架
Resume-Matcher 的简历模板系统是一个"前端组件 + 设置类型 + CSS 变量 + 打印路由"四位一体的架构。在动手添加新模板之前,需要先看清这条链路:
- 模板组件:每个模板是一个 React 组件,位于 components/resume/,例如
resume-single-column.tsx、resume-two-column.tsx、resume-modern.tsx等; - 统一出口:components/resume/index.ts 集中 re-export 所有模板组件,是外部引用模板的唯一入口;
- 设置类型定义:lib/types/template-settings.ts 定义了
TemplateSettings结构、默认值、以及从设置到 CSS 变量的映射函数settingsToCssVars; - UI 控制面板:components/builder/formatting-controls.tsx 提供模板选择与全部排版控件;
- 渲染与打印:实时预览通过 CSS 变量直接作用,PDF 生成则由后端 routers/resumes.py 拼接打印 URL,交给 Playwright 打开 print/resumes/[id] 无头渲染。
当前仓库已经注册了七套模板(见 template-settings.ts 与 template-registration.test.ts 的断言),它们是你新增模板时最好的参考实现:
| 模板 ID | 布局特征 | 适用场景 |
|---|---|---|
swiss-single | 全宽单栏,内容密度最高 | 1–2 页标准简历 |
swiss-two-column | 65% 主栏 + 35% 侧栏 | 内容密集的简历 |
modern | 单栏 + 彩色强调标题 | 需要主题色的单栏简历 |
modern-two-column | 65% + 35% 双栏 + 强调色 | 彩色密集内容 |
latex | 单栏衬线体、Title-Case 下划线标题、公司优先条目(LaTeX 风格) | 学术/经典简历 |
clean | 极简无衬线单栏、大号灰色 UPPERCASE 标题、单行条目 | 低调现代简历 |
vivid | 63% + 37% 彩色双栏(Awesome-CV 血统) | 彩色强调色简历 |
其中latex与clean属于"单字体族"模板:选择它们时会通过applyTemplatePreset自动注入签名字体(latex 全衬线、clean 全无衬线),但两个字体控件仍然可用,用户可随后覆盖。
快速接入:新增模板的四步清单
官方指南给出了一条精简路径,这也是模板接入的"最小操作集":
- 创建
components/resume/resume-{name}.tsx—— 编写模板组件本体; - 从
components/resume/index.ts导出 —— 让模板进入统一出口; - 加入
FormattingControls模板选择器 —— 让用户在构建器中可见可选; - 创建缩略图 —— 为选择器提供可视化预览。
值得注意的是,模板类型TemplateType(swiss-single、swiss-two-column、modern、modern-two-column、latex、clean、vivid)同样定义在 template-settings.ts,因此新增模板时还需要在类型定义中追加你的模板 ID,并在TEMPLATE_OPTIONS元数据数组(template-settings.ts)中登记名称与描述——否则选择器拿不到可渲染的选项。仓库的单元测试 template-registration.test.ts 会校验所有模板 ID 唯一且元数据非空,这相当于模板注册的"质量门禁"。
模板组件:实现TemplateProps契约
每个模板组件都遵循同一份数据契约。指南中的TemplateProps是理论接口,而当前仓库实际模板的 props 略有演化——以 resume-vivid.tsx 为例,真实签名是:
interface ResumeVividProps { data: ResumeData; // 简历完整数据 showContactIcons?: boolean; // 是否显示联系图标 sectionHeadings?: Partial<ResumeSectionHeadings>; // 章节标题(支持 i18n) fallbackLabels?: Partial<ResumeFallbackLabels>; // 兜底文案 }而ResumeData的结构(见 docs/agent/design/template-system.md)包含:personalInfo(姓名、职位、联系方式)、summary、workExperience、education、personalProjects、additional(技能/语言/证书/奖项)、sectionMeta(顺序与可见性)以及customSections(用户自定义章节)。
指南给出的骨架可以视为新模板的起点,结合真实实现,一个最小但完整的模板组件应当具备以下要素:
import { getSortedSections, getSectionMeta } from '@/lib/utils/section-helpers'; import { SafeHtml } from './safe-html'; import baseStyles from './styles/_base.module.css'; import styles from './styles/your-template.module.css'; export function ResumeNewTemplate({ data, showContactIcons = false }: ResumeNewTemplateProps) { const { personalInfo, summary, workExperience } = data; const sortedSections = getSortedSections(data); // 按用户设定的顺序/可见性排序 const allSections = getSectionMeta(data); const isSectionVisible = (key: string) => allSections.find((s) => s.key === key)?.isVisible ?? true; return ( <div className="resume-print"> {/* Header */} <header className={baseStyles['resume-header']}> <h1 className={baseStyles['resume-name']}>{personalInfo?.name}</h1> {personalInfo?.title && <div className={baseStyles['resume-title']}>{personalInfo.title}</div>} </header> {/* Sections */} {sortedSections.map((section) => ( <section key={section.id} className={baseStyles['resume-section']}> <h3 className={baseStyles['resume-section-title']}>{section.displayName}</h3> <div className={baseStyles['resume-items']}> {/* 按 section.sectionType 渲染 text / itemList / stringList */} </div> </section> ))} </div> ); }几个关键实现细节值得注意:
- 章节顺序与可见性:不要硬编码章节顺序,应使用
getSortedSections/getSectionMeta(见 lib/utils/section-helpers.ts),这样用户在主简历中调整的顺序与可见性才能在模板中生效; - 富文本安全渲染:条目描述等富文本字段应通过
SafeHtml(components/resume/safe-html.tsx)渲染,保证 HTML 被清洗后再输出; - 自定义章节支持:用户通过
AddSectionDialog添加的自定义章节(text/itemList/stringList三种类型)应像 resume-vivid.tsx 中的DynamicResumeSectionVivid那样被模板渲染,而不是被忽略。
必备 CSS 类:模板的"结构协议"
指南强调模板必须提供以下 CSS 类,这既是样式约定,也是 PDF 渲染的依赖:
.resume-print /* 根容器(Playwright 等待此选择器出现) */ .resume-section /* 章节容器 */ .resume-section-title /* 章节标题 */ .resume-items /* 条目容器 */ .resume-item /* 单个条目(禁止跨页断行) */在当前实现中,这些类由共享样式表 styles/_base.module.css 提供,关键行为包括:
.resume-section通过margin-bottom: var(--section-gap)控制章节间距;.resume-items使用display: flex; flex-direction: column; gap: var(--item-gap)布局条目;.resume-item设置了break-inside: avoid; page-break-inside: avoid,保证单条记录在 PDF 与打印输出中不被拦腰截断;.resume-section-title设置了break-after: avoid与orphans/widows: 3,防止标题孤立在页尾;@media print块中进一步强化了这些防断页规则。
因此,新模板不要重新发明这些类,而应直接复用baseStyles(即_base.module.css的模块化导入),例如baseStyles['resume-section']、baseStyles['resume-item-subtitle']。这样你的模板会自动响应格式化面板中所有间距与排版设置。模板特有的视觉(配色、字体变体、特殊组件)则放在自己的*.module.css中,例如 styles/vivid.module.css 之于vivid。
基础样式表还提供了专门的字号语义类,用于提升副标题可读性(详见 docs/agent/features/resume-templates.md):
| 类 | 字号 | 字重 | 用途 |
|---|---|---|---|
resume-item-subtitle | 0.95× 基准 | 600 | 公司名、学位、项目角色 |
resume-item-subtitle-sm | 0.88× 基准 | 600 | 紧凑双栏布局中的同字段 |
相比通用resume-meta类(0.82× 基准、字重 400),这两类让副标题大 13–16% 且为半粗体,视觉层级更清晰。
导出与注册:让模板"可见"
第一步:统一出口导出。在 components/resume/index.ts 中追加一行:
// components/resume/index.ts export { ResumeSingleColumn } from './resume-single-column'; export { ResumeTwoColumn } from './resume-two-column'; // ... 既有导出 export { ResumeNewTemplate } from './resume-new-template'; // 新增第二步:登记元数据。在 lib/types/template-settings.ts 中,把模板 ID 加入TemplateType联合类型,并在TEMPLATE_OPTIONS数组中追加条目:
export type TemplateType = | 'swiss-single' | 'swiss-two-column' | 'modern' | 'modern-two-column' | 'latex' | 'clean' | 'vivid' | 'new-template'; // 新增 // TEMPLATE_OPTIONS 中追加 { id: 'new-template', name: 'New Template', description: '...' },第三步:接入选择器。指南中示意在 components/builder/formatting-controls.tsx 维护一个TEMPLATES数组。当前仓库实现已经演化为:选择器直接消费TEMPLATE_OPTIONS元数据,并在 template-selector.tsx 中用TemplateThumbnail渲染每个模板的迷你缩略图。因此真正需要做的是:
- 在
TemplateThumbnail中为你的模板 ID 增加一个分支,绘制代表该布局的线框缩略图(可参考 template-selector.tsx 中七个既有分支的写法); - 为模板名称/描述补充 i18n 文案(formatting-controls.tsx 中的
templateLabels使用useTranslations读取翻译键)。
模板设置与 CSS 变量:让控件自动生效
新增模板最省力的地方在于:只要你的组件复用baseStyles并通过settingsToCssVars注入的 CSS 变量取样式,格式化面板里几乎所有控件就会"免费"生效。
settingsToCssVars(template-settings.ts)会把TemplateSettings映射为一组 CSS 自定义属性:
--section-gap、--item-gap、--line-height—— 间距体系--font-size-base、--header-scale、--section-header-scale—— 字号体系--header-font、--body-font—— 字体族--margin-top/bottom/left/right—— 页面边距--resume-accent-primary、--resume-accent-light—— 强调色(modern / modern-two-column / vivid 使用)
控件取值范围与默认值(来自 docs/agent/features/resume-templates.md 及源码映射表):
| 控件 | 取值范围 | 默认值 | 效果 |
|---|---|---|---|
| Margins | 5–25mm | 10mm(控制面板滑块范围见 formatting-controls.tsx) | 页面边距 |
| Section Spacing | 1–5 | 3 | 章节间距(映射 0.375–1.5rem) |
| Item Spacing | 1–5 | 2 | 条目间距(映射 0.125–1rem) |
| Line Height | 1–5 | 3 | 行高(映射 1.15–1.55) |
| Base Font Size | 1–5 | 3 | 基准字号(映射 11–16px) |
| Header Scale | 1–5 | 3 | 姓名/章节标题缩放倍率(映射 1.5–2.5) |
| Header Font | serif/sans-serif/mono | serif | 标题字体族 |
| Body Font | serif/sans-serif/mono | sans-serif | 正文字体族 |
| Compact Mode | boolean | false | 间距乘以 0.6(边距不变) |
| Contact Icons | boolean | false | 联系方式旁显示图标 |
| Accent Color | blue/green/orange/red | blue | modern / modern-two-column / vivid 的强调色 |
其中AccentColor的具体色值定义在 ACCENT_COLOR_MAP:blue(#1D4ED8/#DBEAFE)、green(#15803D/#DCFCE7)、orange(#EA580C/#FED7AA)、red(#DC2626/#FEE2E2)。如果你的模板想支持强调色控件,直接引用var(--resume-accent-primary)/var(--resume-accent-light)即可。
两个细节对新增模板尤其重要:
- Compact Mode 只压缩间距:
COMPACT_MULTIPLIER = 0.6只作用于--section-gap/--item-gap,边距保持字面值;行高则使用更温和的COMPACT_LINE_HEIGHT_MULTIPLIER = 0.92,避免文字重叠(见 template-settings.ts); - "Effective Output" 摘要:格式化面板底部会实时显示间距/行高/字号的最终生效值(考虑 compact 调整),方便用户核对(formatting-controls.tsx)。
PDF 渲染链路:新增模板的最后一公里
实时预览通过 CSS 变量即时生效,而 PDF 生成走的是完全不同的链路——后端 Playwright 无头渲染。理解这条链路,才能确保新模板在"下载 PDF"时表现正常:
GET /resumes/{id}/pdf ├── 后端拼接打印 URL:{FRONTEND_BASE_URL}/print/resumes/{id}?template=...&pageSize=...&margins=... ├── Playwright 启动 headless Chrome ├── 等待 .resume-print 选择器出现(见 apps/backend/app/pdf.py) ├── 等待 document.fonts.ready ├── 以零边距 + print_background=true 生成 PDF └── 返回 PDF 字节要点如下:
- 根类名是契约:pdf.py 中
render_resume_pdf默认等待.resume-print选择器(pdf.py),这就是为什么新模板根容器必须保留className="resume-print"; - 参数经由 Query 传递:resumes.py 的 PDF 端点把
template、pageSize、marginTop/Bottom/Left/Right(5–25)、sectionSpacing、itemSpacing、lineHeight、fontSize、headerScale、headerFont、bodyFont、compactMode、showContactIcons、accentColor全部拼进打印 URL。新增模板只要模板 ID 属于TemplateType,就能直接通过template=参数被选中; - 边距由 Playwright 负责:打印页 print/resumes/[id]/page.tsx 会把 CSS 边距清零(
margins: {top:0, ...}),注释明确说明"边距由 Playwright 渲染器应用,确保每一页都有边距,而非仅第一页"——前端只用 CSS 变量控制间距与字体; - 打印白名单 CSS:在
globals.css的@media print中,.resume-print及其子元素必须处于可见状态,否则 PDF 会空白(详见 docs/agent/design/pdf-template-guide.md 中的关键 CSS 规则)。
测试与验收清单
指南最后给出了一份手工验收清单,结合仓库现状可扩展为如下完整验证步骤:
- 模板注册测试:运行 template-registration.test.ts,确认新模板 ID 已包含在
TEMPLATE_OPTIONS中、ID 唯一、名称与描述非空;若为单字体族模板,还应验证applyTemplatePreset正确注入签名字体; - 构建器加载:打开 Builder,确认新模板出现在选择器中,缩略图正常显示,切换后实时预览立即变化;
- 全章节渲染:用一份包含 Summary、Experience、Projects、Education、Additional(技能/语言/证书/奖项)及自定义章节的完整数据测试,确认所有章节按
sectionMeta的顺序与可见性渲染; - PDF 生成:触发
GET /resumes/{id}/pdf,确认新模板的 PDF 正常产出、字体就绪、分页合理; - 多页内容压测:用超过两页的内容验证
.resume-item不跨页断裂、.resume-section-title不孤立在页尾; - 设置联动:在格式化面板调整边距、间距、字号、字体族、Compact Mode 与强调色(如适用),确认新模板全部响应。
常见坑位速查
- PDF 空白:多半是打印白名单 CSS 缺失,或根容器类名不是
.resume-print; - 章节顺序错乱:忘记使用
getSortedSections,而是硬编码了章节顺序; - 控件不生效:组件内样式没有引用
var(--section-gap)等 CSS 变量,或没有复用baseStyles; - 自定义章节丢失:模板未处理
customSections,可参考DynamicResumeSectionVivid的实现; - 富文本 XSS 风险:条目描述直接用
dangerouslySetInnerHTML而未走SafeHtml。
按照上述四步流程(创建组件 → 统一导出 → 登记元数据 → 注册选择器与缩略图),再结合模板设置、CSS 变量与 Playwright 渲染链路的理解,你就可以在 Resume-Matcher 中稳定地接入任意风格的全新简历模板,并让它同时服务于实时预览、多语言打印页与 PDF 下载三个场景。
【免费下载链接】Resume-MatcherThe #1 AI Harness for Building Resumes, PDFs, Cover Letters & more, locally with 100+ LLMs support.项目地址: https://gitcode.com/GitHub_Trending/re/Resume-Matcher
创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考