投了47家只拿了3个面试,我写了个AI简历工具,顺便开源了

27 阅读8分钟

我花了 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 (独立仓库)

选型理由说三点:

  1. Vue3.5 + Vite7:最新的工具链,开发体验极好,HMR 秒级
  2. Pinia 替代 Vuex:类型推断完美,配合 persist 插件做 token 持久化毫无痛点
  3. 自定义分页算法而非 jsPDF:服务端渲染 PDF 比客户端方案更稳定,中文排版也不乱码

三、几个我觉得「做得不错」的设计

3.1 模块驱动的编辑器架构

简历编辑器不是一张大白纸让你随便写。我把简历拆成了 11 个独立模块

基本信息 | 求职意向 | 教育背景 | 工作经历 | 项目经历 | 校园经历 | 实习经历 | 技能特长 | 证书荣誉 | 自我评价 | 自定义模块

editor.png

每个模块 = 一个 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 雷达图、环形分数图、竞争力徽章、市场分析卡片等。

analysis.png

这样做的目的是:不让 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

前端仓库github.com/dhj-l/resum…

后端仓库github.com/dhj-l/ai-re…

本地跑起来只需要三步:

# 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 呗,这比什么鼓励都实在。