我花了 3 个月,用 Vue3 + AI 写了一个能「帮你改简历」的开源项目
一个大学生,一份焦虑,和一个让 AI 替你改简历的副业级项目。
你有多久没更新简历了?
年初的时候,我的一个学长投了 47 家公司,只收到了 3 个面试。
他简历其实不差——绩点 3.8,两段实习,还有一个拿过奖的项目。但他把简历发给我看的那天,我发现问题根本不在内容上:
- 项目经历杂乱无章,面试官根本抓不住重点
- 工作成果全是「负责了 XX 项目」,没有一句量化
- 自我评价里写满了「吃苦耐劳」「性格开朗」
他不是能力不行,他是不会「卖」自己。
这件事让我想起一个更扎心的数据:HR 平均 6 秒扫完一份简历。6 秒内没亮点?直接滑走。
于是我决定:写一个 AI 驱动的简历工具,让写简历这件事从「碰运气」变成「有标准」。
这就是 AI Resume —— 一个基于 Vue3 + TypeScript + AI 的全栈简历生成系统。
线上地址:ai-jl.top/home
一、它是干什么的?
一句话:帮你从「不知道怎么写」到「写出一份能被 HR 多看一眼的简历」。
整个流程拆成三层:
| 阶段 | 功能 | AI 角色 |
|---|---|---|
| 生成 | 输入岗位 + 个人信息,AI 直接生成完整简历 | AI 是你的「代写助手」 |
| 润色 | 选中任意段落,AI 改写为更专业、更有数据感的表达 | AI 是你的「文字编辑」 |
| 分析 | 多维度打分 + 雷达图 + 市场竞争力分析 | AI 是你的「HR 面试官」 |
说人话就是——你可以完全不写一个字,让 AI 先吐一份初稿;然后针对每个模块精修;最后让 AI 帮你做一次「模拟 HR 审阅」,告诉你哪里还不行。
二、技术栈一览(这可能是你想要的轮子)
整个项目前后端分离,前端是 Vue3 SPA,后端是 NestJS + MongoDB。具体技术选型:
框架: Vue 3.5 + Composition API + <script setup>
语言: TypeScript 5.9 (strict 模式)
构建: Vite 7
UI 库: Ant Design Vue 4 + Tailwind CSS 3
状态: Pinia 3 + 持久化插件
路由: Vue Router 4 (history 模式 + 认证守卫)
编辑器: WangEditor 5 (富文本)
图表: ECharts 6 + vue-echarts
动画: GSAP 3 (路由过渡动画)
导出: html2canvas + 自定义 A4 分页算法
安全: DOMPurify (XSS 防护)
测试: Playwright (E2E)
后端: NestJS + MongoDB (独立仓库)
选型理由说三点:
- Vue3.5 + Vite7:最新的工具链,开发体验极好,HMR 秒级
- Pinia 替代 Vuex:类型推断完美,配合 persist 插件做 token 持久化毫无痛点
- 自定义分页算法而非 jsPDF:服务端渲染 PDF 比客户端方案更稳定,中文排版也不乱码
三、几个我觉得「做得不错」的设计
3.1 模块驱动的编辑器架构
简历编辑器不是一张大白纸让你随便写。我把简历拆成了 11 个独立模块:
基本信息 | 求职意向 | 教育背景 | 工作经历 | 项目经历 | 校园经历 | 实习经历 | 技能特长 | 证书荣誉 | 自我评价 | 自定义模块
每个模块 = 一个 Form 组件(编辑表单)+ 一个 Section 组件(预览渲染),通过 store 中的 moduleOrder 数组统一管理。
这样做的好处是:
- 新增一个简历模块,只需要加两个组件 + 注册到数组,不改任何核心逻辑
- 模块拖拽排序时,交换的是
globalSort值,数据流完全可控 - 「基本信息」和「求职意向」标记为
FIXED_MODULES,永远置顶,不会被误拖
// stores/resumeStore.ts 中的核心设计
const DEFAULT_MODULE_ORDER: ModuleItem[] = [
{ moduleKey: "basicInfo", label: "基本信息", ... },
{ moduleKey: "jobIntention", label: "求职意向", ... },
// ... 其他 9 个模块
];
const FIXED_MODULES = ["basicInfo", "jobIntention"] as const;
这种「注册表 + 动态组件」的模式,在需要高度可配置的编辑器类项目里非常实用。
3.2 双层级排序系统
简历模块有两种排序需求:
- 模块级:教育经历放工作经历上面还是下面?
- 条目级:三段工作经历谁先谁后?
方案是 globalSort + localSort 双层排序:
模块 A (globalSort: 1)
├── 条目 1 (localSort: 0)
├── 条目 2 (localSort: 1)
└── 条目 3 (localSort: 2)
模块 B (globalSort: 2)
├── 条目 1 (localSort: 0)
└── 条目 2 (localSort: 1)
拖拽时只交换相邻两个元素的 sort 值,不动整个数组。这样不管你简历有多少个模块、每个模块有多少条经历,排序性能都是 O(1)。
3.3 智能 A4 分页算法
PDF 导出最头疼的就是分页——你不知道哪里会断页,断在某个标题上就尴尬了。
我的方案是用 ResizeObserver + MutationObserver 监控预览区的 DOM 变化,以固定 A4 高度 1122px 为基准,计算每一页从哪里断开:
const A4_HEIGHT = 1122; // A4 纸在屏幕上的等效高度
// 使用 useDebounceFn 防抖,内容变了 100ms 后再重新算分页
const { markers } = usePageMarkers({
firstPageHeight: 1050, // 首页稍矮(留给标题区)
pageHeight: A4_HEIGHT,
debounceMs: 100,
});
然后渲染时在对应位置插入 page-break 标记。效果是——无论你怎么改内容、调间距,分页标记都会自动重新计算,预览就是最终效果。
3.4 AI 分析系统的数据模型
我觉得这是整个项目里最「重」的一个模块。
AI 分析不只是给一个分数,而是返回一整套结构化数据:
interface AnalysisResultData {
overall_score: number; // 总分 (环形图)
competitiveness_level: string; // 竞争力评级 (A/B/C/D)
dimension_scores: Dimension[]; // 多维度分数 (雷达图)
strengths: AnalysisItem[]; // 优势 (带标签)
weaknesses: AnalysisItem[]; // 劣势 (带严重度)
suggestions: Suggestion[]; // 建议 (优先级 + 时间线)
market_analysis?: Market; // 市场分析 (需求/薪资/竞争)
technology_assessment?: Tech; // 技术评估 (技能匹配/趋势/风险)
career_analysis?: Career; // 职业分析 (阶段/轨迹/增长率)
}
前端用 12 个专用组件 渲染这些数据,包括 ECharts 雷达图、环形分数图、竞争力徽章、市场分析卡片等。
这样做的目的是:不让 AI 分析变成一个「黑盒打分」,而是给出具体的、可操作的改进方向。
四、Gem in the Code——几个「小但关键」的设计
4.1 DOMPurify XSS 防护
简历编辑器里允许用户粘贴 HTML 内容(富文本编辑器),这是 XSS 攻击的经典入口。我写了一个 v-safe-html 自定义指令,在渲染前统一走 DOMPurify 清洗:
export const safeHtml: Directive<HTMLElement, string> = {
mounted(el, binding) {
el.innerHTML = DOMPurify.sanitize(binding.value ?? "");
},
updated(el, binding) {
if (binding.value !== binding.oldValue) {
el.innerHTML = DOMPurify.sanitize(binding.value ?? "");
}
},
};
全局注册后,任何需要渲染用户输入 HTML 的地方,只需要 v-safe-html="content"。不需要每个组件里写 dompurify import。
4.2 GSAP 路由过渡动画
路由切换的时候,页面「突然出现」体验很差。我用 GSAP 写了一个声明式的动画系统:
5 种预设:fade | fade-slide-up | fade-slide-left | scale-in | none
路由里这样用:
// router/index.ts
{ path: "/editor", meta: { animation: "scale-in" } }
{ path: "/templates", meta: { animation: "fade-slide-up" } }
然后一个 <GSAPTransition> 组件自动读取 route.meta.animation,驱动 GSAP 的 fromTo 动画。还自动检测了 prefers-reduced-motion: reduce,尊重用户的系统无障碍设置。
4.3 Axios 拦截器的异步循环依赖处理
常见做法是直接在拦截器里 import authStore,但 TypeScript strict 模式下容易循环依赖。我的方案是用动态 import:
// 响应拦截器
if (error.response?.status === 401) {
const { useAuthStore } = await import("@/stores/auth");
const authStore = useAuthStore();
authStore.logout();
router.push("/auth/login");
}
这样既解决了循环依赖,又不影响类型检查。
4.4 Playwright E2E 测试
3 个测试套件覆盖了核心流程:
- 未登录流程:首页加载、模板列表浏览、路由守卫跳转、404 页面
- 认证流程:登录/注册表单交互、OAuth 回调
- 简历制作流程:我的简历列表、编辑器加载、模块编辑
这些测试不花哨,但覆盖了用户最可能遇到 Bug 的场景。CI 里配了失败自动截图,出问题直接看图定位。
五、在线体验 & 开源
线上地址:ai-jl.top/home
本地跑起来只需要三步:
# 1. 克隆 + 安装
git clone https://github.com/dhj-l/resume.git
cd resume
pnpm install
# 2. 配置环境变量
cp .env.example .env
# 编辑 .env,填入后端 API 地址
# 3. 启动
pnpm dev
后端需要 NestJS + MongoDB,具体见 后端仓库 README。
六、还有什么没做的?
写在 README 的 Roadmap 里了,欢迎大家一起来搞:
- 更多简历模板(目前 7 套,计划补充到 15+)
- 一键投递功能(对接主流招聘平台)
- 简历版本管理 & 对比
- 国际化(英文简历支持)
- 移动端适配
写在最后
说实话,这个项目不是什么惊天动地的创新。
它解决的问题很朴素:大多数人不会写简历,而 AI 恰好擅长「把零散经历组织成结构化文本」。
但我觉得这恰恰是最值得做的事情——用技术解决一个真实的、高频的痛点。
如果你是:
- 正在找实习 / 工作的同学 → 直接去 ai-jl.top 用起来
- 想学习 Vue3 + TypeScript 中大型项目实践 → 代码全开源,架构清晰,随便翻
- 觉得项目有意思 → GitHub Star 是对我最大的鼓励 ⭐
如果你在用的时候发现问题,或者有什么想法——去 GitHub 提 Issue,我一般在 24 小时内回复。
GitHub: github.com/dhj-l/resum…
如果这个项目帮到了你,给个 Star 呗,这比什么鼓励都实在。